image_pack 0.2.3 → 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: 606688569aac1fa1515edefe270b18a04e5b0042de602b18663964726d8fbb34
4
- data.tar.gz: 9092fcf9fa3c061edc87ce2a22544acfe52dfa6ce4fe39961c95a1d17220a6b0
3
+ metadata.gz: 12f5358f1b62d18b5f23c0022bf953ae6efab239645a94329774465b5ff4681a
4
+ data.tar.gz: b0fd8d35a15c5e6ef157c925e0ed967a485755b32f310b5c49bdc10688b58ebf
5
5
  SHA512:
6
- metadata.gz: ad6b0db99c56d69c99d9216559e3c114a2079941a4f879e69508126bbf12b070844dc53e806b3da7637783a205528c10136c3cd3bac8f60b5fd6ee2ddcad01eb
7
- data.tar.gz: f1acfa9f5a7154d1ea201ae66658da704a5ee5d207886b096d284b7b69eca175d668993dba127b0a609d6fb5b68492196edb8d3bb70d12575f129c095c6b7704
6
+ metadata.gz: a3cf71f03cedf98384331e74fdd47d3fa151b432d3af4e059032261a3f4a929d15b9329cbc57aace8f04fda9c73ac1cdad9d99a4b7e499fbda1f7242334c5c6e
7
+ data.tar.gz: 99898ec78f08fe924e5447a514dd37dcd4b141cf6b619fe5713f1050c0ca1ded2e112eda18b407e75b39d3a9c2fe802e1294d02d46ed63a24960944f398603d4
data/CHANGELOG.md CHANGED
@@ -1,6 +1,41 @@
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
+
22
+ ## 0.2.4
23
+
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.
25
+ - Added `strict:` to `compress`, `compress_pixels`, and `optimize_jpeg`. With `strict: true` the first libjpeg warning is promoted to `ImagePack::InvalidImageError` instead of producing a degraded JPEG; default is `false`.
26
+ - Added `report:` to `compress` and `compress_pixels`. With `report: true` the call returns a Hash `{ output:, quality:, ssim:, algo:, bytesize:, input_bytesize:, warning_count:, warning: }`, exposing the SSIM-selected quality, achieved luma SSIM, and decode warning information; `report: false` (default) keeps the previous return value (binary `String` or `true`).
27
+ - Made the no-GVL and offload execution paths interruptible regardless of `cancellable:`. Decode/encode/SSIM checkpoints now always observe the unblock signal, and a pending Ruby interrupt (signal, `Thread#raise`, `Thread#kill`, scheduler) is delivered via `rb_thread_check_ints` after the native section, so the raised exception propagates instead of being masked. Cancellation is expressed by raising on the worker thread; `ImagePack::CancelledError` is the idiomatic choice.
28
+ - Added the `IMAGE_PACK_DISABLE_OFFLOAD=1` runtime opt-out. The vendored MozJPEG SIMD thread-local storage is only an idempotent CPU-feature cache, so offload remains safe with SIMD enabled, but the environment variable forces `offload_safe?` to `false` and prevents `:auto`/`:offload` from offloading.
29
+ - `NATIVE_MOZJPEG_VERSION` is now derived from a dedicated `IMAGE_PACK_MOZJPEG_VERSION` compile define instead of relying on the libjpeg `VERSION` macro being in scope.
30
+ - Removed the unused `IMAGE_PACK_RUBY34_ONLY` compile define.
31
+ - Removed a stale `v0.2.2` reference from the `output:` argument error message.
32
+ - Added tests covering `report:`, `strict:`, warning collection, the cancellation contract, offload availability, and version hygiene.
33
+ - Documentation and message version references updated to 0.2.4.
34
+
35
+ ## 0.2.3
36
+
3
37
  - Lowered the runtime floor to Ruby >= 3.1.0 while keeping `execution: :offload` Ruby >= 3.4-only and explicitly unsupported on older Rubies.
38
+
4
39
  ## 0.2.2
5
40
 
6
41
  - Fixed native cleanup safety: native contexts are now released through `rb_ensure`, including Ruby exception paths from argument coercion, config reads, native status errors, output `String` allocation, and file-output failures.
data/README.md CHANGED
@@ -1,199 +1,177 @@
1
1
  # image_pack
2
2
 
3
- `image_pack` is a Ruby-native JPEG compressor prototype for Ruby `>= 3.1.0`.
3
+ Ruby-native JPEG compression and optimization backed by vendored pure-C MozJPEG/libjpeg.
4
4
 
5
- Current version: `0.2.2`.
6
-
7
- This pure-C variant intentionally removes Jpegli and any C++ toolchain requirement.
8
- The native layer is written in C and links against vendored MozJPEG, which is libjpeg-compatible and based on libjpeg-turbo.
9
-
10
- ## Backends / modes
5
+ No system `libjpeg`, `mozjpeg`, `git`, or `CMake` is required for gem users.
11
6
 
12
7
  ```ruby
13
- ImagePack.compress(input, algo: :jpeg_turbo)
14
- ImagePack.compress(input, algo: :mozjpeg)
8
+ gem "image_pack"
15
9
  ```
16
10
 
17
- - `:jpeg_turbo` / `:fast` — fast libjpeg-compatible mode with MozJPEG-specific size optimizations disabled.
18
- - `:mozjpeg` / `:size` — size-oriented mode using optimized Huffman coding and optional progressive output through the MozJPEG/libjpeg API.
19
-
20
- Both modes produce ordinary `.jpg` files.
21
-
22
- Important implementation note: this prototype uses one pure-C vendored codec family to avoid static-link symbol conflicts between separate libjpeg-compatible libraries. If later we need exact latest `libjpeg-turbo` and exact MozJPEG in one gem, the correct next step is linker isolation / separate native extensions with hidden symbols.
23
-
24
-
25
- ## Native extension layout
26
-
27
- For the first prototype the native layer intentionally lives in one file:
28
-
29
- ```text
30
- ext/image_pack/
31
- ├── extconf.rb
32
- └── image_pack.c
11
+ ```ruby
12
+ require "image_pack"
33
13
  ```
34
14
 
35
- This mirrors the early `multi_compress` style: one translation unit is easier to expand, debug and refactor while the API and codec boundaries are still moving. The code can be split into modules later, after the first working version is stable.
36
-
37
- ## API
15
+ ## Quick use
38
16
 
39
17
  ```ruby
40
- require "image_pack"
41
-
42
18
  jpeg = File.binread("photo.jpg")
43
19
 
44
- ImagePack.compress(jpeg, algo: :jpeg_turbo, quality: 82)
45
- ImagePack.compress(jpeg, algo: :mozjpeg, quality: 82)
20
+ small = ImagePack.compress_bytes(jpeg, quality: 82)
21
+ File.binwrite("photo.small.jpg", small)
46
22
 
47
- # SSIM-guarded compression: choose the smallest acceptable JPEG quality
48
- # while keeping decoded visual similarity >= 0.985.
49
- ImagePack.compress(jpeg, algo: :mozjpeg, min_ssim: 0.985)
50
-
51
- # Start at quality 75, but raise quality if needed to satisfy min_ssim.
52
- ImagePack.compress(jpeg, algo: :mozjpeg, quality: 75, min_ssim: 0.985)
23
+ ImagePack.compress_file("photo.jpg", output: "photo.small.jpg")
24
+ ImagePack.optimize_file("photo.jpg", output: "photo.optimized.jpg")
25
+ ```
53
26
 
54
- ImagePack.compress("photo.jpg", output: "photo.optimized.jpg", algo: :mozjpeg)
55
- ImagePack.compress_file("photo.jpg", output: "photo.optimized.jpg", algo: :mozjpeg)
56
- ImagePack.compress_bytes(jpeg, algo: :fast, quality: 82)
27
+ Prefer explicit helpers:
57
28
 
58
- # Lossless coefficient-level JPEG optimization/transcode.
59
- # This rewrites JPEG coefficients without decoding/re-encoding pixels.
60
- ImagePack.optimize_jpeg(jpeg, progressive: true, strip_metadata: false)
61
- ImagePack.optimize_file("photo.jpg", output: "photo.lossless.jpg")
29
+ ```ruby
30
+ ImagePack.compress_bytes(jpeg)
31
+ ImagePack.compress_file("photo.jpg", output: "out.jpg")
62
32
  ImagePack.optimize_bytes(jpeg)
33
+ ImagePack.optimize_file("photo.jpg", output: "out.jpg")
34
+ ```
63
35
 
64
- ImagePack.compress_pixels(rgb_buffer,
65
- width: 1920,
66
- height: 1080,
67
- channels: 3,
68
- algo: :mozjpeg
69
- )
36
+ ## Compression
70
37
 
71
- ImagePack.inspect_image(jpeg)
72
- # => { format: :jpeg, width: 1920, height: 1080, channels: 3, bit_depth: 8, decoded_bytes: 6220800 }
38
+ ```ruby
39
+ ImagePack.compress_bytes(jpeg,
40
+ algo: :size,
41
+ quality: 82,
42
+ strip_metadata: true
43
+ )
73
44
  ```
74
45
 
75
- ## Input / output policy
46
+ Algorithms:
76
47
 
77
- `compress(input, ...)` accepts JPEG input only:
48
+ - `:size` / `:mozjpeg` smaller files, default; uses optimized progressive MozJPEG output by default
49
+ - `:fast` / `:jpeg_turbo` — faster baseline mode
78
50
 
79
- - binary `String` (`ASCII-8BIT`) — JPEG bytes;
80
- - non-binary `String` that points to an existing file — file path;
81
- - `Pathname` — file path;
82
- - `IO::Buffer` — JPEG bytes, copied before native no-GVL execution for safety.
51
+ Common options:
83
52
 
84
- For non-ambiguous call sites prefer `compress_bytes(bytes, ...)` or `compress_file(path, ...)`.
85
-
86
- `compress_pixels(buffer, ...)` accepts raw pixels as binary `String` or `IO::Buffer`.
87
- `channels` must be `1`, `3`, or `4`. Alpha in RGBA input is dropped in v0.2.2; pass `drop_alpha: true` to make that explicit, or `drop_alpha: false` to reject it.
53
+ ```ruby
54
+ ImagePack.compress_bytes(jpeg, min_ssim: 0.985)
55
+ ImagePack.compress_bytes(jpeg, progressive: false) # force baseline output
56
+ ImagePack.compress_bytes(jpeg, strict: true)
57
+ ImagePack.compress_bytes(jpeg, report: true)
58
+ ```
88
59
 
89
- `output: nil` returns binary JPEG bytes. `output: String/Pathname` writes through a temporary file and renames it into place.
90
- Streaming output is intentionally not supported in v0.2.2.
91
60
 
92
- ## Lossless JPEG optimize
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.
93
62
 
94
- `optimize_jpeg`, `optimize_bytes`, and `optimize_file` are coefficient-level JPEG transcode helpers. They use `jpeg_read_coefficients` / `jpeg_write_coefficients`, so pixels are not decoded and re-encoded. This is the preferred path when you only want to rewrite an existing JPEG with optimized Huffman tables and optional progressive scans.
63
+ `algo: :fast` / `:jpeg_turbo` keeps baseline output by default and remains the throughput path.
95
64
 
96
- Defaults are intentionally conservative:
65
+ `min_ssim:` searches for the lowest acceptable quality using a fast native luma SSIM guard.
97
66
 
98
- - `progressive: true` creates a progressive optimized JPEG;
99
- - `strip_metadata: false` preserves APP/COM metadata by default because this path is meant to be visually/losslessly safe.
67
+ `strict: true` raises `ImagePack::InvalidImageError` on damaged/truncated JPEG warnings.
100
68
 
101
- If `strip_metadata: true` is requested and the source JPEG has EXIF Orientation, `optimize_jpeg` raises `UnsupportedError` instead of silently removing the orientation tag and changing how viewers display the image. Use `strip_metadata: false` for coefficient-level optimization, or `compress(..., strip_metadata: true)` if you want pixel-level orientation normalization.
69
+ `report: true` returns a Hash:
102
70
 
103
- ## SSIM guard
71
+ ```ruby
72
+ {
73
+ output: "\xFF\xD8...",
74
+ quality: 84,
75
+ ssim: 0.9861,
76
+ algo: :mozjpeg,
77
+ bytesize: 50122,
78
+ input_bytesize: 81344,
79
+ warning_count: 0,
80
+ warning: nil
81
+ }
82
+ ```
104
83
 
105
- `compress` and `compress_pixels` accept `min_ssim:`. This enables a native guarded path:
84
+ With `output: "file.jpg"`, `output` is `true`.
106
85
 
107
- 1. decode the original JPEG to reference pixels;
108
- 2. encode trial JPEG candidates with the existing MozJPEG/libjpeg backend;
109
- 3. decode each candidate;
110
- 4. compute a native luma SSIM score;
111
- 5. return the smallest quality that satisfies `SSIM >= min_ssim`.
86
+ ## Lossless optimize
112
87
 
113
- If only `min_ssim:` is provided, the search starts at quality `1`. If both `quality:`
114
- and `min_ssim:` are provided, `quality:` is treated as the lower starting point and
115
- the encoder raises quality only if the candidate violates the SSIM floor.
88
+ ```ruby
89
+ ImagePack.optimize_bytes(jpeg)
90
+ ImagePack.optimize_file("photo.jpg", output: "photo.optimized.jpg")
91
+ ```
116
92
 
117
- If no quality can satisfy the requested floor, `ImagePack::QualityConstraintError`
118
- is raised. With `execution: :auto`, guarded compression uses the no-GVL/offload
119
- path instead of the small-image direct path because it may encode/decode several
120
- candidates. For `compress_pixels`, the reference is the raw pixel buffer itself, not a seed JPEG. RGBA + `min_ssim` is rejected because JPEG cannot represent alpha.
93
+ This rewrites JPEG coefficients without decoding and re-encoding pixels. It is the right path for existing JPEGs when you only want optimized Huffman tables and optional progressive scans.
121
94
 
122
- The metric is a fast native luma SSIM-like guard based on 8x8 blocks, not a full Wang-style Gaussian-window reference implementation. It is intended as a compression guard, not as a general-purpose image quality benchmark.
95
+ Defaults: `progressive: true`, `strip_metadata: false`.
123
96
 
124
- ## Execution modes
97
+ If `strip_metadata: true` would remove EXIF Orientation, `optimize_jpeg` raises `UnsupportedError` instead of silently changing visual orientation.
125
98
 
126
- ### Ruby compatibility for offload
99
+ ## Raw pixels
127
100
 
128
- ImagePack supports Ruby `>= 3.1.0`. Ruby `>= 3.4.0` is required only for the scheduler-aware `execution: :offload` path.
101
+ ```ruby
102
+ ImagePack.compress_pixels(rgb,
103
+ width: 1920,
104
+ height: 1080,
105
+ channels: 3,
106
+ output: "frame.jpg"
107
+ )
108
+ ```
129
109
 
130
- On Ruby 3.13.3:
110
+ `channels` must be `1`, `3`, or `4`. JPEG cannot store alpha, so RGBA input needs explicit opt-in:
131
111
 
132
- - `execution: :direct` works.
133
- - `execution: :nogvl` works.
134
- - `execution: :auto` works, but never selects `:offload`.
135
- - explicit `execution: :offload` raises `ImagePack::UnsupportedError`.
112
+ ```ruby
113
+ ImagePack.compress_pixels(rgba, width: 100, height: 100, channels: 4, drop_alpha: true)
114
+ ```
136
115
 
137
- On Ruby 3.4+:
116
+ ## Inspect
138
117
 
139
- - `execution: :offload` is available.
140
- - `execution: :auto` may select `:offload` when a Fiber scheduler is active.
118
+ ```ruby
119
+ ImagePack.inspect_image(jpeg)
120
+ # => { format: :jpeg, width: 1920, height: 1080, channels: 3, bit_depth: 8, decoded_bytes: 6220800 }
121
+ ```
141
122
 
142
- Use `ImagePack.offload_safe?` or `ImagePack.build_info[:offload_safe]` to check the native capability at runtime.
123
+ ## Execution
143
124
 
125
+ Default mode is `:auto`.
144
126
 
145
127
  ```ruby
146
- ImagePack.compress(jpeg, execution: :direct)
147
- ImagePack.compress(jpeg, execution: :nogvl)
148
- ImagePack.compress(jpeg, execution: :offload)
149
- ImagePack.compress(jpeg, execution: :auto)
128
+ ImagePack.compress_bytes(jpeg, execution: :auto)
129
+ ImagePack.compress_bytes(jpeg, execution: :direct)
130
+ ImagePack.compress_bytes(jpeg, execution: :nogvl)
131
+ ImagePack.compress_bytes(jpeg, execution: :offload) # Ruby >= 3.4 only
150
132
  ```
151
133
 
152
- - `:direct` executes under GVL, intended for small images.
153
- - `:nogvl` — uses `rb_nogvl(..., flags: 0)`.
154
- - `:offload` — uses `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` on Ruby 3.4+; raises `ImagePack::UnsupportedError` on Ruby 3.1–3.3.
155
- - `:auto` — header-first policy chooses direct/nogvl/offload; on Ruby 3.1–3.3 it never selects offload.
156
-
157
- ## Cancellation
134
+ Use `ImagePack.offload_safe?` or `ImagePack.build_info` to inspect runtime support.
158
135
 
159
- `cancellable: true` is supported for `execution: :nogvl`, `:offload`, or `:auto`.
160
- Cancellation is cooperative at decode, encode, and SSIM-search checkpoints, not instant.
136
+ Set `IMAGE_PACK_DISABLE_OFFLOAD=1` before loading the gem to disable offload.
161
137
 
162
- ## Metadata / EXIF orientation
138
+ Long no-GVL/offload calls can be interrupted by raising into the worker thread:
163
139
 
164
- `strip_metadata: true` removes metadata. If the source JPEG contains EXIF Orientation, `image_pack` applies that orientation to decoded pixels before stripping metadata, so the visual orientation is preserved. With `strip_metadata: false`, APP/COM markers are preserved across both fast and size-oriented paths.
140
+ ```ruby
141
+ worker = Thread.new { ImagePack.compress_bytes(jpeg, execution: :nogvl, cancellable: true) }
142
+ worker.raise(ImagePack::CancelledError, "cancelled")
143
+ worker.join
144
+ ```
165
145
 
166
- ## Build info
146
+ ## Configuration
167
147
 
168
148
  ```ruby
169
- ImagePack.build_info
170
- # => { version: "0.2.2", mozjpeg: "4.1.5", simd: true, offload_safe: true }
149
+ ImagePack.configure do |config|
150
+ config.execution = :auto
151
+ config.max_input_size = 256 * 1024 * 1024
152
+ config.max_output_size = 256 * 1024 * 1024
153
+ config.max_pixels = 100_000_000
154
+ end
171
155
  ```
172
156
 
173
- ## Vendoring
174
-
175
- The gem is expected to ship vendored native sources/libs, so end users should not install system libjpeg/mozjpeg.
176
- For repository preparation:
157
+ ## Development
177
158
 
178
159
  ```bash
179
160
  bundle exec rake vendor
180
161
  bundle exec rake compile
162
+ bundle exec rake test
163
+ bundle exec rake release:check
181
164
  ```
182
165
 
183
166
  `rake vendor` pins MozJPEG `v4.1.5`.
184
167
 
185
- ## Current limitations
186
-
187
- - Pixel-level `compress` rejects CMYK/YCCK JPEG input because it decodes to RGB/gray before re-encoding. Use `optimize_jpeg` for coefficient-level lossless optimization of existing CMYK/YCCK JPEGs.
188
- - Arithmetic-coded JPEG support is disabled in the vendored `jconfig.h` for v0.2.2.
189
- - The SSIM guard is a fast 8x8 luma block metric and still assumes quality/SSIM monotonicity during binary search.
190
- - `compress(input, ...)` still has a legacy path-vs-bytes heuristic for non-binary `String`; prefer `compress_bytes` / `compress_file` and `optimize_bytes` / `optimize_file` in new code.
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.
191
169
 
192
- ## What is intentionally not included
170
+ ## Limits
193
171
 
194
- - Jpegli / C++ code
195
- - AVIF/WebP/PNG
196
- - FFI
197
- - shell-out
198
- - external tempfiles for in-memory output
199
- - byte-size targets / max-bytes policy
172
+ - JPEG only.
173
+ - Ruby `>= 3.1`; `execution: :offload` requires Ruby `>= 3.4`.
174
+ - Pixel-level `compress` rejects CMYK/YCCK JPEG input; use `optimize_jpeg` for existing CMYK/YCCK JPEGs.
175
+ - Arithmetic-coded JPEG support is disabled in `0.2.5`.
176
+ - Streaming output is not supported; file output uses atomic write-through-temp-file and rename.
177
+ - `ImagePack.compress(input, ...)` keeps a legacy path-vs-bytes heuristic; prefer explicit `*_bytes` / `*_file` helpers.
@@ -416,8 +416,8 @@ else
416
416
  $CFLAGS += " -O2"
417
417
  end
418
418
 
419
- $CFLAGS += " -DIMAGE_PACK_RUBY34_ONLY=1"
420
419
  $CFLAGS += build_info[:with_simd] ? " -DIMAGE_PACK_HAS_SIMD=1" : " -DIMAGE_PACK_PURE_C=1"
420
+ $CPPFLAGS += " -DIMAGE_PACK_MOZJPEG_VERSION=\\\"#{MOZJPEG_VERSION}\\\""
421
421
  $LIBS += " -lm" unless msvc?
422
422
 
423
423
  have_header("ruby/thread.h") or abort "ruby/thread.h not found"
@@ -40,6 +40,10 @@
40
40
  #define TRUE 1
41
41
  #endif
42
42
 
43
+ #ifndef IMAGE_PACK_MOZJPEG_VERSION
44
+ #define IMAGE_PACK_MOZJPEG_VERSION VERSION
45
+ #endif
46
+
43
47
  #ifndef FALSE
44
48
  #define FALSE 0
45
49
  #endif
@@ -133,6 +137,7 @@ typedef struct {
133
137
  int progressive;
134
138
  int strip_metadata;
135
139
  int mozjpeg_trellis_enabled;
140
+ int mozjpeg_scan_opt_enabled;
136
141
  ip_algo_t algo;
137
142
  ip_execution_t requested_execution;
138
143
  ip_execution_t resolved_execution;
@@ -151,6 +156,9 @@ typedef struct {
151
156
 
152
157
  atomic_int cancelled;
153
158
  int cancellable_requested;
159
+ int strict;
160
+ int warning_count;
161
+ char first_warning[200];
154
162
 
155
163
  jmp_buf jmpbuf;
156
164
  int jmp_armed;
@@ -166,6 +174,7 @@ typedef struct {
166
174
  unsigned char *transient_jpeg_buf;
167
175
  unsigned char *transient_decode_buf;
168
176
  int source_orientation;
177
+ int decoded_as_ycbcr;
169
178
  } ip_context_t;
170
179
 
171
180
  typedef struct {
@@ -174,6 +183,7 @@ typedef struct {
174
183
  } ip_jpeg_error_mgr;
175
184
 
176
185
  static VALUE rb_mImagePack;
186
+ static int ip_offload_runtime_enabled = 1;
177
187
  static VALUE rb_eImagePackError;
178
188
  static VALUE rb_eImagePackInvalidArgumentError;
179
189
  static VALUE rb_eImagePackInvalidImageError;
@@ -243,7 +253,8 @@ static int ip_run_context(ip_context_t *ctx);
243
253
  static void validate_limits_for_pixels(ip_context_t *ctx);
244
254
 
245
255
  static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, int *width,
246
- int *height, int *channels, int fast_decode_mode);
256
+ int *height, int *channels, int fast_decode_mode,
257
+ int allow_ycbcr_transcode);
247
258
  static int ip_decode_jpeg_to_luma_buffer(ip_context_t *ctx, const unsigned char *data, size_t size,
248
259
  unsigned char **luma, int *width, int *height);
249
260
  static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mode);
@@ -255,18 +266,20 @@ static int ip_run_optimize_context(ip_context_t *ctx);
255
266
  typedef struct {
256
267
  VALUE self, input, input_kind, output, output_kind, algo, quality, min_ssim;
257
268
  VALUE mozjpeg_trellis, progressive, strip_metadata, execution, cancellable, has_scheduler;
269
+ VALUE report, strict, mozjpeg_scan_opt;
258
270
  ip_context_t *ctx;
259
271
  } ip_compress_jpeg_call_t;
260
272
 
261
273
  typedef struct {
262
274
  VALUE self, buffer, width, height, channels, output, output_kind, algo, quality, min_ssim;
263
275
  VALUE mozjpeg_trellis, progressive, exact_size, execution, cancellable, has_scheduler;
276
+ VALUE report, strict, mozjpeg_scan_opt;
264
277
  ip_context_t *ctx;
265
278
  } ip_compress_pixels_call_t;
266
279
 
267
280
  typedef struct {
268
281
  VALUE self, input, input_kind, output, output_kind, progressive, strip_metadata;
269
- VALUE execution, cancellable, has_scheduler;
282
+ VALUE execution, cancellable, has_scheduler, strict;
270
283
  ip_context_t *ctx;
271
284
  } ip_optimize_jpeg_call_t;
272
285
 
@@ -284,18 +297,12 @@ static VALUE ip_call_cleanup(VALUE ptr) {
284
297
  return Qnil;
285
298
  }
286
299
 
287
- static VALUE ip_compress_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
288
- VALUE output_kind, VALUE algo, VALUE quality, VALUE min_ssim,
289
- VALUE mozjpeg_trellis, VALUE progressive, VALUE strip_metadata,
290
- VALUE execution, VALUE cancellable, VALUE has_scheduler);
291
- static VALUE ip_compress_pixels_entry(VALUE self, VALUE buffer, VALUE width, VALUE height,
292
- VALUE channels, VALUE output, VALUE output_kind, VALUE algo,
293
- VALUE quality, VALUE min_ssim, VALUE mozjpeg_trellis,
294
- VALUE progressive, VALUE exact_size, VALUE execution,
295
- VALUE cancellable, VALUE has_scheduler);
300
+ static VALUE ip_compress_jpeg_entry(int argc, VALUE *argv, VALUE self);
301
+ static VALUE ip_compress_pixels_entry(int argc, VALUE *argv, VALUE self);
296
302
  static VALUE ip_optimize_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
297
303
  VALUE output_kind, VALUE progressive, VALUE strip_metadata,
298
- VALUE execution, VALUE cancellable, VALUE has_scheduler);
304
+ VALUE execution, VALUE cancellable, VALUE has_scheduler,
305
+ VALUE strict);
299
306
 
300
307
  static VALUE ip_status_to_exception(ip_status_t status) {
301
308
  switch (status) {
@@ -440,6 +447,7 @@ static ip_context_t *ip_context_new(void) {
440
447
  ctx->status = IP_OK;
441
448
  ctx->quality = 82;
442
449
  ctx->mozjpeg_trellis_enabled = 1;
450
+ ctx->mozjpeg_scan_opt_enabled = 1;
443
451
  ctx->selected_quality = 82;
444
452
  ctx->requested_execution = IP_EXEC_AUTO;
445
453
  ctx->resolved_execution = IP_EXEC_AUTO;
@@ -543,10 +551,43 @@ static VALUE ip_sym(const char *name) {
543
551
  return ID2SYM(rb_intern(name));
544
552
  }
545
553
 
554
+ static void ip_jpeg_emit_message_collect(j_common_ptr cinfo, int msg_level) {
555
+ if (msg_level >= 0)
556
+ return;
557
+
558
+ ip_jpeg_error_mgr *err = (ip_jpeg_error_mgr *)cinfo->err;
559
+ ip_context_t *ctx = err->ctx;
560
+ if (!ctx)
561
+ return;
562
+
563
+ ctx->warning_count++;
564
+ if (ctx->first_warning[0] == '\0') {
565
+ char buffer[JMSG_LENGTH_MAX];
566
+ (*cinfo->err->format_message)(cinfo, buffer);
567
+ snprintf(ctx->first_warning, sizeof(ctx->first_warning), "%s", buffer);
568
+ }
569
+
570
+ if (ctx->strict) {
571
+ if (ctx->status == IP_OK) {
572
+ char buffer[JMSG_LENGTH_MAX];
573
+ (*cinfo->err->format_message)(cinfo, buffer);
574
+ ip_context_set_error(ctx, IP_ERR_INVALID_IMAGE, buffer);
575
+ }
576
+ if (ctx->jmp_armed)
577
+ longjmp(ctx->jmpbuf, 1);
578
+ }
579
+ }
580
+
581
+ static void ip_jpeg_output_message_silent(j_common_ptr cinfo) {
582
+ (void)cinfo;
583
+ }
584
+
546
585
  static struct jpeg_error_mgr *ip_use_error(ip_jpeg_error_mgr *jerr, ip_context_t *ctx,
547
586
  void (*handler)(j_common_ptr)) {
548
587
  struct jpeg_error_mgr *base = jpeg_std_error(&jerr->pub);
549
588
  jerr->pub.error_exit = handler;
589
+ jerr->pub.emit_message = ip_jpeg_emit_message_collect;
590
+ jerr->pub.output_message = ip_jpeg_output_message_silent;
550
591
  jerr->ctx = ctx;
551
592
  return base;
552
593
  }
@@ -876,6 +917,23 @@ static VALUE ip_finish_output(ip_context_t *ctx, ip_output_kind_t kind) {
876
917
  return Qtrue;
877
918
  }
878
919
 
920
+ static VALUE ip_build_report(ip_context_t *ctx, VALUE output_value) {
921
+ VALUE hash = rb_hash_new();
922
+ rb_hash_aset(hash, ip_sym("output"), output_value);
923
+ rb_hash_aset(hash, ip_sym("quality"), INT2NUM(ctx->selected_quality));
924
+ rb_hash_aset(hash, ip_sym("ssim"),
925
+ ctx->ssim_guard_enabled ? DBL2NUM(ctx->measured_ssim) : Qnil);
926
+ rb_hash_aset(hash, ip_sym("algo"),
927
+ ctx->algo == IP_ALGO_MOZJPEG ? ip_sym("mozjpeg") : ip_sym("jpeg_turbo"));
928
+ rb_hash_aset(hash, ip_sym("bytesize"), SIZET2NUM(ctx->output_size));
929
+ rb_hash_aset(hash, ip_sym("input_bytesize"),
930
+ SIZET2NUM(ctx->input_size > 0 ? ctx->input_size : ctx->pixel_size));
931
+ rb_hash_aset(hash, ip_sym("warning_count"), INT2NUM(ctx->warning_count));
932
+ rb_hash_aset(hash, ip_sym("warning"),
933
+ ctx->first_warning[0] ? rb_str_new_cstr(ctx->first_warning) : Qnil);
934
+ return hash;
935
+ }
936
+
879
937
  static int ip_save_marker(ip_context_t *ctx, int marker, const unsigned char *data,
880
938
  unsigned int len) {
881
939
  if (ctx->preserved_marker_count == ctx->preserved_marker_capacity) {
@@ -1219,13 +1277,21 @@ static void configure_mozjpeg_profile_before_defaults(struct jpeg_compress_struc
1219
1277
  static void configure_mozjpeg_features_after_defaults(struct jpeg_compress_struct *cinfo,
1220
1278
  int mozjpeg_size_mode,
1221
1279
  int progressive_requested,
1222
- int mozjpeg_trellis_enabled) {
1280
+ int mozjpeg_trellis_enabled,
1281
+ int scan_opt_enabled, int measurement) {
1223
1282
  if (mozjpeg_size_mode) {
1224
1283
  cinfo->optimize_coding = TRUE;
1225
1284
 
1226
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);
1227
1293
  jpeg_simple_progression(cinfo);
1228
- jpeg_c_set_bool_param(cinfo, JBOOLEAN_OPTIMIZE_SCANS, TRUE);
1294
+ jpeg_c_set_bool_param(cinfo, JBOOLEAN_OPTIMIZE_SCANS, run_scan_search);
1229
1295
  } else {
1230
1296
  cinfo->scan_info = NULL;
1231
1297
  cinfo->num_scans = 0;
@@ -1250,7 +1316,7 @@ static void configure_mozjpeg_features_after_defaults(struct jpeg_compress_struc
1250
1316
  }
1251
1317
  }
1252
1318
 
1253
- 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) {
1254
1320
  struct jpeg_compress_struct cinfo;
1255
1321
  ip_jpeg_error_mgr jerr;
1256
1322
  unsigned long jpeg_size = 0;
@@ -1272,14 +1338,19 @@ static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode)
1272
1338
  cinfo.image_width = (JDIMENSION)ctx->width;
1273
1339
  cinfo.image_height = (JDIMENSION)ctx->height;
1274
1340
  cinfo.input_components = ctx->channels;
1275
- cinfo.in_color_space =
1276
- 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
+ }
1277
1347
 
1278
1348
  configure_mozjpeg_profile_before_defaults(&cinfo, mozjpeg_size_mode);
1279
1349
  jpeg_set_defaults(&cinfo);
1280
1350
  jpeg_set_quality(&cinfo, ctx->quality, TRUE);
1281
1351
  configure_mozjpeg_features_after_defaults(&cinfo, mozjpeg_size_mode, ctx->progressive,
1282
- ctx->mozjpeg_trellis_enabled);
1352
+ ctx->mozjpeg_trellis_enabled,
1353
+ ctx->mozjpeg_scan_opt_enabled, measurement);
1283
1354
 
1284
1355
  jpeg_start_compress(&cinfo, TRUE);
1285
1356
 
@@ -1287,7 +1358,7 @@ static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode)
1287
1358
  ip_write_preserved_markers(ctx, &cinfo);
1288
1359
 
1289
1360
  while (cinfo.next_scanline < cinfo.image_height) {
1290
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
1361
+ if (atomic_load(&ctx->cancelled))
1291
1362
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "JPEG encode cancelled");
1292
1363
 
1293
1364
  JSAMPROW rows[16];
@@ -1305,7 +1376,7 @@ static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode)
1305
1376
 
1306
1377
  jpeg_finish_compress(&cinfo);
1307
1378
 
1308
- 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)
1309
1380
  IP_FAIL_GOTO(ctx, IP_ERR_LIMIT, "output exceeds max_output_size");
1310
1381
 
1311
1382
  jpeg_destroy_compress(&cinfo);
@@ -1326,7 +1397,8 @@ fail:
1326
1397
  }
1327
1398
 
1328
1399
  static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, int *width,
1329
- int *height, int *channels, int fast_decode_mode) {
1400
+ int *height, int *channels, int fast_decode_mode,
1401
+ int allow_ycbcr_transcode) {
1330
1402
  struct jpeg_decompress_struct cinfo;
1331
1403
  ip_jpeg_error_mgr jerr;
1332
1404
  memset(&cinfo, 0, sizeof(cinfo));
@@ -1334,6 +1406,7 @@ static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, i
1334
1406
  cinfo.err = ip_use_error(&jerr, ctx, ip_jpeg_invalid_error_exit);
1335
1407
  ctx->transient_decode_buf = NULL;
1336
1408
  ctx->source_orientation = 1;
1409
+ ctx->decoded_as_ycbcr = 0;
1337
1410
 
1338
1411
  ctx->jmp_armed = 1;
1339
1412
  if (setjmp(ctx->jmpbuf)) {
@@ -1377,7 +1450,9 @@ static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, i
1377
1450
  if (ctx->status != IP_OK)
1378
1451
  goto fail;
1379
1452
 
1380
- 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;
1381
1456
  if (fast_decode_mode)
1382
1457
  ip_apply_fast_decode(&cinfo);
1383
1458
 
@@ -1398,7 +1473,7 @@ static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, i
1398
1473
  IP_FAIL_GOTO(ctx, IP_ERR_OOM, "failed to allocate decoded pixel buffer");
1399
1474
 
1400
1475
  while (cinfo.output_scanline < cinfo.output_height) {
1401
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
1476
+ if (atomic_load(&ctx->cancelled))
1402
1477
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "JPEG decode cancelled");
1403
1478
 
1404
1479
  JSAMPROW rows[16];
@@ -1455,14 +1530,14 @@ fail:
1455
1530
 
1456
1531
  static int compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mode) {
1457
1532
  if (ctx->pixel_data) {
1458
- return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode);
1533
+ return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode, 0);
1459
1534
  }
1460
1535
 
1461
1536
  unsigned char *pixels = NULL;
1462
1537
  int width = 0;
1463
1538
  int height = 0;
1464
1539
  int channels = 0;
1465
- 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))
1466
1541
  return 0;
1467
1542
 
1468
1543
  ctx->owned_pixel_data = pixels;
@@ -1473,7 +1548,7 @@ static int compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mod
1473
1548
  ctx->channels = channels;
1474
1549
  ctx->decoded_bytes = ctx->pixel_size;
1475
1550
 
1476
- return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode);
1551
+ return encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode, 0);
1477
1552
  }
1478
1553
 
1479
1554
  static void ip_clear_output_buffer(ip_context_t *ctx) {
@@ -1558,7 +1633,7 @@ static int ip_decode_jpeg_to_luma_buffer(ip_context_t *ctx, const unsigned char
1558
1633
  IP_FAIL_GOTO(ctx, IP_ERR_OOM, "failed to allocate luma buffer");
1559
1634
 
1560
1635
  while (cinfo.output_scanline < cinfo.output_height) {
1561
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
1636
+ if (atomic_load(&ctx->cancelled))
1562
1637
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "JPEG luma decode cancelled");
1563
1638
 
1564
1639
  JSAMPROW rows[16];
@@ -1763,6 +1838,49 @@ static double ip_compute_ssim_luma_buffer(const unsigned char *a, const unsigned
1763
1838
  return windows > 0 ? total_ssim / (double)windows : 0.0;
1764
1839
  }
1765
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
+
1766
1884
  static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_size_mode) {
1767
1885
  unsigned char *reference_pixels = NULL;
1768
1886
  int reference_width = 0;
@@ -1776,7 +1894,7 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1776
1894
  reference_channels = ctx->channels;
1777
1895
  } else {
1778
1896
  if (!ip_jpeg_decode_to_pixels(ctx, &reference_pixels, &reference_width, &reference_height,
1779
- &reference_channels, 1)) {
1897
+ &reference_channels, 1, 0)) {
1780
1898
  return 0;
1781
1899
  }
1782
1900
 
@@ -1791,8 +1909,7 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1791
1909
  }
1792
1910
 
1793
1911
  if (reference_channels == 4) {
1794
- ip_context_set_error(ctx, IP_ERR_UNSUPPORTED,
1795
- "min_ssim is not supported for RGBA input in v0.2.2");
1912
+ ip_context_set_error(ctx, IP_ERR_UNSUPPORTED, "min_ssim is not supported for RGBA input");
1796
1913
  return 0;
1797
1914
  }
1798
1915
 
@@ -1809,59 +1926,45 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1809
1926
  size_t best_jpeg_size = 0;
1810
1927
  double best_seen_ssim = 0.0;
1811
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;
1812
1933
 
1813
1934
  while (search_low <= search_high) {
1814
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled)) {
1935
+ if (atomic_load(&ctx->cancelled)) {
1815
1936
  free(reference_luma);
1816
1937
  free(best_jpeg);
1817
1938
  ip_context_set_error(ctx, IP_ERR_CANCELLED, "SSIM-guarded JPEG encode cancelled");
1818
1939
  return 0;
1819
1940
  }
1820
1941
 
1821
- int trial_quality = search_low + ((search_high - search_low) / 2);
1822
- ctx->quality = trial_quality;
1823
- ip_clear_output_buffer(ctx);
1824
-
1825
- if (!encode_pixels_with_libjpeg(ctx, mozjpeg_size_mode)) {
1826
- free(reference_luma);
1827
- free(best_jpeg);
1828
- 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);
1829
1953
  }
1830
1954
 
1831
- unsigned char *candidate_jpeg = ctx->output_data;
1832
- size_t candidate_jpeg_size = ctx->output_size;
1833
- ctx->output_data = NULL;
1834
- ctx->output_size = 0;
1835
- ctx->output_owner = IP_OUTPUT_OWNER_NONE;
1836
-
1837
- unsigned char *candidate_luma = NULL;
1838
- int candidate_width = 0;
1839
- int candidate_height = 0;
1840
- int decoded_ok =
1841
- ip_decode_jpeg_to_luma_buffer(ctx, candidate_jpeg, candidate_jpeg_size, &candidate_luma,
1842
- &candidate_width, &candidate_height);
1843
-
1844
- if (!decoded_ok) {
1845
- free(reference_luma);
1846
- free(candidate_jpeg);
1847
- free(best_jpeg);
1848
- return 0;
1849
- }
1955
+ ctx->quality = trial_quality;
1850
1956
 
1851
- 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)) {
1852
1963
  free(reference_luma);
1853
- free(candidate_luma);
1854
- free(candidate_jpeg);
1855
1964
  free(best_jpeg);
1856
- ip_context_set_error(ctx, IP_ERR_ENCODE,
1857
- "candidate JPEG dimensions differ from reference image");
1858
1965
  return 0;
1859
1966
  }
1860
1967
 
1861
- double ssim = ip_compute_ssim_luma_buffer(reference_luma, candidate_luma, reference_width,
1862
- reference_height);
1863
- free(candidate_luma);
1864
-
1865
1968
  if (ssim > best_seen_ssim) {
1866
1969
  best_seen_ssim = ssim;
1867
1970
  best_seen_quality = trial_quality;
@@ -1873,9 +1976,15 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1873
1976
  best_jpeg_size = candidate_jpeg_size;
1874
1977
  best_quality = trial_quality;
1875
1978
  best_ssim = ssim;
1979
+ q_hi = trial_quality;
1980
+ s_hi = ssim;
1981
+ have_hi = 1;
1876
1982
  search_high = trial_quality - 1;
1877
1983
  } else {
1878
1984
  free(candidate_jpeg);
1985
+ q_lo = trial_quality;
1986
+ s_lo = ssim;
1987
+ have_lo = 1;
1879
1988
  search_low = trial_quality + 1;
1880
1989
  }
1881
1990
  }
@@ -1890,13 +1999,75 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1890
1999
  return 0;
1891
2000
  }
1892
2001
 
1893
- ctx->quality = best_quality;
1894
- ctx->selected_quality = best_quality;
1895
- 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
+
1896
2064
  free(reference_luma);
1897
2065
 
1898
- ctx->output_data = best_jpeg;
1899
- 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;
1900
2071
  ctx->output_owner = IP_OUTPUT_OWNER_MALLOC;
1901
2072
  return 1;
1902
2073
  }
@@ -1959,7 +2130,7 @@ static int ip_lossless_optimize_jpeg(ip_context_t *ctx) {
1959
2130
  jpeg_mem_src(&srcinfo, ctx->input_data, (unsigned long)ctx->input_size);
1960
2131
  ip_setup_marker_saving(&srcinfo, ctx->strip_metadata);
1961
2132
 
1962
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
2133
+ if (atomic_load(&ctx->cancelled))
1963
2134
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "lossless JPEG optimize cancelled");
1964
2135
 
1965
2136
  if (jpeg_read_header(&srcinfo, TRUE) != JPEG_HEADER_OK)
@@ -1989,7 +2160,7 @@ static int ip_lossless_optimize_jpeg(ip_context_t *ctx) {
1989
2160
 
1990
2161
  jpeg_mem_dest(&dstinfo, &ctx->transient_jpeg_buf, &jpeg_size);
1991
2162
 
1992
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
2163
+ if (atomic_load(&ctx->cancelled))
1993
2164
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "lossless JPEG optimize cancelled");
1994
2165
 
1995
2166
  jpeg_write_coefficients(&dstinfo, coef_arrays);
@@ -2035,7 +2206,7 @@ static int ip_mozjpeg_compress(ip_context_t *ctx) {
2035
2206
 
2036
2207
  static ip_execution_t ip_async_execution(const ip_context_t *ctx) {
2037
2208
  #if IMAGE_PACK_HAS_OFFLOAD_SAFE
2038
- return ctx->has_scheduler ? IP_EXEC_OFFLOAD : IP_EXEC_NOGVL;
2209
+ return (ip_offload_runtime_enabled && ctx->has_scheduler) ? IP_EXEC_OFFLOAD : IP_EXEC_NOGVL;
2039
2210
  #else
2040
2211
  (void)ctx;
2041
2212
  return IP_EXEC_NOGVL;
@@ -2105,12 +2276,16 @@ static int ip_run_context(ip_context_t *ctx) {
2105
2276
  rb_nogvl(ip_run_encode_nogvl, ctx, ip_unblock_function, ctx, RB_NOGVL_OFFLOAD_SAFE);
2106
2277
  #else
2107
2278
  ip_context_set_error(ctx, IP_ERR_UNSUPPORTED,
2108
- "offload execution requires Ruby >= 3.4; use :nogvl or :auto");
2279
+ "offload execution is unavailable in this runtime; it requires Ruby "
2280
+ ">= 3.4 and IMAGE_PACK_DISABLE_OFFLOAD must not be set");
2109
2281
  #endif
2110
2282
  } else {
2111
2283
  ip_context_set_error(ctx, IP_ERR_INVALID_ARGUMENT, "invalid resolved execution mode");
2112
2284
  }
2113
2285
 
2286
+ if (ctx->resolved_execution != IP_EXEC_DIRECT)
2287
+ rb_thread_check_ints();
2288
+
2114
2289
  return ctx->status == IP_OK;
2115
2290
  }
2116
2291
 
@@ -2133,12 +2308,16 @@ static int ip_run_optimize_context(ip_context_t *ctx) {
2133
2308
  rb_nogvl(ip_run_optimize_nogvl, ctx, ip_unblock_function, ctx, RB_NOGVL_OFFLOAD_SAFE);
2134
2309
  #else
2135
2310
  ip_context_set_error(ctx, IP_ERR_UNSUPPORTED,
2136
- "offload execution requires Ruby >= 3.4; use :nogvl or :auto");
2311
+ "offload execution is unavailable in this runtime; it requires Ruby "
2312
+ ">= 3.4 and IMAGE_PACK_DISABLE_OFFLOAD must not be set");
2137
2313
  #endif
2138
2314
  } else {
2139
2315
  ip_context_set_error(ctx, IP_ERR_INVALID_ARGUMENT, "invalid resolved execution mode");
2140
2316
  }
2141
2317
 
2318
+ if (ctx->resolved_execution != IP_EXEC_DIRECT)
2319
+ rb_thread_check_ints();
2320
+
2142
2321
  return ctx->status == IP_OK;
2143
2322
  }
2144
2323
 
@@ -2215,11 +2394,13 @@ static VALUE ip_compress_jpeg_entry_body(VALUE ptr) {
2215
2394
  ctx->ssim_guard_enabled = ctx->min_ssim > 0.0;
2216
2395
  ip_validate_min_ssim_or_raise(ctx);
2217
2396
  ctx->mozjpeg_trellis_enabled = ip_bool_value(call->mozjpeg_trellis);
2397
+ ctx->mozjpeg_scan_opt_enabled = ip_bool_value(call->mozjpeg_scan_opt);
2218
2398
  ctx->progressive = ip_bool_value(call->progressive);
2219
2399
  ctx->strip_metadata = ip_bool_value(call->strip_metadata);
2220
2400
  ctx->requested_execution = ip_parse_execution(call->execution);
2221
2401
  ctx->cancellable_requested = ip_bool_value(call->cancellable);
2222
2402
  ctx->has_scheduler = ip_bool_value(call->has_scheduler);
2403
+ ctx->strict = ip_bool_value(call->strict);
2223
2404
  apply_configuration(call->self, ctx);
2224
2405
 
2225
2406
  ip_input_kind_t in_kind = ip_parse_input_kind(call->input_kind);
@@ -2242,17 +2423,18 @@ static VALUE ip_compress_jpeg_entry_body(VALUE ptr) {
2242
2423
  }
2243
2424
 
2244
2425
  ip_run_context(ctx);
2426
+ if (ip_bool_value(call->report)) {
2427
+ VALUE output_value = ip_finish_output(ctx, out_kind);
2428
+ return ip_build_report(ctx, output_value);
2429
+ }
2245
2430
  return ip_finish_output(ctx, out_kind);
2246
2431
  }
2247
2432
 
2248
- static VALUE ip_compress_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
2249
- VALUE output_kind, VALUE algo, VALUE quality, VALUE min_ssim,
2250
- VALUE mozjpeg_trellis, VALUE progressive, VALUE strip_metadata,
2251
- VALUE execution, VALUE cancellable, VALUE has_scheduler) {
2252
- ip_compress_jpeg_call_t call = {
2253
- self, input, input_kind, output, output_kind,
2254
- algo, quality, min_ssim, mozjpeg_trellis, progressive,
2255
- strip_metadata, execution, cancellable, has_scheduler, 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};
2256
2438
  return rb_ensure(ip_compress_jpeg_entry_body, (VALUE)&call, ip_call_cleanup, (VALUE)&call.ctx);
2257
2439
  }
2258
2440
 
@@ -2272,11 +2454,13 @@ static VALUE ip_compress_pixels_entry_body(VALUE ptr) {
2272
2454
  ctx->ssim_guard_enabled = ctx->min_ssim > 0.0;
2273
2455
  ip_validate_min_ssim_or_raise(ctx);
2274
2456
  ctx->mozjpeg_trellis_enabled = ip_bool_value(call->mozjpeg_trellis);
2457
+ ctx->mozjpeg_scan_opt_enabled = ip_bool_value(call->mozjpeg_scan_opt);
2275
2458
  ctx->progressive = ip_bool_value(call->progressive);
2276
2459
  ctx->strip_metadata = 1;
2277
2460
  ctx->requested_execution = ip_parse_execution(call->execution);
2278
2461
  ctx->cancellable_requested = ip_bool_value(call->cancellable);
2279
2462
  ctx->has_scheduler = ip_bool_value(call->has_scheduler);
2463
+ ctx->strict = ip_bool_value(call->strict);
2280
2464
  apply_configuration(call->self, ctx);
2281
2465
 
2282
2466
  if (!ip_prepare_output_path(ctx, call->output, out_kind) ||
@@ -2297,18 +2481,19 @@ static VALUE ip_compress_pixels_entry_body(VALUE ptr) {
2297
2481
  }
2298
2482
 
2299
2483
  ip_run_context(ctx);
2484
+ if (ip_bool_value(call->report)) {
2485
+ VALUE output_value = ip_finish_output(ctx, out_kind);
2486
+ return ip_build_report(ctx, output_value);
2487
+ }
2300
2488
  return ip_finish_output(ctx, out_kind);
2301
2489
  }
2302
2490
 
2303
- static VALUE ip_compress_pixels_entry(VALUE self, VALUE buffer, VALUE width, VALUE height,
2304
- VALUE channels, VALUE output, VALUE output_kind, VALUE algo,
2305
- VALUE quality, VALUE min_ssim, VALUE mozjpeg_trellis,
2306
- VALUE progressive, VALUE exact_size, VALUE execution,
2307
- VALUE cancellable, VALUE has_scheduler) {
2308
- ip_compress_pixels_call_t call = {
2309
- self, buffer, width, height, channels, output, output_kind,
2310
- algo, quality, min_ssim, mozjpeg_trellis, progressive, exact_size, execution,
2311
- cancellable, has_scheduler, NULL};
2491
+ static VALUE ip_compress_pixels_entry(int argc, VALUE *argv, VALUE self) {
2492
+ rb_check_arity(argc, 18, 18);
2493
+ ip_compress_pixels_call_t call = {self, argv[0], argv[1], argv[2], argv[3],
2494
+ argv[4], argv[5], argv[6], argv[7], argv[8],
2495
+ argv[9], argv[10], argv[11], argv[12], argv[13],
2496
+ argv[14], argv[15], argv[16], argv[17], NULL};
2312
2497
  return rb_ensure(ip_compress_pixels_entry_body, (VALUE)&call, ip_call_cleanup,
2313
2498
  (VALUE)&call.ctx);
2314
2499
  }
@@ -2326,6 +2511,7 @@ static VALUE ip_optimize_jpeg_entry_body(VALUE ptr) {
2326
2511
  ctx->requested_execution = ip_parse_execution(call->execution);
2327
2512
  ctx->cancellable_requested = ip_bool_value(call->cancellable);
2328
2513
  ctx->has_scheduler = ip_bool_value(call->has_scheduler);
2514
+ ctx->strict = ip_bool_value(call->strict);
2329
2515
  ctx->ssim_guard_enabled = 0;
2330
2516
  apply_configuration(call->self, ctx);
2331
2517
 
@@ -2354,10 +2540,11 @@ static VALUE ip_optimize_jpeg_entry_body(VALUE ptr) {
2354
2540
 
2355
2541
  static VALUE ip_optimize_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
2356
2542
  VALUE output_kind, VALUE progressive, VALUE strip_metadata,
2357
- VALUE execution, VALUE cancellable, VALUE has_scheduler) {
2358
- ip_optimize_jpeg_call_t call = {
2359
- self, input, input_kind, output, output_kind, progressive,
2360
- strip_metadata, execution, cancellable, has_scheduler, NULL};
2543
+ VALUE execution, VALUE cancellable, VALUE has_scheduler,
2544
+ VALUE strict) {
2545
+ ip_optimize_jpeg_call_t call = {self, input, input_kind, output,
2546
+ output_kind, progressive, strip_metadata, execution,
2547
+ cancellable, has_scheduler, strict, NULL};
2361
2548
  return rb_ensure(ip_optimize_jpeg_entry_body, (VALUE)&call, ip_call_cleanup, (VALUE)&call.ctx);
2362
2549
  }
2363
2550
 
@@ -2403,14 +2590,17 @@ IMAGE_PACK_INIT_EXPORT void Init_image_pack(void) {
2403
2590
  for (size_t i = 0; i < IP_ARRAY_LEN(exceptions); i++)
2404
2591
  *exceptions[i].slot = rb_const_get(rb_mImagePack, rb_intern(exceptions[i].name));
2405
2592
 
2406
- rb_define_const(rb_mImagePack, "NATIVE_MOZJPEG_VERSION", rb_str_new_cstr(VERSION));
2593
+ rb_define_const(rb_mImagePack, "NATIVE_MOZJPEG_VERSION",
2594
+ rb_str_new_cstr(IMAGE_PACK_MOZJPEG_VERSION));
2407
2595
  #if defined(IMAGE_PACK_HAS_SIMD)
2408
2596
  rb_define_const(rb_mImagePack, "NATIVE_SIMD", Qtrue);
2409
2597
  #else
2410
2598
  rb_define_const(rb_mImagePack, "NATIVE_SIMD", Qfalse);
2411
2599
  #endif
2600
+ ip_offload_runtime_enabled = (getenv("IMAGE_PACK_DISABLE_OFFLOAD") == NULL) ? 1 : 0;
2412
2601
  #if IMAGE_PACK_HAS_OFFLOAD_SAFE
2413
- rb_define_const(rb_mImagePack, "NATIVE_OFFLOAD_SAFE", Qtrue);
2602
+ rb_define_const(rb_mImagePack, "NATIVE_OFFLOAD_SAFE",
2603
+ ip_offload_runtime_enabled ? Qtrue : Qfalse);
2414
2604
  #else
2415
2605
  rb_define_const(rb_mImagePack, "NATIVE_OFFLOAD_SAFE", Qfalse);
2416
2606
  #endif
@@ -2419,9 +2609,9 @@ IMAGE_PACK_INIT_EXPORT void Init_image_pack(void) {
2419
2609
  const char *name;
2420
2610
  VALUE (*fn)(ANYARGS);
2421
2611
  int arity;
2422
- } methods[] = {{"__compress_jpeg", (VALUE (*)(ANYARGS))ip_compress_jpeg_entry, 13},
2423
- {"__compress_pixels", (VALUE (*)(ANYARGS))ip_compress_pixels_entry, 15},
2424
- {"__optimize_jpeg", (VALUE (*)(ANYARGS))ip_optimize_jpeg_entry, 9},
2612
+ } methods[] = {{"__compress_jpeg", (VALUE (*)(ANYARGS))ip_compress_jpeg_entry, -1},
2613
+ {"__compress_pixels", (VALUE (*)(ANYARGS))ip_compress_pixels_entry, -1},
2614
+ {"__optimize_jpeg", (VALUE (*)(ANYARGS))ip_optimize_jpeg_entry, 10},
2425
2615
  {"__inspect_image", (VALUE (*)(ANYARGS))ip_inspect_image_entry, 2}};
2426
2616
  for (size_t i = 0; i < IP_ARRAY_LEN(methods); i++) {
2427
2617
  rb_define_singleton_method(rb_mImagePack, methods[i].name, methods[i].fn, methods[i].arity);
@@ -42,7 +42,7 @@ module ImagePack
42
42
 
43
43
  if value == :offload && ImagePack.respond_to?(:offload_safe?) && !ImagePack.offload_safe?
44
44
  raise UnsupportedError,
45
- "execution: :offload requires Ruby >= 3.4.0; use execution: :nogvl or :auto"
45
+ "execution: :offload is unavailable in this runtime; it requires Ruby >= 3.4.0 and IMAGE_PACK_DISABLE_OFFLOAD must not be set"
46
46
  end
47
47
 
48
48
  @execution = value
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ImagePack
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.5"
5
5
  end
data/lib/image_pack.rb CHANGED
@@ -90,10 +90,12 @@ module ImagePack
90
90
  progressive: true,
91
91
  strip_metadata: false,
92
92
  execution: nil,
93
- cancellable: false)
93
+ cancellable: false,
94
+ strict: false)
94
95
  validate_boolean!(:progressive, progressive)
95
96
  validate_boolean!(:strip_metadata, strip_metadata)
96
97
  validate_boolean!(:cancellable, cancellable)
98
+ validate_boolean!(:strict, strict)
97
99
  execution ||= configuration.execution
98
100
  validate_execution!(execution)
99
101
  validate_execution_supported!(execution)
@@ -109,7 +111,8 @@ module ImagePack
109
111
  strip_metadata ? 1 : 0,
110
112
  execution,
111
113
  cancellable ? 1 : 0,
112
- has_scheduler ? 1 : 0)
114
+ has_scheduler ? 1 : 0,
115
+ strict ? 1 : 0)
113
116
  end
114
117
 
115
118
  def compress(input,
@@ -118,16 +121,23 @@ module ImagePack
118
121
  quality: nil,
119
122
  min_ssim: nil,
120
123
  mozjpeg_trellis: true,
121
- progressive: false,
124
+ mozjpeg_scan_opt: true,
125
+ progressive: nil,
122
126
  strip_metadata: true,
123
127
  execution: nil,
124
- cancellable: false)
128
+ cancellable: false,
129
+ report: false,
130
+ strict: false)
125
131
  validate_algo!(algo)
126
132
  validate_min_ssim!(min_ssim)
127
133
  validate_boolean!(:mozjpeg_trellis, mozjpeg_trellis)
134
+ validate_boolean!(:mozjpeg_scan_opt, mozjpeg_scan_opt)
135
+ progressive = default_progressive_for(algo, progressive)
128
136
  validate_boolean!(:progressive, progressive)
129
137
  validate_boolean!(:strip_metadata, strip_metadata)
130
138
  validate_boolean!(:cancellable, cancellable)
139
+ validate_boolean!(:report, report)
140
+ validate_boolean!(:strict, strict)
131
141
  quality_was_given = !quality.nil?
132
142
  effective_quality = quality_was_given ? quality : DEFAULT_QUALITY
133
143
  effective_quality = 1 if min_ssim && !quality_was_given
@@ -150,7 +160,10 @@ module ImagePack
150
160
  strip_metadata ? 1 : 0,
151
161
  execution,
152
162
  cancellable ? 1 : 0,
153
- has_scheduler ? 1 : 0)
163
+ has_scheduler ? 1 : 0,
164
+ report ? 1 : 0,
165
+ strict ? 1 : 0,
166
+ mozjpeg_scan_opt ? 1 : 0)
154
167
  end
155
168
 
156
169
  def compress_pixels(buffer,
@@ -162,19 +175,26 @@ module ImagePack
162
175
  quality: nil,
163
176
  min_ssim: nil,
164
177
  mozjpeg_trellis: true,
165
- progressive: false,
178
+ mozjpeg_scan_opt: true,
179
+ progressive: nil,
166
180
  drop_alpha: nil,
167
181
  exact_size: false,
168
182
  execution: nil,
169
- cancellable: false)
183
+ cancellable: false,
184
+ report: false,
185
+ strict: false)
170
186
  validate_pixel_buffer!(buffer)
171
187
  validate_algo!(algo)
172
188
  validate_min_ssim!(min_ssim)
173
189
  validate_boolean!(:mozjpeg_trellis, mozjpeg_trellis)
190
+ validate_boolean!(:mozjpeg_scan_opt, mozjpeg_scan_opt)
191
+ progressive = default_progressive_for(algo, progressive)
174
192
  validate_boolean!(:progressive, progressive)
175
193
  validate_drop_alpha!(drop_alpha)
176
194
  validate_boolean!(:exact_size, exact_size)
177
195
  validate_boolean!(:cancellable, cancellable)
196
+ validate_boolean!(:report, report)
197
+ validate_boolean!(:strict, strict)
178
198
  quality_was_given = !quality.nil?
179
199
  effective_quality = quality_was_given ? quality : DEFAULT_QUALITY
180
200
  effective_quality = 1 if min_ssim && !quality_was_given
@@ -214,7 +234,10 @@ module ImagePack
214
234
  exact_size ? 1 : 0,
215
235
  execution,
216
236
  cancellable ? 1 : 0,
217
- has_scheduler ? 1 : 0)
237
+ has_scheduler ? 1 : 0,
238
+ report ? 1 : 0,
239
+ strict ? 1 : 0,
240
+ mozjpeg_scan_opt ? 1 : 0)
218
241
  end
219
242
 
220
243
  def inspect_image(input)
@@ -251,7 +274,7 @@ module ImagePack
251
274
  return :return_string if output.nil?
252
275
  return :path if output.is_a?(String) || output.is_a?(Pathname)
253
276
 
254
- raise InvalidArgumentError, "output must be nil, String path, or Pathname in v0.2.2"
277
+ raise InvalidArgumentError, "output must be nil, String path, or Pathname"
255
278
  end
256
279
 
257
280
  def validate_algo!(algo)
@@ -279,6 +302,13 @@ module ImagePack
279
302
  raise InvalidArgumentError, "#{name} must be true or false, got: #{value.inspect}"
280
303
  end
281
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
+
282
312
  def validate_drop_alpha!(value)
283
313
  return if value.nil? || value == true || value == false
284
314
 
@@ -296,7 +326,7 @@ module ImagePack
296
326
  return if offload_safe?
297
327
 
298
328
  raise UnsupportedError,
299
- "execution: :offload requires Ruby >= 3.4.0; use execution: :nogvl or :auto"
329
+ "execution: :offload is unavailable in this runtime; it requires Ruby >= 3.4.0 and IMAGE_PACK_DISABLE_OFFLOAD must not be set"
300
330
  end
301
331
 
302
332
  def validate_dimensions!(width, height, channels)
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.3
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
@@ -80,9 +80,9 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.21'
83
- description: Single API, vendored pure-C MozJPEG/libjpeg codec, no tempfiles, Ruby
84
- 3.1+ native JPEG execution; Ruby 3.4+ enables Fiber::Scheduler-aware offload. Ships
85
- vendored C sources — no system libjpeg, git, or CMake required.
83
+ description: Single API, vendored pure-C MozJPEG/libjpeg codec, in-memory compression
84
+ and atomic file output, Ruby 3.1+ native JPEG execution; Ruby 3.4+ enables Fiber::Scheduler-aware
85
+ offload. Ships vendored C sources — no system libjpeg, git, or CMake required.
86
86
  email:
87
87
  - romnhajdarov@gmail.com
88
88
  executables: []