PyTurboJPEG 2.2.0__tar.gz → 2.4.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.2.0
3
+ Version: 2.4.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
@@ -179,6 +179,13 @@ with open('input.jpg', 'rb') as f:
179
179
  cropped_data = jpeg.crop(f.read(), 8, 8, 320, 240)
180
180
  with open('cropped_output.jpg', 'wb') as f:
181
181
  f.write(cropped_data)
182
+
183
+ # Lossless Huffman table optimization (re-encodes with optimal tables,
184
+ # identical pixels; typically smaller unless already optimized)
185
+ with open('input.jpg', 'rb') as f:
186
+ optimized_data = jpeg.optimize(f.read())
187
+ with open('optimized_output.jpg', 'wb') as f:
188
+ f.write(optimized_data)
182
189
  ```
183
190
 
184
191
  ### In-Place Operations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyTurboJPEG
3
- Version: 2.2.0
3
+ Version: 2.4.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
@@ -179,6 +179,13 @@ with open('input.jpg', 'rb') as f:
179
179
  cropped_data = jpeg.crop(f.read(), 8, 8, 320, 240)
180
180
  with open('cropped_output.jpg', 'wb') as f:
181
181
  f.write(cropped_data)
182
+
183
+ # Lossless Huffman table optimization (re-encodes with optimal tables,
184
+ # identical pixels; typically smaller unless already optimized)
185
+ with open('input.jpg', 'rb') as f:
186
+ optimized_data = jpeg.optimize(f.read())
187
+ with open('optimized_output.jpg', 'wb') as f:
188
+ f.write(optimized_data)
182
189
  ```
183
190
 
184
191
  ### In-Place Operations
@@ -155,6 +155,13 @@ with open('input.jpg', 'rb') as f:
155
155
  cropped_data = jpeg.crop(f.read(), 8, 8, 320, 240)
156
156
  with open('cropped_output.jpg', 'wb') as f:
157
157
  f.write(cropped_data)
158
+
159
+ # Lossless Huffman table optimization (re-encodes with optimal tables,
160
+ # identical pixels; typically smaller unless already optimized)
161
+ with open('input.jpg', 'rb') as f:
162
+ optimized_data = jpeg.optimize(f.read())
163
+ with open('optimized_output.jpg', 'wb') as f:
164
+ f.write(optimized_data)
158
165
  ```
159
166
 
160
167
  ### In-Place Operations
@@ -2,7 +2,7 @@ import io
2
2
  from setuptools import setup, find_packages
3
3
  setup(
4
4
  name='PyTurboJPEG',
5
- version='2.2.0',
5
+ version='2.4.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',
@@ -513,6 +513,17 @@ class TestCropMultiple:
513
513
  assert subsample == TJSAMP_GRAY
514
514
 
515
515
 
516
+ class TestOptimize:
517
+ """Test optimize function."""
518
+
519
+ def test_optimize_is_lossless(self, jpeg_instance, encoded_sample_jpeg):
520
+ """Test optimization returns a valid JPEG with unchanged pixels."""
521
+ optimized = jpeg_instance.optimize(encoded_sample_jpeg)
522
+ original_pixels = jpeg_instance.decode(encoded_sample_jpeg)
523
+ optimized_pixels = jpeg_instance.decode(optimized)
524
+ assert np.array_equal(original_pixels, optimized_pixels)
525
+
526
+
516
527
  class TestBufferSize:
517
528
  """Test buffer_size function."""
518
529
 
@@ -23,7 +23,7 @@
23
23
  # SOFTWARE.
24
24
 
25
25
  __author__ = 'Lilo Huang <kuso.cc@gmail.com>'
26
- __version__ = '2.2.0'
26
+ __version__ = '2.4.0'
27
27
 
28
28
  from ctypes import *
29
29
  from ctypes.util import find_library
@@ -125,6 +125,7 @@ TJXOPT_GRAY = 8
125
125
  TJXOPT_NOOUTPUT = 16
126
126
  TJXOPT_PROGRESSIVE = 32
127
127
  TJXOPT_COPYNONE = 64
128
+ TJXOPT_OPTIMIZE = 256 # Huffman table optimization
128
129
 
129
130
  # pixel size
130
131
  # see details in https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/src/turbojpeg.h
@@ -305,12 +306,12 @@ def fill_background(coeffs_ptr, arrayRegion, planeRegion, componentID, transform
305
306
  min(arrayRegion.y+arrayRegion.h, background_data.h)
306
307
  - arrayRegion.y
307
308
  )
308
- for x in range(background_data.w//MCU_WIDTH, planeRegion.w//MCU_WIDTH):
309
- for y in range(
310
- left_start_row//MCU_HEIGHT,
311
- left_end_row//MCU_HEIGHT
312
- ):
313
- coeffs[y][x][0] = background_data.lum
309
+ y_start = left_start_row // MCU_HEIGHT
310
+ y_end = left_end_row // MCU_HEIGHT
311
+ x_start = background_data.w // MCU_WIDTH
312
+ x_end = planeRegion.w // MCU_WIDTH
313
+ if y_end > y_start and x_end > x_start:
314
+ coeffs[y_start:y_end, x_start:x_end, 0] = background_data.lum
314
315
 
315
316
  # fill mcus under image
316
317
  bottom_start_row = (
@@ -320,12 +321,11 @@ def fill_background(coeffs_ptr, arrayRegion, planeRegion, componentID, transform
320
321
  max(arrayRegion.y+arrayRegion.h, background_data.h)
321
322
  - arrayRegion.y
322
323
  )
323
- for x in range(0, planeRegion.w//MCU_WIDTH):
324
- for y in range(
325
- bottom_start_row//MCU_HEIGHT,
326
- bottom_end_row//MCU_HEIGHT
327
- ):
328
- coeffs[y][x][0] = background_data.lum
324
+ y_start = bottom_start_row // MCU_HEIGHT
325
+ y_end = bottom_end_row // MCU_HEIGHT
326
+ x_end = planeRegion.w // MCU_WIDTH
327
+ if y_end > y_start and x_end > 0:
328
+ coeffs[y_start:y_end, 0:x_end, 0] = background_data.lum
329
329
 
330
330
  return 1
331
331
 
@@ -1311,6 +1311,8 @@ class TurboJPEG(object):
1311
1311
 
1312
1312
  # Define crop transforms from cropping_regions
1313
1313
  crop_transforms = (TransformStruct * number_of_operations)()
1314
+ # Pre-compute luminance coefficient once for all crops
1315
+ lum_coefficient = None
1314
1316
  for i, crop_region in enumerate(crop_regions):
1315
1317
  # The fill_background callback is slow, only use it if needed
1316
1318
  if self.__need_fill_background(
@@ -1318,14 +1320,16 @@ class TurboJPEG(object):
1318
1320
  (image_width, image_height),
1319
1321
  background_luminance
1320
1322
  ):
1323
+ if lum_coefficient is None:
1324
+ lum_coefficient = self.__map_luminance_to_dc_dct_coefficient(
1325
+ bytearray(jpeg_buf),
1326
+ background_luminance
1327
+ )
1321
1328
  # Use callback to fill in background post-transform
1322
1329
  callback_data = BackgroundStruct(
1323
1330
  image_width,
1324
1331
  image_height,
1325
- self.__map_luminance_to_dc_dct_coefficient(
1326
- bytearray(jpeg_buf),
1327
- background_luminance
1328
- )
1332
+ lum_coefficient
1329
1333
  )
1330
1334
  callback = CUSTOMFILTER(fill_background)
1331
1335
  crop_transforms[i] = TransformStruct(
@@ -1348,9 +1352,45 @@ class TurboJPEG(object):
1348
1352
  finally:
1349
1353
  self.__destroy(handle)
1350
1354
 
1355
+ def optimize(self, jpeg_buf, copynone=False):
1356
+ """Losslessly optimize the Huffman tables of a jpeg image.
1357
+
1358
+ Re-encodes the entropy-coded data with optimal Huffman tables
1359
+ (equivalent to ``jpegtran -optimize``) without any loss in image
1360
+ quality. Typically reduces file size, unless the input is already
1361
+ optimized.
1362
+
1363
+ Parameters
1364
+ ----------
1365
+ jpeg_buf: bytes
1366
+ Input jpeg image.
1367
+ copynone: bool
1368
+ True = do not copy EXIF data (False by default)
1369
+
1370
+ Returns
1371
+ ----------
1372
+ bytes
1373
+ Huffman-optimized jpeg image.
1374
+ """
1375
+ handle = self.__init(TJINIT_TRANSFORM)
1376
+ try:
1377
+ jpeg_array = np.frombuffer(jpeg_buf, dtype=np.uint8)
1378
+ src_addr = self.__getaddr(jpeg_array)
1379
+ status = self.__decompress_header(handle, src_addr, jpeg_array.size)
1380
+ if status != 0:
1381
+ self.__report_error(handle)
1382
+ # Without TJXOPT_CROP the cropping region is ignored and the whole
1383
+ # image is transformed, so the (zeroed) region needs no setup.
1384
+ transforms = (TransformStruct * 1)()
1385
+ transforms[0].op = TJXOP_NONE
1386
+ transforms[0].options = TJXOPT_OPTIMIZE | (copynone and TJXOPT_COPYNONE)
1387
+ return self.__do_transform(handle, src_addr, jpeg_array.size, 1, transforms)[0]
1388
+
1389
+ finally:
1390
+ self.__destroy(handle)
1391
+
1351
1392
  def buffer_size(self, img_array, jpeg_subsample=TJSAMP_422):
1352
1393
  """Get maximum number of bytes of compressed jpeg data"""
1353
- img_array = np.ascontiguousarray(img_array)
1354
1394
  height, width = img_array.shape[:2]
1355
1395
  return self.__buffer_size(width, height, jpeg_subsample)
1356
1396
 
File without changes
File without changes