Source code for girder_large_image.rest.tiles

#############################################################################
#  Copyright Kitware Inc.
#
#  Licensed under the Apache License, Version 2.0 ( the "License" );
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#############################################################################

import hashlib
import io
import math
import os
import pathlib
import pickle
import re
import urllib

import cherrypy

import large_image
from girder.api import access, filter_logging
from girder.api.describe import Description, autoDescribeRoute, describeRoute
from girder.api.rest import filtermodel, loadmodel, setRawResponse, setResponseHeader
from girder.api.v1.item import Item as ItemResource
from girder.constants import AccessType, TokenScope
from girder.exceptions import RestException
from girder.models.file import File
from girder.models.item import Item
from girder.models.upload import Upload
from girder.utility.progress import setResponseTimeLimit
from large_image.cache_util import strhash
from large_image.constants import TileInputUnits, TileOutputMimeTypes
from large_image.exceptions import TileGeneralError

from .. import loadmodelcache
from ..models.image_item import ImageItem

MimeTypeExtensions = {
    'image/jpeg': 'jpg',
    'image/png': 'png',
    'image/tiff': 'tiff',
}
for key, value in TileOutputMimeTypes.items():
    if value not in MimeTypeExtensions:
        MimeTypeExtensions[value] = key.lower()
ImageMimeTypes = list(MimeTypeExtensions)
EncodingTypes = list(TileOutputMimeTypes.keys()) + [
    'pickle', 'pickle:3', 'pickle:4', 'pickle:5']


def _adjustParams(params):
    """
    Check the user agent of a request.  If it appears to be from an iOS device,
    and the request is asking for JPEG encoding (or hasn't specified an
    encoding), then make sure the output is JFIF.

    It is unfortunate that this requires analyzing the user agent, as this
    method if brittle.  However, other browsers can handle non-JFIF jpegs, and
    we do not want to encur the overhead of conversion if it is not necessary
    (converting to JFIF may require colorspace transforms).

    :param params: the request parameters.  May be modified.
    """
    try:
        userAgent = cherrypy.request.headers.get('User-Agent', '').lower()
    except Exception:
        pass
    if params.get('encoding', 'JPEG') == 'JPEG':
        if ('ipad' in userAgent or 'ipod' in userAgent or 'iphone' in userAgent or
                re.match('((?!chrome|android).)*safari', userAgent, re.IGNORECASE)):
            params['encoding'] = 'JFIF'


def _handleETag(key, item, *args, **kwargs):
    """
    Add or check an ETag header.

    :param key: key for making a distinct etag.
    :param item: item used for the item _id and updated timestamp.
    :param max_age: the maximum cache duration.
    :param *args, **kwargs: additional arguments for generating an etag.
    """
    max_age = kwargs.get('max_age', 3600)
    id = str(item['_id'])
    date = item.get('updated', item.get('created'))
    etag = hashlib.md5(strhash(key, id, date, *args, **kwargs).encode()).hexdigest()
    setResponseHeader('ETag', '"%s"' % etag)
    conditions = [str(x) for x in cherrypy.request.headers.elements('If-Match') or []]
    if conditions and not (conditions == ['*'] or etag in conditions):
        raise cherrypy.HTTPError(
            412, 'If-Match failed: ETag %r did not match %r' % (etag, conditions))
    conditions = [str(x).strip('"')
                  for x in cherrypy.request.headers.elements('If-None-Match') or []]
    if conditions == ['*'] or etag in conditions:
        raise cherrypy.HTTPRedirect([], 304)
    # Explicitly set a max-age to recheck the cache after a while
    setResponseHeader('Cache-control', f'public, max-age={max_age}')


def _pickleParams(params):
    """
    Check if the output should be returned as pickled data and adjust the
    parameters accordingly.

    :param params: a dictionary of parameters.  If encoding starts with
        'pickle', numpy format will be requested.
    :return: None if the output should not be pickled.  Otherwise, the pickle
        protocol that should be used.
    """
    if not str(params.get('encoding')).startswith('pickle'):
        return None
    params['format'] = large_image.constants.TILE_FORMAT_NUMPY
    pickle = params['encoding'].split(':')[-1]
    del params['encoding']
    return int(pickle) or 4 if pickle.isdigit() else 4


def _pickleOutput(data, protocol):
    """
    Pickle some data using a specific protocol and return the pickled data
    and the recommended mime type.

    :param data: the data to pickle.
    :param protocol: the pickle protocol level.
    :returns: the pickled data and the mime type.
    """
    return (
        pickle.dumps(data, protocol=min(protocol, pickle.HIGHEST_PROTOCOL)),
        'application/octet-stream')


[docs] class TilesItemResource(ItemResource): def __init__(self, apiRoot): # Don't call the parent (Item) constructor, to avoid redefining routes, # but do call the grandparent (Resource) constructor super(ItemResource, self).__init__() self.resourceName = 'item' apiRoot.item.route('POST', (':itemId', 'tiles'), self.createTiles) apiRoot.item.route('POST', (':itemId', 'tiles', 'convert'), self.convertImage) apiRoot.item.route('GET', (':itemId', 'tiles'), self.getTilesInfo) apiRoot.item.route('DELETE', (':itemId', 'tiles'), self.deleteTiles) apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnail'), self.getTilesThumbnail) apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnails'), self.listTilesThumbnails) apiRoot.item.route('DELETE', (':itemId', 'tiles', 'thumbnails'), self.deleteTilesThumbnails) apiRoot.item.route('POST', (':itemId', 'tiles', 'thumbnails'), self.addTilesThumbnails) apiRoot.item.route('GET', (':itemId', 'tiles', 'region'), self.getTilesRegion) apiRoot.item.route('GET', (':itemId', 'tiles', 'tile_frames'), self.tileFrames) apiRoot.item.route('GET', (':itemId', 'tiles', 'tile_frames', 'quad_info'), self.tileFramesQuadInfo) apiRoot.item.route('GET', (':itemId', 'tiles', 'pixel'), self.getTilesPixel) apiRoot.item.route('GET', (':itemId', 'tiles', 'histogram'), self.getHistogram) apiRoot.item.route('GET', (':itemId', 'tiles', 'bands'), self.getBandInformation) apiRoot.item.route('GET', (':itemId', 'tiles', 'zxy', ':z', ':x', ':y'), self.getTile) apiRoot.item.route('GET', (':itemId', 'tiles', 'fzxy', ':frame', ':z', ':x', ':y'), self.getTileWithFrame) apiRoot.item.route('GET', (':itemId', 'tiles', 'images'), self.getAssociatedImagesList) apiRoot.item.route('GET', (':itemId', 'tiles', 'images', ':image'), self.getAssociatedImage) apiRoot.item.route('GET', (':itemId', 'tiles', 'images', ':image', 'metadata'), self.getAssociatedImageMetadata) apiRoot.item.route('GET', ('test', 'tiles'), self.getTestTilesInfo) apiRoot.item.route('GET', ('test', 'tiles', 'zxy', ':z', ':x', ':y'), self.getTestTile) apiRoot.item.route('GET', (':itemId', 'tiles', 'dzi.dzi'), self.getDZIInfo) apiRoot.item.route('GET', (':itemId', 'tiles', 'dzi_files', ':level', ':xandy'), self.getDZITile) apiRoot.item.route('GET', (':itemId', 'tiles', 'internal_metadata'), self.getInternalMetadata) # Logging rate limiters filter_logging.addLoggingFilter( 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/zxy(/[^/ ?#]+){3}', frequency=250, duration=10) filter_logging.addLoggingFilter( 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/fzxy(/[^/ ?#]+){3}', frequency=250, duration=10) filter_logging.addLoggingFilter( 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/dzi_files(/[^/ ?#]+){2}', frequency=250, duration=10) filter_logging.addLoggingFilter( 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/region', frequency=100, duration=10) # Cache the model singleton self.imageItemModel = ImageItem()
[docs] @describeRoute( Description('Create a large image for this item.') .param('itemId', 'The source item.', paramType='path') .param('fileId', 'The source file containing the image. Required if ' 'there is more than one file in the item.', required=False) .param('force', 'Always use a job to create the large image.', dataType='boolean', default=False, required=False) .param('notify', 'If a job is required to create the large image, ' 'a nofication can be sent when it is complete.', dataType='boolean', default=True, required=False) .param('localJob', 'If true, run as a local job; if false, run via ' 'the remote worker', dataType='boolean', required=False) .param('tileSize', 'Tile size', dataType='int', default=256, required=False) .param('compression', 'Internal compression format', required=False, enum=['none', 'jpeg', 'deflate', 'lzw', 'zstd', 'packbits', 'webp', 'jp2k']) .param('quality', 'JPEG compression quality where 0 is small and 100 ' 'is highest quality', dataType='int', default=90, required=False) .param('level', 'Compression level for deflate (zip) or zstd.', dataType='int', required=False) .param('predictor', 'Predictor for deflate (zip) or lzw.', required=False, enum=['none', 'horizontal', 'float', 'yes']) .param('psnr', 'JP2K compression target peak-signal-to-noise-ratio ' 'where 0 is lossless and otherwise higher numbers are higher ' 'quality', dataType='int', required=False) .param('cr', 'JP2K target compression ratio where 1 is lossless', dataType='int', required=False) .param('concurrent', 'Suggested number of maximum concurrent ' 'processes to use during conversion. Values less than or ' 'equal to 0 use the number of logical cpus less that value. ' 'Default is -2.', dataType='int', required=False), ) @access.user(scope=TokenScope.DATA_WRITE) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.WRITE) @filtermodel(model='job', plugin='jobs') def createTiles(self, item, params): if 'concurrent' in params: params['_concurrency'] = params.pop('concurrent') largeImageFileId = params.get('fileId') if largeImageFileId is None: files = list(Item().childFiles(item=item, limit=2)) if len(files) == 1: largeImageFileId = str(files[0]['_id']) if not largeImageFileId: msg = 'Missing "fileId" parameter.' raise RestException(msg) largeImageFile = File().load(largeImageFileId, force=True, exc=True) user = self.getCurrentUser() token = self.getCurrentToken() notify = self.boolParam('notify', params, default=True) params.pop('notify', None) localJob = self.boolParam('localJob', params, default=None) params.pop('localJob', None) try: return self.imageItemModel.createImageItem( item, largeImageFile, user, token, createJob='always' if self.boolParam('force', params, default=False) else True, notify=notify, localJob=localJob, **params) except TileGeneralError as e: raise RestException(e.args[0])
[docs] @describeRoute( Description('Create a new large image item based on an existing item') .notes('This can be used to make an item that is a different internal ' 'format than the original item.') .param('itemId', 'The source item.', paramType='path') .param('fileId', 'The source file containing the image. Required if ' 'there is more than one file in the item.', required=False) .param('folderId', 'The destination folder.', required=False) .param('name', 'A new name for the output item.', required=False) .param('localJob', 'If true, run as a local job; if false, run via ' 'the remote worker', dataType='boolean', required=False) .param('tileSize', 'Tile size', dataType='int', default=256, required=False) .param('onlyFrame', 'Only convert a specific 0-based frame of a ' 'multiframe file. If not specified, all frames are converted.', dataType='int', required=False) .param('format', 'File format', required=False, enum=['tiff', 'aperio']) .param('compression', 'Internal compression format', required=False, enum=['none', 'jpeg', 'deflate', 'lzw', 'zstd', 'packbits', 'webp', 'jp2k']) .param('quality', 'JPEG compression quality where 0 is small and 100 ' 'is highest quality', dataType='int', default=90, required=False) .param('level', 'Compression level for deflate (zip) or zstd.', dataType='int', required=False) .param('predictor', 'Predictor for deflate (zip) or lzw.', required=False, enum=['none', 'horizontal', 'float', 'yes']) .param('psnr', 'JP2K compression target peak-signal-to-noise-ratio ' 'where 0 is lossless and otherwise higher numbers are higher ' 'quality', dataType='int', required=False) .param('cr', 'JP2K target compression ratio where 1 is lossless', dataType='int', required=False) .param('concurrent', 'Suggested number of maximum concurrent ' 'processes to use during conversion. Values less than or ' 'equal to 0 use the number of logical cpus less that value. ' 'Default is -2.', dataType='int', required=False), ) @access.user(scope=TokenScope.DATA_WRITE) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) @filtermodel(model='job', plugin='jobs') def convertImage(self, item, params): if 'concurrent' in params: params['_concurrency'] = params.pop('concurrent') largeImageFileId = params.get('fileId') if largeImageFileId is None: files = list(Item().childFiles(item=item, limit=2)) if len(files) == 1: largeImageFileId = str(files[0]['_id']) if not largeImageFileId: msg = 'Missing "fileId" parameter.' raise RestException(msg) largeImageFile = File().load(largeImageFileId, force=True, exc=True) user = self.getCurrentUser() token = self.getCurrentToken() params.pop('notify', None) localJob = self.boolParam('localJob', params, default=True) params.pop('localJob', None) try: return self.imageItemModel.convertImage( item, largeImageFile, user, token, localJob=localJob, **params) except TileGeneralError as e: raise RestException(e.args[0])
@classmethod def _parseTestParams(cls, params): _adjustParams(params) return cls._parseParams(params, False, [ ('minLevel', int), ('maxLevel', int), ('tileWidth', int), ('tileHeight', int), ('sizeX', int), ('sizeY', int), ('fractal', lambda val: val == 'true'), ('frame', int), ('frames', str), ('monochrome', lambda val: val == 'true'), ('encoding', str), ]) @classmethod def _parseParams(cls, params, keepUnknownParams, typeList): """ Given a dictionary of parameters, check that a list of parameters are valid data types. The parameters within the list are validated and copied to a dictionary by themselves. :param params: the dictionary of parameters to validate. :param keepUnknownParams: True to copy all parameters, not just those in the typeList. The parameters in the typeList are still validated. :param typeList: a list of tuples of the form (key, dataType, [outkey1, [outkey2]]). If output keys are used, the original key is renamed to the the output key. If two output keys are specified, the original key is renamed to outkey2 and placed in a sub-dictionary names outkey1. :returns: params: a validated and possibly filtered list of parameters. """ results = {} if keepUnknownParams: results = dict(params) for entry in typeList: key, dataType, outkey1, outkey2 = (list(entry) + [None] * 2)[:4] if key in params: if dataType == 'boolOrInt': dataType = bool if str(params[key]).lower() in ( 'true', 'false', 'on', 'off', 'yes', 'no') else int try: if dataType is bool: results[key] = str(params[key]).lower() in ( 'true', 'on', 'yes', '1') else: results[key] = dataType(params[key]) except ValueError: raise RestException( '"%s" parameter is an incorrect type.' % key) if outkey1 is not None: if outkey2 is not None: results.setdefault(outkey1, {})[outkey2] = results[key] else: results[outkey1] = results[key] del results[key] return results def _getTilesInfo(self, item, imageArgs): """ Get metadata for an item's large image. :param item: the item to query. :param imageArgs: additional arguments to use when fetching image data. :return: the tile metadata. """ try: return self.imageItemModel.getMetadata(item, **imageArgs) except TileGeneralError as e: raise RestException(e.args[0], code=400) def _setContentDisposition(self, item, contentDisposition, mime, subname, fullFilename=None): """ If requested, set the content disposition and a suggested file name. :param item: an item that includes a name. :param contentDisposition: either 'inline' or 'attachment', otherwise no header is added. :param mime: the mimetype of the output image. Used for the filename suffix. :param subname: a subname to append to the item name. :param fullFilename: if specified, use this instead of the item name and the subname. """ if (not item or not item.get('name') or mime not in MimeTypeExtensions or contentDisposition not in ('inline', 'attachment')): return if fullFilename: filename = fullFilename else: filename = os.path.splitext(item['name'])[0] if subname: filename += '-' + subname filename += '.' + MimeTypeExtensions[mime] if not isinstance(filename, str): filename = filename.decode('utf8', 'ignore') safeFilename = filename.encode('ascii', 'ignore').replace(b'"', b'') encodedFilename = urllib.parse.quote(filename.encode('utf8', 'ignore')) setResponseHeader( 'Content-Disposition', '%s; filename="%s"; filename*=UTF-8\'\'%s' % ( contentDisposition, safeFilename, encodedFilename))
[docs] @describeRoute( Description('Get large image metadata.') .param('itemId', 'The ID of the item.', paramType='path') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getTilesInfo(self, item, params): return self._getTilesInfo(item, params)
[docs] @describeRoute( Description('Get large image internal metadata.') .param('itemId', 'The ID of the item.', paramType='path') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getInternalMetadata(self, item, params): try: return self.imageItemModel.getInternalMetadata(item, **params) except TileGeneralError as e: raise RestException(e.args[0], code=400)
[docs] @describeRoute( Description('Get test large image metadata.'), ) @access.public(scope=TokenScope.DATA_READ) def getTestTilesInfo(self, params): item = {'largeImage': {'sourceName': 'test'}} imageArgs = self._parseTestParams(params) return self._getTilesInfo(item, imageArgs)
[docs] @describeRoute( Description('Get DeepZoom compatible metadata.') .param('itemId', 'The ID of the item.', paramType='path') .param('overlap', 'Pixel overlap (default 0), must be non-negative.', required=False, dataType='int') .param('tilesize', 'Tile size (default 256), must be a power of 2', required=False, dataType='int') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getDZIInfo(self, item, params): if 'encoding' in params and params['encoding'] not in ('JPEG', 'PNG'): msg = 'Only JPEG and PNG encodings are supported' raise RestException(msg, code=400) info = self._getTilesInfo(item, params) tilesize = int(params.get('tilesize', 256)) if tilesize & (tilesize - 1): msg = 'Invalid tilesize' raise RestException(msg, code=400) overlap = int(params.get('overlap', 0)) if overlap < 0: msg = 'Invalid overlap' raise RestException(msg, code=400) result = ''.join([ '<?xml version="1.0" encoding="UTF-8"?>', '<Image', ' TileSize="%d"' % tilesize, ' Overlap="%d"' % overlap, ' Format="%s"' % ('png' if params.get('encoding') == 'PNG' else 'jpg'), ' xmlns="http://schemas.microsoft.com/deepzoom/2008">', '<Size', ' Width="%d"' % info['sizeX'], ' Height="%d"' % info['sizeY'], '/>' '</Image>', ]) setResponseHeader('Content-Type', 'text/xml') setRawResponse() return result
def _getTile(self, item, z, x, y, imageArgs, mayRedirect=False): """ Get an large image tile. :param item: the item to get a tile from. :param z: tile layer number (0 is the most zoomed-out). .param x: the X coordinate of the tile (0 is the left side). .param y: the Y coordinate of the tile (0 is the top). :param imageArgs: additional arguments to use when fetching image data. :param mayRedirect: if True or one of 'any', 'encoding', or 'exact', allow return a response which may be a redirect. :return: a function that returns the raw image data. """ try: x, y, z = int(x), int(y), int(z) except ValueError: msg = 'x, y, and z must be integers' raise RestException(msg, code=400) if x < 0 or y < 0 or z < 0: msg = 'x, y, and z must be positive integers' raise RestException(msg, code=400) result = self.imageItemModel._tileFromHash( item, x, y, z, mayRedirect=mayRedirect, **imageArgs) if result is not None: tileData, tileMime = result else: try: tileData, tileMime = self.imageItemModel.getTile( item, x, y, z, mayRedirect=mayRedirect, **imageArgs) except TileGeneralError as e: raise RestException(e.args[0], code=404) setResponseHeader('Content-Type', tileMime) setRawResponse() return tileData
[docs] @describeRoute( Description('Get a large image tile.') .param('itemId', 'The ID of the item.', paramType='path') .param('z', 'The layer number of the tile (0 is the most zoomed-out ' 'layer).', paramType='path') .param('x', 'The X coordinate of the tile (0 is the left side).', paramType='path') .param('y', 'The Y coordinate of the tile (0 is the top).', paramType='path') .param('redirect', 'If the tile exists as a complete file, allow an ' 'HTTP redirect instead of returning the data directly. The ' 'redirect might not have the correct mime type. "exact" must ' 'match the image encoding and quality parameters, "encoding" ' 'must match the image encoding but disregards quality, and ' '"any" will redirect to any image if possible.', required=False, enum=['false', 'exact', 'encoding', 'any'], default='false') .produces(ImageMimeTypes) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) # Without caching, this checks for permissions every time. By using the # LoadModelCache, three database lookups are avoided, which saves around # 6 ms in tests. We also avoid the @access.public decorator and directly # set the accessLevel attribute on the method. # @access.public(cookie=True, scope=TokenScope.DATA_READ) # @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) # def getTile(self, item, z, x, y, params): # return self._getTile(item, z, x, y, params, True) def getTile(self, itemId, z, x, y, params): _adjustParams(params) item = loadmodelcache.loadModel( self, 'item', id=itemId, allowCookie=True, level=AccessType.READ) _handleETag('getTile', item, z, x, y, params) redirect = params.get('redirect', False) if redirect not in ('any', 'exact', 'encoding'): redirect = False return self._getTile(item, z, x, y, params, mayRedirect=redirect)
getTile.accessLevel = 'public' getTile.cookieAuth = True getTile.requiredScopes = TokenScope.DATA_READ
[docs] @describeRoute( Description('Get a large image tile with a frame number.') .param('itemId', 'The ID of the item.', paramType='path') .param('frame', 'The frame number of the tile.', paramType='path') .param('z', 'The layer number of the tile (0 is the most zoomed-out ' 'layer).', paramType='path') .param('x', 'The X coordinate of the tile (0 is the left side).', paramType='path') .param('y', 'The Y coordinate of the tile (0 is the top).', paramType='path') .param('redirect', 'If the tile exists as a complete file, allow an ' 'HTTP redirect instead of returning the data directly. The ' 'redirect might not have the correct mime type. "exact" must ' 'match the image encoding and quality parameters, "encoding" ' 'must match the image encoding but disregards quality, and ' '"any" will redirect to any image if possible.', required=False, enum=['false', 'exact', 'encoding', 'any'], default='false') .produces(ImageMimeTypes) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) # See getTile for caching rationale def getTileWithFrame(self, itemId, frame, z, x, y, params): _adjustParams(params) item = loadmodelcache.loadModel( self, 'item', id=itemId, allowCookie=True, level=AccessType.READ) _handleETag('getTileWithFrame', item, frame, z, x, y, params) redirect = params.get('redirect', False) if redirect not in ('any', 'exact', 'encoding'): redirect = False params['frame'] = frame return self._getTile(item, z, x, y, params, mayRedirect=redirect)
getTileWithFrame.accessLevel = 'public'
[docs] @describeRoute( Description('Get a test large image tile.') .param('z', 'The layer number of the tile (0 is the most zoomed-out ' 'layer).', paramType='path') .param('x', 'The X coordinate of the tile (0 is the left side).', paramType='path') .param('y', 'The Y coordinate of the tile (0 is the top).', paramType='path') .produces(ImageMimeTypes), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) def getTestTile(self, z, x, y, params): item = {'largeImage': {'sourceName': 'test'}} imageArgs = self._parseTestParams(params) return self._getTile(item, z, x, y, imageArgs)
[docs] @describeRoute( Description('Get a DeepZoom image tile.') .param('itemId', 'The ID of the item.', paramType='path') .param('level', 'The deepzoom layer number of the tile (8 is the ' 'most zoomed-out layer).', paramType='path') .param('xandy', 'The X and Y coordinate of the tile in the form ' '(x)_(y).(extension) where (0_0 is the left top).', paramType='path') .produces(ImageMimeTypes) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getDZITile(self, item, level, xandy, params): _adjustParams(params) tilesize = int(params.get('tilesize', 256)) if tilesize & (tilesize - 1): msg = 'Invalid tilesize' raise RestException(msg, code=400) overlap = int(params.get('overlap', 0)) if overlap < 0: msg = 'Invalid overlap' raise RestException(msg, code=400) x, y = (int(xy) for xy in xandy.split('.')[0].split('_')) _handleETag('getDZITile', item, level, xandy, params) metadata = self.imageItemModel.getMetadata(item, **params) level = int(level) maxlevel = int(math.ceil(math.log(max( metadata['sizeX'], metadata['sizeY'])) / math.log(2))) if level < 1 or level > maxlevel: msg = 'level must be between 1 and the image scale' raise RestException(msg, code=400) lfactor = 2 ** (maxlevel - level) region = { 'left': (x * tilesize - overlap) * lfactor, 'top': (y * tilesize - overlap) * lfactor, 'right': ((x + 1) * tilesize + overlap) * lfactor, 'bottom': ((y + 1) * tilesize + overlap) * lfactor, } width = height = tilesize + overlap * 2 if region['left'] < 0: width += int(region['left'] / lfactor) region['left'] = 0 if region['top'] < 0: height += int(region['top'] / lfactor) region['top'] = 0 if region['left'] >= metadata['sizeX']: msg = 'x is outside layer' raise RestException(msg, code=400) if region['top'] >= metadata['sizeY']: msg = 'y is outside layer' raise RestException(msg, code=400) if region['left'] < metadata['sizeX'] and region['right'] > metadata['sizeX']: region['right'] = metadata['sizeX'] width = int(math.ceil(float(region['right'] - region['left']) / lfactor)) if region['top'] < metadata['sizeY'] and region['bottom'] > metadata['sizeY']: region['bottom'] = metadata['sizeY'] height = int(math.ceil(float(region['bottom'] - region['top']) / lfactor)) regionData, regionMime = self.imageItemModel.getRegion( item, region=region, output=dict(maxWidth=width, maxHeight=height), **params) setResponseHeader('Content-Type', regionMime) setRawResponse() return regionData
[docs] @describeRoute( Description('Remove a large image from this item.') .param('itemId', 'The ID of the item.', paramType='path'), ) @access.user(scope=TokenScope.DATA_WRITE) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.WRITE) def deleteTiles(self, item, params): deleted = self.imageItemModel.delete(item) return { 'deleted': deleted, }
[docs] @describeRoute( Description('Get a thumbnail of a large image item.') .notes('Aspect ratio is always preserved. If both width and height ' 'are specified, the resulting thumbnail may be smaller in one ' 'of the two dimensions. If neither width nor height is given, ' 'a default size will be returned. ' 'This creates a thumbnail from the lowest level of the source ' 'image, which means that asking for a large thumbnail will not ' 'be a high-quality image.') .param('itemId', 'The ID of the item.', paramType='path') .param('width', 'The maximum width of the thumbnail in pixels.', required=False, dataType='int') .param('height', 'The maximum height of the thumbnail in pixels.', required=False, dataType='int') .param('fill', 'A fill color. If width and height are both specified ' 'and fill is specified and not "none", the output image is ' 'padded on either the sides or the top and bottom to the ' 'requested output size. Most css colors are accepted.', required=False) .param('frame', 'For multiframe images, the 0-based frame number. ' 'This is ignored on non-multiframe images.', required=False, dataType='int') .param('encoding', 'Output image encoding. TILED generates a tiled ' 'tiff without the upper limit on image size the other options ' 'have. For geospatial sources, TILED will also have ' 'appropriate tagging. Pickle emits python pickle data with an ' 'optional specific protocol', required=False, enum=EncodingTypes, default='JPEG') .param('contentDisposition', 'Specify the Content-Disposition response ' 'header disposition-type value.', required=False, enum=['inline', 'attachment']) .param('contentDispositionFilename', 'Specify the filename used in ' 'the Content-Disposition response header.', required=False) .produces(ImageMimeTypes) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getTilesThumbnail(self, item, params): _adjustParams(params) params = self._parseParams(params, True, [ ('width', int), ('height', int), ('fill', str), ('frame', int), ('jpegQuality', int), ('jpegSubsampling', int), ('tiffCompression', str), ('encoding', str), ('style', str), ('contentDisposition', str), ('contentDispositionFileName', str), ]) _handleETag('getTilesThumbnail', item, params) pickle = _pickleParams(params) try: result = self.imageItemModel.getThumbnail(item, **params) except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) if not isinstance(result, tuple): return result thumbData, thumbMime = result if pickle: thumbData, thumbMime = _pickleOutput(thumbData, pickle) self._setContentDisposition( item, params.get('contentDisposition'), thumbMime, 'thumbnail', params.get('contentDispositionFilename')) setResponseHeader('Content-Type', thumbMime) setRawResponse() return thumbData
[docs] @describeRoute( Description('Get any region of a large image item, optionally scaling ' 'it.') .notes('If neither width nor height is specified, the full resolution ' 'region is returned. If a width or height is specified, ' 'aspect ratio is always preserved (if both are given, the ' 'resulting image may be smaller in one of the two ' 'dimensions). When scaling must be applied, the image is ' 'downsampled from a higher resolution layer, never upsampled.') .param('itemId', 'The ID of the item.', paramType='path') .param('left', 'The left column (0-based) of the region to process. ' 'Negative values are offsets from the right edge.', required=False, dataType='float') .param('top', 'The top row (0-based) of the region to process. ' 'Negative values are offsets from the bottom edge.', required=False, dataType='float') .param('right', 'The right column (0-based from the left) of the ' 'region to process. The region will not include this column. ' 'Negative values are offsets from the right edge.', required=False, dataType='float') .param('bottom', 'The bottom row (0-based from the top) of the region ' 'to process. The region will not include this row. Negative ' 'values are offsets from the bottom edge.', required=False, dataType='float') .param('regionWidth', 'The width of the region to process.', required=False, dataType='float') .param('regionHeight', 'The height of the region to process.', required=False, dataType='float') .param('units', 'Units used for left, top, right, bottom, ' 'regionWidth, and regionHeight. base_pixels are pixels at the ' 'maximum resolution, pixels and mm are at the specified ' 'magnfication, fraction is a scale of [0-1].', required=False, enum=sorted(set(TileInputUnits.values())), default='base_pixels') .param('width', 'The maximum width of the output image in pixels.', required=False, dataType='int') .param('height', 'The maximum height of the output image in pixels.', required=False, dataType='int') .param('fill', 'A fill color. If output dimensions are specified and ' 'fill is specified and not "none", the output image is padded ' 'on either the sides or the top and bottom to the requested ' 'output size. Most css colors are accepted.', required=False) .param('magnification', 'Magnification of the output image. If ' 'neither width for height is specified, the magnification, ' 'mm_x, and mm_y parameters are used to select the output size.', required=False, dataType='float') .param('mm_x', 'The size of the output pixels in millimeters', required=False, dataType='float') .param('mm_y', 'The size of the output pixels in millimeters', required=False, dataType='float') .param('exact', 'If magnification, mm_x, or mm_y are specified, they ' 'must match an existing level of the image exactly.', required=False, dataType='boolean', default=False) .param('frame', 'For multiframe images, the 0-based frame number. ' 'This is ignored on non-multiframe images.', required=False, dataType='int') .param('encoding', 'Output image encoding. TILED generates a tiled ' 'tiff without the upper limit on image size the other options ' 'have. For geospatial sources, TILED will also have ' 'appropriate tagging. Pickle emits python pickle data with an ' 'optional specific protocol', required=False, enum=EncodingTypes, default='JPEG') .param('jpegQuality', 'Quality used for generating JPEG images', required=False, dataType='int', default=95) .param('jpegSubsampling', 'Chroma subsampling used for generating ' 'JPEG images. 0, 1, and 2 are full, half, and quarter ' 'resolution chroma respectively.', required=False, enum=['0', '1', '2'], dataType='int', default='0') .param('tiffCompression', 'Compression method when storing a TIFF ' 'image', required=False, enum=['none', 'raw', 'lzw', 'tiff_lzw', 'jpeg', 'deflate', 'tiff_adobe_deflate']) .param('style', 'JSON-encoded style string', required=False) .param('resample', 'If false, an existing level of the image is used ' 'for the region. If true, the internal values are ' 'interpolated to match the specified size as needed. 0-3 for ' 'a specific interpolation method (0-nearest, 1-lanczos, ' '2-bilinear, 3-bicubic)', required=False, enum=['false', 'true', '0', '1', '2', '3']) .param('contentDisposition', 'Specify the Content-Disposition response ' 'header disposition-type value.', required=False, enum=['inline', 'attachment']) .param('contentDispositionFilename', 'Specify the filename used in ' 'the Content-Disposition response header.', required=False) .produces(ImageMimeTypes) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403) .errorResponse('Insufficient memory.'), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getTilesRegion(self, item, params): _adjustParams(params) params = self._parseParams(params, True, [ ('left', float, 'region', 'left'), ('top', float, 'region', 'top'), ('right', float, 'region', 'right'), ('bottom', float, 'region', 'bottom'), ('regionWidth', float, 'region', 'width'), ('regionHeight', float, 'region', 'height'), ('units', str, 'region', 'units'), ('unitsWH', str, 'region', 'unitsWH'), ('width', int, 'output', 'maxWidth'), ('height', int, 'output', 'maxHeight'), ('fill', str), ('magnification', float, 'scale', 'magnification'), ('mm_x', float, 'scale', 'mm_x'), ('mm_y', float, 'scale', 'mm_y'), ('exact', bool, 'scale', 'exact'), ('frame', int), ('encoding', str), ('jpegQuality', int), ('jpegSubsampling', int), ('tiffCompression', str), ('style', str), ('resample', 'boolOrInt'), ('contentDisposition', str), ('contentDispositionFileName', str), ]) _handleETag('getTilesRegion', item, params) pickle = _pickleParams(params) setResponseTimeLimit(86400) try: regionData, regionMime = self.imageItemModel.getRegion( item, **params) if pickle: regionData, regionMime = _pickleOutput(regionData, pickle) except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) self._setContentDisposition( item, params.get('contentDisposition'), regionMime, 'region', params.get('contentDispositionFilename')) setResponseHeader('Content-Type', regionMime) if isinstance(regionData, pathlib.Path): BUF_SIZE = 65536 def stream(): try: with regionData.open('rb') as f: while True: data = f.read(BUF_SIZE) if not data: break yield data finally: regionData.unlink() return stream setRawResponse() return regionData
[docs] @describeRoute( Description('Get a single pixel of a large image item.') .param('itemId', 'The ID of the item.', paramType='path') .param('left', 'The left column (0-based) of the pixel.', required=False, dataType='float') .param('top', 'The top row (0-based) of the pixel.', required=False, dataType='float') .param('units', 'Units used for left and top. base_pixels are pixels ' 'at the maximum resolution, pixels and mm are at the specified ' 'magnfication, fraction is a scale of [0-1].', required=False, enum=sorted(set(TileInputUnits.values())), default='base_pixels') .param('frame', 'For multiframe images, the 0-based frame number. ' 'This is ignored on non-multiframe images.', required=False, dataType='int') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getTilesPixel(self, item, params): params = self._parseParams(params, True, [ ('left', float, 'region', 'left'), ('top', float, 'region', 'top'), ('right', float, 'region', 'right'), ('bottom', float, 'region', 'bottom'), ('units', str, 'region', 'units'), ('frame', int), ]) try: pixel = self.imageItemModel.getPixel(item, **params) except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) return pixel
def _cacheHistograms(self, item, histRange, cache, params): needed = [] result = {'cached': []} tilesource = self.imageItemModel._loadTileSource(item, **params) for frame in range(tilesource.frames): if histRange is not None and histRange != 'round': continue checkParams = params.copy() checkParams['range'] = histRange if tilesource.frames > 1: checkParams['frame'] = frame else: checkParams.pop('frame', None) result['cached'].append(self.imageItemModel.histogram( item, checkAndCreate='check', **checkParams)) if not result['cached'][-1]: needed.append(checkParams) if cache == 'schedule' and not all(result['cached']): result['scheduledJob'] = str(self.imageItemModel._scheduleHistograms( item, needed, self.getCurrentUser())['_id']) return result
[docs] @describeRoute( Description('Get a histogram for any region of a large image item.') .notes('This can take all of the parameters as the region endpoint, ' 'plus some histogram-specific parameters. Only typically used ' 'parameters are listed. The returned result is a list with ' 'one entry per channel (always one of L, LA, RGB, or RGBA ' 'colorspace). Each entry has the histogram values, bin edges, ' 'minimum and maximum values for the channel, and number of ' 'samples (pixels) used in the computation.') .param('itemId', 'The ID of the item.', paramType='path') .param('width', 'The maximum width of the analyzed region in pixels.', default=2048, required=False, dataType='int') .param('height', 'The maximum height of the analyzed region in pixels.', default=2048, required=False, dataType='int') .param('resample', 'If false, an existing level of the image is used ' 'for the histogram. If true, the internal values are ' 'interpolated to match the specified size as needed. 0-3 for ' 'a specific interpolation method (0-nearest, 1-lanczos, ' '2-bilinear, 3-bicubic)', required=False, enum=['false', 'true', '0', '1', '2', '3'], default='false') .param('frame', 'For multiframe images, the 0-based frame number. ' 'This is ignored on non-multiframe images.', required=False, dataType='int') .param('bins', 'The number of bins in the histogram.', default=256, required=False, dataType='int') .param('rangeMin', 'The minimum value in the histogram. Defaults to ' 'the minimum value in the image.', required=False, dataType='float') .param('rangeMax', 'The maximum value in the histogram. Defaults to ' 'the maximum value in the image.', required=False, dataType='float') .param('roundRange', 'If true and neither a minimum or maximum is ' 'specified for the range, round the bin edges and adjust the ' 'number of bins for integer data with smaller ranges.', required=False, dataType='boolean', default=False) .param('density', 'If true, scale the results by the number of ' 'samples.', required=False, dataType='boolean', default=False) .param('cache', 'Report on or request caching the specified histogram ' 'for all frames. Scheduling creates a local job.', required=False, enum=['none', 'report', 'schedule']) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getHistogram(self, item, params): _adjustParams(params) params = self._parseParams(params, True, [ ('left', float, 'region', 'left'), ('top', float, 'region', 'top'), ('right', float, 'region', 'right'), ('bottom', float, 'region', 'bottom'), ('regionWidth', float, 'region', 'width'), ('regionHeight', float, 'region', 'height'), ('units', str, 'region', 'units'), ('unitsWH', str, 'region', 'unitsWH'), ('width', int, 'output', 'maxWidth'), ('height', int, 'output', 'maxHeight'), ('fill', str), ('magnification', float, 'scale', 'magnification'), ('mm_x', float, 'scale', 'mm_x'), ('mm_y', float, 'scale', 'mm_y'), ('exact', bool, 'scale', 'exact'), ('frame', int), ('encoding', str), ('jpegQuality', int), ('jpegSubsampling', int), ('tiffCompression', str), ('style', str), ('resample', 'boolOrInt'), ('bins', int), ('rangeMin', int), ('rangeMax', int), ('roundRange', bool), ('density', bool), ]) _handleETag('getHistogram', item, params) histRange = None if 'rangeMin' in params or 'rangeMax' in params: histRange = [params.pop('rangeMin', 0), params.pop('rangeMax', 256)] if params.get('roundRange'): if params.pop('roundRange', False) and histRange is None: histRange = 'round' cache = params.pop('cache', None) if cache in {'report', 'schedule'}: return self._cacheHistograms(item, histRange, cache, params) result = self.imageItemModel.histogram(item, range=histRange, **params) result = result['histogram'] # Cast everything to lists and floats so json will encode properly for entry in result: for key in {'bin_edges', 'hist', 'range'}: if key in entry: entry[key] = [float(val) for val in list(entry[key])] for key in {'min', 'max', 'samples'}: if key in entry: entry[key] = float(entry[key]) return result
[docs] @describeRoute( Description('Get band information for a large image item.') .param('itemId', 'The ID of the item.', paramType='path') .param('frame', 'For multiframe images, the 0-based frame number. ' 'This is ignored on non-multiframe images.', required=False, dataType='int') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getBandInformation(self, item, params): _adjustParams(params) params = self._parseParams(params, True, [ ('frame', int), ]) _handleETag('getBandInformation', item, params) result = self.imageItemModel.getBandInformation(item, **params) return result
[docs] @describeRoute( Description('Get a list of additional images associated with a large image.') .param('itemId', 'The ID of the item.', paramType='path') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def getAssociatedImagesList(self, item, params): try: return self.imageItemModel.getAssociatedImagesList(item) except TileGeneralError as e: raise RestException(e.args[0], code=400)
[docs] @describeRoute( Description('Get an image associated with a large image.') .notes('Because associated images may contain PHI, admin access to ' 'the item is required.') .param('itemId', 'The ID of the item.', paramType='path') .param('image', 'The key of the associated image.', paramType='path') .param('width', 'The maximum width of the image in pixels.', required=False, dataType='int') .param('height', 'The maximum height of the image in pixels.', required=False, dataType='int') .param('encoding', 'Image output encoding', required=False, enum=['JPEG', 'PNG', 'TIFF'], default='JPEG') .param('contentDisposition', 'Specify the Content-Disposition response ' 'header disposition-type value.', required=False, enum=['inline', 'attachment']) .param('contentDispositionFilename', 'Specify the filename used in ' 'the Content-Disposition response header.', required=False) .produces(ImageMimeTypes) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) def getAssociatedImage(self, itemId, image, params): _adjustParams(params) # We can't use the loadmodel decorator, as we want to allow cookies item = loadmodelcache.loadModel( self, 'item', id=itemId, allowCookie=True, level=AccessType.READ) params = self._parseParams(params, True, [ ('width', int), ('height', int), ('jpegQuality', int), ('jpegSubsampling', int), ('tiffCompression', str), ('encoding', str), ('style', str), ('contentDisposition', str), ('contentDispositionFileName', str), ]) _handleETag('getAssociatedImage', item, image, params) try: result = self.imageItemModel.getAssociatedImage(item, image, **params) except TileGeneralError as e: raise RestException(e.args[0], code=400) if not isinstance(result, tuple): return result imageData, imageMime = result self._setContentDisposition( item, params.get('contentDisposition'), imageMime, image, params.get('contentDispositionFilename')) setResponseHeader('Content-Type', imageMime) setRawResponse() return imageData
[docs] @autoDescribeRoute( Description('Get metadata for an image associated with a large image.') .modelParam('itemId', model=Item, level=AccessType.READ) .param('image', 'The key of the associated image.', paramType='path') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(scope=TokenScope.DATA_READ) def getAssociatedImageMetadata(self, item, image, params): _handleETag('getAssociatedImageMetadata', item, image) tilesource = self.imageItemModel._loadTileSource(item, **params) pilImage = tilesource._getAssociatedImage(image) if pilImage is None: return {} result = { 'sizeX': pilImage.width, 'sizeY': pilImage.height, 'mode': pilImage.mode, } if pilImage.format: result['format'] = pilImage.format if pilImage.info: result['info'] = pilImage.info return result
_tileFramesParams = [ ('framesAcross', int), ('frameList', str), ('left', float, 'region', 'left'), ('top', float, 'region', 'top'), ('right', float, 'region', 'right'), ('bottom', float, 'region', 'bottom'), ('regionWidth', float, 'region', 'width'), ('regionHeight', float, 'region', 'height'), ('units', str, 'region', 'units'), ('unitsWH', str, 'region', 'unitsWH'), ('width', int, 'output', 'maxWidth'), ('height', int, 'output', 'maxHeight'), ('fill', str), ('magnification', float, 'scale', 'magnification'), ('mm_x', float, 'scale', 'mm_x'), ('mm_y', float, 'scale', 'mm_y'), ('exact', bool, 'scale', 'exact'), ('frame', int), ('encoding', str), ('jpegQuality', int), ('jpegSubsampling', int), ('tiffCompression', str), ('style', str), ('resample', 'boolOrInt'), ('contentDisposition', str), ('contentDispositionFileName', str), ]
[docs] @describeRoute( Description('Composite thumbnails of multiple frames into a single image.') .param('itemId', 'The ID of the item.', paramType='path') .param('framesAcross', 'How many frames across', required=False, dataType='int') .param('frameList', 'Comma-separated list of frames', required=False) .param('cache', 'Cache the results for future use', required=False, dataType='boolean', default=False) .param('left', 'The left column (0-based) of the region to process. ' 'Negative values are offsets from the right edge.', required=False, dataType='float') .param('top', 'The top row (0-based) of the region to process. ' 'Negative values are offsets from the bottom edge.', required=False, dataType='float') .param('right', 'The right column (0-based from the left) of the ' 'region to process. The region will not include this column. ' 'Negative values are offsets from the right edge.', required=False, dataType='float') .param('bottom', 'The bottom row (0-based from the top) of the region ' 'to process. The region will not include this row. Negative ' 'values are offsets from the bottom edge.', required=False, dataType='float') .param('regionWidth', 'The width of the region to process.', required=False, dataType='float') .param('regionHeight', 'The height of the region to process.', required=False, dataType='float') .param('units', 'Units used for left, top, right, bottom, ' 'regionWidth, and regionHeight. base_pixels are pixels at the ' 'maximum resolution, pixels and mm are at the specified ' 'magnfication, fraction is a scale of [0-1].', required=False, enum=sorted(set(TileInputUnits.values())), default='base_pixels') .param('width', 'The maximum width of the output image in pixels.', required=False, dataType='int') .param('height', 'The maximum height of the output image in pixels.', required=False, dataType='int') .param('fill', 'A fill color. If output dimensions are specified and ' 'fill is specified and not "none", the output image is padded ' 'on either the sides or the top and bottom to the requested ' 'output size. Most css colors are accepted.', required=False) .param('magnification', 'Magnification of the output image. If ' 'neither width for height is specified, the magnification, ' 'mm_x, and mm_y parameters are used to select the output size.', required=False, dataType='float') .param('mm_x', 'The size of the output pixels in millimeters', required=False, dataType='float') .param('mm_y', 'The size of the output pixels in millimeters', required=False, dataType='float') .param('exact', 'If magnification, mm_x, or mm_y are specified, they ' 'must match an existing level of the image exactly.', required=False, dataType='boolean', default=False) .param('frame', 'For multiframe images, the 0-based frame number. ' 'This is ignored on non-multiframe images.', required=False, dataType='int') .param('encoding', 'Output image encoding. TILED generates a tiled ' 'tiff without the upper limit on image size the other options ' 'have. For geospatial sources, TILED will also have ' 'appropriate tagging. Pickle emits python pickle data with an ' 'optional specific protocol', required=False, enum=EncodingTypes, default='JPEG') .param('jpegQuality', 'Quality used for generating JPEG images', required=False, dataType='int', default=95) .param('jpegSubsampling', 'Chroma subsampling used for generating ' 'JPEG images. 0, 1, and 2 are full, half, and quarter ' 'resolution chroma respectively.', required=False, enum=['0', '1', '2'], dataType='int', default='0') .param('tiffCompression', 'Compression method when storing a TIFF ' 'image', required=False, enum=['none', 'raw', 'lzw', 'tiff_lzw', 'jpeg', 'deflate', 'tiff_adobe_deflate']) .param('style', 'JSON-encoded style string', required=False) .param('resample', 'If false, an existing level of the image is used ' 'for the region. If true, the internal values are ' 'interpolated to match the specified size as needed. 0-3 for ' 'a specific interpolation method (0-nearest, 1-lanczos, ' '2-bilinear, 3-bicubic)', required=False, enum=['false', 'true', '0', '1', '2', '3']) .param('contentDisposition', 'Specify the Content-Disposition response ' 'header disposition-type value.', required=False, enum=['inline', 'attachment']) .param('contentDispositionFilename', 'Specify the filename used in ' 'the Content-Disposition response header.', required=False) .produces(ImageMimeTypes) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403) .errorResponse('Insufficient memory.'), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def tileFrames(self, item, params): cache = params.pop('cache', False) checkAndCreate = False if cache else 'nosave' _adjustParams(params) params = self._parseParams(params, True, self._tileFramesParams) _handleETag('tileFrames', item, params) pickle = _pickleParams(params) if 'frameList' in params: params['frameList'] = [ int(f.strip()) for f in str(params['frameList']).lstrip( '[').rstrip(']').split(',')] setResponseTimeLimit(86400) try: result = self.imageItemModel.tileFrames( item, checkAndCreate=checkAndCreate, **params) except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) if not isinstance(result, tuple): return result regionData, regionMime = result if pickle: regionData, regionMime = _pickleOutput(regionData, pickle) self._setContentDisposition( item, params.get('contentDisposition'), regionMime, 'tileframes', params.get('contentDispositionFilename')) setResponseHeader('Content-Type', regionMime) if isinstance(regionData, pathlib.Path): BUF_SIZE = 65536 def stream(): try: with regionData.open('rb') as f: while True: data = f.read(BUF_SIZE) if not data: break yield data finally: regionData.unlink() return stream setRawResponse() return regionData
[docs] @describeRoute( Description('Get parameters for using tile_frames as background sprite images.') .param('itemId', 'The ID of the item.', paramType='path') .param('format', 'Optional format parameters, such as "encoding=JPEG&' 'jpegQuality=85&jpegSubsampling=1". If specified, these ' 'replace the defaults.', required=False) .param('query', 'Addition query parameters that would be passed to ' 'tile endpoints, such as style.', required=False) .param('frameBase', 'Starting frame number (default 0). If c/z/t/xy ' 'then step through values from 0 to number of that axis - 1. ' 'The axis specification in only useful for cache reporting or ' 'scheduling', required=False, dataType='int') .param('frameStride', 'Only use every frameStride frame of the image ' '(default 1). c/z/t/xy to use the length of that axis', required=False, dataType='int') .param('frameGroup', 'Group frames when using multiple textures to ' 'keep boundaries at a multiple of the group size number. ' 'c/z/t/xy to use the length of that axis.', required=False, dataType='int') .param('frameGroupFactor', 'Ignore grouping if the resultant images ' 'would be more than this factor smaller than without grouping ' '(default 4)', required=False, dataType='int') .param('frameGroupStride', 'Reorder frames based on the to stride ' '(default 1). "auto" to use frameGroup / frameStride if that ' 'value is an integer.', required=False, dataType='int') .param('maxTextureSize', 'Maximum texture size in either dimension. ' 'This should be the smaller of a desired value and of the ' 'intended graphics environment texture buffer (default 16384).', required=False, dataType='int') .param('maxTextures', 'Maximum number of textures to use (default 1).', required=False, dataType='int') .param('maxTotalTexturePixels', 'Limit the total area of all combined ' 'textures (default 2**30).', required=False, dataType='int') .param('alignment', 'Individual frame alignment within a texture. ' 'Used to avoid jpeg artifacts from crossing frames (default ' '16).', required=False, dataType='int') .param('maxFrameSize', 'If specified, frames will never be larger ' 'than this, even if the texture size allows it (default None).', required=False, dataType='int') .param('cache', 'Report on or request caching the resultant frames. ' 'Scheduling creates a local job.', required=False, enum=['none', 'report', 'schedule']) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.public(cookie=True, scope=TokenScope.DATA_READ) @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) def tileFramesQuadInfo(self, item, params): metadata = self.imageItemModel.getMetadata(item) options = self._parseParams(params, False, [ ('format', str), ('query', str), ('frameBase', str), ('frameStride', str), ('frameGroup', str), ('frameGroupFactor', int), ('frameGroupStride', str), ('maxTextureSize', int), ('maxTextures', int), ('maxTotalTexturePixels', int), ('alignment', int), ('maxFrameSize', int), ]) for key in {'format', 'query'}: if key in options: options[key] = dict(urllib.parse.parse_qsl(options[key])) result = large_image.tilesource.utilities.getTileFramesQuadInfo(metadata, options) if params.get('cache') in {'report', 'schedule'}: needed = [] result['cached'] = [] for src in result['src']: tfParams = self._parseParams(src, False, self._tileFramesParams) if 'frameList' in tfParams: tfParams['frameList'] = [ int(f.strip()) for f in str(tfParams['frameList']).lstrip( '[').rstrip(']').split(',')] result['cached'].append(self.imageItemModel.tileFrames( item, checkAndCreate='check', **tfParams)) if not result['cached'][-1]: needed.append(tfParams) if params.get('cache') == 'schedule' and not all(result['cached']): result['scheduledJob'] = str(self.imageItemModel._scheduleTileFrames( item, needed, self.getCurrentUser())['_id']) return result
[docs] @autoDescribeRoute( Description('List all thumbnail and data files associated with a large_image item.') .modelParam('itemId', model=Item, level=AccessType.READ) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.admin(scope=TokenScope.DATA_READ) def listTilesThumbnails(self, item): return self.imageItemModel.removeThumbnailFiles(item, onlyList=True)
[docs] @autoDescribeRoute( Description('Delete thumbnail and data files associated with a large_image item.') .modelParam('itemId', model=Item, level=AccessType.READ) .param('keep', 'Number of thumbnails to keep. Ignored if a key is ' 'specified.', dataType='integer', required=False, default=10000) .param('key', 'A specific key to delete', required=False) .param('thumbnail', 'If a key is specified, true if the key is a ' 'thumbnail; false if the key is a data record', dataType='boolean', required=False) .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.admin(scope=TokenScope.DATA_WRITE) def deleteTilesThumbnails(self, item, keep, key=None, thumbnail=True): if not key: return self.imageItemModel.removeThumbnailFiles(item, keep=keep or 0) thumbnail = str(thumbnail).lower() != 'false' query = { 'attachedToType': 'item', 'attachedToId': item['_id'], 'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True, 'thumbnailKey': key, } file = File().findOne(query) if file: File().remove(file) return [file]
[docs] @autoDescribeRoute( Description('Associate or replace a thumbnail or data file with a large_image items.') .responseClass('File') .modelParam('itemId', model=Item, level=AccessType.WRITE) .param('key', 'A specific key to delete', required=True) .param('thumbnail', 'If a key is specified, true if the key is a ' 'thumbnail; false if the key is a data record', dataType='boolean', required=False) .param('mimeType', 'The MIME type of the file.', required=False) .param('data', 'An image or data block to associated with the large_image item.', paramType='body', dataType='binary') .consumes('application/octet-stream') .errorResponse('ID was invalid.') .errorResponse('Read access was denied for the item.', 403), ) @access.user(scope=TokenScope.DATA_WRITE) def addTilesThumbnails(self, item, key, mimeType, thumbnail=False, data=None): user = self.getCurrentUser() thumbnail = str(thumbnail).lower() != 'false' query = { 'attachedToType': 'item', 'attachedToId': item['_id'], 'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True, 'thumbnailKey': key, } file = File().findOne(query) if file: File().remove(file) data = cherrypy.request.body.read() try: import magic mimeType = magic.from_buffer(data, mime=True) or mimeType except Exception: pass mimeType = mimeType or 'application/octet-stream' datafile = Upload().uploadFromFile( io.BytesIO(data), size=len(data), name='_largeImageThumbnail', parentType='item', parent=item, user=user, mimeType=mimeType, attachParent=True) datafile.update({ 'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True, 'thumbnailKey': key, }) datafile = File().save(datafile) return datafile