def do_quality(self): infile = self.tmpfile outfile = self.basename + '.col' # Quality (bit-depth): quality = self.quality_to_apply() if (quality == 'grey' or quality == 'gray'): if (self.shell_call('cat ' + infile + ' | ' + self.ppmtopgm + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from ppmtopgm.") self.tmpfile = outfile elif (quality == 'bitonal'): if (self.shell_call('cat ' + infile + ' | ' + self.ppmtopgm + ' | ' + self.pamditherbw + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from ppmtopgm.") self.tmpfile = outfile elif ((quality == 'native' and self.api_version < '2.0') or (quality == 'default' and self.api_version >= '2.0') or quality == 'color'): self.tmpfile = infile else: raise IIIFError(code=400, parameter='quality', text="Unknown quality parameter value requested.")
def do_rotation(self): infile = self.tmpfile outfile = self.basename + '.rot' # NOTE: pnmrotate: angle must be between -90 and 90 and # rotations is CCW not CW per IIIF spec # # BUG in pnmrotate: +90 and -90 rotations the output image # size may be off. See for example a 1000x1000 image becoming # 1004x1000: # # simeon@RottenApple iiif>file testimages/67352ccc-d1b0-11e1-89ae-279075081939.png # testimages/67352ccc-d1b0-11e1-89ae-279075081939.png: PNG image data, 1000 x 1000, 8-bit/color RGB, non-interlaced # simeon@RottenApple iiif>cat testimages/67352ccc-d1b0-11e1-89ae-279075081939.png | pngtopnm | pnmrotate -90 | pnmtopng > a.png; file a.png; rm a.png # a.png: PNG image data, 1004 x 1000, 8-bit/color RGB, non-interlaced # simeon@RottenApple iiif>cat testimages/67352ccc-d1b0-11e1-89ae-279075081939.png | pngtopnm | pnmrotate 90 | pnmtopng > a.png; file a.png; rm a.png # a.png: PNG image data, 1004 x 1000, 8-bit/color RGB, non-interlaced # # WORKAROUND is to add a pnmscale for the 90degree case, some # simeon@RottenApple iiif>cat testimages/67352ccc-d1b0-11e1-89ae-279075081939.png | pngtopnm | pnmrotate -90| pnmscale -width 1000 -height 1000 | pnmtopng > a.png; file a.png; rm a.png # a.png: PNG image data, 1000 x 1000, 8-bit/color RGB, non-interlaced # (mirror, rot) = self.rotation_to_apply(no_mirror=True) #FIXME - add mirroring if (rot == 0.0): #print "rotation: no rotation" self.tmpfile = infile elif (rot <= 90.0 or rot >= 270.0): if (rot >= 270.0): rot -= 360.0 #print "rotation: by %f degrees clockwise" % (rot) if (self.shell_call('cat ' + infile + ' | ' + self.pnmrotate + ' -background=#FFF ' + str(-rot) + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from pnmrotate.") self.tmpfile = outfile else: # Between 90 and 270 = flip and then -90 to 90 rot -= 180.0 #print "rotation: by %f degrees clockwise" % (rot) if (self.shell_call('cat ' + infile + ' | ' + self.pnmflip + ' -rotate180 | ' + self.pnmrotate + ' ' + str(-rot) + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from pnmrotate.") self.tmpfile = outfile # Fixup size for 90s if (abs(rot % 180.0 - 90.0) < 0.001): outfile2 = self.basename + '.rot2' if (self.shell_call('cat ' + self.tmpfile + ' | ' + self.pnmscale + ' -width ' + str(self.height) + ' -height ' + str(self.width) + ' > ' + outfile2)): raise IIIFError( text="Oops... failed to fixup size after pnmrotate.") self.tmpfile = outfile2
def do_format(self): infile = self.tmpfile outfile = self.basename + '.out' outfile_jp2 = self.basename + '.jp2' # Now convert finished pnm file to output format #simeon@ice ~>cat m3.pnm | pnmtojpeg > m4.jpg #simeon@ice ~>cat m3.pnm | pnmtotiff > m4.jpg #pnmtotiff: computing colormap... #pnmtotiff: Too many colors - proceeding to write a 24-bit RGB file. #pnmtotiff: If you want an 8-bit palette file, try doing a 'ppmquant 256'. #simeon@ice ~>cat m3.pnm | pnmtopng > m4.png fmt = ('png' if (self.request.format is None) else self.request.format) if (fmt == 'png'): #print "format: png" if (self.shell_call(self.pnmtopng + ' ' + infile + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from pnmtopng.") mime_type = "image/png" elif (fmt == 'jpg'): #print "format: jpg" if (self.shell_call(self.pnmtojpeg + ' ' + infile + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from pnmtojpeg.") mime_type = "image/jpeg" elif (fmt == 'tiff' or fmt == 'jp2'): #print "format: tiff/jp2" if (self.shell_call(self.pnmtotiff + ' ' + infile + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from pnmtotiff.") mime_type = "image/tiff" if (fmt == 'jp2'): # use djatoka after tiff if (self.shell_call(DJATOKA_COMP + ' -i ' + outfile + ' -o ' + outfile_jp2)): raise IIIFError( text="Oops... got nonzero output from DJATOKA_COMP.") mime_type = "image/jp2" outfile = tmpfile_jp2 else: raise IIIFError( code=415, parameter='format', text= "Unsupported output file format (%s), only png,jpg,tiff are supported." % (fmt)) self.outfile = outfile self.output_format = fmt self.mime_type = mime_type
def region_to_apply(self): """Return the x,y,w,h parameters to extract given image width and height Assume image width and height are available in self.width and self.height, and self.request is IIIFRequest object Expected use: (x,y,w,h) = self.region_to_apply() if (x is None): # full image else: # extract Returns (None,None,None,None) if no extraction is required. """ if (self.request.region_full or (self.request.region_pct and self.request.region_xywh == (0, 0, 100, 100))): return (None, None, None, None) # Cannot do anything else unless we know size (in self.width and self.height) if (self.width <= 0 or self.height <= 0): raise IIIFError( code=501, parameter='region', text= "Region parameters require knowledge of image size which is not implemented." ) pct = self.request.region_pct (x, y, w, h) = self.request.region_xywh # Convert pct to pixels based on actual size if (pct): x = int((x / 100.0) * self.width + 0.5) y = int((y / 100.0) * self.height + 0.5) w = int((w / 100.0) * self.width + 0.5) h = int((h / 100.0) * self.height + 0.5) # Check if boundary extends beyond image and truncate if ((x + w) > self.width): w = self.width - x if ((y + h) > self.height): h = self.height - y # Final check to see if we have the whole image if (w == 0 or h == 0): raise IIIFError( code=400, parameter='region', text="Region parameters would result in zero size result image." ) if (x == 0 and y == 0 and w == self.width and h == self.height): return (None, None, None, None) return (x, y, w, h)
def do_quality(self): # Quality if (self.api_version >= '2.0'): if (self.quality_to_apply() != "default"): raise IIIFError( code=501, parameter="default", text="Null manipulator supports only quality=default.") else: # versions 1.0 and 1.1 if (self.quality_to_apply() != "native"): raise IIIFError( code=501, parameter="native", text="Null manipulator supports only quality=native.")
def do_format(self): # assume tiling apps want jpg... fmt = ('jpg' if (self.request.format is None) else self.request.format) if (fmt == 'png'): self.logger.info("format: png") self.mime_type = "image/png" self.output_format = fmt format = 'png' elif (fmt == 'jpg'): self.logger.info("format: jpg") self.mime_type = "image/jpeg" self.output_format = fmt format = 'jpeg' else: raise IIIFError( code=415, parameter='format', text= "Unsupported output file format (%s), only png,jpg are supported." % (fmt)) if (self.outfile is None): # Create temp f = tempfile.NamedTemporaryFile(delete=False) self.outfile = f.name self.outtmp = f.name self.image.save(f, format=format) else: # Save to specified location self.image.save(self.outfile, format=format)
def do_region(self): # Region (x, y, w, h) = self.region_to_apply() if (x is not None): raise IIIFError( code=501, parameter="region", text="Null manipulator supports only region=/full/.")
def do_rotation(self): # Rotate (mirror, rot) = self.rotation_to_apply(no_mirror=True) if (rot != 0.0): raise IIIFError( code=501, parameter="rotation", text="Null manipulator supports only rotation=(0|360).")
def do_size(self): # Size # (w,h) = self.size_to_apply() if (self.request.size_pct != 100.0 and self.request.size != 'full'): raise IIIFError( code=501, parameter="size", text= "Null manipulator supports only size=pct:100 and size=full.")
def do_format(self): # Format (the last step) if (self.request.format is not None): raise IIIFError( code=415, parameter="format", text= "Null manipulator does not support specification of output format." ) # if (self.outfile is None): self.outfile = self.srcfile else: try: shutil.copyfile(self.srcfile, self.outfile) except IOError as e: raise IIIFError(code=500, text="Failed to copy file (%s)." % (str(e))) self.mime_type = None
def do_first(self): """Create PIL object from input image file """ self.logger.info("do_first: src=%s" % (self.srcfile)) try: self.image = Image.open(self.srcfile) self.image.load() except Exception as e: raise IIIFError(text=("PIL Image.open(%s) barfed: %s", (self.srcfile, str(e)))) (self.width, self.height) = self.image.size
def _parse_w_comma_h(self, whstr, param): """ Utility to parse "w,h" "w," or ",h" values Returns (w,h) where w,h are either None or ineteger. Will throw a ValueError if there is a problem with one or both. """ try: (wstr, hstr) = string.split(whstr, ',', 2) w = self._parse_non_negative_int(wstr, 'w') h = self._parse_non_negative_int(hstr, 'h') except ValueError as e: raise IIIFError(code=400, parameter=param, text="Illegal %s value (%s)." % (param, str(e))) if (w is None and h is None): raise IIIFError(code=400, parameter=param, text="Must specify at least one of w,h for %s." % (param)) return (w, h)
def rotation_to_apply(self, only90s=False, no_mirror=False): """Check an interpret rotation Returns a truth value as to whether to mirror, and a floating point number 0 <= angle < 360 (degrees). """ rotation = self.request.rotation_deg if (no_mirror and self.request.rotation_mirror): raise IIIFError( code=501, parameter="rotation", text="This implementation does not support mirroring.") if (only90s and (rotation != 0.0 and rotation != 90.0 and rotation != 180.0 and rotation != 270.0)): raise IIIFError( code=501, parameter="rotation", text= "This implementation supports only 0,90,180,270 degree rotations." ) return (self.request.rotation_mirror, rotation)
def do_first(self): pid = os.getpid() self.basename = os.path.join(self.tmpdir, 'iiif_netpbm_' + str(pid)) outfile = self.basename + '.pnm' # Convert source file to pnm filetype = self.file_type(self.srcfile) if (filetype == 'png'): if (self.shell_call(self.pngtopnm + ' ' + self.srcfile + ' > ' + outfile)): raise IIIFError(text="Oops... got error from pngtopnm.") elif (filetype == 'jpg'): if (self.shell_call(self.jpegtopnm + ' ' + self.srcfile + ' > ' + outfile)): raise IIIFError(text="Oops... got error from jpegtopnm.") else: raise IIIFError( code='501', text='bad input file format (only know how to read png/jpeg)') self.tmpfile = outfile # Get size (self.width, self.height) = self.image_size(self.tmpfile)
def split_url(self, url): """ Perform the initial parsing of an IIIF API URL path into components Will parse a URL or URL path that accords with either the parametrized or info API forms. Will raise an IIIFError on failure. """ # clear data first self.clear() # url must start with baseurl if set if (self.baseurl): (path, num) = re.subn('^' + self.baseurl, '', url, 1) if (num != 1): raise ( IIIFError("URL does not match baseurl (server/prefix).")) url = path # Break up by path segments, count to decide format segs = string.split(url, '/', 5) if (len(segs) > 5): raise (IIIFError( code=404, text="Too many path segments in URL (got %d: %s) in URL." % (len(segs), ' | '.join(segs)))) elif (len(segs) == 5): self.identifier = urllib.unquote(segs[0]) self.region = urllib.unquote(segs[1]) self.size = urllib.unquote(segs[2]) self.rotation = urllib.unquote(segs[3]) self.quality = self.strip_format(urllib.unquote(segs[4])) self.info = False elif (len(segs) == 2): self.identifier = urllib.unquote(segs[0]) info_name = self.strip_format(urllib.unquote(segs[1])) if (info_name != "info"): raise (IIIFError( code=400, text= "Badly formed information request, must be info.json or info.xml" )) if (self.api_version == '1.0'): if (self.format not in ['json', 'xml']): raise (IIIFError( code=400, text= "Bad information request format, must be json or xml")) elif (self.format != 'json'): raise (IIIFError( code=400, text="Bad information request format, must be json")) self.info = True elif (len(segs) == 1): self.identifier = urllib.unquote(segs[0]) raise (IIIFRequestBaseURI()) else: raise (IIIFError( code=400, text="Bad number of path segments (%d: %s) in URL." % (len(segs), ' | '.join(segs)))) return (self)
def parse_rotation(self, rotation=None): """ Check and interpret rotation Uses value of self.rotation at starting point unless rotation parameter is specified in the call. Sets self.rotation_deg to a floating point number 0 <= angle < 360. Includes translation of 360 to 0. If there is a prefix bang (!) then self.rotation_mirror will be set True, otherwise it will be False. """ if (rotation is not None): self.rotation = rotation self.rotation_deg = 0.0 self.rotation_mirror = False if (self.rotation is None): return # Look for ! prefix first if (self.rotation[0] == '!'): self.rotation_mirror = True self.rotation = self.rotation[1:] # Interpret value now try: self.rotation_deg = float(self.rotation) except ValueError: raise IIIFError( code=400, parameter="rotation", text="Bad rotation value, must be a number, got '%s'." % (self.rotation)) if (self.rotation_deg < 0.0 or self.rotation_deg > 360.0): raise IIIFError( code=400, parameter="rotation", text= "Illegal rotation value, must be 0 <= rotation <= 360, got %f." % (self.rotation_deg)) elif (self.rotation_deg == 360.0): # The spec admits 360 as valid, but change to 0 self.rotation_deg = 0.0
def size_to_apply(self): """Calculate size of image scaled using size parameters Assumes current image width and height are available in self.width and self.height, and self.request is IIIFRequest object Formats are: w, ,h w,h pct:p !w,h Returns (None,None) if no scaling is required. """ if (self.request.size_full): return (None, None) elif (self.request.size_pct is not None): w = int(self.width * self.request.size_pct / 100.0 + 0.5) h = int(self.height * self.request.size_pct / 100.0 + 0.5) elif (self.request.size_bang): # Have "!w,h" form (mw, mh) = self.request.size_wh # Pick smaller fraction and then work from that... frac = min((float(mw) / float(self.width)), (float(mh) / float(self.height))) #print "size=!w,h: mw=%d mh=%d -> frac=%f" % (mw,mh,frac) # FIXME - could put in some other function here like factors of two, but # FIXME - for now just pick largest image within requested dimensions w = int(self.width * frac + 0.5) h = int(self.height * frac + 0.5) else: # Must now be "w,h", "w," or ",h". If both are specified then this will the size, # otherwise find other to keep aspect ratio (w, h) = self.request.size_wh if (w is None): w = int(self.width * h / self.height + 0.5) elif (h is None): h = int(self.height * w / self.width + 0.5) # Now have w,h, sanity check and return if (w == 0 or h == 0): raise IIIFError( code=400, parameter='size', text= "Size parameter would result in zero size result image (%d,%d)." % (w, h)) # Below would be test for scaling up image size, this is allowed by spec # if ( w>self.width or h>self.height ): # raise IIIFError(code=400,parameter='size', # text="Size requests scaling up image to larger than orginal.") if (w == self.width and h == self.height): return (None, None) return (w, h)
def parse_quality(self): """ Check quality paramater Sets self.quality_val based on simple substitution of 'native' for default. Checks for the three valid values else throws and IIIFError. """ if (self.quality is None): self.quality_val = self.default_quality elif (self.quality not in self.allowed_qualities): raise IIIFError( code=400, parameter="quality", text="The quality parameter must be '%s', got '%s'." % ("', '".join(self.allowed_qualities), self.quality)) else: self.quality_val = self.quality
def do_region(self): infile = self.tmpfile outfile = self.basename + '.reg' # Region #simeon@ice ~>cat m.pnm | pnmcut 10 10 100 200 > m1.pnm (x, y, w, h) = self.region_to_apply() if (x is None): #print "region: full" self.tmpfile = infile else: #print "region: (%d,%d,%d,%d)" % (x,y,w,h) if (self.shell_call('cat ' + infile + ' | ' + self.pnmcut + ' ' + str(x) + ' ' + str(y) + ' ' + str(w) + ' ' + str(h) + ' > ' + outfile)): raise IIIFError(text="Oops... got nonzero output from pnmcut.") self.width = w self.height = h self.tmpfile = outfile
def image_size(self, pnmfile): """Get width and height of pnm file simeon@homebox src>pnmfile /tmp/214-2.png /tmp/214-2.png:PPM raw, 100 by 100 maxval 255 """ pout = os.popen(self.shellsetup + self.pnmfile + ' ' + pnmfile, 'r') pnmfileout = pout.read(200) pout.close() m = re.search(', (\d+) by (\d+) ', pnmfileout) if (m is None): raise IIIFError( text="Bad output from pnmfile when trying to get size.") w = int(m.group(1)) h = int(m.group(2)) #print "pnmfile output = %s" % (pnmfileout) #print "image size = %d,%d" % (w,h) return (w, h)
def do_size(self): # Size # simeon@ice ~>cat m1.pnm | pnmscale -width 50 > m2.pnm infile = self.tmpfile outfile = self.basename + '.siz' (w, h) = self.size_to_apply() if (w is None): #print "size: no scaling" self.tmpfile = infile else: #print "size: scaling to (%d,%d)" % (w,h) if (self.shell_call('cat ' + infile + ' | ' + self.pnmscale + ' -width ' + str(w) + ' -height ' + str(h) + ' > ' + outfile)): raise IIIFError( text="Oops... got nonzero output from pnmscale.") self.width = w self.height = h self.tmpfile = outfile
def parse_region(self): """ Parse the region component of the path /full/ -> self.region_full = True (test this first) /x,y,w,h/ -> self.region_xywh = (x,y,w,h) /pct:x,y,w,h/ -> self.region_xywh and self.region_pct = True Will throw errors if the paremeters are illegal according to the specification but does not know about and thus cannot do any tests against any image being manipulated. """ self.region_full = False self.region_pct = False if (self.region is None or self.region == 'full'): self.region_full = True return xywh = self.region pct_match = re.match('pct:(.*)$', self.region) if (pct_match): xywh = pct_match.group(1) self.region_pct = True # Now whether this was pct: or now, we expect 4 values... str_values = string.split(xywh, ',', 5) if (len(str_values) != 4): raise IIIFError( code=400, parameter="region", text= "Bad number of values in region specification, must be x,y,w,h but got %d value(s) from '%s'" % (len(str_values), xywh)) values = [] for str_value in str_values: # Must be either integer (not pct) or interger/float (pct) if (pct_match): try: # This is rather more permissive that the iiif spec value = float(str_value) except ValueError: raise IIIFError( code=400, parameter="region", text= "Bad floating point value for percentage in region (%s)." % str_value) if (value > 100.0): raise IIIFError( code=400, parameter="region", text="Percentage over value over 100.0 in region (%s)." % str_value) else: try: value = int(str_value) except ValueError: raise IIIFError(code=400, parameter="region", text="Bad integer value in region (%s)." % str_value) if (value < 0): raise IIIFError( code=400, parameter="region", text="Negative values not allowed in region (%s)." % str_value) values.append(value) # Zero size region is w or h are zero (careful that they may be float) if (values[2] == 0.0 or values[3] == 0.0): raise IIIFError(code=400, parameter="region", text="Zero size region specified (%s))." % xywh) self.region_xywh = values
def parse_size(self, size=None): """Parse the size component of the path /full/ -> self.size_full = True /w,/ -> self.size_wh = (w,None) /,h/ -> self.size_wh = (None,h) /w,h/ -> self.size_wh = (w,h) /pct:p/ -> self.size_pct = p /!w,h/ -> self.size_wh = (w,h), self.size_bang = True Expected use: (w,h) = iiif.size_to_apply(region_w,region_h) if (q is None): # full image else: # scale to w by h Returns (None,None) if no scaling is required. """ if (size is not None): self.size = size self.size_pct = None self.size_bang = False self.size_full = False self.size_wh = (None, None) if (self.size is None or self.size == 'full'): self.size_full = True return pct_match = re.match('pct:(.*)$', self.size) if (pct_match is not None): pct_str = pct_match.group(1) try: self.size_pct = float(pct_str) except ValueError: raise IIIFError( code=400, parameter="size", text="Percentage size value must be a number, got '%s'." % (pct_str)) # FIXME - current spec places no upper limit on size # if (self.size_pct<0.0 or self.size_pct>100.0): # raise IIIFError(code=400,parameter="size", # text="Illegal percentage size, must be 0 <= pct <= 100.") if (self.size_pct < 0.0): raise IIIFError( code=400, parameter="size", text="Base size percentage, must be > 0.0, got %f." % (self.size_pct)) else: if (self.size[0] == '!'): # Have "!w,h" form size_no_bang = self.size[1:] (mw, mh) = self._parse_w_comma_h(size_no_bang, 'size') if (mw is None or mh is None): raise IIIFError( code=400, parameter="size", text= "Illegal size requested: both w,h must be specified in !w,h requests." ) self.size_wh = (mw, mh) self.size_bang = True else: # Must now be "w,h", "w," or ",h" self.size_wh = self._parse_w_comma_h(self.size, 'size') # Sanity check w,h (w, h) = self.size_wh if ((w is not None and w <= 0) or (h is not None and h <= 0)): raise IIIFError( code=400, parameter='size', text="Size parameters request zero size result image.")