Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling tiff files with multiple tiled images #373

Merged
merged 1 commit into from
Oct 30, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions sources/tiff/large_image_source_tiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __init__(self, path, **kwargs):
if level < 0:
continue
# Store information for sorting with the directory.
alldir.append((td.tileWidth * td.tileHeight, level,
alldir.append((level > 0, td.tileWidth * td.tileHeight, level,
td.imageWidth * td.imageHeight, directoryNum, td))
# If there are no tiled images, raise an exception.
if not len(alldir):
Expand All @@ -125,9 +125,11 @@ def __init__(self, path, **kwargs):
# preferred image
for tdir in alldir:
td = tdir[-1]
level = tdir[1]
level = tdir[2]
if (td.tileWidth != highest.tileWidth or
td.tileHeight != highest.tileHeight):
if not len(self._associatedImages):
self._addAssociatedImage(largeImagePath, tdir[-2], True, highest)
continue
# If a layer's image is not a multiple of the tile size, it should
# be near a power of two of the highest resolution image.
Expand All @@ -151,19 +153,29 @@ def __init__(self, path, **kwargs):
self.sizeX = highest.imageWidth
self.sizeY = highest.imageHeight

def _addAssociatedImage(self, largeImagePath, directoryNum):
def _addAssociatedImage(self, largeImagePath, directoryNum, mustBeTiled=False, topImage=None):
"""
Check if the specified TIFF directory contains a non-tiled image with a
sensible image description that can be used as an ID. If so, and if
the image isn't too large, add this image as an associated image.
Check if the specified TIFF directory contains an image with a sensible
image description that can be used as an ID. If so, and if the image
isn't too large, add this image as an associated image.

:param largeImagePath: path to the TIFF file.
:param directoryNum: libtiff directory number of the image.
:param mustBeTiles: if true, use tiled images. If false, require
untiled images.
:param topImage: if specified, add image-embedded metadata to this
image.
"""
try:
associated = TiledTiffDirectory(largeImagePath, directoryNum, False)
id = associated._tiffInfo.get(
'imagedescription').strip().split(None, 1)[0].lower()
associated = TiledTiffDirectory(largeImagePath, directoryNum, mustBeTiled)
id = ''
if associated._tiffInfo.get('imagedescription'):
id = associated._tiffInfo.get(
'imagedescription').strip().split(None, 1)[0].lower()
elif mustBeTiled:
id = 'dir%d' % directoryNum
if not len(self._associatedImages):
id = 'macro'
if not isinstance(id, six.text_type):
id = id.decode('utf8')
# Only use this as an associated image if the parsed id is
Expand All @@ -172,7 +184,14 @@ def _addAssociatedImage(self, largeImagePath, directoryNum):
if (id.isalnum() and len(id) > 3 and len(id) <= 20 and
associated._pixelInfo['width'] <= 8192 and
associated._pixelInfo['height'] <= 8192):
self._associatedImages[id] = associated._tiffFile.read_image()
image = associated._tiffFile.read_image()
# Optrascan scanners are store xml image descriptions in a
# "tiled image". Check if this is the case, and, if so, parse
# such data
if image.tobytes()[:6] == b'<?xml ':
self._parseImageXml(image.tobytes().rsplit(b'>', 1)[0] + b'>', topImage)
return
self._associatedImages[id] = image
except (TiffException, AttributeError):
# If we can't validate or read an associated image or it has no
# useful imagedescription, fail quietly without adding an
Expand All @@ -184,6 +203,34 @@ def _addAssociatedImage(self, largeImagePath, directoryNum):
config.getConfig('logger').exception(
'Could not use non-tiled TIFF image as an associated image.')

def _parseImageXml(self, xml, topImage):
"""
Parse metadata stored in arbitrary xml and associate it with a specific
image.

:params xml: the xml as a string or bytes object.
:params topImage: the image to add metadata to.
"""
if not topImage or topImage.pixelInfo.get('magnificaiton'):
return
topImage.parse_image_description(xml)
if not topImage._description_xml:
return
try:
xml = topImage._description_xml
# Optrascan metadata
scanDetails = xml.get('ScanInfo', xml.get('EncodeInfo'))['ScanDetails']
mag = float(scanDetails['Magnification'])
# In microns; convert to mm
scale = float(scanDetails['PixelResolution']) * 1e-3
topImage._pixelInfo = {
'magnification': mag,
'mm_x': scale,
'mm_y': scale,
}
except Exception:
pass

def getNativeMagnification(self):
"""
Get the magnification at a particular level.
Expand Down