Skip to content

tell buffer and target savers the file format #2499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Oct 28, 2021
Merged

Conversation

jcupitt
Copy link
Member

@jcupitt jcupitt commented Oct 26, 2021

Currently, buffer and target savers are not told the format they should
write.

This is usually OK (the JPEG saver already knows it should write JPEG),
but some savers can write several formats, and these currently need an
extra parameter.

For example:

buf = x.write_to_buffer ".bmp", format: "bmp"

The first ".bmp" gets libvips to pick magicksave, the second
format: param is necessary to tell magicksave to write BMP.

This patch makes vips_image_write_to_buffer and
vips_image_write_to_target add an extra image metadata item called
format_string which contains the string used to pick the write format.

Savers can use this extra field (if present) to pick a default save
format. In this case, the magick saver will extract the "bmp".

See: libvips/ruby-vips#319

Currently, buffer and target savers are not told the format they should
write.

This is usually OK (the JPEG saver already knows it should write JPEG),
but some savers can write several formats, and these currently need an
extra parameter.

For example:

```ruby
buf = x.write_to_buffer ".bmp", format: "bmp"
```

The first ".bmp" gets libvips to pick magicksave, the second
`format:` param is necessary to tell magicksave to write BMP.

This patch makes `vips_image_write_to_buffer` and
`vips_image_write_to_target` add an extra image metadata item called
`format_string` which contains the string used to pick the write format.

Savers can use this extra field (if present) to pick a default save
format. In this case, the magick saver will extract the "bmp".

See: libvips/ruby-vips#319
@jcupitt jcupitt added this to the 8.12 milestone Oct 26, 2021
@jcupitt
Copy link
Member Author

jcupitt commented Oct 26, 2021

ruby-vips needs a small change:

diff --git a/lib/vips/image.rb b/lib/vips/image.rb
index 1b800d1..6e0be6b 100644
--- a/lib/vips/image.rb
+++ b/lib/vips/image.rb
@@ -626,7 +626,12 @@ module Vips
       saver = Vips.vips_foreign_find_save_buffer filename
       raise Vips::Error if saver.nil?
 
-      buffer = Vips::Operation.call saver, [self], opts, option_string
+      # note the format_string for savers
+      image = self.mutate do |x|
+        x.set_type! Vips::REFSTR_TYPE, "format_string", format_string
+      end
+
+      buffer = Vips::Operation.call saver, [image], opts, option_string
       raise Vips::Error if buffer.nil?
 
       write_gc

All language bindings (eg. net-vips, pyvips) will need something similar.

@jcupitt
Copy link
Member Author

jcupitt commented Oct 26, 2021

Test program to show the issue:

require 'vips'

x = Vips::Image.new_from_file ARGV[0]

%w(dz hdr jpc jpt jp2 j2c j2k webp).each do |format|
  begin
    buf = x.write_to_buffer ".#{format}"
    puts "#{format}: written #{buf.length} bytes"
  rescue Vips::Error
    puts "#{format}: buffer write not supported"
  end
end

# imagemagick-based savers need to be told the format to write twice :(
%w(bmp gif).each do |format|
  begin
    buf = x.write_to_buffer ".#{format}", format: format
    puts "#{format}: written #{buf.length} bytes"
  rescue Vips::Error
    puts "#{format}: buffer write not supported"
  end
end

# libheif buffer savers need to be told the format too
buf = x.write_to_buffer ".heif", compression: "hevc"
puts "heic: written #{buf.length} bytes"

buf = x.write_to_buffer ".heif", compression: "av1"
puts "avif: written #{buf.length} bytes"

Rationale

I tried a couple of other fixes, hoping to find some way to pass the format down to the saver without needing changes to bindings, but failed :( The closest was getting vips_filename_get_options() to paste in an extra argument called perhaps format containing the suffix of the filename part, but it seemed very fragile and ugly.

This needs changes to bindings, but the changes are simple and safe, and will be forwards and backwards compatible. And there's no need for extra secret args to the save operations.

TODO

  • heifsave needs this adding
  • ppmsave needs this too (eg. write to ".pfm" to save as float)
  • jp2ksave might need it, I'm unsure
  • pyvips, ruby-vips, the C++ binding and I guess netvips need this change

@jcupitt jcupitt changed the title tell buffer and target savers the file format WIP: tell buffer and target savers the file format Oct 26, 2021
@jcupitt jcupitt marked this pull request as draft October 26, 2021 17:14
@jcupitt
Copy link
Member Author

jcupitt commented Oct 26, 2021

Any thoughts, @kleisauke? This would affect net-vips.

jcupitt added a commit to libvips/ruby-vips that referenced this pull request Oct 27, 2021
jcupitt added a commit to libvips/pyvips that referenced this pull request Oct 27, 2021
and remove gif from the list of things that magick supports (since we
have our own saver now)
@jcupitt
Copy link
Member Author

jcupitt commented Oct 27, 2021

Revised test prog:

#!/usr/bin/ruby

require 'vips'

x = Vips::Image.new_from_file ARGV[0]

# don't try bmp ... we don't have magicksave_target
%w(heif avif ppm pfm pgm).each do |format|
  target = Vips::Target.new_to_memory
  x.write_to_target target, ".#{format}"
  puts "#{format}: written #{target.get("blob").length} bytes"
end

%w(bmp heif avif ppm pfm pgm).each do |format|
  buf = x.write_to_buffer ".#{format}"
  puts "#{format}: written #{buf.length} bytes"
end

With all the changes I see:

$ ./anita.rb ~/pics/k2.jpg
heif: written 209233 bytes
avif: written 254465 bytes
ppm: written 8908859 bytes
pfm: written 35635258 bytes
pgm: written 2969659 bytes
bmp: written 8913034 bytes
heif: written 209233 bytes
avif: written 254465 bytes
ppm: written 8908859 bytes
pfm: written 35635258 bytes
pgm: written 2969659 bytes

So it's correctly picking the target format from the suffix.

@jcupitt
Copy link
Member Author

jcupitt commented Oct 27, 2021

Same for python:

#!/usr/bin/python3

import sys
import pyvips

x = pyvips.Image.new_from_file(sys.argv[1])

for format in ["heif", "avif", "ppm", "pfm", "pgm"]: 
    target = pyvips.Target.new_to_memory()
    x.write_to_target(target, f".{format}")
    print(f"{format}: written {len(target.get('blob'))} bytes")

for format in ["bmp", "heif", "avif", "ppm", "pfm", "pgm"]: 
    buf = x.write_to_buffer(f".{format}")
    print(f"{format}: written {len(buf)} bytes")

Giving:

$ ./anita.py ~/pics/k2.jpg
heif: written 209233 bytes
avif: written 254465 bytes
ppm: written 8908859 bytes
pfm: written 35635258 bytes
pgm: written 2969659 bytes
bmp: written 8913034 bytes
heif: written 209233 bytes
avif: written 254465 bytes
ppm: written 8908859 bytes
pfm: written 35635258 bytes
pgm: written 2969659 bytes

since we use kebab-case elsewhere for image metadata
@jcupitt
Copy link
Member Author

jcupitt commented Oct 27, 2021

OK, all done!

I switched it to format-string for the metadata item, since we use hyphen rather than underscore elsewhere for image metadata.

@jcupitt jcupitt changed the title WIP: tell buffer and target savers the file format tell buffer and target savers the file format Oct 27, 2021
@kleisauke
Copy link
Member

The closest was getting vips_filename_get_options() to paste in an extra argument called perhaps format containing the suffix of the filename part, but it seemed very fragile and ugly.

FWIW, I managed to get this working with commit kleisauke@921922b. This would result in fewer checks within savers, and you can still write (for example):

$ vips magicksave x.jpg x.bmp --format gif
$ head -c 6 x.bmp
GIF89a

But indeed, it's a bit ugly since this option would also be present in all savers and loaders:

$ vips gifload | grep format
   format       - Format hint, input gchararray

I also thought about splitting these deviating savers into aliased GObject classes, for example:

vips_foreign_find_save( "x.avif" ) -> VipsForeignSaveAvifFile -> VipsForeignSaveHeifFile (@compression av1)
vips_foreign_find_save( "x.bmp" ) -> VipsForeignSaveBmpFile -> VipsForeignSaveMagickFile (@format bmp)
vips_foreign_find_save( "x.pgm" ) -> VipsForeignSavePgmFile -> VipsForeignSavePpmFile (@format pgm)
vips_foreign_find_save( "x.pbm" ) -> same as line above
vips_foreign_find_save( "x.pfm" ) -> VipsForeignSavePfmFile -> VipsForeignSavePpmFile (@format pfm)

But I'm not sure if this can be done without too much hassle.

@kleisauke
Copy link
Member

But I'm not sure if this can be done without too much hassle.

This can be done easily, see for example commit kleisauke@d415a9b. With that I see:

$ vips copy x.jpg x.avif
$ vips copy x.jpg x.heic
$ vipsheader x.avif -f heif-compression
av1
$ vipsheader x.heic -f heif-compression
hevc

I marked that subclass as "deprecated" to hide it from vips -l foreign | grep -i save | awk '{ print $1; }'.

@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

I also thought about splitting these deviating savers into aliased GObject classes, for example:

Oh, that's a good idea. Though it would be very wordy (as you say).

I also thought of hiding the extra info inside the VipsTarget, but that would need changes to bindings as well, I think.

Something else I tried was changing vips_object_set_from_string() to take the full format spec, not just a list of options. It could check the object and see if it supported a format argument, then set that from the suffix. This would avoid the fragile trick of appending the format to the option list.

It would still need changes to all the bindings though :( And the binding changes wouldn't work with older libvipses, so bindings would need to see which libvips version they had and alter behaviour.

@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

This can be done easily, see for example commit kleisauke/libvips@d415a9b. With that I see:

Oh nice! We'd need to make these I guess:

VipsForeignSaveAvifBuffer
VipsForeignSaveAvifTarget
VipsForeignSaveMagickBmpBuffer
VipsForeignSavePgmBuffer
VipsForeignSavePgmTarget
VipsForeignSavePfmBuffer
VipsForeignSavePfmTarget

So 7 new subclasses.

We could possibly skip the Buffer versions (though not for magick), so we'd only need four new subclasses. The C vips_image_write_to_buffer() tries the target API first, and only checks the buffer API if there's no target writer.

pyvips and ruby-vips don't yet have this logic, but they probably should.

@kleisauke
Copy link
Member

kleisauke commented Oct 28, 2021

Commit kleisauke@9b33fb9 adds the remaining subclasses. There are now a total of new 5 saver subclasses:

VipsForeignSaveAvifTarget
VipsForeignSaveMagickBmpFile
VipsForeignSaveMagickBmpBuffer
VipsForeignSavePgmTarget
VipsForeignSavePfmTarget

vips_image_write_to_file() will also try the target API first, so we don't have to subclass the file versions (I think). Though, the bindings should probably also add this logic (as you say).

I've tested this with:

$ vips copy x.jpg x.avif
$ vips copy x.jpg x.heic
$ vips copy x.jpg x.ppm
$ vips copy x.jpg x.pgm
$ vips copy x.jpg x.pbm
$ vips copy x.jpg x.pfm
$ vips copy x.jpg x.bmp
$ vipsheader x.avif -f heif-compression
av1
$ vipsheader x.heic -f heif-compression
hevc
$ head -c 2 x.ppm
P6
$ head -c 2 x.pgm
P5
$ head -c 2 x.pbm
P5
$ head -c 2 x.pfm
PF
$ head -c 2 x.bmp
BM

@kleisauke
Copy link
Member

Oh, CI is currently failing due to:

pyvips.error.Error: unable to make temp file
  VipsForeignSave: "/tmp/vips-33-1024954283.avif" is not a known file format

To ensure that these subclasses are also skipped in `cplusplus/gen-operators.py`.
let's get pyvips etc. to use the target API instead
@kleisauke
Copy link
Member

Great! You might also be interested in these 2 commits:
kleisauke@5f6b13f
kleisauke@8508761

@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

Sure, they look good.

kleisauke added a commit to kleisauke/net-vips that referenced this pull request Oct 28, 2021
@kleisauke
Copy link
Member

kleisauke commented Oct 28, 2021

Commit kleisauke/net-vips@f0b7ab9 implements the "try to save with the target API first" approach in net-vips, this seems to work for me.

While doing that, I noticed that sometimes the vips_error_freeze() / vips_error_thaw() combination is used and sometimes not, see for example:

vips_error_freeze();
operation_name = vips_foreign_find_load_source( source );
vips_error_thaw();

vips_error_freeze();
operation_name = vips_foreign_find_save_target( filename );
vips_error_thaw();
if( operation_name ) {

if( (operation_name = vips_foreign_find_save_target( filename )) ) {

Perhaps this mechanism for hiding errors can be removed?

and fix up heif a bit too
in vips_image_write_to_buffer()
@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

You're right that third one needs a freeze/thaw as well.

Freeze/thaw is ugly and stupid :( It would be better if vips_foreign_find_save_target() didn't log an error and worked more like a tester. But that would break API compat :(

A cleaner solution would be to make eg. vips_foreign_lookup_save_target(), make vips_foreign_find_save_target() just call that and set the error, and also tag it as deprecated.

jcupitt added a commit to libvips/ruby-vips that referenced this pull request Oct 28, 2021
write_to_buffer will now attempt to use the new Target API before
falling back to the old buffer system. More savers support target than
buffer now.

See libvips/libvips#2499
@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

https://github.com/libvips/ruby-vips/tree/revise-buffer-save4 adds target-first to ruby-vips. I'll have a look at py as well.

@kleisauke
Copy link
Member

Nice! I mirrored some of that ruby-vips code with commit kleisauke/net-vips@b7d1fd6 in net-vips.

I think we are almost done, the only missing piece is that ppmsave currently misses this logic in vips_foreign_save_ppm_file_build:

	if( vips_iscasepostfix( file->filename, ".pbm" ) ||
		vips_iscasepostfix( file->filename, ".pgm" ) )
		VIPS_SETSTR( ppm->format, "pgm" );
	else if( vips_iscasepostfix( file->filename, ".pfm" ) )
		VIPS_SETSTR( ppm->format, "pfm" );

(Similar to the logic in heifsave)

jcupitt added a commit to libvips/pyvips that referenced this pull request Oct 28, 2021
@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

OK, py done as well!

@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

Oh you're right. Let's add an enum, all these string suffixes are a bit clunky.

and add a @Format param to ppmsave
@jcupitt
Copy link
Member Author

jcupitt commented Oct 28, 2021

It now supports PBM (bitmap format) too!

We'll need to regenerate the static parts of the bindings, and I guess eg. php-vips needs some work too, but that can all wait.

This is a nice API cleanup! And good job on the stub-subclass idea, it's much neater.

OK to merge?

Copy link
Member

@kleisauke kleisauke left a comment

Choose a reason for hiding this comment

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

I left a small comment regarding the .pbm file extension, but otherwise this looks good to me!

Copy link
Member

@kleisauke kleisauke left a comment

Choose a reason for hiding this comment

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

Oh, I just saw some potentially redundant code too.

jcupitt and others added 2 commits October 28, 2021 18:33
Co-authored-by: Kleis Auke Wolthuizen <github@kleisauke.nl>
@jcupitt jcupitt marked this pull request as ready for review October 28, 2021 17:55
@jcupitt jcupitt merged commit 47383b5 into master Oct 28, 2021
@jcupitt jcupitt deleted the revise-buffer-save3 branch October 28, 2021 17:57
kleisauke added a commit to kleisauke/libvips that referenced this pull request Nov 2, 2021
jcupitt pushed a commit that referenced this pull request Nov 2, 2021
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.

2 participants