image_pack 0.2.3 → 0.2.4

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: 124f64ccca4910cee483d3c63a189f6de7d7ea72e8f4a8b88d538fe503cf0673
4
+ data.tar.gz: d51e698fdad475590f685ddd6097f9bf8486dcb154ce37716e60fac256949c6c
5
5
  SHA512:
6
- metadata.gz: ad6b0db99c56d69c99d9216559e3c114a2079941a4f879e69508126bbf12b070844dc53e806b3da7637783a205528c10136c3cd3bac8f60b5fd6ee2ddcad01eb
7
- data.tar.gz: f1acfa9f5a7154d1ea201ae66658da704a5ee5d207886b096d284b7b69eca175d668993dba127b0a609d6fb5b68492196edb8d3bb70d12575f129c095c6b7704
6
+ metadata.gz: 6ed979776d78d8e1d64e01ec46499d2453d13483495688cc79b71e0fc84b542d68e2a076015d480aaac812c90ef1dea49952f40b156d11d2b4dd95ace73a7688
7
+ data.tar.gz: 233229ccf2b0daa138dc6bd664ce14c8ddf770aaa232d42efc33d43419a3c7f0f5b673daf985e6745e49fcf8ceeac9503d84910319cb8a7d269178e08cd657eb
data/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ - 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.
6
+ - 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`.
7
+ - 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`).
8
+ - 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.
9
+ - 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.
10
+ - `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.
11
+ - Removed the unused `IMAGE_PACK_RUBY34_ONLY` compile define.
12
+ - Removed a stale `v0.2.2` reference from the `output:` argument error message.
13
+ - Added tests covering `report:`, `strict:`, warning collection, the cancellation contract, offload availability, and version hygiene.
14
+ - Documentation and message version references updated to 0.2.4.
15
+
16
+ ## 0.2.3
17
+
3
18
  - Lowered the runtime floor to Ruby >= 3.1.0 while keeping `execution: :offload` Ruby >= 3.4-only and explicitly unsupported on older Rubies.
19
+
4
20
  ## 0.2.2
5
21
 
6
22
  - 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,169 @@
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)
46
-
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)
20
+ small = ImagePack.compress_bytes(jpeg, quality: 82)
21
+ File.binwrite("photo.small.jpg", small)
50
22
 
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)
63
-
64
- ImagePack.compress_pixels(rgb_buffer,
65
- width: 1920,
66
- height: 1080,
67
- channels: 3,
68
- algo: :mozjpeg
69
- )
70
-
71
- ImagePack.inspect_image(jpeg)
72
- # => { format: :jpeg, width: 1920, height: 1080, channels: 3, bit_depth: 8, decoded_bytes: 6220800 }
33
+ ImagePack.optimize_file("photo.jpg", output: "out.jpg")
73
34
  ```
74
35
 
75
- ## Input / output policy
36
+ ## Compression
76
37
 
77
- `compress(input, ...)` accepts JPEG input only:
78
-
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.
83
-
84
- For non-ambiguous call sites prefer `compress_bytes(bytes, ...)` or `compress_file(path, ...)`.
38
+ ```ruby
39
+ ImagePack.compress_bytes(jpeg,
40
+ algo: :size,
41
+ quality: 82,
42
+ strip_metadata: true
43
+ )
44
+ ```
85
45
 
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.
46
+ Algorithms:
88
47
 
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.
48
+ - `:size` / `:mozjpeg` smaller files, default
49
+ - `:fast` / `:jpeg_turbo` faster mode
91
50
 
92
- ## Lossless JPEG optimize
51
+ Common options:
93
52
 
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.
53
+ ```ruby
54
+ ImagePack.compress_bytes(jpeg, min_ssim: 0.985)
55
+ ImagePack.compress_bytes(jpeg, progressive: true)
56
+ ImagePack.compress_bytes(jpeg, strict: true)
57
+ ImagePack.compress_bytes(jpeg, report: true)
58
+ ```
95
59
 
96
- Defaults are intentionally conservative:
60
+ `min_ssim:` searches for the lowest acceptable quality using a fast native luma SSIM guard.
97
61
 
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.
62
+ `strict: true` raises `ImagePack::InvalidImageError` on damaged/truncated JPEG warnings.
100
63
 
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.
64
+ `report: true` returns a Hash:
102
65
 
103
- ## SSIM guard
66
+ ```ruby
67
+ {
68
+ output: "\xFF\xD8...",
69
+ quality: 84,
70
+ ssim: 0.9861,
71
+ algo: :mozjpeg,
72
+ bytesize: 50122,
73
+ input_bytesize: 81344,
74
+ warning_count: 0,
75
+ warning: nil
76
+ }
77
+ ```
104
78
 
105
- `compress` and `compress_pixels` accept `min_ssim:`. This enables a native guarded path:
79
+ With `output: "file.jpg"`, `output` is `true`.
106
80
 
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`.
81
+ ## Lossless optimize
112
82
 
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.
83
+ ```ruby
84
+ ImagePack.optimize_bytes(jpeg)
85
+ ImagePack.optimize_file("photo.jpg", output: "photo.optimized.jpg")
86
+ ```
116
87
 
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.
88
+ 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
89
 
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.
90
+ Defaults: `progressive: true`, `strip_metadata: false`.
123
91
 
124
- ## Execution modes
92
+ If `strip_metadata: true` would remove EXIF Orientation, `optimize_jpeg` raises `UnsupportedError` instead of silently changing visual orientation.
125
93
 
126
- ### Ruby compatibility for offload
94
+ ## Raw pixels
127
95
 
128
- ImagePack supports Ruby `>= 3.1.0`. Ruby `>= 3.4.0` is required only for the scheduler-aware `execution: :offload` path.
96
+ ```ruby
97
+ ImagePack.compress_pixels(rgb,
98
+ width: 1920,
99
+ height: 1080,
100
+ channels: 3,
101
+ output: "frame.jpg"
102
+ )
103
+ ```
129
104
 
130
- On Ruby 3.13.3:
105
+ `channels` must be `1`, `3`, or `4`. JPEG cannot store alpha, so RGBA input needs explicit opt-in:
131
106
 
132
- - `execution: :direct` works.
133
- - `execution: :nogvl` works.
134
- - `execution: :auto` works, but never selects `:offload`.
135
- - explicit `execution: :offload` raises `ImagePack::UnsupportedError`.
107
+ ```ruby
108
+ ImagePack.compress_pixels(rgba, width: 100, height: 100, channels: 4, drop_alpha: true)
109
+ ```
136
110
 
137
- On Ruby 3.4+:
111
+ ## Inspect
138
112
 
139
- - `execution: :offload` is available.
140
- - `execution: :auto` may select `:offload` when a Fiber scheduler is active.
113
+ ```ruby
114
+ ImagePack.inspect_image(jpeg)
115
+ # => { format: :jpeg, width: 1920, height: 1080, channels: 3, bit_depth: 8, decoded_bytes: 6220800 }
116
+ ```
141
117
 
142
- Use `ImagePack.offload_safe?` or `ImagePack.build_info[:offload_safe]` to check the native capability at runtime.
118
+ ## Execution
143
119
 
120
+ Default mode is `:auto`.
144
121
 
145
122
  ```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)
123
+ ImagePack.compress_bytes(jpeg, execution: :auto)
124
+ ImagePack.compress_bytes(jpeg, execution: :direct)
125
+ ImagePack.compress_bytes(jpeg, execution: :nogvl)
126
+ ImagePack.compress_bytes(jpeg, execution: :offload) # Ruby >= 3.4 only
150
127
  ```
151
128
 
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.
129
+ Use `ImagePack.offload_safe?` or `ImagePack.build_info` to inspect runtime support.
156
130
 
157
- ## Cancellation
131
+ Set `IMAGE_PACK_DISABLE_OFFLOAD=1` before loading the gem to disable offload.
158
132
 
159
- `cancellable: true` is supported for `execution: :nogvl`, `:offload`, or `:auto`.
160
- Cancellation is cooperative at decode, encode, and SSIM-search checkpoints, not instant.
133
+ Long no-GVL/offload calls can be interrupted by raising into the worker thread:
161
134
 
162
- ## Metadata / EXIF orientation
163
-
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.
135
+ ```ruby
136
+ worker = Thread.new { ImagePack.compress_bytes(jpeg, execution: :nogvl, cancellable: true) }
137
+ worker.raise(ImagePack::CancelledError, "cancelled")
138
+ worker.join
139
+ ```
165
140
 
166
- ## Build info
141
+ ## Configuration
167
142
 
168
143
  ```ruby
169
- ImagePack.build_info
170
- # => { version: "0.2.2", mozjpeg: "4.1.5", simd: true, offload_safe: true }
144
+ ImagePack.configure do |config|
145
+ config.execution = :auto
146
+ config.max_input_size = 256 * 1024 * 1024
147
+ config.max_output_size = 256 * 1024 * 1024
148
+ config.max_pixels = 100_000_000
149
+ end
171
150
  ```
172
151
 
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:
152
+ ## Development
177
153
 
178
154
  ```bash
179
155
  bundle exec rake vendor
180
156
  bundle exec rake compile
157
+ bundle exec rake test
181
158
  ```
182
159
 
183
160
  `rake vendor` pins MozJPEG `v4.1.5`.
184
161
 
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.
191
-
192
- ## What is intentionally not included
162
+ ## Limits
193
163
 
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
164
+ - JPEG only.
165
+ - Ruby `>= 3.1`; `execution: :offload` requires Ruby `>= 3.4`.
166
+ - Pixel-level `compress` rejects CMYK/YCCK JPEG input; use `optimize_jpeg` for existing CMYK/YCCK JPEGs.
167
+ - Arithmetic-coded JPEG support is disabled in `0.2.4`.
168
+ - Streaming output is not supported; file output uses atomic write-through-temp-file and rename.
169
+ - `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
@@ -151,6 +155,9 @@ typedef struct {
151
155
 
152
156
  atomic_int cancelled;
153
157
  int cancellable_requested;
158
+ int strict;
159
+ int warning_count;
160
+ char first_warning[200];
154
161
 
155
162
  jmp_buf jmpbuf;
156
163
  int jmp_armed;
@@ -174,6 +181,7 @@ typedef struct {
174
181
  } ip_jpeg_error_mgr;
175
182
 
176
183
  static VALUE rb_mImagePack;
184
+ static int ip_offload_runtime_enabled = 1;
177
185
  static VALUE rb_eImagePackError;
178
186
  static VALUE rb_eImagePackInvalidArgumentError;
179
187
  static VALUE rb_eImagePackInvalidImageError;
@@ -255,18 +263,20 @@ static int ip_run_optimize_context(ip_context_t *ctx);
255
263
  typedef struct {
256
264
  VALUE self, input, input_kind, output, output_kind, algo, quality, min_ssim;
257
265
  VALUE mozjpeg_trellis, progressive, strip_metadata, execution, cancellable, has_scheduler;
266
+ VALUE report, strict;
258
267
  ip_context_t *ctx;
259
268
  } ip_compress_jpeg_call_t;
260
269
 
261
270
  typedef struct {
262
271
  VALUE self, buffer, width, height, channels, output, output_kind, algo, quality, min_ssim;
263
272
  VALUE mozjpeg_trellis, progressive, exact_size, execution, cancellable, has_scheduler;
273
+ VALUE report, strict;
264
274
  ip_context_t *ctx;
265
275
  } ip_compress_pixels_call_t;
266
276
 
267
277
  typedef struct {
268
278
  VALUE self, input, input_kind, output, output_kind, progressive, strip_metadata;
269
- VALUE execution, cancellable, has_scheduler;
279
+ VALUE execution, cancellable, has_scheduler, strict;
270
280
  ip_context_t *ctx;
271
281
  } ip_optimize_jpeg_call_t;
272
282
 
@@ -287,15 +297,13 @@ static VALUE ip_call_cleanup(VALUE ptr) {
287
297
  static VALUE ip_compress_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
288
298
  VALUE output_kind, VALUE algo, VALUE quality, VALUE min_ssim,
289
299
  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
+ VALUE execution, VALUE cancellable, VALUE has_scheduler,
301
+ VALUE report, VALUE strict);
302
+ static VALUE ip_compress_pixels_entry(int argc, VALUE *argv, VALUE self);
296
303
  static VALUE ip_optimize_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
297
304
  VALUE output_kind, VALUE progressive, VALUE strip_metadata,
298
- VALUE execution, VALUE cancellable, VALUE has_scheduler);
305
+ VALUE execution, VALUE cancellable, VALUE has_scheduler,
306
+ VALUE strict);
299
307
 
300
308
  static VALUE ip_status_to_exception(ip_status_t status) {
301
309
  switch (status) {
@@ -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) {
@@ -1287,7 +1345,7 @@ static int encode_pixels_with_libjpeg(ip_context_t *ctx, int mozjpeg_size_mode)
1287
1345
  ip_write_preserved_markers(ctx, &cinfo);
1288
1346
 
1289
1347
  while (cinfo.next_scanline < cinfo.image_height) {
1290
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
1348
+ if (atomic_load(&ctx->cancelled))
1291
1349
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "JPEG encode cancelled");
1292
1350
 
1293
1351
  JSAMPROW rows[16];
@@ -1398,7 +1456,7 @@ static int ip_jpeg_decode_to_pixels(ip_context_t *ctx, unsigned char **pixels, i
1398
1456
  IP_FAIL_GOTO(ctx, IP_ERR_OOM, "failed to allocate decoded pixel buffer");
1399
1457
 
1400
1458
  while (cinfo.output_scanline < cinfo.output_height) {
1401
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
1459
+ if (atomic_load(&ctx->cancelled))
1402
1460
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "JPEG decode cancelled");
1403
1461
 
1404
1462
  JSAMPROW rows[16];
@@ -1558,7 +1616,7 @@ static int ip_decode_jpeg_to_luma_buffer(ip_context_t *ctx, const unsigned char
1558
1616
  IP_FAIL_GOTO(ctx, IP_ERR_OOM, "failed to allocate luma buffer");
1559
1617
 
1560
1618
  while (cinfo.output_scanline < cinfo.output_height) {
1561
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
1619
+ if (atomic_load(&ctx->cancelled))
1562
1620
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "JPEG luma decode cancelled");
1563
1621
 
1564
1622
  JSAMPROW rows[16];
@@ -1791,8 +1849,7 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1791
1849
  }
1792
1850
 
1793
1851
  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");
1852
+ ip_context_set_error(ctx, IP_ERR_UNSUPPORTED, "min_ssim is not supported for RGBA input");
1796
1853
  return 0;
1797
1854
  }
1798
1855
 
@@ -1811,7 +1868,7 @@ static int guarded_compress_jpeg_input_with_mode(ip_context_t *ctx, int mozjpeg_
1811
1868
  int best_seen_quality = 0;
1812
1869
 
1813
1870
  while (search_low <= search_high) {
1814
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled)) {
1871
+ if (atomic_load(&ctx->cancelled)) {
1815
1872
  free(reference_luma);
1816
1873
  free(best_jpeg);
1817
1874
  ip_context_set_error(ctx, IP_ERR_CANCELLED, "SSIM-guarded JPEG encode cancelled");
@@ -1959,7 +2016,7 @@ static int ip_lossless_optimize_jpeg(ip_context_t *ctx) {
1959
2016
  jpeg_mem_src(&srcinfo, ctx->input_data, (unsigned long)ctx->input_size);
1960
2017
  ip_setup_marker_saving(&srcinfo, ctx->strip_metadata);
1961
2018
 
1962
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
2019
+ if (atomic_load(&ctx->cancelled))
1963
2020
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "lossless JPEG optimize cancelled");
1964
2021
 
1965
2022
  if (jpeg_read_header(&srcinfo, TRUE) != JPEG_HEADER_OK)
@@ -1989,7 +2046,7 @@ static int ip_lossless_optimize_jpeg(ip_context_t *ctx) {
1989
2046
 
1990
2047
  jpeg_mem_dest(&dstinfo, &ctx->transient_jpeg_buf, &jpeg_size);
1991
2048
 
1992
- if (ctx->cancellable_requested && atomic_load(&ctx->cancelled))
2049
+ if (atomic_load(&ctx->cancelled))
1993
2050
  IP_FAIL_GOTO(ctx, IP_ERR_CANCELLED, "lossless JPEG optimize cancelled");
1994
2051
 
1995
2052
  jpeg_write_coefficients(&dstinfo, coef_arrays);
@@ -2035,7 +2092,7 @@ static int ip_mozjpeg_compress(ip_context_t *ctx) {
2035
2092
 
2036
2093
  static ip_execution_t ip_async_execution(const ip_context_t *ctx) {
2037
2094
  #if IMAGE_PACK_HAS_OFFLOAD_SAFE
2038
- return ctx->has_scheduler ? IP_EXEC_OFFLOAD : IP_EXEC_NOGVL;
2095
+ return (ip_offload_runtime_enabled && ctx->has_scheduler) ? IP_EXEC_OFFLOAD : IP_EXEC_NOGVL;
2039
2096
  #else
2040
2097
  (void)ctx;
2041
2098
  return IP_EXEC_NOGVL;
@@ -2105,12 +2162,16 @@ static int ip_run_context(ip_context_t *ctx) {
2105
2162
  rb_nogvl(ip_run_encode_nogvl, ctx, ip_unblock_function, ctx, RB_NOGVL_OFFLOAD_SAFE);
2106
2163
  #else
2107
2164
  ip_context_set_error(ctx, IP_ERR_UNSUPPORTED,
2108
- "offload execution requires Ruby >= 3.4; use :nogvl or :auto");
2165
+ "offload execution is unavailable in this runtime; it requires Ruby "
2166
+ ">= 3.4 and IMAGE_PACK_DISABLE_OFFLOAD must not be set");
2109
2167
  #endif
2110
2168
  } else {
2111
2169
  ip_context_set_error(ctx, IP_ERR_INVALID_ARGUMENT, "invalid resolved execution mode");
2112
2170
  }
2113
2171
 
2172
+ if (ctx->resolved_execution != IP_EXEC_DIRECT)
2173
+ rb_thread_check_ints();
2174
+
2114
2175
  return ctx->status == IP_OK;
2115
2176
  }
2116
2177
 
@@ -2133,12 +2194,16 @@ static int ip_run_optimize_context(ip_context_t *ctx) {
2133
2194
  rb_nogvl(ip_run_optimize_nogvl, ctx, ip_unblock_function, ctx, RB_NOGVL_OFFLOAD_SAFE);
2134
2195
  #else
2135
2196
  ip_context_set_error(ctx, IP_ERR_UNSUPPORTED,
2136
- "offload execution requires Ruby >= 3.4; use :nogvl or :auto");
2197
+ "offload execution is unavailable in this runtime; it requires Ruby "
2198
+ ">= 3.4 and IMAGE_PACK_DISABLE_OFFLOAD must not be set");
2137
2199
  #endif
2138
2200
  } else {
2139
2201
  ip_context_set_error(ctx, IP_ERR_INVALID_ARGUMENT, "invalid resolved execution mode");
2140
2202
  }
2141
2203
 
2204
+ if (ctx->resolved_execution != IP_EXEC_DIRECT)
2205
+ rb_thread_check_ints();
2206
+
2142
2207
  return ctx->status == IP_OK;
2143
2208
  }
2144
2209
 
@@ -2220,6 +2285,7 @@ static VALUE ip_compress_jpeg_entry_body(VALUE ptr) {
2220
2285
  ctx->requested_execution = ip_parse_execution(call->execution);
2221
2286
  ctx->cancellable_requested = ip_bool_value(call->cancellable);
2222
2287
  ctx->has_scheduler = ip_bool_value(call->has_scheduler);
2288
+ ctx->strict = ip_bool_value(call->strict);
2223
2289
  apply_configuration(call->self, ctx);
2224
2290
 
2225
2291
  ip_input_kind_t in_kind = ip_parse_input_kind(call->input_kind);
@@ -2242,17 +2308,23 @@ static VALUE ip_compress_jpeg_entry_body(VALUE ptr) {
2242
2308
  }
2243
2309
 
2244
2310
  ip_run_context(ctx);
2311
+ if (ip_bool_value(call->report)) {
2312
+ VALUE output_value = ip_finish_output(ctx, out_kind);
2313
+ return ip_build_report(ctx, output_value);
2314
+ }
2245
2315
  return ip_finish_output(ctx, out_kind);
2246
2316
  }
2247
2317
 
2248
2318
  static VALUE ip_compress_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
2249
2319
  VALUE output_kind, VALUE algo, VALUE quality, VALUE min_ssim,
2250
2320
  VALUE mozjpeg_trellis, VALUE progressive, VALUE strip_metadata,
2251
- VALUE execution, VALUE cancellable, VALUE has_scheduler) {
2321
+ VALUE execution, VALUE cancellable, VALUE has_scheduler,
2322
+ VALUE report, VALUE strict) {
2252
2323
  ip_compress_jpeg_call_t call = {
2253
2324
  self, input, input_kind, output, output_kind,
2254
2325
  algo, quality, min_ssim, mozjpeg_trellis, progressive,
2255
- strip_metadata, execution, cancellable, has_scheduler, NULL};
2326
+ strip_metadata, execution, cancellable, has_scheduler, report,
2327
+ strict, NULL};
2256
2328
  return rb_ensure(ip_compress_jpeg_entry_body, (VALUE)&call, ip_call_cleanup, (VALUE)&call.ctx);
2257
2329
  }
2258
2330
 
@@ -2277,6 +2349,7 @@ static VALUE ip_compress_pixels_entry_body(VALUE ptr) {
2277
2349
  ctx->requested_execution = ip_parse_execution(call->execution);
2278
2350
  ctx->cancellable_requested = ip_bool_value(call->cancellable);
2279
2351
  ctx->has_scheduler = ip_bool_value(call->has_scheduler);
2352
+ ctx->strict = ip_bool_value(call->strict);
2280
2353
  apply_configuration(call->self, ctx);
2281
2354
 
2282
2355
  if (!ip_prepare_output_path(ctx, call->output, out_kind) ||
@@ -2297,18 +2370,19 @@ static VALUE ip_compress_pixels_entry_body(VALUE ptr) {
2297
2370
  }
2298
2371
 
2299
2372
  ip_run_context(ctx);
2373
+ if (ip_bool_value(call->report)) {
2374
+ VALUE output_value = ip_finish_output(ctx, out_kind);
2375
+ return ip_build_report(ctx, output_value);
2376
+ }
2300
2377
  return ip_finish_output(ctx, out_kind);
2301
2378
  }
2302
2379
 
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};
2380
+ static VALUE ip_compress_pixels_entry(int argc, VALUE *argv, VALUE self) {
2381
+ rb_check_arity(argc, 17, 17);
2382
+ ip_compress_pixels_call_t call = {self, argv[0], argv[1], argv[2], argv[3],
2383
+ argv[4], argv[5], argv[6], argv[7], argv[8],
2384
+ argv[9], argv[10], argv[11], argv[12], argv[13],
2385
+ argv[14], argv[15], argv[16], NULL};
2312
2386
  return rb_ensure(ip_compress_pixels_entry_body, (VALUE)&call, ip_call_cleanup,
2313
2387
  (VALUE)&call.ctx);
2314
2388
  }
@@ -2326,6 +2400,7 @@ static VALUE ip_optimize_jpeg_entry_body(VALUE ptr) {
2326
2400
  ctx->requested_execution = ip_parse_execution(call->execution);
2327
2401
  ctx->cancellable_requested = ip_bool_value(call->cancellable);
2328
2402
  ctx->has_scheduler = ip_bool_value(call->has_scheduler);
2403
+ ctx->strict = ip_bool_value(call->strict);
2329
2404
  ctx->ssim_guard_enabled = 0;
2330
2405
  apply_configuration(call->self, ctx);
2331
2406
 
@@ -2354,10 +2429,11 @@ static VALUE ip_optimize_jpeg_entry_body(VALUE ptr) {
2354
2429
 
2355
2430
  static VALUE ip_optimize_jpeg_entry(VALUE self, VALUE input, VALUE input_kind, VALUE output,
2356
2431
  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};
2432
+ VALUE execution, VALUE cancellable, VALUE has_scheduler,
2433
+ VALUE strict) {
2434
+ ip_optimize_jpeg_call_t call = {self, input, input_kind, output,
2435
+ output_kind, progressive, strip_metadata, execution,
2436
+ cancellable, has_scheduler, strict, NULL};
2361
2437
  return rb_ensure(ip_optimize_jpeg_entry_body, (VALUE)&call, ip_call_cleanup, (VALUE)&call.ctx);
2362
2438
  }
2363
2439
 
@@ -2403,14 +2479,17 @@ IMAGE_PACK_INIT_EXPORT void Init_image_pack(void) {
2403
2479
  for (size_t i = 0; i < IP_ARRAY_LEN(exceptions); i++)
2404
2480
  *exceptions[i].slot = rb_const_get(rb_mImagePack, rb_intern(exceptions[i].name));
2405
2481
 
2406
- rb_define_const(rb_mImagePack, "NATIVE_MOZJPEG_VERSION", rb_str_new_cstr(VERSION));
2482
+ rb_define_const(rb_mImagePack, "NATIVE_MOZJPEG_VERSION",
2483
+ rb_str_new_cstr(IMAGE_PACK_MOZJPEG_VERSION));
2407
2484
  #if defined(IMAGE_PACK_HAS_SIMD)
2408
2485
  rb_define_const(rb_mImagePack, "NATIVE_SIMD", Qtrue);
2409
2486
  #else
2410
2487
  rb_define_const(rb_mImagePack, "NATIVE_SIMD", Qfalse);
2411
2488
  #endif
2489
+ ip_offload_runtime_enabled = (getenv("IMAGE_PACK_DISABLE_OFFLOAD") == NULL) ? 1 : 0;
2412
2490
  #if IMAGE_PACK_HAS_OFFLOAD_SAFE
2413
- rb_define_const(rb_mImagePack, "NATIVE_OFFLOAD_SAFE", Qtrue);
2491
+ rb_define_const(rb_mImagePack, "NATIVE_OFFLOAD_SAFE",
2492
+ ip_offload_runtime_enabled ? Qtrue : Qfalse);
2414
2493
  #else
2415
2494
  rb_define_const(rb_mImagePack, "NATIVE_OFFLOAD_SAFE", Qfalse);
2416
2495
  #endif
@@ -2419,9 +2498,9 @@ IMAGE_PACK_INIT_EXPORT void Init_image_pack(void) {
2419
2498
  const char *name;
2420
2499
  VALUE (*fn)(ANYARGS);
2421
2500
  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},
2501
+ } methods[] = {{"__compress_jpeg", (VALUE (*)(ANYARGS))ip_compress_jpeg_entry, 15},
2502
+ {"__compress_pixels", (VALUE (*)(ANYARGS))ip_compress_pixels_entry, -1},
2503
+ {"__optimize_jpeg", (VALUE (*)(ANYARGS))ip_optimize_jpeg_entry, 10},
2425
2504
  {"__inspect_image", (VALUE (*)(ANYARGS))ip_inspect_image_entry, 2}};
2426
2505
  for (size_t i = 0; i < IP_ARRAY_LEN(methods); i++) {
2427
2506
  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.4"
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,
@@ -121,13 +124,17 @@ module ImagePack
121
124
  progressive: false,
122
125
  strip_metadata: true,
123
126
  execution: nil,
124
- cancellable: false)
127
+ cancellable: false,
128
+ report: false,
129
+ strict: false)
125
130
  validate_algo!(algo)
126
131
  validate_min_ssim!(min_ssim)
127
132
  validate_boolean!(:mozjpeg_trellis, mozjpeg_trellis)
128
133
  validate_boolean!(:progressive, progressive)
129
134
  validate_boolean!(:strip_metadata, strip_metadata)
130
135
  validate_boolean!(:cancellable, cancellable)
136
+ validate_boolean!(:report, report)
137
+ validate_boolean!(:strict, strict)
131
138
  quality_was_given = !quality.nil?
132
139
  effective_quality = quality_was_given ? quality : DEFAULT_QUALITY
133
140
  effective_quality = 1 if min_ssim && !quality_was_given
@@ -150,7 +157,9 @@ module ImagePack
150
157
  strip_metadata ? 1 : 0,
151
158
  execution,
152
159
  cancellable ? 1 : 0,
153
- has_scheduler ? 1 : 0)
160
+ has_scheduler ? 1 : 0,
161
+ report ? 1 : 0,
162
+ strict ? 1 : 0)
154
163
  end
155
164
 
156
165
  def compress_pixels(buffer,
@@ -166,7 +175,9 @@ module ImagePack
166
175
  drop_alpha: nil,
167
176
  exact_size: false,
168
177
  execution: nil,
169
- cancellable: false)
178
+ cancellable: false,
179
+ report: false,
180
+ strict: false)
170
181
  validate_pixel_buffer!(buffer)
171
182
  validate_algo!(algo)
172
183
  validate_min_ssim!(min_ssim)
@@ -175,6 +186,8 @@ module ImagePack
175
186
  validate_drop_alpha!(drop_alpha)
176
187
  validate_boolean!(:exact_size, exact_size)
177
188
  validate_boolean!(:cancellable, cancellable)
189
+ validate_boolean!(:report, report)
190
+ validate_boolean!(:strict, strict)
178
191
  quality_was_given = !quality.nil?
179
192
  effective_quality = quality_was_given ? quality : DEFAULT_QUALITY
180
193
  effective_quality = 1 if min_ssim && !quality_was_given
@@ -214,7 +227,9 @@ module ImagePack
214
227
  exact_size ? 1 : 0,
215
228
  execution,
216
229
  cancellable ? 1 : 0,
217
- has_scheduler ? 1 : 0)
230
+ has_scheduler ? 1 : 0,
231
+ report ? 1 : 0,
232
+ strict ? 1 : 0)
218
233
  end
219
234
 
220
235
  def inspect_image(input)
@@ -251,7 +266,7 @@ module ImagePack
251
266
  return :return_string if output.nil?
252
267
  return :path if output.is_a?(String) || output.is_a?(Pathname)
253
268
 
254
- raise InvalidArgumentError, "output must be nil, String path, or Pathname in v0.2.2"
269
+ raise InvalidArgumentError, "output must be nil, String path, or Pathname"
255
270
  end
256
271
 
257
272
  def validate_algo!(algo)
@@ -296,7 +311,7 @@ module ImagePack
296
311
  return if offload_safe?
297
312
 
298
313
  raise UnsupportedError,
299
- "execution: :offload requires Ruby >= 3.4.0; use execution: :nogvl or :auto"
314
+ "execution: :offload is unavailable in this runtime; it requires Ruby >= 3.4.0 and IMAGE_PACK_DISABLE_OFFLOAD must not be set"
300
315
  end
301
316
 
302
317
  def validate_dimensions!(width, height, channels)
metadata CHANGED
@@ -1,7 +1,7 @@
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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
@@ -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: []