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