Skip to content

Commit 7fb79a9

Browse files
authored
Ignore bits above the precision when encoding (#105)
1 parent 6685a27 commit 7fb79a9

File tree

9 files changed

+425
-49
lines changed

9 files changed

+425
-49
lines changed

.github/workflows/pytest-builds.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
with:
2626
python-version: ${{ matrix.python-version }}
2727
architecture: ${{ matrix.arch }}
28+
allow-prereleases: true
2829

2930
- name: Install package and dependencies
3031
run: |
@@ -59,6 +60,7 @@ jobs:
5960
uses: actions/setup-python@v5
6061
with:
6162
python-version: ${{ matrix.python-version }}
63+
allow-prereleases: true
6264

6365
- name: Install package and dependencies
6466
run: |
@@ -82,7 +84,7 @@ jobs:
8284
strategy:
8385
fail-fast: false
8486
matrix:
85-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
87+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
8688

8789
steps:
8890
- uses: actions/checkout@v4
@@ -93,6 +95,7 @@ jobs:
9395
uses: actions/setup-python@v5
9496
with:
9597
python-version: ${{ matrix.python-version }}
98+
allow-prereleases: true
9699

97100
- name: Install package and dependencies
98101
run: |
@@ -106,13 +109,14 @@ jobs:
106109
pytest --cov openjpeg openjpeg/tests
107110
108111
- name: Install pydicom dev and rerun pytest (3.10+)
109-
if: ${{ contains('3.10 3.11 3.12 3.13', matrix.python-version) }}
112+
if: ${{ contains('3.10 3.11 3.12 3.13 3.14', matrix.python-version) }}
110113
run: |
111114
pip install pylibjpeg
112115
pip install git+https://github.com/pydicom/pydicom
113116
pytest --cov openjpeg openjpeg/tests
114117
115118
- name: Switch to current pydicom release and rerun pytest
119+
if: ${{ contains('3.10 3.11 3.12 3.13', matrix.python-version) }}
116120
run: |
117121
pip uninstall -y pydicom
118122
pip install pydicom pylibjpeg

.github/workflows/release-wheels.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ jobs:
5757
- os: windows-latest
5858
python: 313
5959
platform_id: win32
60+
- os: windows-latest
61+
python: 314
62+
platform_id: win32
6063

6164
# Windows 64 bit
6265
- os: windows-latest
@@ -74,6 +77,9 @@ jobs:
7477
- os: windows-latest
7578
python: 313
7679
platform_id: win_amd64
80+
- os: windows-latest
81+
python: 314
82+
platform_id: win_amd64
7783

7884
# Linux 64 bit manylinux2014
7985
- os: ubuntu-latest
@@ -96,6 +102,10 @@ jobs:
96102
python: 313
97103
platform_id: manylinux_x86_64
98104
manylinux_image: manylinux2014
105+
- os: ubuntu-latest
106+
python: 314
107+
platform_id: manylinux_x86_64
108+
manylinux_image: manylinux2014
99109

100110
# Linux aarch64
101111
- os: ubuntu-latest
@@ -113,6 +123,9 @@ jobs:
113123
- os: ubuntu-latest
114124
python: 313
115125
platform_id: manylinux_aarch64
126+
- os: ubuntu-latest
127+
python: 314
128+
platform_id: manylinux_aarch64
116129

117130
# MacOS 13 x86_64
118131
- os: macos-13
@@ -130,6 +143,9 @@ jobs:
130143
- os: macos-13
131144
python: 313
132145
platform_id: macosx_x86_64
146+
- os: macos-13
147+
python: 314
148+
platform_id: macosx_x86_64
133149

134150
steps:
135151
- uses: actions/checkout@v4
@@ -194,6 +210,9 @@ jobs:
194210
- os: macos-14
195211
python: 313
196212
platform_id: macosx_arm64
213+
- os: macos-14
214+
python: 314
215+
platform_id: macosx_arm64
197216

198217
steps:
199218
- uses: actions/checkout@v4

docs/changes/v2.5.0.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.. _v2.5.0:
2+
3+
2.5.0
4+
=====
5+
6+
Changes
7+
.......
8+
9+
* Bits above the precision are now ignored when encoding (:issue:`104`)
10+
* Supported Python versions are 3.9 to 3.14.

lib/interface/encode.c

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -781,20 +781,41 @@ extern int EncodeBuffer(
781781
OPJ_UINT64 nr_pixels = rows * columns;
782782
char *data = PyBytes_AsString(src);
783783
if (bytes_per_pixel == 1) {
784+
unsigned char value;
785+
unsigned char unsigned_mask = 0xFF >> (8 - bits_stored);
786+
unsigned char signed_mask = 0xFF << bits_stored;
787+
unsigned char bit_flag = 1 << (bits_stored - 1);
788+
unsigned short do_masking = bits_stored < 8;
784789
for (OPJ_UINT64 ii = 0; ii < nr_pixels; ii++)
785790
{
786791
for (p = 0; p < samples_per_pixel; p++)
787792
{
788793
// comps[...].data[...] is OPJ_INT32 -> int32_t
789-
image->comps[p].data[ii] = is_signed ? (signed char) *data : (unsigned char) *data;
794+
value = (unsigned char) *data;
790795
data++;
796+
797+
if (do_masking) {
798+
// Unsigned: zero out bits above `precision`
799+
// Signed: zero out bits above `precision` if value >= 0, otherwise
800+
// set them to one
801+
if (is_signed && (bit_flag & value)) {
802+
value = value | signed_mask;
803+
} else {
804+
value = value & unsigned_mask;
805+
}
806+
}
807+
808+
image->comps[p].data[ii] = is_signed ? (signed char) value : value;
791809
}
792810
}
793811
} else if (bytes_per_pixel == 2) {
794812
unsigned short value;
795813
unsigned char temp1;
796814
unsigned char temp2;
797-
815+
unsigned short unsigned_mask = 0xFFFF >> (16 - bits_stored);
816+
unsigned short signed_mask = 0xFFFF << bits_stored;
817+
unsigned short bit_flag = 1 << (bits_stored - 1);
818+
unsigned short do_masking = bits_stored < 16;
798819
for (OPJ_UINT64 ii = 0; ii < nr_pixels; ii++)
799820
{
800821
for (p = 0; p < samples_per_pixel; p++)
@@ -803,7 +824,16 @@ extern int EncodeBuffer(
803824
data++;
804825
temp2 = (unsigned char) *data;
805826
data++;
827+
806828
value = (unsigned short) ((temp2 << 8) + temp1);
829+
if (do_masking) {
830+
if (is_signed && (bit_flag & value)) {
831+
value = value | signed_mask;
832+
} else {
833+
value = value & unsigned_mask;
834+
}
835+
}
836+
807837
image->comps[p].data[ii] = is_signed ? (signed short) value : value;
808838
}
809839
}
@@ -813,7 +843,10 @@ extern int EncodeBuffer(
813843
unsigned char temp2;
814844
unsigned char temp3;
815845
unsigned char temp4;
816-
846+
unsigned long unsigned_mask = 0xFFFFFFFF >> (32 - bits_stored);
847+
unsigned long signed_mask = 0xFFFFFFFF << bits_stored;
848+
unsigned long bit_flag = 1 << (bits_stored - 1);
849+
unsigned short do_masking = bits_stored < 32;
817850
for (OPJ_UINT64 ii = 0; ii < nr_pixels; ii++)
818851
{
819852
for (p = 0; p < samples_per_pixel; p++)
@@ -827,13 +860,16 @@ extern int EncodeBuffer(
827860
temp4 = (unsigned char) * data;
828861
data++;
829862

830-
value = (unsigned long)((temp4 << 24) + (temp3 << 16) + (temp2 << 8) + temp1);
831-
832-
if (is_signed) {
833-
image->comps[p].data[ii] = (long) value;
834-
} else {
835-
image->comps[p].data[ii] = value;
863+
value = (unsigned long) ((temp4 << 24) + (temp3 << 16) + (temp2 << 8) + temp1);
864+
if (do_masking) {
865+
if (is_signed && (bit_flag & value)) {
866+
value = value | signed_mask;
867+
} else {
868+
value = value & unsigned_mask;
869+
}
836870
}
871+
872+
image->comps[p].data[ii] = is_signed ? (long) value : value;
837873
}
838874
}
839875
}

openjpeg/tests/test_decode.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from io import BytesIO
44

55
try:
6-
from pydicom.encaps import generate_pixel_data_frame
7-
from pydicom.pixel_data_handlers.util import (
6+
from pydicom.encaps import generate_frames
7+
from pydicom.pixels.utils import (
88
reshape_pixel_array,
99
pixel_dtype,
1010
)
@@ -72,10 +72,10 @@ def test_version():
7272
assert 5 == version[1]
7373

7474

75-
def generate_frames(ds):
75+
def get_frame_generator(ds):
7676
"""Return a frame generator for DICOM datasets."""
7777
nr_frames = ds.get("NumberOfFrames", 1)
78-
return generate_pixel_data_frame(ds.PixelData, nr_frames)
78+
return generate_frames(ds.PixelData, number_of_frames=nr_frames)
7979

8080

8181
def test_get_format_raises():
@@ -91,7 +91,7 @@ def test_bad_decode():
9191
"""Test trying to decode bad data."""
9292
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
9393
ds = index["966.dcm"]["ds"]
94-
frame = next(generate_frames(ds))
94+
frame = next(get_frame_generator(ds))
9595
msg = r"Error decoding the J2K data: failed to decode image"
9696
with pytest.raises(RuntimeError, match=msg):
9797
decode(frame)
@@ -108,7 +108,7 @@ def test_decode_bytes(self):
108108
"""Test decoding using bytes."""
109109
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
110110
ds = index["MR_small_jp2klossless.dcm"]["ds"]
111-
frame = next(generate_frames(ds))
111+
frame = next(get_frame_generator(ds))
112112
assert isinstance(frame, bytes)
113113
arr = decode(frame)
114114
assert arr.flags.writeable
@@ -126,7 +126,7 @@ def test_decode_filelike(self):
126126
"""Test decoding using file-like."""
127127
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
128128
ds = index["MR_small_jp2klossless.dcm"]["ds"]
129-
frame = BytesIO(next(generate_frames(ds)))
129+
frame = BytesIO(next(get_frame_generator(ds)))
130130
assert isinstance(frame, BytesIO)
131131
arr = decode(frame)
132132
assert arr.flags.writeable
@@ -144,7 +144,7 @@ def test_decode_bad_type_raises(self):
144144
"""Test decoding using invalid type raises."""
145145
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
146146
ds = index["MR_small_jp2klossless.dcm"]["ds"]
147-
frame = tuple(next(generate_frames(ds)))
147+
frame = tuple(next(get_frame_generator(ds)))
148148
assert not hasattr(frame, "tell") and not isinstance(frame, bytes)
149149

150150
msg = (
@@ -159,7 +159,7 @@ def test_decode_bad_format_raises(self):
159159
"""Test decoding using invalid jpeg format raises."""
160160
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
161161
ds = index["MR_small_jp2klossless.dcm"]["ds"]
162-
frame = next(generate_frames(ds))
162+
frame = next(get_frame_generator(ds))
163163

164164
msg = r"Unsupported 'j2k_format' value: 3"
165165
with pytest.raises(ValueError, match=msg):
@@ -170,7 +170,7 @@ def test_decode_reshape_true(self):
170170
"""Test decoding using invalid jpeg format raises."""
171171
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
172172
ds = index["US1_J2KR.dcm"]["ds"]
173-
frame = next(generate_frames(ds))
173+
frame = next(get_frame_generator(ds))
174174

175175
arr = decode(frame)
176176
assert arr.flags.writeable
@@ -205,7 +205,7 @@ def test_decode_reshape_false(self):
205205
"""Test decoding using invalid jpeg format raises."""
206206
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
207207
ds = index["US1_J2KR.dcm"]["ds"]
208-
frame = next(generate_frames(ds))
208+
frame = next(get_frame_generator(ds))
209209

210210
arr = decode(frame, reshape=False)
211211
assert arr.flags.writeable
@@ -216,7 +216,7 @@ def test_signed_error(self):
216216
"""Regression test for #30."""
217217
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
218218
ds = index["693_J2KR.dcm"]["ds"]
219-
frame = next(generate_frames(ds))
219+
frame = next(get_frame_generator(ds))
220220

221221
arr = decode(frame)
222222
assert -2000 == arr[0, 0]
@@ -372,7 +372,7 @@ def test_jpeg2000r(self, fname, info):
372372
# info: (rows, columns, spp, bps)
373373
index = get_indexed_datasets("1.2.840.10008.1.2.4.90")
374374
ds = index[fname]["ds"]
375-
frame = next(generate_frames(ds))
375+
frame = next(get_frame_generator(ds))
376376
arr = decode(BytesIO(frame), reshape=False)
377377
assert arr.flags.writeable
378378

@@ -406,7 +406,7 @@ def test_jpeg2000i(self, fname, info):
406406
index = get_indexed_datasets("1.2.840.10008.1.2.4.91")
407407
ds = index[fname]["ds"]
408408

409-
frame = next(generate_frames(ds))
409+
frame = next(get_frame_generator(ds))
410410
arr = decode(BytesIO(frame), reshape=False)
411411
assert arr.flags.writeable
412412

0 commit comments

Comments
 (0)