Glymur 0.13.7__tar.gz → 0.14.0.post1__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.
- {glymur-0.13.7 → glymur-0.14.0.post1}/CHANGES.txt +8 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/Glymur.egg-info/PKG-INFO +5 -3
- {glymur-0.13.7 → glymur-0.14.0.post1}/Glymur.egg-info/SOURCES.txt +6 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/Glymur.egg-info/entry_points.txt +1 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/Glymur.egg-info/requires.txt +2 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/PKG-INFO +5 -3
- {glymur-0.13.7 → glymur-0.14.0.post1}/README.md +1 -1
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/__init__.py +2 -1
- glymur-0.14.0.post1/glymur/_core_converter.py +390 -0
- glymur-0.14.0.post1/glymur/_iccprofile.py +127 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/codestream.py +385 -308
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/command_line.py +129 -1
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/config.py +15 -14
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/core.py +18 -22
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/jp2box.py +758 -576
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/jp2k.py +185 -149
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/jp2kr.py +59 -45
- glymur-0.14.0.post1/glymur/jpeg.py +196 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/lib/openjp2.py +198 -285
- glymur-0.14.0.post1/glymur/lib/tiff.py +2176 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/options.py +33 -28
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/tiff.py +77 -406
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/version.py +1 -1
- {glymur-0.13.7 → glymur-0.14.0.post1}/setup.cfg +7 -2
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_callbacks.py +17 -16
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_cinema.py +31 -21
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_codestream.py +45 -45
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_colour_specification_box.py +46 -40
- glymur-0.14.0.post1/tests/test_commandline_jp2dump.py +199 -0
- glymur-0.14.0.post1/tests/test_commandline_jpeg2jp2.py +253 -0
- glymur-0.14.0.post1/tests/test_commandline_tiff2jp2.py +498 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_config.py +83 -85
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_geo.py +62 -58
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_jp2box.py +194 -237
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_jp2box_jpx.py +131 -128
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_jp2box_uuid.py +82 -68
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_jp2box_xml.py +55 -53
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_jp2k.py +157 -157
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_jp2k_writes.py +22 -47
- glymur-0.14.0.post1/tests/test_jpeg2jp2.py +242 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_libtiff.py +5 -9
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_openjp2.py +3 -8
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_printing.py +1 -168
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_threading.py +2 -1
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_tiff2jp2.py +37 -307
- glymur-0.13.7/glymur/_iccprofile.py +0 -126
- glymur-0.13.7/glymur/lib/tiff.py +0 -2052
- {glymur-0.13.7 → glymur-0.14.0.post1}/Glymur.egg-info/dependency_links.txt +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/Glymur.egg-info/not-zip-safe +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/Glymur.egg-info/top_level.txt +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/LICENSE.txt +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/MANIFEST.in +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/data/__init__.py +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/data/goodstuff.j2k +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/data/heliov.jpx +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/data/nemo.jp2 +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/glymur/lib/__init__.py +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/pyproject.toml +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_jp2kr.py +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_set_decoded_components.py +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_slicing.py +0 -0
- {glymur-0.13.7 → glymur-0.14.0.post1}/tests/test_warnings.py +0 -0
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
Mar 19, 2025 - v0.14.0
|
|
2
|
+
Add description of raw IFD for geojp2 UUIDs
|
|
3
|
+
Add feature to convert JPEGs to JP2
|
|
4
|
+
Remove support for python 3.10
|
|
5
|
+
|
|
6
|
+
Jan 14, 2025 - v0.13.8
|
|
7
|
+
Fix tiff2jp2 bug when stripped TIFF has no RowsPerStrip tag
|
|
8
|
+
|
|
1
9
|
Jan 15, 2025 - v0.13.7
|
|
2
10
|
Qualify on python 3.13
|
|
3
11
|
Fix test warnings due to scikit-image deprecation.
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: Glymur
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0.post1
|
|
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
|
|
@@ -20,10 +19,13 @@ 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
|
+
Requires-Dist: scikit-image; extra == "test"
|
|
28
|
+
Dynamic: license-file
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
**glymur** contains a Python interface to the OpenJPEG library which
|
|
@@ -12,6 +12,7 @@ Glymur.egg-info/not-zip-safe
|
|
|
12
12
|
Glymur.egg-info/requires.txt
|
|
13
13
|
Glymur.egg-info/top_level.txt
|
|
14
14
|
glymur/__init__.py
|
|
15
|
+
glymur/_core_converter.py
|
|
15
16
|
glymur/_iccprofile.py
|
|
16
17
|
glymur/codestream.py
|
|
17
18
|
glymur/command_line.py
|
|
@@ -20,6 +21,7 @@ glymur/core.py
|
|
|
20
21
|
glymur/jp2box.py
|
|
21
22
|
glymur/jp2k.py
|
|
22
23
|
glymur/jp2kr.py
|
|
24
|
+
glymur/jpeg.py
|
|
23
25
|
glymur/options.py
|
|
24
26
|
glymur/tiff.py
|
|
25
27
|
glymur/version.py
|
|
@@ -34,6 +36,9 @@ tests/test_callbacks.py
|
|
|
34
36
|
tests/test_cinema.py
|
|
35
37
|
tests/test_codestream.py
|
|
36
38
|
tests/test_colour_specification_box.py
|
|
39
|
+
tests/test_commandline_jp2dump.py
|
|
40
|
+
tests/test_commandline_jpeg2jp2.py
|
|
41
|
+
tests/test_commandline_tiff2jp2.py
|
|
37
42
|
tests/test_config.py
|
|
38
43
|
tests/test_geo.py
|
|
39
44
|
tests/test_jp2box.py
|
|
@@ -43,6 +48,7 @@ tests/test_jp2box_xml.py
|
|
|
43
48
|
tests/test_jp2k.py
|
|
44
49
|
tests/test_jp2k_writes.py
|
|
45
50
|
tests/test_jp2kr.py
|
|
51
|
+
tests/test_jpeg2jp2.py
|
|
46
52
|
tests/test_libtiff.py
|
|
47
53
|
tests/test_openjp2.py
|
|
48
54
|
tests/test_printing.py
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: Glymur
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0.post1
|
|
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
|
|
@@ -20,10 +19,13 @@ 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
|
+
Requires-Dist: scikit-image; extra == "test"
|
|
28
|
+
Dynamic: license-file
|
|
27
29
|
|
|
28
30
|
|
|
29
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.
|
|
6
|
+
Python 3.11, 3.12, and 3.13.
|
|
7
7
|
|
|
8
8
|
Please read the docs, https://glymur.readthedocs.org/en/latest/
|
|
@@ -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)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Standard library imports ...
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
import datetime
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
# Third party library imports ...
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _ICCProfile(object):
|
|
11
|
+
"""Container for ICC profile information."""
|
|
12
|
+
|
|
13
|
+
profile_class = {
|
|
14
|
+
b"scnr": "input device profile",
|
|
15
|
+
b"mntr": "display device profile",
|
|
16
|
+
b"prtr": "output device profile",
|
|
17
|
+
b"link": "devicelink profile",
|
|
18
|
+
b"spac": "colorspace conversion profile",
|
|
19
|
+
b"abst": "abstract profile",
|
|
20
|
+
b"nmcl": "name colour profile",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
colour_space_dict = {
|
|
24
|
+
b"XYZ ": "XYZ",
|
|
25
|
+
b"Lab ": "Lab",
|
|
26
|
+
b"Luv ": "Luv",
|
|
27
|
+
b"YCbr": "YCbCr",
|
|
28
|
+
b"Yxy ": "Yxy",
|
|
29
|
+
b"RGB ": "RGB",
|
|
30
|
+
b"GRAY": "gray",
|
|
31
|
+
b"HSV ": "hsv",
|
|
32
|
+
b"HLS ": "hls",
|
|
33
|
+
b"CMYK": "CMYK",
|
|
34
|
+
b"CMY ": "cmy",
|
|
35
|
+
b"2CLR": "2colour",
|
|
36
|
+
b"3CLR": "3colour",
|
|
37
|
+
b"4CLR": "4colour",
|
|
38
|
+
b"5CLR": "5colour",
|
|
39
|
+
b"6CLR": "6colour",
|
|
40
|
+
b"7CLR": "7colour",
|
|
41
|
+
b"8CLR": "8colour",
|
|
42
|
+
b"9CLR": "9colour",
|
|
43
|
+
b"ACLR": "10colour",
|
|
44
|
+
b"BCLR": "11colour",
|
|
45
|
+
b"CCLR": "12colour",
|
|
46
|
+
b"DCLR": "13colour",
|
|
47
|
+
b"ECLR": "14colour",
|
|
48
|
+
b"FCLR": "15colour",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
rendering_intent_dict = {
|
|
52
|
+
0: "perceptual",
|
|
53
|
+
1: "media-relative colorimetric",
|
|
54
|
+
2: "saturation",
|
|
55
|
+
3: "ICC-absolute colorimetric",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def __init__(self, read_buffer):
|
|
59
|
+
self._raw_buffer = read_buffer
|
|
60
|
+
header = OrderedDict()
|
|
61
|
+
|
|
62
|
+
data = struct.unpack(">IIBB", self._raw_buffer[0:10])
|
|
63
|
+
header["Size"] = data[0]
|
|
64
|
+
header["Preferred CMM Type"] = data[1]
|
|
65
|
+
major = data[2]
|
|
66
|
+
minor = (data[3] & 0xF0) >> 4
|
|
67
|
+
bugfix = data[3] & 0x0F
|
|
68
|
+
header["Version"] = f"{major}.{minor}.{bugfix}"
|
|
69
|
+
|
|
70
|
+
header["Device Class"] = self.profile_class[self._raw_buffer[12:16]]
|
|
71
|
+
header["Color Space"] = self.colour_space_dict[self._raw_buffer[16:20]]
|
|
72
|
+
data = self.colour_space_dict[self._raw_buffer[20:24]]
|
|
73
|
+
header["Connection Space"] = data
|
|
74
|
+
|
|
75
|
+
data = struct.unpack(">HHHHHH", self._raw_buffer[24:36])
|
|
76
|
+
try:
|
|
77
|
+
header["Datetime"] = datetime.datetime(*data)
|
|
78
|
+
except ValueError:
|
|
79
|
+
header["Datetime"] = None
|
|
80
|
+
header["File Signature"] = read_buffer[36:40].decode("utf-8")
|
|
81
|
+
if read_buffer[40:44] == b"\x00\x00\x00\x00":
|
|
82
|
+
header["Platform"] = "unrecognized"
|
|
83
|
+
else:
|
|
84
|
+
header["Platform"] = read_buffer[40:44].decode("utf-8")
|
|
85
|
+
|
|
86
|
+
(fval,) = struct.unpack(">I", read_buffer[44:48])
|
|
87
|
+
header["Flags"] = (
|
|
88
|
+
f"{'' if fval & 0x01 else 'not '}embedded, "
|
|
89
|
+
f"{'cannot' if fval & 0x02 else 'can'} be used independently"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
header["Device Manufacturer"] = read_buffer[48:52].decode("utf-8")
|
|
93
|
+
if read_buffer[52:56] == b"\x00\x00\x00\x00":
|
|
94
|
+
device_model = ""
|
|
95
|
+
else:
|
|
96
|
+
device_model = read_buffer[52:56].decode("utf-8")
|
|
97
|
+
header["Device Model"] = device_model
|
|
98
|
+
|
|
99
|
+
(val,) = struct.unpack(">Q", read_buffer[56:64])
|
|
100
|
+
attr = (
|
|
101
|
+
f"{'transparency' if val & 0x01 else 'reflective'}, "
|
|
102
|
+
f"{'matte' if val & 0x02 else 'glossy'}, "
|
|
103
|
+
f"{'negative' if val & 0x04 else 'positive'} media polarity, "
|
|
104
|
+
f"{'black and white' if val & 0x08 else 'color'} media"
|
|
105
|
+
)
|
|
106
|
+
header["Device Attributes"] = attr
|
|
107
|
+
|
|
108
|
+
(rval,) = struct.unpack(">I", read_buffer[64:68])
|
|
109
|
+
try:
|
|
110
|
+
header["Rendering Intent"] = self.rendering_intent_dict[rval]
|
|
111
|
+
except KeyError:
|
|
112
|
+
header["Rendering Intent"] = "unknown"
|
|
113
|
+
|
|
114
|
+
data = struct.unpack(">iii", read_buffer[68:80])
|
|
115
|
+
header["Illuminant"] = np.array(data, dtype=np.float64) / 65536
|
|
116
|
+
|
|
117
|
+
if read_buffer[80:84] == b"\x00\x00\x00\x00":
|
|
118
|
+
creator = "unrecognized"
|
|
119
|
+
else:
|
|
120
|
+
creator = read_buffer[80:84].decode("utf-8")
|
|
121
|
+
header["Creator"] = creator
|
|
122
|
+
|
|
123
|
+
if header["Version"][0] == "4":
|
|
124
|
+
header["Profile Id"] = read_buffer[84:100]
|
|
125
|
+
|
|
126
|
+
# Final 27 bytes are reserved.
|
|
127
|
+
self.header = header
|