Skip to content

Commit

Permalink
Handle different tiff orientations.
Browse files Browse the repository at this point in the history
For a Tiled TIFF that is not in the standard top-left orientation, this
uses a less-efficient code path that may composite up to four tiles to
get a conceptual tile at the location desired.

This is a backport of #390.  Tests are done on the master branch.
  • Loading branch information
manthey committed Oct 30, 2019
1 parent ddccaf2 commit 7d0c89f
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ install:
- popd
- girder-install plugin --symlink $large_image_path
# Install all extras (since "girder-install plugin" does not provide a mechanism to specify them
- pip install glymur --find-links https://manthey.github.io/large_image_wheels
- pip install glymur --find-links https://girder.github.io/large_image_wheels
# Trusty supports gdal 1.10.0; don't test mapnik on Python 3 (for now)
- if [ -n "${PY3}" ]; then
pip install -e $large_image_path[memcached,openslide] ;
Expand Down
8 changes: 4 additions & 4 deletions server/tilesource/tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ def _addAssociatedImage(self, largeImagePath, directoryNum, mustBeTiled=False, t
associated._pixelInfo['width'] <= 8192 and
associated._pixelInfo['height'] <= 8192):
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
# Optrascan scanners 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
Expand Down Expand Up @@ -256,7 +256,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, sparseFallback=False,
else:
tile = self._tiffDirectories[z].getTile(x, y)
format = 'JPEG'
if PIL and isinstance(tile, PIL.Image.Image):
if isinstance(tile, PIL.Image.Image):
format = TILE_FORMAT_PIL
return self._outputTile(tile, format, x, y, z, pilImageAllowed,
**kwargs)
Expand Down
135 changes: 119 additions & 16 deletions server/tilesource/tiff_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import ctypes
import os
import six
import threading

from functools import partial
from xml.etree import cElementTree
Expand Down Expand Up @@ -126,6 +127,7 @@ def __init__(self, filePath, directoryNum, mustBeTiled=True):
self._mustBeTiled = mustBeTiled

self._tiffFile = None
self._tileLock = threading.RLock()

self._open(filePath, directoryNum)
self._loadMetadata()
Expand Down Expand Up @@ -228,9 +230,18 @@ def _validate(self): # noqa
'Only greyscale (black is 0), RGB, and YCbCr photometric '
'interpretation TIFF files are supported')

if self._tiffInfo.get('orientation') not in {None, libtiff_ctypes.ORIENTATION_TOPLEFT}:
if self._tiffInfo.get('orientation') not in {
libtiff_ctypes.ORIENTATION_TOPLEFT,
libtiff_ctypes.ORIENTATION_TOPRIGHT,
libtiff_ctypes.ORIENTATION_BOTRIGHT,
libtiff_ctypes.ORIENTATION_BOTLEFT,
libtiff_ctypes.ORIENTATION_LEFTTOP,
libtiff_ctypes.ORIENTATION_RIGHTTOP,
libtiff_ctypes.ORIENTATION_RIGHTBOT,
libtiff_ctypes.ORIENTATION_LEFTBOT,
None}:
raise ValidationTiffException(
'Only top-left orientation TIFF files are supported')
'Unsupported TIFF orientation')

if self._tiffInfo.get('compression') not in {
libtiff_ctypes.COMPRESSION_NONE,
Expand Down Expand Up @@ -281,6 +292,13 @@ def _loadMetadata(self):
self._tileHeight = info.get('tilelength')
self._imageWidth = info.get('imagewidth')
self._imageHeight = info.get('imagelength')
if info.get('orientation') in {
libtiff_ctypes.ORIENTATION_LEFTTOP,
libtiff_ctypes.ORIENTATION_RIGHTTOP,
libtiff_ctypes.ORIENTATION_RIGHTBOT,
libtiff_ctypes.ORIENTATION_LEFTBOT}:
self._imageWidth, self._imageHeight = self._imageHeight, self._imageWidth
self._tileWidth, self._tileHeight = self._tileHeight, self._tileWidth
self.parse_image_description(info.get('imagedescription', ''))
# From TIFF specification, tag 0x128, 2 is inches, 3 is centimeters.
units = {2: 25.4, 3: 10}
Expand All @@ -295,10 +313,10 @@ def _loadMetadata(self):
units.get(info.get('resolutionunit')) and
info.get('yresolution') >= 100):
self._pixelInfo['mm_y'] = units[info['resolutionunit']] / info['yresolution']
if not self._pixelInfo.get('width') and info.get('imagewidth'):
self._pixelInfo['width'] = info['imagewidth']
if not self._pixelInfo.get('height') and info.get('imagelength'):
self._pixelInfo['height'] = info['imagelength']
if not self._pixelInfo.get('width') and self._imageWidth:
self._pixelInfo['width'] = self._imageWidth
if not self._pixelInfo.get('height') and self._imageHeight:
self._pixelInfo['height'] = self._imageHeight

@methodcache(key=partial(strhash, '_getJpegTables'))
def _getJpegTables(self):
Expand Down Expand Up @@ -351,25 +369,33 @@ def _getJpegTables(self):
tableData = tableBuffer[2:tableSize - 2]
return tableData

def _toTileNum(self, x, y):
def _toTileNum(self, x, y, transpose=False):
"""
Get the internal tile number of a tile, from its row and column index.
:param x: The column index of the desired tile.
:type x: int
:param y: The row index of the desired tile.
:type y: int
:param transpose: If true, transpose width and height
:type tranpose: boolean
:return: The internal tile number of the desired tile.
:rtype int
:raises: InvalidOperationTiffException
"""
# TIFFCheckTile and TIFFComputeTile require pixel coordinates
pixelX = int(x * self._tileWidth)
pixelY = int(y * self._tileHeight)

if pixelX >= self._imageWidth or pixelY >= self._imageHeight:
raise InvalidOperationTiffException(
'Tile x=%d, y=%d does not exist' % (x, y))
if not transpose:
pixelX = int(x * self._tileWidth)
pixelY = int(y * self._tileHeight)
if pixelX >= self._imageWidth or pixelY >= self._imageHeight:
raise InvalidOperationTiffException(
'Tile x=%d, y=%d does not exist' % (x, y))
else:
pixelX = int(x * self._tileHeight)
pixelY = int(y * self._tileWidth)
if pixelX >= self._imageHeight or pixelY >= self._imageWidth:
raise InvalidOperationTiffException(
'Tile x=%d, y=%d does not exist' % (x, y))
if libtiff_ctypes.libtiff.TIFFCheckTile(
self._tiffFile, pixelX, pixelY, 0, 0) == 0:
raise InvalidOperationTiffException(
Expand Down Expand Up @@ -510,11 +536,13 @@ def _getUncompressedTile(self, tileNum):
:rtype: PIL.Image
:raises: IOTiffException
"""
tileSize = libtiff_ctypes.libtiff.TIFFTileSize(self._tiffFile).value
with self._tileLock:
tileSize = libtiff_ctypes.libtiff.TIFFTileSize(self._tiffFile).value
imageBuffer = ctypes.create_string_buffer(tileSize)

readSize = libtiff_ctypes.libtiff.TIFFReadEncodedTile(
self._tiffFile, tileNum, imageBuffer, tileSize)
with self._tileLock:
readSize = libtiff_ctypes.libtiff.TIFFReadEncodedTile(
self._tiffFile, tileNum, imageBuffer, tileSize)
if readSize < tileSize:
raise IOTiffException('Read an unexpected number of bytes from an encoded tile')
if self._tiffInfo.get('samplesperpixel') == 1:
Expand All @@ -528,6 +556,77 @@ def _getUncompressedTile(self, tileNum):
image = PIL.Image.frombytes(mode, (self._tileWidth, self._tileHeight), imageBuffer)
return image

def _getTileRotated(self, x, y):
"""
Get a tile from a rotated TIF. This composites uncompressed tiles as
necessary and then rotates the result.
:param x: The column index of the desired tile.
:param y: The row index of the desired tile.
:return: either a buffer with a JPEG or a PIL image.
"""
x0 = x * self._tileWidth
x1 = x0 + self._tileWidth
y0 = y * self._tileHeight
y1 = y0 + self._tileHeight
iw, ih = self._imageWidth, self._imageHeight
tw, th = self._tileWidth, self._tileHeight
transpose = False
if self._tiffInfo.get('orientation') in {
libtiff_ctypes.ORIENTATION_LEFTTOP,
libtiff_ctypes.ORIENTATION_RIGHTTOP,
libtiff_ctypes.ORIENTATION_RIGHTBOT,
libtiff_ctypes.ORIENTATION_LEFTBOT}:
x0, x1, y0, y1 = y0, y1, x0, x1
iw, ih = ih, iw
tw, th = th, tw
transpose = True
if self._tiffInfo.get('orientation') in {
libtiff_ctypes.ORIENTATION_TOPRIGHT,
libtiff_ctypes.ORIENTATION_BOTRIGHT,
libtiff_ctypes.ORIENTATION_RIGHTTOP,
libtiff_ctypes.ORIENTATION_RIGHTBOT}:
x0, x1 = iw - x1, iw - x0
if self._tiffInfo.get('orientation') in {
libtiff_ctypes.ORIENTATION_BOTRIGHT,
libtiff_ctypes.ORIENTATION_BOTLEFT,
libtiff_ctypes.ORIENTATION_RIGHTBOT,
libtiff_ctypes.ORIENTATION_LEFTBOT}:
y0, y1 = ih - y1, ih - y0
tx0 = x0 // tw
tx1 = (x1 - 1) // tw
ty0 = y0 // th
ty1 = (y1 - 1) // th
tile = None
for ty in range(max(0, ty0), max(0, ty1 + 1)):
for tx in range(max(0, tx0), max(0, tx1 + 1)):
subtile = self._getUncompressedTile(self._toTileNum(tx, ty, transpose))
if not tile:
tile = PIL.Image.new(subtile.mode, (tw, th))
tile.paste(subtile, (tx * tw - x0, ty * th - y0))
if tile is None:
raise InvalidOperationTiffException(
'Tile x=%d, y=%d does not exist' % (x, y))
if self._tiffInfo.get('orientation') in {
libtiff_ctypes.ORIENTATION_BOTRIGHT,
libtiff_ctypes.ORIENTATION_BOTLEFT,
libtiff_ctypes.ORIENTATION_RIGHTBOT,
libtiff_ctypes.ORIENTATION_LEFTBOT}:
tile = tile.transpose(PIL.Image.FLIP_TOP_BOTTOM)
if self._tiffInfo.get('orientation') in {
libtiff_ctypes.ORIENTATION_TOPRIGHT,
libtiff_ctypes.ORIENTATION_BOTRIGHT,
libtiff_ctypes.ORIENTATION_RIGHTTOP,
libtiff_ctypes.ORIENTATION_RIGHTBOT}:
tile = tile.transpose(PIL.Image.FLIP_LEFT_RIGHT)
if self._tiffInfo.get('orientation') in {
libtiff_ctypes.ORIENTATION_LEFTTOP,
libtiff_ctypes.ORIENTATION_RIGHTTOP,
libtiff_ctypes.ORIENTATION_RIGHTBOT,
libtiff_ctypes.ORIENTATION_LEFTBOT}:
tile = tile.transpose(PIL.Image.TRANSPOSE)
return tile

@property
def tileWidth(self):
"""
Expand Down Expand Up @@ -572,6 +671,10 @@ def getTile(self, x, y):
:rtype: bytes
:raises: InvalidOperationTiffException or IOTiffException
"""
if self._tiffInfo.get('orientation') not in {
libtiff_ctypes.ORIENTATION_TOPLEFT,
None}:
return self._getTileRotated(x, y)
# This raises an InvalidOperationTiffException if the tile doesn't exist
tileNum = self._toTileNum(x, y)

Expand Down

0 comments on commit 7d0c89f

Please sign in to comment.