From e36a2cd05721334a5386d4bd4060cb7febef174e Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Aug 2026 17:52:36 -0400 Subject: [PATCH] Add `inherit_fds:` so a loader can name an already-open input A caller holding an open file had no way to have the tool read it in place. The one route that carried a descriptor, handing `source` a pre-built `MiniMagick::Tool`, dropped `loader`, `page` and `geometry` without a word. Accept `inherit_fds:` as a loader option and give it to the tool at construction, so the source stays a `/dev/fd/N` path and every other loader option still applies. mini_magick added the option in 5.4.0, so raise `LoadError` naming that version when an older one is loaded. --- doc/minimagick.md | 10 +++++ lib/image_processing/mini_magick.rb | 23 ++++++++--- test/mini_magick_test.rb | 62 +++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/doc/minimagick.md b/doc/minimagick.md index d64de0e..1e2c13f 100644 --- a/doc/minimagick.md +++ b/doc/minimagick.md @@ -360,6 +360,7 @@ It accepts the following special options: * `:geometry` -- geometry that should be applied when loading * `:auto_orient` -- whether the image should be automatically oriented after it's loaded (defaults to `true`) * `:define` -- creates definitions that coders and decoders use for reading and writing image data +* `:inherit_fds` -- IO objects the command inherits, so the source may name one as `/dev/fd/N` and be read without a copy (requires mini_magick 5.4.0, and a system that exposes descriptors as `/dev/fd`) ```rb ImageProcessing::MiniMagick.loader(loader: "jpg").call(image) @@ -376,6 +377,15 @@ ImageProcessing::MiniMagick.loader(auto_orient: false).call(image) ImageProcessing::MiniMagick.loader(define: { jpeg: { size: "300x300" } }).call(image) # convert -define jpeg:size=300x300 input.jpg -auto-orient output.jpg + +File.open("input.pdf", "rb") do |file| + ImageProcessing::MiniMagick + .source("/dev/fd/#{file.fileno}") + .loader(inherit_fds: [file], page: 0) + .convert("png") + .call + # convert /dev/fd/3[0] -auto-orient output.png +end ``` All other options given will be interpreted as ImageMagick operations to be diff --git a/lib/image_processing/mini_magick.rb b/lib/image_processing/mini_magick.rb index 9e8f3fc..b9ce362 100644 --- a/lib/image_processing/mini_magick.rb +++ b/lib/image_processing/mini_magick.rb @@ -9,11 +9,20 @@ module ImageProcessing module MiniMagick extend Chainable - def self.convert_shim(&block) + # mini_magick gained `inherit_fds:` on MiniMagick::Shell#execute in 5.4.0. + INHERIT_FDS_MINIMUM_VERSION = Gem::Version.new("5.4.0") + + def self.convert_shim(inherit_fds: nil, &block) + if inherit_fds && ::MiniMagick.version < INHERIT_FDS_MINIMUM_VERSION + raise LoadError, "The `inherit_fds` loader option requires mini_magick #{INHERIT_FDS_MINIMUM_VERSION} or newer, but mini_magick #{::MiniMagick.version} is loaded. Please upgrade the gem." + end + + options = inherit_fds ? { inherit_fds: inherit_fds } : {} + if ::MiniMagick.respond_to?(:convert) - ::MiniMagick.convert(&block) + ::MiniMagick.convert(**options, &block) else - ::MiniMagick::Tool::Convert.new(&block) + ::MiniMagick::Tool::Convert.new(**options, &block) end end @@ -37,12 +46,16 @@ class Processor < ImageProcessing::Processor # Initializes the image on disk into a MiniMagick::Tool object. Accepts # additional options related to loading the image (e.g. geometry). # Additionally auto-orients the image to be upright. - def self.load_image(path_or_magick, loader: nil, page: nil, geometry: nil, auto_orient: true, **options) + # `inherit_fds` names IO objects the tool inherits, so the source may be a + # `/dev/fd/N` path. The source stays a path, so `loader`, `page` and + # `geometry` still apply to it, which they would not if the caller passed + # a pre-built MiniMagick::Tool carrying the descriptor. + def self.load_image(path_or_magick, loader: nil, page: nil, geometry: nil, auto_orient: true, inherit_fds: nil, **options) if path_or_magick.is_a?(::MiniMagick::Tool) magick = path_or_magick else source_path = path_or_magick - magick = ::ImageProcessing::MiniMagick.convert_shim + magick = ::ImageProcessing::MiniMagick.convert_shim(inherit_fds: inherit_fds) Utils.apply_options(magick, **options) diff --git a/test/mini_magick_test.rb b/test/mini_magick_test.rb index defd9c5..8bad1bf 100644 --- a/test/mini_magick_test.rb +++ b/test/mini_magick_test.rb @@ -9,6 +9,14 @@ @square = fixture_image("square.jpg") end + def with_mini_magick_version(version) + original = MiniMagick.method(:version) + MiniMagick.define_singleton_method(:version) { Gem::Version.new(version) } + yield + ensure + MiniMagick.define_singleton_method(:version, original) + end + it "applies imagemagick operations" do actual = ImageProcessing::MiniMagick.flip.call(@portrait) expected = Tempfile.new(["result", ".jpg"], binmode: true).tap do |tempfile| @@ -66,6 +74,60 @@ refute_equal 0, processed.size end + if MiniMagick.version >= ImageProcessing::MiniMagick::INHERIT_FDS_MINIMUM_VERSION + it "reads a source given as a descriptor named in inherit_fds" do + tiff = Tempfile.new(["file", ".tiff"]) + ImageProcessing::MiniMagick.convert_shim do |convert| + convert.merge! [@portrait.path, @portrait.path, @portrait.path] + convert << tiff.path + end + + File.open(tiff.path, "rb") do |file| + processed = ImageProcessing::MiniMagick + .source("/dev/fd/#{file.fileno}") + .loader(loader: "tiff", page: 0, inherit_fds: [file]) + .convert!("jpg") + + assert_equal 1, MiniMagick::Image.new(processed.path).pages.size + end + end + + it "applies loader, page and geometry to a source named as an inherited descriptor" do + magick = ImageProcessing::MiniMagick + .source("/dev/fd/3") + .loader(inherit_fds: [@portrait], loader: "jpg", page: 0, geometry: "300x300") + .call(save: false) + + assert_equal %W[jpg:/dev/fd/3[0][300x300] -auto-orient], magick.args + end + else + it "tells the caller to upgrade when the installed mini_magick predates inherit_fds" do + error = assert_raises(LoadError) do + ImageProcessing::MiniMagick + .source(@portrait) + .loader(inherit_fds: [@portrait]) + .convert!("jpg") + end + + assert_includes error.message, MiniMagick.version.to_s + assert_includes error.message, "5.4.0" + end + end + + it "names both the required and the running mini_magick version when it predates inherit_fds" do + error = assert_raises(LoadError) do + with_mini_magick_version("5.3.3") do + ImageProcessing::MiniMagick + .source(@portrait) + .loader(inherit_fds: [@portrait]) + .convert!("jpg") + end + end + + assert_includes error.message, "5.3.3" + assert_includes error.message, "5.4.0" + end + it "disallows split layers by default" do tiff = Tempfile.new(["file", ".tiff"]) ImageProcessing::MiniMagick.convert_shim do |convert|