Skip to content

Save in the #convert format to an extensionless destination - #150

Merged
janko merged 1 commit into
janko:masterfrom
flavorjones:convert-format-fd-destination
Sep 29, 2026
Merged

janko merged 1 commit into
janko:masterfrom
flavorjones:convert-format-fd-destination

Conversation

@flavorjones

@flavorjones flavorjones commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Motivation / Background

This is the output side of #147.

HotCell receives the destination as an open file descriptor, but it can't pass /dev/fd/N as destination:, because the savers pick the format from the path's extension. So HotCell saves to a scratch file named output.png and then copies that file into the descriptor (transforming.rb). That is an extra write and read of every output, plus tmpfs space for the scratch file.

ImageProcessing::Vips.source(src).convert("png").call(destination: "/dev/fd/5")
# Vips::Error: VipsForeignSave: "/dev/fd/5" is not a known file format

ImageProcessing::MiniMagick.source(src).loader(inherit_fds: [out]).convert("jpg").call(destination: "/dev/fd/5")
# writes a PNG

Detail

When the destination has no extension, save in the #convert format.

  • vips: pick the saver with Vips.vips_foreign_find_save(".#{format}") and filter options as select_valid_saver_options does. Options in the destination's [...] suffix still apply, as with write_to_file.
  • MiniMagick: write to "#{format}:#{destination}". A format that isn't a bare word (for example, one containing a colon) raises ImageProcessing::Error, because ImageMagick would read the rest as part of the output path.

Destinations with an extension are unchanged, even when the extension disagrees with #convert. Pipelines without #convert are unchanged. .saver(saver:) still takes precedence.

Pipeline passes format: to Processor.call only in the new case, and Processor.call passes it to save_image as a third positional argument only in the new case, so existing custom processors are unaffected elsewhere.

Additional information

When the destination has no extension, ImageProcessing::Vips::Processor.save_image now calls the libvips save operation directly instead of calling Vips::Image#write_to_file. It therefore skips the garbage collection that write_to_file runs after each save (Vips::Image#write_gc, which is private). The existing .saver(saver:) option already calls the save operation directly and skips it too.

.saver(saver:) doesn't solve this problem, because its value is a libvips save operation name rather than a file format, and it disables the filtering of unsupported saver options.

Closes #149

A destination without an extension, such as `/dev/fd/N`, ignored
`#convert`: libvips raised, and ImageMagick wrote the source format.

Save in the `#convert` format when the destination has no extension.
Destinations with an extension are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@janko janko left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you!

@janko
janko merged commit 5827e83 into janko:master Sep 29, 2026
7 checks passed
@flavorjones
flavorjones deleted the convert-format-fd-destination branch September 30, 2026 15:41
flavorjones added a commit to basecamp/hotcell that referenced this pull request Sep 30, 2026
* Require image_processing 2.2.0 and drop the transform rename

image_processing 2.1.0 chose its saver from the destination's
extension, so the transform operations encoded to a suffixed sibling of
the extensionless scratch path and renamed it into place.

Require image_processing 2.2.0, which saves in the `#convert` format
to a destination without an extension, and write to the scratch path
directly.

On the ImageMagick toolchain, a format ImageMagick has no name for,
such as `jfif`, now fails as `unreadable` instead of being written in
the source's format.

ref: janko/image_processing#150
[Fix #4]

* Describe the image_processing 2.2.0 upgrade as an improvement

The CHANGELOG entry described a known edge case that a separate fix
will handle, and did not mention the rename every transform now saves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let #call write to an already-open output in the #convert format

2 participants