image_pack 0.2.4 → 0.2.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 124f64ccca4910cee483d3c63a189f6de7d7ea72e8f4a8b88d538fe503cf0673
4
- data.tar.gz: d51e698fdad475590f685ddd6097f9bf8486dcb154ce37716e60fac256949c6c
3
+ metadata.gz: 12f5358f1b62d18b5f23c0022bf953ae6efab239645a94329774465b5ff4681a
4
+ data.tar.gz: b0fd8d35a15c5e6ef157c925e0ed967a485755b32f310b5c49bdc10688b58ebf
5
5
  SHA512:
6
- metadata.gz: 6ed979776d78d8e1d64e01ec46499d2453d13483495688cc79b71e0fc84b542d68e2a076015d480aaac812c90ef1dea49952f40b156d11d2b4dd95ace73a7688
7
- data.tar.gz: 233229ccf2b0daa138dc6bd664ce14c8ddf770aaa232d42efc33d43419a3c7f0f5b673daf985e6745e49fcf8ceeac9503d84910319cb8a7d269178e08cd657eb
6
+ metadata.gz: a3cf71f03cedf98384331e74fdd47d3fa151b432d3af4e059032261a3f4a929d15b9329cbc57aace8f04fda9c73ac1cdad9d99a4b7e499fbda1f7242334c5c6e
7
+ data.tar.gz: 99898ec78f08fe924e5447a514dd37dcd4b141cf6b619fe5713f1050c0ca1ded2e112eda18b407e75b39d3a9c2fe802e1294d02d46ed63a24960944f398603d4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5
4
+
5
+ **Output and API changes (upgrade notes)**
6
+
7
+ - `algo: :mozjpeg` and `algo: :size` now default to **progressive JPEG** with full scan optimization. Previously the default was baseline JPEG. Pass `progressive: false` to restore the old behavior.
8
+ - Added `mozjpeg_scan_opt:` keyword to `compress` and `compress_pixels` (default `true`). Passing `false` keeps trellis + progressive + Huffman optimization but skips the multi-pass scan search, trading ~0.7% larger files for ~30% faster encode on the MozJPEG path.
9
+ - `algo: :jpeg_turbo` and `algo: :fast` are unchanged: baseline JPEG, same output as 0.2.4.
10
+
11
+ **Performance improvements**
12
+
13
+ - SSIM guard (`min_ssim:`) now uses an interpolation/secant probe instead of pure bisection. Once a failing and a passing sample are known, the crossing quality is predicted by linear interpolation and clamped inside the live bracket. The accept/reject invariant and the minimal passing quality are unchanged; typical inputs converge in fewer encode/decode/SSIM round trips.
14
+ - In size mode, the SSIM guard scores candidates with a cheap "measurement" encode that skips `OPTIMIZE_SCANS` (which only affects entropy coding/scan layout, never the dequantized coefficients). The winning quality is re-encoded once with the full profile, and the true SSIM of the final output is re-checked, so `report[:ssim] >= min_ssim` continues to hold.
15
+ - The `algo: :jpeg_turbo`/`:fast` JPEG→JPEG path now transcodes through YCbCr instead of RGB, skipping one color-space round trip. Grayscale and `compress_pixels` inputs are unaffected.
16
+
17
+ **Other**
18
+
19
+ - Added stricter smoke coverage for legacy aliases, default algorithm behavior, and parser-backed JPEG assertions.
20
+ - Added `rake simd:check` and `rake release:check` so release builds cannot silently ship scalar x86_64 output unless `IMAGE_PACK_ALLOW_SCALAR=1` is set.
21
+
3
22
  ## 0.2.4
4
23
 
5
24
  - libjpeg/MozJPEG diagnostics are no longer written to `stderr`; instead decode warnings (e.g. "Premature end of JPEG file") are counted and the first message is captured, so a damaged or truncated input is observable rather than silently degraded.
data/README.md CHANGED
@@ -45,18 +45,23 @@ ImagePack.compress_bytes(jpeg,
45
45
 
46
46
  Algorithms:
47
47
 
48
- - `:size` / `:mozjpeg` — smaller files, default
49
- - `:fast` / `:jpeg_turbo` — faster mode
48
+ - `:size` / `:mozjpeg` — smaller files, default; uses optimized progressive MozJPEG output by default
49
+ - `:fast` / `:jpeg_turbo` — faster baseline mode
50
50
 
51
51
  Common options:
52
52
 
53
53
  ```ruby
54
54
  ImagePack.compress_bytes(jpeg, min_ssim: 0.985)
55
- ImagePack.compress_bytes(jpeg, progressive: true)
55
+ ImagePack.compress_bytes(jpeg, progressive: false) # force baseline output
56
56
  ImagePack.compress_bytes(jpeg, strict: true)
57
57
  ImagePack.compress_bytes(jpeg, report: true)
58
58
  ```
59
59
 
60
+
61
+ `algo: :size` / `:mozjpeg` now defaults to optimized progressive scans plus scan-aware MozJPEG trellis tuning because that is the strongest built-in size profile used by the gem. Pass `progressive: false` when you explicitly need baseline JPEG output.
62
+
63
+ `algo: :fast` / `:jpeg_turbo` keeps baseline output by default and remains the throughput path.
64
+
60
65
  `min_ssim:` searches for the lowest acceptable quality using a fast native luma SSIM guard.
61
66
 
62
67
  `strict: true` raises `ImagePack::InvalidImageError` on damaged/truncated JPEG warnings.
@@ -155,15 +160,18 @@ end
155
160
  bundle exec rake vendor
156
161
  bundle exec rake compile
157
162
  bundle exec rake test
163
+ bundle exec rake release:check
158
164
  ```
159
165
 
160
166
  `rake vendor` pins MozJPEG `v4.1.5`.
161
167
 
168
+ `rake release:check` compiles, verifies tests, and fails release builds when SIMD is unavailable. Set `IMAGE_PACK_ALLOW_SCALAR=1` only when intentionally shipping a scalar build.
169
+
162
170
  ## Limits
163
171
 
164
172
  - JPEG only.
165
173
  - Ruby `>= 3.1`; `execution: :offload` requires Ruby `>= 3.4`.
166
174
  - Pixel-level `compress` rejects CMYK/YCCK JPEG input; use `optimize_jpeg` for existing CMYK/YCCK JPEGs.
167
- - Arithmetic-coded JPEG support is disabled in `0.2.4`.
175
+ - Arithmetic-coded JPEG support is disabled in `0.2.5`.
168
176
  - Streaming output is not supported; file output uses atomic write-through-temp-file and rename.
169
177
  - `ImagePack.compress(input, ...)` keeps a legacy path-vs-bytes heuristic; prefer explicit `*_bytes` / `*_file` helpers.
@@ -137,6 +137,7 @@ typedef struct {
137
137
  int progressive;
138
138
  int strip_metadata;
139
139
  int mozjpeg_trellis_enabled;
140
+ int mozjpeg_scan_opt_enabled;
140
141
  ip_algo_t algo;
141
142
  ip_execution_t requested_execution;
142
143
  ip_execution_t resolved_execution;
@@ -173,6 +174,7 @@ typedef struct {
173
174
  unsigned char *transient_jpeg_buf;
174
175
  unsigned char *transient_decode_buf;
175
176
  int source_orientation;
177
+ int decoded_as_ycbcr;
176
178
  } ip_context_t;
177
179
 
178
180
  typedef struct {
@@ -251,7 +253,8 @@ static int ip_run_context(ip_context_t *ctx);
251
253
  static void validate_limits_for_pixels(ip_context_t *ctx);
252
254
 
253
255
  static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, int *width,
254
- int *height, int *channels, int fast_decode_mode);
256
+ int *height, int *channels, int fast_decode_mode,
257
+ int allow_ycbcr_transcode);
255
258
  static int ip_decode_jpeg_to_luma_buffer(ip_context_t *ctx, const unsigned char *data, size_t size,
256
259
  unsigned char **luma, int *width, int *height);
257
260
  static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mode);
@@ -263,14 +266,14 @@ static int ip_run_optimize_context(ip_context_t *ctx);
263
266
  typedef struct {
264
267
  VALUE self, input, input_kind, output, output_kind, algo, quality, min_ssim;
265
268
  VALUE mozjpeg_trellis, progressive, strip_metadata, execution, cancellable, has_scheduler;
266
- VALUE report, strict;
269
+ VALUE report, strict, mozjpeg_scan_opt;
267
270
  ip_context_t *ctx;
268
271
  } ip_compress_jpeg_call_t;
269
272
 
270
273
  typedef struct {
271
274
  VALUE self, buffer, width, height, channels, output, output_kind, algo, quality, min_ssim;
272
275
  VALUE mozjpeg_trellis, progressive, exact_size, execution, cancellable, has_scheduler;
273
- VALUE report, strict;
276
+ VALUE report, strict, mozjpeg_scan_opt;
274
277
  ip_context_t *ctx;
275
278
  } ip_compress_pixels_call_t;
276
279
 
@@ -294,11 +297,7 @@ static VALUE ip_call_cleanup(VALUE ptr) {
294
297
  return Qnil;
295
298
  }
296
299
 
297
- static VALUE ip_compress_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
298
- VALUE output_kind, VALUE algo, VALUE quality, VALUE min_ssim,
299
- VALUE mozjpeg_trellis, VALUE progressive, VALUE strip_metadata,
300
- VALUE execution, VALUE cancellable, VALUE has_scheduler,
301
- VALUE report, VALUE strict);
300
+ static VALUE ip_compress_jpeg_entry(int argc, VALUE *argv, VALUE self);
302
301
  static VALUE ip_compress_pixels_entry(int argc, VALUE *argv, VALUE self);
303
302
  static VALUE ip_optimize_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
304
303
  VALUE output_kind, VALUE progressive, VALUE strip_metadata,
@@ -448,6 +447,7 @@ static ip_context_t *ip_context_new(void) {
448
447
  ctx->status = IP_OK;
449
448
  ctx->quality = 82;
450
449
  ctx->mozjpeg_trellis_enabled = 1;
450
+ ctx->mozjpeg_scan_opt_enabled = 1;
451
451
  ctx->selected_quality = 82;
452
452
  ctx->requested_execution = IP_EXEC_AUTO;
453
453
  ctx->resolved_execution = IP_EXEC_AUTO;
@@ -1277,13 +1277,21 @@ static void configure_mozjpeg_profile_before_defaults(struct jpeg_compress_struc
1277
1277
  static void configure_mozjpeg_features_after_defaults(struct jpeg_compress_struct *cinfo,
1278
1278
  int mozjpeg_size_mode,
1279
1279
  int progressive_requested,
1280
- int mozjpeg_trellis_enabled) {
1280
+ int mozjpeg_trellis_enabled,
1281
+ int scan_opt_enabled, int measurement) {
1281
1282
  if (mozjpeg_size_mode) {
1282
1283
  cinfo->optimize_coding = TRUE;
1283
1284
 
1284
1285
  if (progressive_requested) {
1286
+ if (mozjpeg_trellis_enabled) {
1287
+ jpeg_c_set_bool_param(cinfo, JBOOLEAN_USE_SCANS_IN_TRELLIS, TRUE);
1288
+ jpeg_c_set_bool_param(cinfo, JBOOLEAN_TRELLIS_EOB_OPT, TRUE);
1289
+ }
1290
+ jpeg_c_set_int_param(cinfo, JINT_DC_SCAN_OPT_MODE, 2);
1291
+ int run_scan_search = (measurement || !scan_opt_enabled) ? FALSE : TRUE;
1292
+ jpeg_c_set_bool_param(cinfo, JBOOLEAN_OPTIMIZE_SCANS, run_scan_search);
1285
1293
  jpeg_simple_progression(cinfo);
1286
- jpeg_c_set_bool_param(cinfo, JBOOLEAN_OPTIMIZE_SCANS, TRUE);
1294
+ jpeg_c_set_bool_param(cinfo, JBOOLEAN_OPTIMIZE_SCANS, run_scan_search);
1287
1295
  } else {
1288
1296
  cinfo->scan_info = NULL;
1289
1297
  cinfo->num_scans = 0;
@@ -1308,7 +1316,7 @@ static void configure_mozjpeg_features_after_defaults(struct jpeg_compress_struc
1308
1316
  }
1309
1317
  }
1310
1318
 
1311
- static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode) {
1319
+ static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode, int measurement) {
1312
1320
  struct jpeg_compress_struct cinfo;
1313
1321
  ip_jpeg_error_mgr jerr;
1314
1322
  unsigned long jpeg_size = 0;
@@ -1330,14 +1338,19 @@ static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode)
1330
1338
  cinfo.image_width = (JDIMENSION)ctx->width;
1331
1339
  cinfo.image_height = (JDIMENSION)ctx->height;
1332
1340
  cinfo.input_components = ctx->channels;
1333
- cinfo.in_color_space =
1334
- ctx->channels == 4 ? JCS_EXT_RGBA : color_space_for_channels(ctx->channels);
1341
+ if (ctx->decoded_as_ycbcr && ctx->channels == 3) {
1342
+ cinfo.in_color_space = JCS_YCbCr;
1343
+ } else {
1344
+ cinfo.in_color_space =
1345
+ ctx->channels == 4 ? JCS_EXT_RGBA : color_space_for_channels(ctx->channels);
1346
+ }
1335
1347
 
1336
1348
  configure_mozjpeg_profile_before_defaults(&cinfo, mozjpeg_size_mode);
1337
1349
  jpeg_set_defaults(&cinfo);
1338
1350
  jpeg_set_quality(&cinfo, ctx->quality, TRUE);
1339
1351
  configure_mozjpeg_features_after_defaults(&cinfo, mozjpeg_size_mode, ctx->progressive,
1340
- ctx->mozjpeg_trellis_enabled);
1352
+ ctx->mozjpeg_trellis_enabled,
1353
+ ctx->mozjpeg_scan_opt_enabled, measurement);
1341
1354
 
1342
1355
  jpeg_start_compress(&cinfo, TRUE);
1343
1356
 
@@ -1363,7 +1376,7 @@ static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode)
1363
1376
 
1364
1377
  jpeg_finish_compress(&cinfo);
1365
1378
 
1366
- if (ctx->max_output_size > 0 && (size_t)jpeg_size > ctx->max_output_size)
1379
+ if (!measurement && ctx->max_output_size > 0 && (size_t)jpeg_size > ctx->max_output_size)
1367
1380
  IP_FAIL_GOTO(ctx, IP_ERR_LIMIT, "output exceeds max_output_size");
1368
1381
 
1369
1382
  jpeg_destroy_compress(&cinfo);
@@ -1384,7 +1397,8 @@ fail:
1384
1397
  }
1385
1398
 
1386
1399
  static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, int *width,
1387
- int *height, int *channels, int fast_decode_mode) {
1400
+ int *height, int *channels, int fast_decode_mode,
1401
+ int allow_ycbcr_transcode) {
1388
1402
  struct jpeg_decompress_struct cinfo;
1389
1403
  ip_jpeg_error_mgr jerr;
1390
1404
  memset(&cinfo, 0, sizeof(cinfo));
@@ -1392,6 +1406,7 @@ static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, i
1392
1406
  cinfo.err = ip_use_error(&jerr, ctx, ip_jpeg_invalid_error_exit);
1393
1407
  ctx->transient_decode_buf = NULL;
1394
1408
  ctx->source_orientation = 1;
1409
+ ctx->decoded_as_ycbcr = 0;
1395
1410
 
1396
1411
  ctx->jmp_armed = 1;
1397
1412
  if (setjmp(ctx->jmpbuf)) {
@@ -1435,7 +1450,9 @@ static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, i
1435
1450
  if (ctx->status != IP_OK)
1436
1451
  goto fail;
1437
1452
 
1438
- cinfo.out_color_space = ch == 1 ? JCS_GRAYSCALE : JCS_RGB;
1453
+ int use_ycbcr = allow_ycbcr_transcode && ch == 3;
1454
+ cinfo.out_color_space = ch == 1 ? JCS_GRAYSCALE : (use_ycbcr ? JCS_YCbCr : JCS_RGB);
1455
+ ctx->decoded_as_ycbcr = use_ycbcr;
1439
1456
  if (fast_decode_mode)
1440
1457
  ip_apply_fast_decode(&cinfo);
1441
1458
 
@@ -1513,14 +1530,14 @@ fail:
1513
1530
 
1514
1531
  static int compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mode) {
1515
1532
  if (ctx->pixel_data) {
1516
- return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode);
1533
+ return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode, 0);
1517
1534
  }
1518
1535
 
1519
1536
  unsigned char *pixels = NULL;
1520
1537
  int width = 0;
1521
1538
  int height = 0;
1522
1539
  int channels = 0;
1523
- if (!ip_jpeg_decode_to_pixels(ctx, &pixels, &width, &height, &channels, !mozjpeg_size_mode))
1540
+ if (!ip_jpeg_decode_to_pixels(ctx, &pixels, &width, &height, &channels, !mozjpeg_size_mode, 1))
1524
1541
  return 0;
1525
1542
 
1526
1543
  ctx->owned_pixel_data = pixels;
@@ -1531,7 +1548,7 @@ static int compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mod
1531
1548
  ctx->channels = channels;
1532
1549
  ctx->decoded_bytes = ctx->pixel_size;
1533
1550
 
1534
- return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode);
1551
+ return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode, 0);
1535
1552
  }
1536
1553
 
1537
1554
  static void ip_clear_output_buffer(ip_context_t *ctx) {
@@ -1821,6 +1838,49 @@ static double ip_compute_ssim_luma_buffer(const unsigned char *a, const unsigned
1821
1838
  return windows > 0 ? total_ssim / (double)windows : 0.0;
1822
1839
  }
1823
1840
 
1841
+ static int ip_guard_score_quality(ip_context_t *ctx, int mozjpeg_size_mode, int measurement,
1842
+ const unsigned char *reference_luma, int reference_width,
1843
+ int reference_height, unsigned char **out_jpeg,
1844
+ size_t *out_jpeg_size, double *out_ssim) {
1845
+ *out_jpeg = NULL;
1846
+ *out_jpeg_size = 0;
1847
+ *out_ssim = 0.0;
1848
+
1849
+ ip_clear_output_buffer(ctx);
1850
+ if (!encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode, measurement))
1851
+ return 0;
1852
+
1853
+ unsigned char *candidate_jpeg = ctx->output_data;
1854
+ size_t candidate_jpeg_size = ctx->output_size;
1855
+ ctx->output_data = NULL;
1856
+ ctx->output_size = 0;
1857
+ ctx->output_owner = IP_OUTPUT_OWNER_NONE;
1858
+
1859
+ unsigned char *candidate_luma = NULL;
1860
+ int candidate_width = 0;
1861
+ int candidate_height = 0;
1862
+ if (!ip_decode_jpeg_to_luma_buffer(ctx, candidate_jpeg, candidate_jpeg_size, &candidate_luma,
1863
+ &candidate_width, &candidate_height)) {
1864
+ free(candidate_jpeg);
1865
+ return 0;
1866
+ }
1867
+
1868
+ if (candidate_width != reference_width || candidate_height != reference_height) {
1869
+ free(candidate_luma);
1870
+ free(candidate_jpeg);
1871
+ ip_context_set_error(ctx, IP_ERR_ENCODE,
1872
+ "candidate JPEG dimensions differ from reference image");
1873
+ return 0;
1874
+ }
1875
+
1876
+ *out_ssim = ip_compute_ssim_luma_buffer(reference_luma, candidate_luma, reference_width,
1877
+ reference_height);
1878
+ free(candidate_luma);
1879
+ *out_jpeg = candidate_jpeg;
1880
+ *out_jpeg_size = candidate_jpeg_size;
1881
+ return 1;
1882
+ }
1883
+
1824
1884
  static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mode) {
1825
1885
  unsigned char *reference_pixels = NULL;
1826
1886
  int reference_width = 0;
@@ -1834,7 +1894,7 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1834
1894
  reference_channels = ctx->channels;
1835
1895
  } else {
1836
1896
  if (!ip_jpeg_decode_to_pixels(ctx, &reference_pixels, &reference_width, &reference_height,
1837
- &reference_channels, 1)) {
1897
+ &reference_channels, 1, 0)) {
1838
1898
  return 0;
1839
1899
  }
1840
1900
 
@@ -1866,6 +1926,10 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1866
1926
  size_t best_jpeg_size = 0;
1867
1927
  double best_seen_ssim = 0.0;
1868
1928
  int best_seen_quality = 0;
1929
+ int probe_measurement = mozjpeg_size_mode ? 1 : 0;
1930
+ int have_lo = 0, have_hi = 0;
1931
+ int q_lo = 0, q_hi = 0;
1932
+ double s_lo = 0.0, s_hi = 0.0;
1869
1933
 
1870
1934
  while (search_low <= search_high) {
1871
1935
  if (atomic_load(&ctx->cancelled)) {
@@ -1875,50 +1939,32 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1875
1939
  return 0;
1876
1940
  }
1877
1941
 
1878
- int trial_quality = search_low + ((search_high - search_low) / 2);
1879
- ctx->quality = trial_quality;
1880
- ip_clear_output_buffer(ctx);
1881
-
1882
- if (!encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode)) {
1883
- free(reference_luma);
1884
- free(best_jpeg);
1885
- return 0;
1942
+ int trial_quality;
1943
+ if (have_lo && have_hi && s_hi > s_lo) {
1944
+ double t = (ctx->min_ssim - s_lo) / (s_hi - s_lo);
1945
+ double q_est = (double)q_lo + t * (double)(q_hi - q_lo);
1946
+ trial_quality = (int)(q_est + 0.5);
1947
+ if (trial_quality < search_low)
1948
+ trial_quality = search_low;
1949
+ if (trial_quality > search_high)
1950
+ trial_quality = search_high;
1951
+ } else {
1952
+ trial_quality = search_low + ((search_high - search_low) / 2);
1886
1953
  }
1887
1954
 
1888
- unsigned char *candidate_jpeg = ctx->output_data;
1889
- size_t candidate_jpeg_size = ctx->output_size;
1890
- ctx->output_data = NULL;
1891
- ctx->output_size = 0;
1892
- ctx->output_owner = IP_OUTPUT_OWNER_NONE;
1893
-
1894
- unsigned char *candidate_luma = NULL;
1895
- int candidate_width = 0;
1896
- int candidate_height = 0;
1897
- int decoded_ok =
1898
- ip_decode_jpeg_to_luma_buffer(ctx, candidate_jpeg, candidate_jpeg_size, &candidate_luma,
1899
- &candidate_width, &candidate_height);
1900
-
1901
- if (!decoded_ok) {
1902
- free(reference_luma);
1903
- free(candidate_jpeg);
1904
- free(best_jpeg);
1905
- return 0;
1906
- }
1955
+ ctx->quality = trial_quality;
1907
1956
 
1908
- if (candidate_width != reference_width || candidate_height != reference_height) {
1957
+ unsigned char *candidate_jpeg = NULL;
1958
+ size_t candidate_jpeg_size = 0;
1959
+ double ssim = 0.0;
1960
+ if (!ip_guard_score_quality(ctx, mozjpeg_size_mode, probe_measurement, reference_luma,
1961
+ reference_width, reference_height, &candidate_jpeg,
1962
+ &candidate_jpeg_size, &ssim)) {
1909
1963
  free(reference_luma);
1910
- free(candidate_luma);
1911
- free(candidate_jpeg);
1912
1964
  free(best_jpeg);
1913
- ip_context_set_error(ctx, IP_ERR_ENCODE,
1914
- "candidate JPEG dimensions differ from reference image");
1915
1965
  return 0;
1916
1966
  }
1917
1967
 
1918
- double ssim = ip_compute_ssim_luma_buffer(reference_luma, candidate_luma, reference_width,
1919
- reference_height);
1920
- free(candidate_luma);
1921
-
1922
1968
  if (ssim > best_seen_ssim) {
1923
1969
  best_seen_ssim = ssim;
1924
1970
  best_seen_quality = trial_quality;
@@ -1930,9 +1976,15 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1930
1976
  best_jpeg_size = candidate_jpeg_size;
1931
1977
  best_quality = trial_quality;
1932
1978
  best_ssim = ssim;
1979
+ q_hi = trial_quality;
1980
+ s_hi = ssim;
1981
+ have_hi = 1;
1933
1982
  search_high = trial_quality - 1;
1934
1983
  } else {
1935
1984
  free(candidate_jpeg);
1985
+ q_lo = trial_quality;
1986
+ s_lo = ssim;
1987
+ have_lo = 1;
1936
1988
  search_low = trial_quality + 1;
1937
1989
  }
1938
1990
  }
@@ -1947,13 +1999,75 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1947
1999
  return 0;
1948
2000
  }
1949
2001
 
1950
- ctx->quality = best_quality;
1951
- ctx->selected_quality = best_quality;
1952
- ctx->measured_ssim = best_ssim;
2002
+ if (!mozjpeg_size_mode) {
2003
+ ctx->quality = best_quality;
2004
+ ctx->selected_quality = best_quality;
2005
+ ctx->measured_ssim = best_ssim;
2006
+ free(reference_luma);
2007
+ ctx->output_data = best_jpeg;
2008
+ ctx->output_size = best_jpeg_size;
2009
+ ctx->output_owner = IP_OUTPUT_OWNER_MALLOC;
2010
+ return 1;
2011
+ }
2012
+
2013
+ free(best_jpeg);
2014
+ best_jpeg = NULL;
2015
+
2016
+ int final_quality = best_quality;
2017
+ unsigned char *final_jpeg = NULL;
2018
+ size_t final_jpeg_size = 0;
2019
+ double final_ssim = 0.0;
2020
+ int satisfied = 0;
2021
+
2022
+ while (final_quality <= 100) {
2023
+ if (atomic_load(&ctx->cancelled)) {
2024
+ free(final_jpeg);
2025
+ free(reference_luma);
2026
+ ip_context_set_error(ctx, IP_ERR_CANCELLED, "SSIM-guarded JPEG encode cancelled");
2027
+ return 0;
2028
+ }
2029
+
2030
+ ctx->quality = final_quality;
2031
+ unsigned char *jpeg = NULL;
2032
+ size_t jpeg_size = 0;
2033
+ double ssim = 0.0;
2034
+ if (!ip_guard_score_quality(ctx, mozjpeg_size_mode, 0, reference_luma, reference_width,
2035
+ reference_height, &jpeg, &jpeg_size, &ssim)) {
2036
+ free(final_jpeg);
2037
+ free(reference_luma);
2038
+ return 0;
2039
+ }
2040
+
2041
+ free(final_jpeg);
2042
+ final_jpeg = jpeg;
2043
+ final_jpeg_size = jpeg_size;
2044
+ final_ssim = ssim;
2045
+
2046
+ if (ssim >= ctx->min_ssim) {
2047
+ satisfied = 1;
2048
+ break;
2049
+ }
2050
+ final_quality++;
2051
+ }
2052
+
2053
+ if (!satisfied) {
2054
+ char message[512];
2055
+ snprintf(message, sizeof(message),
2056
+ "cannot satisfy min_ssim=%.6f; best full-profile SSIM %.6f at quality=100",
2057
+ ctx->min_ssim, final_ssim);
2058
+ free(final_jpeg);
2059
+ free(reference_luma);
2060
+ ip_context_set_error(ctx, IP_ERR_QUALITY, message);
2061
+ return 0;
2062
+ }
2063
+
1953
2064
  free(reference_luma);
1954
2065
 
1955
- ctx->output_data = best_jpeg;
1956
- ctx->output_size = best_jpeg_size;
2066
+ ctx->quality = final_quality;
2067
+ ctx->selected_quality = final_quality;
2068
+ ctx->measured_ssim = final_ssim;
2069
+ ctx->output_data = final_jpeg;
2070
+ ctx->output_size = final_jpeg_size;
1957
2071
  ctx->output_owner = IP_OUTPUT_OWNER_MALLOC;
1958
2072
  return 1;
1959
2073
  }
@@ -2280,6 +2394,7 @@ static VALUE ip_compress_jpeg_entry_body(VALUE ptr) {
2280
2394
  ctx->ssim_guard_enabled = ctx->min_ssim > 0.0;
2281
2395
  ip_validate_min_ssim_or_raise(ctx);
2282
2396
  ctx->mozjpeg_trellis_enabled = ip_bool_value(call->mozjpeg_trellis);
2397
+ ctx->mozjpeg_scan_opt_enabled = ip_bool_value(call->mozjpeg_scan_opt);
2283
2398
  ctx->progressive = ip_bool_value(call->progressive);
2284
2399
  ctx->strip_metadata = ip_bool_value(call->strip_metadata);
2285
2400
  ctx->requested_execution = ip_parse_execution(call->execution);
@@ -2315,16 +2430,11 @@ static VALUE ip_compress_jpeg_entry_body(VALUE ptr) {
2315
2430
  return ip_finish_output(ctx, out_kind);
2316
2431
  }
2317
2432
 
2318
- static VALUE ip_compress_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
2319
- VALUE output_kind, VALUE algo, VALUE quality, VALUE min_ssim,
2320
- VALUE mozjpeg_trellis, VALUE progressive, VALUE strip_metadata,
2321
- VALUE execution, VALUE cancellable, VALUE has_scheduler,
2322
- VALUE report, VALUE strict) {
2323
- ip_compress_jpeg_call_t call = {
2324
- self, input, input_kind, output, output_kind,
2325
- algo, quality, min_ssim, mozjpeg_trellis, progressive,
2326
- strip_metadata, execution, cancellable, has_scheduler, report,
2327
- strict, NULL};
2433
+ static VALUE ip_compress_jpeg_entry(int argc, VALUE *argv, VALUE self) {
2434
+ rb_check_arity(argc, 16, 16);
2435
+ ip_compress_jpeg_call_t call = {self, argv[0], argv[1], argv[2], argv[3], argv[4],
2436
+ argv[5], argv[6], argv[7], argv[8], argv[9], argv[10],
2437
+ argv[11], argv[12], argv[13], argv[14], argv[15], NULL};
2328
2438
  return rb_ensure(ip_compress_jpeg_entry_body, (VALUE)&call, ip_call_cleanup, (VALUE)&call.ctx);
2329
2439
  }
2330
2440
 
@@ -2344,6 +2454,7 @@ static VALUE ip_compress_pixels_entry_body(VALUE ptr) {
2344
2454
  ctx->ssim_guard_enabled = ctx->min_ssim > 0.0;
2345
2455
  ip_validate_min_ssim_or_raise(ctx);
2346
2456
  ctx->mozjpeg_trellis_enabled = ip_bool_value(call->mozjpeg_trellis);
2457
+ ctx->mozjpeg_scan_opt_enabled = ip_bool_value(call->mozjpeg_scan_opt);
2347
2458
  ctx->progressive = ip_bool_value(call->progressive);
2348
2459
  ctx->strip_metadata = 1;
2349
2460
  ctx->requested_execution = ip_parse_execution(call->execution);
@@ -2378,11 +2489,11 @@ static VALUE ip_compress_pixels_entry_body(VALUE ptr) {
2378
2489
  }
2379
2490
 
2380
2491
  static VALUE ip_compress_pixels_entry(int argc, VALUE *argv, VALUE self) {
2381
- rb_check_arity(argc, 17, 17);
2492
+ rb_check_arity(argc, 18, 18);
2382
2493
  ip_compress_pixels_call_t call = {self, argv[0], argv[1], argv[2], argv[3],
2383
2494
  argv[4], argv[5], argv[6], argv[7], argv[8],
2384
2495
  argv[9], argv[10], argv[11], argv[12], argv[13],
2385
- argv[14], argv[15], argv[16], NULL};
2496
+ argv[14], argv[15], argv[16], argv[17], NULL};
2386
2497
  return rb_ensure(ip_compress_pixels_entry_body, (VALUE)&call, ip_call_cleanup,
2387
2498
  (VALUE)&call.ctx);
2388
2499
  }
@@ -2498,7 +2609,7 @@ IMAGE_PACK_INIT_EXPORT void Init_image_pack(void) {
2498
2609
  const char *name;
2499
2610
  VALUE (*fn)(ANYARGS);
2500
2611
  int arity;
2501
- } methods[] = {{"__compress_jpeg", (VALUE (*)(ANYARGS))ip_compress_jpeg_entry, 15},
2612
+ } methods[] = {{"__compress_jpeg", (VALUE (*)(ANYARGS))ip_compress_jpeg_entry, -1},
2502
2613
  {"__compress_pixels", (VALUE (*)(ANYARGS))ip_compress_pixels_entry, -1},
2503
2614
  {"__optimize_jpeg", (VALUE (*)(ANYARGS))ip_optimize_jpeg_entry, 10},
2504
2615
  {"__inspect_image", (VALUE (*)(ANYARGS))ip_inspect_image_entry, 2}};
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ImagePack
4
- VERSION = "0.2.4"
4
+ VERSION = "0.2.5"
5
5
  end
data/lib/image_pack.rb CHANGED
@@ -121,7 +121,8 @@ module ImagePack
121
121
  quality: nil,
122
122
  min_ssim: nil,
123
123
  mozjpeg_trellis: true,
124
- progressive: false,
124
+ mozjpeg_scan_opt: true,
125
+ progressive: nil,
125
126
  strip_metadata: true,
126
127
  execution: nil,
127
128
  cancellable: false,
@@ -130,6 +131,8 @@ module ImagePack
130
131
  validate_algo!(algo)
131
132
  validate_min_ssim!(min_ssim)
132
133
  validate_boolean!(:mozjpeg_trellis, mozjpeg_trellis)
134
+ validate_boolean!(:mozjpeg_scan_opt, mozjpeg_scan_opt)
135
+ progressive = default_progressive_for(algo, progressive)
133
136
  validate_boolean!(:progressive, progressive)
134
137
  validate_boolean!(:strip_metadata, strip_metadata)
135
138
  validate_boolean!(:cancellable, cancellable)
@@ -159,7 +162,8 @@ module ImagePack
159
162
  cancellable ? 1 : 0,
160
163
  has_scheduler ? 1 : 0,
161
164
  report ? 1 : 0,
162
- strict ? 1 : 0)
165
+ strict ? 1 : 0,
166
+ mozjpeg_scan_opt ? 1 : 0)
163
167
  end
164
168
 
165
169
  def compress_pixels(buffer,
@@ -171,7 +175,8 @@ module ImagePack
171
175
  quality: nil,
172
176
  min_ssim: nil,
173
177
  mozjpeg_trellis: true,
174
- progressive: false,
178
+ mozjpeg_scan_opt: true,
179
+ progressive: nil,
175
180
  drop_alpha: nil,
176
181
  exact_size: false,
177
182
  execution: nil,
@@ -182,6 +187,8 @@ module ImagePack
182
187
  validate_algo!(algo)
183
188
  validate_min_ssim!(min_ssim)
184
189
  validate_boolean!(:mozjpeg_trellis, mozjpeg_trellis)
190
+ validate_boolean!(:mozjpeg_scan_opt, mozjpeg_scan_opt)
191
+ progressive = default_progressive_for(algo, progressive)
185
192
  validate_boolean!(:progressive, progressive)
186
193
  validate_drop_alpha!(drop_alpha)
187
194
  validate_boolean!(:exact_size, exact_size)
@@ -229,7 +236,8 @@ module ImagePack
229
236
  cancellable ? 1 : 0,
230
237
  has_scheduler ? 1 : 0,
231
238
  report ? 1 : 0,
232
- strict ? 1 : 0)
239
+ strict ? 1 : 0,
240
+ mozjpeg_scan_opt ? 1 : 0)
233
241
  end
234
242
 
235
243
  def inspect_image(input)
@@ -294,6 +302,13 @@ module ImagePack
294
302
  raise InvalidArgumentError, "#{name} must be true or false, got: #{value.inspect}"
295
303
  end
296
304
 
305
+
306
+ def default_progressive_for(algo, value)
307
+ return value unless value.nil?
308
+
309
+ ALGO_TO_NATIVE.fetch(algo) == :mozjpeg
310
+ end
311
+
297
312
  def validate_drop_alpha!(value)
298
313
  return if value.nil? || value == true || value == false
299
314
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_pack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-17 00:00:00.000000000 Z
11
+ date: 2026-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake