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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyTurboJPEG
3
- Version: 2.1.0
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
+ [![PyPI Version](https://img.shields.io/pypi/v/pyturbojpeg.svg?style=flat-square&color=blue)](https://pypi.org/project/pyturbojpeg/)
30
+ ![Python Version](https://img.shields.io/badge/python-3.6+-blue?logo=python&logoColor=white)
31
+ [![Downloads](https://img.shields.io/pypi/dm/pyturbojpeg.svg?style=flat-square&color=orange)](https://pypistats.org/packages/pyturbojpeg)
32
+ [![License](https://img.shields.io/github/license/lilohuang/PyTurboJPEG.svg?style=flat-square)](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
- ### 16-bit JPEG (Lossless)
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.1.0
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
+ [![PyPI Version](https://img.shields.io/pypi/v/pyturbojpeg.svg?style=flat-square&color=blue)](https://pypi.org/project/pyturbojpeg/)
30
+ ![Python Version](https://img.shields.io/badge/python-3.6+-blue?logo=python&logoColor=white)
31
+ [![Downloads](https://img.shields.io/pypi/dm/pyturbojpeg.svg?style=flat-square&color=orange)](https://pypistats.org/packages/pyturbojpeg)
32
+ [![License](https://img.shields.io/github/license/lilohuang/PyTurboJPEG.svg?style=flat-square)](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
- ### 16-bit JPEG (Lossless)
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
+ [![PyPI Version](https://img.shields.io/pypi/v/pyturbojpeg.svg?style=flat-square&color=blue)](https://pypi.org/project/pyturbojpeg/)
6
+ ![Python Version](https://img.shields.io/badge/python-3.6+-blue?logo=python&logoColor=white)
7
+ [![Downloads](https://img.shields.io/pypi/dm/pyturbojpeg.svg?style=flat-square&color=orange)](https://pypistats.org/packages/pyturbojpeg)
8
+ [![License](https://img.shields.io/github/license/lilohuang/PyTurboJPEG.svg?style=flat-square)](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
- ### 16-bit JPEG (Lossless)
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.1.0',
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.1.0'
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
- # tjGetScalingFactors - still the current API in 3.1.x
523
- get_scaling_factors = turbo_jpeg.tjGetScalingFactors
524
- get_scaling_factors.argtypes = [POINTER(c_int)]
525
- get_scaling_factors.restype = POINTER(ScalingFactor)
526
- num_scaling_factors = c_int()
527
- scaling_factors = get_scaling_factors(byref(num_scaling_factors))
528
- self.__scaling_factors = frozenset(
529
- (scaling_factors[i].num, scaling_factors[i].denom)
530
- for i in range(num_scaling_factors.value)
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