Glymur 0.13.8__tar.gz → 0.14.0.post2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {glymur-0.13.8 → glymur-0.14.0.post2}/CHANGES.txt +8 -0
  2. {glymur-0.13.8 → glymur-0.14.0.post2}/Glymur.egg-info/PKG-INFO +5 -4
  3. {glymur-0.13.8 → glymur-0.14.0.post2}/Glymur.egg-info/SOURCES.txt +7 -0
  4. {glymur-0.13.8 → glymur-0.14.0.post2}/Glymur.egg-info/entry_points.txt +1 -0
  5. {glymur-0.13.8 → glymur-0.14.0.post2}/Glymur.egg-info/requires.txt +1 -0
  6. {glymur-0.13.8 → glymur-0.14.0.post2}/PKG-INFO +5 -4
  7. {glymur-0.13.8 → glymur-0.14.0.post2}/README.md +1 -1
  8. glymur-0.14.0.post2/a.txt +91 -0
  9. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/__init__.py +2 -1
  10. glymur-0.14.0.post2/glymur/_core_converter.py +390 -0
  11. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/command_line.py +129 -1
  12. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/jp2box.py +31 -8
  13. glymur-0.14.0.post2/glymur/jpeg.py +196 -0
  14. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/lib/tiff.py +132 -4
  15. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/tiff.py +28 -359
  16. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/version.py +1 -1
  17. {glymur-0.13.8 → glymur-0.14.0.post2}/setup.cfg +4 -3
  18. glymur-0.14.0.post2/tests/test_commandline_jp2dump.py +199 -0
  19. glymur-0.14.0.post2/tests/test_commandline_jpeg2jp2.py +253 -0
  20. glymur-0.14.0.post2/tests/test_commandline_tiff2jp2.py +498 -0
  21. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_geo.py +5 -4
  22. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_jp2box_uuid.py +6 -4
  23. glymur-0.14.0.post2/tests/test_jpeg2jp2.py +242 -0
  24. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_printing.py +1 -168
  25. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_tiff2jp2.py +16 -307
  26. {glymur-0.13.8 → glymur-0.14.0.post2}/Glymur.egg-info/dependency_links.txt +0 -0
  27. {glymur-0.13.8 → glymur-0.14.0.post2}/Glymur.egg-info/not-zip-safe +0 -0
  28. {glymur-0.13.8 → glymur-0.14.0.post2}/Glymur.egg-info/top_level.txt +0 -0
  29. {glymur-0.13.8 → glymur-0.14.0.post2}/LICENSE.txt +0 -0
  30. {glymur-0.13.8 → glymur-0.14.0.post2}/MANIFEST.in +0 -0
  31. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/_iccprofile.py +0 -0
  32. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/codestream.py +0 -0
  33. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/config.py +0 -0
  34. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/core.py +0 -0
  35. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/data/__init__.py +0 -0
  36. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/data/goodstuff.j2k +0 -0
  37. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/data/heliov.jpx +0 -0
  38. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/data/nemo.jp2 +0 -0
  39. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/jp2k.py +0 -0
  40. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/jp2kr.py +0 -0
  41. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/lib/__init__.py +0 -0
  42. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/lib/openjp2.py +0 -0
  43. {glymur-0.13.8 → glymur-0.14.0.post2}/glymur/options.py +0 -0
  44. {glymur-0.13.8 → glymur-0.14.0.post2}/pyproject.toml +0 -0
  45. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_callbacks.py +0 -0
  46. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_cinema.py +0 -0
  47. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_codestream.py +0 -0
  48. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_colour_specification_box.py +0 -0
  49. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_config.py +0 -0
  50. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_jp2box.py +0 -0
  51. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_jp2box_jpx.py +0 -0
  52. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_jp2box_xml.py +0 -0
  53. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_jp2k.py +0 -0
  54. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_jp2k_writes.py +0 -0
  55. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_jp2kr.py +0 -0
  56. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_libtiff.py +0 -0
  57. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_openjp2.py +0 -0
  58. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_set_decoded_components.py +0 -0
  59. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_slicing.py +0 -0
  60. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_threading.py +0 -0
  61. {glymur-0.13.8 → glymur-0.14.0.post2}/tests/test_warnings.py +0 -0
@@ -1,3 +1,11 @@
1
+ Mar 29, 2025 - v0.14.0post2
2
+ Fix python_requires value
3
+
4
+ Mar 19, 2025 - v0.14.0
5
+ Add description of raw IFD for geojp2 UUIDs
6
+ Add feature to convert JPEGs to JP2
7
+ Remove support for python 3.10
8
+
1
9
  Jan 14, 2025 - v0.13.8
2
10
  Fix tiff2jp2 bug when stripped TIFF has no RowsPerStrip tag
3
11
 
@@ -1,12 +1,11 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: Glymur
3
- Version: 0.13.8
3
+ Version: 0.14.0.post2
4
4
  Home-page: https://github.com/quintusdias/glymur
5
5
  Author: 'John Evans'
6
6
  Author-email: "John Evans" <jevans667cc@proton.me>
7
7
  License: 'MIT'
8
8
  Classifier: Programming Language :: Python
9
- Classifier: Programming Language :: Python :: 3.10
10
9
  Classifier: Programming Language :: Python :: 3.11
11
10
  Classifier: Programming Language :: Python :: 3.12
12
11
  Classifier: Programming Language :: Python :: 3.13
@@ -15,16 +14,18 @@ Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Intended Audience :: Science/Research
16
15
  Classifier: Operating System :: OS Independent
17
16
  Classifier: Topic :: Scientific/Engineering
18
- Requires-Python: >=3.9
17
+ Requires-Python: >=3.11
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE.txt
21
20
  Requires-Dist: numpy
22
21
  Requires-Dist: lxml
22
+ Requires-Dist: imageio
23
23
  Requires-Dist: packaging
24
24
  Provides-Extra: test
25
25
  Requires-Dist: pytest; extra == "test"
26
26
  Requires-Dist: pillow; extra == "test"
27
27
  Requires-Dist: scikit-image; extra == "test"
28
+ Dynamic: license-file
28
29
 
29
30
 
30
31
  **glymur** contains a Python interface to the OpenJPEG library which
@@ -2,6 +2,7 @@ CHANGES.txt
2
2
  LICENSE.txt
3
3
  MANIFEST.in
4
4
  README.md
5
+ a.txt
5
6
  pyproject.toml
6
7
  setup.cfg
7
8
  Glymur.egg-info/PKG-INFO
@@ -12,6 +13,7 @@ Glymur.egg-info/not-zip-safe
12
13
  Glymur.egg-info/requires.txt
13
14
  Glymur.egg-info/top_level.txt
14
15
  glymur/__init__.py
16
+ glymur/_core_converter.py
15
17
  glymur/_iccprofile.py
16
18
  glymur/codestream.py
17
19
  glymur/command_line.py
@@ -20,6 +22,7 @@ glymur/core.py
20
22
  glymur/jp2box.py
21
23
  glymur/jp2k.py
22
24
  glymur/jp2kr.py
25
+ glymur/jpeg.py
23
26
  glymur/options.py
24
27
  glymur/tiff.py
25
28
  glymur/version.py
@@ -34,6 +37,9 @@ tests/test_callbacks.py
34
37
  tests/test_cinema.py
35
38
  tests/test_codestream.py
36
39
  tests/test_colour_specification_box.py
40
+ tests/test_commandline_jp2dump.py
41
+ tests/test_commandline_jpeg2jp2.py
42
+ tests/test_commandline_tiff2jp2.py
37
43
  tests/test_config.py
38
44
  tests/test_geo.py
39
45
  tests/test_jp2box.py
@@ -43,6 +49,7 @@ tests/test_jp2box_xml.py
43
49
  tests/test_jp2k.py
44
50
  tests/test_jp2k_writes.py
45
51
  tests/test_jp2kr.py
52
+ tests/test_jpeg2jp2.py
46
53
  tests/test_libtiff.py
47
54
  tests/test_openjp2.py
48
55
  tests/test_printing.py
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
2
  jp2dump = glymur.command_line:main
3
+ jpeg2jp2 = glymur.command_line:jpeg2jp2
3
4
  tiff2jp2 = glymur.command_line:tiff2jp2
@@ -1,5 +1,6 @@
1
1
  numpy
2
2
  lxml
3
+ imageio
3
4
  packaging
4
5
 
5
6
  [test]
@@ -1,12 +1,11 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: Glymur
3
- Version: 0.13.8
3
+ Version: 0.14.0.post2
4
4
  Home-page: https://github.com/quintusdias/glymur
5
5
  Author: 'John Evans'
6
6
  Author-email: "John Evans" <jevans667cc@proton.me>
7
7
  License: 'MIT'
8
8
  Classifier: Programming Language :: Python
9
- Classifier: Programming Language :: Python :: 3.10
10
9
  Classifier: Programming Language :: Python :: 3.11
11
10
  Classifier: Programming Language :: Python :: 3.12
12
11
  Classifier: Programming Language :: Python :: 3.13
@@ -15,16 +14,18 @@ Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Intended Audience :: Science/Research
16
15
  Classifier: Operating System :: OS Independent
17
16
  Classifier: Topic :: Scientific/Engineering
18
- Requires-Python: >=3.9
17
+ Requires-Python: >=3.11
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE.txt
21
20
  Requires-Dist: numpy
22
21
  Requires-Dist: lxml
22
+ Requires-Dist: imageio
23
23
  Requires-Dist: packaging
24
24
  Provides-Extra: test
25
25
  Requires-Dist: pytest; extra == "test"
26
26
  Requires-Dist: pillow; extra == "test"
27
27
  Requires-Dist: scikit-image; extra == "test"
28
+ Dynamic: license-file
28
29
 
29
30
 
30
31
  **glymur** contains a Python interface to the OpenJPEG library which
@@ -3,6 +3,6 @@ glymur: a Python interface for JPEG 2000
3
3
 
4
4
  **glymur** contains a Python interface to the OpenJPEG library which
5
5
  allows one to read and write JPEG 2000 files. **glymur** works on
6
- Python 3.9, 3.10, 3.11, and 3.12.
6
+ Python 3.11, 3.12, and 3.13.
7
7
 
8
8
  Please read the docs, https://glymur.readthedocs.org/en/latest/
@@ -0,0 +1,91 @@
1
+ UUID Box (uuid) @ (149, 523)
2
+ UUID: b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03 (GeoTIFF)
3
+ UUID Data:
4
+
5
+ Geo Metadata:
6
+ Driver: JP2OpenJPEG/JPEG-2000 driver based on OpenJPEG library
7
+ Size is 2592, 1456
8
+ Coordinate System is:
9
+ PROJCRS["Equirectangular MARS",
10
+ BASEGEOGCRS["GCS_MARS",
11
+ DATUM["unnamed",
12
+ ELLIPSOID["unnamed",3396190,0,
13
+ LENGTHUNIT["metre",1,
14
+ ID["EPSG",9001]]]],
15
+ PRIMEM["Reference meridian",0,
16
+ ANGLEUNIT["degree",0.0174532925199433,
17
+ ID["EPSG",9122]]]],
18
+ CONVERSION["Equidistant Cylindrical",
19
+ METHOD["Equidistant Cylindrical",
20
+ ID["EPSG",1028]],
21
+ PARAMETER["Latitude of 1st standard parallel",0,
22
+ ANGLEUNIT["degree",0.0174532925199433],
23
+ ID["EPSG",8823]],
24
+ PARAMETER["Longitude of natural origin",180,
25
+ ANGLEUNIT["degree",0.0174532925199433],
26
+ ID["EPSG",8802]],
27
+ PARAMETER["False easting",0,
28
+ LENGTHUNIT["metre",1],
29
+ ID["EPSG",8806]],
30
+ PARAMETER["False northing",0,
31
+ LENGTHUNIT["metre",1],
32
+ ID["EPSG",8807]]],
33
+ CS[Cartesian,2],
34
+ AXIS["easting",east,
35
+ ORDER[1],
36
+ LENGTHUNIT["metre",1,
37
+ ID["EPSG",9001]]],
38
+ AXIS["northing",north,
39
+ ORDER[2],
40
+ LENGTHUNIT["metre",1,
41
+ ID["EPSG",9001]]]]
42
+ Data axis to CRS axis mapping: 1,2
43
+ Origin = (-2523306.125000000000000,-268608.875000000000000)
44
+ Pixel Size = (0.250000000000000,-0.250000000000000)
45
+ Image Structure Metadata:
46
+ INTERLEAVE=PIXEL
47
+ COMPRESSION_REVERSIBILITY=LOSSLESS (possibly)
48
+ Corner Coordinates:
49
+ Upper Left (-2523306.125, -268608.875) (137d25'49.08"E, 4d31'53.74"S)
50
+ Lower Left (-2523306.125, -268972.875) (137d25'49.08"E, 4d32'15.85"S)
51
+ Upper Right (-2522658.125, -268608.875) (137d26'28.43"E, 4d31'53.74"S)
52
+ Lower Right (-2522658.125, -268972.875) (137d26'28.43"E, 4d32'15.85"S)
53
+ Center (-2522982.125, -268790.875) (137d26' 8.76"E, 4d32' 4.79"S)
54
+ Band 1 Block=1024x1024 Type=Byte, ColorInterp=Red
55
+ Overviews: 1296x728
56
+ Overviews: arbitrary
57
+ Image Structure Metadata:
58
+ COMPRESSION=JPEG2000
59
+ Band 2 Block=1024x1024 Type=Byte, ColorInterp=Green
60
+ Overviews: 1296x728
61
+ Overviews: arbitrary
62
+ Image Structure Metadata:
63
+ COMPRESSION=JPEG2000
64
+ Band 3 Block=1024x1024 Type=Byte, ColorInterp=Blue
65
+ Overviews: 1296x728
66
+ Overviews: arbitrary
67
+ Image Structure Metadata:
68
+ COMPRESSION=JPEG2000
69
+
70
+ Raw IFD Metadata:
71
+ OrderedDict([ ('ImageWidth', 1),
72
+ ('ImageLength', 1),
73
+ ('BitsPerSample', 8),
74
+ ('Compression', 1),
75
+ ('Photometric', 1),
76
+ ('StripOffsets', 8),
77
+ ('SamplesPerPixel', 1),
78
+ ('RowsPerStrip', 1),
79
+ ('StripByteCounts', 1),
80
+ ('PlanarConfig', 1),
81
+ ('ModelPixelScale', array([0.25, 0.25, 0. ])),
82
+ ( 'ModelTiePoint',
83
+ array([ 0. , 0. , 0. , -2523306.125,
84
+ -268608.875, 0. ], shape=(6,))),
85
+ ( 'GeoKeyDirectory',
86
+ array([ 1, 1, 0, ..., 34736, 1, 0],
87
+ shape=(76,), dtype=uint16)),
88
+ ( 'GeoDoubleParams',
89
+ array([0.00000e+00, 1.80000e+02, 0.00000e+00, 0.00000e+00, 3.39619e+06,
90
+ 3.39619e+06], shape=(6,))),
91
+ ('GeoAsciiParams', 'Equirectangular MARS|GCS_MARS|')])
@@ -5,7 +5,7 @@ __all__ = [
5
5
  'get_option', 'set_option', 'reset_option',
6
6
  'get_printoptions', 'set_printoptions',
7
7
  'get_parseoptions', 'set_parseoptions',
8
- 'Jp2k', 'Jp2kr', 'Tiff2Jp2k',
8
+ 'Jp2k', 'Jp2kr', 'JPEG2JP2', 'Tiff2Jp2k',
9
9
  ]
10
10
 
11
11
  # Local imports
@@ -13,6 +13,7 @@ from glymur import version
13
13
  from .options import (get_option, set_option, reset_option,
14
14
  get_printoptions, set_printoptions,
15
15
  get_parseoptions, set_parseoptions)
16
+ from .jpeg import JPEG2JP2
16
17
  from .jp2k import Jp2k, Jp2kr
17
18
  from .tiff import Tiff2Jp2k
18
19
  from . import data
@@ -0,0 +1,390 @@
1
+ """Core definitions to be shared amongst the modules."""
2
+ # standard library imports
3
+ import io
4
+ import logging
5
+ import shutil
6
+ import struct
7
+ from typing import Tuple
8
+ from uuid import UUID
9
+
10
+ # local imports
11
+ from . import jp2box
12
+ from .lib.tiff import DATATYPE2FMT
13
+ from .jp2k import Jp2k
14
+ from glymur.core import RESTRICTED_ICC_PROFILE
15
+
16
+ # Mnemonics for the two TIFF format version numbers.
17
+ TIFF = 42
18
+ BIGTIFF = 43
19
+
20
+
21
+ class _2JP2Converter(object):
22
+ """
23
+ This private class is used by both the TIFF2JP2 and the JPEG2JP2
24
+ converters.
25
+
26
+ Attributes
27
+ ----------
28
+ create_exif_uuid : bool
29
+ Create a UUIDBox for the TIFF IFD metadata.
30
+ tilesize : tuple
31
+ The dimensions of a tile in the JP2K file.
32
+ verbosity : int
33
+ Set the level of logging, i.e. WARNING, INFO, etc.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ create_exif_uuid: bool,
39
+ create_xmp_uuid: bool,
40
+ include_icc_profile: bool,
41
+ tilesize: Tuple[int, int] | None,
42
+ verbosity: int
43
+ ):
44
+
45
+ self.create_exif_uuid = create_exif_uuid
46
+ self.create_xmp_uuid = create_xmp_uuid
47
+ self.include_icc_profile = include_icc_profile
48
+ self.tilesize = tilesize
49
+
50
+ # Assume that there is no ICC profile tag until we know otherwise.
51
+ self.icc_profile = None
52
+
53
+ self.setup_logging(verbosity)
54
+
55
+ def read_ifd(self, tfp):
56
+ """Process either the main IFD or an Exif IFD
57
+
58
+ Parameters
59
+ ----------
60
+ tfp : file-like
61
+ FILE pointer for TIFF
62
+
63
+ Returns
64
+ -------
65
+ dictionary of the TIFF IFD
66
+ """
67
+
68
+ self.found_geotiff_tags = False
69
+
70
+ tag_length = 20 if self.version == BIGTIFF else 12
71
+
72
+ # how many tags?
73
+ if self.version == BIGTIFF:
74
+ buffer = tfp.read(8)
75
+ (num_tags,) = struct.unpack(self.endian + "Q", buffer)
76
+ else:
77
+ buffer = tfp.read(2)
78
+ (num_tags,) = struct.unpack(self.endian + "H", buffer)
79
+
80
+ # Ok, so now we have the IFD main body, but following that we have
81
+ # the tag payloads that cannot fit into 4 bytes.
82
+
83
+ # the IFD main body in the TIFF. As it might be big endian, we
84
+ # cannot just process it as one big chunk.
85
+ buffer = tfp.read(num_tags * tag_length)
86
+
87
+ if self.version == BIGTIFF:
88
+ tag_format_str = self.endian + "HHQQ"
89
+ tag_payload_offset = 12
90
+ max_tag_payload_length = 8
91
+ else:
92
+ tag_format_str = self.endian + "HHII"
93
+ tag_payload_offset = 8
94
+ max_tag_payload_length = 4
95
+
96
+ tags = {}
97
+
98
+ for idx in range(num_tags):
99
+
100
+ self.logger.debug(f"tag #: {idx}")
101
+
102
+ tag_data = buffer[idx * tag_length:(idx + 1) * tag_length]
103
+
104
+ tag, dtype, nvalues, offset = struct.unpack(
105
+ tag_format_str, tag_data
106
+ ) # noqa : E501
107
+
108
+ if tag == 34735:
109
+ self.found_geotiff_tags = True
110
+
111
+ payload_length = DATATYPE2FMT[dtype]["nbytes"] * nvalues
112
+
113
+ if tag in (34665, 34853):
114
+
115
+ # found exif or gps ifd
116
+ # save our location, go get that IFD, and come on back
117
+ orig_pos = tfp.tell()
118
+ tfp.seek(offset)
119
+ payload = self.read_ifd(tfp)
120
+ tfp.seek(orig_pos)
121
+
122
+ elif payload_length > max_tag_payload_length:
123
+ # the payload does not fit into the tag entry, so use the
124
+ # offset to seek to that position
125
+ current_position = tfp.tell()
126
+ tfp.seek(offset)
127
+ payload_buffer = tfp.read(payload_length)
128
+ tfp.seek(current_position)
129
+
130
+ # read the payload from the TIFF
131
+ payload_format = DATATYPE2FMT[dtype]["format"] * nvalues
132
+ payload = struct.unpack(
133
+ self.endian + payload_format,
134
+ payload_buffer
135
+ )
136
+
137
+ else:
138
+ # the payload DOES fit into the TIFF tag entry
139
+ payload_buffer = tag_data[tag_payload_offset:]
140
+
141
+ # read ALL of the payload buffer
142
+ fmt = DATATYPE2FMT[dtype]["format"]
143
+ nelts = max_tag_payload_length / DATATYPE2FMT[dtype]["nbytes"]
144
+ num_items = int(nelts)
145
+ payload_format = self.endian + fmt * num_items
146
+ payload = struct.unpack(payload_format, payload_buffer)
147
+
148
+ # Extract the actual payload. Two things going
149
+ # on here. First of all, not all of the items may
150
+ # be used. For example, if the payload length is
151
+ # 4 bytes but the format string was HHH, the that
152
+ # last 16 bit value is not wanted, so we should
153
+ # discard it. Second thing is that the signed and
154
+ # unsigned rational datatypes effectively have twice
155
+ # the number of values so we need to account for that.
156
+ if dtype in [5, 10]:
157
+ payload = payload[: 2 * nvalues]
158
+ else:
159
+ payload = payload[:nvalues]
160
+
161
+ tags[tag] = {"dtype": dtype, "nvalues": nvalues, "payload": payload}
162
+
163
+ return tags
164
+
165
+ def setup_logging(self, verbosity):
166
+
167
+ self.logger = logging.getLogger("tiff2jp2")
168
+ self.logger.setLevel(verbosity)
169
+ ch = logging.StreamHandler()
170
+ ch.setLevel(verbosity)
171
+ self.logger.addHandler(ch)
172
+
173
+ def read_tiff_header(self, tfp):
174
+ """Get the endian-ness of the TIFF, seek to the main IFD"""
175
+
176
+ buffer = tfp.read(4)
177
+ data = struct.unpack("BB", buffer[:2])
178
+
179
+ # big endian or little endian?
180
+ if data[0] == 73 and data[1] == 73:
181
+ # little endian
182
+ self.endian = "<"
183
+ elif data[0] == 77 and data[1] == 77:
184
+ # big endian
185
+ self.endian = ">"
186
+ # no other option is possible, libtiff.open would have errored out
187
+ # else:
188
+ # msg = (
189
+ # f"The byte order indication in the TIFF header "
190
+ # f"({data}) is invalid. It should be either "
191
+ # f"{bytes([73, 73])} or {bytes([77, 77])}."
192
+ # )
193
+ # raise RuntimeError(msg)
194
+
195
+ # version number and offset to the first IFD
196
+ (version,) = struct.unpack(self.endian + "H", buffer[2:4])
197
+ self.version = TIFF if version == 42 else BIGTIFF
198
+
199
+ if self.version == BIGTIFF:
200
+ buffer = tfp.read(12)
201
+ _, _, offset = struct.unpack(self.endian + "HHQ", buffer)
202
+ else:
203
+ buffer = tfp.read(4)
204
+ (offset,) = struct.unpack(self.endian + "I", buffer)
205
+ tfp.seek(offset)
206
+
207
+ def append_exif_uuid_box(self):
208
+ """Append an EXIF UUID box onto the end of the JPEG 2000 file. It will
209
+ contain metadata from the TIFF IFD.
210
+ """
211
+ if not self.create_exif_uuid:
212
+ return
213
+
214
+ # create a bytesio object for the IFD
215
+ b = io.BytesIO()
216
+
217
+ # write this 32-bit header into the UUID, no matter if we had bigtiff
218
+ # or regular tiff or big endian
219
+ data = struct.pack("<BBHI", 73, 73, 42, 8)
220
+ b.write(data)
221
+
222
+ self.write_ifd(b, self.tags)
223
+
224
+ # create the Exif UUID
225
+ if self.found_geotiff_tags:
226
+ # geotiff UUID
227
+ the_uuid = UUID("b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03")
228
+ payload = b.getvalue()
229
+ else:
230
+ # Make it an exif UUID.
231
+ the_uuid = UUID(bytes=b"JpgTiffExif->JP2")
232
+ payload = b"EXIF\0\0" + b.getvalue()
233
+
234
+ # the length of the box is the length of the payload plus 8 bytes
235
+ # to store the length of the box and the box ID
236
+ box_length = len(payload) + 8
237
+
238
+ uuid_box = jp2box.UUIDBox(the_uuid, payload, box_length)
239
+ with self.jp2_path.open(mode="ab") as f:
240
+ uuid_box.write(f)
241
+
242
+ self.jp2.finalize(force_parse=True)
243
+
244
+ def write_ifd(self, b, tags):
245
+ """Write the IFD out to the UUIDBox. We will always write IFDs
246
+ for 32-bit TIFFs, i.e. 12 byte tags, meaning just 4 bytes within
247
+ the tag for the tag data
248
+ """
249
+
250
+ little_tiff_tag_length = 12
251
+ max_tag_payload_length = 4
252
+
253
+ # exclude any unwanted tags
254
+ if self.exclude_tags is not None:
255
+ for tag in self.exclude_tags:
256
+ if tag in tags:
257
+ tags.pop(tag)
258
+
259
+ num_tags = len(tags)
260
+ write_buffer = struct.pack("<H", num_tags)
261
+ b.write(write_buffer)
262
+
263
+ # Ok, so now we have the IFD main body, but following that we have
264
+ # the tag payloads that cannot fit into 4 bytes.
265
+
266
+ ifd_start_loc = b.tell()
267
+ after_ifd_position = ifd_start_loc + num_tags * little_tiff_tag_length
268
+
269
+ for idx, tag in enumerate(tags):
270
+
271
+ tag_offset = ifd_start_loc + idx * little_tiff_tag_length
272
+ self.logger.debug(f"tag #: {tag}, writing to {tag_offset}")
273
+ self.logger.debug(f"tag #: {tag}, after IFD {after_ifd_position}")
274
+
275
+ b.seek(tag_offset)
276
+
277
+ try:
278
+ dtype = tags[tag]["dtype"]
279
+ except IndexError:
280
+ breakpoint()
281
+ pass
282
+
283
+ nvalues = tags[tag]["nvalues"]
284
+ payload = tags[tag]["payload"]
285
+
286
+ payload_length = DATATYPE2FMT[dtype]["nbytes"] * nvalues
287
+
288
+ if payload_length > max_tag_payload_length:
289
+
290
+ # the payload does not fit into the tag entry
291
+
292
+ # read the payload from the TIFF
293
+ payload_format = DATATYPE2FMT[dtype]["format"] * nvalues
294
+
295
+ # write the tag entry to the UUID
296
+ new_offset = after_ifd_position
297
+ buffer = struct.pack("<HHII", tag, dtype, nvalues, new_offset)
298
+ b.write(buffer)
299
+
300
+ # now write the payload at the outlying position and then come
301
+ # back to the same position in the file stream
302
+ cpos = b.tell()
303
+ b.seek(new_offset)
304
+
305
+ format = "<" + DATATYPE2FMT[dtype]["format"] * nvalues
306
+ buffer = struct.pack(format, *payload)
307
+ b.write(buffer)
308
+
309
+ # keep track of the next position to write out-of-IFD data
310
+ after_ifd_position = b.tell()
311
+ b.seek(cpos)
312
+
313
+ else:
314
+
315
+ # the payload DOES fit into the TIFF tag entry
316
+ # write the tag metadata
317
+ buffer = struct.pack("<HHI", tag, dtype, nvalues)
318
+ b.write(buffer)
319
+
320
+ payload_format = DATATYPE2FMT[dtype]["format"] * nvalues
321
+
322
+ # we may need to alter the output format
323
+ if payload_format in ["H", "B", "I"]:
324
+ # just write it as an integer
325
+ payload_format = "I"
326
+
327
+ if tag in (34665, 34853):
328
+
329
+ # special case for an EXIF or GPS IFD
330
+ buffer = struct.pack("<I", after_ifd_position)
331
+ b.write(buffer)
332
+ b.seek(after_ifd_position)
333
+ after_ifd_position = self.write_ifd(b, payload)
334
+
335
+ else:
336
+
337
+ buffer = struct.pack("<" + payload_format, *payload)
338
+ b.write(buffer)
339
+
340
+ return after_ifd_position
341
+
342
+ def append_xmp_uuid_box(self):
343
+ """Append an XMP UUID box onto the end of the JPEG 2000 file if there
344
+ was an XMP tag in the TIFF IFD.
345
+ """
346
+
347
+ if self.xmp_data is None:
348
+ return
349
+
350
+ if not self.create_xmp_uuid:
351
+ return
352
+
353
+ # create the XMP UUID
354
+ the_uuid = jp2box.UUID("be7acfcb-97a9-42e8-9c71-999491e3afac")
355
+ box_length = len(self.xmp_data) + 8
356
+ uuid_box = jp2box.UUIDBox(the_uuid, self.xmp_data, box_length)
357
+ with self.jp2_path.open(mode="ab") as f:
358
+ uuid_box.write(f)
359
+
360
+ def rewrap_for_icc_profile(self):
361
+ """Consume an ICC profile, if one is there."""
362
+ if self.icc_profile is None and self.include_icc_profile:
363
+ self.logger.warning("No ICC profile was found.")
364
+
365
+ if self.icc_profile is None or not self.include_icc_profile:
366
+ return
367
+
368
+ self.logger.info(
369
+ "Consuming an ICC profile into JP2 color specification box."
370
+ )
371
+
372
+ colr = jp2box.ColourSpecificationBox(
373
+ method=RESTRICTED_ICC_PROFILE,
374
+ precedence=0,
375
+ icc_profile=self.icc_profile
376
+ )
377
+
378
+ # construct the new set of JP2 boxes, insert the color specification
379
+ # box with the ICC profile
380
+ jp2 = Jp2k(self.jp2_path)
381
+ boxes = jp2.box
382
+ boxes[2].box = [boxes[2].box[0], colr]
383
+
384
+ # re-wrap the codestream, involves a file copy
385
+ tmp_filename = str(self.jp2_path) + ".tmp"
386
+
387
+ with open(tmp_filename, mode="wb") as tfile:
388
+ jp2.wrap(tfile.name, boxes=boxes)
389
+
390
+ shutil.move(tmp_filename, self.jp2_path)