image_pack 0.2.0 → 0.2.2

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: 96839f17672b82deb6a21685649f5f577fedeb8d7f07b79410ddce761de9d40d
4
- data.tar.gz: a806d3ff58c3a32d17e20df100511e31ad6d582facb9508c202ab928cc7cf7f7
3
+ metadata.gz: 301f10cb975250ef59e3bf1beae23208aeb3e40d6598721323b93e4ea8d2eee8
4
+ data.tar.gz: cc8b9139d2a16ebe94f2c32a047202ea9e9fdf04a608c1bc4608eb5d9e53d589
5
5
  SHA512:
6
- metadata.gz: cd8d2f768961d288cbfede915ee0a30f3fe538508c9d88f48d37cf27bf3f77f603e47ab60d0d4d044f298d6bb35336fad15df8fffabc034ecf04e71eb6d6fe44
7
- data.tar.gz: dbe7c27f803a23e9181276c8b4efda825ed14cb172385bf86c48591278e0776a63f4c4c2f1664e2dc6547745addc0268613860bd3e1bf3761ac6ce000022566e
6
+ metadata.gz: 2f9bbde96c6269da00a9514aa6eb7660024435ddcb05ae10e57e02338ce69b8a3465b6f7380c551986e0b0e067db041c34fc640074b86e3da189442486e2c111
7
+ data.tar.gz: 906df1d615b6ae0fb0fbb05636fcb8916ff671886b7319aad70801099974bc742da54444fcb858293c4777ffe89a87390c8d4de78d83c1a613314132dbf1661a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ - 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.
6
+ - Fixed libjpeg `longjmp` cleanup for encode/decode/luma-decode transient buffers.
7
+ - Fixed `max_output_size = 0` semantics; all size/dimension limits now consistently treat `0` as disabled.
8
+ - Fixed `compress_pixels(min_ssim:)`: it now uses the raw pixel buffer as the reference instead of a temporary quality-95 seed JPEG, and it respects the requested execution mode.
9
+ - Fixed metadata preservation drift: `strip_metadata: false` now preserves markers across fast, size, and SSIM paths.
10
+ - Fixed EXIF Orientation stripping: when metadata is stripped, orientation is applied to decoded pixels before output.
11
+ - Fixed `progressive:` for `algo: :mozjpeg`; `progressive: false` no longer silently inherits the max-compression progressive scan script.
12
+ - Added decode/luma-decode cancellation checkpoints.
13
+ - Reduced avoidable copying for explicit `execution: :direct` String/pixel inputs.
14
+ - Batched RGBA→RGB scanline encoding instead of writing one line at a time.
15
+ - Decoded SSIM candidate luma directly as grayscale instead of RGB + manual luma conversion.
16
+ - Made output-path writes safer by writing to a temporary file, checking `fclose`, and renaming into place.
17
+ - Added `compress_bytes`, `compress_file`, `optimize_jpeg`, `optimize_bytes`, `optimize_file`, and `build_info`.
18
+ - Added coefficient-level lossless JPEG optimization through `jpeg_read_coefficients` / `jpeg_write_coefficients`; this avoids decode→pixels→re-encode when only optimizing an existing JPEG.
19
+ - Made private native entrypoints private class methods.
20
+ - Added `-fvisibility=hidden` and `IMAGE_PACK_REQUIRE_SIMD=1` build guard.
21
+
22
+ ## 0.2.1
23
+
24
+ - `ip_compute_ssim_luma_buffer`: rewrote inner accumulators from `double` to
25
+ `int32_t`. For an 8x8 luma window all partial sums (sum, sum-of-squares,
26
+ cross-product) fit in 32 bits. GCC was already auto-vectorizing the
27
+ `double` version on AVX2 (4 lanes × fp64), but int32 doubles the lane
28
+ count (8 lanes × i32) and uses cheaper integer multiplies. Split the
29
+ kernel into a fixed-size 8x8 specialization plus a variable edge kernel.
30
+ Top hot symbol in the SSIM-guarded perf profile.
31
+ - `ip_build_luma_buffer`: split the runtime-strided BT.601 loop into
32
+ channels==3 and channels==4 specializations with `__restrict__` pointers
33
+ so the compiler can vectorize per-pixel work.
34
+ - `prepare_encode_row` (RGBA→RGB): `__restrict__` + hoisted width to enable
35
+ vectorization by the compiler.
36
+ - `ip_malloc_hot`: new helper for buffers we touch in tight loops right
37
+ after allocation. On Linux it issues `madvise(MADV_HUGEPAGE)` for
38
+ allocations >= 256 KiB to remove the per-cacheline minor page faults
39
+ that previously appeared inside `jsimd_*_avx2` hot loops in the perf
40
+ profile. No-op on macOS / non-Linux. Used for the decoded pixel buffer,
41
+ the SSIM luma buffer, and the pixel input buffer.
42
+ - SSIM-guarded path: reference and candidate decodes both now use
43
+ `fast_decode_mode=1` (no fancy upsampling, no block smoothing). The
44
+ comparison stays apples-to-apples since both sides use the same decode
45
+ pipeline; ~30% reduction in candidate-decode cost.
46
+ - `extconf.rb`: opt-in `IMAGE_PACK_MARCH=<arch>` env knob for tuned
47
+ builds (`native`, `x86-64-v3`, etc). Default build stays portable.
48
+ Also added `-fno-math-errno -fno-trapping-math` to remove libm-related
49
+ vectorization barriers without changing semantics for the integer hot
50
+ paths.
51
+ - CI: Linux x86_64 jobs now compile with `IMAGE_PACK_MARCH=x86-64-v3`
52
+ (AVX2 baseline; covers all current GitHub-hosted runner generations).
53
+ arm64 / macOS unchanged.
54
+
3
55
  ## 0.2.0
4
56
 
5
57
  - Added `min_ssim:` to `ImagePack.compress` for SSIM-guarded JPEG compression.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `image_pack` is a Ruby-native JPEG compressor prototype for Ruby `>= 3.4.0`.
4
4
 
5
- Current version: `0.2.0`.
5
+ Current version: `0.2.2`.
6
6
 
7
7
  This pure-C variant intentionally removes Jpegli and any C++ toolchain requirement.
8
8
  The native layer is written in C and links against vendored MozJPEG, which is libjpeg-compatible and based on libjpeg-turbo.
@@ -14,8 +14,8 @@ ImagePack.compress(input, algo: :jpeg_turbo)
14
14
  ImagePack.compress(input, algo: :mozjpeg)
15
15
  ```
16
16
 
17
- - `:jpeg_turbo` — fast libjpeg-compatible mode with MozJPEG-specific size optimizations disabled.
18
- - `:mozjpeg` — size-oriented mode using progressive/optimized Huffman coding through the MozJPEG/libjpeg API.
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
19
 
20
20
  Both modes produce ordinary `.jpg` files.
21
21
 
@@ -52,6 +52,14 @@ ImagePack.compress(jpeg, algo: :mozjpeg, min_ssim: 0.985)
52
52
  ImagePack.compress(jpeg, algo: :mozjpeg, quality: 75, min_ssim: 0.985)
53
53
 
54
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)
57
+
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")
62
+ ImagePack.optimize_bytes(jpeg)
55
63
 
56
64
  ImagePack.compress_pixels(rgb_buffer,
57
65
  width: 1920,
@@ -69,19 +77,32 @@ ImagePack.inspect_image(jpeg)
69
77
  `compress(input, ...)` accepts JPEG input only:
70
78
 
71
79
  - binary `String` (`ASCII-8BIT`) — JPEG bytes;
72
- - non-binary `String` — file path;
80
+ - non-binary `String` that points to an existing file — file path;
73
81
  - `Pathname` — file path;
74
82
  - `IO::Buffer` — JPEG bytes, copied before native no-GVL execution for safety.
75
83
 
84
+ For non-ambiguous call sites prefer `compress_bytes(bytes, ...)` or `compress_file(path, ...)`.
85
+
76
86
  `compress_pixels(buffer, ...)` accepts raw pixels as binary `String` or `IO::Buffer`.
77
- `channels` must be `1`, `3`, or `4`. Alpha in RGBA input is dropped in v0.2.0.
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.
88
+
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
+
92
+ ## Lossless JPEG optimize
78
93
 
79
- `output: nil` returns binary JPEG bytes. `output: String/Pathname` writes to path and returns `true`.
80
- Streaming output is intentionally not supported in v0.2.0.
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.
95
+
96
+ Defaults are intentionally conservative:
97
+
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.
100
+
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.
81
102
 
82
103
  ## SSIM guard
83
104
 
84
- `compress` accepts `min_ssim:` for JPEG input. This enables a native guarded path:
105
+ `compress` and `compress_pixels` accept `min_ssim:`. This enables a native guarded path:
85
106
 
86
107
  1. decode the original JPEG to reference pixels;
87
108
  2. encode trial JPEG candidates with the existing MozJPEG/libjpeg backend;
@@ -96,8 +117,9 @@ the encoder raises quality only if the candidate violates the SSIM floor.
96
117
  If no quality can satisfy the requested floor, `ImagePack::QualityConstraintError`
97
118
  is raised. With `execution: :auto`, guarded compression uses the no-GVL/offload
98
119
  path instead of the small-image direct path because it may encode/decode several
99
- candidates. `min_ssim` is intentionally not supported by `compress_pixels` in
100
- this release.
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.
121
+
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.
101
123
 
102
124
  ## Execution modes
103
125
 
@@ -115,8 +137,19 @@ ImagePack.compress(jpeg, execution: :auto)
115
137
 
116
138
  ## Cancellation
117
139
 
118
- `cancellable: true` is supported only for `algo: :mozjpeg` and `execution: :nogvl`, `:offload`, or `:auto`.
119
- Cancellation is cooperative at encoder scanline checkpoints, not instant.
140
+ `cancellable: true` is supported for `execution: :nogvl`, `:offload`, or `:auto`.
141
+ Cancellation is cooperative at decode, encode, and SSIM-search checkpoints, not instant.
142
+
143
+ ## Metadata / EXIF orientation
144
+
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.
146
+
147
+ ## Build info
148
+
149
+ ```ruby
150
+ ImagePack.build_info
151
+ # => { version: "0.2.2", mozjpeg: "4.1.5", simd: true }
152
+ ```
120
153
 
121
154
  ## Vendoring
122
155
 
@@ -130,11 +163,18 @@ bundle exec rake compile
130
163
 
131
164
  `rake vendor` pins MozJPEG `v4.1.5`.
132
165
 
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
+
133
173
  ## What is intentionally not included
134
174
 
135
175
  - Jpegli / C++ code
136
176
  - AVIF/WebP/PNG
137
177
  - FFI
138
178
  - shell-out
139
- - tempfiles by default
179
+ - external tempfiles for in-memory output
140
180
  - byte-size targets / max-bytes policy
@@ -186,10 +186,17 @@ def detect_simd_arch
186
186
  end
187
187
 
188
188
  def find_nasm
189
- ENV["AS_NASM"].presence_or_nil ||
189
+ presence(ENV["AS_NASM"]) ||
190
190
  %w[nasm yasm].find { |bin| system("which #{bin} >/dev/null 2>&1") }
191
191
  end
192
192
 
193
+ def presence(string)
194
+ return nil if string.nil?
195
+ return nil if string.empty?
196
+
197
+ string
198
+ end
199
+
193
200
  def write_mozjpeg_config_headers!(build_dir, with_simd:)
194
201
  with_simd_flag = with_simd ? "#define WITH_SIMD 1" : "#undef WITH_SIMD"
195
202
 
@@ -287,7 +294,11 @@ def select_simd_backend(mozjpeg_dir, arch)
287
294
 
288
295
  when :x86_64
289
296
  if find_nasm.nil?
290
- warn "image_pack: x86_64 detected but NASM/YASM not found on PATH. Falling back to scalar; install nasm for ~3-4x speedup."
297
+ message = "image_pack: x86_64 detected but NASM/YASM not found on PATH."
298
+ if ENV["IMAGE_PACK_REQUIRE_SIMD"] == "1"
299
+ abort "#{message} Install nasm/yasm or unset IMAGE_PACK_REQUIRE_SIMD."
300
+ end
301
+ warn "#{message} Falling back to scalar; install nasm for ~3-4x speedup. Set IMAGE_PACK_REQUIRE_SIMD=1 to fail instead."
291
302
  { kind: :none }
292
303
  elsif !X86_64_C_SOURCES.all? { |rel| File.exist?(File.join(mozjpeg_dir, rel)) }
293
304
  warn "image_pack: x86_64 SIMD sources missing under #{mozjpeg_dir}/simd/x86_64/. Falling back to scalar."
@@ -350,21 +361,6 @@ def write_neon_compat_header!(mozjpeg_dir)
350
361
  target
351
362
  end
352
363
 
353
- class String
354
- unless instance_methods.include?(:presence_or_nil)
355
- def presence_or_nil
356
- empty? ? nil : self
357
- end
358
- end
359
- end
360
- class NilClass
361
- unless instance_methods.include?(:presence_or_nil)
362
- def presence_or_nil
363
- nil
364
- end
365
- end
366
- end
367
-
368
364
  def configure_vendored_mozjpeg(vendor_dir)
369
365
  versions = File.read(File.join(vendor_dir, ".vendored"))
370
366
  mozjpeg_dir = File.join(vendor_dir, "mozjpeg")
@@ -497,6 +493,14 @@ $warnflags = ""
497
493
 
498
494
  unless msvc?
499
495
  $CFLAGS += " -O3 -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11"
496
+ $CFLAGS += " -fno-math-errno -fno-trapping-math -fvisibility=hidden"
497
+
498
+ march = ENV["IMAGE_PACK_MARCH"].to_s
499
+ unless march.empty?
500
+ $CFLAGS += " -march=#{march}"
501
+ $CFLAGS += " -mtune=#{march}" if march == "native"
502
+ puts "image_pack: using -march=#{march}"
503
+ end
500
504
  else
501
505
  $CFLAGS += " -O2"
502
506
  end