Glymur 0.13.6__py3-none-any.whl → 0.13.8__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-0.13.6.dist-info → Glymur-0.13.8.dist-info}/METADATA +5 -3
- Glymur-0.13.8.dist-info/RECORD +25 -0
- {Glymur-0.13.6.dist-info → Glymur-0.13.8.dist-info}/WHEEL +1 -1
- glymur/_iccprofile.py +73 -72
- glymur/codestream.py +385 -308
- glymur/config.py +15 -14
- glymur/core.py +18 -22
- glymur/jp2box.py +736 -577
- glymur/jp2k.py +185 -149
- glymur/jp2kr.py +62 -48
- glymur/lib/openjp2.py +198 -285
- glymur/lib/tiff.py +1152 -1156
- glymur/options.py +33 -28
- glymur/tiff.py +105 -103
- glymur/version.py +1 -1
- Glymur-0.13.6.dist-info/RECORD +0 -25
- {Glymur-0.13.6.dist-info → Glymur-0.13.8.dist-info}/LICENSE.txt +0 -0
- {Glymur-0.13.6.dist-info → Glymur-0.13.8.dist-info}/entry_points.txt +0 -0
- {Glymur-0.13.6.dist-info → Glymur-0.13.8.dist-info}/top_level.txt +0 -0
glymur/jp2k.py
CHANGED
|
@@ -6,6 +6,7 @@ Copyright 2013 John Evans
|
|
|
6
6
|
|
|
7
7
|
License: MIT
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
# Standard library imports...
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
from collections import Counter
|
|
@@ -26,9 +27,13 @@ import glymur
|
|
|
26
27
|
from . import core, version, get_option
|
|
27
28
|
from .jp2kr import Jp2kr
|
|
28
29
|
from .jp2box import (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
ColourSpecificationBox,
|
|
31
|
+
ContiguousCodestreamBox,
|
|
32
|
+
FileTypeBox,
|
|
33
|
+
ImageHeaderBox,
|
|
34
|
+
InvalidJp2kError,
|
|
35
|
+
JP2HeaderBox,
|
|
36
|
+
JPEG2000SignatureBox,
|
|
32
37
|
)
|
|
33
38
|
from .lib import openjp2 as opj2
|
|
34
39
|
|
|
@@ -171,15 +176,15 @@ class Jp2k(Jp2kr):
|
|
|
171
176
|
self._shape = shape
|
|
172
177
|
elif data is not None:
|
|
173
178
|
self._shape = data.shape
|
|
174
|
-
elif not hasattr(self,
|
|
179
|
+
elif not hasattr(self, "shape"):
|
|
175
180
|
# We must be writing via slicing.
|
|
176
181
|
# Must be determined when writing.
|
|
177
182
|
self._shape = None
|
|
178
183
|
|
|
179
|
-
if not hasattr(self,
|
|
184
|
+
if not hasattr(self, "_codec_format"):
|
|
180
185
|
# Only set codec format if the superclass has not done so, i.e.
|
|
181
186
|
# we are writing instead of reading.
|
|
182
|
-
if self.filename[-4:].endswith((
|
|
187
|
+
if self.filename[-4:].endswith((".jp2", ".JP2", ".jpx", "JPX")):
|
|
183
188
|
self._codec_format = opj2.CODEC_JP2
|
|
184
189
|
else:
|
|
185
190
|
self._codec_format = opj2.CODEC_J2K
|
|
@@ -228,25 +233,27 @@ class Jp2k(Jp2kr):
|
|
|
228
233
|
header box if we were so instructed. This requires a wrapping
|
|
229
234
|
operation.
|
|
230
235
|
"""
|
|
231
|
-
jp2h = next(filter(lambda x: x.box_id ==
|
|
236
|
+
jp2h = next(filter(lambda x: x.box_id == "jp2h", self.box), None)
|
|
232
237
|
|
|
233
238
|
extra_boxes = []
|
|
234
239
|
if self._capture_resolution is not None:
|
|
235
240
|
resc = glymur.jp2box.CaptureResolutionBox(
|
|
236
|
-
self._capture_resolution[0],
|
|
241
|
+
self._capture_resolution[0],
|
|
242
|
+
self._capture_resolution[1],
|
|
237
243
|
)
|
|
238
244
|
extra_boxes.append(resc)
|
|
239
245
|
|
|
240
246
|
if self._display_resolution is not None:
|
|
241
247
|
resd = glymur.jp2box.DisplayResolutionBox(
|
|
242
|
-
self._display_resolution[0],
|
|
248
|
+
self._display_resolution[0],
|
|
249
|
+
self._display_resolution[1],
|
|
243
250
|
)
|
|
244
251
|
extra_boxes.append(resd)
|
|
245
252
|
|
|
246
253
|
rbox = glymur.jp2box.ResolutionBox(extra_boxes)
|
|
247
254
|
jp2h.box.append(rbox)
|
|
248
255
|
|
|
249
|
-
temp_filename = self.filename +
|
|
256
|
+
temp_filename = self.filename + ".tmp"
|
|
250
257
|
self.wrap(temp_filename, boxes=self.box)
|
|
251
258
|
shutil.move(temp_filename, self.filename)
|
|
252
259
|
self.parse(force=True)
|
|
@@ -254,18 +261,23 @@ class Jp2k(Jp2kr):
|
|
|
254
261
|
def _validate_kwargs(self):
|
|
255
262
|
"""Validate keyword parameters passed to the constructor."""
|
|
256
263
|
non_cinema_args = (
|
|
257
|
-
self._mct,
|
|
258
|
-
self.
|
|
259
|
-
self.
|
|
264
|
+
self._mct,
|
|
265
|
+
self._cratios,
|
|
266
|
+
self._psnr,
|
|
267
|
+
self._irreversible,
|
|
268
|
+
self._cbsize,
|
|
269
|
+
self._eph,
|
|
270
|
+
self._grid_offset,
|
|
271
|
+
self._modesw,
|
|
272
|
+
self._prog,
|
|
273
|
+
self._psizes,
|
|
274
|
+
self._sop,
|
|
275
|
+
self._subsam,
|
|
260
276
|
)
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
and not all([arg is None or not arg for arg in non_cinema_args])
|
|
277
|
+
if (self._cinema2k or self._cinema4k) and not all(
|
|
278
|
+
[arg is None or not arg for arg in non_cinema_args]
|
|
264
279
|
):
|
|
265
|
-
msg =
|
|
266
|
-
"Cannot specify cinema2k/cinema4k along with any other "
|
|
267
|
-
"options."
|
|
268
|
-
)
|
|
280
|
+
msg = "Do not specify cinema2k/cinema4k along with other options."
|
|
269
281
|
raise InvalidJp2kError(msg)
|
|
270
282
|
|
|
271
283
|
if self._psnr is not None:
|
|
@@ -295,7 +307,7 @@ class Jp2k(Jp2kr):
|
|
|
295
307
|
self._codec_format == opj2.CODEC_J2K
|
|
296
308
|
and self._colorspace is not None
|
|
297
309
|
):
|
|
298
|
-
msg =
|
|
310
|
+
msg = "Do not specify a colorspace when writing a raw codestream."
|
|
299
311
|
raise InvalidJp2kError(msg)
|
|
300
312
|
|
|
301
313
|
if (
|
|
@@ -304,8 +316,8 @@ class Jp2k(Jp2kr):
|
|
|
304
316
|
and self._display_resolution is not None
|
|
305
317
|
):
|
|
306
318
|
msg = (
|
|
307
|
-
|
|
308
|
-
|
|
319
|
+
"Do not specify capture/display resolution when writing a raw "
|
|
320
|
+
"codestream."
|
|
309
321
|
)
|
|
310
322
|
raise InvalidJp2kError(msg)
|
|
311
323
|
|
|
@@ -328,9 +340,9 @@ class Jp2k(Jp2kr):
|
|
|
328
340
|
|
|
329
341
|
if self.shape[:2] == self.tilesize:
|
|
330
342
|
msg = (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
343
|
+
"Do not write an image tile-by-tile "
|
|
344
|
+
"if there is only one tile in the first place. "
|
|
345
|
+
"See issue #586"
|
|
334
346
|
)
|
|
335
347
|
raise RuntimeError(msg)
|
|
336
348
|
|
|
@@ -352,9 +364,9 @@ class Jp2k(Jp2kr):
|
|
|
352
364
|
# Cinema modes imply MCT.
|
|
353
365
|
self._cparams.tcp_mct = 1
|
|
354
366
|
|
|
355
|
-
if cinema_mode ==
|
|
367
|
+
if cinema_mode == "cinema2k":
|
|
356
368
|
if fps not in [24, 48]:
|
|
357
|
-
msg =
|
|
369
|
+
msg = "Cinema2K frame rate must be either 24 or 48."
|
|
358
370
|
raise ValueError(msg)
|
|
359
371
|
|
|
360
372
|
if fps == 24:
|
|
@@ -384,7 +396,7 @@ class Jp2k(Jp2kr):
|
|
|
384
396
|
|
|
385
397
|
outfile = self.filename.encode()
|
|
386
398
|
num_pad_bytes = opj2.PATH_LEN - len(outfile)
|
|
387
|
-
outfile += b
|
|
399
|
+
outfile += b"0" * num_pad_bytes
|
|
388
400
|
cparams.outfile = outfile
|
|
389
401
|
|
|
390
402
|
cparams.codec_fmt = self._codec_format
|
|
@@ -394,11 +406,11 @@ class Jp2k(Jp2kr):
|
|
|
394
406
|
if self._cinema2k:
|
|
395
407
|
# cinema2k is an integer, so this test is "truthy"
|
|
396
408
|
self._cparams = cparams
|
|
397
|
-
self._set_cinema_params(
|
|
409
|
+
self._set_cinema_params("cinema2k", self._cinema2k)
|
|
398
410
|
|
|
399
411
|
if self._cinema4k:
|
|
400
412
|
self._cparams = cparams
|
|
401
|
-
self._set_cinema_params(
|
|
413
|
+
self._set_cinema_params("cinema4k", self._cinema4k)
|
|
402
414
|
|
|
403
415
|
if self._cbsize is not None:
|
|
404
416
|
cparams.cblockw_init = self._cbsize[1]
|
|
@@ -491,14 +503,14 @@ class Jp2k(Jp2kr):
|
|
|
491
503
|
This method can only be used to create JPEG 2000 images that can fit
|
|
492
504
|
in memory.
|
|
493
505
|
"""
|
|
494
|
-
if version.openjpeg_version <
|
|
506
|
+
if version.openjpeg_version < "2.3.0":
|
|
495
507
|
msg = (
|
|
496
508
|
"You must have at least version 2.3.0 of OpenJPEG in order to "
|
|
497
509
|
"write images."
|
|
498
510
|
)
|
|
499
511
|
raise RuntimeError(msg)
|
|
500
512
|
|
|
501
|
-
if hasattr(self,
|
|
513
|
+
if hasattr(self, "_cparams"):
|
|
502
514
|
msg = (
|
|
503
515
|
"You cannot write image data to a JPEG 2000 file "
|
|
504
516
|
"that already exists."
|
|
@@ -540,10 +552,9 @@ class Jp2k(Jp2kr):
|
|
|
540
552
|
f"and width dimensions must be larger than 4 pixels."
|
|
541
553
|
)
|
|
542
554
|
raise InvalidJp2kError(msg)
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
):
|
|
555
|
+
if np.log2(height) != np.floor(np.log2(height)) or np.log2(
|
|
556
|
+
width
|
|
557
|
+
) != np.floor(np.log2(width)):
|
|
547
558
|
msg = (
|
|
548
559
|
f"Bad code block size ({height} x {width}). "
|
|
549
560
|
f"The dimensions must be powers of 2."
|
|
@@ -585,15 +596,13 @@ class Jp2k(Jp2kr):
|
|
|
585
596
|
raise InvalidJp2kError(msg)
|
|
586
597
|
|
|
587
598
|
def _validate_image_rank(self, img_array):
|
|
588
|
-
"""Images must be either 2D or 3D.
|
|
589
|
-
"""
|
|
599
|
+
"""Images must be either 2D or 3D."""
|
|
590
600
|
if img_array.ndim == 1 or img_array.ndim > 3:
|
|
591
601
|
msg = f"{img_array.ndim}D imagery is not allowed."
|
|
592
602
|
raise InvalidJp2kError(msg)
|
|
593
603
|
|
|
594
604
|
def _validate_image_datatype(self, img_array):
|
|
595
|
-
"""Only uint8 and uint16 images are currently supported.
|
|
596
|
-
"""
|
|
605
|
+
"""Only uint8 and uint16 images are currently supported."""
|
|
597
606
|
if img_array.dtype != np.uint8 and img_array.dtype != np.uint16:
|
|
598
607
|
msg = (
|
|
599
608
|
"Only uint8 and uint16 datatypes are currently supported when "
|
|
@@ -631,20 +640,20 @@ class Jp2k(Jp2kr):
|
|
|
631
640
|
# Anything else must be RGB, right?
|
|
632
641
|
self._colorspace = opj2.CLRSPC_SRGB
|
|
633
642
|
else:
|
|
634
|
-
if self._colorspace.lower() not in (
|
|
643
|
+
if self._colorspace.lower() not in ("rgb", "grey", "gray"):
|
|
635
644
|
msg = f'Invalid colorspace "{self._colorspace}".'
|
|
636
645
|
raise InvalidJp2kError(msg)
|
|
637
|
-
elif self._colorspace.lower() ==
|
|
638
|
-
msg =
|
|
646
|
+
elif self._colorspace.lower() == "rgb" and self.shape[2] < 3:
|
|
647
|
+
msg = "RGB colorspace requires at least 3 components."
|
|
639
648
|
raise InvalidJp2kError(msg)
|
|
640
649
|
|
|
641
650
|
# Turn the colorspace from a string to the enumerated value that
|
|
642
651
|
# the library expects.
|
|
643
652
|
COLORSPACE_MAP = {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
653
|
+
"rgb": opj2.CLRSPC_SRGB,
|
|
654
|
+
"gray": opj2.CLRSPC_GRAY,
|
|
655
|
+
"grey": opj2.CLRSPC_GRAY,
|
|
656
|
+
"ycc": opj2.CLRSPC_YCC,
|
|
648
657
|
}
|
|
649
658
|
|
|
650
659
|
self._colorspace = COLORSPACE_MAP[self._colorspace.lower()]
|
|
@@ -679,14 +688,14 @@ class Jp2k(Jp2kr):
|
|
|
679
688
|
|
|
680
689
|
strm = opj2.stream_create_default_file_stream(self.filename, False)
|
|
681
690
|
|
|
682
|
-
num_threads = get_option(
|
|
683
|
-
if version.openjpeg_version >=
|
|
691
|
+
num_threads = get_option("lib.num_threads")
|
|
692
|
+
if version.openjpeg_version >= "2.4.0":
|
|
684
693
|
opj2.codec_set_threads(codec, num_threads)
|
|
685
694
|
elif num_threads > 1:
|
|
686
695
|
msg = (
|
|
687
|
-
f
|
|
688
|
-
f
|
|
689
|
-
f
|
|
696
|
+
f"Threaded encoding is not supported in library versions "
|
|
697
|
+
f"prior to 2.4.0. Your version is "
|
|
698
|
+
f"{version.openjpeg_version}."
|
|
690
699
|
)
|
|
691
700
|
warnings.warn(msg, UserWarning)
|
|
692
701
|
|
|
@@ -697,7 +706,8 @@ class Jp2k(Jp2kr):
|
|
|
697
706
|
opj2.end_compress(codec, strm)
|
|
698
707
|
|
|
699
708
|
def append(self, box):
|
|
700
|
-
"""
|
|
709
|
+
"""
|
|
710
|
+
Append a metadata box to the JP2 file. This will not result in a
|
|
701
711
|
file-copy operation. Only XML UUID (XMP), or ASOC boxes can be
|
|
702
712
|
appended at this time.
|
|
703
713
|
|
|
@@ -710,14 +720,11 @@ class Jp2k(Jp2kr):
|
|
|
710
720
|
msg = "You cannot append to a J2K file (raw codestream)."
|
|
711
721
|
raise RuntimeError(msg)
|
|
712
722
|
|
|
713
|
-
box_is_asoc = box.box_id ==
|
|
714
|
-
box_is_xml = box.box_id ==
|
|
715
|
-
box_is_xmp = (
|
|
716
|
-
box.
|
|
717
|
-
|
|
718
|
-
box.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
|
|
719
|
-
or box.uuid == UUID('b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03')
|
|
720
|
-
)
|
|
723
|
+
box_is_asoc = box.box_id == "asoc"
|
|
724
|
+
box_is_xml = box.box_id == "xml "
|
|
725
|
+
box_is_xmp = box.box_id == "uuid" and (
|
|
726
|
+
box.uuid == UUID("be7acfcb-97a9-42e8-9c71-999491e3afac")
|
|
727
|
+
or box.uuid == UUID("b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03")
|
|
721
728
|
)
|
|
722
729
|
if not (box_is_asoc or box_is_xml or box_is_xmp):
|
|
723
730
|
msg = (
|
|
@@ -728,27 +735,28 @@ class Jp2k(Jp2kr):
|
|
|
728
735
|
|
|
729
736
|
# Check the last box. If the length field is zero, then rewrite
|
|
730
737
|
# the length field to reflect the true length of the box.
|
|
731
|
-
with self.path.open(
|
|
738
|
+
with self.path.open("rb") as ifile:
|
|
732
739
|
offset = self.box[-1].offset
|
|
733
740
|
ifile.seek(offset)
|
|
734
741
|
read_buffer = ifile.read(4)
|
|
735
|
-
box_length, = struct.unpack(
|
|
742
|
+
(box_length,) = struct.unpack(">I", read_buffer)
|
|
736
743
|
if box_length == 0:
|
|
737
744
|
# Reopen the file in write mode and rewrite the length field.
|
|
738
745
|
true_box_length = self.path.stat().st_size - offset
|
|
739
|
-
with self.path.open(
|
|
746
|
+
with self.path.open("r+b") as ofile:
|
|
740
747
|
ofile.seek(offset)
|
|
741
|
-
write_buffer = struct.pack(
|
|
748
|
+
write_buffer = struct.pack(">I", true_box_length)
|
|
742
749
|
ofile.write(write_buffer)
|
|
743
750
|
|
|
744
751
|
# Can now safely append the box.
|
|
745
|
-
with self.path.open(
|
|
752
|
+
with self.path.open("ab") as ofile:
|
|
746
753
|
box.write(ofile)
|
|
747
754
|
|
|
748
755
|
self.parse(force=True)
|
|
749
756
|
|
|
750
757
|
def wrap(self, filename, boxes=None):
|
|
751
|
-
"""
|
|
758
|
+
"""
|
|
759
|
+
Create a new JP2/JPX file wrapped in a new set of JP2 boxes.
|
|
752
760
|
|
|
753
761
|
This method is primarily aimed at wrapping a raw codestream in a set of
|
|
754
762
|
of JP2 boxes (turning it into a JP2 file instead of just a raw
|
|
@@ -776,9 +784,9 @@ class Jp2k(Jp2kr):
|
|
|
776
784
|
|
|
777
785
|
self._validate_jp2_box_sequence(boxes)
|
|
778
786
|
|
|
779
|
-
with open(filename,
|
|
787
|
+
with open(filename, "wb") as ofile:
|
|
780
788
|
for box in boxes:
|
|
781
|
-
if box.box_id !=
|
|
789
|
+
if box.box_id != "jp2c":
|
|
782
790
|
box.write(ofile)
|
|
783
791
|
else:
|
|
784
792
|
self._write_wrapped_codestream(ofile, box)
|
|
@@ -794,9 +802,9 @@ class Jp2k(Jp2kr):
|
|
|
794
802
|
if len(self.box) == 0:
|
|
795
803
|
# Yes, just write the codestream box header plus all
|
|
796
804
|
# of myself out to file.
|
|
797
|
-
ofile.write(struct.pack(
|
|
798
|
-
ofile.write(b
|
|
799
|
-
with open(self.filename,
|
|
805
|
+
ofile.write(struct.pack(">I", self.length + 8))
|
|
806
|
+
ofile.write(b"jp2c")
|
|
807
|
+
with open(self.filename, "rb") as ifile:
|
|
800
808
|
ofile.write(ifile.read())
|
|
801
809
|
return
|
|
802
810
|
|
|
@@ -804,7 +812,7 @@ class Jp2k(Jp2kr):
|
|
|
804
812
|
# actually starts.
|
|
805
813
|
offset = box.offset
|
|
806
814
|
if offset == -1:
|
|
807
|
-
if self.box[1].brand ==
|
|
815
|
+
if self.box[1].brand == "jpx ":
|
|
808
816
|
msg = (
|
|
809
817
|
"The codestream box must have its offset and length "
|
|
810
818
|
"attributes fully specified if the file type brand is JPX."
|
|
@@ -812,17 +820,17 @@ class Jp2k(Jp2kr):
|
|
|
812
820
|
raise InvalidJp2kError(msg)
|
|
813
821
|
|
|
814
822
|
# Find the first codestream in the file.
|
|
815
|
-
jp2c = [_box for _box in self.box if _box.box_id ==
|
|
823
|
+
jp2c = [_box for _box in self.box if _box.box_id == "jp2c"]
|
|
816
824
|
offset = jp2c[0].offset
|
|
817
825
|
|
|
818
826
|
# Ready to write the codestream.
|
|
819
|
-
with open(self.filename,
|
|
827
|
+
with open(self.filename, "rb") as ifile:
|
|
820
828
|
ifile.seek(offset)
|
|
821
829
|
|
|
822
830
|
# Verify that the specified codestream is right.
|
|
823
831
|
read_buffer = ifile.read(8)
|
|
824
|
-
L, T = struct.unpack_from(
|
|
825
|
-
if T != b
|
|
832
|
+
L, T = struct.unpack_from(">I4s", read_buffer, 0)
|
|
833
|
+
if T != b"jp2c":
|
|
826
834
|
msg = "Unable to locate the specified codestream."
|
|
827
835
|
raise InvalidJp2kError(msg)
|
|
828
836
|
if L == 0:
|
|
@@ -833,7 +841,7 @@ class Jp2k(Jp2kr):
|
|
|
833
841
|
elif L == 1:
|
|
834
842
|
# The length of the box is in the XL field, a 64-bit value.
|
|
835
843
|
read_buffer = ifile.read(8)
|
|
836
|
-
L, = struct.unpack(
|
|
844
|
+
(L,) = struct.unpack(">Q", read_buffer)
|
|
837
845
|
|
|
838
846
|
ifile.seek(offset)
|
|
839
847
|
read_buffer = ifile.read(L)
|
|
@@ -846,7 +854,7 @@ class Jp2k(Jp2kr):
|
|
|
846
854
|
JPEG2000SignatureBox(),
|
|
847
855
|
FileTypeBox(),
|
|
848
856
|
JP2HeaderBox(),
|
|
849
|
-
ContiguousCodestreamBox()
|
|
857
|
+
ContiguousCodestreamBox(),
|
|
850
858
|
]
|
|
851
859
|
height = self.codestream.segment[1].ysiz
|
|
852
860
|
width = self.codestream.segment[1].xsiz
|
|
@@ -860,14 +868,14 @@ class Jp2k(Jp2kr):
|
|
|
860
868
|
else:
|
|
861
869
|
# Take whatever the first jp2 header / color specification
|
|
862
870
|
# says.
|
|
863
|
-
jp2hs = [box for box in self.box if box.box_id ==
|
|
871
|
+
jp2hs = [box for box in self.box if box.box_id == "jp2h"]
|
|
864
872
|
colorspace = jp2hs[0].box[1].colorspace
|
|
865
873
|
|
|
866
874
|
boxes[2].box = [
|
|
867
875
|
ImageHeaderBox(
|
|
868
876
|
height=height, width=width, num_components=num_components
|
|
869
877
|
),
|
|
870
|
-
ColourSpecificationBox(colorspace=colorspace)
|
|
878
|
+
ColourSpecificationBox(colorspace=colorspace),
|
|
871
879
|
]
|
|
872
880
|
|
|
873
881
|
return boxes
|
|
@@ -1011,8 +1019,10 @@ class Jp2k(Jp2kr):
|
|
|
1011
1019
|
|
|
1012
1020
|
# Stage the image data to the openjpeg data structure.
|
|
1013
1021
|
for k in range(0, num_comps):
|
|
1014
|
-
if self._cparams.rsiz in (
|
|
1015
|
-
|
|
1022
|
+
if self._cparams.rsiz in (
|
|
1023
|
+
core.OPJ_PROFILE_CINEMA_2K,
|
|
1024
|
+
core.OPJ_PROFILE_CINEMA_4K,
|
|
1025
|
+
):
|
|
1016
1026
|
image.contents.comps[k].prec = 12
|
|
1017
1027
|
image.contents.comps[k].bpp = 12
|
|
1018
1028
|
|
|
@@ -1064,15 +1074,29 @@ class Jp2k(Jp2kr):
|
|
|
1064
1074
|
This is non-exhaustive.
|
|
1065
1075
|
"""
|
|
1066
1076
|
JP2_IDS = [
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1077
|
+
"colr",
|
|
1078
|
+
"cdef",
|
|
1079
|
+
"cmap",
|
|
1080
|
+
"jp2c",
|
|
1081
|
+
"ftyp",
|
|
1082
|
+
"ihdr",
|
|
1083
|
+
"jp2h",
|
|
1084
|
+
"jP ",
|
|
1085
|
+
"pclr",
|
|
1086
|
+
"res ",
|
|
1087
|
+
"resc",
|
|
1088
|
+
"resd",
|
|
1089
|
+
"xml ",
|
|
1090
|
+
"ulst",
|
|
1091
|
+
"uinf",
|
|
1092
|
+
"url ",
|
|
1093
|
+
"uuid",
|
|
1070
1094
|
]
|
|
1071
1095
|
|
|
1072
1096
|
self._validate_signature_compatibility(boxes)
|
|
1073
1097
|
self._validate_jp2h(boxes)
|
|
1074
1098
|
self._validate_jp2c(boxes)
|
|
1075
|
-
if boxes[1].brand ==
|
|
1099
|
+
if boxes[1].brand == "jpx ":
|
|
1076
1100
|
self._validate_jpx_box_sequence(boxes)
|
|
1077
1101
|
else:
|
|
1078
1102
|
# Validate the JP2 box IDs.
|
|
@@ -1089,8 +1113,8 @@ class Jp2k(Jp2kr):
|
|
|
1089
1113
|
|
|
1090
1114
|
def _validate_jp2_colr(self, boxes):
|
|
1091
1115
|
"""Validate JP2 requirements on colour specification boxes."""
|
|
1092
|
-
jp2h = next(filter(lambda x: x.box_id ==
|
|
1093
|
-
for colr in [box for box in jp2h.box if box.box_id ==
|
|
1116
|
+
jp2h = next(filter(lambda x: x.box_id == "jp2h", boxes), None)
|
|
1117
|
+
for colr in [box for box in jp2h.box if box.box_id == "colr"]:
|
|
1094
1118
|
if colr.approximation != 0:
|
|
1095
1119
|
msg = (
|
|
1096
1120
|
"A JP2 colr box cannot have a non-zero approximation "
|
|
@@ -1109,7 +1133,7 @@ class Jp2k(Jp2kr):
|
|
|
1109
1133
|
"""Validate the file signature and compatibility status."""
|
|
1110
1134
|
# Check for a bad sequence of boxes.
|
|
1111
1135
|
# 1st two boxes must be 'jP ' and 'ftyp'
|
|
1112
|
-
if boxes[0].box_id !=
|
|
1136
|
+
if boxes[0].box_id != "jP " or boxes[1].box_id != "ftyp":
|
|
1113
1137
|
msg = (
|
|
1114
1138
|
"The first box must be the signature box and the second must "
|
|
1115
1139
|
"be the file type box."
|
|
@@ -1117,7 +1141,7 @@ class Jp2k(Jp2kr):
|
|
|
1117
1141
|
raise InvalidJp2kError(msg)
|
|
1118
1142
|
|
|
1119
1143
|
# The compatibility list must contain at a minimum 'jp2 '.
|
|
1120
|
-
if
|
|
1144
|
+
if "jp2 " not in boxes[1].compatibility_list:
|
|
1121
1145
|
msg = "The ftyp box must contain 'jp2 ' in the compatibility list."
|
|
1122
1146
|
raise InvalidJp2kError(msg)
|
|
1123
1147
|
|
|
@@ -1125,11 +1149,11 @@ class Jp2k(Jp2kr):
|
|
|
1125
1149
|
"""Validate the codestream box in relation to other boxes."""
|
|
1126
1150
|
# jp2c must be preceeded by jp2h
|
|
1127
1151
|
jp2h_idx, _ = next(
|
|
1128
|
-
filter(lambda x: x[1].box_id ==
|
|
1152
|
+
filter(lambda x: x[1].box_id == "jp2h", enumerate(boxes)),
|
|
1129
1153
|
(None, None)
|
|
1130
1154
|
)
|
|
1131
1155
|
jp2c_idx, _ = next(
|
|
1132
|
-
filter(lambda x: x[1].box_id ==
|
|
1156
|
+
filter(lambda x: x[1].box_id == "jp2c", enumerate(boxes)),
|
|
1133
1157
|
(None, None)
|
|
1134
1158
|
)
|
|
1135
1159
|
if jp2c_idx is None:
|
|
@@ -1145,9 +1169,9 @@ class Jp2k(Jp2kr):
|
|
|
1145
1169
|
|
|
1146
1170
|
def _validate_jp2h(self, boxes):
|
|
1147
1171
|
"""Validate the JP2 Header box."""
|
|
1148
|
-
self._check_jp2h_child_boxes(boxes,
|
|
1172
|
+
self._check_jp2h_child_boxes(boxes, "top-level")
|
|
1149
1173
|
|
|
1150
|
-
jp2h = next(filter(lambda x: x.box_id ==
|
|
1174
|
+
jp2h = next(filter(lambda x: x.box_id == "jp2h", boxes), None)
|
|
1151
1175
|
|
|
1152
1176
|
# 1st jp2 header box cannot be empty.
|
|
1153
1177
|
if len(jp2h.box) == 0:
|
|
@@ -1155,7 +1179,7 @@ class Jp2k(Jp2kr):
|
|
|
1155
1179
|
raise InvalidJp2kError(msg)
|
|
1156
1180
|
|
|
1157
1181
|
# 1st jp2 header box must be ihdr
|
|
1158
|
-
if jp2h.box[0].box_id !=
|
|
1182
|
+
if jp2h.box[0].box_id != "ihdr":
|
|
1159
1183
|
msg = (
|
|
1160
1184
|
"The first box in the jp2 header box must be the image header "
|
|
1161
1185
|
"box."
|
|
@@ -1163,7 +1187,7 @@ class Jp2k(Jp2kr):
|
|
|
1163
1187
|
raise InvalidJp2kError(msg)
|
|
1164
1188
|
|
|
1165
1189
|
# colr must be present in jp2 header box.
|
|
1166
|
-
colr = next(filter(lambda x: x.box_id ==
|
|
1190
|
+
colr = next(filter(lambda x: x.box_id == "colr", jp2h.box), None)
|
|
1167
1191
|
if colr is None:
|
|
1168
1192
|
msg = "The jp2 header box must contain a color definition box."
|
|
1169
1193
|
raise InvalidJp2kError(msg)
|
|
@@ -1173,36 +1197,44 @@ class Jp2k(Jp2kr):
|
|
|
1173
1197
|
def _validate_channel_definition(self, jp2h, colr):
|
|
1174
1198
|
"""Validate the channel definition box."""
|
|
1175
1199
|
cdef_lst = [
|
|
1176
|
-
idx for (idx, box) in enumerate(jp2h.box) if box.box_id ==
|
|
1200
|
+
idx for (idx, box) in enumerate(jp2h.box) if box.box_id == "cdef"
|
|
1177
1201
|
]
|
|
1178
1202
|
if len(cdef_lst) > 1:
|
|
1179
|
-
msg = (
|
|
1180
|
-
|
|
1203
|
+
msg = (
|
|
1204
|
+
"Only one channel definition box is allowed in the "
|
|
1205
|
+
"JP2 header."
|
|
1206
|
+
)
|
|
1181
1207
|
raise InvalidJp2kError(msg)
|
|
1182
1208
|
elif len(cdef_lst) == 1:
|
|
1183
1209
|
cdef = jp2h.box[cdef_lst[0]]
|
|
1184
1210
|
if colr.colorspace == core.SRGB:
|
|
1185
|
-
if any(
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1211
|
+
if any(
|
|
1212
|
+
[
|
|
1213
|
+
chan + 1 not in cdef.association
|
|
1214
|
+
or cdef.channel_type[chan] != 0
|
|
1215
|
+
for chan in [0, 1, 2]
|
|
1216
|
+
]
|
|
1217
|
+
):
|
|
1218
|
+
msg = (
|
|
1219
|
+
"All color channels must be defined in the "
|
|
1220
|
+
"channel definition box."
|
|
1221
|
+
)
|
|
1192
1222
|
raise InvalidJp2kError(msg)
|
|
1193
1223
|
elif colr.colorspace == core.GREYSCALE:
|
|
1194
1224
|
if 0 not in cdef.channel_type:
|
|
1195
|
-
msg = (
|
|
1196
|
-
|
|
1225
|
+
msg = (
|
|
1226
|
+
"All color channels must be defined in the "
|
|
1227
|
+
"channel definition box."
|
|
1228
|
+
)
|
|
1197
1229
|
raise InvalidJp2kError(msg)
|
|
1198
1230
|
|
|
1199
1231
|
def _check_jp2h_child_boxes(self, boxes, parent_box_name):
|
|
1200
1232
|
"""Certain boxes can only reside in the JP2 header."""
|
|
1201
|
-
JP2H_CHILDREN = set([
|
|
1233
|
+
JP2H_CHILDREN = set(["bpcc", "cdef", "cmap", "ihdr", "pclr"])
|
|
1202
1234
|
|
|
1203
1235
|
box_ids = set([box.box_id for box in boxes])
|
|
1204
1236
|
intersection = box_ids.intersection(JP2H_CHILDREN)
|
|
1205
|
-
if len(intersection) > 0 and parent_box_name not in [
|
|
1237
|
+
if len(intersection) > 0 and parent_box_name not in ["jp2h", "jpch"]:
|
|
1206
1238
|
msg = (
|
|
1207
1239
|
f"A {list(intersection)[0]} box can only be nested in a JP2 "
|
|
1208
1240
|
f"header box."
|
|
@@ -1211,7 +1243,7 @@ class Jp2k(Jp2kr):
|
|
|
1211
1243
|
|
|
1212
1244
|
# Recursively check any contained superboxes.
|
|
1213
1245
|
for box in boxes:
|
|
1214
|
-
if hasattr(box,
|
|
1246
|
+
if hasattr(box, "box"):
|
|
1215
1247
|
self._check_jp2h_child_boxes(box.box, box.box_id)
|
|
1216
1248
|
|
|
1217
1249
|
def _collect_box_count(self, boxes):
|
|
@@ -1220,7 +1252,7 @@ class Jp2k(Jp2kr):
|
|
|
1220
1252
|
|
|
1221
1253
|
# Add the counts in the superboxes.
|
|
1222
1254
|
for box in boxes:
|
|
1223
|
-
if hasattr(box,
|
|
1255
|
+
if hasattr(box, "box"):
|
|
1224
1256
|
count.update(self._collect_box_count(box.box))
|
|
1225
1257
|
|
|
1226
1258
|
return count
|
|
@@ -1229,7 +1261,7 @@ class Jp2k(Jp2kr):
|
|
|
1229
1261
|
"""Several boxes can only occur at the top level."""
|
|
1230
1262
|
# We are only looking at the boxes contained in a superbox, so if any
|
|
1231
1263
|
# of the blacklisted boxes show up here, it's an error.
|
|
1232
|
-
TOP_LEVEL_ONLY_BOXES = set([
|
|
1264
|
+
TOP_LEVEL_ONLY_BOXES = set(["dtbl"])
|
|
1233
1265
|
box_ids = set([box.box_id for box in boxes])
|
|
1234
1266
|
intersection = box_ids.intersection(TOP_LEVEL_ONLY_BOXES)
|
|
1235
1267
|
if len(intersection) > 0:
|
|
@@ -1241,22 +1273,24 @@ class Jp2k(Jp2kr):
|
|
|
1241
1273
|
|
|
1242
1274
|
# Recursively check any contained superboxes.
|
|
1243
1275
|
for box in boxes:
|
|
1244
|
-
if hasattr(box,
|
|
1276
|
+
if hasattr(box, "box"):
|
|
1245
1277
|
self._check_superbox_for_top_levels(box.box)
|
|
1246
1278
|
|
|
1247
1279
|
def _validate_top_level(self, boxes):
|
|
1248
1280
|
"""Several boxes can only occur at the top level."""
|
|
1249
1281
|
# Add the counts in the superboxes.
|
|
1250
1282
|
for box in boxes:
|
|
1251
|
-
if hasattr(box,
|
|
1283
|
+
if hasattr(box, "box"):
|
|
1252
1284
|
self._check_superbox_for_top_levels(box.box)
|
|
1253
1285
|
|
|
1254
1286
|
count = self._collect_box_count(boxes)
|
|
1255
1287
|
|
|
1256
1288
|
# If there is one data reference box, then there must also be one ftbl.
|
|
1257
|
-
if
|
|
1258
|
-
msg = (
|
|
1259
|
-
|
|
1289
|
+
if "dtbl" in count and "ftbl" not in count:
|
|
1290
|
+
msg = (
|
|
1291
|
+
"The presence of a data reference box requires the "
|
|
1292
|
+
"presence of a fragment table box as well."
|
|
1293
|
+
)
|
|
1260
1294
|
raise InvalidJp2kError(msg)
|
|
1261
1295
|
|
|
1262
1296
|
def _validate_singletons(self, boxes):
|
|
@@ -1264,24 +1298,24 @@ class Jp2k(Jp2kr):
|
|
|
1264
1298
|
count = self._collect_box_count(boxes)
|
|
1265
1299
|
# Which boxes occur more than once?
|
|
1266
1300
|
multiples = [box_id for box_id, bcount in count.items() if bcount > 1]
|
|
1267
|
-
if
|
|
1268
|
-
raise InvalidJp2kError(
|
|
1301
|
+
if "dtbl" in multiples:
|
|
1302
|
+
raise InvalidJp2kError("There can only be one dtbl box in a file.")
|
|
1269
1303
|
|
|
1270
1304
|
def _validate_jpx_compatibility(self, boxes, compatibility_list):
|
|
1271
1305
|
"""If there is a JPX box then the compatibility list must also contain
|
|
1272
1306
|
'jpx '.
|
|
1273
1307
|
"""
|
|
1274
|
-
JPX_IDS = [
|
|
1308
|
+
JPX_IDS = ["asoc", "nlst"]
|
|
1275
1309
|
jpx_cl = set(compatibility_list)
|
|
1276
1310
|
for box in boxes:
|
|
1277
1311
|
if box.box_id in JPX_IDS:
|
|
1278
|
-
if len(set([
|
|
1312
|
+
if len(set(["jpx ", "jpxb"]).intersection(jpx_cl)) == 0:
|
|
1279
1313
|
msg = (
|
|
1280
1314
|
"A JPX box requires that either 'jpx ' or 'jpxb' be "
|
|
1281
1315
|
"present in the ftype compatibility list."
|
|
1282
1316
|
)
|
|
1283
1317
|
raise InvalidJp2kError(msg)
|
|
1284
|
-
if hasattr(box,
|
|
1318
|
+
if hasattr(box, "box") != 0:
|
|
1285
1319
|
# Same set of checks on any child boxes.
|
|
1286
1320
|
self._validate_jpx_compatibility(box.box, compatibility_list)
|
|
1287
1321
|
|
|
@@ -1290,16 +1324,15 @@ class Jp2k(Jp2kr):
|
|
|
1290
1324
|
compositing layer header boxes.
|
|
1291
1325
|
"""
|
|
1292
1326
|
for box in boxes:
|
|
1293
|
-
if box.box_id !=
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
# Same set of checks on any child boxes.
|
|
1327
|
+
if box.box_id != "asoc" and hasattr(box, "box"):
|
|
1328
|
+
for boxi in box.box:
|
|
1329
|
+
if boxi.box_id == "lbl ":
|
|
1330
|
+
msg = (
|
|
1331
|
+
f"A label box cannot be nested inside a "
|
|
1332
|
+
f"{box.box_id} box."
|
|
1333
|
+
)
|
|
1334
|
+
raise InvalidJp2kError(msg)
|
|
1335
|
+
# Same set of checks on any child boxes.
|
|
1303
1336
|
self._validate_label(box.box)
|
|
1304
1337
|
|
|
1305
1338
|
|
|
@@ -1348,9 +1381,11 @@ class _TileWriter(object):
|
|
|
1348
1381
|
"""Write image data to a JP2/JPX/J2k file. Intended usage of the
|
|
1349
1382
|
various parameters follows that of OpenJPEG's opj_compress utility.
|
|
1350
1383
|
"""
|
|
1351
|
-
if version.openjpeg_version <
|
|
1352
|
-
msg = (
|
|
1353
|
-
|
|
1384
|
+
if version.openjpeg_version < "2.3.0":
|
|
1385
|
+
msg = (
|
|
1386
|
+
"You must have at least version 2.3.0 of OpenJPEG "
|
|
1387
|
+
"in order to write images."
|
|
1388
|
+
)
|
|
1354
1389
|
raise RuntimeError(msg)
|
|
1355
1390
|
|
|
1356
1391
|
if not isinstance(index, slice):
|
|
@@ -1368,7 +1403,7 @@ class _TileWriter(object):
|
|
|
1368
1403
|
self.codec,
|
|
1369
1404
|
self.tile_index,
|
|
1370
1405
|
_set_planar_pixel_order(img_array),
|
|
1371
|
-
self.stream
|
|
1406
|
+
self.stream,
|
|
1372
1407
|
)
|
|
1373
1408
|
except glymur.lib.openjp2.OpenJPEGLibraryError as e:
|
|
1374
1409
|
# properly dispose of these resources
|
|
@@ -1386,8 +1421,7 @@ class _TileWriter(object):
|
|
|
1386
1421
|
opj2.destroy_codec(self.codec)
|
|
1387
1422
|
|
|
1388
1423
|
def setup_first_tile(self, img_array):
|
|
1389
|
-
"""Only do these things for the first tile.
|
|
1390
|
-
"""
|
|
1424
|
+
"""Only do these things for the first tile."""
|
|
1391
1425
|
self.jp2k._determine_colorspace()
|
|
1392
1426
|
self.jp2k._populate_cparams(img_array)
|
|
1393
1427
|
self.jp2k._populate_comptparms(img_array)
|
|
@@ -1408,9 +1442,10 @@ class _TileWriter(object):
|
|
|
1408
1442
|
)
|
|
1409
1443
|
|
|
1410
1444
|
self.jp2k._populate_image_struct(
|
|
1411
|
-
self.image,
|
|
1445
|
+
self.image,
|
|
1446
|
+
img_array,
|
|
1412
1447
|
tile_x_factor=self.num_tile_cols,
|
|
1413
|
-
tile_y_factor=self.num_tile_rows
|
|
1448
|
+
tile_y_factor=self.num_tile_rows,
|
|
1414
1449
|
)
|
|
1415
1450
|
self.image.contents.x1 = self.jp2k.shape[1]
|
|
1416
1451
|
self.image.contents.y1 = self.jp2k.shape[0]
|
|
@@ -1421,17 +1456,18 @@ class _TileWriter(object):
|
|
|
1421
1456
|
opj2.encoder_set_extra_options(self.codec, plt=self.jp2k._plt)
|
|
1422
1457
|
|
|
1423
1458
|
self.stream = opj2.stream_create_default_file_stream(
|
|
1424
|
-
self.jp2k.filename,
|
|
1459
|
+
self.jp2k.filename,
|
|
1460
|
+
False
|
|
1425
1461
|
)
|
|
1426
1462
|
|
|
1427
|
-
num_threads = get_option(
|
|
1428
|
-
if version.openjpeg_version >=
|
|
1463
|
+
num_threads = get_option("lib.num_threads")
|
|
1464
|
+
if version.openjpeg_version >= "2.4.0":
|
|
1429
1465
|
opj2.codec_set_threads(self.codec, num_threads)
|
|
1430
1466
|
elif num_threads > 1:
|
|
1431
1467
|
msg = (
|
|
1432
|
-
f
|
|
1433
|
-
f
|
|
1434
|
-
f
|
|
1468
|
+
f"Threaded encoding is not supported in library versions "
|
|
1469
|
+
f"prior to 2.4.0. Your version is "
|
|
1470
|
+
f"{version.openjpeg_version}."
|
|
1435
1471
|
)
|
|
1436
1472
|
warnings.warn(msg, UserWarning)
|
|
1437
1473
|
|