Glymur 0.13.7__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/_iccprofile.py +73 -72
- glymur/codestream.py +385 -308
- glymur/command_line.py +129 -1
- glymur/config.py +15 -14
- glymur/core.py +18 -22
- glymur/jp2box.py +758 -576
- glymur/jp2k.py +185 -149
- glymur/jp2kr.py +59 -45
- glymur/jpeg.py +196 -0
- glymur/lib/openjp2.py +198 -285
- glymur/lib/tiff.py +1251 -1127
- glymur/options.py +33 -28
- glymur/tiff.py +77 -406
- glymur/version.py +1 -1
- {Glymur-0.13.7.dist-info → glymur-0.14.0.post1.dist-info}/METADATA +5 -3
- glymur-0.14.0.post1.dist-info/RECORD +27 -0
- {Glymur-0.13.7.dist-info → glymur-0.14.0.post1.dist-info}/WHEEL +1 -1
- {Glymur-0.13.7.dist-info → glymur-0.14.0.post1.dist-info}/entry_points.txt +1 -0
- Glymur-0.13.7.dist-info/RECORD +0 -25
- {Glymur-0.13.7.dist-info → glymur-0.14.0.post1.dist-info/licenses}/LICENSE.txt +0 -0
- {Glymur-0.13.7.dist-info → glymur-0.14.0.post1.dist-info}/top_level.txt +0 -0
glymur/tiff.py
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
# standard library imports
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
import io
|
|
4
3
|
import logging
|
|
5
4
|
import pathlib
|
|
6
5
|
import shutil
|
|
7
|
-
import struct
|
|
8
6
|
import sys
|
|
9
7
|
from typing import List, Tuple
|
|
10
|
-
from uuid import UUID
|
|
11
8
|
import warnings
|
|
12
9
|
|
|
13
10
|
# 3rd party library imports
|
|
@@ -15,21 +12,16 @@ import numpy as np
|
|
|
15
12
|
|
|
16
13
|
# local imports
|
|
17
14
|
from glymur import Jp2k, set_option
|
|
18
|
-
from glymur.core import SRGB
|
|
15
|
+
from glymur.core import SRGB
|
|
16
|
+
from ._core_converter import _2JP2Converter
|
|
19
17
|
from .lib import tiff as libtiff
|
|
20
|
-
from .lib.tiff import DATATYPE2FMT
|
|
21
18
|
from . import jp2box
|
|
22
19
|
|
|
23
20
|
# we need a lower case mapping from the tag name to the tag number
|
|
24
|
-
TAGNAME2NUM = {k.lower(): v[
|
|
21
|
+
TAGNAME2NUM = {k.lower(): v["number"] for k, v in libtiff.TAGS.items()}
|
|
25
22
|
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
_TIFF = 42
|
|
29
|
-
_BIGTIFF = 43
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class Tiff2Jp2k(object):
|
|
24
|
+
class Tiff2Jp2k(_2JP2Converter):
|
|
33
25
|
"""
|
|
34
26
|
Transform a TIFF image into a JP2 image.
|
|
35
27
|
|
|
@@ -45,7 +37,7 @@ class Tiff2Jp2k(object):
|
|
|
45
37
|
Dimensions of the image.
|
|
46
38
|
jp2 : JP2K object
|
|
47
39
|
Write to this JPEG2000 file
|
|
48
|
-
|
|
40
|
+
jp2_path : path
|
|
49
41
|
Path to JPEG 2000 file to be written.
|
|
50
42
|
jp2_kwargs : dict
|
|
51
43
|
Keyword arguments to pass along to the Jp2k constructor.
|
|
@@ -55,7 +47,7 @@ class Tiff2Jp2k(object):
|
|
|
55
47
|
The number of rows per strip in the TIFF.
|
|
56
48
|
spp : int
|
|
57
49
|
Samples Per Pixel TIFF tag value
|
|
58
|
-
|
|
50
|
+
tiff_path : path
|
|
59
51
|
Path to TIFF file.
|
|
60
52
|
tilesize : tuple
|
|
61
53
|
The dimensions of a tile in the JP2K file.
|
|
@@ -69,8 +61,8 @@ class Tiff2Jp2k(object):
|
|
|
69
61
|
|
|
70
62
|
def __init__(
|
|
71
63
|
self,
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
tiff_path: pathlib.Path,
|
|
65
|
+
jp2_path: pathlib.Path,
|
|
74
66
|
create_exif_uuid: bool = True,
|
|
75
67
|
create_xmp_uuid: bool = True,
|
|
76
68
|
exclude_tags: List[int | str] | None = None,
|
|
@@ -78,7 +70,7 @@ class Tiff2Jp2k(object):
|
|
|
78
70
|
tilesize: Tuple[int, int] | None = None,
|
|
79
71
|
include_icc_profile: bool = False,
|
|
80
72
|
verbosity: int = logging.CRITICAL,
|
|
81
|
-
**kwargs
|
|
73
|
+
**kwargs,
|
|
82
74
|
):
|
|
83
75
|
"""
|
|
84
76
|
Construct the object.
|
|
@@ -105,16 +97,25 @@ class Tiff2Jp2k(object):
|
|
|
105
97
|
verbosity : int
|
|
106
98
|
Set the level of logging, i.e. WARNING, INFO, etc.
|
|
107
99
|
"""
|
|
100
|
+
super().__init__(
|
|
101
|
+
create_exif_uuid, create_xmp_uuid, include_icc_profile, tilesize,
|
|
102
|
+
verbosity
|
|
103
|
+
)
|
|
108
104
|
|
|
109
|
-
self.
|
|
110
|
-
if not self.
|
|
111
|
-
raise FileNotFoundError(f
|
|
105
|
+
self.tiff_path = pathlib.Path(tiff_path)
|
|
106
|
+
if not self.tiff_path.exists():
|
|
107
|
+
raise FileNotFoundError(f"{tiff_path} does not exist")
|
|
108
|
+
|
|
109
|
+
self.jp2_path = pathlib.Path(jp2_path)
|
|
110
|
+
if self.jp2_path.exists():
|
|
111
|
+
msg = (
|
|
112
|
+
f'{str(self.jp2_path)} already exists, ',
|
|
113
|
+
'please delete if you wish to overwrite.'
|
|
114
|
+
)
|
|
115
|
+
raise FileExistsError(msg)
|
|
112
116
|
|
|
113
|
-
self.jp2_filename = jp2_filename
|
|
114
|
-
self.tilesize = tilesize
|
|
115
117
|
self.create_exif_uuid = create_exif_uuid
|
|
116
118
|
self.create_xmp_uuid = create_xmp_uuid
|
|
117
|
-
self.include_icc_profile = include_icc_profile
|
|
118
119
|
|
|
119
120
|
if exclude_tags is None:
|
|
120
121
|
exclude_tags = []
|
|
@@ -126,16 +127,13 @@ class Tiff2Jp2k(object):
|
|
|
126
127
|
# Assume that there is no ColorMap tag until we know otherwise.
|
|
127
128
|
self._colormap = None
|
|
128
129
|
|
|
129
|
-
# Assume that there is no ICC profile tag until we know otherwise.
|
|
130
|
-
self.icc_profile = None
|
|
131
|
-
|
|
132
130
|
# Assume no XML_PACKET tag until we know otherwise.
|
|
133
131
|
self.xmp_data = None
|
|
134
132
|
|
|
135
133
|
self.setup_logging(verbosity)
|
|
136
134
|
|
|
137
135
|
if num_threads > 1:
|
|
138
|
-
set_option(
|
|
136
|
+
set_option("lib.num_threads", num_threads)
|
|
139
137
|
|
|
140
138
|
def _process_exclude_tags(self, exclude_tags):
|
|
141
139
|
"""The list of tags to exclude may be mixed type (str or integer).
|
|
@@ -188,16 +186,9 @@ class Tiff2Jp2k(object):
|
|
|
188
186
|
|
|
189
187
|
return lst
|
|
190
188
|
|
|
191
|
-
def setup_logging(self, verbosity):
|
|
192
|
-
self.logger = logging.getLogger('tiff2jp2')
|
|
193
|
-
self.logger.setLevel(verbosity)
|
|
194
|
-
ch = logging.StreamHandler()
|
|
195
|
-
ch.setLevel(verbosity)
|
|
196
|
-
self.logger.addHandler(ch)
|
|
197
|
-
|
|
198
189
|
def __enter__(self):
|
|
199
190
|
"""The Tiff2Jp2k must be used with a context manager."""
|
|
200
|
-
self.tiff_fp = libtiff.open(self.
|
|
191
|
+
self.tiff_fp = libtiff.open(str(self.tiff_path))
|
|
201
192
|
return self
|
|
202
193
|
|
|
203
194
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
@@ -226,7 +217,7 @@ class Tiff2Jp2k(object):
|
|
|
226
217
|
if photo != libtiff.Photometric.PALETTE:
|
|
227
218
|
return
|
|
228
219
|
|
|
229
|
-
jp2h = [box for box in self.jp2.box if box.box_id ==
|
|
220
|
+
jp2h = [box for box in self.jp2.box if box.box_id == "jp2h"][0]
|
|
230
221
|
|
|
231
222
|
bps = (8, 8, 8)
|
|
232
223
|
pclr = jp2box.PaletteBox(
|
|
@@ -246,46 +237,14 @@ class Tiff2Jp2k(object):
|
|
|
246
237
|
|
|
247
238
|
# fix the colr box. the colorspace needs to be changed from greyscale
|
|
248
239
|
# to rgb
|
|
249
|
-
colr = [box for box in jp2h.box if box.box_id ==
|
|
240
|
+
colr = [box for box in jp2h.box if box.box_id == "colr"][0]
|
|
250
241
|
colr.colorspace = SRGB
|
|
251
242
|
|
|
252
|
-
temp_filename = str(self.
|
|
243
|
+
temp_filename = str(self.jp2_path) + ".tmp"
|
|
253
244
|
self.jp2.wrap(temp_filename, boxes=self.jp2.box)
|
|
254
|
-
shutil.move(temp_filename, self.
|
|
245
|
+
shutil.move(temp_filename, self.jp2_path)
|
|
255
246
|
self.jp2.parse()
|
|
256
247
|
|
|
257
|
-
def rewrap_for_icc_profile(self):
|
|
258
|
-
"""Consume a TIFF ICC profile, if one is there."""
|
|
259
|
-
if self.icc_profile is None and self.include_icc_profile:
|
|
260
|
-
self.logger.warning("No ICC profile was found.")
|
|
261
|
-
|
|
262
|
-
if self.icc_profile is None or not self.include_icc_profile:
|
|
263
|
-
return
|
|
264
|
-
|
|
265
|
-
self.logger.info(
|
|
266
|
-
'Consuming an ICC profile into JP2 color specification box.'
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
colr = jp2box.ColourSpecificationBox(
|
|
270
|
-
method=RESTRICTED_ICC_PROFILE,
|
|
271
|
-
precedence=0,
|
|
272
|
-
icc_profile=self.icc_profile
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
# construct the new set of JP2 boxes, insert the color specification
|
|
276
|
-
# box with the ICC profile
|
|
277
|
-
jp2 = Jp2k(self.jp2_filename)
|
|
278
|
-
boxes = jp2.box
|
|
279
|
-
boxes[2].box = [boxes[2].box[0], colr]
|
|
280
|
-
|
|
281
|
-
# re-wrap the codestream, involves a file copy
|
|
282
|
-
tmp_filename = str(self.jp2_filename) + '.tmp'
|
|
283
|
-
|
|
284
|
-
with open(tmp_filename, mode='wb') as tfile:
|
|
285
|
-
jp2.wrap(tfile.name, boxes=boxes)
|
|
286
|
-
|
|
287
|
-
shutil.move(tmp_filename, self.jp2_filename)
|
|
288
|
-
|
|
289
248
|
def append_extra_jp2_boxes(self):
|
|
290
249
|
"""Copy over the TIFF IFD. Place it in a UUID box. Append to the JPEG
|
|
291
250
|
2000 file.
|
|
@@ -293,69 +252,13 @@ class Tiff2Jp2k(object):
|
|
|
293
252
|
self.append_exif_uuid_box()
|
|
294
253
|
self.append_xmp_uuid_box()
|
|
295
254
|
|
|
296
|
-
def append_exif_uuid_box(self):
|
|
297
|
-
"""Append an EXIF UUID box onto the end of the JPEG 2000 file. It will
|
|
298
|
-
contain metadata from the TIFF IFD.
|
|
299
|
-
"""
|
|
300
|
-
if not self.create_exif_uuid:
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
# create a bytesio object for the IFD
|
|
304
|
-
b = io.BytesIO()
|
|
305
|
-
|
|
306
|
-
# write this 32-bit header into the UUID, no matter if we had bigtiff
|
|
307
|
-
# or regular tiff or big endian
|
|
308
|
-
data = struct.pack('<BBHI', 73, 73, 42, 8)
|
|
309
|
-
b.write(data)
|
|
310
|
-
|
|
311
|
-
self._write_ifd(b, self.tags)
|
|
312
|
-
|
|
313
|
-
# create the Exif UUID
|
|
314
|
-
if self.found_geotiff_tags:
|
|
315
|
-
# geotiff UUID
|
|
316
|
-
the_uuid = UUID('b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03')
|
|
317
|
-
payload = b.getvalue()
|
|
318
|
-
else:
|
|
319
|
-
# Make it an exif UUID.
|
|
320
|
-
the_uuid = UUID(bytes=b'JpgTiffExif->JP2')
|
|
321
|
-
payload = b'EXIF\0\0' + b.getvalue()
|
|
322
|
-
|
|
323
|
-
# the length of the box is the length of the payload plus 8 bytes
|
|
324
|
-
# to store the length of the box and the box ID
|
|
325
|
-
box_length = len(payload) + 8
|
|
326
|
-
|
|
327
|
-
uuid_box = jp2box.UUIDBox(the_uuid, payload, box_length)
|
|
328
|
-
with open(self.jp2_filename, mode='ab') as f:
|
|
329
|
-
uuid_box.write(f)
|
|
330
|
-
|
|
331
|
-
self.jp2.finalize(force_parse=True)
|
|
332
|
-
|
|
333
|
-
def append_xmp_uuid_box(self):
|
|
334
|
-
"""Append an XMP UUID box onto the end of the JPEG 2000 file if there
|
|
335
|
-
was an XMP tag in the TIFF IFD.
|
|
336
|
-
"""
|
|
337
|
-
|
|
338
|
-
if self.xmp_data is None:
|
|
339
|
-
return
|
|
340
|
-
|
|
341
|
-
if not self.create_xmp_uuid:
|
|
342
|
-
return
|
|
343
|
-
|
|
344
|
-
# create the XMP UUID
|
|
345
|
-
the_uuid = jp2box.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
|
|
346
|
-
payload = bytes(self.xmp_data)
|
|
347
|
-
box_length = len(payload) + 8
|
|
348
|
-
uuid_box = jp2box.UUIDBox(the_uuid, payload, box_length)
|
|
349
|
-
with open(self.jp2_filename, mode='ab') as f:
|
|
350
|
-
uuid_box.write(f)
|
|
351
|
-
|
|
352
255
|
def get_main_ifd(self):
|
|
353
256
|
"""Read all the tags in the main IFD. We do it this way because of the
|
|
354
257
|
difficulty in using TIFFGetFieldDefaulted when the datatype of a tag
|
|
355
258
|
can differ.
|
|
356
259
|
"""
|
|
357
260
|
|
|
358
|
-
with open(
|
|
261
|
+
with self.tiff_path.open(mode="rb") as tfp:
|
|
359
262
|
|
|
360
263
|
self.read_tiff_header(tfp)
|
|
361
264
|
|
|
@@ -364,7 +267,7 @@ class Tiff2Jp2k(object):
|
|
|
364
267
|
if 320 in self.tags:
|
|
365
268
|
|
|
366
269
|
# the TIFF must have PALETTE photometric interpretation
|
|
367
|
-
data = np.array(self.tags[320][
|
|
270
|
+
data = np.array(self.tags[320]["payload"])
|
|
368
271
|
self._colormap = data.reshape(len(data) // 3, 3)
|
|
369
272
|
self._colormap = self._colormap / 65535
|
|
370
273
|
self._colormap = (self._colormap * 255).astype(np.uint8)
|
|
@@ -372,256 +275,18 @@ class Tiff2Jp2k(object):
|
|
|
372
275
|
if 700 in self.tags:
|
|
373
276
|
|
|
374
277
|
# XMLPacket
|
|
375
|
-
self.xmp_data = self.tags[700][
|
|
278
|
+
self.xmp_data = bytes(self.tags[700]["payload"])
|
|
376
279
|
|
|
377
280
|
else:
|
|
378
281
|
self.xmp_data = None
|
|
379
282
|
|
|
380
|
-
if 34665 in self.tags:
|
|
381
|
-
# we have an EXIF IFD
|
|
382
|
-
offset = self.tags[34665]['payload'][0]
|
|
383
|
-
tfp.seek(offset)
|
|
384
|
-
exif_ifd = self.read_ifd(tfp)
|
|
385
|
-
|
|
386
|
-
self.tags[34665]['payload'] = exif_ifd
|
|
387
|
-
|
|
388
283
|
if 34675 in self.tags:
|
|
389
284
|
# ICC profile
|
|
390
|
-
self.icc_profile = bytes(self.tags[34675][
|
|
285
|
+
self.icc_profile = bytes(self.tags[34675]["payload"])
|
|
391
286
|
|
|
392
287
|
else:
|
|
393
288
|
self.icc_profile = None
|
|
394
289
|
|
|
395
|
-
def read_ifd(self, tfp):
|
|
396
|
-
"""Process either the main IFD or an Exif IFD
|
|
397
|
-
|
|
398
|
-
Parameters
|
|
399
|
-
----------
|
|
400
|
-
tfp : file-like
|
|
401
|
-
FILE pointer for TIFF
|
|
402
|
-
|
|
403
|
-
Returns
|
|
404
|
-
-------
|
|
405
|
-
dictionary of the TIFF IFD
|
|
406
|
-
"""
|
|
407
|
-
|
|
408
|
-
self.found_geotiff_tags = False
|
|
409
|
-
|
|
410
|
-
tag_length = 20 if self.version == _BIGTIFF else 12
|
|
411
|
-
|
|
412
|
-
# how many tags?
|
|
413
|
-
if self.version == _BIGTIFF:
|
|
414
|
-
buffer = tfp.read(8)
|
|
415
|
-
num_tags, = struct.unpack(self.endian + 'Q', buffer)
|
|
416
|
-
else:
|
|
417
|
-
buffer = tfp.read(2)
|
|
418
|
-
num_tags, = struct.unpack(self.endian + 'H', buffer)
|
|
419
|
-
|
|
420
|
-
# Ok, so now we have the IFD main body, but following that we have
|
|
421
|
-
# the tag payloads that cannot fit into 4 bytes.
|
|
422
|
-
|
|
423
|
-
# the IFD main body in the TIFF. As it might be big endian, we
|
|
424
|
-
# cannot just process it as one big chunk.
|
|
425
|
-
buffer = tfp.read(num_tags * tag_length)
|
|
426
|
-
|
|
427
|
-
if self.version == _BIGTIFF:
|
|
428
|
-
tag_format_str = self.endian + 'HHQQ'
|
|
429
|
-
tag_payload_offset = 12
|
|
430
|
-
max_tag_payload_length = 8
|
|
431
|
-
else:
|
|
432
|
-
tag_format_str = self.endian + 'HHII'
|
|
433
|
-
tag_payload_offset = 8
|
|
434
|
-
max_tag_payload_length = 4
|
|
435
|
-
|
|
436
|
-
tags = {}
|
|
437
|
-
|
|
438
|
-
for idx in range(num_tags):
|
|
439
|
-
|
|
440
|
-
self.logger.debug(f'tag #: {idx}')
|
|
441
|
-
|
|
442
|
-
tag_data = buffer[idx * tag_length:(idx + 1) * tag_length]
|
|
443
|
-
|
|
444
|
-
tag, dtype, nvalues, offset = struct.unpack(tag_format_str, tag_data) # noqa : E501
|
|
445
|
-
|
|
446
|
-
if tag == 34735:
|
|
447
|
-
self.found_geotiff_tags = True
|
|
448
|
-
|
|
449
|
-
payload_length = DATATYPE2FMT[dtype]['nbytes'] * nvalues
|
|
450
|
-
|
|
451
|
-
if payload_length > max_tag_payload_length:
|
|
452
|
-
# the payload does not fit into the tag entry, so use the
|
|
453
|
-
# offset to seek to that position
|
|
454
|
-
current_position = tfp.tell()
|
|
455
|
-
tfp.seek(offset)
|
|
456
|
-
payload_buffer = tfp.read(payload_length)
|
|
457
|
-
tfp.seek(current_position)
|
|
458
|
-
|
|
459
|
-
# read the payload from the TIFF
|
|
460
|
-
payload_format = DATATYPE2FMT[dtype]['format'] * nvalues
|
|
461
|
-
payload = struct.unpack(
|
|
462
|
-
self.endian + payload_format, payload_buffer
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
else:
|
|
466
|
-
# the payload DOES fit into the TIFF tag entry
|
|
467
|
-
payload_buffer = tag_data[tag_payload_offset:]
|
|
468
|
-
|
|
469
|
-
# read ALL of the payload buffer
|
|
470
|
-
fmt = DATATYPE2FMT[dtype]['format']
|
|
471
|
-
num_items = (
|
|
472
|
-
int(max_tag_payload_length / DATATYPE2FMT[dtype]['nbytes'])
|
|
473
|
-
)
|
|
474
|
-
payload_format = self.endian + fmt * num_items
|
|
475
|
-
payload = struct.unpack(payload_format, payload_buffer)
|
|
476
|
-
|
|
477
|
-
# Extract the actual payload. Two things going
|
|
478
|
-
# on here. First of all, not all of the items may
|
|
479
|
-
# be used. For example, if the payload length is
|
|
480
|
-
# 4 bytes but the format string was HHH, the that
|
|
481
|
-
# last 16 bit value is not wanted, so we should
|
|
482
|
-
# discard it. Second thing is that the signed and
|
|
483
|
-
# unsigned rational datatypes effectively have twice
|
|
484
|
-
# the number of values so we need to account for that.
|
|
485
|
-
if dtype in [5, 10]:
|
|
486
|
-
payload = payload[:2 * nvalues]
|
|
487
|
-
else:
|
|
488
|
-
payload = payload[:nvalues]
|
|
489
|
-
|
|
490
|
-
tags[tag] = {
|
|
491
|
-
'dtype': dtype,
|
|
492
|
-
'nvalues': nvalues,
|
|
493
|
-
'payload': payload
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return tags
|
|
497
|
-
|
|
498
|
-
def _write_ifd(self, b, tags):
|
|
499
|
-
"""Write the IFD out to the UUIDBox. We will always write IFDs
|
|
500
|
-
for 32-bit TIFFs, i.e. 12 byte tags, meaning just 4 bytes within
|
|
501
|
-
the tag for the tag data
|
|
502
|
-
"""
|
|
503
|
-
|
|
504
|
-
little_tiff_tag_length = 12
|
|
505
|
-
max_tag_payload_length = 4
|
|
506
|
-
|
|
507
|
-
# exclude any unwanted tags
|
|
508
|
-
if self.exclude_tags is not None:
|
|
509
|
-
for tag in self.exclude_tags:
|
|
510
|
-
if tag in tags:
|
|
511
|
-
tags.pop(tag)
|
|
512
|
-
|
|
513
|
-
num_tags = len(tags)
|
|
514
|
-
write_buffer = struct.pack('<H', num_tags)
|
|
515
|
-
b.write(write_buffer)
|
|
516
|
-
|
|
517
|
-
# Ok, so now we have the IFD main body, but following that we have
|
|
518
|
-
# the tag payloads that cannot fit into 4 bytes.
|
|
519
|
-
|
|
520
|
-
ifd_start_loc = b.tell()
|
|
521
|
-
after_ifd_position = ifd_start_loc + num_tags * little_tiff_tag_length
|
|
522
|
-
|
|
523
|
-
for idx, tag in enumerate(tags):
|
|
524
|
-
|
|
525
|
-
tag_offset = ifd_start_loc + idx * little_tiff_tag_length
|
|
526
|
-
self.logger.debug(f'tag #: {tag}, writing to {tag_offset}')
|
|
527
|
-
self.logger.debug(f'tag #: {tag}, after IFD {after_ifd_position}')
|
|
528
|
-
|
|
529
|
-
b.seek(tag_offset)
|
|
530
|
-
|
|
531
|
-
dtype = tags[tag]['dtype']
|
|
532
|
-
nvalues = tags[tag]['nvalues']
|
|
533
|
-
payload = tags[tag]['payload']
|
|
534
|
-
|
|
535
|
-
payload_length = DATATYPE2FMT[dtype]['nbytes'] * nvalues
|
|
536
|
-
|
|
537
|
-
if payload_length > max_tag_payload_length:
|
|
538
|
-
# the payload does not fit into the tag entry
|
|
539
|
-
|
|
540
|
-
# read the payload from the TIFF
|
|
541
|
-
payload_format = DATATYPE2FMT[dtype]['format'] * nvalues
|
|
542
|
-
|
|
543
|
-
# write the tag entry to the UUID
|
|
544
|
-
new_offset = after_ifd_position
|
|
545
|
-
buffer = struct.pack(
|
|
546
|
-
'<HHII', tag, dtype, nvalues, new_offset
|
|
547
|
-
)
|
|
548
|
-
b.write(buffer)
|
|
549
|
-
|
|
550
|
-
# now write the payload at the outlying position and then come
|
|
551
|
-
# back to the same position in the file stream
|
|
552
|
-
cpos = b.tell()
|
|
553
|
-
b.seek(new_offset)
|
|
554
|
-
|
|
555
|
-
format = '<' + DATATYPE2FMT[dtype]['format'] * nvalues
|
|
556
|
-
buffer = struct.pack(format, *payload)
|
|
557
|
-
b.write(buffer)
|
|
558
|
-
|
|
559
|
-
# keep track of the next position to write out-of-IFD data
|
|
560
|
-
after_ifd_position = b.tell()
|
|
561
|
-
b.seek(cpos)
|
|
562
|
-
|
|
563
|
-
else:
|
|
564
|
-
|
|
565
|
-
# the payload DOES fit into the TIFF tag entry
|
|
566
|
-
# write the tag metadata
|
|
567
|
-
buffer = struct.pack('<HHI', tag, dtype, nvalues)
|
|
568
|
-
b.write(buffer)
|
|
569
|
-
|
|
570
|
-
payload_format = DATATYPE2FMT[dtype]['format'] * nvalues
|
|
571
|
-
|
|
572
|
-
# we may need to alter the output format
|
|
573
|
-
if payload_format in ['H', 'B', 'I']:
|
|
574
|
-
# just write it as an integer
|
|
575
|
-
payload_format = 'I'
|
|
576
|
-
|
|
577
|
-
if tag == 34665:
|
|
578
|
-
# special case for an EXIF IFD
|
|
579
|
-
buffer = struct.pack('<I', after_ifd_position)
|
|
580
|
-
b.write(buffer)
|
|
581
|
-
b.seek(after_ifd_position)
|
|
582
|
-
after_ifd_position = self._write_ifd(b, payload)
|
|
583
|
-
|
|
584
|
-
else:
|
|
585
|
-
# write a normal tag
|
|
586
|
-
buffer = struct.pack('<' + payload_format, *payload)
|
|
587
|
-
b.write(buffer)
|
|
588
|
-
|
|
589
|
-
return after_ifd_position
|
|
590
|
-
|
|
591
|
-
def read_tiff_header(self, tfp):
|
|
592
|
-
"""Get the endian-ness of the TIFF, seek to the main IFD"""
|
|
593
|
-
|
|
594
|
-
buffer = tfp.read(4)
|
|
595
|
-
data = struct.unpack('BB', buffer[:2])
|
|
596
|
-
|
|
597
|
-
# big endian or little endian?
|
|
598
|
-
if data[0] == 73 and data[1] == 73:
|
|
599
|
-
# little endian
|
|
600
|
-
self.endian = '<'
|
|
601
|
-
elif data[0] == 77 and data[1] == 77:
|
|
602
|
-
# big endian
|
|
603
|
-
self.endian = '>'
|
|
604
|
-
# no other option is possible, libtiff.open would have errored out
|
|
605
|
-
# else:
|
|
606
|
-
# msg = (
|
|
607
|
-
# f"The byte order indication in the TIFF header "
|
|
608
|
-
# f"({data}) is invalid. It should be either "
|
|
609
|
-
# f"{bytes([73, 73])} or {bytes([77, 77])}."
|
|
610
|
-
# )
|
|
611
|
-
# raise RuntimeError(msg)
|
|
612
|
-
|
|
613
|
-
# version number and offset to the first IFD
|
|
614
|
-
version, = struct.unpack(self.endian + 'H', buffer[2:4])
|
|
615
|
-
self.version = _TIFF if version == 42 else _BIGTIFF
|
|
616
|
-
|
|
617
|
-
if self.version == _BIGTIFF:
|
|
618
|
-
buffer = tfp.read(12)
|
|
619
|
-
_, _, offset = struct.unpack(self.endian + 'HHQ', buffer)
|
|
620
|
-
else:
|
|
621
|
-
buffer = tfp.read(4)
|
|
622
|
-
offset, = struct.unpack(self.endian + 'I', buffer)
|
|
623
|
-
tfp.seek(offset)
|
|
624
|
-
|
|
625
290
|
def get_tag_value(self, tagnum):
|
|
626
291
|
"""Return the value associated with the tag. Some tags are not
|
|
627
292
|
actually written into the IFD, but are instead "defaulted".
|
|
@@ -640,11 +305,10 @@ class Tiff2Jp2k(object):
|
|
|
640
305
|
return 1
|
|
641
306
|
|
|
642
307
|
# The tag value is always stored as a tuple with at least one member.
|
|
643
|
-
return self.tags[tagnum][
|
|
308
|
+
return self.tags[tagnum]["payload"][0]
|
|
644
309
|
|
|
645
310
|
def copy_image(self):
|
|
646
|
-
"""Transfer the image data from the TIFF to the JPEG 2000 file.
|
|
647
|
-
"""
|
|
311
|
+
"""Transfer the image data from the TIFF to the JPEG 2000 file."""
|
|
648
312
|
|
|
649
313
|
if libtiff.isTiled(self.tiff_fp):
|
|
650
314
|
isTiled = True
|
|
@@ -695,7 +359,12 @@ class Tiff2Jp2k(object):
|
|
|
695
359
|
self.th = self.get_tag_value(323)
|
|
696
360
|
else:
|
|
697
361
|
self.tw = self.imagewidth
|
|
698
|
-
|
|
362
|
+
try:
|
|
363
|
+
self.rps = self.get_tag_value(278)
|
|
364
|
+
except KeyError:
|
|
365
|
+
# stripped but no RowsPerStrip tag? default to the image
|
|
366
|
+
# height
|
|
367
|
+
self.rps = self.imageheight
|
|
699
368
|
|
|
700
369
|
if self.spp == 1:
|
|
701
370
|
shape = (self.imageheight, self.imagewidth)
|
|
@@ -703,7 +372,7 @@ class Tiff2Jp2k(object):
|
|
|
703
372
|
shape = (self.imageheight, self.imagewidth, self.spp)
|
|
704
373
|
|
|
705
374
|
self.jp2 = Jp2k(
|
|
706
|
-
self.
|
|
375
|
+
self.jp2_path,
|
|
707
376
|
shape=shape,
|
|
708
377
|
tilesize=self.tilesize,
|
|
709
378
|
**self.jp2_kwargs
|
|
@@ -759,7 +428,7 @@ class Tiff2Jp2k(object):
|
|
|
759
428
|
)
|
|
760
429
|
|
|
761
430
|
# must reorder image planes on big-endian
|
|
762
|
-
if sys.byteorder ==
|
|
431
|
+
if sys.byteorder == "big":
|
|
763
432
|
image = np.flip(image, axis=2)
|
|
764
433
|
|
|
765
434
|
# potentially get rid of the alpha plane
|
|
@@ -777,7 +446,9 @@ class Tiff2Jp2k(object):
|
|
|
777
446
|
# This might be a bit bigger than the actual image because of a
|
|
778
447
|
# possibly partial last strip.
|
|
779
448
|
stripped_shape = (
|
|
780
|
-
num_tiff_strip_rows * self.rps,
|
|
449
|
+
num_tiff_strip_rows * self.rps,
|
|
450
|
+
self.imagewidth,
|
|
451
|
+
self.spp
|
|
781
452
|
)
|
|
782
453
|
image = np.zeros(stripped_shape, dtype=self.dtype)
|
|
783
454
|
|
|
@@ -793,7 +464,7 @@ class Tiff2Jp2k(object):
|
|
|
793
464
|
|
|
794
465
|
if self.imageheight != stripped_shape[0]:
|
|
795
466
|
# cut the image down due to a partial last strip
|
|
796
|
-
image = image[:self.imageheight, :, :]
|
|
467
|
+
image = image[: self.imageheight, :, :]
|
|
797
468
|
|
|
798
469
|
self.jp2[:] = image
|
|
799
470
|
|
|
@@ -810,7 +481,7 @@ class Tiff2Jp2k(object):
|
|
|
810
481
|
tiled_shape = (
|
|
811
482
|
num_tiff_tile_rows * self.th,
|
|
812
483
|
num_tiff_tile_cols * self.tw,
|
|
813
|
-
self.spp
|
|
484
|
+
self.spp,
|
|
814
485
|
)
|
|
815
486
|
|
|
816
487
|
image = np.zeros(tiled_shape, dtype=self.dtype)
|
|
@@ -830,7 +501,7 @@ class Tiff2Jp2k(object):
|
|
|
830
501
|
image[rows, cols, :] = tiff_tile
|
|
831
502
|
|
|
832
503
|
if final_shape != tiled_shape:
|
|
833
|
-
image = image[:final_shape[0], :final_shape[1], :]
|
|
504
|
+
image = image[: final_shape[0], : final_shape[1], :]
|
|
834
505
|
|
|
835
506
|
self.jp2[:] = image
|
|
836
507
|
|
|
@@ -841,7 +512,7 @@ class Tiff2Jp2k(object):
|
|
|
841
512
|
for jp2k_tilenum, tilewriter in enumerate(self.jp2.get_tilewriters()):
|
|
842
513
|
tiff_tiles = self._get_covering_tiles(jp2k_tilenum)
|
|
843
514
|
jp2k_tile = self._cover_tile(jp2k_tilenum, tiff_tiles)
|
|
844
|
-
self.logger.info(f
|
|
515
|
+
self.logger.info(f"Writing tile {jp2k_tilenum}")
|
|
845
516
|
tilewriter[:] = jp2k_tile
|
|
846
517
|
|
|
847
518
|
def _cover_tile(self, jp2k_tile_num, tiff_tile_nums):
|
|
@@ -859,8 +530,12 @@ class Tiff2Jp2k(object):
|
|
|
859
530
|
|
|
860
531
|
# Does the JP2K have partial tiles on the far right and bottom of the
|
|
861
532
|
# image.
|
|
862
|
-
partial_jp2_tile_rows = (self.imageheight / jth) != (
|
|
863
|
-
|
|
533
|
+
partial_jp2_tile_rows = (self.imageheight / jth) != (
|
|
534
|
+
self.imageheight // jth
|
|
535
|
+
) # noqa : E501
|
|
536
|
+
partial_jp2_tile_cols = (self.imagewidth / jtw) != (
|
|
537
|
+
self.imagewidth // jtw
|
|
538
|
+
) # noqa : E501
|
|
864
539
|
|
|
865
540
|
num_jp2k_tile_rows = int(np.ceil(self.imageheight / jth))
|
|
866
541
|
num_jp2k_tile_cols = int(np.ceil(self.imagewidth / jtw))
|
|
@@ -896,7 +571,7 @@ class Tiff2Jp2k(object):
|
|
|
896
571
|
libtiff.readRGBATile(self.tiff_fp, x, y, rgba_tile)
|
|
897
572
|
|
|
898
573
|
# The RGBA interface requires some reordering.
|
|
899
|
-
if sys.byteorder ==
|
|
574
|
+
if sys.byteorder == "little":
|
|
900
575
|
# image is upside down
|
|
901
576
|
dims = [0]
|
|
902
577
|
else:
|
|
@@ -911,7 +586,8 @@ class Tiff2Jp2k(object):
|
|
|
911
586
|
else:
|
|
912
587
|
|
|
913
588
|
tiff_tile = np.zeros(
|
|
914
|
-
(self.th, self.tw, self.spp),
|
|
589
|
+
(self.th, self.tw, self.spp),
|
|
590
|
+
dtype=self.dtype
|
|
915
591
|
)
|
|
916
592
|
libtiff.readEncodedTile(self.tiff_fp, ttile_num, tiff_tile)
|
|
917
593
|
|
|
@@ -936,17 +612,11 @@ class Tiff2Jp2k(object):
|
|
|
936
612
|
jp2k_tile[jrows, jcols, :] = tiff_tile[trows, tcols, :]
|
|
937
613
|
|
|
938
614
|
# last tile column? last tile row? If so, we may have a partial tile.
|
|
939
|
-
if
|
|
940
|
-
partial_jp2_tile_cols
|
|
941
|
-
and jp2k_tile_col == num_jp2k_tile_cols - 1
|
|
942
|
-
):
|
|
615
|
+
if partial_jp2_tile_cols and jp2k_tile_col == num_jp2k_tile_cols - 1:
|
|
943
616
|
last_j2k_cols = slice(0, self.imagewidth - jp2k_ulx)
|
|
944
617
|
jp2k_tile = jp2k_tile[:, last_j2k_cols, :].copy()
|
|
945
618
|
|
|
946
|
-
if
|
|
947
|
-
partial_jp2_tile_rows
|
|
948
|
-
and jp2k_tile_row == num_jp2k_tile_rows - 1
|
|
949
|
-
):
|
|
619
|
+
if partial_jp2_tile_rows and jp2k_tile_row == num_jp2k_tile_rows - 1:
|
|
950
620
|
last_j2k_rows = slice(0, self.imageheight - jp2k_uly)
|
|
951
621
|
jp2k_tile = jp2k_tile[last_j2k_rows, :, :].copy()
|
|
952
622
|
|
|
@@ -979,8 +649,6 @@ class Tiff2Jp2k(object):
|
|
|
979
649
|
# lower left corner
|
|
980
650
|
llx = ulx
|
|
981
651
|
lly = min(uly + jth - 1, self.imageheight - 1)
|
|
982
|
-
ll_tiff_tilenum = libtiff.computeTile(self.tiff_fp, llx, lly, 0, 0)
|
|
983
|
-
lower_tiff_tile_row = int(np.ceil(ll_tiff_tilenum // num_tiff_tile_cols)) # noqa : E501
|
|
984
652
|
|
|
985
653
|
# lower right corner
|
|
986
654
|
lrx = min(llx + jtw - 1, self.imagewidth - 1)
|
|
@@ -1014,8 +682,8 @@ class Tiff2Jp2k(object):
|
|
|
1014
682
|
|
|
1015
683
|
jth, jtw = self.tilesize
|
|
1016
684
|
|
|
1017
|
-
self.logger.debug(f
|
|
1018
|
-
self.logger.debug(f
|
|
685
|
+
self.logger.debug(f"image: {self.imageheight} x {self.imagewidth}")
|
|
686
|
+
self.logger.debug(f"jptile: {jth} x {jtw}")
|
|
1019
687
|
num_strips = libtiff.numberOfStrips(self.tiff_fp)
|
|
1020
688
|
|
|
1021
689
|
num_jp2k_tile_cols = int(np.ceil(self.imagewidth / jtw))
|
|
@@ -1025,7 +693,7 @@ class Tiff2Jp2k(object):
|
|
|
1025
693
|
jp2k_tile_row = idx // num_jp2k_tile_cols
|
|
1026
694
|
jp2k_tile_col = idx % num_jp2k_tile_cols
|
|
1027
695
|
|
|
1028
|
-
msg = f
|
|
696
|
+
msg = f"Tile: #{idx} row #{jp2k_tile_row} col #{jp2k_tile_col}"
|
|
1029
697
|
self.logger.info(msg)
|
|
1030
698
|
|
|
1031
699
|
# the coordinates of the upper left pixel of the jp2k tile
|
|
@@ -1037,7 +705,9 @@ class Tiff2Jp2k(object):
|
|
|
1037
705
|
# jp2k tiles from this same TIFF multi-strip.
|
|
1038
706
|
if jp2k_tile_col == 0:
|
|
1039
707
|
tiff_multi_strip = self._construct_multi_strip(
|
|
1040
|
-
july,
|
|
708
|
+
july,
|
|
709
|
+
num_strips,
|
|
710
|
+
jth,
|
|
1041
711
|
)
|
|
1042
712
|
|
|
1043
713
|
# construct the TIFF row and column slices from the multi-strip,
|
|
@@ -1102,7 +772,8 @@ class Tiff2Jp2k(object):
|
|
|
1102
772
|
# This may result in a multi-strip that has more rows than the jp2k
|
|
1103
773
|
# tile
|
|
1104
774
|
tiff_multi_strip = np.zeros(
|
|
1105
|
-
(num_rows, self.imagewidth, spp),
|
|
775
|
+
(num_rows, self.imagewidth, spp),
|
|
776
|
+
dtype=dtype
|
|
1106
777
|
)
|
|
1107
778
|
|
|
1108
779
|
# Fill the multi-strip
|
|
@@ -1111,7 +782,8 @@ class Tiff2Jp2k(object):
|
|
|
1111
782
|
if self.photo == libtiff.Photometric.YCBCR:
|
|
1112
783
|
|
|
1113
784
|
tiff_rgba_strip = np.zeros(
|
|
1114
|
-
(self.rps, self.imagewidth, 4),
|
|
785
|
+
(self.rps, self.imagewidth, 4),
|
|
786
|
+
dtype=dtype
|
|
1115
787
|
)
|
|
1116
788
|
|
|
1117
789
|
libtiff.readRGBAStrip(
|
|
@@ -1138,24 +810,23 @@ class Tiff2Jp2k(object):
|
|
|
1138
810
|
|
|
1139
811
|
# The rgba interface requires at least flipping the image
|
|
1140
812
|
# upside down, and also reordering the planes on big endian
|
|
1141
|
-
if sys.byteorder ==
|
|
813
|
+
if sys.byteorder == "little":
|
|
1142
814
|
dims = [0]
|
|
1143
815
|
else:
|
|
1144
816
|
dims = [0, 2]
|
|
1145
817
|
tiff_rgba_strip = np.flip(tiff_rgba_strip, axis=dims)
|
|
1146
818
|
|
|
1147
819
|
# potentially get rid of alpha plane
|
|
1148
|
-
tiff_strip = tiff_rgba_strip[:, :, :self.spp]
|
|
820
|
+
tiff_strip = tiff_rgba_strip[:, :, : self.spp]
|
|
1149
821
|
|
|
1150
822
|
else:
|
|
1151
823
|
|
|
1152
824
|
tiff_strip = np.zeros(
|
|
1153
|
-
|
|
825
|
+
(self.rps, self.imagewidth, spp),
|
|
826
|
+
dtype=dtype
|
|
1154
827
|
)
|
|
1155
828
|
|
|
1156
|
-
libtiff.readEncodedStrip(
|
|
1157
|
-
self.tiff_fp, stripnum, tiff_strip
|
|
1158
|
-
)
|
|
829
|
+
libtiff.readEncodedStrip(self.tiff_fp, stripnum, tiff_strip)
|
|
1159
830
|
|
|
1160
831
|
# push the strip into the multi-strip
|
|
1161
832
|
top_row = (stripnum - top_strip_num) * self.rps
|