image_pack 0.2.2 → 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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +108 -119
  4. data/ext/image_pack/extconf.rb +36 -125
  5. data/ext/image_pack/image_pack.c +736 -614
  6. data/ext/image_pack/mozjpeg_sources.rb +178 -0
  7. data/ext/image_pack/vendor/mozjpeg/BUILDING.md +744 -0
  8. data/ext/image_pack/vendor/mozjpeg/CODE_OF_CONDUCT.md +15 -0
  9. data/ext/image_pack/vendor/mozjpeg/ChangeLog.md +1996 -0
  10. data/lib/image_pack/configuration.rb +54 -8
  11. data/lib/image_pack/version.rb +1 -1
  12. data/lib/image_pack.rb +87 -25
  13. metadata +14 -79
  14. data/ext/image_pack/vendor/mozjpeg/README.ijg +0 -258
  15. data/ext/image_pack/vendor/mozjpeg/cdjpeg.c +0 -156
  16. data/ext/image_pack/vendor/mozjpeg/cjpeg.c +0 -961
  17. data/ext/image_pack/vendor/mozjpeg/djpeg.c +0 -855
  18. data/ext/image_pack/vendor/mozjpeg/jaricom.c +0 -157
  19. data/ext/image_pack/vendor/mozjpeg/jcarith.c +0 -972
  20. data/ext/image_pack/vendor/mozjpeg/jcstest.c +0 -126
  21. data/ext/image_pack/vendor/mozjpeg/jdarith.c +0 -782
  22. data/ext/image_pack/vendor/mozjpeg/jdatadst-tj.c +0 -198
  23. data/ext/image_pack/vendor/mozjpeg/jdatasrc-tj.c +0 -194
  24. data/ext/image_pack/vendor/mozjpeg/jpegtran.c +0 -827
  25. data/ext/image_pack/vendor/mozjpeg/jpegyuv.c +0 -172
  26. data/ext/image_pack/vendor/mozjpeg/rdbmp.c +0 -690
  27. data/ext/image_pack/vendor/mozjpeg/rdcolmap.c +0 -253
  28. data/ext/image_pack/vendor/mozjpeg/rdgif.c +0 -720
  29. data/ext/image_pack/vendor/mozjpeg/rdjpeg.c +0 -160
  30. data/ext/image_pack/vendor/mozjpeg/rdjpgcom.c +0 -494
  31. data/ext/image_pack/vendor/mozjpeg/rdpng.c +0 -194
  32. data/ext/image_pack/vendor/mozjpeg/rdppm.c +0 -781
  33. data/ext/image_pack/vendor/mozjpeg/rdswitch.c +0 -642
  34. data/ext/image_pack/vendor/mozjpeg/rdtarga.c +0 -508
  35. data/ext/image_pack/vendor/mozjpeg/simd/arm/aarch32/jccolext-neon.c +0 -148
  36. data/ext/image_pack/vendor/mozjpeg/simd/arm/aarch32/jchuff-neon.c +0 -334
  37. data/ext/image_pack/vendor/mozjpeg/simd/arm/aarch32/jsimd.c +0 -976
  38. data/ext/image_pack/vendor/mozjpeg/simd/i386/jsimd.c +0 -1312
  39. data/ext/image_pack/vendor/mozjpeg/simd/mips/jsimd.c +0 -1143
  40. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jccolext-mmi.c +0 -455
  41. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jccolor-mmi.c +0 -148
  42. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jcgray-mmi.c +0 -132
  43. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jcgryext-mmi.c +0 -374
  44. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jcsample-mmi.c +0 -98
  45. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jdcolext-mmi.c +0 -415
  46. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jdcolor-mmi.c +0 -139
  47. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jdmerge-mmi.c +0 -149
  48. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jdmrgext-mmi.c +0 -615
  49. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jdsample-mmi.c +0 -304
  50. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jfdctfst-mmi.c +0 -255
  51. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jfdctint-mmi.c +0 -398
  52. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jidctfst-mmi.c +0 -395
  53. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jidctint-mmi.c +0 -571
  54. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jquanti-mmi.c +0 -124
  55. data/ext/image_pack/vendor/mozjpeg/simd/mips64/jsimd.c +0 -866
  56. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jccolext-altivec.c +0 -269
  57. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jccolor-altivec.c +0 -116
  58. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jcgray-altivec.c +0 -111
  59. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jcgryext-altivec.c +0 -228
  60. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jcsample-altivec.c +0 -159
  61. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jdcolext-altivec.c +0 -276
  62. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jdcolor-altivec.c +0 -106
  63. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jdmerge-altivec.c +0 -130
  64. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jdmrgext-altivec.c +0 -329
  65. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jdsample-altivec.c +0 -400
  66. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jfdctfst-altivec.c +0 -154
  67. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jfdctint-altivec.c +0 -258
  68. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jidctfst-altivec.c +0 -255
  69. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jidctint-altivec.c +0 -357
  70. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jquanti-altivec.c +0 -250
  71. data/ext/image_pack/vendor/mozjpeg/simd/powerpc/jsimd.c +0 -884
  72. data/ext/image_pack/vendor/mozjpeg/strtest.c +0 -170
  73. data/ext/image_pack/vendor/mozjpeg/tjbench.c +0 -1044
  74. data/ext/image_pack/vendor/mozjpeg/tjexample.c +0 -406
  75. data/ext/image_pack/vendor/mozjpeg/tjunittest.c +0 -961
  76. data/ext/image_pack/vendor/mozjpeg/tjutil.c +0 -70
  77. data/ext/image_pack/vendor/mozjpeg/transupp.c +0 -2373
  78. data/ext/image_pack/vendor/mozjpeg/turbojpeg-jni.c +0 -1259
  79. data/ext/image_pack/vendor/mozjpeg/turbojpeg.c +0 -2320
  80. data/ext/image_pack/vendor/mozjpeg/wrbmp.c +0 -552
  81. data/ext/image_pack/vendor/mozjpeg/wrgif.c +0 -580
  82. data/ext/image_pack/vendor/mozjpeg/wrjpgcom.c +0 -577
  83. data/ext/image_pack/vendor/mozjpeg/wrppm.c +0 -366
  84. data/ext/image_pack/vendor/mozjpeg/wrtarga.c +0 -258
  85. data/ext/image_pack/vendor/mozjpeg/yuvjpeg.c +0 -268
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 301f10cb975250ef59e3bf1beae23208aeb3e40d6598721323b93e4ea8d2eee8
4
- data.tar.gz: cc8b9139d2a16ebe94f2c32a047202ea9e9fdf04a608c1bc4608eb5d9e53d589
3
+ metadata.gz: 124f64ccca4910cee483d3c63a189f6de7d7ea72e8f4a8b88d538fe503cf0673
4
+ data.tar.gz: d51e698fdad475590f685ddd6097f9bf8486dcb154ce37716e60fac256949c6c
5
5
  SHA512:
6
- metadata.gz: 2f9bbde96c6269da00a9514aa6eb7660024435ddcb05ae10e57e02338ce69b8a3465b6f7380c551986e0b0e067db041c34fc640074b86e3da189442486e2c111
7
- data.tar.gz: 906df1d615b6ae0fb0fbb05636fcb8916ff671886b7319aad70801099974bc742da54444fcb858293c4777ffe89a87390c8d4de78d83c1a613314132dbf1661a
6
+ metadata.gz: 6ed979776d78d8e1d64e01ec46499d2453d13483495688cc79b71e0fc84b542d68e2a076015d480aaac812c90ef1dea49952f40b156d11d2b4dd95ace73a7688
7
+ data.tar.gz: 233229ccf2b0daa138dc6bd664ce14c8ddf770aaa232d42efc33d43419a3c7f0f5b673daf985e6745e49fcf8ceeac9503d84910319cb8a7d269178e08cd657eb
data/CHANGELOG.md CHANGED
@@ -1,5 +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
+
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
+
3
20
  ## 0.2.2
4
21
 
5
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,180 +1,169 @@
1
1
  # image_pack
2
2
 
3
- `image_pack` is a Ruby-native JPEG compressor prototype for Ruby `>= 3.4.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
49
+ - `:fast` / `:jpeg_turbo` — faster 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, ...)`.
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
+ ```
85
59
 
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.
60
+ `min_ssim:` searches for the lowest acceptable quality using a fast native luma SSIM guard.
88
61
 
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.
62
+ `strict: true` raises `ImagePack::InvalidImageError` on damaged/truncated JPEG warnings.
91
63
 
92
- ## Lossless JPEG optimize
64
+ `report: true` returns a Hash:
93
65
 
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.
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
+ ```
95
78
 
96
- Defaults are intentionally conservative:
79
+ With `output: "file.jpg"`, `output` is `true`.
97
80
 
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.
81
+ ## Lossless optimize
100
82
 
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.
83
+ ```ruby
84
+ ImagePack.optimize_bytes(jpeg)
85
+ ImagePack.optimize_file("photo.jpg", output: "photo.optimized.jpg")
86
+ ```
102
87
 
103
- ## SSIM guard
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.
104
89
 
105
- `compress` and `compress_pixels` accept `min_ssim:`. This enables a native guarded path:
90
+ Defaults: `progressive: true`, `strip_metadata: false`.
106
91
 
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`.
92
+ If `strip_metadata: true` would remove EXIF Orientation, `optimize_jpeg` raises `UnsupportedError` instead of silently changing visual orientation.
112
93
 
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.
94
+ ## Raw pixels
116
95
 
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.
96
+ ```ruby
97
+ ImagePack.compress_pixels(rgb,
98
+ width: 1920,
99
+ height: 1080,
100
+ channels: 3,
101
+ output: "frame.jpg"
102
+ )
103
+ ```
121
104
 
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.
105
+ `channels` must be `1`, `3`, or `4`. JPEG cannot store alpha, so RGBA input needs explicit opt-in:
123
106
 
124
- ## Execution modes
107
+ ```ruby
108
+ ImagePack.compress_pixels(rgba, width: 100, height: 100, channels: 4, drop_alpha: true)
109
+ ```
110
+
111
+ ## Inspect
125
112
 
126
113
  ```ruby
127
- ImagePack.compress(jpeg, execution: :direct)
128
- ImagePack.compress(jpeg, execution: :nogvl)
129
- ImagePack.compress(jpeg, execution: :offload)
130
- ImagePack.compress(jpeg, execution: :auto)
114
+ ImagePack.inspect_image(jpeg)
115
+ # => { format: :jpeg, width: 1920, height: 1080, channels: 3, bit_depth: 8, decoded_bytes: 6220800 }
131
116
  ```
132
117
 
133
- - `:direct` — executes under GVL, intended for small images.
134
- - `:nogvl` — uses `rb_nogvl(..., flags: 0)`.
135
- - `:offload` — uses `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` on Ruby 3.4+.
136
- - `:auto` — header-first policy chooses direct/nogvl/offload.
118
+ ## Execution
137
119
 
138
- ## Cancellation
120
+ Default mode is `:auto`.
139
121
 
140
- `cancellable: true` is supported for `execution: :nogvl`, `:offload`, or `:auto`.
141
- Cancellation is cooperative at decode, encode, and SSIM-search checkpoints, not instant.
122
+ ```ruby
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
127
+ ```
142
128
 
143
- ## Metadata / EXIF orientation
129
+ Use `ImagePack.offload_safe?` or `ImagePack.build_info` to inspect runtime support.
144
130
 
145
- `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.
131
+ Set `IMAGE_PACK_DISABLE_OFFLOAD=1` before loading the gem to disable offload.
146
132
 
147
- ## Build info
133
+ Long no-GVL/offload calls can be interrupted by raising into the worker thread:
148
134
 
149
135
  ```ruby
150
- ImagePack.build_info
151
- # => { version: "0.2.2", mozjpeg: "4.1.5", simd: true }
136
+ worker = Thread.new { ImagePack.compress_bytes(jpeg, execution: :nogvl, cancellable: true) }
137
+ worker.raise(ImagePack::CancelledError, "cancelled")
138
+ worker.join
152
139
  ```
153
140
 
154
- ## Vendoring
141
+ ## Configuration
142
+
143
+ ```ruby
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
150
+ ```
155
151
 
156
- The gem is expected to ship vendored native sources/libs, so end users should not install system libjpeg/mozjpeg.
157
- For repository preparation:
152
+ ## Development
158
153
 
159
154
  ```bash
160
155
  bundle exec rake vendor
161
156
  bundle exec rake compile
157
+ bundle exec rake test
162
158
  ```
163
159
 
164
160
  `rake vendor` pins MozJPEG `v4.1.5`.
165
161
 
166
- ## Current limitations
167
-
168
- - 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.
169
- - Arithmetic-coded JPEG support is disabled in the vendored `jconfig.h` for v0.2.2.
170
- - The SSIM guard is a fast 8x8 luma block metric and still assumes quality/SSIM monotonicity during binary search.
171
- - `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.
172
-
173
- ## What is intentionally not included
162
+ ## Limits
174
163
 
175
- - Jpegli / C++ code
176
- - AVIF/WebP/PNG
177
- - FFI
178
- - shell-out
179
- - external tempfiles for in-memory output
180
- - 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.
@@ -4,127 +4,19 @@ require "mkmf"
4
4
  require "fileutils"
5
5
  require "rbconfig"
6
6
 
7
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4.0")
8
- abort "image_pack requires Ruby >= 3.4.0; got #{RUBY_VERSION}"
7
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1.0")
8
+ abort "image_pack requires Ruby >= 3.1.0; got #{RUBY_VERSION}"
9
9
  end
10
10
 
11
- MOZJPEG_CORE_SOURCES = %w[
12
- jcapimin.c
13
- jcapistd.c
14
- jccoefct.c
15
- jccolor.c
16
- jcdctmgr.c
17
- jchuff.c
18
- jcext.c
19
- jcicc.c
20
- jcinit.c
21
- jcmainct.c
22
- jcmarker.c
23
- jcmaster.c
24
- jcomapi.c
25
- jcparam.c
26
- jcphuff.c
27
- jcprepct.c
28
- jcsample.c
29
- jctrans.c
30
- jdapimin.c
31
- jdapistd.c
32
- jdatadst.c
33
- jdatasrc.c
34
- jdcoefct.c
35
- jdcolor.c
36
- jddctmgr.c
37
- jdhuff.c
38
- jdicc.c
39
- jdinput.c
40
- jdmainct.c
41
- jdmarker.c
42
- jdmaster.c
43
- jdmerge.c
44
- jdphuff.c
45
- jdpostct.c
46
- jdsample.c
47
- jdtrans.c
48
- jerror.c
49
- jfdctflt.c
50
- jfdctfst.c
51
- jfdctint.c
52
- jidctflt.c
53
- jidctfst.c
54
- jidctint.c
55
- jidctred.c
56
- jquant1.c
57
- jquant2.c
58
- jutils.c
59
- jmemmgr.c
60
- jmemnobs.c
61
- ].freeze
62
-
63
- NEON_AARCH64_SOURCES = [
64
- "simd/arm/aarch64/jsimd.c",
65
- "simd/arm/aarch64/jchuff-neon.c",
66
- "simd/arm/jccolor-neon.c",
67
- "simd/arm/jcgray-neon.c",
68
- "simd/arm/jcphuff-neon.c",
69
- "simd/arm/jcsample-neon.c",
70
- "simd/arm/jdcolor-neon.c",
71
- "simd/arm/jdmerge-neon.c",
72
- "simd/arm/jdsample-neon.c",
73
- "simd/arm/jfdctfst-neon.c",
74
- "simd/arm/jfdctint-neon.c",
75
- "simd/arm/jidctfst-neon.c",
76
- "simd/arm/jidctint-neon.c",
77
- "simd/arm/jidctred-neon.c",
78
- "simd/arm/jquanti-neon.c",
79
- ].freeze
80
-
81
- X86_64_C_SOURCES = [
82
- "simd/x86_64/jsimd.c",
83
- ].freeze
84
-
85
- X86_64_ASM_SOURCES = %w[
86
- simd/x86_64/jsimdcpu.asm
87
- simd/x86_64/jfdctflt-sse.asm
88
- simd/x86_64/jccolor-sse2.asm
89
- simd/x86_64/jcgray-sse2.asm
90
- simd/x86_64/jchuff-sse2.asm
91
- simd/x86_64/jcphuff-sse2.asm
92
- simd/x86_64/jcsample-sse2.asm
93
- simd/x86_64/jdcolor-sse2.asm
94
- simd/x86_64/jdmerge-sse2.asm
95
- simd/x86_64/jdsample-sse2.asm
96
- simd/x86_64/jfdctfst-sse2.asm
97
- simd/x86_64/jfdctint-sse2.asm
98
- simd/x86_64/jidctflt-sse2.asm
99
- simd/x86_64/jidctfst-sse2.asm
100
- simd/x86_64/jidctint-sse2.asm
101
- simd/x86_64/jidctred-sse2.asm
102
- simd/x86_64/jquantf-sse2.asm
103
- simd/x86_64/jquanti-sse2.asm
104
- simd/x86_64/jccolor-avx2.asm
105
- simd/x86_64/jcgray-avx2.asm
106
- simd/x86_64/jcsample-avx2.asm
107
- simd/x86_64/jdcolor-avx2.asm
108
- simd/x86_64/jdmerge-avx2.asm
109
- simd/x86_64/jdsample-avx2.asm
110
- simd/x86_64/jfdctint-avx2.asm
111
- simd/x86_64/jidctint-avx2.asm
112
- simd/x86_64/jquanti-avx2.asm
113
- ].freeze
114
-
115
- X86_64_ASM_TEMPLATE_SOURCES = %w[
116
- simd/x86_64/jccolext-sse2.asm
117
- simd/x86_64/jcgryext-sse2.asm
118
- simd/x86_64/jdcolext-sse2.asm
119
- simd/x86_64/jdmrgext-sse2.asm
120
- simd/x86_64/jccolext-avx2.asm
121
- simd/x86_64/jcgryext-avx2.asm
122
- simd/x86_64/jdcolext-avx2.asm
123
- simd/x86_64/jdmrgext-avx2.asm
124
- ].freeze
125
-
126
- MOZJPEG_VERSION = "4.1.5"
127
- MOZJPEG_VERSION_NUMBER = "4001005"
11
+ require_relative "mozjpeg_sources"
12
+
13
+ MOZJPEG_CORE_SOURCES = ImagePackMozjpegSources::TOPLEVEL_C_SOURCES
14
+ MOZJPEG_INCLUDE_TEMPLATES = ImagePackMozjpegSources::TOPLEVEL_INCLUDE_TEMPLATES
15
+ NEON_AARCH64_SOURCES = ImagePackMozjpegSources::NEON_AARCH64_C_SOURCES
16
+ X86_64_C_SOURCES = ImagePackMozjpegSources::X86_64_C_SOURCES
17
+ X86_64_ASM_SOURCES = ImagePackMozjpegSources::X86_64_ASM_SOURCES
18
+ MOZJPEG_VERSION = ImagePackMozjpegSources::VERSION
19
+ MOZJPEG_VERSION_NUMBER = ImagePackMozjpegSources::VERSION_NUMBER
128
20
 
129
21
  def find_vendor_dir
130
22
  candidates = [
@@ -285,8 +177,14 @@ def select_simd_backend(mozjpeg_dir, arch)
285
177
  else
286
178
  missing = NEON_AARCH64_SOURCES.reject { |rel| File.exist?(File.join(mozjpeg_dir, rel)) }
287
179
  missing << "simd/arm/neon-compat.h.in" unless template_present
288
- warn "image_pack: ARM64 detected but NEON sources missing under #{mozjpeg_dir}:"
289
- missing.each { |m| warn " - #{m}" }
180
+ message = "image_pack: ARM64 detected but NEON sources are incomplete under #{mozjpeg_dir}:
181
+ " "#{missing.map { |m| " - #{m}" }.join("
182
+ ")}"
183
+ if ENV["IMAGE_PACK_REQUIRE_SIMD"] == "1"
184
+ abort "#{message}
185
+ image_pack: refusing scalar fallback because IMAGE_PACK_REQUIRE_SIMD=1."
186
+ end
187
+ warn message
290
188
  warn "image_pack: re-run `bundle exec rake vendor` after updating script/vendor_libs.rb."
291
189
  warn "image_pack: falling back to scalar."
292
190
  { kind: :none }
@@ -301,13 +199,25 @@ def select_simd_backend(mozjpeg_dir, arch)
301
199
  warn "#{message} Falling back to scalar; install nasm for ~3-4x speedup. Set IMAGE_PACK_REQUIRE_SIMD=1 to fail instead."
302
200
  { kind: :none }
303
201
  elsif !X86_64_C_SOURCES.all? { |rel| File.exist?(File.join(mozjpeg_dir, rel)) }
304
- warn "image_pack: x86_64 SIMD sources missing under #{mozjpeg_dir}/simd/x86_64/. Falling back to scalar."
202
+ missing = X86_64_C_SOURCES.reject { |rel| File.exist?(File.join(mozjpeg_dir, rel)) }
203
+ message = "image_pack: x86_64 SIMD C sources missing under #{mozjpeg_dir}: #{missing.join(', ')}"
204
+ ENV["IMAGE_PACK_REQUIRE_SIMD"] == "1" ? abort(message) : warn("#{message}. Falling back to scalar.")
305
205
  { kind: :none }
306
206
  else
207
+ missing_asm = X86_64_ASM_SOURCES.reject { |rel| File.exist?(File.join(mozjpeg_dir, rel)) }
208
+ unless missing_asm.empty?
209
+ message = "image_pack: x86_64 SIMD ASM sources missing under #{mozjpeg_dir}: #{missing_asm.join(', ')}"
210
+ if ENV["IMAGE_PACK_REQUIRE_SIMD"] == "1"
211
+ abort message
212
+ end
213
+ warn "#{message}. Falling back to scalar."
214
+ return { kind: :none }
215
+ end
216
+
307
217
  {
308
218
  kind: :x86_64_simd,
309
219
  c_sources: X86_64_C_SOURCES,
310
- asm_sources: X86_64_ASM_SOURCES.select { |rel| File.exist?(File.join(mozjpeg_dir, rel)) },
220
+ asm_sources: X86_64_ASM_SOURCES,
311
221
  extra_includes: [
312
222
  File.join(mozjpeg_dir, "simd"),
313
223
  File.join(mozjpeg_dir, "simd", "nasm"),
@@ -367,7 +277,8 @@ def configure_vendored_mozjpeg(vendor_dir)
367
277
 
368
278
  abort "vendored MozJPEG sources not found in #{mozjpeg_dir}" unless Dir.exist?(mozjpeg_dir)
369
279
 
370
- missing_sources = MOZJPEG_CORE_SOURCES.reject { |source| File.exist?(File.join(mozjpeg_dir, source)) }
280
+ required_sources = MOZJPEG_CORE_SOURCES + MOZJPEG_INCLUDE_TEMPLATES
281
+ missing_sources = required_sources.reject { |source| File.exist?(File.join(mozjpeg_dir, source)) }
371
282
  abort "vendored MozJPEG sources are incomplete: #{missing_sources.join(', ')}" unless missing_sources.empty?
372
283
 
373
284
  arch = detect_simd_arch
@@ -505,8 +416,8 @@ else
505
416
  $CFLAGS += " -O2"
506
417
  end
507
418
 
508
- $CFLAGS += " -DIMAGE_PACK_RUBY34_ONLY=1"
509
419
  $CFLAGS += build_info[:with_simd] ? " -DIMAGE_PACK_HAS_SIMD=1" : " -DIMAGE_PACK_PURE_C=1"
420
+ $CPPFLAGS += " -DIMAGE_PACK_MOZJPEG_VERSION=\\\"#{MOZJPEG_VERSION}\\\""
510
421
  $LIBS += " -lm" unless msvc?
511
422
 
512
423
  have_header("ruby/thread.h") or abort "ruby/thread.h not found"