Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/minimagick.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ File.extname(result.path)
By default the original format is retained when writing the image to a file. If
the source file doesn't have a file extension, the format will default to JPEG.

When a destination path is given, its extension determines the format. When
the destination path has no extension, as with a `/dev/fd/N` path, the format
passed to `#convert` is used instead.

#### `#define`

Adds coder/decoder options with [`-define`] from the specified Hash.
Expand Down
4 changes: 4 additions & 0 deletions doc/vips.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ File.extname(result.path)
By default the original format is retained when writing the image to a file. If
the source file doesn't have a file extension, the format will default to JPEG.

When a destination path is given, its extension determines the format. When
the destination path has no extension, as with a `/dev/fd/N` path, the format
passed to `#convert` is used instead.

NOTE: libvips 8.6 is able to normally read GIF images (and convert them to
other formats), but it's not able to *save* to GIF format. If you need full GIF
support, you need to use libvips 8.7+ compiled with ImageMagick support
Expand Down
16 changes: 13 additions & 3 deletions lib/image_processing/mini_magick.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,21 @@ def self.load_image(path_or_magick, loader: nil, page: nil, geometry: nil, auto_

# Calls the built ImageMagick command to perform processing and save
# the result to disk. Accepts additional options related to saving the
# image (e.g. quality).
def self.save_image(magick, destination_path, allow_splitting: false, **options)
# image (e.g. quality). `format` names the output format when given,
# otherwise ImageMagick infers it from the path's extension.
def self.save_image(magick, destination_path, format = nil, allow_splitting: false, **options)
Utils.apply_options(magick, **options)

magick << destination_path
if format
# ImageMagick reads everything before the first colon as the
# format, so a format carrying a colon could redirect the output.
format = format.to_s
fail Error, "invalid format: #{format.inspect}" unless format.ascii_only? && format.match?(/\A\w+\z/)

magick << "#{format}:#{destination_path}"
else
magick << destination_path
end
magick.call

Utils.disallow_split_layers!(destination_path) unless allow_splitting
Expand Down
6 changes: 5 additions & 1 deletion lib/image_processing/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ def call(save: true)
call_processor
elsif destination
handle_destination do
call_processor(destination: destination)
if format && !determine_format(destination)
call_processor(destination: destination, format: format)
else
call_processor(destination: destination)
end
end
else
create_tempfile do |tempfile|
Expand Down
6 changes: 4 additions & 2 deletions lib/image_processing/processor.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module ImageProcessing
# Abstract class inherited by individual processors.
class Processor
def self.call(source:, loader:, operations:, saver:, destination: nil)
def self.call(source:, loader:, operations:, saver:, destination: nil, format: nil)
unless source.is_a?(String) || source.is_a?(self::ACCUMULATOR_CLASS)
fail Error, "invalid source: #{source.inspect}"
end
Expand All @@ -19,7 +19,9 @@ def self.call(source:, loader:, operations:, saver:, destination: nil)
accumulator = apply_operation(accumulator, operation)
end

if destination
if destination && format
save_image(accumulator, destination, format, **saver)
elsif destination
save_image(accumulator, destination, **saver)
else
accumulator
Expand Down
13 changes: 11 additions & 2 deletions lib/image_processing/vips.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,21 @@ def self.supports_resize_on_load?

# Writes the Vips::Image object to disk. This starts the processing
# pipeline defined in the Vips::Image object. Accepts additional
# saver-specific options (e.g. quality).
def self.save_image(image, path, saver: nil, quality: nil, **options)
# saver-specific options (e.g. quality). The saver is chosen by `format`
# when given, otherwise by the path's extension.
def self.save_image(image, path, format = nil, saver: nil, quality: nil, **options)
options[:Q] = quality if quality

if saver
image.public_send(:"#{saver}save", path, **options)
elsif format
saver = ::Vips.vips_foreign_find_save(".#{format}")
fail ::Vips::Error, "No known saver for '#{format}'." unless saver

filename = ::Vips.p2str(::Vips.vips_filename_get_filename(path))
option_string = ::Vips.p2str(::Vips.vips_filename_get_options(path))
options = Utils.select_valid_options(saver, options)
::Vips::Operation.call(saver, [image, filename], options, option_string)
else
options = Utils.select_valid_saver_options(path, options)
image.write_to_file(path, **options)
Expand Down
43 changes: 43 additions & 0 deletions test/mini_magick_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ def with_mini_magick_version(version)
assert_type "PNG", result
end

it "saves in the #convert format to a destination without an extension" do
destination = Tempfile.new("destination")
ImageProcessing::MiniMagick.convert("png").call(@portrait, destination: destination.path)
assert_type "PNG", destination
end

it "refuses a #convert format that ImageMagick would read as more than a format" do
destination = Tempfile.new("destination")
pipeline = ImageProcessing::MiniMagick.convert("png:redirected-")
assert_raises(ImageProcessing::Error) { pipeline.call(@portrait, destination: destination.path) }
end

it "saves in the destination's extension rather than the #convert format" do
destination = Tempfile.new(["destination", ".jpg"])
ImageProcessing::MiniMagick.convert("png").call(@portrait, destination: destination.path)
assert_type "JPEG", destination
end

it "accepts page" do
tiff = Tempfile.new(["file", ".tiff"])
ImageProcessing::MiniMagick.convert_shim do |convert|
Expand Down Expand Up @@ -92,6 +110,17 @@ def with_mini_magick_version(version)
end
end

it "saves in the #convert format to a destination named as an inherited descriptor" do
destination = Tempfile.new("destination")
File.open(destination.path, "wb") do |file|
ImageProcessing::MiniMagick
.loader(inherit_fds: [file])
.convert("png")
.call(@portrait, destination: "/dev/fd/#{file.fileno}")
end
assert_type "PNG", destination
end

it "applies loader, page and geometry to a source named as an inherited descriptor" do
magick = ImageProcessing::MiniMagick
.source("/dev/fd/3")
Expand Down Expand Up @@ -147,6 +176,20 @@ def with_mini_magick_version(version)
end
end

it "disallows split layers in the #convert format to a destination without an extension" do
tiff = Tempfile.new(["file", ".tiff"])
ImageProcessing::MiniMagick.convert_shim do |convert|
convert.merge! [@portrait.path, @portrait.path, @portrait.path]
convert << tiff.path
end
destination = Tempfile.new("destination")

pipeline = ImageProcessing::MiniMagick.source(tiff).convert("jpg")

assert_raises(ImageProcessing::Error) { pipeline.call(destination: destination.path) }
assert_empty Dir["#{destination.path}-*"]
end

it "allows resizing images without extension" do
result1 = Tempfile.new("image") # no file extension

Expand Down
11 changes: 11 additions & 0 deletions test/pipeline_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@
assert_raises(Vips::Error) { pipeline.call(destination: destination_path) }
end

it "calls a processor whose .call predates the format keyword" do
processor = Class.new(ImageProcessing::Vips::Processor) do
def self.call(source:, loader:, operations:, saver:, destination: nil)
super
end
end
destination = Tempfile.new(["destination", ".png"])
ImageProcessing::Vips.branch(processor: processor).convert("jpg").call(@portrait, destination: destination.path)
assert_type "PNG", destination
end

it "accepts loader options" do
pipeline = ImageProcessing::Vips.loader(shrink: 2)
assert_equal Hash[shrink: 2], pipeline.options[:loader]
Expand Down
46 changes: 46 additions & 0 deletions test/vips_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,52 @@
assert_type "PNG", result
end

it "saves in the #convert format to a destination without an extension" do
destination = Tempfile.new("destination")
ImageProcessing::Vips.convert("png").call(@portrait, destination: destination.path)
assert_type "PNG", destination
end

it "saves in the #convert format to a destination named as /dev/fd/N" do
destination = Tempfile.new("destination")
File.open(destination.path, "wb") do |file|
ImageProcessing::Vips.convert("png").call(@portrait, destination: "/dev/fd/#{file.fileno}")
end
assert_type "PNG", destination
end

it "ignores saver options that the #convert format's saver does not define" do
destination = Tempfile.new("destination")
ImageProcessing::Vips.saver(compression: 9).convert("jpg").call(@portrait, destination: destination.path)
assert_type "JPEG", destination
end

it "applies saver options in the #convert format to a destination without an extension" do
low, high = Tempfile.new("low"), Tempfile.new("high")
ImageProcessing::Vips.saver(quality: 10).convert("jpg").call(@portrait, destination: low.path)
ImageProcessing::Vips.saver(quality: 100).convert("jpg").call(@portrait, destination: high.path)
assert File.size(low.path) < File.size(high.path)
end

it "applies saver options named in a destination without an extension" do
low, high = Tempfile.new("low"), Tempfile.new("high")
ImageProcessing::Vips.convert("jpg").call(@portrait, destination: "#{low.path}[Q=10]")
ImageProcessing::Vips.convert("jpg").call(@portrait, destination: "#{high.path}[Q=100]")
assert File.size(low.path) < File.size(high.path)
end

it "prefers :saver to the #convert format" do
destination = Tempfile.new("destination")
ImageProcessing::Vips.saver(saver: :png).convert("jpg").call(@portrait, destination: destination.path)
assert_type "PNG", destination
end

it "saves in the destination's extension rather than the #convert format" do
destination = Tempfile.new(["destination", ".jpg"])
ImageProcessing::Vips.convert("png").call(@portrait, destination: destination.path)
assert_type "JPEG", destination
end

describe ".valid_image?" do
it "returns true for correct images" do
assert ImageProcessing::Vips.valid_image?(@portrait)
Expand Down
Loading