Skip to content

Commit

Permalink
Add image loading from an InputStream, and image writing to an Output…
Browse files Browse the repository at this point in the history
…Stream
  • Loading branch information
lopcode committed Oct 7, 2024
1 parent 10c9507 commit 5f91581
Show file tree
Hide file tree
Showing 64 changed files with 4,861 additions and 334 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ repositories {
}

dependencies {
implementation("app.photofox.vips-ffm:vips-ffm-core:1.1.1")
implementation("app.photofox.vips-ffm:vips-ffm-core:1.2.0")
}
```
When running your project you must add `--enable-native-access=ALL-UNNAMED` to your JVM runtime arguments. If you
Expand Down Expand Up @@ -80,7 +80,7 @@ Vips.run { arena ->
val sourceImage = VImage.newFromFile(
arena,
"sample/src/main/resources/sample_images/rabbit.jpg",
VipsOption.Enum("access", VipsAccess.ACCESS_SEQUENTIAL)
VipsOption.Enum("access", VipsAccess.ACCESS_SEQUENTIAL) // example of an option
)
val sourceWidth = sourceImage.width
val sourceHeight = sourceImage.height
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/app/photofox/vipsffm/VBlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import static app.photofox.vipsffm.jextract.VipsRaw.C_LONG;

/**
* Represents a VipsBlob, boxed to avoid exposing its raw MemorySegment
* Represents a VipsBlob, which is backed by a contiguous area of off-heap memory
* Its constructor is package private to prevent leaking MemorySegments in to the vips-ffm API
* Use its static helper methods to create new blobs
*/
public final class VBlob {

Expand Down
108 changes: 108 additions & 0 deletions core/src/main/java/app/photofox/vipsffm/VCustomSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package app.photofox.vipsffm;

import app.photofox.vipsffm.jextract.CustomStreamReadCallback;
import app.photofox.vipsffm.jextract.CustomStreamSeekCallback;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;

/**
* Models a libvips "custom streaming" source
* Provides callbacks for read and seek operations
* See <a href="https://www.libvips.org/2019/11/29/True-streaming-for-libvips.html">true streaming for libvips</a>
*/
public final class VCustomSource extends VSource {

@FunctionalInterface
public interface ReadCallback {

long read(MemorySegment dataPointer, long length);
}

@FunctionalInterface
public interface SeekCallback {

long seek(int whence);
}

private final VCustomSource.ReadCallback readCallback;
private final VCustomSource.SeekCallback seekCallback;

public VCustomSource(
Arena arena,
VCustomSource.ReadCallback readCallback,
VCustomSource.SeekCallback seekCallback
) throws VipsError {
super(arena, VipsHelper.source_custom_new(arena));
this.readCallback = readCallback;
this.seekCallback = seekCallback;

attachReadSignal(arena, this);
if (seekCallback != null) {
attachSeekSignal(arena, this, seekCallback);
}
}

public VCustomSource(
Arena arena,
VCustomSource.ReadCallback readCallback
) throws VipsError {
this(arena, readCallback, null);
}

private void attachReadSignal(Arena arena, VSource source) {
var callback = new CustomStreamReadCallback.Function() {

@Override
public long apply(
MemorySegment source,
MemorySegment data,
long length,
MemorySegment handle
) {
return readCallback.read(data, length);
}
};
var callbackPointer = CustomStreamReadCallback.allocate(callback, arena);
var result = VipsHelper.g_signal_connect_data(
arena,
source.address,
"read",
callbackPointer,
MemorySegment.NULL,
MemorySegment.NULL,
0
);
if (result <= 0) {
throw new VipsError("failed to create read signal");
}
}

private void attachSeekSignal(Arena arena, VSource source, SeekCallback seekCallback) {
var callback = new CustomStreamSeekCallback.Function() {

@Override
public long apply(
MemorySegment source,
MemorySegment data,
int whence,
MemorySegment handle
) {
return seekCallback.seek(whence);
}
};
var callbackPointer = CustomStreamSeekCallback.allocate(callback, arena);
var result = VipsHelper.g_signal_connect_data(
arena,
source.address,
"seek",
callbackPointer,
MemorySegment.NULL,
MemorySegment.NULL,
0
);
if (result <= 0) {
throw new VipsError("failed to create seek signal");
}
}
}
98 changes: 98 additions & 0 deletions core/src/main/java/app/photofox/vipsffm/VCustomTarget.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package app.photofox.vipsffm;

import app.photofox.vipsffm.jextract.CustomStreamEndCallback;
import app.photofox.vipsffm.jextract.CustomStreamWriteCallback;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;

/**
* Models a libvips "custom streaming" target
* Provides callbacks for write and end operations
* See <a href="https://www.libvips.org/2019/11/29/True-streaming-for-libvips.html">true streaming for libvips</a>
*/
public final class VCustomTarget extends VTarget {

@FunctionalInterface
public interface WriteCallback {

long write(MemorySegment dataPointer);
}

@FunctionalInterface
public interface EndCallback {

int end();
}

private final WriteCallback writeCallback;
private final EndCallback endCallback;

public VCustomTarget(
Arena arena,
WriteCallback writeCallback,
EndCallback endCallback
) throws VipsError {
super(arena, VipsHelper.target_custom_new(arena));
this.writeCallback = writeCallback;
this.endCallback = endCallback;

attachWriteSignal(arena, this);
attachEndSignal(arena, this);
}

private void attachWriteSignal(Arena arena, VTarget target) {
var callback = new CustomStreamWriteCallback.Function() {

@Override
public long apply(
MemorySegment source,
MemorySegment data,
long length,
MemorySegment handle
) {
var segment = data.asSlice(0, length);
return writeCallback.write(segment);
}
};
var callbackPointer = CustomStreamWriteCallback.allocate(callback, arena);
var result = VipsHelper.g_signal_connect_data(
arena,
target.address,
"write",
callbackPointer,
MemorySegment.NULL,
MemorySegment.NULL,
0
);
if (result <= 0) {
throw new VipsError("failed to create write signal");
}
}

private void attachEndSignal(Arena arena, VTarget target) {
var callback = new CustomStreamEndCallback.Function() {

@Override
public int apply(
MemorySegment source,
MemorySegment handle
) {
return endCallback.end();
}
};
var callbackPointer = CustomStreamEndCallback.allocate(callback, arena);
var result = VipsHelper.g_signal_connect_data(
arena,
target.address,
"end",
callbackPointer,
MemorySegment.NULL,
MemorySegment.NULL,
0
);
if (result <= 0) {
throw new VipsError("failed to create end signal");
}
}
}
37 changes: 37 additions & 0 deletions core/src/main/java/app/photofox/vipsffm/VImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import app.photofox.vipsffm.enums.VipsOperationMorphology;
import app.photofox.vipsffm.enums.VipsOperationRelational;
import app.photofox.vipsffm.enums.VipsOperationRound;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Deprecated;
import java.lang.Double;
import java.lang.Integer;
Expand Down Expand Up @@ -10381,6 +10383,28 @@ public static VImage newFromBytes(Arena arena, byte[] bytes, VipsOption... optio
return newFromSource(arena, source, options);
}

/**
* Creates a new VImage from an {@link InputStream}. This uses libvips' "custom streaming" feature and is
* therefore quite efficient, avoiding the need to make extra full copies of the image's data.
* You could, for example, use this function to create an image directly from an API call, thumbnail it,
* and then upload directly to an S3-compatible API efficiently in memory - all without creating a local
* file.
*/
public static VImage newFromStream(Arena arena, InputStream stream, String optionString,
VipsOption... options) throws VipsError {
var source = VSource.newFromInputStream(arena, stream);
return newFromSource(arena, source, optionString, options);
}

/**
* See {@link VImage#newFromStream(Arena, InputStream, String, VipsOption...)}
*/
public static VImage newFromStream(Arena arena, InputStream stream, VipsOption... options) throws
VipsError {
var source = VSource.newFromInputStream(arena, stream);
return newFromSource(arena, source, options);
}

public void writeToFile(String path, VipsOption... options) throws VipsError {
var filename = VipsHelper.filename_get_filename(arena, path);
var filenameOptions = VipsHelper.filename_get_options(arena, filename);
Expand Down Expand Up @@ -10408,6 +10432,19 @@ public void writeToTarget(VTarget target, String suffix, VipsOption... options)
VipsInvoker.invokeOperation(arena, loader, callArgs);
}

/**
* Writes this VImage to an {@link OutputStream}. This uses libvips' "custom streaming" feature and is
* therefore quite efficient, avoiding the need to make extra full copies of the image's data.
* You could, for example, use this function to create an image directly from an API call, thumbnail it,
* and then upload directly to an S3-compatible API efficiently in memory - all without creating a local
* file.
*/
public void writeToStream(OutputStream stream, String suffix, VipsOption... options) throws
VipsError {
var target = VTarget.newFromOutputStream(arena, stream);
this.writeToTarget(target, suffix, options);
}

public static VImage newImage(Arena arena) throws VipsError {
var newImagePointer = VipsHelper.image_new(arena);
return new VImage(arena, newImagePointer);
Expand Down
56 changes: 49 additions & 7 deletions core/src/main/java/app/photofox/vipsffm/VSource.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package app.photofox.vipsffm;

import java.io.IOException;
import java.io.InputStream;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;

/**
* Represents a VipsSource, boxed to avoid exposing its raw MemorySegment
* Represents a VipsSource
* Its constructor is package private to prevent leaking MemorySegments in to the vips-ffm API
* Use its static helper methods to create new sources
*/
public final class VSource {
public sealed class VSource permits VCustomSource {

final Arena arena;
final MemorySegment address;

VSource(MemorySegment address) throws VipsError {
VSource(Arena arena, MemorySegment address) throws VipsError {
if (!VipsValidation.isValidPointer(address)) {
throw new VipsError("invalid pointer used for creation");
}
this.arena = arena;
this.address = address;
}

Expand Down Expand Up @@ -56,23 +62,23 @@ public MemorySegment getUnsafeStructAddress() {
*/
public static VSource newFromDescriptor(Arena arena, int descriptor) throws VipsError {
var pointer = VipsHelper.source_new_from_descriptor(arena, descriptor);
return new VSource(pointer);
return new VSource(arena, pointer);
}

/**
* Create a new VSource from a file path
*/
public static VSource newFromFile(Arena arena, String filename) throws VipsError {
var pointer = VipsHelper.source_new_from_file(arena, filename);
return new VSource(pointer);
return new VSource(arena, pointer);
}

/**
* Create a new VSource from a VBlob, usually received from a Vips operation
*/
public static VSource newFromBlob(Arena arena, VBlob blob) throws VipsError {
var pointer = VipsHelper.source_new_from_blob(arena, blob.address);
return new VSource(pointer);
return new VSource(arena, pointer);
}

/**
Expand All @@ -87,6 +93,42 @@ public static VSource newFromBytes(Arena arena, byte[] bytes) throws VipsError {

public static VSource newFromOptions(Arena arena, String options) throws VipsError {
var pointer = VipsHelper.source_new_from_options(arena, options);
return new VSource(pointer);
return new VSource(arena, pointer);
}

/**
* Creates a new VSource from a Java {@link InputStream}
* The provided InputStream is coupled to the arena's lifetime, and closed when its scope ends
* Note that you can read an image directly from an InputStream using {@link VImage#newFromStream(Arena, InputStream, VipsOption...)}
* This stream does not support seeking, because InputStream does not support it, so cannot be maximally
* efficient - but it is still likely more efficient than taking a full intermediate copy of bytes
*/
public static VSource newFromInputStream(Arena arena, InputStream stream) throws VipsError {
VCustomSource.ReadCallback readCallback = (dataPointer, length) -> {
if (length < 0) {
throw new VipsError("invalid length to read provided: " + length);
}
// bytebuffer only supports reading int max bytes at a time
var clippedLength = (int) Math.min(length, Integer.MAX_VALUE);
byte[] bytes;
try {
bytes = stream.readNBytes(clippedLength);
} catch (IOException e) {
throw new VipsError("failed to read bytes from stream", e);
}
var buffer = dataPointer.asSlice(0, clippedLength).asByteBuffer();
buffer.put(bytes);
return bytes.length;
};
var source = new VCustomSource(arena, readCallback);
// attempt to close stream when arena scope ends, in case users have not already done so
source.address.reinterpret(arena, (_) -> {
try {
stream.close();
} catch (IOException e) {
// deliberately ignored
}
});
return source;
}
}
Loading

0 comments on commit 5f91581

Please sign in to comment.