예제 #1
0
 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.")
예제 #2
0
 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
예제 #3
0
 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
예제 #4
0
    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)
예제 #5
0
 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.")
예제 #6
0
    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)
예제 #7
0
 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/.")
예제 #8
0
 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).")
예제 #9
0
 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.")
예제 #10
0
 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
예제 #11
0
 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
예제 #12
0
 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)
예제 #13
0
    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)
예제 #14
0
 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)
예제 #15
0
    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)
예제 #16
0
    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
예제 #17
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)
예제 #18
0
    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
예제 #19
0
 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
예제 #20
0
    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)
예제 #21
0
 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
예제 #22
0
    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
예제 #23
0
    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.")