def test02_derive(self): m = IIIFManipulator() r = IIIFRequest() r.parse_url('id1/full/full/0/default') tmp = tempfile.mkdtemp() outfile = os.path.join(tmp,'testout.png') try: m.derive(srcfile='testimages/test1.png', request=r, outfile=outfile); self.assertTrue( os.path.isfile(outfile) ) self.assertEqual( os.path.getsize(outfile), 65810 ) finally: shutil.rmtree(tmp) # and where path to outfile must be created outfile = os.path.join(tmp,'a','b','testout.png') try: m.derive(srcfile='testimages/test1.png', request=r, outfile=outfile); self.assertTrue( os.path.isfile(outfile) ) self.assertEqual( os.path.getsize(outfile), 65810 ) finally: shutil.rmtree(tmp)
def test02_derive(self): """Test derive method by running complete conversions.""" m = IIIFManipulator() r = IIIFRequest() r.parse_url('id1/full/full/0/default') tmp = tempfile.mkdtemp() outfile = os.path.join(tmp, 'testout.png') try: m.derive(srcfile='testimages/test1.png', request=r, outfile=outfile) self.assertTrue(os.path.isfile(outfile)) self.assertEqual(os.path.getsize(outfile), 65810) finally: shutil.rmtree(tmp) # and where path to outfile must be created outfile = os.path.join(tmp, 'a', 'b', 'testout.png') try: m.derive(srcfile='testimages/test1.png', request=r, outfile=outfile) self.assertTrue(os.path.isfile(outfile)) self.assertEqual(os.path.getsize(outfile), 65810) finally: shutil.rmtree(tmp)
class IIIFHandler(object): """IIIFHandler class.""" def __init__(self, prefix, identifier, config, klass, auth): """Initialize IIIFHandler setting key configurations. Positional parameters: prefix -- URI prefix (without leading or trailing slashes) identifier -- identifier of image config -- instance of Config class klass -- IIIFManipulator sub-class to do manipulations auth -- IIIFAuth sub-class for auth """ self.prefix = prefix self.identifier = identifier self.config = config self.klass = klass self.api_version = config.api_version self.auth = auth self.degraded = False self.logger = logging.getLogger('IIIFHandler') # # Create objects to process request self.iiif = IIIFRequest(api_version=self.api_version, identifier=self.identifier) self.manipulator = klass(api_version=self.api_version) # # Set up auth object with locations if not already done if (self.auth and not self.auth.login_uri): self.auth.login_uri = self.server_and_prefix + '/login' if (self.auth.logout_handler is not None): self.auth.logout_uri = self.server_and_prefix + '/logout' self.auth.access_token_uri = self.server_and_prefix + '/token' # # Response headers # -- All responses should have CORS header self.headers = {'Access-control-allow-origin': '*'} @property def server_and_prefix(self): """Server and prefix from config.""" return (host_port_prefix(self.config.host, self.config.port, self.prefix)) @property def json_mime_type(self): """Return the MIME type for a JSON response. For version 2.0+ the server must return json-ld MIME type if that format is requested. Implement for 1.1 also. http://iiif.io/api/image/2.1/#information-request """ mime_type = "application/json" if (self.api_version >= '1.1' and 'Accept' in request.headers): mime_type = do_conneg(request.headers['Accept'], ['application/ld+json']) or mime_type return mime_type @property def file(self): """Filename property for the source image for the current identifier.""" file = None if (self.config.klass_name == 'gen'): for ext in ['.py']: file = os.path.join(self.config.generator_dir, self.identifier + ext) if (os.path.isfile(file)): return file else: for ext in ['.jpg', '.png', '.tif']: file = os.path.join(self.config.image_dir, self.identifier + ext) if (os.path.isfile(file)): return file # failed, show list of available identifiers as error available = "\n ".join(identifiers(self.config)) raise IIIFError(code=404, parameter="identifier", text="Image resource '" + self.identifier + "' not found. Local resources available:" + available + "\n") def add_compliance_header(self): """Add IIIF Compliance level header to response.""" if (self.manipulator.compliance_uri is not None): self.headers['Link'] = '<' + \ self.manipulator.compliance_uri + '>;rel="profile"' def make_response(self, content, code=200, headers=None): """Wrapper around Flask.make_response which also adds any local headers.""" if headers: for header in headers: self.headers[header] = headers[header] return make_response(content, code, self.headers) def image_information_response(self): """Parse image information request and create response.""" dr = degraded_request(self.identifier) if (dr): self.logger.info("image_information: degraded %s -> %s" % (self.identifier, dr)) self.degraded = self.identifier self.identifier = dr else: self.logger.info("image_information: %s" % (self.identifier)) # get size self.manipulator.srcfile = self.file self.manipulator.do_first() # most of info.json comes from config, a few things specific to image info = { 'tile_height': self.config.tile_height, 'tile_width': self.config.tile_width, 'scale_factors': self.config.scale_factors } # calculate scale factors if not hard-coded if ('auto' in self.config.scale_factors): info['scale_factors'] = self.manipulator.scale_factors( self.config.tile_width, self.config.tile_height) i = IIIFInfo(conf=info, api_version=self.api_version) i.server_and_prefix = self.server_and_prefix i.identifier = self.iiif.identifier i.width = self.manipulator.width i.height = self.manipulator.height if (self.api_version >= '2.0'): # FIXME - should come from manipulator i.qualities = ["default", "color", "gray"] else: # FIXME - should come from manipulator i.qualities = ["native", "color", "gray"] i.formats = ["jpg", "png"] # FIXME - should come from manipulator if (self.auth): self.auth.add_services(i) return self.make_response( i.as_json(), headers={"Content-Type": self.json_mime_type}) def image_request_response(self, path): """Parse image request and create response.""" # Parse the request in path if (len(path) > 1024): raise IIIFError(code=414, text="URI Too Long: Max 1024 chars, got %d\n" % len(path)) # print "GET " + path try: self.iiif.identifier = self.identifier self.iiif.parse_url(path) except IIIFRequestPathError as e: # Reraise as IIIFError with code=404 because we can't tell # whether there was an encoded slash in the identifier or # whether there was a bad number of path segments. raise IIIFError(code=404, text=e.text) except IIIFRequestBaseURI as e: info_uri = self.server_and_prefix + '/' + \ urlquote(self.iiif.identifier) + '/info.json' raise IIIFError(code=303, headers={'Location': info_uri}) except IIIFError as e: # Pass through raise e except Exception as e: # Something completely unexpected => 500 raise IIIFError( code=500, text= "Internal Server Error: unexpected exception parsing request (" + str(e) + ")") dr = degraded_request(self.identifier) if (dr): self.logger.info("image_request: degraded %s -> %s" % (self.identifier, dr)) self.degraded = self.identifier self.identifier = dr self.iiif.quality = 'gray' else: # Parsed request OK, attempt to fulfill self.logger.info("image_request: %s" % (self.identifier)) file = self.file self.manipulator.srcfile = file self.manipulator.do_first() if (self.api_version < '2.0' and self.iiif.format is None and 'Accept' in request.headers): # In 1.0 and 1.1 conneg was specified as an alternative to format, see: # http://iiif.io/api/image/1.0/#format # http://iiif.io/api/image/1.1/#parameters-format formats = { 'image/jpeg': 'jpg', 'image/tiff': 'tif', 'image/png': 'png', 'image/gif': 'gif', 'image/jp2': 'jps', 'application/pdf': 'pdf' } accept = do_conneg(request.headers['Accept'], list(formats.keys())) # Ignore Accept header if not recognized, should this be an error # instead? if (accept in formats): self.iiif.format = formats[accept] (outfile, mime_type) = self.manipulator.derive(file, self.iiif) # FIXME - find efficient way to serve file with headers self.add_compliance_header() return send_file(outfile, mimetype=mime_type) def error_response(self, e): """Make response for an IIIFError e. Also add compliance header. """ self.add_compliance_header() return self.make_response(*e.image_server_response(self.api_version))
def test03_is_scaled_full_image(self): """Test check for whether request is scaled full image.""" r = IIIFRequest(identifier='dummy', api_version='2.1') r.parse_url('full/full/0/default.jpg') self.assertFalse(r.is_scaled_full_image()) r.parse_url('full/!10,10/0/default.jpg') self.assertFalse(r.is_scaled_full_image()) r.parse_url('10,10,90,90/10,10/0/default.jpg') self.assertFalse(r.is_scaled_full_image()) r.parse_url('full/10,10/1/default.jpg') self.assertFalse(r.is_scaled_full_image()) r.parse_url('full/10,10/0/default.png') self.assertFalse(r.is_scaled_full_image()) r.parse_url('full/10,10/0/color.jpg') self.assertFalse(r.is_scaled_full_image()) # r = IIIFRequest(identifier='dummy', api_version='2.1') r.parse_url('full/10,10/0/default.jpg') self.assertTrue(r.is_scaled_full_image())
class IIIFHandler(object): """IIIFHandler class.""" def __init__(self, prefix, identifier, config, klass, auth): """Initialize IIIFHandler setting key configurations. Positional parameters: prefix -- URI prefix (without leading or trailing slashes) identifier -- identifier of image config -- instance of Config class klass -- IIIFManipulator sub-class to do manipulations auth -- IIIFAuth sub-class instance for auth or None """ self.prefix = prefix self.identifier = identifier self.config = config self.klass = klass self.api_version = config.api_version self.auth = auth self.degraded = False self.logger = logging.getLogger('IIIFHandler') # # Create objects to process request self.iiif = IIIFRequest(api_version=self.api_version, identifier=self.identifier) self.manipulator = klass(api_version=self.api_version) # # Set up auth object with locations if not already done if (self.auth and not self.auth.login_uri): self.auth.login_uri = self.server_and_prefix + '/login' if (self.auth.logout_handler is not None): self.auth.logout_uri = self.server_and_prefix + '/logout' self.auth.access_token_uri = self.server_and_prefix + '/token' # # Response headers # -- All responses should have CORS header self.headers = {'Access-control-allow-origin': '*'} @property def server_and_prefix(self): """Server and prefix from config.""" return(host_port_prefix(self.config.host, self.config.port, self.prefix)) @property def json_mime_type(self): """Return the MIME type for a JSON response. For version 2.0+ the server must return json-ld MIME type if that format is requested. Implement for 1.1 also. http://iiif.io/api/image/2.1/#information-request """ mime_type = "application/json" if (self.api_version >= '1.1' and 'Accept' in request.headers): mime_type = do_conneg(request.headers['Accept'], [ 'application/ld+json']) or mime_type return mime_type @property def file(self): """Filename property for the source image for the current identifier.""" file = None if (self.config.klass_name == 'gen'): for ext in ['.py']: file = os.path.join( self.config.generator_dir, self.identifier + ext) if (os.path.isfile(file)): return file else: for ext in ['.jpg', '.png', '.tif']: file = os.path.join(self.config.image_dir, self.identifier + ext) if (os.path.isfile(file)): return file # failed, show list of available identifiers as error available = "\n ".join(identifiers(self.config)) raise IIIFError(code=404, parameter="identifier", text="Image resource '" + self.identifier + "' not found. Local resources available:" + available + "\n") def add_compliance_header(self): """Add IIIF Compliance level header to response.""" if (self.manipulator.compliance_uri is not None): self.headers['Link'] = '<' + \ self.manipulator.compliance_uri + '>;rel="profile"' def make_response(self, content, code=200, headers=None): """Wrapper around Flask.make_response which also adds any local headers.""" if headers: for header in headers: self.headers[header] = headers[header] return make_response(content, code, self.headers) def image_information_response(self): """Parse image information request and create response.""" dr = degraded_request(self.identifier) if (dr): self.logger.info("image_information: degraded %s -> %s" % (self.identifier, dr)) self.degraded = self.identifier self.identifier = dr else: self.logger.info("image_information: %s" % (self.identifier)) # get size self.manipulator.srcfile = self.file self.manipulator.do_first() # most of info.json comes from config, a few things specific to image info = {'tile_height': self.config.tile_height, 'tile_width': self.config.tile_width, 'scale_factors': self.config.scale_factors } # calculate scale factors if not hard-coded if ('auto' in self.config.scale_factors): info['scale_factors'] = self.manipulator.scale_factors( self.config.tile_width, self.config.tile_height) i = IIIFInfo(conf=info, api_version=self.api_version) i.server_and_prefix = self.server_and_prefix i.identifier = self.iiif.identifier i.width = self.manipulator.width i.height = self.manipulator.height if (self.api_version >= '2.0'): # FIXME - should come from manipulator i.qualities = ["default", "color", "gray"] else: # FIXME - should come from manipulator i.qualities = ["native", "color", "gray"] i.formats = ["jpg", "png"] # FIXME - should come from manipulator if (self.auth): self.auth.add_services(i) return self.make_response(i.as_json(), headers={"Content-Type": self.json_mime_type}) def image_request_response(self, path): """Parse image request and create response.""" # Parse the request in path if (len(path) > 1024): raise IIIFError(code=414, text="URI Too Long: Max 1024 chars, got %d\n" % len(path)) try: self.iiif.identifier = self.identifier self.iiif.parse_url(path) except IIIFRequestPathError as e: # Reraise as IIIFError with code=404 because we can't tell # whether there was an encoded slash in the identifier or # whether there was a bad number of path segments. raise IIIFError(code=404, text=e.text) except IIIFError as e: # Pass through raise e except Exception as e: # Something completely unexpected => 500 raise IIIFError(code=500, text="Internal Server Error: unexpected exception parsing request (" + str(e) + ")") dr = degraded_request(self.identifier) if (dr): self.logger.info("image_request: degraded %s -> %s" % (self.identifier, dr)) self.degraded = self.identifier self.identifier = dr self.iiif.quality = 'gray' else: # Parsed request OK, attempt to fulfill self.logger.info("image_request: %s" % (self.identifier)) file = self.file self.manipulator.srcfile = file self.manipulator.do_first() if (self.api_version < '2.0' and self.iiif.format is None and 'Accept' in request.headers): # In 1.0 and 1.1 conneg was specified as an alternative to format, see: # http://iiif.io/api/image/1.0/#format # http://iiif.io/api/image/1.1/#parameters-format formats = {'image/jpeg': 'jpg', 'image/tiff': 'tif', 'image/png': 'png', 'image/gif': 'gif', 'image/jp2': 'jps', 'application/pdf': 'pdf'} accept = do_conneg(request.headers['Accept'], list(formats.keys())) # Ignore Accept header if not recognized, should this be an error # instead? if (accept in formats): self.iiif.format = formats[accept] (outfile, mime_type) = self.manipulator.derive(file, self.iiif) # FIXME - find efficient way to serve file with headers self.add_compliance_header() return send_file(outfile, mimetype=mime_type) def error_response(self, e): """Make response for an IIIFError e. Also add compliance header. """ self.add_compliance_header() return self.make_response(*e.image_server_response(self.api_version))