- First real commit.

This commit is contained in:
Wim Pomp
2021-06-17 15:24:50 +02:00
parent b6e816e099
commit b91190ffd5
5 changed files with 434 additions and 1 deletions

View File

@@ -1,2 +1,2 @@
# tiffexplore
# TIFF Explorer
Explore tiff structures, tags, and frames in a GUI.

6
bin/tiffexplore Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
from tiffexplore import main
if __name__ == '__main__':
main()

23
setup.py Normal file
View 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
View 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
View 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