From 04709222758e199c8c77654326dc2c97a91e82f9 Mon Sep 17 00:00:00 2001 From: John Cupitt <jcupitt@gmail.com> Date: Tue, 24 May 2022 16:41:30 +0100 Subject: [PATCH] add extended targetcustom support but TIFF write fails for some reason argh --- example/connection.rb | 32 +++++++++++++----- lib/vips/image.rb | 8 +++-- lib/vips/object.rb | 8 +++++ lib/vips/targetcustom.rb | 57 ++++++++++++++++++++++++++++++- spec/connection_spec.rb | 73 +++++++++++++++++++++++++++------------- 5 files changed, 144 insertions(+), 34 deletions(-) diff --git a/example/connection.rb b/example/connection.rb index 1a9c45cf..129ee4aa 100755 --- a/example/connection.rb +++ b/example/connection.rb @@ -1,26 +1,42 @@ #!/usr/bin/ruby require "vips" -require "down/http" +# gem install down +require "down" # byte_source = File.open ARGV[0], "rb" # eg. https://images.unsplash.com/photo-1491933382434-500287f9b54b -byte_source = Down::Http.open(ARGV[0]) +byte_source = Down::open(ARGV[0]) source = Vips::SourceCustom.new source.on_read do |length| - puts "reading #{length} bytes ..." + puts "source: reading #{length} bytes ..." byte_source.read length end source.on_seek do |offset, whence| - puts "seeking to #{offset}, #{whence}" + puts "source: seeking to #{offset}, #{whence}" byte_source.seek(offset, whence) end -byte_target = File.open ARGV[1], "wb" +byte_target = File.open ARGV[1], "w+b" + target = Vips::TargetCustom.new -target.on_write { |chunk| byte_target.write(chunk) } -target.on_finish { byte_target.close } +target.on_write do |chunk| + puts "target: writing #{chunk.length} bytes ..." + byte_target.write(chunk) +end +target.on_read do |length| + puts "target: reading #{length} bytes ..." + byte_target.read length +end +target.on_seek do |offset, whence| + puts "target: seeking to #{offset}, #{whence}" + byte_target.seek(offset, whence) +end +target.on_end do + puts "target: ending" + byte_target.close +end image = Vips::Image.new_from_source source, "", access: :sequential -image.write_to_target target, ".jpg" +image.write_to_target target, ARGV[2] diff --git a/lib/vips/image.rb b/lib/vips/image.rb index bae9c372..03d63e62 100644 --- a/lib/vips/image.rb +++ b/lib/vips/image.rb @@ -548,8 +548,12 @@ def self.new_from_array array, scale = 1, offset = 0 def new_from_image value pixel = (Vips::Image.black(1, 1) + value).cast(format) image = pixel.embed 0, 0, width, height, extend: :copy - image.copy interpretation: interpretation, xres: xres, yres: yres, - xoffset: xoffset, yoffset: yoffset + image.copy \ + interpretation: interpretation, + xres: xres, + yres: yres, + xoffset: xoffset, + yoffset: yoffset end # Write this image to a file. Save options may be encoded in the diff --git a/lib/vips/object.rb b/lib/vips/object.rb index 3b3e92de..860892c9 100644 --- a/lib/vips/object.rb +++ b/lib/vips/object.rb @@ -108,6 +108,13 @@ class Progress < FFI::Struct end end + MARSHAL_END = proc do |handler| + FFI::Function.new(:int, [:pointer, :pointer]) do |i, cb| + # this can't throw an exception, so no catch is necessary + handler.call + end + end + MARSHAL_FINISH = proc do |handler| FFI::Function.new(:void, [:pointer, :pointer]) do |i, cb| # this can't throw an exception, so no catch is necessary @@ -123,6 +130,7 @@ class Progress < FFI::Struct read: MARSHAL_READ, seek: MARSHAL_SEEK, write: MARSHAL_WRITE, + end: MARSHAL_END, finish: MARSHAL_FINISH } diff --git a/lib/vips/targetcustom.rb b/lib/vips/targetcustom.rb index 9f07db75..4372f365 100644 --- a/lib/vips/targetcustom.rb +++ b/lib/vips/targetcustom.rb @@ -66,8 +66,63 @@ def on_write &block end end + # The block is executed to read data from the target. The interface is + # exactly as IO::read, ie. it takes a maximum number of bytes to read and + # returns a string of bytes from the target, or nil if the target is already + # at end of file. + # + # This handler is optional and only needed for image formats which have + # to be able to read their own output, like TIFF. + # + # @yieldparam length [Integer] Read and return up to this many bytes + # @yieldreturn [String] Up to length bytes of data, or nil for EOF + def on_read &block + # target read added in 8.13 + if Vips.at_least_libvips?(8, 13) + signal_connect "read" do |buf, len| + chunk = block.call len + return 0 if chunk.nil? + bytes_read = chunk.bytesize + buf.put_bytes(0, chunk, 0, bytes_read) + chunk.clear + + bytes_read + end + end + end + + # The block is executed to seek the target. The interface is exactly as + # IO::seek, ie. it should take an offset and whence, and return the + # new read position. + # + # This handler is optional and only needed for image formats which have + # to be able to read their own output, like TIFF. + # + # @yieldparam offset [Integer] Seek offset + # @yieldparam whence [Integer] Seek whence + # @yieldreturn [Integer] the new read position, or -1 on error + def on_seek &block + # target seek added in 8.13 + if Vips.at_least_libvips?(8, 13) + signal_connect "seek" do |offset, whence| + block.call offset, whence + end + end + end + # The block is executed at the end of write. It should do any necessary - # finishing action, such as closing a file. + # finishing action, such as closing a file, and return 0 on sucess and -1 + # on error + # + # @yieldreturn [Integer] 0 on sucess, or -1 on error + def on_end &block + signal_name = Vips.at_least_libvips?(8, 13) ? "end" : "finish" + signal_connect signal_name do + block.call + end + end + + # Deprecated name for libvips before 8.13 def on_finish &block signal_connect "finish" do block.call diff --git a/spec/connection_spec.rb b/spec/connection_spec.rb index 6c4ee0df..33bc1d2d 100644 --- a/spec/connection_spec.rb +++ b/spec/connection_spec.rb @@ -36,12 +36,13 @@ it "can load an image from filename source" do source = Vips::Source.new_from_file simg("wagon.jpg") image = Vips::Image.new_from_source source, "" + real = Vips::Image.new_from_file simg("wagon.jpg") expect(image) - expect(image.width).to eq(685) - expect(image.height).to eq(478) - expect(image.bands).to eq(3) - expect(image.avg).to be_within(0.001).of(109.789) + expect(image.width).to eq(real.width) + expect(image.height).to eq(real.height) + expect(image.bands).to eq(real.bands) + expect(image.avg).to eq(real.avg) end end @@ -76,13 +77,14 @@ filename = timg("x4.png") target = Vips::Target.new_to_file filename image.write_to_target target, ".png" + real = Vips::Image.new_from_file simg("wagon.jpg") image = Vips::Image.new_from_file filename expect(image) - expect(image.width).to eq(685) - expect(image.height).to eq(478) - expect(image.bands).to eq(3) - expect(image.avg).to be_within(0.001).of(109.789) + expect(image.width).to eq(real.width) + expect(image.height).to eq(real.height) + expect(image.bands).to eq(real.bands) + expect(image.avg).to eq(real.avg) end it "can save an image to a memory target" do @@ -114,12 +116,13 @@ source.on_read { |length| file.read length } source.on_seek { |offset, whence| file.seek(offset, whence) } image = Vips::Image.new_from_source source, "" + real = Vips::Image.new_from_file simg("wagon.jpg") expect(image) - expect(image.width).to eq(685) - expect(image.height).to eq(478) - expect(image.bands).to eq(3) - expect(image.avg).to be_within(0.001).of(109.789) + expect(image.width).to eq(real.width) + expect(image.height).to eq(real.height) + expect(image.bands).to eq(real.bands) + expect(image.avg).to eq(real.avg) end it "on_seek is optional" do @@ -127,34 +130,58 @@ source = Vips::SourceCustom.new source.on_read { |length| file.read length } image = Vips::Image.new_from_source source, "" + real = Vips::Image.new_from_file simg("wagon.jpg") expect(image) - expect(image.width).to eq(685) - expect(image.height).to eq(478) - expect(image.bands).to eq(3) - expect(image.avg).to be_within(0.001).of(109.789) + expect(image.width).to eq(real.width) + expect(image.height).to eq(real.height) + expect(image.bands).to eq(real.bands) + expect(image.avg).to eq(real.avg) end - it "can create a user output stream" do + it "can create a custom target" do target = Vips::TargetCustom.new expect(target) end - it "can write an image to a user output stream" do + it "can write an image to a custom target" do filename = timg("x5.png") file = File.open filename, "wb" target = Vips::TargetCustom.new target.on_write { |chunk| file.write(chunk) } - target.on_finish { file.close } + target.on_end { file.close } image = Vips::Image.new_from_file simg("wagon.jpg") image.write_to_target target, ".png" image = Vips::Image.new_from_file filename + real = Vips::Image.new_from_file simg("wagon.jpg") expect(image) - expect(image.width).to eq(685) - expect(image.height).to eq(478) - expect(image.bands).to eq(3) - expect(image.avg).to be_within(0.001).of(109.789) + expect(image.width).to eq(real.width) + expect(image.height).to eq(real.height) + expect(image.bands).to eq(real.bands) + expect(image.avg).to eq(real.avg) + end +end + +RSpec.describe Vips::TargetCustom, version: [8, 13] do + it "can write to a custom target as TIFF" do + filename = timg("x6.tif") + file = File.open filename, "w+b" + target = Vips::TargetCustom.new + target.on_write { |chunk| file.write(chunk) } + target.on_read { |length| file.read length } + target.on_seek { |offset, whence| file.seek(offset, whence) } + target.on_end { file.close; 0 } + image = Vips::Image.new_from_file simg("wagon.jpg") + image.write_to_target target, ".tif" + + image = Vips::Image.new_from_file filename + real = Vips::Image.new_from_file simg("wagon.jpg") + expect(image) + expect(image.width).to eq(real.width) + expect(image.height).to eq(real.height) + expect(image.bands).to eq(real.bands) + expect(image.avg).to eq(real.avg) end end