Skip to content

Commit

Permalink
Merge pull request #78 from pulibrary/jp2_precinct_support
Browse files Browse the repository at this point in the history
Adds jp2 precinct support and iiif_img_info command line tool.
  • Loading branch information
Jon Stroop committed May 26, 2014
2 parents 8047a8a + de54ae2 commit 98da082
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ rmdirs.sh
files.txt

big.jp2

xb526qv4524_05_0001.jp2
74 changes: 74 additions & 0 deletions bin/iiif_img_info
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python
#-*-coding:utf-8-*-

# iiif_img_info
#
# Command line tool for extracting dumping iiif info.json to stdout.
#
# Syntax: $ iiif_img_info [-v] path/to/an/image.fmt
# -v will show loris's debug logging.
#
# This tool does not report a profile or formats available as they are
# depend on the server. The file extension must be a conventional image format
# extension, e.g.: jpg, jp2, tif
#

from json import dumps
from os.path import dirname
from os.path import exists
from os.path import isfile
from os.path import join
from os.path import realpath
from os.path import splitext
from sys import argv
from sys import exit
from sys import stderr
from sys import stdout
from urllib import pathname2url
from urlparse import urljoin

try:
# Use the version on the system if it's there
from loris.img_info import ImageInfo
except ImportError:
# Otherwise try from the source
loris_proj_dp = dirname(dirname(realpath(__file__)))
from sys import path
path.append(loris_proj_dp)
from loris.img_info import ImageInfo

EX_USAGE = 64
EX_DATAERR = 65
EX_NOINPUT = 66
VERBOSE = '-v'


if VERBOSE in argv:
import logging
logging.basicConfig(level=logging.DEBUG)
argv.remove(VERBOSE)

try:
fp = realpath(argv[1])
except IndexError:
stderr.write("\nPlease supply the path to an image file.\n\n")
exit(EX_USAGE)

if not exists(fp):
stderr.write("\n%s does not exist.\n\n" % (fp,))
exit(EX_NOINPUT)

fmt = splitext(fp)[1][1:]
uri = urljoin('file:', pathname2url(fp))

try:
info = ImageInfo.from_image_file(uri, fp, fmt)
except Exception as e:
stderr.write("\n%s\n\n" % (e,))
exit(EX_DATAERR)

info_dict = info.to_dict()
del info_dict['profile']
del info_dict['formats']

stdout.write("%s\n" % (dumps(info_dict, sort_keys=True, indent=4),))
63 changes: 54 additions & 9 deletions loris/img_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class ImageInfo(object):
'src_img_fp', 'color_profile_bytes')

@staticmethod
def from_image_file(ident, uri, src_img_fp, src_format, formats=[]):
def from_image_file(uri, src_img_fp, src_format, formats=[]):
'''
Args:
ident (str): The URI for the image.
Expand Down Expand Up @@ -153,6 +153,8 @@ def __from_jp2(self, fp):
'''
logger.debug('Extracting info from JP2 file.')
self.qualities = ['native', 'bitonal']
scod_is_0 = True
scoc_is_0 = True # TODO.

jp2 = open(fp, 'rb')
b = jp2.read(1)
Expand Down Expand Up @@ -210,7 +212,7 @@ def __from_jp2(self, fp):

b = jp2.read(1)
while (ord(b) != 0xFF): b = jp2.read(1)
b = jp2.read(1) #skip over the SOC, 0x4F
b = jp2.read(1) #skip over the SOC and 0x4F

while (ord(b) != 0xFF): b = jp2.read(1)
b = jp2.read(1) # 0x51: The SIZ marker segment
Expand All @@ -221,22 +223,57 @@ def __from_jp2(self, fp):
self.tile_width = int(struct.unpack(">I", jp2.read(4))[0]) # XTsiz (32)
self.tile_height = int(struct.unpack(">I", jp2.read(4))[0]) # YTsiz (32)
logger.debug("tile width: " + str(self.tile_width))
logger.debug("tile height: " + str(self.tile_height))
logger.debug("tile height: " + str(self.tile_height))
jp2.read(4) # XTOsiz (32)
jp2.read(4) # YTOsiz (32)
csiz = struct.unpack(">h", jp2.read(2)) # may need this later

while (ord(b) != 0xFF): b = jp2.read(1)
b = jp2.read(1) # 0x52: The COD marker segment
if (ord(b) == 0x52):
jp2.read(7) # through Lcod, Scod, SGcod (16 + 8 + 32 = 56 bits)
jp2.read(2) # through Lcod (16)
jp2.read(1) # Scod (8)
jp2.read(4) # SGcod (32)
levels = int(struct.unpack(">B", jp2.read(1))[0])
logger.debug("levels: " + str(levels))
self.scale_factors = [pow(2, l) for l in range(0,levels+1)]
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 and self.tile_width == self.width and self.tile_height == self.height:
[jp2.read(1) for _ in range(levels-1)]
b = jp2.read(1)
b_str = bin(struct.unpack(">B", b)[0])[2:].zfill(8)
i = int(b_str,2)
x = i&15
y = i >> 4
self.tile_width = 2**x
self.tile_height = 2**y
logger.debug("using tile width from precint: " + str(self.tile_width))
logger.debug("using tile height from precint: " + str(self.tile_height))

# Still debugging...this prints all levels
# for _ 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)
# print "{%d,%d}" % (w,h)


jp2.close()

def to_json(self):
'''Serialize as json.
Returns:
str (json)
'''


def to_dict(self):
d = {}
d['@context'] = 'http://library.stanford.edu/iiif/image-api/1.1/context.json'
d['@id'] = self.ident
Expand All @@ -251,6 +288,14 @@ def to_json(self):
d['formats'] = self.formats
d['qualities'] = self.qualities
d['profile'] = COMPLIANCE
return d

def to_json(self):
'''Serialize as json.
Returns:
str (json)
'''
d = self.to_dict()
return json.dumps(d)

class InfoCache(object):
Expand Down
6 changes: 4 additions & 2 deletions loris/webapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
except ImportError:
import uuid

# Loris's etc dir MUST either be a sibling to the loris/loris directory or at the below:
# Loris's etc dir MUST either be a sibling to the loris/loris directory or at
# the below:
ETC_DP = '/etc/loris'
# We can figure out everything else from there.

Expand Down Expand Up @@ -216,6 +217,7 @@ def filter(self,record):
class LorisResponse(BaseResponse, CommonResponseDescriptorsMixin):
'''Similar to Response, but IIIF Compliance Header is added and none of the
ETagResponseMixin, ResponseStreamMixin, or WWWAuthenticateMixin capabilities
are included.
See: http://werkzeug.pocoo.org/docs/wrappers/#werkzeug.wrappers.Response
'''
def __init__(self, response=None, status=None, content_type=None):
Expand Down Expand Up @@ -475,7 +477,7 @@ def _get_info(self,ident,request,src_fp=None,src_format=None):
logger.debug('Identifier: %s' % (ident,))

# get the info
info = ImageInfo.from_image_file(ident, uri, src_fp, src_format, formats)
info = ImageInfo.from_image_file(uri, src_fp, src_format, formats)

# store
if self.enable_caching:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@

data_files=[
(ETC_DP, ['etc/loris.conf']),
(BIN_DP, ['bin/loris-cache_clean.sh', JP2_Transformer.local_kdu_expand_path()]),
(BIN_DP, ['bin/loris-cache_clean.sh', 'bin/iiif_img_info', JP2_Transformer.local_kdu_expand_path()]),
(LIB_DP, [JP2_Transformer.local_libkdu_path()]),
(log_dp, []),
(cache_dp, []),
Expand Down
Binary file added tests/img/sul_precincts.jp2
Binary file not shown.
12 changes: 6 additions & 6 deletions tests/img_info_t.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_color_jp2_info_from_image(self):
ident = self.test_jp2_color_id
uri = self.test_jp2_color_uri

info = img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
info = img_info.ImageInfo.from_image_file(uri, fp, fmt)

self.assertEqual(info.width, self.test_jp2_color_dims[0])
self.assertEqual(info.height, self.test_jp2_color_dims[1])
Expand All @@ -40,7 +40,7 @@ def test_extract_icc_profile_from_jp2(self):
uri = self.test_jp2_with_embedded_profile_uri
profile_copy_fp = self.test_jp2_embedded_profile_copy_fp

info = img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
info = img_info.ImageInfo.from_image_file(uri, fp, fmt)

with open(self.test_jp2_embedded_profile_copy_fp, 'rb') as fixture_bytes:
self.assertEqual(info.color_profile_bytes, fixture_bytes.read())
Expand All @@ -51,7 +51,7 @@ def test_no_embedded_profile_info_color_profile_bytes_is_None(self):
ident = self.test_jp2_color_id
uri = self.test_jp2_color_uri

info = img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
info = img_info.ImageInfo.from_image_file(uri, fp, fmt)

self.assertEqual(info.color_profile_bytes, None)

Expand All @@ -62,7 +62,7 @@ def test_grey_jp2_info_from_image(self):
ident = self.test_jp2_grey_id
uri = self.test_jp2_grey_uri

info = img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
info = img_info.ImageInfo.from_image_file(uri, fp, fmt)

self.assertEqual(info.width, self.test_jp2_grey_dims[0])
self.assertEqual(info.height, self.test_jp2_grey_dims[1])
Expand All @@ -78,7 +78,7 @@ def test_jpeg_info_from_image(self):
ident = self.test_jpeg_id
uri = self.test_jpeg_uri

info = img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
info = img_info.ImageInfo.from_image_file(uri, fp, fmt)

self.assertEqual(info.width, self.test_jpeg_dims[0])
self.assertEqual(info.height, self.test_jpeg_dims[1])
Expand All @@ -92,7 +92,7 @@ def test_tiff_info_from_image(self):
ident = self.test_tiff_id
uri = self.test_tiff_uri

info = img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
info = img_info.ImageInfo.from_image_file(uri, fp, fmt)

self.assertEqual(info.width, self.test_tiff_dims[0])
self.assertEqual(info.height, self.test_tiff_dims[1])
Expand Down
6 changes: 3 additions & 3 deletions tests/parameters_t.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ def _get_info(self):
fmt = self.test_jp2_color_fmt
ident = self.test_jp2_color_id
uri = self.test_jp2_color_uri
return img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
return img_info.ImageInfo.from_image_file(uri, fp, fmt)

def _get_info2(self):
# jpeg, x is long dimension
fp = self.test_jpeg_fp
fmt = self.test_jpeg_fmt
ident = self.test_jpeg_id
uri = self.test_jpeg_uri
return img_info.ImageInfo.from_image_file(ident, uri, fp, fmt)
return img_info.ImageInfo.from_image_file(uri, fp, fmt)


class Test_G_RegionParameterUnit(_ParameterUnitTest):
Expand Down Expand Up @@ -308,4 +308,4 @@ def suite():
test_suites.append(unittest.makeSuite(Test_K_RotationParameterUnit, 'test'))
test_suites.append(unittest.makeSuite(Test_L_RotationParameterFunctional, 'test'))
test_suite = unittest.TestSuite(test_suites)
return test_suite
return test_suite

0 comments on commit 98da082

Please sign in to comment.