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/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
- "ProcessingSoftware": {
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
- value = float(payload[j * 2]) / float(payload[j * 2 + 1])
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 == "ExifTag":
2021
- # There's an Exif IFD at the offset specified here.
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: