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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29ce0a1f4932ad1cf1dd7e47c6ae9b29009a7d2d63d54e350ec62feb1dc09e22
4
- data.tar.gz: 2bd28448920dc859ddb8e5a0703d51163aee702807184971b8bcea594fd36fec
3
+ metadata.gz: befa424ca6c37f5008b16030433887ea82049fc5a814b562b354842b1b4654ba
4
+ data.tar.gz: ea6932183795638094040dbe2dae77a8a4ab698193aa0dd720f5b805091d0cfb
5
5
  SHA512:
6
- metadata.gz: c756eb00ec11291985e5b9f6f573abf19399c0592e1e7d390e9239e29d359893432f55a86283441d22e22bd6a3b0d0f98b3886735d73737c809937320220b0ca
7
- data.tar.gz: 2b7b4e9d309a6184a9881010eb058990eb4b86b293ca717c26f12d89cc9310fa8dc03a8bb9dcbeb6071840529a86f0d68726cda8715602d46ab1bc7bb80f8aae
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
- LIBS = #{pkg_libs} -lm
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
- $(CC) $(CFLAGS) -o #{helper} #{source} $(LDFLAGS) $(LIBS)
31
-
32
- install: #{helper}
33
- mkdir -p #{lib_dir}
34
- cp #{helper} #{File.join(lib_dir, helper)}
35
- chmod 0755 #{File.join(lib_dir, helper)}
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
@@ -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
- font_name,
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)
@@ -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))
@@ -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
@@ -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
- TRUSTED_PATH = "/usr/bin:/bin:/usr/local/bin".freeze
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafeImage
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
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"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_image
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron