PyTurboJPEG 2.1.0__tar.gz → 2.2.0__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.
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/PKG-INFO +101 -29
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/PyTurboJPEG.egg-info/PKG-INFO +101 -29
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/README.md +100 -28
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/setup.py +1 -1
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/tests/test_turbojpeg.py +62 -1
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/turbojpeg.py +130 -14
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/LICENSE +0 -0
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/PyTurboJPEG.egg-info/SOURCES.txt +0 -0
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/PyTurboJPEG.egg-info/dependency_links.txt +0 -0
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/PyTurboJPEG.egg-info/requires.txt +0 -0
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/PyTurboJPEG.egg-info/top_level.txt +0 -0
- {pyturbojpeg-2.1.0 → pyturbojpeg-2.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyTurboJPEG
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A Python wrapper of libjpeg-turbo for decoding and encoding JPEG image.
|
|
5
5
|
Home-page: https://github.com/lilohuang/PyTurboJPEG
|
|
6
6
|
Author: Lilo Huang
|
|
@@ -26,6 +26,11 @@ Dynamic: summary
|
|
|
26
26
|
|
|
27
27
|
A Python wrapper for libjpeg-turbo that enables efficient JPEG image decoding and encoding.
|
|
28
28
|
|
|
29
|
+
[](https://pypi.org/project/pyturbojpeg/)
|
|
30
|
+

|
|
31
|
+
[](https://pypistats.org/packages/pyturbojpeg)
|
|
32
|
+
[](https://github.com/lilohuang/PyTurboJPEG/blob/master/LICENSE)
|
|
33
|
+
|
|
29
34
|
## Prerequisites
|
|
30
35
|
|
|
31
36
|
- [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo/releases) **3.0 or later** (required for PyTurboJPEG 2.0+)
|
|
@@ -239,6 +244,73 @@ cv2.imshow('transposed_image', transposed_image)
|
|
|
239
244
|
cv2.waitKey(0)
|
|
240
245
|
```
|
|
241
246
|
|
|
247
|
+
### ICC Color Management Workflow
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import io
|
|
251
|
+
import numpy as np
|
|
252
|
+
from PIL import Image, ImageCms
|
|
253
|
+
from turbojpeg import TurboJPEG, TJPF_BGR
|
|
254
|
+
|
|
255
|
+
def decode_jpeg_with_color_management(jpeg_path):
|
|
256
|
+
"""
|
|
257
|
+
Decodes a JPEG and applies color management (ICC Profile to sRGB).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
jpeg_path (str): Path to the input JPEG file.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
PIL.Image: The color-corrected sRGB Image object.
|
|
264
|
+
"""
|
|
265
|
+
# 1. Initialize TurboJPEG
|
|
266
|
+
jpeg = TurboJPEG()
|
|
267
|
+
|
|
268
|
+
with open(jpeg_path, 'rb') as f:
|
|
269
|
+
jpeg_data = f.read()
|
|
270
|
+
|
|
271
|
+
# 2. Get image headers and decode pixels
|
|
272
|
+
# Using TJPF_BGR format (OpenCV standard) for the raw buffer
|
|
273
|
+
width, height, _, _ = jpeg.decode_header(jpeg_data)
|
|
274
|
+
pixels = jpeg.decode(jpeg_data, pixel_format=TJPF_BGR)
|
|
275
|
+
|
|
276
|
+
# 3. Encapsulate into a Pillow Image object
|
|
277
|
+
# Key: Use 'raw' and 'BGR' decoder to correctly map BGR bytes to an RGB Image object
|
|
278
|
+
img = Image.frombytes('RGB', (width, height), pixels, 'raw', 'BGR')
|
|
279
|
+
|
|
280
|
+
# 4. Handle ICC Profile transformation
|
|
281
|
+
try:
|
|
282
|
+
# Extract embedded ICC Profile
|
|
283
|
+
icc_profile = jpeg.get_icc_profile(jpeg_data)
|
|
284
|
+
|
|
285
|
+
if icc_profile:
|
|
286
|
+
# Create Source and Destination Profile objects
|
|
287
|
+
src_profile = ImageCms.getOpenProfile(io.BytesIO(icc_profile))
|
|
288
|
+
dst_profile = ImageCms.createProfile("sRGB")
|
|
289
|
+
|
|
290
|
+
# Perform color transformation (similar to "Convert to Profile" in Photoshop)
|
|
291
|
+
# This step recalculates pixel values to align with sRGB standards
|
|
292
|
+
img = ImageCms.profileToProfile(
|
|
293
|
+
img,
|
|
294
|
+
src_profile,
|
|
295
|
+
dst_profile,
|
|
296
|
+
outputMode='RGB'
|
|
297
|
+
)
|
|
298
|
+
print(f"Successfully applied ICC profile from {jpeg_path}")
|
|
299
|
+
else:
|
|
300
|
+
print("No ICC profile found, assuming sRGB.")
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
print(f"Color Management Error: {e}. Returning original raw image.")
|
|
304
|
+
|
|
305
|
+
return img
|
|
306
|
+
|
|
307
|
+
# --- Example Usage ---
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
result_img = decode_jpeg_with_color_management('icc_profile.jpg')
|
|
310
|
+
result_img.show()
|
|
311
|
+
# result_img.save('output_srgb.jpg', quality=95)
|
|
312
|
+
```
|
|
313
|
+
|
|
242
314
|
## High-Precision JPEG Support
|
|
243
315
|
|
|
244
316
|
PyTurboJPEG 2.0+ supports 12-bit and 16-bit precision JPEG encoding and decoding using libjpeg-turbo 3.0+ APIs. This feature is ideal for medical imaging, scientific photography, and other applications requiring higher bit depth.
|
|
@@ -278,7 +350,34 @@ with open('output_12bit.jpg', 'rb') as f:
|
|
|
278
350
|
decoded_from_file = jpeg.decode_12bit(f.read())
|
|
279
351
|
```
|
|
280
352
|
|
|
281
|
-
###
|
|
353
|
+
### Lossless JPEG for 12-bit and 16-bit
|
|
354
|
+
|
|
355
|
+
12-bit and 16-bit JPEG support lossless compression for perfect reconstruction:
|
|
356
|
+
|
|
357
|
+
#### 12-bit Lossless JPEG
|
|
358
|
+
|
|
359
|
+
12-bit precision with lossless compression:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import numpy as np
|
|
363
|
+
from turbojpeg import TurboJPEG
|
|
364
|
+
|
|
365
|
+
jpeg = TurboJPEG()
|
|
366
|
+
|
|
367
|
+
# Create 12-bit image
|
|
368
|
+
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
|
|
369
|
+
|
|
370
|
+
# Encode to 12-bit lossless JPEG using encode_12bit() with lossless=True
|
|
371
|
+
jpeg_data = jpeg.encode_12bit(img_12bit, lossless=True)
|
|
372
|
+
|
|
373
|
+
# Decode using decode_12bit()
|
|
374
|
+
decoded_img = jpeg.decode_12bit(jpeg_data)
|
|
375
|
+
|
|
376
|
+
# Perfect reconstruction
|
|
377
|
+
assert np.array_equal(img_12bit, decoded_img) # True
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### 16-bit Lossless JPEG
|
|
282
381
|
|
|
283
382
|
16-bit JPEG provides the highest precision with perfect reconstruction through lossless compression. The JPEG standard only supports 16-bit for lossless mode.
|
|
284
383
|
|
|
@@ -309,33 +408,6 @@ with open('output_16bit_lossless.jpg', 'rb') as f:
|
|
|
309
408
|
decoded_from_file = jpeg.decode_16bit(f.read())
|
|
310
409
|
```
|
|
311
410
|
|
|
312
|
-
### Lossless JPEG for 12-bit and 16-bit
|
|
313
|
-
|
|
314
|
-
12-bit and 16-bit JPEG support lossless compression for perfect reconstruction:
|
|
315
|
-
|
|
316
|
-
#### 12-bit Lossless JPEG
|
|
317
|
-
|
|
318
|
-
12-bit precision with lossless compression:
|
|
319
|
-
|
|
320
|
-
```python
|
|
321
|
-
import numpy as np
|
|
322
|
-
from turbojpeg import TurboJPEG
|
|
323
|
-
|
|
324
|
-
jpeg = TurboJPEG()
|
|
325
|
-
|
|
326
|
-
# Create 12-bit image
|
|
327
|
-
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
|
|
328
|
-
|
|
329
|
-
# Encode to 12-bit lossless JPEG using encode_12bit() with lossless=True
|
|
330
|
-
jpeg_data = jpeg.encode_12bit(img_12bit, lossless=True)
|
|
331
|
-
|
|
332
|
-
# Decode using decode_12bit()
|
|
333
|
-
decoded_img = jpeg.decode_12bit(jpeg_data)
|
|
334
|
-
|
|
335
|
-
# Perfect reconstruction
|
|
336
|
-
assert np.array_equal(img_12bit, decoded_img) # True
|
|
337
|
-
```
|
|
338
|
-
|
|
339
411
|
### Medical and Scientific Imaging
|
|
340
412
|
|
|
341
413
|
For medical and scientific applications, 12-bit JPEG provides excellent precision while maintaining file size efficiency:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyTurboJPEG
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A Python wrapper of libjpeg-turbo for decoding and encoding JPEG image.
|
|
5
5
|
Home-page: https://github.com/lilohuang/PyTurboJPEG
|
|
6
6
|
Author: Lilo Huang
|
|
@@ -26,6 +26,11 @@ Dynamic: summary
|
|
|
26
26
|
|
|
27
27
|
A Python wrapper for libjpeg-turbo that enables efficient JPEG image decoding and encoding.
|
|
28
28
|
|
|
29
|
+
[](https://pypi.org/project/pyturbojpeg/)
|
|
30
|
+

|
|
31
|
+
[](https://pypistats.org/packages/pyturbojpeg)
|
|
32
|
+
[](https://github.com/lilohuang/PyTurboJPEG/blob/master/LICENSE)
|
|
33
|
+
|
|
29
34
|
## Prerequisites
|
|
30
35
|
|
|
31
36
|
- [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo/releases) **3.0 or later** (required for PyTurboJPEG 2.0+)
|
|
@@ -239,6 +244,73 @@ cv2.imshow('transposed_image', transposed_image)
|
|
|
239
244
|
cv2.waitKey(0)
|
|
240
245
|
```
|
|
241
246
|
|
|
247
|
+
### ICC Color Management Workflow
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import io
|
|
251
|
+
import numpy as np
|
|
252
|
+
from PIL import Image, ImageCms
|
|
253
|
+
from turbojpeg import TurboJPEG, TJPF_BGR
|
|
254
|
+
|
|
255
|
+
def decode_jpeg_with_color_management(jpeg_path):
|
|
256
|
+
"""
|
|
257
|
+
Decodes a JPEG and applies color management (ICC Profile to sRGB).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
jpeg_path (str): Path to the input JPEG file.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
PIL.Image: The color-corrected sRGB Image object.
|
|
264
|
+
"""
|
|
265
|
+
# 1. Initialize TurboJPEG
|
|
266
|
+
jpeg = TurboJPEG()
|
|
267
|
+
|
|
268
|
+
with open(jpeg_path, 'rb') as f:
|
|
269
|
+
jpeg_data = f.read()
|
|
270
|
+
|
|
271
|
+
# 2. Get image headers and decode pixels
|
|
272
|
+
# Using TJPF_BGR format (OpenCV standard) for the raw buffer
|
|
273
|
+
width, height, _, _ = jpeg.decode_header(jpeg_data)
|
|
274
|
+
pixels = jpeg.decode(jpeg_data, pixel_format=TJPF_BGR)
|
|
275
|
+
|
|
276
|
+
# 3. Encapsulate into a Pillow Image object
|
|
277
|
+
# Key: Use 'raw' and 'BGR' decoder to correctly map BGR bytes to an RGB Image object
|
|
278
|
+
img = Image.frombytes('RGB', (width, height), pixels, 'raw', 'BGR')
|
|
279
|
+
|
|
280
|
+
# 4. Handle ICC Profile transformation
|
|
281
|
+
try:
|
|
282
|
+
# Extract embedded ICC Profile
|
|
283
|
+
icc_profile = jpeg.get_icc_profile(jpeg_data)
|
|
284
|
+
|
|
285
|
+
if icc_profile:
|
|
286
|
+
# Create Source and Destination Profile objects
|
|
287
|
+
src_profile = ImageCms.getOpenProfile(io.BytesIO(icc_profile))
|
|
288
|
+
dst_profile = ImageCms.createProfile("sRGB")
|
|
289
|
+
|
|
290
|
+
# Perform color transformation (similar to "Convert to Profile" in Photoshop)
|
|
291
|
+
# This step recalculates pixel values to align with sRGB standards
|
|
292
|
+
img = ImageCms.profileToProfile(
|
|
293
|
+
img,
|
|
294
|
+
src_profile,
|
|
295
|
+
dst_profile,
|
|
296
|
+
outputMode='RGB'
|
|
297
|
+
)
|
|
298
|
+
print(f"Successfully applied ICC profile from {jpeg_path}")
|
|
299
|
+
else:
|
|
300
|
+
print("No ICC profile found, assuming sRGB.")
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
print(f"Color Management Error: {e}. Returning original raw image.")
|
|
304
|
+
|
|
305
|
+
return img
|
|
306
|
+
|
|
307
|
+
# --- Example Usage ---
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
result_img = decode_jpeg_with_color_management('icc_profile.jpg')
|
|
310
|
+
result_img.show()
|
|
311
|
+
# result_img.save('output_srgb.jpg', quality=95)
|
|
312
|
+
```
|
|
313
|
+
|
|
242
314
|
## High-Precision JPEG Support
|
|
243
315
|
|
|
244
316
|
PyTurboJPEG 2.0+ supports 12-bit and 16-bit precision JPEG encoding and decoding using libjpeg-turbo 3.0+ APIs. This feature is ideal for medical imaging, scientific photography, and other applications requiring higher bit depth.
|
|
@@ -278,7 +350,34 @@ with open('output_12bit.jpg', 'rb') as f:
|
|
|
278
350
|
decoded_from_file = jpeg.decode_12bit(f.read())
|
|
279
351
|
```
|
|
280
352
|
|
|
281
|
-
###
|
|
353
|
+
### Lossless JPEG for 12-bit and 16-bit
|
|
354
|
+
|
|
355
|
+
12-bit and 16-bit JPEG support lossless compression for perfect reconstruction:
|
|
356
|
+
|
|
357
|
+
#### 12-bit Lossless JPEG
|
|
358
|
+
|
|
359
|
+
12-bit precision with lossless compression:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import numpy as np
|
|
363
|
+
from turbojpeg import TurboJPEG
|
|
364
|
+
|
|
365
|
+
jpeg = TurboJPEG()
|
|
366
|
+
|
|
367
|
+
# Create 12-bit image
|
|
368
|
+
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
|
|
369
|
+
|
|
370
|
+
# Encode to 12-bit lossless JPEG using encode_12bit() with lossless=True
|
|
371
|
+
jpeg_data = jpeg.encode_12bit(img_12bit, lossless=True)
|
|
372
|
+
|
|
373
|
+
# Decode using decode_12bit()
|
|
374
|
+
decoded_img = jpeg.decode_12bit(jpeg_data)
|
|
375
|
+
|
|
376
|
+
# Perfect reconstruction
|
|
377
|
+
assert np.array_equal(img_12bit, decoded_img) # True
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### 16-bit Lossless JPEG
|
|
282
381
|
|
|
283
382
|
16-bit JPEG provides the highest precision with perfect reconstruction through lossless compression. The JPEG standard only supports 16-bit for lossless mode.
|
|
284
383
|
|
|
@@ -309,33 +408,6 @@ with open('output_16bit_lossless.jpg', 'rb') as f:
|
|
|
309
408
|
decoded_from_file = jpeg.decode_16bit(f.read())
|
|
310
409
|
```
|
|
311
410
|
|
|
312
|
-
### Lossless JPEG for 12-bit and 16-bit
|
|
313
|
-
|
|
314
|
-
12-bit and 16-bit JPEG support lossless compression for perfect reconstruction:
|
|
315
|
-
|
|
316
|
-
#### 12-bit Lossless JPEG
|
|
317
|
-
|
|
318
|
-
12-bit precision with lossless compression:
|
|
319
|
-
|
|
320
|
-
```python
|
|
321
|
-
import numpy as np
|
|
322
|
-
from turbojpeg import TurboJPEG
|
|
323
|
-
|
|
324
|
-
jpeg = TurboJPEG()
|
|
325
|
-
|
|
326
|
-
# Create 12-bit image
|
|
327
|
-
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
|
|
328
|
-
|
|
329
|
-
# Encode to 12-bit lossless JPEG using encode_12bit() with lossless=True
|
|
330
|
-
jpeg_data = jpeg.encode_12bit(img_12bit, lossless=True)
|
|
331
|
-
|
|
332
|
-
# Decode using decode_12bit()
|
|
333
|
-
decoded_img = jpeg.decode_12bit(jpeg_data)
|
|
334
|
-
|
|
335
|
-
# Perfect reconstruction
|
|
336
|
-
assert np.array_equal(img_12bit, decoded_img) # True
|
|
337
|
-
```
|
|
338
|
-
|
|
339
411
|
### Medical and Scientific Imaging
|
|
340
412
|
|
|
341
413
|
For medical and scientific applications, 12-bit JPEG provides excellent precision while maintaining file size efficiency:
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
A Python wrapper for libjpeg-turbo that enables efficient JPEG image decoding and encoding.
|
|
4
4
|
|
|
5
|
+
[](https://pypi.org/project/pyturbojpeg/)
|
|
6
|
+

|
|
7
|
+
[](https://pypistats.org/packages/pyturbojpeg)
|
|
8
|
+
[](https://github.com/lilohuang/PyTurboJPEG/blob/master/LICENSE)
|
|
9
|
+
|
|
5
10
|
## Prerequisites
|
|
6
11
|
|
|
7
12
|
- [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo/releases) **3.0 or later** (required for PyTurboJPEG 2.0+)
|
|
@@ -215,6 +220,73 @@ cv2.imshow('transposed_image', transposed_image)
|
|
|
215
220
|
cv2.waitKey(0)
|
|
216
221
|
```
|
|
217
222
|
|
|
223
|
+
### ICC Color Management Workflow
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
import io
|
|
227
|
+
import numpy as np
|
|
228
|
+
from PIL import Image, ImageCms
|
|
229
|
+
from turbojpeg import TurboJPEG, TJPF_BGR
|
|
230
|
+
|
|
231
|
+
def decode_jpeg_with_color_management(jpeg_path):
|
|
232
|
+
"""
|
|
233
|
+
Decodes a JPEG and applies color management (ICC Profile to sRGB).
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
jpeg_path (str): Path to the input JPEG file.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
PIL.Image: The color-corrected sRGB Image object.
|
|
240
|
+
"""
|
|
241
|
+
# 1. Initialize TurboJPEG
|
|
242
|
+
jpeg = TurboJPEG()
|
|
243
|
+
|
|
244
|
+
with open(jpeg_path, 'rb') as f:
|
|
245
|
+
jpeg_data = f.read()
|
|
246
|
+
|
|
247
|
+
# 2. Get image headers and decode pixels
|
|
248
|
+
# Using TJPF_BGR format (OpenCV standard) for the raw buffer
|
|
249
|
+
width, height, _, _ = jpeg.decode_header(jpeg_data)
|
|
250
|
+
pixels = jpeg.decode(jpeg_data, pixel_format=TJPF_BGR)
|
|
251
|
+
|
|
252
|
+
# 3. Encapsulate into a Pillow Image object
|
|
253
|
+
# Key: Use 'raw' and 'BGR' decoder to correctly map BGR bytes to an RGB Image object
|
|
254
|
+
img = Image.frombytes('RGB', (width, height), pixels, 'raw', 'BGR')
|
|
255
|
+
|
|
256
|
+
# 4. Handle ICC Profile transformation
|
|
257
|
+
try:
|
|
258
|
+
# Extract embedded ICC Profile
|
|
259
|
+
icc_profile = jpeg.get_icc_profile(jpeg_data)
|
|
260
|
+
|
|
261
|
+
if icc_profile:
|
|
262
|
+
# Create Source and Destination Profile objects
|
|
263
|
+
src_profile = ImageCms.getOpenProfile(io.BytesIO(icc_profile))
|
|
264
|
+
dst_profile = ImageCms.createProfile("sRGB")
|
|
265
|
+
|
|
266
|
+
# Perform color transformation (similar to "Convert to Profile" in Photoshop)
|
|
267
|
+
# This step recalculates pixel values to align with sRGB standards
|
|
268
|
+
img = ImageCms.profileToProfile(
|
|
269
|
+
img,
|
|
270
|
+
src_profile,
|
|
271
|
+
dst_profile,
|
|
272
|
+
outputMode='RGB'
|
|
273
|
+
)
|
|
274
|
+
print(f"Successfully applied ICC profile from {jpeg_path}")
|
|
275
|
+
else:
|
|
276
|
+
print("No ICC profile found, assuming sRGB.")
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
print(f"Color Management Error: {e}. Returning original raw image.")
|
|
280
|
+
|
|
281
|
+
return img
|
|
282
|
+
|
|
283
|
+
# --- Example Usage ---
|
|
284
|
+
if __name__ == "__main__":
|
|
285
|
+
result_img = decode_jpeg_with_color_management('icc_profile.jpg')
|
|
286
|
+
result_img.show()
|
|
287
|
+
# result_img.save('output_srgb.jpg', quality=95)
|
|
288
|
+
```
|
|
289
|
+
|
|
218
290
|
## High-Precision JPEG Support
|
|
219
291
|
|
|
220
292
|
PyTurboJPEG 2.0+ supports 12-bit and 16-bit precision JPEG encoding and decoding using libjpeg-turbo 3.0+ APIs. This feature is ideal for medical imaging, scientific photography, and other applications requiring higher bit depth.
|
|
@@ -254,7 +326,34 @@ with open('output_12bit.jpg', 'rb') as f:
|
|
|
254
326
|
decoded_from_file = jpeg.decode_12bit(f.read())
|
|
255
327
|
```
|
|
256
328
|
|
|
257
|
-
###
|
|
329
|
+
### Lossless JPEG for 12-bit and 16-bit
|
|
330
|
+
|
|
331
|
+
12-bit and 16-bit JPEG support lossless compression for perfect reconstruction:
|
|
332
|
+
|
|
333
|
+
#### 12-bit Lossless JPEG
|
|
334
|
+
|
|
335
|
+
12-bit precision with lossless compression:
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
import numpy as np
|
|
339
|
+
from turbojpeg import TurboJPEG
|
|
340
|
+
|
|
341
|
+
jpeg = TurboJPEG()
|
|
342
|
+
|
|
343
|
+
# Create 12-bit image
|
|
344
|
+
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
|
|
345
|
+
|
|
346
|
+
# Encode to 12-bit lossless JPEG using encode_12bit() with lossless=True
|
|
347
|
+
jpeg_data = jpeg.encode_12bit(img_12bit, lossless=True)
|
|
348
|
+
|
|
349
|
+
# Decode using decode_12bit()
|
|
350
|
+
decoded_img = jpeg.decode_12bit(jpeg_data)
|
|
351
|
+
|
|
352
|
+
# Perfect reconstruction
|
|
353
|
+
assert np.array_equal(img_12bit, decoded_img) # True
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### 16-bit Lossless JPEG
|
|
258
357
|
|
|
259
358
|
16-bit JPEG provides the highest precision with perfect reconstruction through lossless compression. The JPEG standard only supports 16-bit for lossless mode.
|
|
260
359
|
|
|
@@ -285,33 +384,6 @@ with open('output_16bit_lossless.jpg', 'rb') as f:
|
|
|
285
384
|
decoded_from_file = jpeg.decode_16bit(f.read())
|
|
286
385
|
```
|
|
287
386
|
|
|
288
|
-
### Lossless JPEG for 12-bit and 16-bit
|
|
289
|
-
|
|
290
|
-
12-bit and 16-bit JPEG support lossless compression for perfect reconstruction:
|
|
291
|
-
|
|
292
|
-
#### 12-bit Lossless JPEG
|
|
293
|
-
|
|
294
|
-
12-bit precision with lossless compression:
|
|
295
|
-
|
|
296
|
-
```python
|
|
297
|
-
import numpy as np
|
|
298
|
-
from turbojpeg import TurboJPEG
|
|
299
|
-
|
|
300
|
-
jpeg = TurboJPEG()
|
|
301
|
-
|
|
302
|
-
# Create 12-bit image
|
|
303
|
-
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
|
|
304
|
-
|
|
305
|
-
# Encode to 12-bit lossless JPEG using encode_12bit() with lossless=True
|
|
306
|
-
jpeg_data = jpeg.encode_12bit(img_12bit, lossless=True)
|
|
307
|
-
|
|
308
|
-
# Decode using decode_12bit()
|
|
309
|
-
decoded_img = jpeg.decode_12bit(jpeg_data)
|
|
310
|
-
|
|
311
|
-
# Perfect reconstruction
|
|
312
|
-
assert np.array_equal(img_12bit, decoded_img) # True
|
|
313
|
-
```
|
|
314
|
-
|
|
315
387
|
### Medical and Scientific Imaging
|
|
316
388
|
|
|
317
389
|
For medical and scientific applications, 12-bit JPEG provides excellent precision while maintaining file size efficiency:
|
|
@@ -2,7 +2,7 @@ import io
|
|
|
2
2
|
from setuptools import setup, find_packages
|
|
3
3
|
setup(
|
|
4
4
|
name='PyTurboJPEG',
|
|
5
|
-
version='2.
|
|
5
|
+
version='2.2.0',
|
|
6
6
|
description='A Python wrapper of libjpeg-turbo for decoding and encoding JPEG image.',
|
|
7
7
|
author='Lilo Huang',
|
|
8
8
|
author_email='kuso.cc@gmail.com',
|
|
@@ -30,7 +30,8 @@ from turbojpeg import (
|
|
|
30
30
|
TJSAMP_444, TJSAMP_422, TJSAMP_420, TJSAMP_GRAY, TJSAMP_440, TJSAMP_411,
|
|
31
31
|
TJCS_RGB, TJCS_YCbCr, TJCS_GRAY,
|
|
32
32
|
TJFLAG_PROGRESSIVE, TJFLAG_FASTUPSAMPLE, TJFLAG_FASTDCT,
|
|
33
|
-
TJPRECISION_8, TJPRECISION_12, TJPRECISION_16
|
|
33
|
+
TJPRECISION_8, TJPRECISION_12, TJPRECISION_16,
|
|
34
|
+
TJPARAM_SAVEMARKERS
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
|
|
@@ -1802,5 +1803,65 @@ class TestLosslessJPEG:
|
|
|
1802
1803
|
assert np.array_equal(img_gray, decoded_gray), "Lossless grayscale should be perfect"
|
|
1803
1804
|
|
|
1804
1805
|
|
|
1806
|
+
import struct
|
|
1807
|
+
|
|
1808
|
+
def _make_minimal_srgb_icc():
|
|
1809
|
+
"""Build a minimal but structurally valid ICC profile for sRGB."""
|
|
1810
|
+
profile = bytearray(132)
|
|
1811
|
+
struct.pack_into('>I', profile, 0, 132)
|
|
1812
|
+
struct.pack_into('>I', profile, 8, 0x02100000)
|
|
1813
|
+
profile[12:16] = b'mntr'
|
|
1814
|
+
profile[16:20] = b'RGB '
|
|
1815
|
+
profile[20:24] = b'XYZ '
|
|
1816
|
+
profile[36:40] = b'acsp'
|
|
1817
|
+
return bytes(profile)
|
|
1818
|
+
|
|
1819
|
+
MINIMAL_SRGB_ICC = _make_minimal_srgb_icc()
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
class TestICCProfile:
|
|
1823
|
+
"""Test ICC profile embed/extract functionality (requires TurboJPEG 3.1+)."""
|
|
1824
|
+
|
|
1825
|
+
@pytest.fixture(autouse=True)
|
|
1826
|
+
def require_icc_support(self, jpeg_instance):
|
|
1827
|
+
"""Skip the test if the loaded libturbojpeg does not support ICC profiles."""
|
|
1828
|
+
try:
|
|
1829
|
+
jpeg_instance.get_icc_profile(b'\xff\xd8\xff\xd9')
|
|
1830
|
+
except NotImplementedError as e:
|
|
1831
|
+
pytest.skip(str(e))
|
|
1832
|
+
except (OSError, Exception):
|
|
1833
|
+
pass # Header parse error is fine — the function exists
|
|
1834
|
+
|
|
1835
|
+
def test_get_icc_profile_with_embedded_profile(self, jpeg_instance, sample_bgr_image):
|
|
1836
|
+
"""Test that get_icc_profile returns non-empty bytes for a JPEG with ICC profile."""
|
|
1837
|
+
jpeg_with_icc = jpeg_instance.encode(
|
|
1838
|
+
sample_bgr_image, quality=85, icc_profile=MINIMAL_SRGB_ICC)
|
|
1839
|
+
icc = jpeg_instance.get_icc_profile(jpeg_with_icc)
|
|
1840
|
+
assert icc is not None, "Expected ICC profile but got None"
|
|
1841
|
+
assert isinstance(icc, bytes), "ICC profile should be bytes"
|
|
1842
|
+
assert len(icc) > 0, "ICC profile should be non-empty"
|
|
1843
|
+
assert icc == MINIMAL_SRGB_ICC, "Extracted ICC profile should match embedded profile"
|
|
1844
|
+
|
|
1845
|
+
def test_get_icc_profile_without_profile(self, jpeg_instance, sample_bgr_image):
|
|
1846
|
+
"""Test that get_icc_profile returns None for a JPEG without ICC profile."""
|
|
1847
|
+
jpeg_without_icc = jpeg_instance.encode(sample_bgr_image, quality=85)
|
|
1848
|
+
icc = jpeg_instance.get_icc_profile(jpeg_without_icc)
|
|
1849
|
+
assert icc is None, \
|
|
1850
|
+
"Expected None for JPEG without ICC profile"
|
|
1851
|
+
|
|
1852
|
+
def test_icc_profile_roundtrip(self, jpeg_instance, sample_bgr_image):
|
|
1853
|
+
"""Test ICC profile survives a full encode→decode_header roundtrip."""
|
|
1854
|
+
jpeg_data = jpeg_instance.encode(
|
|
1855
|
+
sample_bgr_image, quality=95,
|
|
1856
|
+
jpeg_subsample=TJSAMP_444,
|
|
1857
|
+
icc_profile=MINIMAL_SRGB_ICC)
|
|
1858
|
+
assert jpeg_data is not None
|
|
1859
|
+
assert len(jpeg_data) > 0
|
|
1860
|
+
extracted_icc = jpeg_instance.get_icc_profile(jpeg_data)
|
|
1861
|
+
assert extracted_icc is not None, "ICC profile missing after roundtrip"
|
|
1862
|
+
assert extracted_icc == MINIMAL_SRGB_ICC, \
|
|
1863
|
+
f"ICC profile mismatch: expected {len(MINIMAL_SRGB_ICC)} bytes, got {len(extracted_icc) if extracted_icc else 0}"
|
|
1864
|
+
|
|
1865
|
+
|
|
1805
1866
|
if __name__ == '__main__':
|
|
1806
1867
|
pytest.main([__file__, '-v'])
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
# SOFTWARE.
|
|
24
24
|
|
|
25
25
|
__author__ = 'Lilo Huang <kuso.cc@gmail.com>'
|
|
26
|
-
__version__ = '2.
|
|
26
|
+
__version__ = '2.2.0'
|
|
27
27
|
|
|
28
28
|
from ctypes import *
|
|
29
29
|
from ctypes.util import find_library
|
|
@@ -192,6 +192,7 @@ TJPARAM_YDENSITY = 21
|
|
|
192
192
|
TJPARAM_DENSITYUNITS = 22
|
|
193
193
|
TJPARAM_MAXPIXELS = 23
|
|
194
194
|
TJPARAM_MAXMEMORY = 24
|
|
195
|
+
TJPARAM_SAVEMARKERS = 25
|
|
195
196
|
|
|
196
197
|
class CroppingRegion(Structure):
|
|
197
198
|
_fields_ = [("x", c_int), ("y", c_int), ("w", c_int), ("h", c_int)]
|
|
@@ -328,14 +329,12 @@ def fill_background(coeffs_ptr, arrayRegion, planeRegion, componentID, transform
|
|
|
328
329
|
|
|
329
330
|
return 1
|
|
330
331
|
|
|
331
|
-
|
|
332
332
|
def split_byte_into_nibbles(value):
|
|
333
333
|
"""Split byte int into 2 nibbles (4 bits)."""
|
|
334
334
|
first = value >> 4
|
|
335
335
|
second = value & 0x0F
|
|
336
336
|
return first, second
|
|
337
337
|
|
|
338
|
-
|
|
339
338
|
class TurboJPEG(object):
|
|
340
339
|
"""A Python wrapper of libjpeg-turbo for decoding and encoding JPEG image."""
|
|
341
340
|
def __init__(self, lib_path=None):
|
|
@@ -489,6 +488,17 @@ class TurboJPEG(object):
|
|
|
489
488
|
self.__decompress16.argtypes = [
|
|
490
489
|
c_void_p, POINTER(c_ubyte), c_size_t, POINTER(c_ushort), c_int, c_int]
|
|
491
490
|
self.__decompress16.restype = c_int
|
|
491
|
+
|
|
492
|
+
# tjGetScalingFactors
|
|
493
|
+
get_scaling_factors = turbo_jpeg.tjGetScalingFactors
|
|
494
|
+
get_scaling_factors.argtypes = [POINTER(c_int)]
|
|
495
|
+
get_scaling_factors.restype = POINTER(ScalingFactor)
|
|
496
|
+
num_scaling_factors = c_int()
|
|
497
|
+
scaling_factors = get_scaling_factors(byref(num_scaling_factors))
|
|
498
|
+
self.__scaling_factors = frozenset(
|
|
499
|
+
(scaling_factors[i].num, scaling_factors[i].denom)
|
|
500
|
+
for i in range(num_scaling_factors.value)
|
|
501
|
+
)
|
|
492
502
|
|
|
493
503
|
# tj3CompressFromYUV16 - compress 16-bit YUV to JPEG (TurboJPEG 3.1+)
|
|
494
504
|
# These functions may not be available in all TurboJPEG 3.x versions
|
|
@@ -519,16 +529,21 @@ class TurboJPEG(object):
|
|
|
519
529
|
except AttributeError:
|
|
520
530
|
self.__decompressToYUVPlanes16 = None
|
|
521
531
|
|
|
522
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
+
# tj3GetICCProfile - retrieve ICC profile from decompressor after header parsing (TurboJPEG 3.1+)
|
|
533
|
+
try:
|
|
534
|
+
self.__get_icc_profile = turbo_jpeg.tj3GetICCProfile
|
|
535
|
+
self.__get_icc_profile.argtypes = [c_void_p, POINTER(c_void_p), POINTER(c_size_t)]
|
|
536
|
+
self.__get_icc_profile.restype = c_int
|
|
537
|
+
except AttributeError:
|
|
538
|
+
self.__get_icc_profile = None
|
|
539
|
+
|
|
540
|
+
# tj3SetICCProfile - attach ICC profile to compressor before compression (TurboJPEG 3.1+)
|
|
541
|
+
try:
|
|
542
|
+
self.__set_icc_profile = turbo_jpeg.tj3SetICCProfile
|
|
543
|
+
self.__set_icc_profile.argtypes = [c_void_p, c_void_p, c_size_t]
|
|
544
|
+
self.__set_icc_profile.restype = c_int
|
|
545
|
+
except AttributeError:
|
|
546
|
+
self.__set_icc_profile = None
|
|
532
547
|
|
|
533
548
|
def decode_header(self, jpeg_buf, return_precision=False):
|
|
534
549
|
"""decodes JPEG header and returns image properties as a tuple.
|
|
@@ -592,6 +607,101 @@ class TurboJPEG(object):
|
|
|
592
607
|
finally:
|
|
593
608
|
self.__destroy(handle)
|
|
594
609
|
|
|
610
|
+
def get_icc_profile(self, jpeg_buf):
|
|
611
|
+
"""Extracts the embedded ICC color profile from a JPEG image.
|
|
612
|
+
|
|
613
|
+
Requires TurboJPEG 3.1 or later with tj3GetICCProfile support.
|
|
614
|
+
|
|
615
|
+
Parameters
|
|
616
|
+
----------
|
|
617
|
+
jpeg_buf : bytes
|
|
618
|
+
JPEG image data buffer containing an embedded ICC profile.
|
|
619
|
+
|
|
620
|
+
Returns
|
|
621
|
+
-------
|
|
622
|
+
bytes or None
|
|
623
|
+
Raw ICC profile data as a bytes object, or None if no ICC profile
|
|
624
|
+
is present in the JPEG stream.
|
|
625
|
+
|
|
626
|
+
Raises
|
|
627
|
+
------
|
|
628
|
+
OSError
|
|
629
|
+
If the JPEG header cannot be parsed or a fatal error occurs.
|
|
630
|
+
NotImplementedError
|
|
631
|
+
If the loaded libturbojpeg does not export tj3GetICCProfile.
|
|
632
|
+
|
|
633
|
+
Examples
|
|
634
|
+
--------
|
|
635
|
+
>>> jpeg = TurboJPEG()
|
|
636
|
+
>>> with open('photo_with_icc.jpg', 'rb') as f:
|
|
637
|
+
... data = f.read()
|
|
638
|
+
>>> icc = jpeg.get_icc_profile(data)
|
|
639
|
+
>>> if icc:
|
|
640
|
+
... print(f'ICC profile size: {len(icc)} bytes')
|
|
641
|
+
"""
|
|
642
|
+
if self.__get_icc_profile is None:
|
|
643
|
+
raise NotImplementedError(
|
|
644
|
+
'tj3GetICCProfile is not available in the loaded libturbojpeg. '
|
|
645
|
+
'Please upgrade to libjpeg-turbo 3.1 or later.')
|
|
646
|
+
handle = self.__init(TJINIT_DECOMPRESS)
|
|
647
|
+
try:
|
|
648
|
+
# Set TJPARAM_SAVEMARKERS to 2 (APP2) so the decompressor
|
|
649
|
+
# retains ICC profile markers during header parsing.
|
|
650
|
+
if self.__set(handle, TJPARAM_SAVEMARKERS, 2) != 0:
|
|
651
|
+
self.__report_error(handle)
|
|
652
|
+
jpeg_array = np.frombuffer(jpeg_buf, dtype=np.uint8)
|
|
653
|
+
src_addr = self.__getaddr(jpeg_array)
|
|
654
|
+
status = self.__decompress_header(handle, src_addr, jpeg_array.size)
|
|
655
|
+
if status != 0:
|
|
656
|
+
self.__report_error(handle)
|
|
657
|
+
icc_buf = c_void_p()
|
|
658
|
+
icc_size = c_size_t()
|
|
659
|
+
status = self.__get_icc_profile(handle, byref(icc_buf), byref(icc_size))
|
|
660
|
+
if status != 0:
|
|
661
|
+
# A non-fatal return (e.g. no profile present) should return None
|
|
662
|
+
err_code = self.__get_error_code(handle)
|
|
663
|
+
if err_code == TJERR_WARNING:
|
|
664
|
+
return None
|
|
665
|
+
self.__report_error(handle)
|
|
666
|
+
if icc_buf.value is None or icc_size.value == 0:
|
|
667
|
+
return None
|
|
668
|
+
result = self.__copy_from_buffer(icc_buf.value, icc_size.value)
|
|
669
|
+
self.__free(icc_buf)
|
|
670
|
+
return result
|
|
671
|
+
finally:
|
|
672
|
+
self.__destroy(handle)
|
|
673
|
+
|
|
674
|
+
def set_icc_profile(self, handle, icc_buf):
|
|
675
|
+
"""Attaches an ICC color profile to an active compressor handle.
|
|
676
|
+
|
|
677
|
+
This is a low-level helper intended for use when building custom
|
|
678
|
+
compression pipelines. In most cases, use encode() with the
|
|
679
|
+
icc_profile parameter instead.
|
|
680
|
+
|
|
681
|
+
Parameters
|
|
682
|
+
----------
|
|
683
|
+
handle : ctypes void pointer
|
|
684
|
+
An active TurboJPEG compressor handle (TJINIT_COMPRESS).
|
|
685
|
+
icc_buf : bytes
|
|
686
|
+
Raw ICC profile data to embed.
|
|
687
|
+
|
|
688
|
+
Raises
|
|
689
|
+
------
|
|
690
|
+
OSError
|
|
691
|
+
If tj3SetICCProfile returns a non-zero status.
|
|
692
|
+
NotImplementedError
|
|
693
|
+
If the loaded libturbojpeg does not export tj3SetICCProfile.
|
|
694
|
+
"""
|
|
695
|
+
if self.__set_icc_profile is None:
|
|
696
|
+
raise NotImplementedError(
|
|
697
|
+
'tj3SetICCProfile is not available in the loaded libturbojpeg. '
|
|
698
|
+
'Please upgrade to libjpeg-turbo 3.1 or later.')
|
|
699
|
+
icc_array = np.frombuffer(icc_buf, dtype=np.uint8)
|
|
700
|
+
icc_addr = self.__getaddr(icc_array)
|
|
701
|
+
status = self.__set_icc_profile(handle, icc_addr, len(icc_buf))
|
|
702
|
+
if status != 0:
|
|
703
|
+
self.__report_error(handle)
|
|
704
|
+
|
|
595
705
|
def decode(self, jpeg_buf, pixel_format=TJPF_BGR, scaling_factor=None, flags=0, dst=None):
|
|
596
706
|
"""decodes JPEG memory buffer to numpy array.
|
|
597
707
|
|
|
@@ -707,7 +817,7 @@ class TurboJPEG(object):
|
|
|
707
817
|
finally:
|
|
708
818
|
self.__destroy(handle)
|
|
709
819
|
|
|
710
|
-
def encode(self, img_array, quality=85, pixel_format=TJPF_BGR, jpeg_subsample=TJSAMP_422, flags=0, dst=None, lossless=False):
|
|
820
|
+
def encode(self, img_array, quality=85, pixel_format=TJPF_BGR, jpeg_subsample=TJSAMP_422, flags=0, dst=None, lossless=False, icc_profile=None):
|
|
711
821
|
"""encodes numpy array to JPEG memory buffer.
|
|
712
822
|
|
|
713
823
|
Parameters
|
|
@@ -729,6 +839,9 @@ class TurboJPEG(object):
|
|
|
729
839
|
When True, provides perfect reconstruction with larger file sizes.
|
|
730
840
|
Note: quality and jpeg_subsample parameters are ignored in lossless mode;
|
|
731
841
|
subsampling is automatically set to 4:4:4 by the library.
|
|
842
|
+
icc_profile : bytes or None
|
|
843
|
+
Raw ICC profile data to embed in the JPEG (optional).
|
|
844
|
+
Requires TurboJPEG 3.1 or later with tj3SetICCProfile support.
|
|
732
845
|
|
|
733
846
|
Returns
|
|
734
847
|
-------
|
|
@@ -763,6 +876,9 @@ class TurboJPEG(object):
|
|
|
763
876
|
if img_array.dtype != np.uint8:
|
|
764
877
|
raise ValueError('encode() requires uint8 array (values 0-255); use encode_12bit() for 12-bit images (uint16, 0-4095) or encode_16bit() for 16-bit images (uint16, 0-65535)')
|
|
765
878
|
|
|
879
|
+
if icc_profile is not None:
|
|
880
|
+
self.set_icc_profile(handle, icc_profile)
|
|
881
|
+
|
|
766
882
|
if dst is not None and not self.__is_buffer(dst):
|
|
767
883
|
raise TypeError('\'dst\' argument must support buffer protocol')
|
|
768
884
|
if (dst is not None and
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|