Пример #1
0
    def from_image_file(uri, src_img_fp, src_format, formats=[]):
        '''
        Args:
            ident (str): The URI for the image.
            src_img_fp (str): The absolute path to the image.
            src_format (str): The format of the image as a three-char str.
            formats ([str]): The derivative formats the application can produce.
        '''
        # Assumes that the image exists and the format is supported. Exceptions
        # should be raised by the resolver if that's not the case.
        new_inst = ImageInfo()
        new_inst.ident = uri
        new_inst.src_img_fp = src_img_fp
        new_inst.tiles = []
        new_inst.sizes = None
        new_inst.scaleFactors = None
        local_profile = {'formats' : formats, 'supports' : OPTIONAL_FEATURES}
        new_inst.profile = [ COMPLIANCE, local_profile ]

        logger.debug('Source Format: %s' % (src_format,))
        logger.debug('Source File Path: %s' % (new_inst.src_img_fp,))
        logger.debug('Identifier: %s' % (new_inst.ident,))

        if src_format == 'jp2':
            new_inst._from_jp2(src_img_fp)
        elif src_format  in ('jpg','tif','png'):
            new_inst._extract_with_pillow(src_img_fp)
        else:
            m = 'Didn\'t get a source format, or at least one we recognize ("%s")' % src_format
            raise ImageInfoException(http_status=500, message=m)

        return new_inst
Пример #2
0
    def _from_jp2(self, fp):
        '''Get info about a JP2.
        '''
        logger.debug('Extracting info from JP2 file.')
        self.profile[1]['qualities'] = ['default', 'bitonal']

        scaleFactors = []

        #TODO use context manager so it's automatically closed
        jp2 = open(fp, 'rb')

        #check that this is a jp2 file
        initial_bytes = jp2.read(24)
        if (not initial_bytes[:12] == '\x00\x00\x00\x0cjP  \r\n\x87\n') or \
            (not initial_bytes[16:] == 'ftypjp2 '):
            jp2.close()
            raise ImageInfoException(http_status=500,
                                     message='Invalid JP2 file')

        #grab width and height
        window = deque([], 4)
        b = jp2.read(1)
        while ''.join(window) != 'ihdr':
            b = jp2.read(1)
            c = struct.unpack('c', b)[0]
            window.append(c)
        self.height = int(struct.unpack(">I",
                                        jp2.read(4))[0])  # height (pg. 136)
        self.width = int(struct.unpack(">I", jp2.read(4))[0])  # width
        logger.debug("width: " + str(self.width))
        logger.debug("height: " + str(self.height))

        # Figure out color or grayscale.
        # Depending color profiles; there's probably a better way (or more than
        # one, anyway.)
        # see: JP2 I.5.3.3 Colour Specification box
        while ''.join(window) != 'colr':
            b = jp2.read(1)
            c = struct.unpack('c', b)[0]
            window.append(c)

        colr_meth = struct.unpack('B', jp2.read(1))[0]
        logger.debug('colr METH: %d' % (colr_meth, ))

        # PREC and APPROX, 1 byte each
        colr_prec = struct.unpack('b', jp2.read(1))[0]
        colr_approx = struct.unpack('B', jp2.read(1))[0]
        logger.debug('colr PREC: %d' % (colr_prec))
        logger.debug('colr APPROX: %d' % (colr_approx))

        if colr_meth == 1:  # Enumerated Colourspace
            self.color_profile_bytes = None
            enum_cs = int(struct.unpack(">HH", jp2.read(4))[1])
            logger.debug('Image contains an enumerated colourspace: %d' %
                         (enum_cs, ))
            logger.debug('Enumerated colourspace: %d' % (enum_cs))
            if enum_cs == 16:  # sRGB
                self.profile[1]['qualities'] += ['gray', 'color']
            elif enum_cs == 17:  # grayscale
                self.profile[1]['qualities'] += ['gray']
            elif enum_cs == 18:  # sYCC
                pass
            else:
                msg = 'Enumerated colourspace is neither "16", "17", or "18". '
                msg += 'See jp2 spec pg. 139.'
                logger.warn(msg)
        elif colr_meth == 2:
            # (Restricted ICC profile).
            logger.debug(
                'Image contains a restricted, embedded colour profile')
            # see http://www.color.org/icc-1_1998-09.pdf, page 18.
            self.assign_color_profile(jp2)
        else:
            logger.warn(
                'colr METH is neither "1" or "2". See jp2 spec pg. 139.')

            # colr METH 3 = Any ICC method, colr METH 4 = Vendor Colour method
            # See jp2 spec pg. 182 -  Table M.24 (Color spec box legal values)
            if colr_meth <= 4 and -128 <= colr_prec <= 127 and 1 <= colr_approx <= 4:
                self.assign_color_profile(jp2)

        logger.debug('qualities: ' + str(self.profile[1]['qualities']))

        window = deque(jp2.read(2), 2)
        while map(ord, window) != [0xFF, 0x4F]:  # (SOC - required, see pg 14)
            window.append(jp2.read(1))
        while map(ord, window) != [0xFF, 0x51]:  # (SIZ  - required, see pg 14)
            window.append(jp2.read(1))
        jp2.read(
            20
        )  # through Lsiz (16), Rsiz (16), Xsiz (32), Ysiz (32), XOsiz (32), YOsiz (32)
        tile_width = int(struct.unpack(">I", jp2.read(4))[0])  # XTsiz (32)
        tile_height = int(struct.unpack(">I", jp2.read(4))[0])  # YTsiz (32)
        logger.debug("tile width: " + str(tile_width))
        logger.debug("tile height: " + str(tile_height))
        self.tiles.append({'width': tile_width})
        if tile_width != tile_height:
            self.tiles[0]['height'] = tile_height
        jp2.read(10)  # XTOsiz (32), YTOsiz (32), Csiz (16)

        window = deque(jp2.read(2), 2)
        # while (ord(b) != 0xFF): b = jp2.read(1)
        # b = jp2.read(1) # 0x52: The COD marker segment
        while map(ord, window) != [0xFF, 0x52]:  # (COD - required, see pg 14)
            window.append(jp2.read(1))

        jp2.read(7)  # through Lcod (16), Scod (8), SGcod (32)
        levels = int(struct.unpack(">B", jp2.read(1))[0])
        logger.debug("levels: " + str(levels))
        scaleFactors = [pow(2, l) for l in range(0, levels + 1)]
        self.tiles[0]['scaleFactors'] = scaleFactors
        jp2.read(4)  # through code block stuff

        # We may have precincts if Scod or Scoc = xxxx xxx0
        # But we don't need to examine as this is the last variable in the
        # COD segment. Instead check if the next byte == 0xFF. If it is,
        # we don't have a Precint size parameter and we've moved on to either
        # the COC (optional, marker = 0xFF53) or the QCD (required,
        # marker = 0xFF5C)
        b = jp2.read(1)
        if ord(b) != 0xFF:
            if self.tiles[0]['width'] == self.width \
                and self.tiles[0].get('height') in (self.height, None):
                # Clear what we got above in SIZ and prefer this. This could
                # technically break as it's possible to have precincts inside tiles.
                # Let's wait for that to come up....
                self.tiles = []

                for level in range(levels + 1):
                    i = int(bin(struct.unpack(">B", b)[0])[2:].zfill(8), 2)
                    x = i & 15
                    y = i >> 4
                    w = 2**x
                    h = 2**y
                    b = jp2.read(1)
                    try:
                        entry = next(
                            (i for i in self.tiles if i['width'] == w))
                        entry['scaleFactors'].append(pow(2, level))
                    except StopIteration:
                        self.tiles.append({
                            'width': w,
                            'scaleFactors': [pow(2, level)]
                        })

        jp2.close()

        self.sizes = []
        [
            self.sizes.append({
                'width': w,
                'height': h
            }) for w, h in self.sizes_for_scales(scaleFactors)
        ]
        self.sizes.sort(key=lambda size: max([size['width'], size['height']]))