Glymur 0.13.8__py3-none-any.whl → 0.14.0.post1__py3-none-any.whl
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/__init__.py +2 -1
- glymur/_core_converter.py +390 -0
- glymur/command_line.py +129 -1
- glymur/jp2box.py +31 -8
- glymur/jpeg.py +196 -0
- glymur/lib/tiff.py +132 -4
- glymur/tiff.py +28 -359
- glymur/version.py +1 -1
- {Glymur-0.13.8.dist-info → glymur-0.14.0.post1.dist-info}/METADATA +4 -3
- glymur-0.14.0.post1.dist-info/RECORD +27 -0
- {Glymur-0.13.8.dist-info → glymur-0.14.0.post1.dist-info}/WHEEL +1 -1
- {Glymur-0.13.8.dist-info → glymur-0.14.0.post1.dist-info}/entry_points.txt +1 -0
- Glymur-0.13.8.dist-info/RECORD +0 -25
- {Glymur-0.13.8.dist-info → glymur-0.14.0.post1.dist-info/licenses}/LICENSE.txt +0 -0
- {Glymur-0.13.8.dist-info → glymur-0.14.0.post1.dist-info}/top_level.txt +0 -0
glymur/jpeg.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# standard library imports
|
|
2
|
+
import io
|
|
3
|
+
import logging
|
|
4
|
+
import pathlib
|
|
5
|
+
import struct
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
# 3rd party library imports
|
|
9
|
+
import imageio.v3 as iio
|
|
10
|
+
|
|
11
|
+
# local imports
|
|
12
|
+
from .jp2k import Jp2k
|
|
13
|
+
from .options import set_option
|
|
14
|
+
from ._core_converter import _2JP2Converter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JPEG2JP2(_2JP2Converter):
|
|
18
|
+
"""
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
create_exif_uuid : bool
|
|
22
|
+
Create a UUIDBox for the Exif metadata. Always True for JPEG.
|
|
23
|
+
jp2_filename : path
|
|
24
|
+
Path to JPEG 2000 file to be written.
|
|
25
|
+
jpeg_filename : path
|
|
26
|
+
Path to JPEG file.
|
|
27
|
+
tilesize : tuple
|
|
28
|
+
The dimensions of a tile in the JP2K file.
|
|
29
|
+
verbosity : int
|
|
30
|
+
Set the level of logging, i.e. WARNING, INFO, etc.
|
|
31
|
+
tags : dict
|
|
32
|
+
Tags retrieved from APP1 segment, if any.
|
|
33
|
+
"""
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
jpeg: pathlib.Path | str,
|
|
37
|
+
jp2: pathlib.Path | str,
|
|
38
|
+
include_icc_profile: bool = False,
|
|
39
|
+
num_threads: int = 1,
|
|
40
|
+
tilesize: Tuple[int, int] | None = None,
|
|
41
|
+
verbosity: int = logging.CRITICAL,
|
|
42
|
+
**kwargs
|
|
43
|
+
):
|
|
44
|
+
super().__init__(True, True, include_icc_profile, tilesize, verbosity)
|
|
45
|
+
|
|
46
|
+
self.jpeg_path = pathlib.Path(jpeg)
|
|
47
|
+
|
|
48
|
+
self.jp2_path = pathlib.Path(jp2)
|
|
49
|
+
if self.jp2_path.exists():
|
|
50
|
+
raise FileExistsError(f'{str(self.jp2_path)} already exists, please delete if you wish to overwrite.') # noqa : E501
|
|
51
|
+
|
|
52
|
+
self.jp2_kwargs = kwargs
|
|
53
|
+
|
|
54
|
+
self.tags = None
|
|
55
|
+
|
|
56
|
+
# This is never set for JPEG
|
|
57
|
+
self.exclude_tags = None
|
|
58
|
+
|
|
59
|
+
if num_threads > 1:
|
|
60
|
+
set_option("lib.num_threads", num_threads)
|
|
61
|
+
|
|
62
|
+
def __enter__(self):
|
|
63
|
+
"""The JPEG2JP2 object must be used with a context manager."""
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def run(self):
|
|
70
|
+
|
|
71
|
+
self.copy_image()
|
|
72
|
+
self.copy_metadata()
|
|
73
|
+
|
|
74
|
+
def copy_metadata(self):
|
|
75
|
+
"""Transfer any EXIF or XMP metadata from the APPx segments."""
|
|
76
|
+
|
|
77
|
+
with self.jpeg_path.open(mode='rb') as f:
|
|
78
|
+
|
|
79
|
+
eof = False
|
|
80
|
+
while not eof:
|
|
81
|
+
|
|
82
|
+
marker = f.read(2)
|
|
83
|
+
|
|
84
|
+
match marker:
|
|
85
|
+
|
|
86
|
+
case b'\xff\xd8':
|
|
87
|
+
# marker-only, SOI
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
case b'\xff\xe0' | b'\xff\xec' | b'\xff\xee':
|
|
91
|
+
self.process_appx_segment(marker, f)
|
|
92
|
+
|
|
93
|
+
case b'\xff\xe1':
|
|
94
|
+
# EXIF using APP1
|
|
95
|
+
self.process_app1_segment(f)
|
|
96
|
+
|
|
97
|
+
case b'\xff\xe2':
|
|
98
|
+
# ICC profile
|
|
99
|
+
self.process_app2_segment(f)
|
|
100
|
+
|
|
101
|
+
case _:
|
|
102
|
+
# We don't care about anything else. No need to scan
|
|
103
|
+
# the file any further, we're done.
|
|
104
|
+
eof = True
|
|
105
|
+
|
|
106
|
+
if self.include_icc_profile and self.icc_profile is not None:
|
|
107
|
+
self.rewrap_for_icc_profile()
|
|
108
|
+
|
|
109
|
+
def process_appx_segment(self, marker, f):
|
|
110
|
+
# APP0 (JFIF) is b'\xff\xe0'
|
|
111
|
+
# APP12 ducky(?) is b'\xff\xec'
|
|
112
|
+
# APP14 Adobe(?) is b'\xff\xee'
|
|
113
|
+
_, n = struct.unpack('BB', marker)
|
|
114
|
+
|
|
115
|
+
msg = f'Skipping APP{n - 224} segment...'
|
|
116
|
+
self.logger.info(msg)
|
|
117
|
+
|
|
118
|
+
data = f.read(2)
|
|
119
|
+
size, = struct.unpack('>H', data)
|
|
120
|
+
_ = f.read(size - 2)
|
|
121
|
+
|
|
122
|
+
def process_app1_segment(self, f):
|
|
123
|
+
"""
|
|
124
|
+
An APP1 segment can contain Exif or XMP data.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
data = f.read(2)
|
|
128
|
+
size, = struct.unpack('>H', data)
|
|
129
|
+
buffer = f.read(size - 2)
|
|
130
|
+
|
|
131
|
+
if buffer[:6] == b'Exif\x00\x00':
|
|
132
|
+
|
|
133
|
+
# ok it is Exif
|
|
134
|
+
|
|
135
|
+
buffer = buffer[6:]
|
|
136
|
+
|
|
137
|
+
bf = io.BytesIO(buffer)
|
|
138
|
+
|
|
139
|
+
self.read_tiff_header(bf)
|
|
140
|
+
self.tags = self.read_ifd(bf)
|
|
141
|
+
self.append_exif_uuid_box()
|
|
142
|
+
|
|
143
|
+
elif buffer[:28] == b'http://ns.adobe.com/xap/1.0/':
|
|
144
|
+
|
|
145
|
+
# XMP APP segment
|
|
146
|
+
self.xmp_data = buffer[29:]
|
|
147
|
+
self.append_xmp_uuid_box()
|
|
148
|
+
|
|
149
|
+
else:
|
|
150
|
+
|
|
151
|
+
offset = f.tell() - 2 - 2 - len(buffer)
|
|
152
|
+
msg = f'Unrecognized APP1 segment at offset {offset}'
|
|
153
|
+
self.logger.warning(msg)
|
|
154
|
+
|
|
155
|
+
def process_app2_segment(self, f):
|
|
156
|
+
"""
|
|
157
|
+
The APP2 segment(s) usually contains an ICC profile. It may be split
|
|
158
|
+
across more than one APP2 segment.
|
|
159
|
+
"""
|
|
160
|
+
data = f.read(2)
|
|
161
|
+
size, = struct.unpack('>H', data)
|
|
162
|
+
buffer = f.read(size - 2)
|
|
163
|
+
|
|
164
|
+
if buffer[:12] == b'ICC_PROFILE\x00':
|
|
165
|
+
|
|
166
|
+
count, nchunks = struct.unpack('BB', buffer[12:14])
|
|
167
|
+
|
|
168
|
+
if not self.include_icc_profile:
|
|
169
|
+
msg = (
|
|
170
|
+
f'ICC profile chunk {count} of {nchunks} detected '
|
|
171
|
+
'(skipped)'
|
|
172
|
+
)
|
|
173
|
+
self.logger.warning(msg)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
msg = f'Processing ICC profile chunk {count} of {nchunks}...'
|
|
177
|
+
self.logger.info(msg)
|
|
178
|
+
|
|
179
|
+
if count == 1:
|
|
180
|
+
self.icc_profile = b''
|
|
181
|
+
|
|
182
|
+
# accumulate the ICC profile stored in this chunk. it's likely
|
|
183
|
+
# that this is all that there is, though.
|
|
184
|
+
self.icc_profile += bytes(buffer[14:])
|
|
185
|
+
|
|
186
|
+
def copy_image(self):
|
|
187
|
+
"""Transfer the image data from the JPEG to the JP2 file."""
|
|
188
|
+
image = iio.imread(self.jpeg_path)
|
|
189
|
+
|
|
190
|
+
self.jp2 = Jp2k(
|
|
191
|
+
self.jp2_path,
|
|
192
|
+
tilesize=self.tilesize,
|
|
193
|
+
**self.jp2_kwargs
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self.jp2[:] = image
|
glymur/lib/tiff.py
CHANGED
|
@@ -670,10 +670,134 @@ def check_error(status):
|
|
|
670
670
|
|
|
671
671
|
|
|
672
672
|
TAGS = {
|
|
673
|
-
"
|
|
673
|
+
"GPSVersionID": {
|
|
674
|
+
"number": 0,
|
|
675
|
+
"type": ctypes.c_char_p,
|
|
676
|
+
},
|
|
677
|
+
"GPSLatitudeRef": {
|
|
678
|
+
"number": 1,
|
|
679
|
+
"type": ctypes.c_char_p,
|
|
680
|
+
},
|
|
681
|
+
"GPSLatitude": {
|
|
682
|
+
"number": 2,
|
|
683
|
+
"type": ctypes.c_char_p,
|
|
684
|
+
},
|
|
685
|
+
"GPSLongitudeRef": {
|
|
686
|
+
"number": 3,
|
|
687
|
+
"type": ctypes.c_char_p,
|
|
688
|
+
},
|
|
689
|
+
"GPSLongitude": {
|
|
690
|
+
"number": 4,
|
|
691
|
+
"type": ctypes.c_char_p,
|
|
692
|
+
},
|
|
693
|
+
"GPSAltitudeRef": {
|
|
694
|
+
"number": 5,
|
|
695
|
+
"type": ctypes.c_char_p,
|
|
696
|
+
},
|
|
697
|
+
"GPSAltitude": {
|
|
698
|
+
"number": 6,
|
|
699
|
+
"type": ctypes.c_char_p,
|
|
700
|
+
},
|
|
701
|
+
"GPSTimestamp": {
|
|
702
|
+
"number": 7,
|
|
703
|
+
"type": ctypes.c_char_p,
|
|
704
|
+
},
|
|
705
|
+
"GPSSatellites": {
|
|
706
|
+
"number": 8,
|
|
707
|
+
"type": ctypes.c_char_p,
|
|
708
|
+
},
|
|
709
|
+
"GPSStatus": {
|
|
710
|
+
"number": 9,
|
|
711
|
+
"type": ctypes.c_char_p,
|
|
712
|
+
},
|
|
713
|
+
"GPSMeasureMode": {
|
|
714
|
+
"number": 10,
|
|
715
|
+
"type": ctypes.c_char_p,
|
|
716
|
+
},
|
|
717
|
+
"GPSDOP": {
|
|
674
718
|
"number": 11,
|
|
675
719
|
"type": ctypes.c_char_p,
|
|
676
720
|
},
|
|
721
|
+
"GPSSpeedRef": {
|
|
722
|
+
"number": 12,
|
|
723
|
+
"type": ctypes.c_char_p,
|
|
724
|
+
},
|
|
725
|
+
"GPSSpeed": {
|
|
726
|
+
"number": 13,
|
|
727
|
+
"type": ctypes.c_char_p,
|
|
728
|
+
},
|
|
729
|
+
"GPSTrackRef": {
|
|
730
|
+
"number": 14,
|
|
731
|
+
"type": ctypes.c_char_p,
|
|
732
|
+
},
|
|
733
|
+
"GPSTrack": {
|
|
734
|
+
"number": 15,
|
|
735
|
+
"type": ctypes.c_char_p,
|
|
736
|
+
},
|
|
737
|
+
"GPSImgDirectionRef": {
|
|
738
|
+
"number": 16,
|
|
739
|
+
"type": ctypes.c_char_p,
|
|
740
|
+
},
|
|
741
|
+
"GPSImgDirection": {
|
|
742
|
+
"number": 17,
|
|
743
|
+
"type": ctypes.c_char_p,
|
|
744
|
+
},
|
|
745
|
+
"GPSMapDatum": {
|
|
746
|
+
"number": 18,
|
|
747
|
+
"type": ctypes.c_char_p,
|
|
748
|
+
},
|
|
749
|
+
"GPSDestLatitudeRef": {
|
|
750
|
+
"number": 19,
|
|
751
|
+
"type": ctypes.c_char_p,
|
|
752
|
+
},
|
|
753
|
+
"GPSDestLatitude": {
|
|
754
|
+
"number": 20,
|
|
755
|
+
"type": ctypes.c_char_p,
|
|
756
|
+
},
|
|
757
|
+
"GPSDestLongitudeRef": {
|
|
758
|
+
"number": 21,
|
|
759
|
+
"type": ctypes.c_char_p,
|
|
760
|
+
},
|
|
761
|
+
"GPSDestLongitude": {
|
|
762
|
+
"number": 22,
|
|
763
|
+
"type": ctypes.c_char_p,
|
|
764
|
+
},
|
|
765
|
+
"GPSBearingRef": {
|
|
766
|
+
"number": 23,
|
|
767
|
+
"type": ctypes.c_char_p,
|
|
768
|
+
},
|
|
769
|
+
"GPSBearing": {
|
|
770
|
+
"number": 24,
|
|
771
|
+
"type": ctypes.c_char_p,
|
|
772
|
+
},
|
|
773
|
+
"GPSDestDistanceRef": {
|
|
774
|
+
"number": 25,
|
|
775
|
+
"type": ctypes.c_char_p,
|
|
776
|
+
},
|
|
777
|
+
"GPSDestDistance": {
|
|
778
|
+
"number": 26,
|
|
779
|
+
"type": ctypes.c_int16,
|
|
780
|
+
},
|
|
781
|
+
"GPSProcessingMethod": {
|
|
782
|
+
"number": 27,
|
|
783
|
+
"type": ctypes.c_char_p,
|
|
784
|
+
},
|
|
785
|
+
"GPSAreaInformation": {
|
|
786
|
+
"number": 28,
|
|
787
|
+
"type": ctypes.c_char_p,
|
|
788
|
+
},
|
|
789
|
+
"GPSDateStamp": {
|
|
790
|
+
"number": 29,
|
|
791
|
+
"type": ctypes.c_char_p,
|
|
792
|
+
},
|
|
793
|
+
"GPSDifferential": {
|
|
794
|
+
"number": 30,
|
|
795
|
+
"type": ctypes.c_char_p,
|
|
796
|
+
},
|
|
797
|
+
"GPSHPositioningError": {
|
|
798
|
+
"number": 31,
|
|
799
|
+
"type": ctypes.c_int16,
|
|
800
|
+
},
|
|
677
801
|
"SubFileType": {
|
|
678
802
|
"number": 254,
|
|
679
803
|
"type": ctypes.c_int16,
|
|
@@ -1968,6 +2092,7 @@ class _Ifd(object):
|
|
|
1968
2092
|
|
|
1969
2093
|
def parse_tag(self, tag, dtype, count, offset_buf):
|
|
1970
2094
|
"""Interpret an Exif image tag data payload."""
|
|
2095
|
+
|
|
1971
2096
|
try:
|
|
1972
2097
|
fmt = DATATYPE2FMT[dtype]["format"] * count
|
|
1973
2098
|
payload_size = DATATYPE2FMT[dtype]["nbytes"] * count
|
|
@@ -1994,7 +2119,10 @@ class _Ifd(object):
|
|
|
1994
2119
|
# Rational or Signed Rational. Construct the list of values.
|
|
1995
2120
|
rational_payload = []
|
|
1996
2121
|
for j in range(count):
|
|
1997
|
-
|
|
2122
|
+
try:
|
|
2123
|
+
value = float(payload[j * 2]) / float(payload[j * 2 + 1]) # noqa : E501
|
|
2124
|
+
except ZeroDivisionError:
|
|
2125
|
+
value = np.nan
|
|
1998
2126
|
rational_payload.append(value)
|
|
1999
2127
|
payload = np.array(rational_payload)
|
|
2000
2128
|
if count == 1:
|
|
@@ -2017,8 +2145,8 @@ class _Ifd(object):
|
|
|
2017
2145
|
warnings.warn(msg, UserWarning)
|
|
2018
2146
|
tag_name = tag
|
|
2019
2147
|
|
|
2020
|
-
if tag_name
|
|
2021
|
-
# There's an
|
|
2148
|
+
if tag_name in ("ExifTag", 'GPSIFD'):
|
|
2149
|
+
# There's an IFD at the offset specified here.
|
|
2022
2150
|
ifd = _Ifd(self.endian, self.read_buffer, value)
|
|
2023
2151
|
self.processed_ifd[tag_name] = ifd.processed_ifd
|
|
2024
2152
|
else:
|