PyTurboJPEG 2.1.0__tar.gz → 2.3.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.3.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.3.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.3.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.3.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)]
@@ -304,12 +305,12 @@ def fill_background(coeffs_ptr, arrayRegion, planeRegion, componentID, transform
304
305
  min(arrayRegion.y+arrayRegion.h, background_data.h)
305
306
  - arrayRegion.y
306
307
  )
307
- for x in range(background_data.w//MCU_WIDTH, planeRegion.w//MCU_WIDTH):
308
- for y in range(
309
- left_start_row//MCU_HEIGHT,
310
- left_end_row//MCU_HEIGHT
311
- ):
312
- coeffs[y][x][0] = background_data.lum
308
+ y_start = left_start_row // MCU_HEIGHT
309
+ y_end = left_end_row // MCU_HEIGHT
310
+ x_start = background_data.w // MCU_WIDTH
311
+ x_end = planeRegion.w // MCU_WIDTH
312
+ if y_end > y_start and x_end > x_start:
313
+ coeffs[y_start:y_end, x_start:x_end, 0] = background_data.lum
313
314
 
314
315
  # fill mcus under image
315
316
  bottom_start_row = (
@@ -319,23 +320,20 @@ def fill_background(coeffs_ptr, arrayRegion, planeRegion, componentID, transform
319
320
  max(arrayRegion.y+arrayRegion.h, background_data.h)
320
321
  - arrayRegion.y
321
322
  )
322
- for x in range(0, planeRegion.w//MCU_WIDTH):
323
- for y in range(
324
- bottom_start_row//MCU_HEIGHT,
325
- bottom_end_row//MCU_HEIGHT
326
- ):
327
- coeffs[y][x][0] = background_data.lum
323
+ y_start = bottom_start_row // MCU_HEIGHT
324
+ y_end = bottom_end_row // MCU_HEIGHT
325
+ x_end = planeRegion.w // MCU_WIDTH
326
+ if y_end > y_start and x_end > 0:
327
+ coeffs[y_start:y_end, 0:x_end, 0] = background_data.lum
328
328
 
329
329
  return 1
330
330
 
331
-
332
331
  def split_byte_into_nibbles(value):
333
332
  """Split byte int into 2 nibbles (4 bits)."""
334
333
  first = value >> 4
335
334
  second = value & 0x0F
336
335
  return first, second
337
336
 
338
-
339
337
  class TurboJPEG(object):
340
338
  """A Python wrapper of libjpeg-turbo for decoding and encoding JPEG image."""
341
339
  def __init__(self, lib_path=None):
@@ -489,6 +487,17 @@ class TurboJPEG(object):
489
487
  self.__decompress16.argtypes = [
490
488
  c_void_p, POINTER(c_ubyte), c_size_t, POINTER(c_ushort), c_int, c_int]
491
489
  self.__decompress16.restype = c_int
490
+
491
+ # tjGetScalingFactors
492
+ get_scaling_factors = turbo_jpeg.tjGetScalingFactors
493
+ get_scaling_factors.argtypes = [POINTER(c_int)]
494
+ get_scaling_factors.restype = POINTER(ScalingFactor)
495
+ num_scaling_factors = c_int()
496
+ scaling_factors = get_scaling_factors(byref(num_scaling_factors))
497
+ self.__scaling_factors = frozenset(
498
+ (scaling_factors[i].num, scaling_factors[i].denom)
499
+ for i in range(num_scaling_factors.value)
500
+ )
492
501
 
493
502
  # tj3CompressFromYUV16 - compress 16-bit YUV to JPEG (TurboJPEG 3.1+)
494
503
  # These functions may not be available in all TurboJPEG 3.x versions
@@ -519,16 +528,21 @@ class TurboJPEG(object):
519
528
  except AttributeError:
520
529
  self.__decompressToYUVPlanes16 = None
521
530
 
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
- )
531
+ # tj3GetICCProfile - retrieve ICC profile from decompressor after header parsing (TurboJPEG 3.1+)
532
+ try:
533
+ self.__get_icc_profile = turbo_jpeg.tj3GetICCProfile
534
+ self.__get_icc_profile.argtypes = [c_void_p, POINTER(c_void_p), POINTER(c_size_t)]
535
+ self.__get_icc_profile.restype = c_int
536
+ except AttributeError:
537
+ self.__get_icc_profile = None
538
+
539
+ # tj3SetICCProfile - attach ICC profile to compressor before compression (TurboJPEG 3.1+)
540
+ try:
541
+ self.__set_icc_profile = turbo_jpeg.tj3SetICCProfile
542
+ self.__set_icc_profile.argtypes = [c_void_p, c_void_p, c_size_t]
543
+ self.__set_icc_profile.restype = c_int
544
+ except AttributeError:
545
+ self.__set_icc_profile = None
532
546
 
533
547
  def decode_header(self, jpeg_buf, return_precision=False):
534
548
  """decodes JPEG header and returns image properties as a tuple.
@@ -592,6 +606,101 @@ class TurboJPEG(object):
592
606
  finally:
593
607
  self.__destroy(handle)
594
608
 
609
+ def get_icc_profile(self, jpeg_buf):
610
+ """Extracts the embedded ICC color profile from a JPEG image.
611
+
612
+ Requires TurboJPEG 3.1 or later with tj3GetICCProfile support.
613
+
614
+ Parameters
615
+ ----------
616
+ jpeg_buf : bytes
617
+ JPEG image data buffer containing an embedded ICC profile.
618
+
619
+ Returns
620
+ -------
621
+ bytes or None
622
+ Raw ICC profile data as a bytes object, or None if no ICC profile
623
+ is present in the JPEG stream.
624
+
625
+ Raises
626
+ ------
627
+ OSError
628
+ If the JPEG header cannot be parsed or a fatal error occurs.
629
+ NotImplementedError
630
+ If the loaded libturbojpeg does not export tj3GetICCProfile.
631
+
632
+ Examples
633
+ --------
634
+ >>> jpeg = TurboJPEG()
635
+ >>> with open('photo_with_icc.jpg', 'rb') as f:
636
+ ... data = f.read()
637
+ >>> icc = jpeg.get_icc_profile(data)
638
+ >>> if icc:
639
+ ... print(f'ICC profile size: {len(icc)} bytes')
640
+ """
641
+ if self.__get_icc_profile is None:
642
+ raise NotImplementedError(
643
+ 'tj3GetICCProfile is not available in the loaded libturbojpeg. '
644
+ 'Please upgrade to libjpeg-turbo 3.1 or later.')
645
+ handle = self.__init(TJINIT_DECOMPRESS)
646
+ try:
647
+ # Set TJPARAM_SAVEMARKERS to 2 (APP2) so the decompressor
648
+ # retains ICC profile markers during header parsing.
649
+ if self.__set(handle, TJPARAM_SAVEMARKERS, 2) != 0:
650
+ self.__report_error(handle)
651
+ jpeg_array = np.frombuffer(jpeg_buf, dtype=np.uint8)
652
+ src_addr = self.__getaddr(jpeg_array)
653
+ status = self.__decompress_header(handle, src_addr, jpeg_array.size)
654
+ if status != 0:
655
+ self.__report_error(handle)
656
+ icc_buf = c_void_p()
657
+ icc_size = c_size_t()
658
+ status = self.__get_icc_profile(handle, byref(icc_buf), byref(icc_size))
659
+ if status != 0:
660
+ # A non-fatal return (e.g. no profile present) should return None
661
+ err_code = self.__get_error_code(handle)
662
+ if err_code == TJERR_WARNING:
663
+ return None
664
+ self.__report_error(handle)
665
+ if icc_buf.value is None or icc_size.value == 0:
666
+ return None
667
+ result = self.__copy_from_buffer(icc_buf.value, icc_size.value)
668
+ self.__free(icc_buf)
669
+ return result
670
+ finally:
671
+ self.__destroy(handle)
672
+
673
+ def set_icc_profile(self, handle, icc_buf):
674
+ """Attaches an ICC color profile to an active compressor handle.
675
+
676
+ This is a low-level helper intended for use when building custom
677
+ compression pipelines. In most cases, use encode() with the
678
+ icc_profile parameter instead.
679
+
680
+ Parameters
681
+ ----------
682
+ handle : ctypes void pointer
683
+ An active TurboJPEG compressor handle (TJINIT_COMPRESS).
684
+ icc_buf : bytes
685
+ Raw ICC profile data to embed.
686
+
687
+ Raises
688
+ ------
689
+ OSError
690
+ If tj3SetICCProfile returns a non-zero status.
691
+ NotImplementedError
692
+ If the loaded libturbojpeg does not export tj3SetICCProfile.
693
+ """
694
+ if self.__set_icc_profile is None:
695
+ raise NotImplementedError(
696
+ 'tj3SetICCProfile is not available in the loaded libturbojpeg. '
697
+ 'Please upgrade to libjpeg-turbo 3.1 or later.')
698
+ icc_array = np.frombuffer(icc_buf, dtype=np.uint8)
699
+ icc_addr = self.__getaddr(icc_array)
700
+ status = self.__set_icc_profile(handle, icc_addr, len(icc_buf))
701
+ if status != 0:
702
+ self.__report_error(handle)
703
+
595
704
  def decode(self, jpeg_buf, pixel_format=TJPF_BGR, scaling_factor=None, flags=0, dst=None):
596
705
  """decodes JPEG memory buffer to numpy array.
597
706
 
@@ -707,7 +816,7 @@ class TurboJPEG(object):
707
816
  finally:
708
817
  self.__destroy(handle)
709
818
 
710
- def encode(self, img_array, quality=85, pixel_format=TJPF_BGR, jpeg_subsample=TJSAMP_422, flags=0, dst=None, lossless=False):
819
+ 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
820
  """encodes numpy array to JPEG memory buffer.
712
821
 
713
822
  Parameters
@@ -729,6 +838,9 @@ class TurboJPEG(object):
729
838
  When True, provides perfect reconstruction with larger file sizes.
730
839
  Note: quality and jpeg_subsample parameters are ignored in lossless mode;
731
840
  subsampling is automatically set to 4:4:4 by the library.
841
+ icc_profile : bytes or None
842
+ Raw ICC profile data to embed in the JPEG (optional).
843
+ Requires TurboJPEG 3.1 or later with tj3SetICCProfile support.
732
844
 
733
845
  Returns
734
846
  -------
@@ -763,6 +875,9 @@ class TurboJPEG(object):
763
875
  if img_array.dtype != np.uint8:
764
876
  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
877
 
878
+ if icc_profile is not None:
879
+ self.set_icc_profile(handle, icc_profile)
880
+
766
881
  if dst is not None and not self.__is_buffer(dst):
767
882
  raise TypeError('\'dst\' argument must support buffer protocol')
768
883
  if (dst is not None and
@@ -1195,6 +1310,8 @@ class TurboJPEG(object):
1195
1310
 
1196
1311
  # Define crop transforms from cropping_regions
1197
1312
  crop_transforms = (TransformStruct * number_of_operations)()
1313
+ # Pre-compute luminance coefficient once for all crops
1314
+ lum_coefficient = None
1198
1315
  for i, crop_region in enumerate(crop_regions):
1199
1316
  # The fill_background callback is slow, only use it if needed
1200
1317
  if self.__need_fill_background(
@@ -1202,14 +1319,16 @@ class TurboJPEG(object):
1202
1319
  (image_width, image_height),
1203
1320
  background_luminance
1204
1321
  ):
1322
+ if lum_coefficient is None:
1323
+ lum_coefficient = self.__map_luminance_to_dc_dct_coefficient(
1324
+ bytearray(jpeg_buf),
1325
+ background_luminance
1326
+ )
1205
1327
  # Use callback to fill in background post-transform
1206
1328
  callback_data = BackgroundStruct(
1207
1329
  image_width,
1208
1330
  image_height,
1209
- self.__map_luminance_to_dc_dct_coefficient(
1210
- bytearray(jpeg_buf),
1211
- background_luminance
1212
- )
1331
+ lum_coefficient
1213
1332
  )
1214
1333
  callback = CUSTOMFILTER(fill_background)
1215
1334
  crop_transforms[i] = TransformStruct(
@@ -1234,7 +1353,6 @@ class TurboJPEG(object):
1234
1353
 
1235
1354
  def buffer_size(self, img_array, jpeg_subsample=TJSAMP_422):
1236
1355
  """Get maximum number of bytes of compressed jpeg data"""
1237
- img_array = np.ascontiguousarray(img_array)
1238
1356
  height, width = img_array.shape[:2]
1239
1357
  return self.__buffer_size(width, height, jpeg_subsample)
1240
1358
 
File without changes
File without changes