Glymur 0.13.2.post1__tar.gz → 0.13.4__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.
Files changed (54) hide show
  1. {glymur-0.13.2.post1 → glymur-0.13.4}/CHANGES.txt +10 -2
  2. {glymur-0.13.2.post1 → glymur-0.13.4}/Glymur.egg-info/PKG-INFO +1 -1
  3. {glymur-0.13.2.post1 → glymur-0.13.4}/PKG-INFO +1 -1
  4. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/codestream.py +12 -6
  5. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/jp2box.py +8 -5
  6. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/jp2k.py +24 -26
  7. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/jp2kr.py +67 -38
  8. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/version.py +1 -1
  9. {glymur-0.13.2.post1 → glymur-0.13.4}/setup.cfg +1 -1
  10. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_codestream.py +111 -5
  11. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_jp2box.py +48 -27
  12. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_jp2k.py +14 -39
  13. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_jp2k_writes.py +2 -3
  14. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_jp2kr.py +0 -25
  15. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_printing.py +21 -0
  16. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_set_decoded_components.py +0 -1
  17. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_warnings.py +0 -57
  18. {glymur-0.13.2.post1 → glymur-0.13.4}/Glymur.egg-info/SOURCES.txt +0 -0
  19. {glymur-0.13.2.post1 → glymur-0.13.4}/Glymur.egg-info/dependency_links.txt +0 -0
  20. {glymur-0.13.2.post1 → glymur-0.13.4}/Glymur.egg-info/entry_points.txt +0 -0
  21. {glymur-0.13.2.post1 → glymur-0.13.4}/Glymur.egg-info/not-zip-safe +0 -0
  22. {glymur-0.13.2.post1 → glymur-0.13.4}/Glymur.egg-info/requires.txt +0 -0
  23. {glymur-0.13.2.post1 → glymur-0.13.4}/Glymur.egg-info/top_level.txt +0 -0
  24. {glymur-0.13.2.post1 → glymur-0.13.4}/LICENSE.txt +0 -0
  25. {glymur-0.13.2.post1 → glymur-0.13.4}/MANIFEST.in +0 -0
  26. {glymur-0.13.2.post1 → glymur-0.13.4}/README.md +0 -0
  27. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/__init__.py +0 -0
  28. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/_iccprofile.py +0 -0
  29. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/command_line.py +0 -0
  30. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/config.py +0 -0
  31. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/core.py +0 -0
  32. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/data/__init__.py +0 -0
  33. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/data/goodstuff.j2k +0 -0
  34. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/data/heliov.jpx +0 -0
  35. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/data/nemo.jp2 +0 -0
  36. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/lib/__init__.py +0 -0
  37. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/lib/openjp2.py +0 -0
  38. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/lib/tiff.py +0 -0
  39. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/options.py +0 -0
  40. {glymur-0.13.2.post1 → glymur-0.13.4}/glymur/tiff.py +0 -0
  41. {glymur-0.13.2.post1 → glymur-0.13.4}/pyproject.toml +0 -0
  42. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_callbacks.py +0 -0
  43. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_cinema.py +0 -0
  44. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_colour_specification_box.py +0 -0
  45. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_config.py +0 -0
  46. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_geo.py +0 -0
  47. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_jp2box_jpx.py +0 -0
  48. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_jp2box_uuid.py +0 -0
  49. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_jp2box_xml.py +0 -0
  50. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_libtiff.py +0 -0
  51. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_openjp2.py +0 -0
  52. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_slicing.py +0 -0
  53. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_threading.py +0 -0
  54. {glymur-0.13.2.post1 → glymur-0.13.4}/tests/test_tiff2jp2.py +0 -0
@@ -1,5 +1,13 @@
1
- May 07, 2024 - v0.13.2.post1
2
- Fix big endian test issue
1
+ July 4, 2024 - v0.13.4
2
+ Don't reset openjpeg codec in Jp2k if already set in Jp2kr.
3
+ Update CI configuration to specify openjpeg versions.
4
+
5
+ June 30, 2024 - v0.13.3
6
+ Refactor parsing errors and warnings.
7
+ Update CI configuration for numpy 2.0.
8
+ Skip psnr doctest for numpy 2.0.
9
+ Fix test issue on s390x.
10
+ Refactor code pattern for finding first element.
3
11
 
4
12
  May 07, 2024 - v0.13.2
5
13
  Improve doctesting, fix broken libtiff doctest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Glymur
3
- Version: 0.13.2.post1
3
+ Version: 0.13.4
4
4
  Home-page: https://github.com/quintusdias/glymur
5
5
  Author: 'John Evans'
6
6
  Author-email: "John Evans" <jevans667cc@proton.me>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Glymur
3
- Version: 0.13.2.post1
3
+ Version: 0.13.4
4
4
  Home-page: https://github.com/quintusdias/glymur
5
5
  Author: 'John Evans'
6
6
  Author-email: "John Evans" <jevans667cc@proton.me>
@@ -736,7 +736,9 @@ class Codestream(object):
736
736
  data = struct.unpack_from('>HIIIIIIIIH', read_buffer)
737
737
 
738
738
  rsiz = data[0]
739
- if rsiz not in _KNOWN_PROFILES:
739
+
740
+ # Bit 14 (16384) signifies HTJ2K (JPH)
741
+ if rsiz not in _KNOWN_PROFILES and not np.bitwise_and(rsiz, 16384):
740
742
  msg = f"Invalid profile: (Rsiz={rsiz})."
741
743
  warnings.warn(msg, UserWarning)
742
744
 
@@ -767,11 +769,10 @@ class Codestream(object):
767
769
  num_tiles_y = (xysiz[1] - xyosiz[1]) / (xytsiz[1] - xytosiz[1])
768
770
  except ZeroDivisionError:
769
771
  msg = (
770
- f"Invalid tile specification: "
771
- f"size of {xytsiz[1]} x {xytsiz[0]}, "
772
- f"offset of {xytosiz[1]} x {xytsiz[0]}."
772
+ f"Invalid tile specification in SIZ segment at byte offset "
773
+ f"{offset}: tile size of {xytsiz[1]} x {xytsiz[0]}."
773
774
  )
774
- warnings.warn(msg, UserWarning)
775
+ raise ZeroDivisionError(msg)
775
776
  else:
776
777
  numtiles = np.ceil(num_tiles_x) * np.ceil(num_tiles_y)
777
778
  if numtiles > 65535:
@@ -1667,7 +1668,12 @@ class SIZsegment(Segment):
1667
1668
  try:
1668
1669
  profile = _CAPABILITIES_DISPLAY[self.rsiz]
1669
1670
  except KeyError:
1670
- profile = f'{self.rsiz} (invalid)'
1671
+ if np.bitwise_and(self.rsiz, 16384):
1672
+ # HTJ2K profile that is uninterpreted
1673
+ profile = f'{self.rsiz}'
1674
+ else:
1675
+ # profile unknown
1676
+ profile = f'{self.rsiz} (invalid)'
1671
1677
  msg = msg.format(
1672
1678
  profile=profile,
1673
1679
  height=self.ysiz, width=self.xsiz,
@@ -71,7 +71,7 @@ _EXIF_UUID = UUID(bytes=b'JpgTiffExif->JP2')
71
71
  _XMP_UUID = UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
72
72
 
73
73
 
74
- class InvalidJp2kWarning(RuntimeError):
74
+ class InvalidJp2kWarning(UserWarning):
75
75
  """Issue this warning in case the file is technically invalid but we can
76
76
  still read the image.
77
77
  """
@@ -1259,7 +1259,7 @@ class FileTypeBox(Jp2kBox):
1259
1259
  """
1260
1260
  box_id = 'ftyp'
1261
1261
  longname = 'File Type'
1262
- _valid_cls = ['jp2 ', 'jpx ', 'jpxb']
1262
+ _valid_cls = ['jp2 ', 'jph ', 'jpx ', 'jpxb']
1263
1263
 
1264
1264
  def __init__(
1265
1265
  self, brand='jp2 ', minor_version=0, compatibility_list=None,
@@ -1304,16 +1304,19 @@ class FileTypeBox(Jp2kBox):
1304
1304
  return text
1305
1305
 
1306
1306
  def _validate(self, writing=False):
1307
- """Validate the box before writing to file."""
1308
- if self.brand not in ['jp2 ', 'jpx ']:
1307
+ """
1308
+ Validate the box before writing to file.
1309
+ """
1310
+ if self.brand not in ['jp2 ', 'jpx ', 'jph ']:
1309
1311
  msg = (
1310
1312
  f"The file type brand was '{self.brand}'. "
1311
- f"It should be either 'jp2 ' or 'jpx '."
1313
+ f"It should be either 'jp2 ', 'jpx ', or 'jph '."
1312
1314
  )
1313
1315
  if writing:
1314
1316
  raise InvalidJp2kError(msg)
1315
1317
  else:
1316
1318
  warnings.warn(msg, UserWarning)
1319
+
1317
1320
  for item in self.compatibility_list:
1318
1321
  if item not in self._valid_cls:
1319
1322
  msg = (
@@ -206,13 +206,13 @@ class Jp2k(Jp2kr):
206
206
  # Must be determined when writing.
207
207
  self._shape = None
208
208
 
209
- # If there already was a shape attribute, then don't mess with it,
210
- # it was set by the reader superclass.
211
-
212
- if self.filename[-4:].endswith(('.jp2', '.JP2', '.jpx', 'JPX')):
213
- self._codec_format = opj2.CODEC_JP2
214
- else:
215
- self._codec_format = opj2.CODEC_J2K
209
+ if not hasattr(self, '_codec_format'):
210
+ # Only set codec format if the superclass has not done so, i.e.
211
+ # we are writing instead of reading.
212
+ if self.filename[-4:].endswith(('.jp2', '.JP2', '.jpx', 'JPX')):
213
+ self._codec_format = opj2.CODEC_JP2
214
+ else:
215
+ self._codec_format = opj2.CODEC_J2K
216
216
 
217
217
  self._validate_kwargs()
218
218
 
@@ -258,7 +258,7 @@ class Jp2k(Jp2kr):
258
258
  header box if we were so instructed. This requires a wrapping
259
259
  operation.
260
260
  """
261
- jp2h = [box for box in self.box if box.box_id == 'jp2h'][0]
261
+ jp2h = next(filter(lambda x: x.box_id == 'jp2h', self.box), None)
262
262
 
263
263
  extra_boxes = []
264
264
  if self._capture_resolution is not None:
@@ -1125,8 +1125,7 @@ class Jp2k(Jp2kr):
1125
1125
 
1126
1126
  def _validate_jp2_colr(self, boxes):
1127
1127
  """Validate JP2 requirements on colour specification boxes."""
1128
- lst = [box for box in boxes if box.box_id == 'jp2h']
1129
- jp2h = lst[0]
1128
+ jp2h = next(filter(lambda x: x.box_id == 'jp2h', boxes), None)
1130
1129
  for colr in [box for box in jp2h.box if box.box_id == 'colr']:
1131
1130
  if colr.approximation != 0:
1132
1131
  msg = (
@@ -1161,19 +1160,21 @@ class Jp2k(Jp2kr):
1161
1160
  def _validate_jp2c(self, boxes):
1162
1161
  """Validate the codestream box in relation to other boxes."""
1163
1162
  # jp2c must be preceeded by jp2h
1164
- jp2h_lst = [idx for (idx, box) in enumerate(boxes)
1165
- if box.box_id == 'jp2h']
1166
- jp2h_idx = jp2h_lst[0]
1167
- jp2c_lst = [idx for (idx, box) in enumerate(boxes)
1168
- if box.box_id == 'jp2c']
1169
- if len(jp2c_lst) == 0:
1163
+ jp2h_idx, _ = next(
1164
+ filter(lambda x: x[1].box_id == 'jp2h', enumerate(boxes)),
1165
+ (None, None)
1166
+ )
1167
+ jp2c_idx, _ = next(
1168
+ filter(lambda x: x[1].box_id == 'jp2c', enumerate(boxes)),
1169
+ (None, None)
1170
+ )
1171
+ if jp2c_idx is None:
1170
1172
  msg = (
1171
1173
  "A codestream box must be defined in the outermost list of "
1172
1174
  "boxes."
1173
1175
  )
1174
1176
  raise InvalidJp2kError(msg)
1175
1177
 
1176
- jp2c_idx = jp2c_lst[0]
1177
1178
  if jp2h_idx >= jp2c_idx:
1178
1179
  msg = "The codestream box must be preceeded by a jp2 header box."
1179
1180
  raise InvalidJp2kError(msg)
@@ -1182,8 +1183,7 @@ class Jp2k(Jp2kr):
1182
1183
  """Validate the JP2 Header box."""
1183
1184
  self._check_jp2h_child_boxes(boxes, 'top-level')
1184
1185
 
1185
- jp2h_lst = [box for box in boxes if box.box_id == 'jp2h']
1186
- jp2h = jp2h_lst[0]
1186
+ jp2h = next(filter(lambda x: x.box_id == 'jp2h', boxes), None)
1187
1187
 
1188
1188
  # 1st jp2 header box cannot be empty.
1189
1189
  if len(jp2h.box) == 0:
@@ -1199,20 +1199,18 @@ class Jp2k(Jp2kr):
1199
1199
  raise InvalidJp2kError(msg)
1200
1200
 
1201
1201
  # colr must be present in jp2 header box.
1202
- colr_lst = [
1203
- j for (j, box) in enumerate(jp2h.box) if box.box_id == 'colr'
1204
- ]
1205
- if len(colr_lst) == 0:
1202
+ colr = next(filter(lambda x: x.box_id == 'colr', jp2h.box), None)
1203
+ if colr is None:
1206
1204
  msg = "The jp2 header box must contain a color definition box."
1207
1205
  raise InvalidJp2kError(msg)
1208
- colr = jp2h.box[colr_lst[0]]
1209
1206
 
1210
1207
  self._validate_channel_definition(jp2h, colr)
1211
1208
 
1212
1209
  def _validate_channel_definition(self, jp2h, colr):
1213
1210
  """Validate the channel definition box."""
1214
- cdef_lst = [j for (j, box) in enumerate(jp2h.box)
1215
- if box.box_id == 'cdef']
1211
+ cdef_lst = [
1212
+ idx for (idx, box) in enumerate(jp2h.box) if box.box_id == 'cdef'
1213
+ ]
1216
1214
  if len(cdef_lst) > 1:
1217
1215
  msg = ("Only one channel definition box is allowed in the "
1218
1216
  "JP2 header.")
@@ -9,11 +9,11 @@ License: MIT
9
9
  # Standard library imports...
10
10
  from __future__ import annotations
11
11
  from contextlib import ExitStack
12
- from itertools import filterfalse
13
12
  import ctypes
14
13
  import pathlib
15
14
  import re
16
15
  import struct
16
+ import sys
17
17
  import warnings
18
18
 
19
19
  # Third party library imports
@@ -22,7 +22,7 @@ import numpy as np
22
22
  # Local imports...
23
23
  from .codestream import Codestream
24
24
  from . import core, version, get_option
25
- from .jp2box import Jp2kBox, FileTypeBox, InvalidJp2kError
25
+ from .jp2box import Jp2kBox, FileTypeBox, InvalidJp2kError, InvalidJp2kWarning
26
26
  from .lib import openjp2 as opj2
27
27
 
28
28
 
@@ -108,8 +108,8 @@ class Jp2kr(Jp2kBox):
108
108
  num_components = len(cstr.segment[1].xrsiz)
109
109
  else:
110
110
  # try to get the image size from the IHDR box
111
- jp2h = [box for box in self.box if box.box_id == 'jp2h'][0]
112
- ihdr = [box for box in jp2h.box if box.box_id == 'ihdr'][0]
111
+ jp2h = next(filter(lambda x: x.box_id == 'jp2h', self.box), None)
112
+ ihdr = next(filter(lambda x: x.box_id == 'ihdr', jp2h.box), None)
113
113
 
114
114
  height, width = ihdr.height, ihdr.width
115
115
  num_components = ihdr.num_components
@@ -189,10 +189,10 @@ class Jp2kr(Jp2kBox):
189
189
  @layer.setter
190
190
  def layer(self, layer):
191
191
  # Set to the indicated value so long as it is valid.
192
- cod = [
193
- segment for segment in self.codestream.segment
194
- if segment.marker_id == 'COD'
195
- ][0]
192
+ cod = next(
193
+ filter(lambda x: x.marker_id == 'COD', self.codestream.segment),
194
+ None
195
+ )
196
196
  if layer < 0 or layer >= cod.layers:
197
197
  msg = f"Invalid layer number, must be in range [0, {cod.layers})."
198
198
  raise ValueError(msg)
@@ -355,7 +355,13 @@ class Jp2kr(Jp2kBox):
355
355
  # Don't bother trying to validate JPX.
356
356
  return
357
357
 
358
- jp2h = [box for box in self.box if box.box_id == 'jp2h'][0]
358
+ jp2h = next(filter(lambda x: x.box_id == 'jp2h', self.box), None)
359
+ if jp2h is None:
360
+ msg = (
361
+ "No JP2 header box was located in the outermost jacket of "
362
+ "boxes."
363
+ )
364
+ raise InvalidJp2kError(msg)
359
365
 
360
366
  # An IHDR box is required as the first child box of the JP2H box.
361
367
  if jp2h.box[0].box_id != 'ihdr':
@@ -373,7 +379,7 @@ class Jp2kr(Jp2kBox):
373
379
  "enumerated colorspace or a restricted ICC profile if the "
374
380
  "file type box brand is 'jp2 '."
375
381
  )
376
- warnings.warn(msg, UserWarning)
382
+ warnings.warn(msg, InvalidJp2kWarning)
377
383
 
378
384
  # We need to have one and only one JP2H box if we have a JP2 file.
379
385
  num_jp2h_boxes = len([box for box in self.box if box.box_id == 'jp2h'])
@@ -382,7 +388,7 @@ class Jp2kr(Jp2kBox):
382
388
  f"This file has {num_jp2h_boxes} JP2H boxes in the outermost "
383
389
  "layer of boxes. There should only be one."
384
390
  )
385
- warnings.warn(msg)
391
+ warnings.warn(msg, InvalidJp2kWarning)
386
392
 
387
393
  # We should have one and only one JP2C box if we have a JP2 file.
388
394
  num_jp2c_boxes = len([box for box in self.box if box.box_id == 'jp2c'])
@@ -404,10 +410,10 @@ class Jp2kr(Jp2kBox):
404
410
  ihdr = jp2h.box[0]
405
411
  ihdr_dims = ihdr.height, ihdr.width, ihdr.num_components
406
412
 
407
- siz = [
408
- segment for segment in self.codestream.segment
409
- if segment.marker_id == 'SIZ'
410
- ][0]
413
+ siz = next(
414
+ filter(lambda x: x.marker_id == 'SIZ', self.codestream.segment),
415
+ None
416
+ )
411
417
 
412
418
  siz_dims = (siz.ysiz, siz.xsiz, len(siz.bitdepth))
413
419
  if ihdr_dims != siz_dims:
@@ -460,9 +466,9 @@ class Jp2kr(Jp2kBox):
460
466
  if isinstance(pargs, tuple) and any(isinstance(x, int) for x in pargs):
461
467
  # Replace the first such integer argument, replace it with a slice.
462
468
  lst = list(pargs)
463
- g = filterfalse(lambda x: not isinstance(x[1], int),
464
- enumerate(pargs))
465
- idx = next(g)[0]
469
+ idx, _ = next(
470
+ filter(lambda x: isinstance(x[1], int), enumerate(lst)), None
471
+ )
466
472
  lst[idx] = slice(pargs[idx], pargs[idx] + 1)
467
473
  newindex = tuple(lst)
468
474
 
@@ -676,10 +682,13 @@ class Jp2kr(Jp2kBox):
676
682
  # Must check the specified rlevel against the maximum.
677
683
  if rlevel != 0:
678
684
  # Must check the specified rlevel against the maximum.
679
- cod_seg = [
680
- segment for segment in self.codestream.segment
681
- if segment.marker_id == 'COD'
682
- ][0]
685
+ cod_seg = next(
686
+ filter(
687
+ lambda x: x.marker_id == 'COD',
688
+ self.codestream.segment
689
+ ),
690
+ None
691
+ )
683
692
  max_rlevel = cod_seg.num_res
684
693
  if rlevel == -1:
685
694
  # -1 is shorthand for the largest rlevel
@@ -889,25 +898,45 @@ class Jp2kr(Jp2kBox):
889
898
  Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))
890
899
  """
891
900
  with self.path.open('rb') as fptr:
901
+
902
+ # if it's just a raw codestream file, it's easy
892
903
  if self._codec_format == opj2.CODEC_J2K:
893
- codestream = Codestream(fptr, self.length,
894
- header_only=header_only)
895
- else:
896
- box = [x for x in self.box if x.box_id == 'jp2c']
897
- fptr.seek(box[0].offset)
904
+ return self._get_codestream(fptr, self.length, header_only)
905
+
906
+ # continue assuming JP2, must seek to the JP2C box and past its
907
+ # header
908
+ box = next(filter(lambda x: x.box_id == 'jp2c', self.box), None)
909
+
910
+ fptr.seek(box.offset)
911
+ read_buffer = fptr.read(8)
912
+ (box_length, _) = struct.unpack('>I4s', read_buffer)
913
+ if box_length == 0:
914
+ # The length of the box is presumed to last until the end
915
+ # of the file. Compute the effective length of the box.
916
+ box_length = self.path.stat().st_size - fptr.tell() + 8
917
+ elif box_length == 1:
918
+ # Seek past the XL field.
898
919
  read_buffer = fptr.read(8)
899
- (box_length, _) = struct.unpack('>I4s', read_buffer)
900
- if box_length == 0:
901
- # The length of the box is presumed to last until the end
902
- # of the file. Compute the effective length of the box.
903
- box_length = self.path.stat().st_size - fptr.tell() + 8
904
- elif box_length == 1:
905
- # Seek past the XL field.
906
- read_buffer = fptr.read(8)
907
- box_length, = struct.unpack('>Q', read_buffer)
908
- codestream = Codestream(fptr, box_length - 8,
909
- header_only=header_only)
920
+ box_length, = struct.unpack('>Q', read_buffer)
921
+
922
+ return self._get_codestream(fptr, box_length - 8, header_only)
923
+
924
+ def _get_codestream(self, fptr, length, header_only):
925
+ """
926
+ Parsing errors can make for confusing errors sometimes, so catch any
927
+ such error and add context to it.
928
+ """
910
929
 
930
+ try:
931
+ codestream = Codestream(fptr, length, header_only=header_only)
932
+ except Exception:
933
+ _, value, traceback = sys.exc_info()
934
+ msg = (
935
+ f'The file is invalid '
936
+ f'because the codestream could not be parsed: "{value}"'
937
+ )
938
+ raise InvalidJp2kError(msg).with_traceback(traceback)
939
+ else:
911
940
  return codestream
912
941
 
913
942
  def _validate_nonzero_image_size(self, nrows, ncols, component_index):
@@ -20,7 +20,7 @@ from .lib import tiff
20
20
 
21
21
  # Do not change the format of this next line! Doing so risks breaking
22
22
  # setup.py
23
- version = "0.13.2.post1"
23
+ version = "0.13.4"
24
24
 
25
25
  version_tuple = parse(version).release
26
26
 
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = Glymur
3
- version = 0.13.2.post1
3
+ version = 0.13.4
4
4
  author = 'John Evans'
5
5
  author_email = "John Evans" <jevans667cc@proton.me>
6
6
  license = 'MIT'
@@ -6,18 +6,21 @@ Test suite for codestream oddities
6
6
  # Standard library imports ...
7
7
  import importlib.resources as ir
8
8
  from io import BytesIO
9
+ import pathlib
9
10
  import struct
11
+ import tempfile
10
12
  import unittest
11
13
  import warnings
12
14
 
13
15
  # Local imports ...
14
16
  import glymur
15
- from glymur import Jp2k
17
+ from glymur import Jp2k, Jp2kr
18
+ from glymur.jp2box import InvalidJp2kError
16
19
  from . import fixtures
17
20
 
18
21
 
19
22
  class TestSuite(fixtures.TestCommon):
20
- """Test suite for ICC Profile code."""
23
+ """Test suite for codestreams."""
21
24
 
22
25
  def setUp(self):
23
26
  super().setUp()
@@ -28,6 +31,107 @@ class TestSuite(fixtures.TestCommon):
28
31
  self.issue142 = ir.files('tests.data').joinpath('issue142.j2k')
29
32
  self.edf_c2_1178956 = ir.files('tests.data').joinpath('edf_c2_1178956.jp2') # noqa : E501
30
33
 
34
+ def test_unrecognized_marker(self):
35
+ """
36
+ SCENARIO: There is an unrecognized marker just after an SOT marker but
37
+ before the EOC marker. All markers must have a leading byte value of
38
+ 0xff.
39
+
40
+ EXPECTED RESULT: InvalidJp2kError
41
+ """
42
+ with open(self.temp_j2k_filename, mode='wb') as tfile:
43
+ with open(self.j2kfile, 'rb') as ifile:
44
+ # Everything up until the SOT marker.
45
+ read_buffer = ifile.read(98)
46
+ tfile.write(read_buffer)
47
+
48
+ # Write the bad marker 0xd900
49
+ read_buffer = struct.pack('>H', 0xd900)
50
+ tfile.write(read_buffer)
51
+
52
+ # Get the rest of the input file.
53
+ read_buffer = ifile.read()
54
+ tfile.write(read_buffer)
55
+ tfile.flush()
56
+
57
+ with self.assertRaises(InvalidJp2kError):
58
+ Jp2k(tfile.name).get_codestream(header_only=False)
59
+
60
+ def test_bad_tile_part_pointer(self):
61
+ """
62
+ SCENARIO: A bad SOT marker segment is encountered (Psot value pointing
63
+ far beyond the end of the EOC marker) when requesting a fully parsed
64
+ codestream.
65
+
66
+ EXPECTED RESULT: InvalidJp2kError
67
+ """
68
+ with open(self.temp_jp2_filename, 'wb') as ofile:
69
+ with open(self.jp2file, 'rb') as ifile:
70
+ # Copy up until Psot field.
71
+ ofile.write(ifile.read(204))
72
+
73
+ # Write a bad Psot value.
74
+ ofile.write(struct.pack('>I', 2000000))
75
+
76
+ # copy the rest of the file as-is.
77
+ ifile.seek(208)
78
+ ofile.write(ifile.read())
79
+ ofile.flush()
80
+
81
+ j = Jp2kr(self.temp_jp2_filename)
82
+ with self.assertRaises(InvalidJp2kError):
83
+ j.get_codestream(header_only=False)
84
+
85
+ def test_tile_height_is_zero(self):
86
+ """
87
+ Scenario: A tile has height of zero.
88
+
89
+ Expected result: ZeroDivisionError
90
+
91
+ Original test file was input/nonregression/2539.pdf.SIGFPE.706.1712.jp2
92
+ """
93
+ fp = BytesIO()
94
+
95
+ buffer = struct.pack('>H', 47) # length
96
+
97
+ # kwargs = {'rsiz': 1,
98
+ # 'xysiz': (1000, 1000),
99
+ # 'xyosiz': (0, 0),
100
+ # 'xytsiz': (0, 1000),
101
+ # 'xytosiz': (0, 0),
102
+ # 'Csiz': 3,
103
+ # 'bitdepth': (8, 8, 8),
104
+ # 'signed': (False, False, False),
105
+ # 'xyrsiz': ((1, 1, 1), (1, 1, 1)),
106
+ # 'length': 47,
107
+ # 'offset': 2}
108
+ buffer += struct.pack('>HIIIIIIIIH', 1, 1000, 1000, 0, 0, 0, 1000,
109
+ 0, 0, 3)
110
+ buffer += struct.pack('>BBBBBBBBB', 7, 1, 1, 7, 1, 1, 7, 1, 1)
111
+ fp.write(buffer)
112
+ fp.seek(0)
113
+
114
+ with self.assertRaises(ZeroDivisionError):
115
+ glymur.codestream.Codestream._parse_siz_segment(fp)
116
+
117
+ def test_invalid_codestream_past_header(self):
118
+ """
119
+ Scenario: the codestream is ok thru the header, but invalid after
120
+ that. The codestream header for the complete test file ends at byte
121
+
122
+ Expected result: InvalidJp2kError
123
+ """
124
+ path = ir.files('tests.data').joinpath('p1_06.j2k')
125
+
126
+ with tempfile.TemporaryDirectory() as tdir:
127
+ with open(path, mode='rb') as ifile:
128
+ with open(pathlib.Path(tdir) / 'tmp.j2k', mode='wb') as ofile:
129
+ ofile.write(ifile.read(555))
130
+
131
+ with self.assertRaises(InvalidJp2kError):
132
+ j = Jp2k(pathlib.Path(tdir) / 'tmp.j2k')
133
+ j.get_codestream(header_only=False)
134
+
31
135
  def test_tlm_segment(self):
32
136
  """
33
137
  Verify parsing of the TLM segment.
@@ -141,12 +245,14 @@ class TestSuite(fixtures.TestCommon):
141
245
 
142
246
  def test_626(self):
143
247
  """
144
- Scenario:
248
+ Scenario: After parsing the SOC and SIZ segments, an unknown segment
249
+ (probably invalid) is hit, and then the file ends, leaving us trying
250
+ to interpret EOF as another marker segment.
145
251
 
146
- Expected result: J2KParseError
252
+ Expected result: InvalidJp2kError
147
253
  """
148
254
  path = ir.files('tests.data').joinpath('issue626.j2k')
149
- with self.assertRaises(glymur.codestream.J2KParseError):
255
+ with self.assertRaises(InvalidJp2kError):
150
256
  Jp2k(path)
151
257
 
152
258
 
@@ -1,7 +1,6 @@
1
1
  """Test suite specifically targeting JP2 box layout.
2
2
  """
3
3
  # Standard library imports ...
4
- import doctest
5
4
  import importlib.resources as ir
6
5
  from io import BytesIO
7
6
  import os
@@ -21,7 +20,7 @@ import numpy as np
21
20
 
22
21
  # Local imports ...
23
22
  import glymur
24
- from glymur import Jp2k
23
+ from glymur import Jp2k, Jp2kr
25
24
  from glymur.jp2box import (
26
25
  ColourSpecificationBox, ContiguousCodestreamBox, FileTypeBox,
27
26
  ImageHeaderBox, JP2HeaderBox, JPEG2000SignatureBox, BitsPerComponentBox,
@@ -33,20 +32,6 @@ from . import fixtures
33
32
  from .fixtures import OPENJPEG_NOT_AVAILABLE, OPENJPEG_NOT_AVAILABLE_MSG
34
33
 
35
34
 
36
- def docTearDown(doctest_obj): # pragma: no cover
37
- glymur.set_option('parse.full_codestream', False)
38
-
39
-
40
- def load_tests(loader, tests, ignore): # pragma: no cover
41
- """Run doc tests as well."""
42
- if os.name == "nt":
43
- # Can't do it on windows, temporary file issue.
44
- return tests
45
- tests.addTests(doctest.DocTestSuite('glymur.jp2box',
46
- tearDown=docTearDown))
47
- return tests
48
-
49
-
50
35
  @unittest.skipIf(OPENJPEG_NOT_AVAILABLE, OPENJPEG_NOT_AVAILABLE_MSG)
51
36
  class TestDataEntryURL(fixtures.TestCommon):
52
37
  """Test suite for DataEntryURL boxes."""
@@ -371,6 +356,16 @@ class TestChannelDefinition(fixtures.TestCommon):
371
356
  class TestFileTypeBox(fixtures.TestCommon):
372
357
  """Test suite for ftyp box issues."""
373
358
 
359
+ def test_jph(self):
360
+ """
361
+ SCENARIO: JPH box
362
+
363
+ EXPECTED RESULT: The brand is verified.
364
+ """
365
+ path = ir.files('tests.data').joinpath('oj-ht-byte.jph')
366
+ j = Jp2kr(path)
367
+ self.assertEqual(j.box[1].brand, 'jph ')
368
+
374
369
  def test_bad_brand_on_parse(self):
375
370
  """
376
371
  SCENARIO: The JP2 file file type box does not contain a valid brand.
@@ -1055,11 +1050,40 @@ class TestJp2Boxes(fixtures.TestCommon):
1055
1050
  box._filename = str(self.jp2file)
1056
1051
  box.codestream
1057
1052
 
1053
+ def test_no_jp2h_box(self):
1054
+ """
1055
+ SCENARIO: The JP2/JP2H box is missing
1056
+
1057
+ EXPECTED RESULT: InvalidJp2kError
1058
+ """
1059
+ # Write a new JP2 file that omits the IHDR box.
1060
+ j = Jp2k(self.jp2file)
1061
+ jp2h = [box for box in j.box if box.box_id == 'jp2h'][0]
1062
+ with open(self.temp_jp2_filename, mode='wb') as tfile:
1063
+ numbytes = jp2h.offset
1064
+ with open(self.jp2file, 'rb') as ifile:
1065
+ # Write all the way up to the ihdr box
1066
+ tfile.write(ifile.read(numbytes))
1067
+
1068
+ # Seek past the ihdr box
1069
+ ifile.seek(jp2h.length, os.SEEK_CUR)
1070
+
1071
+ # Write the rest of the JP2 file
1072
+ tfile.write(ifile.read(numbytes))
1073
+
1074
+ tfile.flush()
1075
+
1076
+ with self.assertRaises(InvalidJp2kError):
1077
+ with warnings.catch_warnings():
1078
+ # Lots of things wrong with this file.
1079
+ warnings.simplefilter('ignore')
1080
+ Jp2k(tfile.name)
1081
+
1058
1082
  def test_no_ihdr_box(self):
1059
1083
  """
1060
1084
  SCENARIO: The JP2/IHDR box cannot be parsed.
1061
1085
 
1062
- EXPECTED RESULT: An RuntimeError is issued.
1086
+ EXPECTED RESULT: InvalidJp2kError
1063
1087
  """
1064
1088
  # Write a new JP2 file that omits the IHDR box.
1065
1089
  j = Jp2k(self.jp2file)
@@ -1089,19 +1113,16 @@ class TestJp2Boxes(fixtures.TestCommon):
1089
1113
  """
1090
1114
  SCENARIO: The JP2 file has no JP2C box.
1091
1115
 
1092
- EXPECTED RESULT: An InvalidJp2kError is issued.
1116
+ EXPECTED RESULT: An InvalidJp2kError is issued when the file is
1117
+ parsed.
1093
1118
  """
1094
- # Write a new JP2 file that omits the JP2C box.
1095
- j = Jp2k(self.jp2file)
1096
- jp2c = [box for box in j.box if box.box_id == 'jp2c'][0]
1097
- with open(self.temp_jp2_filename, mode='wb') as tfile:
1098
- numbytes = jp2c.offset
1099
- with open(self.jp2file, 'rb') as ifile:
1100
- tfile.write(ifile.read(numbytes))
1101
- tfile.flush()
1119
+ testfile = ir.files('tests.data').joinpath('no_jp2c.jp2')
1102
1120
 
1121
+ with warnings.catch_warnings():
1122
+ # Lots of things wrong with this file.
1123
+ warnings.simplefilter('ignore')
1103
1124
  with self.assertRaises(InvalidJp2kError):
1104
- Jp2k(tfile.name)
1125
+ Jp2k(testfile)
1105
1126
 
1106
1127
  def test_two_jp2c_boxes(self):
1107
1128
  """
@@ -38,6 +38,20 @@ class TestJp2k(fixtures.TestCommon):
38
38
  super().setUp()
39
39
  glymur.reset_option('all')
40
40
 
41
+ @unittest.skipIf(
42
+ glymur.version.openjpeg_version < '2.5.0', "Requires as least v2.5.0"
43
+ )
44
+ def test_read_htj2k(self):
45
+ """
46
+ Scenario: read an HTJ2K (JPH) file using Jp2k
47
+
48
+ Expected response: The size of the image read is verified.
49
+ """
50
+ path = ir.files('tests.data').joinpath('oj-ht-byte.jph')
51
+ j = Jp2k(path)
52
+ d = j[:]
53
+ self.assertEqual(d.shape, (20, 20))
54
+
41
55
  def test_repr(self):
42
56
  """
43
57
  Scenario: run repr on Jp2k object
@@ -262,31 +276,6 @@ class TestJp2k(fixtures.TestCommon):
262
276
 
263
277
  np.testing.assert_array_equal(rgb, bgr[:, :, [2, 1, 0]])
264
278
 
265
- def test_bad_tile_part_pointer(self):
266
- """
267
- SCENARIO: A bad SOT marker segment is encountered (Psot value pointing
268
- far beyond the end of the EOC marker) when requesting a fully parsed
269
- codestream.
270
-
271
- EXPECTED RESULT: struct.error
272
- """
273
- with open(self.temp_jp2_filename, 'wb') as ofile:
274
- with open(self.jp2file, 'rb') as ifile:
275
- # Copy up until Psot field.
276
- ofile.write(ifile.read(204))
277
-
278
- # Write a bad Psot value.
279
- ofile.write(struct.pack('>I', 2000000))
280
-
281
- # copy the rest of the file as-is.
282
- ifile.seek(208)
283
- ofile.write(ifile.read())
284
- ofile.flush()
285
-
286
- j = Jp2k(self.temp_jp2_filename)
287
- with self.assertRaises(struct.error):
288
- j.get_codestream(header_only=False)
289
-
290
279
  def test_read_differing_subsamples(self):
291
280
  """
292
281
  SCENARIO: Attempt to read a file where the components have differing
@@ -393,20 +382,6 @@ class TestJp2k(fixtures.TestCommon):
393
382
  with self.assertRaises(InvalidJp2kError):
394
383
  Jp2k(path)
395
384
 
396
- @unittest.skip("This test may not be appropriate")
397
- def test_file_does_not_exist(self):
398
- """
399
- Scenario: The Jp2k construtor is passed a file that does not exist
400
- and the intent is reading.
401
-
402
- Expected Result: FileNotFoundError
403
- """
404
- # Verify that we error out appropriately if not given an existing file
405
- # at all.
406
- filename = 'this file does not actually exist on the file system.'
407
- with self.assertRaises(FileNotFoundError):
408
- Jp2k(filename)
409
-
410
385
  def test_codestream(self):
411
386
  """
412
387
  Verify the markers and segments of a JP2 file codestream.
@@ -1842,7 +1842,7 @@ class TestSuite(fixtures.TestCommon):
1842
1842
 
1843
1843
  def test_1x1_tile(self):
1844
1844
  """
1845
- SCENARIO: Write an image that is tiled 1x1.
1845
+ SCENARIO: Write by tiles an image that is tiled 1x1.
1846
1846
 
1847
1847
  EXPECTED RESULT: RuntimeError, as this triggers an unresolved
1848
1848
  bug, issue586.
@@ -1858,8 +1858,7 @@ class TestSuite(fixtures.TestCommon):
1858
1858
  self.temp_j2k_filename, shape=shape, tilesize=tilesize,
1859
1859
  )
1860
1860
  with self.assertRaises(RuntimeError):
1861
- for tw in j.get_tilewriters():
1862
- tw[:] = j2k_data
1861
+ j.get_tilewriters()
1863
1862
 
1864
1863
  def test_openjpeg_library_too_old_for_tile_writing(self):
1865
1864
  """
@@ -205,31 +205,6 @@ class TestJp2kr(fixtures.TestCommon):
205
205
  rgb_from_idx[r, c] = palette[idx[r, c]]
206
206
  np.testing.assert_array_equal(rgb, rgb_from_idx)
207
207
 
208
- def test_bad_tile_part_pointer(self):
209
- """
210
- SCENARIO: A bad SOT marker segment is encountered (Psot value pointing
211
- far beyond the end of the EOC marker) when requesting a fully parsed
212
- codestream.
213
-
214
- EXPECTED RESULT: struct.error
215
- """
216
- with open(self.temp_jp2_filename, 'wb') as ofile:
217
- with open(self.jp2file, 'rb') as ifile:
218
- # Copy up until Psot field.
219
- ofile.write(ifile.read(204))
220
-
221
- # Write a bad Psot value.
222
- ofile.write(struct.pack('>I', 2000000))
223
-
224
- # copy the rest of the file as-is.
225
- ifile.seek(208)
226
- ofile.write(ifile.read())
227
- ofile.flush()
228
-
229
- j = Jp2kr(self.temp_jp2_filename)
230
- with self.assertRaises(struct.error):
231
- j.get_codestream(header_only=False)
232
-
233
208
  def test_read_differing_subsamples(self):
234
209
  """
235
210
  SCENARIO: Attempt to read a file where the components have differing
@@ -1607,6 +1607,27 @@ class TestPrinting(fixtures.TestCommon):
1607
1607
  )
1608
1608
  self.assertEqual(actual, expected)
1609
1609
 
1610
+ def test_jph_rsiz(self):
1611
+ """
1612
+ Scenario: parse a JPH file, print the SIZ segment
1613
+
1614
+ Expected result: no warnings, the output is verified
1615
+ """
1616
+ path = ir.files('tests.data').joinpath('oj-ht-byte.jph')
1617
+
1618
+ with warnings.catch_warnings():
1619
+ warnings.simplefilter('error')
1620
+ j = Jp2k(path)
1621
+ actual = str(j.codestream.segment[1])
1622
+
1623
+ expected = (
1624
+ ir.files('tests.data')
1625
+ .joinpath('jph_siz.txt')
1626
+ .read_text()
1627
+ .rstrip()
1628
+ )
1629
+ self.assertEqual(actual, expected)
1630
+
1610
1631
 
1611
1632
  class TestJp2dump(fixtures.TestCommon):
1612
1633
  """Tests for verifying how jp2dump console script works."""
@@ -150,7 +150,6 @@ class TestSuite(unittest.TestCase):
150
150
  with warnings.catch_warnings():
151
151
  warnings.simplefilter('error')
152
152
  j2k.decoded_components = -1
153
- j2k[:]
154
153
 
155
154
  def test_same_component_several_times(self):
156
155
  """
@@ -78,33 +78,6 @@ class TestSuite(fixtures.TestCommon):
78
78
  # c = Jp2k(tfile.name).get_codestream(header_only=False)
79
79
  Jp2k(tfile.name)
80
80
 
81
- def test_unrecognized_marker(self):
82
- """
83
- SCENARIO: There is an unrecognized marker just after an SOT marker but
84
- before the EOC marker. All markers must have a leading byte value of
85
- 0xff.
86
-
87
- EXPECTED RESULT: The SOT marker is the last one retrieved from the
88
- codestream.
89
- """
90
- with open(self.temp_j2k_filename, mode='wb') as tfile:
91
- with open(self.j2kfile, 'rb') as ifile:
92
- # Everything up until the SOT marker.
93
- read_buffer = ifile.read(98)
94
- tfile.write(read_buffer)
95
-
96
- # Write the bad marker 0xd900
97
- read_buffer = struct.pack('>H', 0xd900)
98
- tfile.write(read_buffer)
99
-
100
- # Get the rest of the input file.
101
- read_buffer = ifile.read()
102
- tfile.write(read_buffer)
103
- tfile.flush()
104
-
105
- with self.assertRaises(ValueError):
106
- Jp2k(tfile.name).get_codestream(header_only=False)
107
-
108
81
  def test_unrecoverable_xml(self):
109
82
  """
110
83
  Bad byte sequence in XML that cannot be parsed.
@@ -123,36 +96,6 @@ class TestSuite(fixtures.TestCommon):
123
96
 
124
97
  self.assertIsNone(box.xml)
125
98
 
126
- def test_tile_height_is_zero(self):
127
- """
128
- Zero tile height should not cause an exception.
129
-
130
- Original test file was input/nonregression/2539.pdf.SIGFPE.706.1712.jp2
131
- """
132
- fp = BytesIO()
133
-
134
- buffer = struct.pack('>H', 47) # length
135
-
136
- # kwargs = {'rsiz': 1,
137
- # 'xysiz': (1000, 1000),
138
- # 'xyosiz': (0, 0),
139
- # 'xytsiz': (0, 1000),
140
- # 'xytosiz': (0, 0),
141
- # 'Csiz': 3,
142
- # 'bitdepth': (8, 8, 8),
143
- # 'signed': (False, False, False),
144
- # 'xyrsiz': ((1, 1, 1), (1, 1, 1)),
145
- # 'length': 47,
146
- # 'offset': 2}
147
- buffer += struct.pack('>HIIIIIIIIH', 1, 1000, 1000, 0, 0, 0, 1000,
148
- 0, 0, 3)
149
- buffer += struct.pack('>BBBBBBBBB', 7, 1, 1, 7, 1, 1, 7, 1, 1)
150
- fp.write(buffer)
151
- fp.seek(0)
152
-
153
- with self.assertWarns(UserWarning):
154
- glymur.codestream.Codestream._parse_siz_segment(fp)
155
-
156
99
  def test_invalid_progression_order(self):
157
100
  """
158
101
  Should still be able to parse even if prog order is invalid.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes