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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +12 -4
- data/ext/image_pack/image_pack.c +186 -75
- data/lib/image_pack/version.rb +1 -1
- data/lib/image_pack.rb +19 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12f5358f1b62d18b5f23c0022bf953ae6efab239645a94329774465b5ff4681a
|
|
4
|
+
data.tar.gz: b0fd8d35a15c5e6ef157c925e0ed967a485755b32f310b5c49bdc10688b58ebf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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.
|
|
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.
|
data/ext/image_pack/image_pack.c
CHANGED
|
@@ -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(
|
|
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,
|
|
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
|
-
|
|
1334
|
-
|
|
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
|
-
|
|
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
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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->
|
|
1956
|
-
ctx->
|
|
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(
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
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,
|
|
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,
|
|
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}};
|
data/lib/image_pack/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
+
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-
|
|
11
|
+
date: 2026-06-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|