- First real commit.
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
# tiffexplore
|
||||
# TIFF Explorer
|
||||
Explore tiff structures, tags, and frames in a GUI.
|
||||
|
||||
6
bin/tiffexplore
Normal file
6
bin/tiffexplore
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from tiffexplore import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
23
setup.py
Normal file
23
setup.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import setuptools
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="tiffexplore",
|
||||
packages=["tiffexplore"],
|
||||
version="2021.06.0",
|
||||
author="Wim Pomp",
|
||||
author_email="wimpomp@gmail.com",
|
||||
description="Explore a tiff structure.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires='>=3.7',
|
||||
install_requires=['numpy', 'tifffile', 'PyQt5'],
|
||||
scripts=['bin/tiffexplore'],
|
||||
)
|
||||
246
tiffexplore/__init__.py
Normal file
246
tiffexplore/__init__.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
from os.path import isfile, basename
|
||||
from sys import argv
|
||||
|
||||
if __package__ is None:
|
||||
import tiffread
|
||||
else:
|
||||
from . import tiffread
|
||||
|
||||
|
||||
class UiMainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
MainWindow.resize(800, 600)
|
||||
self.centralwidget = QtWidgets.QWidget(MainWindow)
|
||||
self.centrallayout = QtWidgets.QHBoxLayout(self.centralwidget)
|
||||
self.scrollArea = QtWidgets.QScrollArea(self.centralwidget)
|
||||
self.scrollArea.setFixedWidth(150)
|
||||
self.scrollArea.setMinimumHeight(200)
|
||||
self.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.leftcolumnWidget = QtWidgets.QWidget()
|
||||
self.leftcolumnWidget.setFixedWidth(130)
|
||||
self.leftcolumnWidget.setMinimumHeight(200)
|
||||
self.verticalLayoutWidget = QtWidgets.QWidget(self.leftcolumnWidget)
|
||||
self.verticalLayoutWidget.setFixedWidth(130)
|
||||
self.verticalLayoutWidget.setMinimumHeight(200)
|
||||
self.leftcolumn = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
|
||||
self.leftcolumn.setContentsMargins(0, 0, 0, 0)
|
||||
self.scrollArea.setWidget(self.leftcolumnWidget)
|
||||
self.middlecolumnWidget = QtWidgets.QWidget()
|
||||
self.middlecolumn = QtWidgets.QVBoxLayout(self.middlecolumnWidget)
|
||||
self.middlecolumn.setContentsMargins(0, 0, 0, 0)
|
||||
self.properties = QtWidgets.QTextEdit(self.centralwidget)
|
||||
self.middlecolumn.addWidget(self.properties)
|
||||
self.rightcolumnWidget = QtWidgets.QWidget()
|
||||
self.rightcolumn = QtWidgets.QVBoxLayout(self.rightcolumnWidget)
|
||||
self.rightcolumn.setContentsMargins(0, 0, 0, 0)
|
||||
self.binary = QtWidgets.QTextEdit(self.rightcolumnWidget)
|
||||
self.rightcolumn.addWidget(self.binary)
|
||||
self.image = QtWidgets.QLabel(self.rightcolumnWidget)
|
||||
self.image.setMinimumWidth(200)
|
||||
self.image.setMinimumHeight(200)
|
||||
self.rightcolumn.addWidget(self.image)
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QtWidgets.QMenuBar(MainWindow)
|
||||
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22))
|
||||
self.menubar.setObjectName("menubar")
|
||||
self.menuFile = QtWidgets.QMenu(self.menubar)
|
||||
self.menuFile.setObjectName("menuFile")
|
||||
MainWindow.setMenuBar(self.menubar)
|
||||
self.statusbar = QtWidgets.QStatusBar(MainWindow)
|
||||
self.statusbar.setObjectName("statusbar")
|
||||
MainWindow.setStatusBar(self.statusbar)
|
||||
self.actionOpen = QtWidgets.QAction(MainWindow)
|
||||
self.actionOpen.setObjectName("actionOpen")
|
||||
self.menuFile.addAction(self.actionOpen)
|
||||
self.menubar.addAction(self.menuFile.menuAction())
|
||||
self.centrallayout.addWidget(self.scrollArea)
|
||||
self.centrallayout.addWidget(self.middlecolumnWidget)
|
||||
self.centrallayout.addWidget(self.rightcolumnWidget)
|
||||
self.centralwidget.setLayout(self.centrallayout)
|
||||
self.retranslateUi(MainWindow)
|
||||
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
MainWindow.setWindowTitle(_translate("MainWindow", "TiffExp"))
|
||||
self.menuFile.setTitle(_translate("MainWindow", "File"))
|
||||
self.actionOpen.setText(_translate("MainWindow", "Open"))
|
||||
self.actionOpen.setShortcut(_translate("MainWindow", "Ctrl+O"))
|
||||
|
||||
|
||||
class PaintBox(QtWidgets.QWidget):
|
||||
def drawText(self, qp, rect, text):
|
||||
qp.setPen(QtGui.QColor('black'))
|
||||
qp.setFont(QtGui.QFont('Decorative', 10 if rect[3] > 20 else rect[3] // 2))
|
||||
qp.drawText(QtCore.QRect(*rect), QtCore.Qt.AlignCenter, text)
|
||||
|
||||
def drawRectangle(self, qp, rect, color):
|
||||
color = QtGui.QColor(color) if isinstance(color, str) else QtGui.QColor(*color)
|
||||
qp.setPen(QtGui.QColor('gray'))
|
||||
qp.setBrush(color)
|
||||
qp.drawRect(*rect)
|
||||
|
||||
class Legend(PaintBox):
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.color = {'header': 'red', 'ifd': 'cyan', 'tagdata': 'lightgreen', 'image': 'yellow', 'empty': 'white',
|
||||
'shared tagdata': 'green', 'shared image': 'orange', 'unknown': 'gray'}
|
||||
super().__init__()
|
||||
self.setFixedHeight(120)
|
||||
self.show()
|
||||
|
||||
def paintEvent(self, *args, **kwargs):
|
||||
qp = QtGui.QPainter()
|
||||
qp.begin(self)
|
||||
for i, (key, value) in enumerate(self.color.items()):
|
||||
self.drawRectangle(qp, (0, 15*i, 125, 15), value)
|
||||
self.drawText(qp, (0, 15*i, 125, 15), key)
|
||||
qp.end()
|
||||
|
||||
|
||||
class Bar(PaintBox):
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.color = {'header': 'red', 'ifd': 'cyan', 'tagdata': 'lightgreen', 'image': 'yellow', 'empty': 'white',
|
||||
'HEADER': 'red', 'IFD': 'blue', 'TAGDATA': 'green', 'IMAGE': 'orange', 'EMPTY': 'white'}
|
||||
self.tiff = None
|
||||
self.bar = tiffread.assignments()
|
||||
super().__init__()
|
||||
self.setFixedWidth(150)
|
||||
self.show()
|
||||
|
||||
def new_file(self):
|
||||
self.tiff = self.parent.tiff
|
||||
self.bar = tiffread.assignments() if self.tiff is None else self.get_bar()
|
||||
self.parent.leftcolumnWidget.setFixedHeight(self.bar.max_addr)
|
||||
self.parent.verticalLayoutWidget.setFixedHeight(self.bar.max_addr)
|
||||
|
||||
def paintEvent(self, *args, **kwargs):
|
||||
qp = QtGui.QPainter()
|
||||
qp.begin(self)
|
||||
self.parent.leftcolumnWidget.setFixedHeight(self.bar.max_addr)
|
||||
self.parent.verticalLayoutWidget.setFixedHeight(self.bar.max_addr)
|
||||
for key, value in self.bar.items():
|
||||
self.drawRectangle(qp, (0, value[0], 125, value[1]), self.color.get(key[0], "gray"))
|
||||
self.drawText(qp, (0, value[0], 125, value[1]), ('_'.join(('{}',) * len(key))).format(*key).lower())
|
||||
qp.end()
|
||||
|
||||
def get_bar(self):
|
||||
min_size = 10
|
||||
scale = 100
|
||||
bar = tiffread.assignments()
|
||||
pos = 0
|
||||
for item in self.tiff.addresses.get_assignments():
|
||||
key, value = item[0]
|
||||
size = value[1] // scale if value[1] // scale >= min_size else min_size
|
||||
if not (key[0].lower() == 'empty' and value[1] == 1):
|
||||
if key[0].lower() == 'empty':
|
||||
bar[('empty', value[0] + value[1] // 2)] = (pos, size)
|
||||
else:
|
||||
if len(item) > 1:
|
||||
bar[(key[0].upper(),) + key[1:]] = (pos, size)
|
||||
else:
|
||||
bar[key] = (pos, size)
|
||||
pos += size
|
||||
|
||||
bar.max_addr = pos
|
||||
return bar
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
keys, vals = zip(*self.bar.get_assignment(event.localPos().y()))
|
||||
key, val = keys[0], vals[0]
|
||||
if key[0].lower() == 'empty':
|
||||
addr = key[1]
|
||||
else:
|
||||
addr = self.tiff.addresses[(key[0].lower(),) + key[1:]]
|
||||
addr = addr[0] + addr[1] // 2
|
||||
keys, addrs = zip(*self.parent.tiff.addresses.get_assignment(addr))
|
||||
addr = addrs[0]
|
||||
|
||||
text = [('_'.join(('{}',) * len(key))).format(*key) for key in keys]
|
||||
text.append('')
|
||||
text.append(f'Adresses: {addr[0]} - {sum(addr)}')
|
||||
text.append(f'Length: {addr[1]}')
|
||||
text.append('')
|
||||
if key[0].lower() == 'header':
|
||||
text.append(f'File size: {len(self.tiff)}')
|
||||
text.append(f'Byte order: {self.tiff.byteorder}')
|
||||
text.append(f'Big tiff: {self.tiff.bigtiff}')
|
||||
text.append(f'Tag size: {self.tiff.tagsize}')
|
||||
text.append(f'Tag number format: {self.tiff.tagnoformat}')
|
||||
text.append(f'Offset size: {self.tiff.offsetsize}')
|
||||
text.append(f'Offset format: {self.tiff.offsetformat}')
|
||||
text.append(f'First ifd offset: {self.tiff.offset}')
|
||||
if key[0].lower() == 'ifd':
|
||||
text.extend([f'{k}: {v}' for k, v in self.tiff.tags[key[1]].items()])
|
||||
if key[1] < self.tiff.nifds-1:
|
||||
text.append(f'Next ifd offset: {self.tiff.addresses[("ifd", key[1] + 1)][0]}')
|
||||
if key[0].lower() == 'tagdata':
|
||||
text.append(f'{key[2]}: {self.tiff.tags[key[1]][key[2]]}')
|
||||
if key[0].lower() == 'image':
|
||||
self.parent.setImage(key[1], key[2])
|
||||
else:
|
||||
self.parent.setImage()
|
||||
self.parent.properties.setText('\n'.join(text))
|
||||
self.parent.binary.setText(''.join([chr(i) for i in self.tiff[addr[0]:addr[0]+addr[1]]]))
|
||||
|
||||
|
||||
class App(QtWidgets.QMainWindow, UiMainWindow):
|
||||
def __init__(self, tiff=None):
|
||||
super().__init__()
|
||||
self.tiff = None
|
||||
self.setupUi(self)
|
||||
self.bar = Bar(self)
|
||||
self.leftcolumn.addWidget(self.bar)
|
||||
self.legend = Legend(self)
|
||||
self.middlecolumn.addWidget(self.legend)
|
||||
self.actionOpen.triggered.connect(self.openDialog)
|
||||
self.open(tiff)
|
||||
self.show()
|
||||
|
||||
def setImage(self, *args):
|
||||
if len(args):
|
||||
im = self.tiff.asarray(*args)
|
||||
if im.ndim == 3:
|
||||
im = im[0]
|
||||
if im.max() - im.min() > 0:
|
||||
im = (255 * ((im - im.min()) / (im.max() - im.min()))).astype('uint8')
|
||||
shape = im.shape
|
||||
im = QtGui.QImage(im, im.shape[1], im.shape[0], QtGui.QImage.Format_Grayscale8)
|
||||
f = min([a / b for a, b in zip((self.image.height(), self.image.width()), shape)])
|
||||
pix = QtGui.QPixmap(im).scaled(f * shape[1], f * shape[0])
|
||||
else:
|
||||
pix = QtGui.QPixmap()
|
||||
self.image.setPixmap(pix)
|
||||
|
||||
def openDialog(self):
|
||||
file, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Open config file", "", "TIFF Files (*.tif *.tiff);;All Files (*)",
|
||||
options=(QtWidgets.QFileDialog.Options() | QtWidgets.QFileDialog.DontUseNativeDialog))
|
||||
self.open(file)
|
||||
|
||||
def open(self, file):
|
||||
if file is not None and isfile(file):
|
||||
if self.tiff is not None:
|
||||
self.tiff.close()
|
||||
self.tiff = tiffread.tiff(file)
|
||||
self.bar.new_file()
|
||||
self.setWindowTitle(f'TiffExp: {basename(self.tiff.file)}')
|
||||
|
||||
def closeEvent(self, *args, **kwargs):
|
||||
if self.tiff is not None:
|
||||
self.tiff.close()
|
||||
|
||||
|
||||
def main():
|
||||
app = QtWidgets.QApplication([])
|
||||
w = App(argv[1]) if len(argv) > 1 else App()
|
||||
exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
158
tiffexplore/tiffread.py
Normal file
158
tiffexplore/tiffread.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import struct
|
||||
import tifffile
|
||||
import numpy as np
|
||||
|
||||
|
||||
class tiff():
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
self.fh = open(file, 'rb')
|
||||
self.tiff = tifffile.TiffFile(self.file)
|
||||
self.tags = []
|
||||
self.get_file_len()
|
||||
self.addresses = assignments(len(self))
|
||||
nifd = self.read_header()
|
||||
i = 0
|
||||
while nifd:
|
||||
nifd = self.read_ifd(nifd, i)
|
||||
i += 1
|
||||
self.nifds = i
|
||||
for i, tags in enumerate(self.tags):
|
||||
if 273 in tags and 279 in tags:
|
||||
for j, a in enumerate(zip(tags[273][-1], tags[279][-1])):
|
||||
self.addresses[('image', i, j)] = a
|
||||
|
||||
def get_file_len(self):
|
||||
self.fh.seek(0, 2)
|
||||
self.len = self.fh.tell()
|
||||
|
||||
def __len__(self):
|
||||
return self.len
|
||||
|
||||
def asarray(self, page, segment):
|
||||
return [d for d in zip(self.tiff.pages[page].segments(), range(segment + 1))][-1][0][0].squeeze()
|
||||
|
||||
def read_header(self):
|
||||
self.fh.seek(0)
|
||||
self.byteorder = {b'II': '<', b'MM': '>'}[self.fh.read(2)]
|
||||
self.bigtiff = {42: False, 43: True}[struct.unpack(self.byteorder + 'H', self.fh.read(2))[0]]
|
||||
if self.bigtiff:
|
||||
self.tagsize = 20
|
||||
self.tagnoformat = 'Q'
|
||||
self.offsetsize = struct.unpack(self.byteorder + 'H', self.fh.read(2))[0]
|
||||
self.offsetformat = {8: 'Q', 16: '2Q'}[self.offsetsize]
|
||||
assert struct.unpack(self.byteorder + 'H', self.fh.read(2))[0] == 0, 'Not a TIFF-file'
|
||||
self.offset = struct.unpack(self.byteorder + self.offsetformat, self.fh.read(self.offsetsize))[0]
|
||||
else:
|
||||
self.tagsize = 12
|
||||
self.tagnoformat = 'H'
|
||||
self.offsetformat = 'I'
|
||||
self.offsetsize = 4
|
||||
self.offset = struct.unpack(self.byteorder + self.offsetformat, self.fh.read(self.offsetsize))[0]
|
||||
self.addresses[('header',)] = (0, 4 + self.bigtiff * 4 + self.offsetsize)
|
||||
return self.offset
|
||||
|
||||
def read_ifd(self, offset, idx):
|
||||
""" Reads an IFD of the tiff file
|
||||
wp@tl20200214
|
||||
"""
|
||||
self.fh.seek(offset)
|
||||
nTags = struct.unpack(self.byteorder + self.tagnoformat, self.fh.read(struct.calcsize(self.tagnoformat)))[0]
|
||||
# print('Found {} tags'.format(nTags))
|
||||
assert nTags < 4096, 'Too many tags'
|
||||
|
||||
length = 8 if self.bigtiff else 2
|
||||
length += nTags * self.tagsize + self.offsetsize
|
||||
|
||||
tags = {}
|
||||
for i in range(nTags):
|
||||
pos = offset + struct.calcsize(self.tagnoformat) + self.tagsize * i
|
||||
self.fh.seek(pos)
|
||||
|
||||
code, ttype = struct.unpack(self.byteorder + 'HH', self.fh.read(4))
|
||||
count = struct.unpack(self.byteorder + self.offsetformat, self.fh.read(self.offsetsize))[0]
|
||||
dtype = tifffile.TIFF.DATA_FORMATS[ttype]
|
||||
dtypelen = struct.calcsize(dtype)
|
||||
|
||||
toolong = struct.calcsize(dtype) * count > self.offsetsize
|
||||
if toolong:
|
||||
caddr = struct.unpack(self.byteorder + self.offsetformat, self.fh.read(self.offsetsize))[0]
|
||||
self.addresses[('tagdata', idx, code)] = (caddr, dtypelen * count)
|
||||
cp = self.fh.tell()
|
||||
self.fh.seek(caddr)
|
||||
else:
|
||||
caddr = self.fh.tell()
|
||||
|
||||
if ttype == 1:
|
||||
value = self.fh.read(count)
|
||||
elif ttype == 2:
|
||||
value = self.fh.read(count).decode('ascii').rstrip('\x00')
|
||||
elif ttype == 5:
|
||||
value = [struct.unpack(self.byteorder + dtype, self.fh.read(dtypelen)) for _ in range(count)]
|
||||
else:
|
||||
value = [struct.unpack(self.byteorder + dtype, self.fh.read(dtypelen))[0] for _ in range(count)]
|
||||
|
||||
if toolong:
|
||||
self.fh.seek(cp)
|
||||
|
||||
tags[code] = (ttype, caddr, dtypelen*count, value)
|
||||
|
||||
nifd = struct.unpack(self.byteorder + self.tagnoformat, self.fh.read(struct.calcsize(self.tagnoformat)))[0]
|
||||
self.fh.seek(offset)
|
||||
self.addresses[('ifd', idx)] = (offset, 2 * struct.calcsize(self.tagnoformat) + nTags * self.tagsize)
|
||||
self.tags.append(tags)
|
||||
return nifd
|
||||
|
||||
def __getitem__(self, item):
|
||||
if not isinstance(item, slice):
|
||||
item = slice(item, item+1)
|
||||
start = 0 if item.start is None or item.start < 0 else item.start
|
||||
step = item.step or 1
|
||||
stop = len(self) if item.stop is None or item.stop > len(self) else item.stop
|
||||
self.fh.seek(start)
|
||||
return self.fh.read(stop - start)[::step]
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.tiff.close()
|
||||
self.close()
|
||||
|
||||
def close(self, *args, **kwargs):
|
||||
self.fh.close()
|
||||
|
||||
def get_bytes(self, part):
|
||||
offset, length = self.addresses[part]
|
||||
self.fh.seek(offset)
|
||||
return self.fh.read(length)
|
||||
|
||||
|
||||
class assignments(dict):
|
||||
def __init__(self, max_addr=0, *args, **kwargs):
|
||||
self.max_addr = max_addr
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_assignment(self, addr):
|
||||
keys, values = zip(*self.items())
|
||||
offsets, lengths = zip(*values)
|
||||
offset_arr = np.array(offsets)
|
||||
offset_len = offset_arr + lengths
|
||||
nearest_addr = np.max(offset_arr[offset_arr <= addr])
|
||||
idxs = np.where(offset_arr == nearest_addr)[0]
|
||||
if addr < nearest_addr + lengths[idxs[0]]:
|
||||
return [(keys[idx], self[keys[idx]]) for idx in idxs]
|
||||
else:
|
||||
previous_addr = np.max(offset_len[offset_len <= addr])
|
||||
next_addr = offset_arr[offset_arr > addr]
|
||||
length = np.min(next_addr) - previous_addr if len(next_addr) else self.max_addr - previous_addr
|
||||
return [(('empty',), (previous_addr, length))]
|
||||
|
||||
def get_assignments(self, start=0, end=-1):
|
||||
addr = start
|
||||
if end == -1:
|
||||
end = self.max_addr
|
||||
while addr < end:
|
||||
item = self.get_assignment(addr)
|
||||
addr = sum(item[0][1])
|
||||
yield item
|
||||
Reference in New Issue
Block a user