[Git][debian-gis-team/glymur][master] 8 commits: New upstream version 0.11.0
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Fri Aug 5 10:21:51 BST 2022
Antonio Valentino pushed to branch master at Debian GIS Project / glymur
Commits:
6d2047e5 by Antonio Valentino at 2022-08-05T07:54:52+00:00
New upstream version 0.11.0
- - - - -
dc908e29 by Antonio Valentino at 2022-08-05T07:54:56+00:00
Update upstream source from tag 'upstream/0.11.0'
Update to upstream version '0.11.0'
with Debian dir a38f5abc9509b46c5b9a900d1ef31eb5f2400f90
- - - - -
62706d81 by Antonio Valentino at 2022-08-05T07:58:08+00:00
New upstream release
- - - - -
83f22ae8 by Antonio Valentino at 2022-08-05T08:28:25+00:00
Add python3-pytest to build-dependencies
- - - - -
20e76db3 by Antonio Valentino at 2022-08-05T08:48:42+00:00
Skip broken tests
- - - - -
52aa54a8 by Antonio Valentino at 2022-08-05T09:09:28+00:00
Do not install temporary files
- - - - -
b9b94c50 by Antonio Valentino at 2022-08-05T09:10:11+00:00
Add autopkgtests
- - - - -
1f121789 by Antonio Valentino at 2022-08-05T09:14:55+00:00
Set distribution to unstable
- - - - -
24 changed files:
- CHANGES.txt
- ci/travis-310.yaml
- debian/changelog
- debian/control
- debian/python3-glymur.install
- debian/rules
- + debian/tests/control
- + debian/tests/python3
- docs/source/conf.py
- docs/source/introduction.rst
- + docs/source/whatsnew/0.11.rst
- docs/source/whatsnew/index.rst
- glymur/_tiff.py
- glymur/command_line.py
- glymur/config.py
- glymur/jp2box.py
- glymur/jp2k.py
- glymur/lib/tiff.py
- glymur/tiff.py
- glymur/version.py
- setup.cfg
- tests/test_jp2box_uuid.py
- tests/test_jp2k.py
- tests/test_tiff2jp2.py
Changes:
=====================================
CHANGES.txt
=====================================
@@ -1,3 +1,9 @@
+July 29, 2022 - v0.11.0
+ Add options for supporting ResolutionBoxes
+ Fix ctypes interface to C library on windows
+ Add option to convert XMLPacket into UUID box
+ Add option for excluding tags from Exif UUID box
+
July 16, 2022 - v0.10.2
Fix appveyor builds
Fix tiff2jp2 when ExifTag is present
=====================================
ci/travis-310.yaml
=====================================
@@ -4,8 +4,9 @@ channels:
dependencies:
- python=3.10.*
- gdal
+ - libtiff
- lxml
- numpy
- openjpeg
- - libtiff
- pytest-xdist
+ - scikit-image
=====================================
debian/changelog
=====================================
@@ -1,3 +1,18 @@
+glymur (0.11.0-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/control:
+ - add python3-pytest to build-dependencies.
+ * debian/rules:
+ - use pytest for testing.
+ - skip broken tests.
+ * debian/python3-glymur.install
+ - improve pattern for file selection to exclude spurious temporary files.
+ * devian/tests:
+ - add autopkgtests.
+
+ -- Antonio Valentino <antonio.valentino at tiscali.it> Fri, 05 Aug 2022 09:14:40 +0000
+
glymur (0.10.2-1) unstable; urgency=medium
* New upstream release.
=====================================
debian/control
=====================================
@@ -14,6 +14,7 @@ Build-Depends: debhelper-compat (= 12),
python3-lxml,
python3-numpy,
python3-packaging,
+ python3-pytest,
python3-setuptools,
python3-skimage
Standards-Version: 4.6.1
=====================================
debian/python3-glymur.install
=====================================
@@ -1 +1 @@
-usr/lib/python3*/dist-packages/
+usr/lib/python3*/dist-packages/[Gg]lymur*
=====================================
debian/rules
=====================================
@@ -4,17 +4,13 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
+export PYBUILD_TEST_ARGS=\
+-k 'not test__printing__geotiff_uuid__xml_sidecar and not test_print_bad_geotiff' \
+$(CURDIR)/tests
+
%:
dh $@ --with python3 --buildsystem=pybuild
override_dh_auto_clean:
dh_auto_clean
-
$(RM) -r *.egg-info
-
-override_dh_auto_test:
-ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
- PYBUILD_SYSTEM=custom \
- PYBUILD_TEST_ARGS="env PYTHONPATH={build_dir} python{version} -m unittest discover -v" \
- dh_auto_test
-endif
=====================================
debian/tests/control
=====================================
@@ -0,0 +1,2 @@
+Tests: python3
+Depends: @, python3-all, python3-pytest
=====================================
debian/tests/python3
=====================================
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -efu
+
+PYS=${PYS:-"$(py3versions --supported 2>/dev/null)"}
+TESTPKG=${TESTPKG:-glymur}
+TESTDIR=${PWD}/tests
+cd "$AUTOPKGTEST_TMP"
+
+for py in $PYS; do
+ echo "=== $py ==="
+ $py -m pytest -k "not test__printing__geotiff_uuid__xml_sidecar and not test_print_bad_geotiff" ${TESTDIR}
+done
=====================================
docs/source/conf.py
=====================================
@@ -76,9 +76,9 @@ copyright = '2013-2022, John Evans'
# built documents.
#
# The short X.Y version.
-version = '0.10'
+version = '0.11'
# The full version, including alpha/beta/rc tags.
-release = '0.10.2'
+release = '0.11.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
=====================================
docs/source/introduction.rst
=====================================
@@ -4,17 +4,14 @@ Glymur: a Python interface for JPEG 2000
**Glymur** is an interface to the OpenJPEG library
which allows one to read and write JPEG 2000 files from Python.
-Glymur supports both reading and writing of JPEG 2000 images, but writing
-JPEG 2000 images is currently limited to images that can fit in memory.
+Glymur supports both reading and writing of JPEG 2000 images. A historical
+limitation of glymur was that it could not write images that did not fit
+into memory, but that limitation has been removed.
In regards to metadata, most JP2 boxes are properly interpreted.
Certain optional JP2 boxes can also be written, including XML boxes and
XMP UUIDs. There is incomplete support for reading JPX metadata.
-Glymur will look to use **lxml** when processing boxes with XML content, but can
-fall back upon the standard library's **ElementTree** if **lxml** is not
-available.
-
The current version of glymur works on Python versions 3.7, 3.8, 3.9,
and 3.10.
=====================================
docs/source/whatsnew/0.11.rst
=====================================
@@ -0,0 +1,12 @@
+######################
+Changes in glymur 0.11
+######################
+
+*****************
+Changes in 0.11.0
+*****************
+
+ * Add options for supporting ResolutionBoxes
+ * Fix ctypes interface to C library on windows
+ * Add option to convert XMLPacket into UUID box
+ * Add option for excluding tags from Exif UUID box
=====================================
docs/source/whatsnew/index.rst
=====================================
@@ -8,6 +8,7 @@ These document the changes between minor (or major) versions of glymur.
.. toctree::
+ 0.11
0.10
0.9
0.8
=====================================
glymur/_tiff.py
=====================================
@@ -31,7 +31,7 @@ def tiff_header(read_buffer):
_, offset = struct.unpack(endian + 'HI', read_buffer[2:8])
# This is the 'Exif Image' portion.
- exif = ExifImageIfd(endian, read_buffer, offset)
+ exif = Ifd(endian, read_buffer, offset)
return exif.processed_ifd
@@ -49,9 +49,6 @@ class Ifd(object):
----------
read_buffer : bytes
Raw byte stream consisting of the UUID data.
- datatype2fmt : dictionary
- Class attribute, maps the TIFF enumerated datatype to the python
- datatype and data width.
endian : str
Either '<' for big-endian, or '>' for little-endian.
num_tags : int
@@ -61,23 +58,6 @@ class Ifd(object):
processed_ifd : dictionary
Maps tag name to "mildly-interpreted" tag value.
"""
- datatype2fmt = {
- 1: ('B', 1),
- 2: ('B', 1),
- 3: ('H', 2),
- 4: ('I', 4),
- 5: ('II', 8),
- 7: ('B', 1),
- 9: ('i', 4),
- 10: ('ii', 8),
- 11: ('f', 4),
- 12: ('d', 8),
- 13: ('I', 4),
- 16: ('Q', 8),
- 17: ('q', 8),
- 18: ('Q', 8)
- }
-
def __init__(self, endian, read_buffer, offset):
self.endian = endian
self.read_buffer = read_buffer
@@ -101,13 +81,15 @@ class Ifd(object):
)
self.raw_ifd[tag] = tag_data
+ self.post_process()
+
def parse_tag(self, tag, dtype, count, offset_buf):
"""Interpret an Exif image tag data payload.
"""
try:
- fmt = self.datatype2fmt[dtype][0] * count
- payload_size = self.datatype2fmt[dtype][1] * count
+ fmt = DATATYPE2FMT[dtype][0] * count
+ payload_size = DATATYPE2FMT[dtype][1] * count
except KeyError:
msg = f'Invalid TIFF tag datatype ({dtype}).'
raise BadTiffTagDatatype(msg)
@@ -141,12 +123,12 @@ class Ifd(object):
return payload
- def post_process(self, tagnum2name):
+ def post_process(self):
"""Map the tag name instead of tag number to the tag value.
"""
for tag, value in self.raw_ifd.items():
try:
- tag_name = tagnum2name[tag]
+ tag_name = TAGNUM2NAME[tag]
except KeyError:
# Ok, we don't recognize this tag. Just use the numeric id.
msg = f'Unrecognized UUID box TIFF tag ({tag}).'
@@ -155,306 +137,314 @@ class Ifd(object):
if tag_name == 'ExifTag':
# There's an Exif IFD at the offset specified here.
- ifd = ExifImageIfd(self.endian, self.read_buffer, value)
+ ifd = Ifd(self.endian, self.read_buffer, value)
self.processed_ifd[tag_name] = ifd.processed_ifd
else:
# just a regular tag, treat it as a simple value
self.processed_ifd[tag_name] = value
-class ExifImageIfd(Ifd):
- """
- Attributes
- ----------
- tagnum2name : dict
- Maps Exif image tag numbers to the tag names.
- ifd : dict
- Maps tag names to tag values.
- """
- tagnum2name = {
- 11: 'ProcessingSoftware',
- 254: 'NewSubfileType',
- 255: 'SubfileType',
- 256: 'ImageWidth',
- 257: 'ImageLength',
- 258: 'BitsPerSample',
- 259: 'Compression',
- 262: 'PhotometricInterpretation',
- 263: 'Threshholding',
- 264: 'CellWidth',
- 265: 'CellLength',
- 266: 'FillOrder',
- 269: 'DocumentName',
- 270: 'ImageDescription',
- 271: 'Make',
- 272: 'Model',
- 273: 'StripOffsets',
- 274: 'Orientation',
- 277: 'SamplesPerPixel',
- 278: 'RowsPerStrip',
- 279: 'StripByteCounts',
- 280: 'MinSampleValue',
- 281: 'MaxSampleValue',
- 282: 'XResolution',
- 283: 'YResolution',
- 284: 'PlanarConfiguration',
- 286: 'XPosition',
- 287: 'YPosition',
- 290: 'GrayResponseUnit',
- 291: 'GrayResponseCurve',
- 292: 'T4Options',
- 293: 'T6Options',
- 296: 'ResolutionUnit',
- 301: 'TransferFunction',
- 305: 'Software',
- 306: 'DateTime',
- 315: 'Artist',
- 316: 'HostComputer',
- 317: 'Predictor',
- 318: 'WhitePoint',
- 319: 'PrimaryChromaticities',
- 320: 'ColorMap',
- 321: 'HalftoneHints',
- 322: 'TileWidth',
- 323: 'TileLength',
- 324: 'TileOffsets',
- 325: 'TileByteCounts',
- 330: 'SubIFDs',
- 332: 'InkSet',
- 333: 'InkNames',
- 334: 'NumberOfInks',
- 336: 'DotRange',
- 337: 'TargetPrinter',
- 338: 'ExtraSamples',
- 339: 'SampleFormat',
- 340: 'SMinSampleValue',
- 341: 'SMaxSampleValue',
- 342: 'TransferRange',
- 343: 'ClipPath',
- 344: 'XClipPathUnits',
- 345: 'YClipPathUnits',
- 346: 'Indexed',
- 347: 'JPEGTables',
- 351: 'OPIProxy',
- 512: 'JPEGProc',
- 513: 'JPEGInterchangeFormat',
- 514: 'JPEGInterchangeFormatLength',
- 515: 'JPEGRestartInterval',
- 517: 'JPEGLosslessPredictors',
- 518: 'JPEGPointTransforms',
- 519: 'JPEGQTables',
- 520: 'JPEGDCTables',
- 521: 'JPEGACTables',
- 529: 'YCbCrCoefficients',
- 530: 'YCbCrSubSampling',
- 531: 'YCbCrPositioning',
- 532: 'ReferenceBlackWhite',
- 700: 'XMLPacket',
- 18246: 'Rating',
- 18249: 'RatingPercent',
- 32781: 'ImageID',
- 32996: 'Datatype',
- 32997: 'ImageDepth',
- 32998: 'TileDepth',
- 33421: 'CFARepeatPatternDim',
- 33422: 'CFAPattern',
- 33423: 'BatteryLevel',
- 33432: 'Copyright',
- 33434: 'ExposureTime',
- 33437: 'FNumber',
- 33550: 'ModelPixelScale',
- 33723: 'IPTCNAA',
- 33918: 'INGRPacketDataTag',
- 33922: 'ModelTiePoint',
- 34264: 'ModelTransformation',
- 34377: 'ImageResources',
- 34665: 'ExifTag',
- 34675: 'InterColorProfile',
- 34735: 'GeoKeyDirectory',
- 34736: 'GeoDoubleParams',
- 34737: 'GeoAsciiParams',
- 34850: 'ExposureProgram',
- 34852: 'SpectralSensitivity',
- 34853: 'GPSTag',
- 34855: 'ISOSpeedRatings',
- 34856: 'OECF',
- 34857: 'Interlace',
- 34858: 'TimeZoneOffset',
- 34859: 'SelfTimerMode',
- 34864: 'SensitivityType',
- 34865: 'StandardOutputSensitivity',
- 34866: 'RecommendedExposureIndex',
- 34867: 'ISOSpeed',
- 34868: 'ISOSpeedLatitudeYYY',
- 34869: 'ISOSpeedLatitudeZZZ',
- 36864: 'ExifVersion',
- 36880: 'OffsetTime',
- 36881: 'OffsetTimeOriginal',
- 36882: 'OffsetTimeDigitized',
- 36867: 'DateTimeOriginal',
- 36868: 'DateTimeDigitized',
- 37121: 'ComponentsConfiguration',
- 37122: 'CompressedBitsPerPixel',
- 37377: 'ShutterSpeedValue',
- 37378: 'ApertureValue',
- 37379: 'BrightnessValue',
- 37380: 'ExposureBiasValue',
- 37381: 'MaxApertureValue',
- 37382: 'SubjectDistance',
- 37383: 'MeteringMode',
- 37384: 'LightSource',
- 37385: 'Flash',
- 37386: 'FocalLength',
- 37387: 'FlashEnergy',
- 37388: 'SpatialFrequencyResponse',
- 37389: 'Noise',
- 37390: 'FocalPlaneXResolution',
- 37391: 'FocalPlaneYResolution',
- 37392: 'FocalPlaneResolutionUnit',
- 37393: 'ImageNumber',
- 37394: 'SecurityClassification',
- 37395: 'ImageHistory',
- 37396: 'SubjectLocation',
- 37397: 'ExposureIndex',
- 37398: 'TIFFEPStandardID',
- 37399: 'SensingMethod',
- 37500: 'MakerNote',
- 37510: 'UserComment',
- 37520: 'SubSecTime',
- 37521: 'SubSecTimeOriginal',
- 37522: 'SubSecTimeDigitized',
- 37888: 'Temperature',
- 37889: 'Humidity',
- 37890: 'Pressure',
- 37891: 'WaterDepth',
- 37892: 'Acceleration',
- 37893: 'CameraElevationAngle',
- 40091: 'XPTitle',
- 40092: 'XPComment',
- 40093: 'XPAuthor',
- 40094: 'XPKeywords',
- 40095: 'XPSubject',
- 40960: 'FlashPixVersion',
- 40961: 'ColorSpace',
- 40962: 'PixelXDimension',
- 40963: 'PixelYDimension',
- 41483: 'FlashEnergy',
- 41484: 'SpatialFrequencyResponse',
- 41486: 'FocalPlaneXResolution',
- 41487: 'FocalPlaneYResolution',
- 41488: 'FocalPlaneResolutionUnit',
- 41492: 'SubjectLocation',
- 41493: 'ExposureIndex',
- 41495: 'SensingMethod',
- 41728: 'FileSource',
- 41729: 'SceneType',
- 41730: 'CFAPattern',
- 41985: 'CustomRendered',
- 41986: 'ExposureMode',
- 41987: 'WhiteBalance',
- 41988: 'DigitalZoomRatio',
- 41989: 'FocalLengthIn35MMFilm',
- 41990: 'SceneCaptureType',
- 41991: 'GainControl',
- 41992: 'Contrast',
- 41993: 'Saturation',
- 41994: 'Sharpness',
- 41995: 'DeviceSettingDescription',
- 41996: 'SubjectDistanceRange',
- 42016: 'ImageUniqueID',
- 42032: 'CameraOwnerName',
- 42033: 'BodySerialNumber',
- 42034: 'LensSpecification',
- 42035: 'LensMake',
- 42036: 'LensModel',
- 42037: 'LensSerialNumber',
- 42080: 'CompositeImage',
- 42081: 'SourceImageNumberOfCompositeImage',
- 42082: 'SourceExposureTimeOfCompositeImage',
- 42112: 'GDALMetadata',
- 42113: 'GDALNoData',
- 42240: 'Gamma',
- 50341: 'PrintImageMatching',
- 50706: 'DNGVersion',
- 50707: 'DNGBackwardVersion',
- 50708: 'UniqueCameraModel',
- 50709: 'LocalizedCameraModel',
- 50710: 'CFAPlaneColor',
- 50711: 'CFALayout',
- 50712: 'LinearizationTable',
- 50713: 'BlackLevelRepeatDim',
- 50714: 'BlackLevel',
- 50715: 'BlackLevelDeltaH',
- 50716: 'BlackLevelDeltaV',
- 50717: 'WhiteLevel',
- 50718: 'DefaultScale',
- 50719: 'DefaultCropOrigin',
- 50720: 'DefaultCropSize',
- 50721: 'ColorMatrix1',
- 50722: 'ColorMatrix2',
- 50723: 'CameraCalibration1',
- 50724: 'CameraCalibration2',
- 50725: 'ReductionMatrix1',
- 50726: 'ReductionMatrix2',
- 50727: 'AnalogBalance',
- 50728: 'AsShotNeutral',
- 50729: 'AsShotWhiteXY',
- 50730: 'BaselineExposure',
- 50731: 'BaselineNoise',
- 50732: 'BaselineSharpness',
- 50733: 'BayerGreenSplit',
- 50734: 'LinearResponseLimit',
- 50735: 'CameraSerialNumber',
- 50736: 'LensInfo',
- 50737: 'ChromaBlurRadius',
- 50738: 'AntiAliasStrength',
- 50739: 'ShadowScale',
- 50740: 'DNGPrivateData',
- 50741: 'MakerNoteSafety',
- 50778: 'CalibrationIlluminant1',
- 50779: 'CalibrationIlluminant2',
- 50780: 'BestQualityScale',
- 50781: 'RawDataUniqueID',
- 50827: 'OriginalRawFileName',
- 50828: 'OriginalRawFileData',
- 50829: 'ActiveArea',
- 50830: 'MaskedAreas',
- 50831: 'AsShotICCProfile',
- 50832: 'AsShotPreProfileMatrix',
- 50833: 'CurrentICCProfile',
- 50834: 'CurrentPreProfileMatrix',
- 50879: 'ColorimetricReference',
- 50931: 'CameraCalibrationSignature',
- 50932: 'ProfileCalibrationSignature',
- 50934: 'AsShotProfileName',
- 50935: 'NoiseReductionApplied',
- 50936: 'ProfileName',
- 50937: 'ProfileHueSatMapDims',
- 50938: 'ProfileHueSatMapData1',
- 50939: 'ProfileHueSatMapData2',
- 50940: 'ProfileToneCurve',
- 50941: 'ProfileEmbedPolicy',
- 50942: 'ProfileCopyright',
- 50964: 'ForwardMatrix1',
- 50965: 'ForwardMatrix2',
- 50966: 'PreviewApplicationName',
- 50967: 'PreviewApplicationVersion',
- 50968: 'PreviewSettingsName',
- 50969: 'PreviewSettingsDigest',
- 50970: 'PreviewColorSpace',
- 50971: 'PreviewDateTime',
- 50972: 'RawImageDigest',
- 50973: 'OriginalRawFileDigest',
- 50974: 'SubTileBlockSize',
- 50975: 'RowInterleaveFactor',
- 50981: 'ProfileLookTableDims',
- 50982: 'ProfileLookTableData',
- 51008: 'OpcodeList1',
- 51009: 'OpcodeList2',
- 51022: 'OpcodeList3',
- 51041: 'NoiseProfile',
- }
+# Maps TIFF image tag numbers to the tag names.
+TAGNUM2NAME = {
+ 11: 'ProcessingSoftware',
+ 254: 'NewSubfileType',
+ 255: 'SubfileType',
+ 256: 'ImageWidth',
+ 257: 'ImageLength',
+ 258: 'BitsPerSample',
+ 259: 'Compression',
+ 262: 'PhotometricInterpretation',
+ 263: 'Threshholding',
+ 264: 'CellWidth',
+ 265: 'CellLength',
+ 266: 'FillOrder',
+ 269: 'DocumentName',
+ 270: 'ImageDescription',
+ 271: 'Make',
+ 272: 'Model',
+ 273: 'StripOffsets',
+ 274: 'Orientation',
+ 277: 'SamplesPerPixel',
+ 278: 'RowsPerStrip',
+ 279: 'StripByteCounts',
+ 280: 'MinSampleValue',
+ 281: 'MaxSampleValue',
+ 282: 'XResolution',
+ 283: 'YResolution',
+ 284: 'PlanarConfiguration',
+ 286: 'XPosition',
+ 287: 'YPosition',
+ 290: 'GrayResponseUnit',
+ 291: 'GrayResponseCurve',
+ 292: 'T4Options',
+ 293: 'T6Options',
+ 296: 'ResolutionUnit',
+ 297: 'PageNumber',
+ 301: 'TransferFunction',
+ 305: 'Software',
+ 306: 'DateTime',
+ 315: 'Artist',
+ 316: 'HostComputer',
+ 317: 'Predictor',
+ 318: 'WhitePoint',
+ 319: 'PrimaryChromaticities',
+ 320: 'ColorMap',
+ 321: 'HalftoneHints',
+ 322: 'TileWidth',
+ 323: 'TileLength',
+ 324: 'TileOffsets',
+ 325: 'TileByteCounts',
+ 330: 'SubIFDs',
+ 332: 'InkSet',
+ 333: 'InkNames',
+ 334: 'NumberOfInks',
+ 336: 'DotRange',
+ 337: 'TargetPrinter',
+ 338: 'ExtraSamples',
+ 339: 'SampleFormat',
+ 340: 'SMinSampleValue',
+ 341: 'SMaxSampleValue',
+ 342: 'TransferRange',
+ 343: 'ClipPath',
+ 344: 'XClipPathUnits',
+ 345: 'YClipPathUnits',
+ 346: 'Indexed',
+ 347: 'JPEGTables',
+ 351: 'OPIProxy',
+ 512: 'JPEGProc',
+ 513: 'JPEGInterchangeFormat',
+ 514: 'JPEGInterchangeFormatLength',
+ 515: 'JPEGRestartInterval',
+ 517: 'JPEGLosslessPredictors',
+ 518: 'JPEGPointTransforms',
+ 519: 'JPEGQTables',
+ 520: 'JPEGDCTables',
+ 521: 'JPEGACTables',
+ 529: 'YCbCrCoefficients',
+ 530: 'YCbCrSubSampling',
+ 531: 'YCbCrPositioning',
+ 532: 'ReferenceBlackWhite',
+ 700: 'XMLPacket',
+ 18246: 'Rating',
+ 18249: 'RatingPercent',
+ 32781: 'ImageID',
+ 32996: 'Datatype',
+ 32997: 'ImageDepth',
+ 32998: 'TileDepth',
+ 33421: 'CFARepeatPatternDim',
+ 33422: 'CFAPattern',
+ 33423: 'BatteryLevel',
+ 33432: 'Copyright',
+ 33434: 'ExposureTime',
+ 33437: 'FNumber',
+ 33550: 'ModelPixelScale',
+ 33723: 'IPTCNAA',
+ 33918: 'INGRPacketDataTag',
+ 33922: 'ModelTiePoint',
+ 34264: 'ModelTransformation',
+ 34377: 'ImageResources',
+ 34665: 'ExifTag',
+ 34675: 'InterColorProfile',
+ 34735: 'GeoKeyDirectory',
+ 34736: 'GeoDoubleParams',
+ 34737: 'GeoAsciiParams',
+ 34850: 'ExposureProgram',
+ 34852: 'SpectralSensitivity',
+ 34853: 'GPSTag',
+ 34855: 'ISOSpeedRatings',
+ 34856: 'OECF',
+ 34857: 'Interlace',
+ 34858: 'TimeZoneOffset',
+ 34859: 'SelfTimerMode',
+ 34864: 'SensitivityType',
+ 34865: 'StandardOutputSensitivity',
+ 34866: 'RecommendedExposureIndex',
+ 34867: 'ISOSpeed',
+ 34868: 'ISOSpeedLatitudeYYY',
+ 34869: 'ISOSpeedLatitudeZZZ',
+ 36864: 'ExifVersion',
+ 36880: 'OffsetTime',
+ 36881: 'OffsetTimeOriginal',
+ 36882: 'OffsetTimeDigitized',
+ 36867: 'DateTimeOriginal',
+ 36868: 'DateTimeDigitized',
+ 37121: 'ComponentsConfiguration',
+ 37122: 'CompressedBitsPerPixel',
+ 37377: 'ShutterSpeedValue',
+ 37378: 'ApertureValue',
+ 37379: 'BrightnessValue',
+ 37380: 'ExposureBiasValue',
+ 37381: 'MaxApertureValue',
+ 37382: 'SubjectDistance',
+ 37383: 'MeteringMode',
+ 37384: 'LightSource',
+ 37385: 'Flash',
+ 37386: 'FocalLength',
+ 37387: 'FlashEnergy',
+ 37388: 'SpatialFrequencyResponse',
+ 37389: 'Noise',
+ 37390: 'FocalPlaneXResolution',
+ 37391: 'FocalPlaneYResolution',
+ 37392: 'FocalPlaneResolutionUnit',
+ 37393: 'ImageNumber',
+ 37394: 'SecurityClassification',
+ 37395: 'ImageHistory',
+ 37396: 'SubjectLocation',
+ 37397: 'ExposureIndex',
+ 37398: 'TIFFEPStandardID',
+ 37399: 'SensingMethod',
+ 37500: 'MakerNote',
+ 37510: 'UserComment',
+ 37520: 'SubSecTime',
+ 37521: 'SubSecTimeOriginal',
+ 37522: 'SubSecTimeDigitized',
+ 37888: 'Temperature',
+ 37889: 'Humidity',
+ 37890: 'Pressure',
+ 37891: 'WaterDepth',
+ 37892: 'Acceleration',
+ 37893: 'CameraElevationAngle',
+ 40091: 'XPTitle',
+ 40092: 'XPComment',
+ 40093: 'XPAuthor',
+ 40094: 'XPKeywords',
+ 40095: 'XPSubject',
+ 40960: 'FlashPixVersion',
+ 40961: 'ColorSpace',
+ 40962: 'PixelXDimension',
+ 40963: 'PixelYDimension',
+ 41483: 'FlashEnergy',
+ 41484: 'SpatialFrequencyResponse',
+ 41486: 'FocalPlaneXResolution',
+ 41487: 'FocalPlaneYResolution',
+ 41488: 'FocalPlaneResolutionUnit',
+ 41492: 'SubjectLocation',
+ 41493: 'ExposureIndex',
+ 41495: 'SensingMethod',
+ 41728: 'FileSource',
+ 41729: 'SceneType',
+ 41730: 'CFAPattern',
+ 41985: 'CustomRendered',
+ 41986: 'ExposureMode',
+ 41987: 'WhiteBalance',
+ 41988: 'DigitalZoomRatio',
+ 41989: 'FocalLengthIn35MMFilm',
+ 41990: 'SceneCaptureType',
+ 41991: 'GainControl',
+ 41992: 'Contrast',
+ 41993: 'Saturation',
+ 41994: 'Sharpness',
+ 41995: 'DeviceSettingDescription',
+ 41996: 'SubjectDistanceRange',
+ 42016: 'ImageUniqueID',
+ 42032: 'CameraOwnerName',
+ 42033: 'BodySerialNumber',
+ 42034: 'LensSpecification',
+ 42035: 'LensMake',
+ 42036: 'LensModel',
+ 42037: 'LensSerialNumber',
+ 42080: 'CompositeImage',
+ 42081: 'SourceImageNumberOfCompositeImage',
+ 42082: 'SourceExposureTimeOfCompositeImage',
+ 42112: 'GDALMetadata',
+ 42113: 'GDALNoData',
+ 42240: 'Gamma',
+ 50341: 'PrintImageMatching',
+ 50706: 'DNGVersion',
+ 50707: 'DNGBackwardVersion',
+ 50708: 'UniqueCameraModel',
+ 50709: 'LocalizedCameraModel',
+ 50710: 'CFAPlaneColor',
+ 50711: 'CFALayout',
+ 50712: 'LinearizationTable',
+ 50713: 'BlackLevelRepeatDim',
+ 50714: 'BlackLevel',
+ 50715: 'BlackLevelDeltaH',
+ 50716: 'BlackLevelDeltaV',
+ 50717: 'WhiteLevel',
+ 50718: 'DefaultScale',
+ 50719: 'DefaultCropOrigin',
+ 50720: 'DefaultCropSize',
+ 50721: 'ColorMatrix1',
+ 50722: 'ColorMatrix2',
+ 50723: 'CameraCalibration1',
+ 50724: 'CameraCalibration2',
+ 50725: 'ReductionMatrix1',
+ 50726: 'ReductionMatrix2',
+ 50727: 'AnalogBalance',
+ 50728: 'AsShotNeutral',
+ 50729: 'AsShotWhiteXY',
+ 50730: 'BaselineExposure',
+ 50731: 'BaselineNoise',
+ 50732: 'BaselineSharpness',
+ 50733: 'BayerGreenSplit',
+ 50734: 'LinearResponseLimit',
+ 50735: 'CameraSerialNumber',
+ 50736: 'LensInfo',
+ 50737: 'ChromaBlurRadius',
+ 50738: 'AntiAliasStrength',
+ 50739: 'ShadowScale',
+ 50740: 'DNGPrivateData',
+ 50741: 'MakerNoteSafety',
+ 50778: 'CalibrationIlluminant1',
+ 50779: 'CalibrationIlluminant2',
+ 50780: 'BestQualityScale',
+ 50781: 'RawDataUniqueID',
+ 50827: 'OriginalRawFileName',
+ 50828: 'OriginalRawFileData',
+ 50829: 'ActiveArea',
+ 50830: 'MaskedAreas',
+ 50831: 'AsShotICCProfile',
+ 50832: 'AsShotPreProfileMatrix',
+ 50833: 'CurrentICCProfile',
+ 50834: 'CurrentPreProfileMatrix',
+ 50879: 'ColorimetricReference',
+ 50931: 'CameraCalibrationSignature',
+ 50932: 'ProfileCalibrationSignature',
+ 50934: 'AsShotProfileName',
+ 50935: 'NoiseReductionApplied',
+ 50936: 'ProfileName',
+ 50937: 'ProfileHueSatMapDims',
+ 50938: 'ProfileHueSatMapData1',
+ 50939: 'ProfileHueSatMapData2',
+ 50940: 'ProfileToneCurve',
+ 50941: 'ProfileEmbedPolicy',
+ 50942: 'ProfileCopyright',
+ 50964: 'ForwardMatrix1',
+ 50965: 'ForwardMatrix2',
+ 50966: 'PreviewApplicationName',
+ 50967: 'PreviewApplicationVersion',
+ 50968: 'PreviewSettingsName',
+ 50969: 'PreviewSettingsDigest',
+ 50970: 'PreviewColorSpace',
+ 50971: 'PreviewDateTime',
+ 50972: 'RawImageDigest',
+ 50973: 'OriginalRawFileDigest',
+ 50974: 'SubTileBlockSize',
+ 50975: 'RowInterleaveFactor',
+ 50981: 'ProfileLookTableDims',
+ 50982: 'ProfileLookTableData',
+ 51008: 'OpcodeList1',
+ 51009: 'OpcodeList2',
+ 51022: 'OpcodeList3',
+ 51041: 'NoiseProfile',
+}
- def __init__(self, endian, read_buffer, offset):
- super().__init__(endian, read_buffer, offset)
- self.post_process(self.tagnum2name)
+# maps the TIFF enumerated datatype to the corresponding structs datatype code,
+# along with and data width
+DATATYPE2FMT = {
+ 1: ('B', 1),
+ 2: ('B', 1),
+ 3: ('H', 2),
+ 4: ('I', 4),
+ 5: ('II', 8),
+ 7: ('B', 1),
+ 9: ('i', 4),
+ 10: ('ii', 8),
+ 11: ('f', 4),
+ 12: ('d', 8),
+ 13: ('I', 4),
+ 16: ('Q', 8),
+ 17: ('q', 8),
+ 18: ('Q', 8)
+}
=====================================
glymur/command_line.py
=====================================
@@ -89,76 +89,114 @@ def tiff2jp2():
epilog = (
"Normally you should at least provide the tilesize argument. "
- "tiff2jp2 will NOT automatically use the TIFF tile dimensions "
- "(if tiled)."
+ "Even if the TIFF is tiled, tiff2jp2 will NOT automatically use the "
+ "TIFF tile dimensions."
)
kwargs = {
'description': 'Convert TIFF to JPEG 2000.',
'formatter_class': argparse.ArgumentDefaultsHelpFormatter,
- 'epilog': epilog
+ 'epilog': epilog,
+ 'add_help': False
}
parser = argparse.ArgumentParser(**kwargs)
- help = 'Dimensions of JP2K tile.'
- parser.add_argument(
- '--tilesize', nargs=2, type=int, help=help, metavar=('h', 'w')
+ group1 = parser.add_argument_group(
+ 'JP2K', 'Pass-through arguments to Jp2k.'
)
- help = (
- 'Logging level, one of "critical", "error", "warning", "info", '
- 'or "debug".'
+ help = 'Capture resolution parameters'
+ group1.add_argument(
+ '--capture-resolution', nargs=2, type=float, help=help,
+ metavar=('VRESC', 'HRESC')
)
- parser.add_argument(
- '--verbosity', help=help, default='warning',
- choices=['critical', 'error', 'warning', 'info', 'debug']
+
+ help = 'Display resolution parameters'
+ group1.add_argument(
+ '--display-resolution', nargs=2, type=float, help=help,
+ metavar=('VRESD', 'HRESD')
)
help = (
'Compression ratio for successive layers. You may specify more '
'than once to get multiple layers.'
)
- parser.add_argument(
- '--cratio', action='append', type=int, help=help,
- )
+ group1.add_argument('--cratio', action='append', type=int, help=help)
help = (
'PSNR for successive layers. You may specify more than once to get '
'multiple layers.'
)
- parser.add_argument(
- '--psnr', action='append', type=int, help=help,
- )
+ group1.add_argument('--psnr', action='append', type=int, help=help)
help = 'Codeblock size.'
- parser.add_argument(
+ group1.add_argument(
'--codeblocksize', nargs=2, type=int, help=help,
metavar=('cblkh', 'cblkw')
)
help = 'Number of decomposition levels.'
- parser.add_argument('--numres', type=int, help=help, default=6)
+ group1.add_argument('--numres', type=int, help=help, default=6)
help = 'Progression order.'
choices = ['lrcp', 'rlcp', 'rpcl', 'prcl', 'cprl']
- parser.add_argument('--prog', choices=choices, help=help, default='lrcp')
+ group1.add_argument('--prog', choices=choices, help=help, default='lrcp')
help = 'Use irreversible 9x7 transform.'
- parser.add_argument('--irreversible', help=help, action='store_true')
+ group1.add_argument('--irreversible', help=help, action='store_true')
help = 'Generate EPH markers.'
- parser.add_argument('--eph', help=help, action='store_true')
+ group1.add_argument('--eph', help=help, action='store_true')
help = 'Generate PLT markers.'
- parser.add_argument('--plt', help=help, action='store_true')
+ group1.add_argument('--plt', help=help, action='store_true')
help = 'Generate SOP markers.'
- parser.add_argument('--sop', help=help, action='store_true')
+ group1.add_argument('--sop', help=help, action='store_true')
+
+ group2 = parser.add_argument_group(
+ 'TIFF', 'Arguments specific to conversion of TIFF imagery.'
+ )
- help = 'Do not create UUID box for TIFF metadata.'
- parser.add_argument('--nouuid', help=help, action='store_false')
+ help = 'Create Exif UUID box from TIFF metadata.'
+ group2.add_argument(
+ '--create-exif-uuid', help=help, action='store_true', default=True
+ )
- parser.add_argument('tifffile')
- parser.add_argument('jp2kfile')
+ help = (
+ 'Extract XMLPacket tag value from TIFF IFD and store in XMP UUID box. '
+ 'This will exclude the XMLPacket tag from the Exif UUID box.'
+ )
+ group2.add_argument('--create-xmp-uuid', help=help, action='store_true')
+
+ help = (
+ 'Exclude TIFF tag(s) from EXIF UUID (if creating such a UUID). '
+ 'This option may be specified as tag numbers or names.'
+ )
+ group2.add_argument('--exclude-tags', help=help, nargs='*')
+
+ help = (
+ 'Dimensions of JP2K tile. If not provided, the JPEG2000 image will '
+ 'be written as a single tile.'
+ )
+ group2.add_argument(
+ '--tilesize', nargs=2, type=int, help=help, metavar=('NROWS', 'NCOLS')
+ )
+
+ group2.add_argument('tifffile', help='Input TIFF file.')
+ group2.add_argument('jp2kfile', help='Output JPEG 2000 file.')
+
+ # These arguments are not specific to either group.
+ help = 'Show this help message and exit'
+ parser.add_argument('--help', '-h', action='help', help=help)
+
+ help = (
+ 'Logging level, one of "critical", "error", "warning", "info", '
+ 'or "debug".'
+ )
+ parser.add_argument(
+ '--verbosity', help=help, default='warning',
+ choices=['critical', 'error', 'warning', 'info', 'debug']
+ )
args = parser.parse_args()
@@ -167,10 +205,24 @@ def tiff2jp2():
tiffpath = pathlib.Path(args.tifffile)
jp2kpath = pathlib.Path(args.jp2kfile)
- with Tiff2Jp2k(
- tiffpath, jp2kpath, tilesize=args.tilesize, verbosity=logging_level,
- cbsize=args.codeblocksize, cratios=args.cratio, numres=args.numres,
- plt=args.plt, eph=args.eph, sop=args.sop, prog=args.prog,
- irreversible=args.irreversible, psnr=args.psnr, create_uuid=args.nouuid
- ) as j:
+ kwargs = {
+ 'cbsize': args.codeblocksize,
+ 'cratios': args.cratio,
+ 'capture_resolution': args.capture_resolution,
+ 'create_exif_uuid': args.create_exif_uuid,
+ 'create_xmp_uuid': args.create_xmp_uuid,
+ 'display_resolution': args.display_resolution,
+ 'eph': args.eph,
+ 'exclude_tags': args.exclude_tags,
+ 'irreversible': args.irreversible,
+ 'numres': args.numres,
+ 'plt': args.plt,
+ 'prog': args.prog,
+ 'psnr': args.psnr,
+ 'sop': args.sop,
+ 'tilesize': args.tilesize,
+ 'verbosity': logging_level,
+ }
+
+ with Tiff2Jp2k(tiffpath, jp2kpath, **kwargs) as j:
j.run()
=====================================
glymur/config.py
=====================================
@@ -118,6 +118,9 @@ def glymur_config(libname):
-------
loaded shared library
"""
+ if platform.system().startswith('Windows') and libname == 'c':
+ return ctypes.cdll.msvcrt
+
path = _determine_full_path(libname)
if path is None or path in ['None', 'none']:
=====================================
glymur/jp2box.py
=====================================
@@ -142,16 +142,19 @@ class Jp2kBox(object):
box_id : bytes
4-byte sequence that identifies the superbox.
"""
- # Write the contained boxes, then come back and write the length.
- orig_pos = fptr.tell()
- fptr.write(struct.pack('>I4s', 0, box_id))
+ b = io.BytesIO()
+ b.write(struct.pack('>I4s', 0, box_id))
for box in self.box:
- box.write(fptr)
+ box.write(b)
- end_pos = fptr.tell()
- fptr.seek(orig_pos)
- fptr.write(struct.pack('>I', end_pos - orig_pos))
- fptr.seek(end_pos)
+ box_length = b.tell()
+
+ # come back and write the length.
+ b.seek(0)
+ buffer = struct.pack('>I', box_length)
+ b.write(buffer)
+
+ fptr.write(b.getvalue())
def _parse_this_box(self, fptr, box_id, start, num_bytes):
"""Parse the current box.
@@ -232,7 +235,7 @@ class Jp2kBox(object):
read_buffer = fptr.read(8)
if len(read_buffer) < 8:
- msg = "Extra bytes at end of file ignored."
+ msg = f"{len(read_buffer)} extra bytes at end of file ignored."
warnings.warn(msg, UserWarning)
return superbox
@@ -3522,10 +3525,12 @@ class UUIDBox(Jp2kBox):
# gdal not found? The representation of the TIFF IFD will have to
# do.
return self.data
- except RuntimeError:
+ except RuntimeError as e:
# Unusual situation where gdal code fails
# The representation of the TIFF IFD will have to do.
- return self.data
+ warnings.warn(str(e))
+ text = f"{pprint.pformat(self.data)}"
+ return text
def _print_geotiff_as_geotiff(self):
"""
=====================================
glymur/jp2k.py
=====================================
@@ -96,10 +96,12 @@ class Jp2k(Jp2kBox):
def __init__(
self, filename, data=None, shape=None, tilesize=None, verbose=False,
- cbsize=None, cinema2k=None, cinema4k=None, colorspace=None,
- cratios=None, eph=None, grid_offset=None, irreversible=None, mct=None,
- modesw=None, numres=None, plt=False, prog=None, psizes=None, psnr=None,
- sop=None, subsam=None, tlm=False
+ capture_resolution=None, cbsize=None, cinema2k=None,
+ cinema4k=None, colorspace=None, cratios=None,
+ display_resolution=None, eph=None, grid_offset=None,
+ irreversible=None, mct=None, modesw=None, numres=None,
+ plt=False, prog=None, psizes=None, psnr=None, sop=None,
+ subsam=None, tlm=False,
):
"""
Parameters
@@ -112,6 +114,9 @@ class Jp2k(Jp2kBox):
Image data to be written to file.
shape : tuple, optional
Size of image data, only required when image_data is not provided.
+ capture_resolution : tuple, optional
+ Capture solution (VRES, HRES). This appends a capture resolution
+ box onto the end of the JP2 file when it is created.
cbsize : tuple, optional
Code block size (NROWS, NCOLS)
cinema2k : int, optional
@@ -122,6 +127,9 @@ class Jp2k(Jp2kBox):
The image color space.
cratios : iterable, optional
Compression ratios for successive layers.
+ display_resolution : tuple, optional
+ Display solution (VRES, HRES). This appends a display resolution
+ box onto the end of the JP2 file when it is created.
eph : bool, optional
If true, write EPH marker after each header packet.
grid_offset : tuple, optional
@@ -168,16 +176,17 @@ class Jp2k(Jp2kBox):
self.path = pathlib.Path(self.filename)
self.box = []
- self._codec_format = None
self._layer = 0
self._codestream = None
self._decoded_components = None
+ self._capture_resolution = capture_resolution
self._cbsize = cbsize
self._cinema2k = cinema2k
self._cinema4k = cinema4k
self._colorspace = colorspace
self._cratios = cratios
+ self._display_resolution = display_resolution
self._eph = eph
self._grid_offset = grid_offset
self._irreversible = irreversible
@@ -193,52 +202,105 @@ class Jp2k(Jp2kBox):
self._tilesize = tilesize
self._tlm = tlm
- self._shape = None
+ self._shape = shape
self._ndim = None
self._dtype = None
self._ignore_pclr_cmap_cdef = False
self._verbose = verbose
- self._validate_kwargs()
-
if self.filename[-4:].endswith(('.jp2', '.JP2')):
self._codec_format = opj2.CODEC_JP2
else:
self._codec_format = opj2.CODEC_J2K
- if self._codec_format == opj2.CODEC_J2K and colorspace is not None:
- msg = 'Do not specify a colorspace when writing a raw codestream.'
- raise InvalidJp2kError(msg)
+ if data is None and shape is None and self.path.exists():
+ self._readonly = True
+ else:
+ self._readonly = False
+
+ if data is None and tilesize is not None and shape is not None:
+ self._writing_by_tiles = True
+ else:
+ self._writing_by_tiles = False
+
+ if data is None and tilesize is None:
+ # case of
+ # j = Jp2k(filename)
+ # j[:] = data
+ self._expecting_to_write_by_setitem = True
+ else:
+ self._expecting_to_write_by_setitem = False
if data is not None:
- # We are writing a JP2/J2K/JPX file where the image is
- # contained in memory.
+ self._have_data = True
self._shape = data.shape
- self._write(data)
- elif data is None and shape is not None:
- # We are writing an entire image via the slice protocol, or we are
- # writing an image tile-by-tile. A future course of action will
- # determine that.
- self._shape = shape
- elif data is None and shape is None and self.path.exists():
+ else:
+ self._have_data = False
+
+ self._validate_kwargs()
+
+ if self._readonly:
# We must be just reading a JP2/J2K/JPX file. Parse its
- # contents, then determine "shape".
+ # contents, then determine the shape. We are then done.
self.parse()
self._initialize_shape()
+ return
+ if data is not None:
+ # We are writing a JP2/J2K/JPX file where the image is
+ # contained in memory.
+ self._write(data)
+
+ self.finalize()
+
+ def finalize(self, force_parse=False):
+ """
+ For now, the only task remaining is to possibly write out a
+ ResolutionBox if we were so instructed. There could be other
+ possibilities in the future.
+
+ Parameters
+ ----------
+ force : bool
+ If true, then run finalize operations
+ """
+ # Cases where we do NOT want to parse.
if (
- self.shape is not None
- and self.tilesize is not None
- and (
- self.tilesize[0] > self.shape[0]
- or self.tilesize[1] > self.shape[1]
- )
+ (self._writing_by_tiles or self._expecting_to_write_by_setitem)
+ and not force_parse
):
- msg = (
- f"The tile size {self.tilesize} cannot exceed the image "
- f"size {self.shape[:2]}."
+ # We are writing by tiles but we are not finished doing that.
+ # or
+ # we are writing by __setitem__ but aren't finished doing that
+ # either
+ return
+
+ # So now we are basically done writing a JP2/Jp2k file ...
+ if self._capture_resolution is None:
+ # ... and we don't have any extra boxes, so go ahead and parse.
+ self.parse()
+ return
+
+ # So we DO have extra boxes. Handle them, and THEN parse.
+ self._append_resolution_superbox()
+ self.parse()
+
+ def _append_resolution_superbox(self):
+ """
+ As a close-out task, append a resolution superbox to the end of the
+ file if we were so instructed.
+ """
+ with open(self.filename, mode='ab') as f:
+ resc = glymur.jp2box.CaptureResolutionBox(
+ self._capture_resolution[0], self._capture_resolution[1],
)
- raise RuntimeError(msg)
+ resd = glymur.jp2box.DisplayResolutionBox(
+ self._display_resolution[0], self._display_resolution[1],
+ )
+ rbox = glymur.jp2box.ResolutionBox([resc, resd])
+ rbox.write(f)
+
+ # self.box.append(rbox)
def _validate_kwargs(self):
"""
@@ -288,6 +350,55 @@ class Jp2k(Jp2kBox):
)
raise InvalidJp2kError(msg)
+ if (
+ self._codec_format == opj2.CODEC_J2K
+ and self._colorspace is not None
+ ):
+ msg = 'Do not specify a colorspace when writing a raw codestream.'
+ raise InvalidJp2kError(msg)
+
+ if (
+ self._codec_format == opj2.CODEC_J2K
+ and self._capture_resolution is not None
+ and self._display_resolution is not None
+ ):
+ msg = (
+ 'Do not specify capture/display resolution when writing a raw '
+ 'codestream.'
+ )
+ raise InvalidJp2kError(msg)
+
+ if (
+ (self._capture_resolution is not None)
+ ^ (self._display_resolution is not None)
+ ):
+ msg = (
+ 'The capture_resolution and display resolution keywords must'
+ 'both be supplied or neither supplied.'
+ )
+ raise RuntimeError(msg)
+
+ if self._readonly and self._capture_resolution is not None:
+ msg = (
+ 'Capture/Display resolution keyword parameters cannot be '
+ 'supplied when the intent seems to be to read an image.'
+ )
+ raise RuntimeError(msg)
+
+ if (
+ self._shape is not None
+ and self.tilesize is not None
+ and (
+ self.tilesize[0] > self.shape[0]
+ or self.tilesize[1] > self.shape[1]
+ )
+ ):
+ msg = (
+ f"The tile size {self.tilesize} cannot exceed the image "
+ f"size {self.shape[:2]}."
+ )
+ raise RuntimeError(msg)
+
def _initialize_shape(self):
"""
If there was no image data provided and if no shape was
@@ -924,9 +1035,6 @@ class Jp2k(Jp2kBox):
opj2.encode(codec, strm)
opj2.end_compress(codec, strm)
- # Refresh the metadata.
- self.parse()
-
def append(self, box):
"""Append a JP2 box to the file in-place.
@@ -2073,6 +2181,7 @@ class _TileWriter(object):
return self
else:
# We've gone thru all the tiles by this point.
+ self.jp2k.finalize(force_parse=True)
raise StopIteration
def __setitem__(self, index, img_array):
@@ -2116,9 +2225,6 @@ class _TileWriter(object):
opj2.image_destroy(self.image)
opj2.destroy_codec(self.codec)
- # ... and reparse the newly created file to get all the metadata
- self.jp2k.parse()
-
def setup_first_tile(self, img_array):
"""
Only do these things for the first tile.
=====================================
glymur/lib/tiff.py
=====================================
@@ -615,6 +615,7 @@ def open(filename, mode='r'):
file_argument = ctypes.c_char_p(filename.encode())
mode_argument = ctypes.c_char_p(mode.encode())
fp = _LIBTIFF.TIFFOpen(file_argument, mode_argument)
+ check_error(fp)
_reset_error_warning_handlers(err_handler, warn_handler)
=====================================
glymur/tiff.py
=====================================
@@ -11,7 +11,12 @@ from uuid import UUID
# local imports
from glymur import Jp2k
from .lib import tiff as libtiff
-from .jp2box import UUIDBox
+from . import jp2box
+from ._tiff import TAGNUM2NAME
+
+# Create a mapping of tag names to tag numbers. Make the tag names to be
+# lower-case because we need to compare against user-supplied tag names.
+TAGNAME2NUM = {v.lower(): k for k, v in TAGNUM2NAME.items()}
# Map the numeric TIFF datatypes to the format string used by the struct module
@@ -48,18 +53,42 @@ class Tiff2Jp2k(object):
Path to TIFF file.
jp2_filename : path or str
Path to JPEG 2000 file to be written.
+ jp2_kwargs : dict
+ Keyword arguments to pass along to the Jp2k constructor.
tilesize : tuple
The dimensions of a tile in the JP2K file.
- create_uuid : bool
+ create_exif_uuid : bool
Create a UUIDBox for the TIFF IFD metadata.
version : int
Identifies the TIFF as 32-bit TIFF or 64-bit TIFF.
"""
def __init__(
- self, tiff_filename, jp2_filename, tilesize=None,
- verbosity=logging.CRITICAL, create_uuid=True, **kwargs
+ self, tiff_filename, jp2_filename,
+ create_exif_uuid=True, create_xmp_uuid=False, exclude_tags=None,
+ tilesize=None, verbosity=logging.CRITICAL,
+ **kwargs
):
+ """
+ Parameters
+ ----------
+ create_exif_uuid : bool
+ If true, create an EXIF UUID out of the TIFF metadata (tags)
+ exclude_tags : list or None
+ If not None and if create_exif_uuid is True, exclude any listed
+ tags from the EXIF UUID.
+ tiff_filename : path or str
+ Path to TIFF file.
+ jp2_filename : path or str
+ Path to JPEG 2000 file to be written.
+ tilesize : tuple
+ The dimensions of a tile in the JP2K file.
+ verbosity : int
+ Set the level of logging, i.e. WARNING, INFO, etc.
+ create_xmp_uuid : bool
+ If true and if there is an XMLPacket (700) tag in the TIFF main
+ IFD, it will be removed from the IFD and placed in a UUID box.
+ """
self.tiff_filename = tiff_filename
if not self.tiff_filename.exists():
@@ -67,12 +96,72 @@ class Tiff2Jp2k(object):
self.jp2_filename = jp2_filename
self.tilesize = tilesize
- self.create_uuid = create_uuid
+ self.create_exif_uuid = create_exif_uuid
+ self.create_xmp_uuid = create_xmp_uuid
+
+ self.exclude_tags = self._process_exclude_tags(exclude_tags)
- self.kwargs = kwargs
+ self.jp2_kwargs = kwargs
self.setup_logging(verbosity)
+ def _process_exclude_tags(self, exclude_tags):
+ """
+ The list of tags to exclude may be mixed type (str or integer). There
+ is also the possibility that they may be capitalized differently than
+ our internal list, so the goal here is to convert them all to integer
+ values.
+
+ Parameters
+ ----------
+ exclude_tags : list or None
+ List of tags that are meant to be excluded from the EXIF UUID.
+
+ Returns
+ -------
+ list of numeric tag values
+ """
+ if exclude_tags is None:
+ return exclude_tags
+
+ lst = []
+
+ # first, make the tags all str datatype
+ # compare the tags as lower case for consistency's sake
+ exclude_tags = [
+ tag.lower() if isinstance(tag, str) else str(tag)
+ for tag in exclude_tags
+ ]
+
+ # If any tags were specified as strings, we need to convert them
+ # into tag numbers.
+ for tag in exclude_tags:
+
+ # convert from str to numeric
+ #
+ # is it a string like '325'? then convert to integer 325
+ try:
+ tag_num = int(tag)
+ except ValueError:
+ # tag wasn't '325', but rather 'tilebytecounts',
+ # hopefully? Try to map from the name back to the tag
+ # number.
+ try:
+ tag_num = TAGNAME2NUM[tag]
+ except KeyError:
+ msg = f"{tag} is not a recognized TIFF tag"
+ warnings.warn(msg)
+ else:
+ lst.append(tag_num)
+ else:
+ # tag really was something like '325', so we keep the
+ # numeric value
+ lst.append(tag_num)
+
+ exclude_tags = lst
+
+ return exclude_tags
+
def setup_logging(self, verbosity):
self.logger = logging.getLogger('tiff2jp2')
self.logger.setLevel(verbosity)
@@ -91,15 +180,16 @@ class Tiff2Jp2k(object):
self.get_main_ifd()
self.copy_image()
+ self.append_extra_jp2_boxes()
- if self.create_uuid:
- self.copy_metadata()
-
- def copy_metadata(self):
+ def append_extra_jp2_boxes(self):
"""
Copy over the TIFF IFD. Place it in a UUID box. Append to the JPEG
2000 file.
"""
+ if not self.create_exif_uuid:
+ return
+
# create a bytesio object for the IFD
b = io.BytesIO()
@@ -108,22 +198,40 @@ class Tiff2Jp2k(object):
data = struct.pack('<BBHI', 73, 73, 42, 8)
b.write(data)
- self._process_tags(b, self.tags)
+ if 700 in self.tags and self.create_xmp_uuid:
+ # remove the XMLPacket data from the IFD dictionary
+ xmp_data = self.tags.pop(700)
+ else:
+ xmp_data = None
+
+ self._write_ifd(b, self.tags)
+ # create the Exif UUID
if self.found_geotiff_tags:
# geotiff UUID
- uuid = UUID('b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03')
+ the_uuid = UUID('b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03')
payload = b.getvalue()
else:
# Make it an exif UUID.
- uuid = UUID(bytes=b'JpgTiffExif->JP2')
+ the_uuid = UUID(bytes=b'JpgTiffExif->JP2')
payload = b'EXIF\0\0' + b.getvalue()
# the length of the box is the length of the payload plus 8 bytes
# to store the length of the box and the box ID
box_length = len(payload) + 8
- uuid_box = UUIDBox(uuid, payload, box_length)
+ uuid_box = jp2box.UUIDBox(the_uuid, payload, box_length)
+ with open(self.jp2_filename, mode='ab') as f:
+ uuid_box.write(f)
+
+ if xmp_data is None:
+ return
+
+ # create the XMP UUID
+ the_uuid = jp2box.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
+ payload = bytes(xmp_data['payload'])
+ box_length = len(payload) + 8
+ uuid_box = jp2box.UUIDBox(the_uuid, payload, box_length)
with open(self.jp2_filename, mode='ab') as f:
uuid_box.write(f)
@@ -252,11 +360,17 @@ class Tiff2Jp2k(object):
return tags
- def _process_tags(self, b, tags):
+ def _write_ifd(self, b, tags):
- # keep this for writing to the UUID, which will always be 32-bit
+ # keep this for writing to the UUID, which will always be for 32-bit
+ # TIFFs
little_tiff_tag_length = 12
+ if self.exclude_tags is not None:
+ for tag in self.exclude_tags:
+ if tag in tags:
+ tags.pop(tag)
+
num_tags = len(tags)
write_buffer = struct.pack('<H', num_tags)
@@ -340,7 +454,7 @@ class Tiff2Jp2k(object):
outbuffer = struct.pack('<I', after_ifd_position)
b.write(outbuffer)
b.seek(after_ifd_position)
- self._process_tags(b, payload)
+ self._write_ifd(b, payload)
else:
# write a normal tag
outbuffer = struct.pack('<' + payload_format, *payload)
@@ -497,9 +611,10 @@ class Tiff2Jp2k(object):
else:
tw = imagewidth
rps = self.get_tag_value(278)
- num_strips = libtiff.numberOfStrips(self.tiff_fp)
if self.tilesize is not None:
+ # The JP2K tile size was specified. Compute the number of JP2K
+ # tile rows and columns.
jth, jtw = self.tilesize
num_jp2k_tile_rows = int(np.ceil(imagewidth / jtw))
@@ -521,230 +636,197 @@ class Tiff2Jp2k(object):
self.jp2_filename,
shape=(imageheight, imagewidth, spp),
tilesize=self.tilesize,
- **self.kwargs
+ **self.jp2_kwargs
)
if not libtiff.RGBAImageOK(self.tiff_fp):
photometric_string = self.tagvalue2str(libtiff.Photometric, photo)
msg = (
- f"The TIFF Photometric tag is {photometric_string} and is "
- "not supported."
+ f"The TIFF Photometric tag is {photometric_string}. It is "
+ "not supported by this program."
)
raise RuntimeError(msg)
- elif self.tilesize is None and libtiff.RGBAImageOK(self.tiff_fp):
+ elif self.tilesize is None:
- # if no jp2k tiling was specified and if the image is ok to read
- # via the RGBA interface, then just do that.
- msg = (
- "Reading using the RGBA interface, writing as a single tile "
- "image."
- )
- self.logger.info(msg)
-
- if photo not in [
- libtiff.Photometric.MINISWHITE,
- libtiff.Photometric.MINISBLACK,
- libtiff.Photometric.PALETTE,
- libtiff.Photometric.YCBCR,
- libtiff.Photometric.RGB
- ]:
- photostr = self.tagvalue2str(libtiff.Photometric, photo)
- msg = (
- "Beware, the RGBA interface to attempt to read this TIFF "
- f"when it has a PhotometricInterpretation of {photostr}."
- )
- warnings.warn(msg)
+ # this handles both cases of a striped TIFF and a tiled TIFF
- image = libtiff.readRGBAImageOriented(
- self.tiff_fp, imagewidth, imageheight
+ self._write_rgba_single_tile(
+ photo, imagewidth, imageheight, spp, jp2
)
- if spp < 4:
- image = image[:, :, :3]
-
- jp2[:] = image
-
elif isTiled and self.tilesize is not None:
- num_tiff_tile_cols = int(np.ceil(imagewidth / tw))
-
- partial_jp2_tile_rows = (imageheight / jth) != (imageheight // jth)
- partial_jp2_tile_cols = (imagewidth / jtw) != (imagewidth // jtw)
-
- rgba_tile = np.zeros((th, tw, 4), dtype=np.uint8)
-
- self.logger.debug(f'image: {imageheight} x {imagewidth}')
- self.logger.debug(f'jptile: {jth} x {jtw}')
- self.logger.debug(f'ttile: {th} x {tw}')
- for idx, tilewriter in enumerate(jp2.get_tilewriters()):
-
- # populate the jp2k tile with tiff tiles
- self.logger.info(f'Tile: #{idx}')
- self.logger.debug(f'J tile row: #{idx // num_jp2k_tile_cols}')
- self.logger.debug(f'J tile col: #{idx % num_jp2k_tile_cols}')
-
- jp2k_tile = np.zeros((jth, jtw, spp), dtype=dtype)
- tiff_tile = np.zeros((th, tw, spp), dtype=dtype)
-
- jp2k_tile_row = int(np.ceil(idx // num_jp2k_tile_cols))
- jp2k_tile_col = int(np.ceil(idx % num_jp2k_tile_cols))
-
- # the coordinates of the upper left pixel of the jp2k tile
- julr, julc = jp2k_tile_row * jth, jp2k_tile_col * jtw
-
- # loop while the upper left corner of the current tiff file is
- # less than the lower left corner of the jp2k tile
- r = julr
- while (r // th) * th < min(julr + jth, imageheight):
- c = julc
-
- tilenum = libtiff.computeTile(self.tiff_fp, c, r, 0, 0)
- self.logger.debug(f'TIFF tile # {tilenum}')
-
- tiff_tile_row = int(np.ceil(tilenum // num_tiff_tile_cols))
- tiff_tile_col = int(np.ceil(tilenum % num_tiff_tile_cols))
+ self._write_tiled_tiff_to_tiled_jp2k(
+ imagewidth, imageheight, spp,
+ jtw, jth, tw, th,
+ num_jp2k_tile_cols, num_jp2k_tile_rows,
+ dtype,
+ use_rgba_interface,
+ jp2
+ )
- # the coordinates of the upper left pixel of the TIFF tile
- tulr = tiff_tile_row * th
- tulc = tiff_tile_col * tw
+ elif not isTiled and self.tilesize is not None:
- # loop while the left corner of the current tiff tile is
- # less than the right hand corner of the jp2k tile
- while ((c // tw) * tw) < min(julc + jtw, imagewidth):
+ self._write_striped_tiff_to_tiled_jp2k(
+ imagewidth, imageheight, spp,
+ jtw, jth, rps,
+ num_jp2k_tile_cols, num_jp2k_tile_rows,
+ dtype,
+ use_rgba_interface,
+ jp2
+ )
- if use_rgba_interface:
- libtiff.readRGBATile(
- self.tiff_fp, tulc, tulr, rgba_tile
- )
+ def tagvalue2str(self, cls, tag_value):
+ """
+ Take a class that encompasses all of a tag's allowed values and find
+ the name of that value.
+ """
- # flip the tile upside down!!
- tiff_tile = np.flipud(rgba_tile[:, :, :3])
- else:
- libtiff.readEncodedTile(
- self.tiff_fp, tilenum, tiff_tile
- )
+ tag_value_string = [
+ key for key in dir(cls) if getattr(cls, key) == tag_value
+ ][0]
- # determine how to fit this tiff tile into the jp2k
- # tile
- #
- # these are the section coordinates in image space
- ulr = max(julr, tulr)
- llr = min(julr + jth, tulr + th)
+ return tag_value_string
- ulc = max(julc, tulc)
- urc = min(julc + jtw, tulc + tw)
+ def _write_rgba_single_tile(
+ self, photo, imagewidth, imageheight, spp, jp2
+ ):
+ """
+ If no jp2k tiling was specified and if the image is ok to read
+ via the RGBA interface, then just do that. The image will be
+ written with the tilesize equal to the image size, so it will
+ be written using a single write operation.
- # convert to JP2K tile coordinates
- jrows = slice(ulr % jth, (llr - 1) % jth + 1)
- jcols = slice(ulc % jtw, (urc - 1) % jtw + 1)
+ Parameters
+ ----------
+ photo, imagewidth, imageheight, spp : int
+ TIFF tag values corresponding to the photometric interpretation,
+ image width and height, and samples per pixel
+ jp2 : JP2K object
+ Write to this JPEG2000 file
+ """
+ msg = (
+ "Reading using the RGBA interface, writing as a single tile "
+ "image."
+ )
+ self.logger.info(msg)
+
+ if photo not in [
+ libtiff.Photometric.MINISWHITE,
+ libtiff.Photometric.MINISBLACK,
+ libtiff.Photometric.PALETTE,
+ libtiff.Photometric.YCBCR,
+ libtiff.Photometric.RGB
+ ]:
+ photostr = self.tagvalue2str(libtiff.Photometric, photo)
+ msg = (
+ "Beware, the RGBA interface to attempt to read this TIFF "
+ f"when it has a PhotometricInterpretation of {photostr}."
+ )
+ warnings.warn(msg)
- # convert to TIFF tile coordinates
- trows = slice(ulr % th, (llr - 1) % th + 1)
- tcols = slice(ulc % tw, (urc - 1) % tw + 1)
+ image = libtiff.readRGBAImageOriented(
+ self.tiff_fp, imagewidth, imageheight
+ )
- jp2k_tile[jrows, jcols, :] = tiff_tile[trows, tcols, :]
+ if spp < 4:
+ image = image[:, :, :3]
- # move exactly one tiff tile over
- c += tw
+ jp2[:] = image
- tilenum = libtiff.computeTile(self.tiff_fp, c, r, 0, 0)
+ def _write_tiled_tiff_to_tiled_jp2k(
+ self, imagewidth, imageheight, spp, jtw, jth, tw, th,
+ num_jp2k_tile_cols, num_jp2k_tile_rows, dtype, use_rgba_interface, jp2
+ ):
+ """
+ The input TIFF image is tiled and we are to create the output JPEG2000
+ image with specific tile dimensions.
- tiff_tile_row = int(
- np.ceil(tilenum // num_tiff_tile_cols)
- )
- tiff_tile_col = int(
- np.ceil(tilenum % num_tiff_tile_cols)
- )
+ Parameters
+ ----------
+ imagewidth, imageheight, spp : int
+ TIFF tag values corresponding to the photometric interpretation,
+ image width and height, and samples per pixel.
+ jtw, jth : int
+ The tile dimensions for the JPEG2000 image.
+ tw, th : int
+ The tile dimensions for the TIFF image.
+ num_jp2k_tile_cols, num_jp2k_tile_rows
+ The number of tiles down the rows and across the columns for the
+ JPEG2000 image
+ dtype : np.dtype
+ Datatype of the image.
+ use_rgba_interface : bool
+ If true, use the RGBA interface to read the TIFF image data.
+ jp2 : JP2K object
+ Write to this JPEG2000 file
+ """
- # the coordinates of the upper left pixel of the TIFF
- # tile
- tulr = tiff_tile_row * th
- tulc = tiff_tile_col * tw
-
- r += th
-
- # last tile column? If so, we may have a partial tile.
- if (
- partial_jp2_tile_cols
- and jp2k_tile_col == num_jp2k_tile_cols - 1
- ):
- last_j2k_cols = slice(0, imagewidth - julc)
- jp2k_tile = jp2k_tile[:, last_j2k_cols, :].copy()
- if (
- partial_jp2_tile_rows
- and jp2k_tile_row == num_jp2k_tile_rows - 1
- ):
- last_j2k_rows = slice(0, imageheight - julr)
- jp2k_tile = jp2k_tile[last_j2k_rows, :, :].copy()
-
- tilewriter[:] = jp2k_tile
+ num_tiff_tile_cols = int(np.ceil(imagewidth / tw))
- elif not isTiled and self.tilesize is not None:
+ partial_jp2_tile_rows = (imageheight / jth) != (imageheight // jth)
+ partial_jp2_tile_cols = (imagewidth / jtw) != (imagewidth // jtw)
- num_strips = libtiff.numberOfStrips(self.tiff_fp)
+ rgba_tile = np.zeros((th, tw, 4), dtype=np.uint8)
- num_jp2k_tile_cols = int(np.ceil(imagewidth / jtw))
+ self.logger.debug(f'image: {imageheight} x {imagewidth}')
+ self.logger.debug(f'jptile: {jth} x {jtw}')
+ self.logger.debug(f'ttile: {th} x {tw}')
+ for idx, tilewriter in enumerate(jp2.get_tilewriters()):
- partial_jp2_tile_rows = (imageheight / jth) != (imageheight // jth)
- partial_jp2_tile_cols = (imagewidth / jtw) != (imagewidth // jtw)
+ # populate the jp2k tile with tiff tiles
+ self.logger.info(f'Tile: #{idx}')
+ self.logger.debug(f'J tile row: #{idx // num_jp2k_tile_cols}')
+ self.logger.debug(f'J tile col: #{idx % num_jp2k_tile_cols}')
- tiff_strip = np.zeros((rps, imagewidth, spp), dtype=dtype)
- rgba_strip = np.zeros((rps, imagewidth, 4), dtype=np.uint8)
+ jp2k_tile = np.zeros((jth, jtw, spp), dtype=dtype)
+ tiff_tile = np.zeros((th, tw, spp), dtype=dtype)
- for idx, tilewriter in enumerate(jp2.get_tilewriters()):
- self.logger.info(f'Tile: #{idx}')
+ jp2k_tile_row = int(np.ceil(idx // num_jp2k_tile_cols))
+ jp2k_tile_col = int(np.ceil(idx % num_jp2k_tile_cols))
- jp2k_tile = np.zeros((jth, jtw, spp), dtype=dtype)
+ # the coordinates of the upper left pixel of the jp2k tile
+ julr, julc = jp2k_tile_row * jth, jp2k_tile_col * jtw
- jp2k_tile_row = idx // num_jp2k_tile_cols
- jp2k_tile_col = idx % num_jp2k_tile_cols
+ # loop while the upper left corner of the current tiff file is
+ # less than the lower left corner of the jp2k tile
+ r = julr
+ while (r // th) * th < min(julr + jth, imageheight):
+ c = julc
- # the coordinates of the upper left pixel of the jp2k tile
- julr, julc = jp2k_tile_row * jth, jp2k_tile_col * jtw
+ tilenum = libtiff.computeTile(self.tiff_fp, c, r, 0, 0)
+ self.logger.debug(f'TIFF tile # {tilenum}')
- # Populate the jp2k tile with tiff strips.
- # Move by strips from the start of the jp2k tile to the bottom
- # of the jp2k tile. That last strip may be partially empty,
- # worry about that later.
- #
- # loop while the upper left corner of the current tiff file is
- # less than the lower left corner of the jp2k tile
- r = julr
- while (r // rps) * rps < min(julr + jth, imageheight):
+ tiff_tile_row = int(np.ceil(tilenum // num_tiff_tile_cols))
+ tiff_tile_col = int(np.ceil(tilenum % num_tiff_tile_cols))
- stripnum = libtiff.computeStrip(self.tiff_fp, r, 0)
+ # the coordinates of the upper left pixel of the TIFF tile
+ tulr = tiff_tile_row * th
+ tulc = tiff_tile_col * tw
- if stripnum >= num_strips:
- # we've moved past the end of the tiff
- break
+ # loop while the left corner of the current tiff tile is
+ # less than the right hand corner of the jp2k tile
+ while ((c // tw) * tw) < min(julc + jtw, imagewidth):
if use_rgba_interface:
-
- # must use the first row in the strip
- libtiff.readRGBAStrip(
- self.tiff_fp, stripnum * rps, rgba_strip
+ libtiff.readRGBATile(
+ self.tiff_fp, tulc, tulr, rgba_tile
)
- # must flip the rows (!!) and get rid of the alpha
- # plane
- tiff_strip = np.flipud(rgba_strip[:, :, :spp])
+ # flip the tile upside down!!
+ tiff_tile = np.flipud(rgba_tile[:, :, :3])
else:
- libtiff.readEncodedStrip(
- self.tiff_fp, stripnum, tiff_strip
+ libtiff.readEncodedTile(
+ self.tiff_fp, tilenum, tiff_tile
)
- # the coordinates of the upper left pixel of the TIFF
- # strip
- tulr = stripnum * rps
- tulc = 0
-
- # determine how to fit this tiff strip into the jp2k
+ # determine how to fit this tiff tile into the jp2k
# tile
#
# these are the section coordinates in image space
ulr = max(julr, tulr)
- llr = min(julr + jth, tulr + rps)
+ llr = min(julr + jth, tulr + th)
ulc = max(julc, tulc)
urc = min(julc + jtw, tulc + tw)
@@ -753,45 +835,179 @@ class Tiff2Jp2k(object):
jrows = slice(ulr % jth, (llr - 1) % jth + 1)
jcols = slice(ulc % jtw, (urc - 1) % jtw + 1)
- # convert to TIFF strip coordinates
- trows = slice(ulr % rps, (llr - 1) % rps + 1)
+ # convert to TIFF tile coordinates
+ trows = slice(ulr % th, (llr - 1) % th + 1)
tcols = slice(ulc % tw, (urc - 1) % tw + 1)
- jp2k_tile[jrows, jcols, :] = tiff_strip[trows, tcols, :]
-
- r += rps
-
- # last tile column? If so, we may have a partial tile.
- # j2k_cols is not sufficient here, must shorten it from 250
- # to 230
- if (
- partial_jp2_tile_cols
- and jp2k_tile_col == num_jp2k_tile_cols - 1
- ):
- # decrease the number of columns by however many it sticks
- # over the image width
- last_j2k_cols = slice(0, imagewidth - julc)
- jp2k_tile = jp2k_tile[:, last_j2k_cols, :].copy()
-
- if (
- partial_jp2_tile_rows
- and stripnum == num_strips - 1
- ):
- # decrease the number of rows by however many it sticks
- # over the image height
- last_j2k_rows = slice(0, imageheight - julr)
- jp2k_tile = jp2k_tile[last_j2k_rows, :, :].copy()
-
- tilewriter[:] = jp2k_tile
+ jp2k_tile[jrows, jcols, :] = tiff_tile[trows, tcols, :]
- def tagvalue2str(self, cls, tag_value):
+ # move exactly one tiff tile over
+ c += tw
+
+ tilenum = libtiff.computeTile(self.tiff_fp, c, r, 0, 0)
+
+ tiff_tile_row = int(
+ np.ceil(tilenum // num_tiff_tile_cols)
+ )
+ tiff_tile_col = int(
+ np.ceil(tilenum % num_tiff_tile_cols)
+ )
+
+ # the coordinates of the upper left pixel of the TIFF
+ # tile
+ tulr = tiff_tile_row * th
+ tulc = tiff_tile_col * tw
+
+ r += th
+
+ # last tile column? If so, we may have a partial tile.
+ if (
+ partial_jp2_tile_cols
+ and jp2k_tile_col == num_jp2k_tile_cols - 1
+ ):
+ last_j2k_cols = slice(0, imagewidth - julc)
+ jp2k_tile = jp2k_tile[:, last_j2k_cols, :].copy()
+ if (
+ partial_jp2_tile_rows
+ and jp2k_tile_row == num_jp2k_tile_rows - 1
+ ):
+ last_j2k_rows = slice(0, imageheight - julr)
+ jp2k_tile = jp2k_tile[last_j2k_rows, :, :].copy()
+
+ tilewriter[:] = jp2k_tile
+
+ def _write_striped_tiff_to_tiled_jp2k(
+ self, imagewidth, imageheight, spp, jtw, jth, rps,
+ num_jp2k_tile_cols, num_jp2k_tile_rows, dtype, use_rgba_interface, jp2
+ ):
"""
- Take a class that encompasses all of a tag's allowed values and find
- the name of that value.
+ The input TIFF image is striped and we are to create the output
+ JPEG2000 image as a single tile.
+
+ Parameters
+ ----------
+ imagewidth, imageheight, spp : int
+ TIFF tag values corresponding to the photometric interpretation,
+ image width and height, and samples per pixel.
+ jtw, jth : int
+ The tile dimensions for the JPEG2000 image.
+ rps : int
+ The number of rows per strip in the TIFF.
+ num_jp2k_tile_cols, num_jp2k_tile_rows
+ The number of tiles down the rows and across the columns for the
+ JPEG2000 image
+ dtype : np.dtype
+ Datatype of the image.
+ use_rgba_interface : bool
+ If true, use the RGBA interface to read the TIFF image data.
+ jp2 : JP2K object
+ Write to this JPEG2000 file
"""
- tag_value_string = [
- key for key in dir(cls) if getattr(cls, key) == tag_value
- ][0]
+ partial_jp2_tile_rows = (imageheight / jth) != (imageheight // jth)
+ partial_jp2_tile_cols = (imagewidth / jtw) != (imagewidth // jtw)
- return tag_value_string
+ self.logger.debug(f'image: {imageheight} x {imagewidth}')
+ self.logger.debug(f'jptile: {jth} x {jtw}')
+ num_strips = libtiff.numberOfStrips(self.tiff_fp)
+
+ num_jp2k_tile_cols = int(np.ceil(imagewidth / jtw))
+
+ partial_jp2_tile_rows = (imageheight / jth) != (imageheight // jth)
+ partial_jp2_tile_cols = (imagewidth / jtw) != (imagewidth // jtw)
+
+ tiff_strip = np.zeros((rps, imagewidth, spp), dtype=dtype)
+ rgba_strip = np.zeros((rps, imagewidth, 4), dtype=np.uint8)
+
+ for idx, tilewriter in enumerate(jp2.get_tilewriters()):
+ self.logger.info(f'Tile: #{idx}')
+
+ jp2k_tile = np.zeros((jth, jtw, spp), dtype=dtype)
+
+ jp2k_tile_row = idx // num_jp2k_tile_cols
+ jp2k_tile_col = idx % num_jp2k_tile_cols
+
+ # the coordinates of the upper left pixel of the jp2k tile
+ julr, julc = jp2k_tile_row * jth, jp2k_tile_col * jtw
+
+ # Populate the jp2k tile with tiff strips.
+ # Move by strips from the start of the jp2k tile to the bottom
+ # of the jp2k tile. That last strip may be partially empty,
+ # worry about that later.
+ #
+ # loop while the upper left corner of the current tiff file is
+ # less than the lower left corner of the jp2k tile
+ r = julr
+ while (r // rps) * rps < min(julr + jth, imageheight):
+
+ stripnum = libtiff.computeStrip(self.tiff_fp, r, 0)
+ self.logger.debug(f'Strip: #{stripnum}')
+
+ if stripnum >= num_strips:
+ # we've moved past the end of the tiff
+ break
+
+ if use_rgba_interface:
+
+ # must use the first row in the strip
+ libtiff.readRGBAStrip(
+ self.tiff_fp, stripnum * rps, rgba_strip
+ )
+ # must flip the rows (!!) and get rid of the alpha
+ # plane
+ tiff_strip = np.flipud(rgba_strip[:, :, :spp])
+
+ else:
+ libtiff.readEncodedStrip(
+ self.tiff_fp, stripnum, tiff_strip
+ )
+
+ # the coordinates of the upper left pixel of the TIFF
+ # strip
+ tulr = stripnum * rps
+ tulc = 0
+
+ # determine how to fit this tiff strip into the jp2k
+ # tile
+ #
+ # these are the section coordinates in image space
+ ulr = max(julr, tulr)
+ llr = min(julr + jth, tulr + rps)
+
+ ulc = max(julc, tulc)
+ urc = min(julc + jtw, tulc + imagewidth)
+
+ # convert to JP2K tile coordinates
+ jrows = slice(ulr % jth, (llr - 1) % jth + 1)
+ jcols = slice(ulc % jtw, (urc - 1) % jtw + 1)
+
+ # convert to TIFF strip coordinates
+ trows = slice(ulr % rps, (llr - 1) % rps + 1)
+ tcols = slice(ulc % imagewidth, (urc - 1) % imagewidth + 1)
+
+ jp2k_tile[jrows, jcols, :] = tiff_strip[trows, tcols, :]
+
+ r += rps
+
+ # last tile column? If so, we may have a partial tile.
+ # j2k_cols is not sufficient here, must shorten it from 250
+ # to 230
+ if (
+ partial_jp2_tile_cols
+ and jp2k_tile_col == num_jp2k_tile_cols - 1
+ ):
+ # decrease the number of columns by however many it sticks
+ # over the image width
+ last_j2k_cols = slice(0, imagewidth - julc)
+ jp2k_tile = jp2k_tile[:, last_j2k_cols, :].copy()
+
+ if (
+ partial_jp2_tile_rows
+ and stripnum == num_strips - 1
+ ):
+ # decrease the number of rows by however many it sticks
+ # over the image height
+ last_j2k_rows = slice(0, imageheight - julr)
+ jp2k_tile = jp2k_tile[last_j2k_rows, :, :].copy()
+
+ tilewriter[:] = jp2k_tile
=====================================
glymur/version.py
=====================================
@@ -21,7 +21,7 @@ from .lib import tiff
# Do not change the format of this next line! Doing so risks breaking
# setup.py
-version = "0.10.2"
+version = "0.11.0"
version_tuple = parse(version).release
=====================================
setup.cfg
=====================================
@@ -1,6 +1,6 @@
[metadata]
name = Glymur
-version = 0.10.2
+version = 0.11.0
author = 'John Evans'
author_email = "John Evans" <john.g.evans.ne at gmail.com>
license = 'MIT'
=====================================
tests/test_jp2box_uuid.py
=====================================
@@ -4,6 +4,7 @@
# Standard library imports
import importlib.resources as ir
import io
+import platform
import shutil
import struct
import unittest
@@ -232,17 +233,23 @@ class TestSuite(fixtures.TestCommon):
expected = uuid.UUID(bytes=b'JpgTiffExif->JP2')
self.assertEqual(actual, expected)
+ @unittest.skipIf(
+ platform.system().startswith('Windows'),
+ "Skipping on windows, see issue 560"
+ )
def test__printing__geotiff_uuid__xml_sidecar(self):
"""
- SCENARIO: Print a geotiff UUID with XML sidecar file
+ SCENARIO: Print a geotiff UUID with XML sidecar file.
- EXPECTED RESULT: Should not error out.
+ EXPECTED RESULT: Should not error out. There is a warning about GDAL
+ not being able to print the UUID data as expected.
"""
box_data = ir.read_binary('tests.data', '0220000800_uuid.dat')
bf = io.BytesIO(box_data)
bf.seek(8)
box = UUIDBox.parse(bf, 0, 703)
- str(box)
+ with self.assertWarns(UserWarning):
+ str(box)
def test_append_xmp_uuid(self):
"""
@@ -347,29 +354,26 @@ class TestSuite(fixtures.TestCommon):
expected = 'UTM Zone 16N NAD27"|Clarke, 1866 by Default| '
self.assertEqual(box.data['GeoAsciiParams'], expected)
- @unittest.skip('not sure why this was corrupt')
+ @unittest.skipIf(
+ platform.system().startswith('Windows'),
+ "Skipping on windows, see issue 560"
+ )
def test_print_bad_geotiff(self):
"""
SCENARIO: A GeoTIFF UUID is corrupt.
- EXPECTED RESULT: The string representation should validate and clearly
- state that the UUID box is corrupt.
+ EXPECTED RESULT: No errors. There is a warning issued when we try
+ to print the box.
"""
+ self.maxDiff = None
with ir.path(data, 'issue398.dat') as path:
with path.open('rb') as f:
f.seek(8)
- with warnings.catch_warnings():
- # Ignore the warnings about invalid TIFF tags, we already
- # know that.
- warnings.simplefilter('ignore')
+ with warnings.catch_warnings(record=True) as w:
box = glymur.jp2box.UUIDBox.parse(f, 0, 380)
+ str(box)
- actual = str(box)
- expected = ("UUID Box (uuid) @ (0, 380)\n"
- " UUID: "
- "b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03 (GeoTIFF)\n"
- " UUID Data: corrupt")
- self.assertEqual(actual, expected)
+ self.assertEqual(len(w), 1)
class TestSuiteHiRISE(fixtures.TestCommon):
=====================================
tests/test_jp2k.py
=====================================
@@ -9,6 +9,7 @@ import importlib.resources as ir
from io import BytesIO
import os
import pathlib
+import shutil
import struct
import tempfile
import time
@@ -359,8 +360,13 @@ class TestJp2k(fixtures.TestCommon):
with self.assertRaises(InvalidJp2kError):
Jp2k(path)
- def test_read_from_a_file_that_does_not_exist(self):
- """Should error out if reading from a file that does not exist"""
+ def test_file_does_not_exist(self):
+ """
+ Scenario: The Jp2k construtor is passed a file that does not exist
+ and the intent is reading.
+
+ Expected Result: FileNotFoundError
+ """
# Verify that we error out appropriately if not given an existing file
# at all.
filename = 'this file does not actually exist on the file system.'
@@ -1120,6 +1126,88 @@ class TestJp2k_write(fixtures.MetadataBase):
else:
self.assertEqual(len(w), 1)
+ def test_capture_resolution(self):
+ """
+ SCENARIO: The capture_resolution keyword is specified.
+
+ EXPECTED RESULT: The cres box is created.
+ """
+ vresc, hresc = 0.1, 0.2
+ vresd, hresd = 0.3, 0.4
+ j = glymur.Jp2k(
+ self.temp_jp2_filename, data=self.jp2_data,
+ capture_resolution=[vresc, hresc],
+ display_resolution=[vresd, hresd],
+ )
+
+ self.assertEqual(j.box[-1].box_id, 'res ')
+
+ self.assertEqual(j.box[-1].box[0].box_id, 'resc')
+ self.assertEqual(j.box[-1].box[0].vertical_resolution, vresc)
+ self.assertEqual(j.box[-1].box[0].horizontal_resolution, hresc)
+
+ self.assertEqual(j.box[-1].box[1].box_id, 'resd')
+ self.assertEqual(j.box[-1].box[1].vertical_resolution, vresd)
+ self.assertEqual(j.box[-1].box[1].horizontal_resolution, hresd)
+
+ def test_capture_resolution_when_j2k_specified(self):
+ """
+ Scenario: Capture/Display resolution boxes are specified when the file
+ name indicates J2K.
+
+ Expected Result: InvalidJp2kError
+ """
+
+ vresc, hresc = 0.1, 0.2
+ vresd, hresd = 0.3, 0.4
+ with self.assertRaises(InvalidJp2kError):
+ glymur.Jp2k(
+ self.temp_j2k_filename, data=self.jp2_data,
+ capture_resolution=[vresc, hresc],
+ display_resolution=[vresd, hresd],
+ )
+
+ def test_capture_resolution_when_not_writing(self):
+ """
+ Scenario: Jp2k is invoked in a read-only situation but capture/display
+ resolution arguments are supplied.
+
+ Expected result: RuntimeError
+ """
+ vresc, hresc = 0.1, 0.2
+ vresd, hresd = 0.3, 0.4
+
+ shutil.copyfile(self.jp2file, self.temp_jp2_filename)
+
+ with self.assertRaises(RuntimeError):
+ glymur.Jp2k(
+ self.temp_jp2_filename,
+ capture_resolution=[vresc, hresc],
+ display_resolution=[vresd, hresd],
+ )
+
+ def test_one_of_capture_display_resolution_but_not_both(self):
+ """
+ Scenario: Writing a JP2 is intended, but not both of capture/display
+ resolution key word parameters are supplied.
+
+ Expected Result: RuntimeError
+ """
+ vresc, hresc = 0.1, 0.2
+ vresd, hresd = 0.3, 0.4
+
+ with self.assertRaises(RuntimeError):
+ glymur.Jp2k(
+ self.temp_jp2_filename, data=self.jp2_data,
+ capture_resolution=[vresc, hresc],
+ )
+
+ with self.assertRaises(RuntimeError):
+ glymur.Jp2k(
+ self.temp_jp2_filename, data=self.jp2_data,
+ display_resolution=[vresd, hresd],
+ )
+
def test_no_jp2c_box_in_outermost_jp2_list(self):
"""
SCENARIO: A JP2 file is encountered without a JP2C box in the outer-
=====================================
tests/test_tiff2jp2.py
=====================================
@@ -53,10 +53,10 @@ class TestSuite(fixtures.TestCommon):
f.write(strip)
f.write(strip)
- # write a minimal IFD. with 10 tags
- main_ifd_data_offset = main_ifd_offset + 2 + 10 * 12 + 4
+ # write an IFD with 11 tags
+ main_ifd_data_offset = main_ifd_offset + 2 + 11 * 12 + 4
- buffer = struct.pack('<H', 10)
+ buffer = struct.pack('<H', 11)
f.write(buffer)
# width and length and bitspersample
@@ -87,6 +87,10 @@ class TestSuite(fixtures.TestCommon):
buffer = struct.pack('<HHII', 279, 4, 4, main_ifd_data_offset + 16)
f.write(buffer)
+ # pagenumber
+ buffer = struct.pack('<HHIHH', 297, 3, 2, 1, 0)
+ f.write(buffer)
+
# XMP
with ir.path('tests.data', 'issue555.xmp') as xmp_path:
with xmp_path.open() as f2:
@@ -566,20 +570,40 @@ class TestSuite(fixtures.TestCommon):
def tearDownClass(cls):
shutil.rmtree(cls.test_tiff_dir)
- def test_exif(self):
+ def test_exclude_tags_camelcase(self):
"""
- Scenario: Convert TIFF with Exif IFD to JP2
+ Scenario: Convert TIFF to JP2, but exclude the StripByteCounts and
+ StripOffsets tags. Supply the argments as camel-case.
- Expected Result: No errors. The Exif LensModel tag is recoverable
- from the UUIDbox.
+ Expected Result: No warnings, no errors. The Exif LensModel tag is
+ recoverable from the UUIDbox.
"""
with Tiff2Jp2k(
- self.exif_tiff, self.temp_jp2_filename, verbosity=logging.DEBUG
+ self.exif_tiff, self.temp_jp2_filename,
+ exclude_tags=['StripOffsets', 'StripByteCounts']
) as p:
p.run()
j = Jp2k(self.temp_jp2_filename)
+ tags = j.box[-1].data
+ self.assertNotIn('StripByteCounts', tags)
+ self.assertNotIn('StripOffsets', tags)
+
+ def test_exif(self):
+ """
+ Scenario: Convert TIFF with Exif IFD to JP2
+
+ Expected Result: No warnings, no errors. The Exif LensModel tag is
+ recoverable from the UUIDbox.
+ """
+ with Tiff2Jp2k(self.exif_tiff, self.temp_jp2_filename) as p:
+ with warnings.catch_warnings(record=True) as w:
+ p.run()
+ self.assertEqual(len(w), 0)
+
+ j = Jp2k(self.temp_jp2_filename)
+
tags = j.box[-1].data
self.assertEqual(tags['ExifTag']['LensModel'], 'Canon')
@@ -645,9 +669,10 @@ class TestSuite(fixtures.TestCommon):
def test_geotiff(self):
"""
- SCENARIO: Convert GEOTIFF file to JP2
+ SCENARIO: Convert a one-component GEOTIFF file to JP2
- EXPECTED RESULT: there is a geotiff UUID.
+ EXPECTED RESULT: there is a geotiff UUID. The JP2 file has only one
+ component.
"""
with warnings.catch_warnings():
warnings.simplefilter('ignore')
@@ -661,6 +686,7 @@ class TestSuite(fixtures.TestCommon):
self.assertEqual(
j.box[-1].uuid, UUID('b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03')
)
+ self.assertEqual(j.box[2].box[0].num_components, 1)
def test_no_uuid(self):
"""
@@ -671,7 +697,7 @@ class TestSuite(fixtures.TestCommon):
"""
with Tiff2Jp2k(
self.astronaut_ycbcr_jpeg_tif, self.temp_jp2_filename,
- create_uuid=False
+ create_exif_uuid=False
) as j:
j.run()
@@ -1350,6 +1376,27 @@ class TestSuite(fixtures.TestCommon):
self.assertEqual(c.segment[1].xtsiz, 256)
self.assertEqual(c.segment[1].ytsiz, 256)
+ def test_commandline_tiff2jp2_exclude_tags_numeric(self):
+ """
+ Scenario: patch sys such that we can run the command line tiff2jp2
+ script. Exclude TileByteCounts and TileByteOffsets, but provide those
+ tags as numeric values.
+
+ Expected Results: Same as test_astronaut.
+ """
+ sys.argv = [
+ '', str(self.astronaut_tif), str(self.temp_jp2_filename),
+ '--tilesize', '256', '256',
+ '--exclude-tags', '324', '325'
+ ]
+ command_line.tiff2jp2()
+
+ jp2 = Jp2k(self.temp_jp2_filename)
+ tags = jp2.box[-1].data
+
+ self.assertNotIn('TileByteCounts', tags)
+ self.assertNotIn('TileOffsets', tags)
+
def test_cmyk(self):
"""
Scenario: CMYK (or separated) is not a supported colorspace.
@@ -1387,6 +1434,27 @@ class TestSuite(fixtures.TestCommon):
with self.assertRaises(RuntimeError):
j.run()
+ def test_commandline_tiff2jp2_exclude_tags(self):
+ """
+ Scenario: patch sys such that we can run the command line tiff2jp2
+ script. Exclude TileByteCounts and TileByteOffsets
+
+ Expected Results: TileByteCounts and TileOffsets are not in the EXIF
+ UUID.
+ """
+ sys.argv = [
+ '', str(self.astronaut_tif), str(self.temp_jp2_filename),
+ '--tilesize', '256', '256',
+ '--exclude-tags', 'tilebytecounts', 'tileoffsets'
+ ]
+ command_line.tiff2jp2()
+
+ jp2 = Jp2k(self.temp_jp2_filename)
+ tags = jp2.box[-1].data
+
+ self.assertNotIn('TileByteCounts', tags)
+ self.assertNotIn('TileOffsets', tags)
+
class TestSuiteNoScikitImage(fixtures.TestCommon):
@@ -1398,6 +1466,127 @@ class TestSuiteNoScikitImage(fixtures.TestCommon):
cls.setup_rgb_evenly_stripped(cls.test_tiff_path / 'goodstuff.tif')
+ cls.setup_exif(cls.test_tiff_path / 'exif.tif')
+
+ @classmethod
+ def setup_exif(cls, path):
+ """
+ Create a simple TIFF file that is constructed to contain an EXIF IFD.
+ """
+
+ with path.open(mode='wb') as f:
+
+ w = 256
+ h = 256
+ rps = 64
+ header_length = 8
+
+ # write the header (8 bytes). The IFD will follow the image data
+ # (256x256 bytes), so the offset to the IFD will be 8 + h * w.
+ main_ifd_offset = header_length + h * w
+ buffer = struct.pack('<BBHI', 73, 73, 42, main_ifd_offset)
+ f.write(buffer)
+
+ # write the image data, 4 64x256 strips of all zeros
+ strip = bytes([0] * rps * w)
+ f.write(strip)
+ f.write(strip)
+ f.write(strip)
+ f.write(strip)
+
+ # write an IFD with 11 tags
+ main_ifd_data_offset = main_ifd_offset + 2 + 11 * 12 + 4
+
+ buffer = struct.pack('<H', 11)
+ f.write(buffer)
+
+ # width and length and bitspersample
+ buffer = struct.pack('<HHII', 256, 4, 1, w)
+ f.write(buffer)
+ buffer = struct.pack('<HHII', 257, 4, 1, h)
+ f.write(buffer)
+ buffer = struct.pack('<HHII', 258, 4, 1, 8)
+ f.write(buffer)
+
+ # photometric
+ buffer = struct.pack('<HHII', 262, 4, 1, 1)
+ f.write(buffer)
+
+ # strip offsets
+ buffer = struct.pack('<HHII', 273, 4, 4, main_ifd_data_offset)
+ f.write(buffer)
+
+ # spp
+ buffer = struct.pack('<HHII', 277, 4, 1, 1)
+ f.write(buffer)
+
+ # rps
+ buffer = struct.pack('<HHII', 278, 4, 1, 64)
+ f.write(buffer)
+
+ # strip byte counts
+ buffer = struct.pack('<HHII', 279, 4, 4, main_ifd_data_offset + 16)
+ f.write(buffer)
+
+ # pagenumber
+ buffer = struct.pack('<HHIHH', 297, 3, 2, 1, 0)
+ f.write(buffer)
+
+ # XMP
+ with ir.path('tests.data', 'issue555.xmp') as xmp_path:
+ with xmp_path.open() as f2:
+ xmp = f2.read()
+ xmp = xmp + '\0'
+ buffer = struct.pack(
+ '<HHII', 700, 1, len(xmp), main_ifd_data_offset + 32
+ )
+ f.write(buffer)
+
+ # exif tag
+ exif_ifd_offset = main_ifd_data_offset + 32 + len(xmp)
+ buffer = struct.pack('<HHII', 34665, 4, 1, exif_ifd_offset)
+ f.write(buffer)
+
+ # terminate the IFD
+ buffer = struct.pack('<I', 0)
+ f.write(buffer)
+
+ # write the strip offsets here
+ buffer = struct.pack(
+ '<IIII', 8, 8 + rps*w, 8 + 2*rps*w, 8 + 3*rps*w
+ )
+ f.write(buffer)
+
+ # write the strip byte counts
+ buffer = struct.pack('<IIII', rps*w, rps*w, rps*w, rps*w)
+ f.write(buffer)
+
+ # write the XMP data
+ f.write(xmp.encode('utf-8'))
+
+ # write a minimal Exif IFD
+ buffer = struct.pack('<H', 2)
+ f.write(buffer)
+
+ # exposure program
+ buffer = struct.pack('<HHIHH', 34850, 3, 1, 2, 0)
+ f.write(buffer)
+
+ # lens model
+ data_location = exif_ifd_offset + 2 + 2*12 + 4
+ buffer = struct.pack('<HHII', 42036, 2, 6, data_location)
+ f.write(buffer)
+
+ # terminate the IFD
+ buffer = struct.pack('<I', 0)
+ f.write(buffer)
+
+ data = 'Canon\0'.encode('utf-8')
+ buffer = struct.pack('<BBBBBB', *data)
+ f.write(buffer)
+
+ cls.exif_tiff = path
+
@classmethod
def setup_rgb_evenly_stripped(cls, path):
"""
@@ -1433,7 +1622,8 @@ class TestSuiteNoScikitImage(fixtures.TestCommon):
"""
Scenario: input TIFF is organized by strips and logging is turned on.
- Expected result: there are 104 log messages, one for each tile
+ Expected result: there are 104 log messages. These messages come from
+ the tiles (a 13x8 grid of tiles).
"""
with Tiff2Jp2k(
self.goodstuff_path, self.temp_jp2_filename, tilesize=(64, 64),
@@ -1490,3 +1680,222 @@ class TestSuiteNoScikitImage(fixtures.TestCommon):
self.assertEqual(c.segment[1].ysiz, 800)
self.assertEqual(c.segment[1].xtsiz, 75)
self.assertEqual(c.segment[1].ytsiz, 75)
+
+ def test_exclude_tags(self):
+ """
+ Scenario: Convert TIFF to JP2, but exclude the StripByteCounts and
+ StripOffsets tags.
+
+ Expected Result: No warnings, no errors. The Exif LensModel tag is
+ recoverable from the UUIDbox.
+ """
+ with Tiff2Jp2k(
+ self.exif_tiff, self.temp_jp2_filename,
+ exclude_tags=[273, 'stripbytecounts']
+ ) as p:
+ p.run()
+
+ j = Jp2k(self.temp_jp2_filename)
+
+ tags = j.box[-1].data
+ self.assertNotIn('StripByteCounts', tags)
+ self.assertNotIn('StripOffsets', tags)
+
+ str(j.box[-1])
+
+ def test_exclude_tags_but_specify_a_bad_tag(self):
+ """
+ Scenario: Convert TIFF to JP2, but exclude the StripByteCounts and
+ StripOffsets tags. In addition, specify a tag that is not recognized.
+
+ Expected Result: The results should be the same as the previous
+ test except that a warning is issued due to the bad tag.
+ """
+ with self.assertWarns(UserWarning):
+ with Tiff2Jp2k(
+ self.exif_tiff, self.temp_jp2_filename,
+ exclude_tags=[273, 'stripbytecounts', 'gdalstuff']
+ ) as p:
+ p.run()
+
+ j = Jp2k(self.temp_jp2_filename)
+
+ tags = j.box[-1].data
+ self.assertNotIn('StripByteCounts', tags)
+ self.assertNotIn('StripOffsets', tags)
+
+ def test_exclude_tags_camelcase(self):
+ """
+ Scenario: Convert TIFF to JP2, but exclude the StripByteCounts and
+ StripOffsets tags. Supply the argments as camel-case.
+
+ Expected Result: No warnings, no errors. The Exif LensModel tag is
+ recoverable from the UUIDbox.
+ """
+ with Tiff2Jp2k(
+ self.exif_tiff, self.temp_jp2_filename,
+ exclude_tags=['StripOffsets', 'StripByteCounts']
+ ) as p:
+ p.run()
+
+ j = Jp2k(self.temp_jp2_filename)
+
+ tags = j.box[-1].data
+ self.assertNotIn('StripByteCounts', tags)
+ self.assertNotIn('StripOffsets', tags)
+
+ def test_exif(self):
+ """
+ Scenario: Convert TIFF with Exif IFD to JP2
+
+ Expected Result: No warnings, no errors. The Exif LensModel tag is
+ recoverable from the UUIDbox.
+ """
+ with Tiff2Jp2k(self.exif_tiff, self.temp_jp2_filename) as p:
+ with warnings.catch_warnings(record=True) as w:
+ p.run()
+ self.assertEqual(len(w), 0)
+
+ j = Jp2k(self.temp_jp2_filename)
+
+ tags = j.box[-1].data
+ self.assertEqual(tags['ExifTag']['LensModel'], 'Canon')
+
+ str(j.box[-1])
+
+ def test_xmp(self):
+ """
+ Scenario: Convert TIFF with Exif IFD to JP2. The main IFD has an
+ XML Packet tag (700). Supply the 'xmp_uuid' keyword.
+
+ Expected Result: The XMLPacket tag is removed from the main IFD.
+ An Exif UUID is appended to the end of the JP2 file, and then an XMP
+ UUID is appended.
+ """
+ with Tiff2Jp2k(
+ self.exif_tiff, self.temp_jp2_filename, create_xmp_uuid=True
+ ) as p:
+ p.run()
+
+ j = Jp2k(self.temp_jp2_filename)
+
+ # first we find the Exif UUID, then the XMP UUID. The Exif UUID
+ # data should not have the XMLPacket tag.
+ actual = j.box[-2].uuid
+ expected = UUID(bytes=b'JpgTiffExif->JP2')
+ self.assertEqual(actual, expected)
+ self.assertNotIn('XMLPacket', j.box[-2].data)
+
+ actual = j.box[-1].uuid
+ expected = UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
+ self.assertEqual(actual, expected)
+ self.assertEqual(
+ j.box[-1].data.getroot().values(), ['Public XMP Toolkit Core 3.5']
+ )
+
+ def test_commandline_capture_display_resolution(self):
+ """
+ Scenario: patch sys such that we can run the command line tiff2jp2
+ script. Supply the --capture-resolution and --display-resolution
+ arguments.
+
+ Expected Result: The last box is a ResolutionBox.
+ """
+ vresc, hresc = 0.1, 0.2
+ vresd, hresd = 0.3, 0.4
+
+ sys.argv = [
+ '', str(self.exif_tiff), str(self.temp_jp2_filename),
+ '--tilesize', '64', '64',
+ '--capture-resolution', str(vresc), str(hresc),
+ '--display-resolution', str(vresd), str(hresd),
+ ]
+ command_line.tiff2jp2()
+
+ j = Jp2k(self.temp_jp2_filename)
+
+ # the resolution superbox is appended after the codestream, but before
+ # the exif uuid
+ self.assertEqual(j.box[-2].box_id, 'res ')
+ self.assertEqual(j.box[-1].box_id, 'uuid')
+
+ self.assertEqual(j.box[-2].box[0].box_id, 'resc')
+ self.assertEqual(j.box[-2].box[0].vertical_resolution, vresc)
+ self.assertEqual(j.box[-2].box[0].horizontal_resolution, hresc)
+
+ self.assertEqual(j.box[-2].box[1].box_id, 'resd')
+ self.assertEqual(j.box[-2].box[1].vertical_resolution, vresd)
+ self.assertEqual(j.box[-2].box[1].horizontal_resolution, hresd)
+
+ def test_commandline_tiff2jp2_xmp_uuid(self):
+ """
+ Scenario: patch sys such that we can run the command line tiff2jp2
+ script. Use the --create-xmp-uuid option.
+
+ Expected Result: The XMLPacket tag is removed from the main IFD.
+ An Exif UUID is appended to the end of the JP2 file, and then an XMP
+ UUID is appended.
+ """
+ sys.argv = [
+ '', str(self.exif_tiff), str(self.temp_jp2_filename),
+ '--tilesize', '64', '64',
+ '--create-xmp-uuid'
+ ]
+ command_line.tiff2jp2()
+
+ j = Jp2k(self.temp_jp2_filename)
+
+ # first we find the Exif UUID, then the XMP UUID. The Exif UUID
+ # data should not have the XMLPacket tag.
+ actual = j.box[-2].uuid
+ expected = UUID(bytes=b'JpgTiffExif->JP2')
+ self.assertEqual(actual, expected)
+ self.assertNotIn('XMLPacket', j.box[-2].data)
+
+ actual = j.box[-1].uuid
+ expected = UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
+ self.assertEqual(actual, expected)
+ self.assertEqual(
+ j.box[-1].data.getroot().values(), ['Public XMP Toolkit Core 3.5']
+ )
+
+ def test_one_component_no_tilesize(self):
+ """
+ Scenario: The jp2 tilesize is the same as the image size.
+
+ Expected Result: No errors.
+ """
+ with Tiff2Jp2k(
+ self.exif_tiff, self.temp_jp2_filename,
+ ) as p:
+ p.run()
+
+ j = Jp2k(self.temp_jp2_filename)
+ self.assertEqual(j.box[2].box[0].num_components, 1)
+
+ @unittest.skip('segfaulting')
+ def test_one_component_tilesize(self):
+ """
+ Scenario: The jp2 tilesize is the same as the image size,
+ and the tilesize is specified.
+
+ Expected Result: No errors.
+ """
+ with Tiff2Jp2k(
+ self.exif_tiff, self.temp_jp2_filename, tilesize=[256, 256]
+ ) as p:
+ p.run()
+
+ Jp2k(self.temp_jp2_filename)
+
+ def test_not_a_tiff(self):
+ """
+ Scenario: The input "TIFF" is not actually a TIFF. This used to
+ segfault.
+
+ Expected Result: no segfault
+ """
+ with self.assertRaises(RuntimeError):
+ with ir.path('tests.data', 'simple_rdf.txt') as path:
+ with Tiff2Jp2k(path, self.temp_jp2_filename):
+ pass
View it on GitLab: https://salsa.debian.org/debian-gis-team/glymur/-/compare/6035577dfc833f3b32d8134f5f98ba1fecdcf020...1f121789e2b5e611cd0b2017d6b2fce2b924c4fc
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/glymur/-/compare/6035577dfc833f3b32d8134f5f98ba1fecdcf020...1f121789e2b5e611cd0b2017d6b2fce2b924c4fc
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20220805/4c5b788a/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list