safe_image 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/ext/safe_image_vips_helper/extconf.rb +33 -15
- data/lib/safe_image/ico.rb +1 -1
- data/lib/safe_image/image_magick_backend.rb +14 -2
- data/lib/safe_image/metadata_operations.rb +1 -1
- data/lib/safe_image/native.rb +1 -1
- data/lib/safe_image/remote.rb +2 -2
- data/lib/safe_image/runner.rb +5 -1
- data/lib/safe_image/version.rb +1 -1
- data/lib/safe_image.rb +5 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: befa424ca6c37f5008b16030433887ea82049fc5a814b562b354842b1b4654ba
|
|
4
|
+
data.tar.gz: ea6932183795638094040dbe2dae77a8a4ab698193aa0dd720f5b805091d0cfb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f3f74766cfe5c514976dee2f62f76c60bdec7799ba6602c88dfdba189046c7cce32b844f5ff529677ecf3daed8d5780a617900a3959f83e6e1242505133715a1
|
|
7
|
+
data.tar.gz: 624d2deff70c1c908719069775d3bd7a7103eaf77e6b57a3020d014f06676300cba97cff43ce57354ec05b432f647054276d8a2367170365fe91fe700ec0a8b8
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.1 - 2026-06-23]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added macOS CI coverage for the full test suite against Homebrew-provided
|
|
13
|
+
dependencies, including libvips, ImageMagick, jpegoptim, jpeg-turbo,
|
|
14
|
+
pngquant, and oxipng.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Made the bundled `safe_image_vips_helper` build optional at gem install time.
|
|
19
|
+
Installing Safe Image no longer fails on systems without libvips development
|
|
20
|
+
files or when the helper cannot be compiled; instead, install completes without
|
|
21
|
+
the helper and `SafeImage.configure!(backend: :vips)` continues to fail closed
|
|
22
|
+
with `VipsUnavailableError`.
|
|
23
|
+
- Clean up stale or partially-built `safe_image_vips_helper` binaries when the
|
|
24
|
+
optional helper build is skipped, avoiding accidental use of an old helper.
|
|
25
|
+
- Look for trusted external tools in Homebrew's Apple Silicon paths so the
|
|
26
|
+
ImageMagick and JPEG optimizer backends work on macOS CI and typical Homebrew
|
|
27
|
+
installs.
|
|
28
|
+
- Use real temporary-directory paths for remote-download tempfiles and tests so
|
|
29
|
+
macOS' `/var` symlink does not trip Safe Image's symlink-component checks.
|
|
30
|
+
- Use the bundled DejaVu font for ImageMagick letter-avatar rendering so the
|
|
31
|
+
default font does not depend on host fontconfig availability.
|
|
32
|
+
- Escape custom `PKG_CONFIG` paths in the generated helper Makefile so install
|
|
33
|
+
works when the path contains shell- or Makefile-significant characters.
|
|
34
|
+
|
|
8
35
|
## [0.5.0 - 2026-06-22]
|
|
9
36
|
|
|
10
37
|
### Changed
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "mkmf"
|
|
4
3
|
require "rbconfig"
|
|
5
|
-
|
|
6
|
-
pkg_config("vips") or abort "libvips development files are required (pkg-config vips failed)"
|
|
7
|
-
|
|
8
|
-
pkg_cflags = `pkg-config --cflags vips`.strip
|
|
9
|
-
pkg_libs = `pkg-config --libs vips`.strip
|
|
10
|
-
cflags = [ENV["CFLAGS"] || RbConfig::CONFIG["CFLAGS"], RbConfig::CONFIG["CPPFLAGS"], pkg_cflags].compact.join(" ")
|
|
11
|
-
ldflags = [ENV["LDFLAGS"] || RbConfig::CONFIG["LDFLAGS"]].compact.join(" ")
|
|
4
|
+
require "shellwords"
|
|
12
5
|
|
|
13
6
|
helper = "safe_image_vips_helper"
|
|
14
7
|
source = "safe_image_vips_helper.c"
|
|
15
8
|
lib_dir = File.expand_path("../../lib/safe_image", __dir__)
|
|
9
|
+
installed_helper = File.join(lib_dir, helper)
|
|
10
|
+
|
|
11
|
+
cflags =
|
|
12
|
+
[ENV["CFLAGS"] || RbConfig::CONFIG["CFLAGS"], RbConfig::CONFIG["CPPFLAGS"], "$(VIPS_CFLAGS)"].compact.join(" ")
|
|
13
|
+
ldflags = [ENV["LDFLAGS"] || RbConfig::CONFIG["LDFLAGS"]].compact.join(" ")
|
|
14
|
+
pkg_config = ENV.fetch("PKG_CONFIG", "pkg-config")
|
|
15
|
+
make_pkg_config = pkg_config.shellescape.gsub("$", "$$")
|
|
16
16
|
|
|
17
17
|
File.write(
|
|
18
18
|
"Makefile",
|
|
@@ -21,18 +21,36 @@ File.write(
|
|
|
21
21
|
CC = #{RbConfig::CONFIG.fetch("CC")}
|
|
22
22
|
CFLAGS = #{cflags}
|
|
23
23
|
LDFLAGS = #{ldflags}
|
|
24
|
-
|
|
24
|
+
PKG_CONFIG = #{make_pkg_config}
|
|
25
|
+
VIPS_CFLAGS = $(shell $(PKG_CONFIG) --cflags vips 2>/dev/null)
|
|
26
|
+
VIPS_LIBS = $(shell $(PKG_CONFIG) --libs vips 2>/dev/null)
|
|
27
|
+
LIBS = $(VIPS_LIBS) -lm
|
|
25
28
|
INSTALL = #{RbConfig::CONFIG.fetch("INSTALL", "install")}
|
|
26
29
|
|
|
30
|
+
.PHONY: all install clean distclean
|
|
31
|
+
|
|
27
32
|
all: #{helper}
|
|
28
33
|
|
|
29
34
|
#{helper}: #{source}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
rm -f #{helper}
|
|
36
|
+
if $(PKG_CONFIG) --exists vips >/dev/null 2>&1; then \
|
|
37
|
+
$(CC) $(CFLAGS) -o #{helper} #{source} $(LDFLAGS) $(LIBS) || { \
|
|
38
|
+
echo "safe_image: warning: failed to compile optional libvips helper; install will continue without vips backend support" >&2; \
|
|
39
|
+
rm -f #{helper}; \
|
|
40
|
+
}; \
|
|
41
|
+
else \
|
|
42
|
+
echo "safe_image: warning: pkg-config could not find libvips; install will continue without vips backend support" >&2; \
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
install: all
|
|
46
|
+
mkdir -p #{lib_dir.shellescape}
|
|
47
|
+
if [ -x #{helper} ]; then \
|
|
48
|
+
cp #{helper} #{installed_helper.shellescape}; \
|
|
49
|
+
chmod 0755 #{installed_helper.shellescape}; \
|
|
50
|
+
else \
|
|
51
|
+
rm -f #{installed_helper.shellescape}; \
|
|
52
|
+
echo "safe_image: warning: optional libvips helper was not installed; configure!(backend: :vips) will raise" >&2; \
|
|
53
|
+
fi
|
|
36
54
|
|
|
37
55
|
clean:
|
|
38
56
|
rm -f #{helper} *.o
|
data/lib/safe_image/ico.rb
CHANGED
|
@@ -59,7 +59,7 @@ module SafeImage
|
|
|
59
59
|
# reaches a decoder.
|
|
60
60
|
validate_pixels!(*entry_dimensions(data, entry), max_pixels)
|
|
61
61
|
payload = data.byteslice(entry.offset, entry.size)
|
|
62
|
-
Tempfile.create(%w[safe-image-ico .png]) do |tmp|
|
|
62
|
+
Tempfile.create(%w[safe-image-ico .png], SafeImage.real_tmpdir) do |tmp|
|
|
63
63
|
tmp.binmode
|
|
64
64
|
tmp.write(payload)
|
|
65
65
|
tmp.close
|
|
@@ -28,6 +28,7 @@ module SafeImage
|
|
|
28
28
|
].freeze
|
|
29
29
|
|
|
30
30
|
ALLOWED_FONTS = %w[NimbusSans-Regular DejaVu-Sans Liberation-Sans Arial Helvetica Adwaita-Sans].freeze
|
|
31
|
+
BUNDLED_DEJAVU = File.expand_path("fonts/DejaVuSans.ttf", __dir__)
|
|
31
32
|
|
|
32
33
|
def probe(path, timeout: Runner::DEFAULT_TIMEOUT, max_pixels: nil)
|
|
33
34
|
raise UnsupportedFormatError, "ImageMagick identify not available" unless Runner.available?("identify")
|
|
@@ -261,6 +262,7 @@ module SafeImage
|
|
|
261
262
|
if ALLOWED_FONTS.none? { |candidate| candidate == font_name }
|
|
262
263
|
raise ArgumentError, "unsupported font: #{font_name.inspect}"
|
|
263
264
|
end
|
|
265
|
+
font_arg = imagemagick_font(font_name)
|
|
264
266
|
|
|
265
267
|
argv = [
|
|
266
268
|
command,
|
|
@@ -273,7 +275,7 @@ module SafeImage
|
|
|
273
275
|
"-fill",
|
|
274
276
|
"#FFFFFFCC",
|
|
275
277
|
"-font",
|
|
276
|
-
|
|
278
|
+
font_arg,
|
|
277
279
|
"-gravity",
|
|
278
280
|
"Center",
|
|
279
281
|
"-annotate",
|
|
@@ -283,7 +285,17 @@ module SafeImage
|
|
|
283
285
|
"8",
|
|
284
286
|
output_arg
|
|
285
287
|
]
|
|
286
|
-
run_image_command(argv, output, "generated", "png", timeout)
|
|
288
|
+
run_image_command(argv, output, "generated", "png", timeout, read: font_read_paths(font_arg))
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def imagemagick_font(font_name)
|
|
292
|
+
return BUNDLED_DEJAVU if font_name == "DejaVu-Sans" && File.file?(BUNDLED_DEJAVU)
|
|
293
|
+
|
|
294
|
+
font_name
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def font_read_paths(font_arg)
|
|
298
|
+
font_arg.include?(File::SEPARATOR) ? [font_arg] : []
|
|
287
299
|
end
|
|
288
300
|
|
|
289
301
|
def fix_orientation(input:, output:, timeout: Runner::DEFAULT_TIMEOUT)
|
|
@@ -128,7 +128,7 @@ module SafeImage
|
|
|
128
128
|
private
|
|
129
129
|
|
|
130
130
|
def vips_ico_dominant_color(path, max_pixels:)
|
|
131
|
-
Tempfile.create(%w[safe-image-ico .png]) do |tmp|
|
|
131
|
+
Tempfile.create(%w[safe-image-ico .png], SafeImage.real_tmpdir) do |tmp|
|
|
132
132
|
tmp.close
|
|
133
133
|
Ico.convert_to_png(path, tmp.path, max_pixels: max_pixels)
|
|
134
134
|
VipsBackend.dominant_color(tmp.path, max_pixels: max_pixels)
|
data/lib/safe_image/native.rb
CHANGED
|
@@ -121,7 +121,7 @@ module SafeImage
|
|
|
121
121
|
raise LimitError, "rgba buffer dimensions exceed 4096x4096" if width > 4096 || height > 4096
|
|
122
122
|
raise ArgumentError, "rgba buffer must be width*height*4 bytes" if bytes.bytesize != width * height * 4
|
|
123
123
|
|
|
124
|
-
Tempfile.create(%w[safe-image-rgba .rgba], binmode: true) do |raw|
|
|
124
|
+
Tempfile.create(%w[safe-image-rgba .rgba], SafeImage.real_tmpdir, binmode: true) do |raw|
|
|
125
125
|
raw.write(bytes)
|
|
126
126
|
raw.close
|
|
127
127
|
NativeHelper.png_from_rgba(raw.path, width, height, String(output))
|
data/lib/safe_image/remote.rb
CHANGED
|
@@ -149,7 +149,7 @@ module SafeImage
|
|
|
149
149
|
uri = parse_uri(url)
|
|
150
150
|
started_at = monotonic_time
|
|
151
151
|
|
|
152
|
-
Tempfile.create(%w[safe-image-remote .bin], binmode: true) do |file|
|
|
152
|
+
Tempfile.create(%w[safe-image-remote .bin], SafeImage.real_tmpdir, binmode: true) do |file|
|
|
153
153
|
response =
|
|
154
154
|
request(
|
|
155
155
|
uri,
|
|
@@ -313,7 +313,7 @@ module SafeImage
|
|
|
313
313
|
uri = parse_uri(url)
|
|
314
314
|
started_at = monotonic_time
|
|
315
315
|
|
|
316
|
-
Tempfile.create(%w[safe-image-remote .bin], binmode: true) do |file|
|
|
316
|
+
Tempfile.create(%w[safe-image-remote .bin], SafeImage.real_tmpdir, binmode: true) do |file|
|
|
317
317
|
original_path = file.path
|
|
318
318
|
path = original_path
|
|
319
319
|
ext = nil
|
data/lib/safe_image/runner.rb
CHANGED
|
@@ -37,7 +37,11 @@ module SafeImage
|
|
|
37
37
|
# Give well-behaved tools a short flush window after TERM before KILL keeps
|
|
38
38
|
# the timeout hard without leaking process groups.
|
|
39
39
|
TERMINATE_GRACE_SECONDS = 0.2
|
|
40
|
-
|
|
40
|
+
# Homebrew on Apple Silicon installs some trusted tools outside /usr/local/bin;
|
|
41
|
+
# jpeg-turbo is keg-only, so jpegtran lives under its opt prefix.
|
|
42
|
+
TRUSTED_PATH = %w[/usr/bin /bin /usr/local/bin /opt/homebrew/bin /opt/homebrew/opt/jpeg-turbo/bin].join(
|
|
43
|
+
File::PATH_SEPARATOR
|
|
44
|
+
).freeze
|
|
41
45
|
ALLOWED_ENV_KEYS = %w[LANG LC_ALL LC_CTYPE TZ].freeze
|
|
42
46
|
IMAGEMAGICK_POLICY_PATH = File.expand_path("imagemagick_policy", __dir__)
|
|
43
47
|
IMAGEMAGICK_POLICY_FILE = File.join(IMAGEMAGICK_POLICY_PATH, "policy.xml").freeze
|
data/lib/safe_image/version.rb
CHANGED
data/lib/safe_image.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "tmpdir"
|
|
3
4
|
require_relative "safe_image/version"
|
|
4
5
|
|
|
5
6
|
module SafeImage
|
|
@@ -47,6 +48,10 @@ module SafeImage
|
|
|
47
48
|
# it in with a single assignment, so readers never observe a half-applied
|
|
48
49
|
# config.
|
|
49
50
|
Config = Data.define(:backend, :landlock, :max_pixels)
|
|
51
|
+
|
|
52
|
+
def self.real_tmpdir
|
|
53
|
+
@real_tmpdir ||= File.realpath(Dir.tmpdir)
|
|
54
|
+
end
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
require_relative "safe_image/native"
|