From 20fbb2c9bda30010ff43f0eb0ba46e1cc63e7d8e Mon Sep 17 00:00:00 2001 From: carrot <149851+lopcode@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:04:14 +0100 Subject: [PATCH] Add a way to make a VBlob from some bytes (#104) --- README.md | 2 +- .../main/java/app/photofox/vipsffm/VBlob.java | 20 +++++++++- .../java/app/photofox/vipsffm/VImage.java | 23 +++++++++++ .../java/app/photofox/vipsffm/VSource.java | 19 ++++++++++ .../main/kotlin/vipsffm/GenerateVClasses.kt | 38 +++++++++++++++++++ .../src/main/kotlin/vipsffm/SampleHelper.kt | 7 ++++ .../src/main/kotlin/vipsffm/SampleRunner.kt | 4 +- .../kotlin/vipsffm/VTargetMemorySample.kt | 4 +- .../vipsffm/sample/VBlobByteBufferSample.kt | 2 +- .../vipsffm/sample/VImageFromBytesSample.kt | 29 ++++++++++++++ .../vipsffm/sample/VSourceTargetSample.kt | 2 +- 11 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 sample/src/main/kotlin/vipsffm/sample/VImageFromBytesSample.kt diff --git a/README.md b/README.md index 4329dd9..122c0fa 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ repositories { } dependencies { - implementation("app.photofox.vips-ffm:vips-ffm-core:1.0.0") + implementation("app.photofox.vips-ffm:vips-ffm-core:1.1.0") } ``` When running your project you must add `--enable-native-access=ALL-UNNAMED` to your JVM runtime arguments. If you diff --git a/core/src/main/java/app/photofox/vipsffm/VBlob.java b/core/src/main/java/app/photofox/vipsffm/VBlob.java index b5b2049..0a26f6b 100644 --- a/core/src/main/java/app/photofox/vipsffm/VBlob.java +++ b/core/src/main/java/app/photofox/vipsffm/VBlob.java @@ -5,6 +5,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import java.nio.ByteBuffer; import static app.photofox.vipsffm.jextract.VipsRaw.C_LONG; @@ -28,6 +29,23 @@ public final class VBlob { this.address = address; } + /** + * Creates a new VBlob from a fixed array of bytes + * This must copy the data - it's generally more efficient to use {@link VImage#newFromFile(Arena, String, VipsOption...)}, + * {@link VImage#newFromSource(Arena, VSource, String, VipsOption...)}, and friends + * @param arena An arena constraining the lifetime of this blob + * @param bytes The bytes to wrap + */ + public static VBlob newFromBytes(Arena arena, byte[] bytes) throws VipsError { + var offHeapSegment = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes); + var blobAddress = VipsRaw.vips_blob_new(MemorySegment.NULL, offHeapSegment, offHeapSegment.byteSize()); + if (!VipsValidation.isValidPointer(blobAddress)) { + throw new VipsError("invalid blob returned from libvips"); + } + blobAddress.reinterpret(arena, VipsRaw::vips_area_unref); + return new VBlob(arena, blobAddress); + } + /** * @deprecated Replaced by {@link #getUnsafeStructAddress()} */ @@ -103,7 +121,7 @@ public ByteBuffer asClonedByteBuffer() { } buffer.rewind(); var newBuffer = ByteBuffer.allocate(buffer.capacity()); - buffer.put(newBuffer); + newBuffer.put(buffer); newBuffer.rewind(); return newBuffer; } diff --git a/core/src/main/java/app/photofox/vipsffm/VImage.java b/core/src/main/java/app/photofox/vipsffm/VImage.java index 3bfa647..1dab1ed 100644 --- a/core/src/main/java/app/photofox/vipsffm/VImage.java +++ b/core/src/main/java/app/photofox/vipsffm/VImage.java @@ -10358,6 +10358,29 @@ public static VImage newFromSource(Arena arena, VSource source, String optionStr return outOption.valueOrThrow(); } + public static VImage newFromSource(Arena arena, VSource source, VipsOption... options) throws + VipsError { + return newFromSource(arena, source, "", options); + } + + /** + * Creates a new VImage from raw bytes. Note that this is quite inefficient, use {@link VImage#newFromFile(Arena, String, VipsOption...)} and friends instead. + */ + public static VImage newFromBytes(Arena arena, byte[] bytes, String optionString, + VipsOption... options) throws VipsError { + var source = VSource.newFromBytes(arena, bytes); + return newFromSource(arena, source, optionString, options); + } + + /** + * See {@link VImage#newFromBytes(Arena, byte[], String, VipsOption...)} + */ + public static VImage newFromBytes(Arena arena, byte[] bytes, VipsOption... options) throws + VipsError { + var source = VSource.newFromBytes(arena, bytes); + 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); diff --git a/core/src/main/java/app/photofox/vipsffm/VSource.java b/core/src/main/java/app/photofox/vipsffm/VSource.java index 39cddda..5d00e81 100644 --- a/core/src/main/java/app/photofox/vipsffm/VSource.java +++ b/core/src/main/java/app/photofox/vipsffm/VSource.java @@ -51,21 +51,40 @@ public MemorySegment getUnsafeStructAddress() { return this.address; } + /** + * Create a new VSource from a file descriptor + */ public static VSource newFromDescriptor(Arena arena, int descriptor) throws VipsError { var pointer = VipsHelper.source_new_from_descriptor(arena, descriptor); return new VSource(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); } + /** + * 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); } + /** + * Create a new VSource directly from some bytes + * Note that this makes a full copy of the data, which is inefficient - prefer {@link VImage#newFromFile(Arena, String, VipsOption...)} + * and friends + */ + public static VSource newFromBytes(Arena arena, byte[] bytes) throws VipsError { + var blob = VBlob.newFromBytes(arena, bytes); + return newFromBlob(arena, blob); + } + public static VSource newFromOptions(Arena arena, String options) throws VipsError { var pointer = VipsHelper.source_new_from_options(arena, options); return new VSource(pointer); diff --git a/generator/src/main/kotlin/vipsffm/GenerateVClasses.kt b/generator/src/main/kotlin/vipsffm/GenerateVClasses.kt index 5d46696..9951c41 100644 --- a/generator/src/main/kotlin/vipsffm/GenerateVClasses.kt +++ b/generator/src/main/kotlin/vipsffm/GenerateVClasses.kt @@ -564,6 +564,41 @@ object GenerateVClasses { .addStatement("\$T.invokeOperation(arena, loader, optionString, callArgs)", vipsInvokerType) .addStatement("return outOption.valueOrThrow()") .build() + val newFromSourceNoOptionsMethod = MethodSpec.methodBuilder("newFromSource") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(arenaType, "arena") + .addParameter(vsourceType, "source") + .addParameter(vipsOptionVarargType, "options") + .returns(vimageType) + .varargs(true) + .addException(vipsErrorType) + .addStatement("return newFromSource(arena, source, \"\", options)") + .build() + val newFromBytesMethod = MethodSpec.methodBuilder("newFromBytes") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(arenaType, "arena") + .addParameter(ArrayTypeName.of(TypeName.BYTE), "bytes") + .addParameter(stringType, "optionString") + .addParameter(vipsOptionVarargType, "options") + .returns(vimageType) + .varargs(true) + .addException(vipsErrorType) + .addStatement("var source = \$T.newFromBytes(arena, bytes)", vsourceType) + .addStatement("return newFromSource(arena, source, optionString, options)", vipsOptionSourceType) + .addJavadoc("Creates a new VImage from raw bytes. Note that this is quite inefficient, use {@link VImage#newFromFile(Arena, String, VipsOption...)} and friends instead.") + .build() + val newFromBytesNoOptionsMethod = MethodSpec.methodBuilder("newFromBytes") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(arenaType, "arena") + .addParameter(ArrayTypeName.of(TypeName.BYTE), "bytes") + .addParameter(vipsOptionVarargType, "options") + .returns(vimageType) + .varargs(true) + .addException(vipsErrorType) + .addStatement("var source = \$T.newFromBytes(arena, bytes)", vsourceType) + .addStatement("return newFromSource(arena, source, options)", vipsOptionSourceType) + .addJavadoc("See {@link VImage#newFromBytes(Arena, byte[], String, VipsOption...)}") + .build() val writeToFileMethod = MethodSpec.methodBuilder("writeToFile") .addModifiers(Modifier.PUBLIC) .addParameter(stringType, "path") @@ -617,6 +652,9 @@ object GenerateVClasses { alphaMethod, newFromFileMethod, newFromSourceMethod, + newFromSourceNoOptionsMethod, + newFromBytesMethod, + newFromBytesNoOptionsMethod, writeToFileMethod, writeToImageMethod, writeToTargetMethod, diff --git a/sample/src/main/kotlin/vipsffm/SampleHelper.kt b/sample/src/main/kotlin/vipsffm/SampleHelper.kt index 96b6ce2..ca207eb 100644 --- a/sample/src/main/kotlin/vipsffm/SampleHelper.kt +++ b/sample/src/main/kotlin/vipsffm/SampleHelper.kt @@ -34,6 +34,13 @@ object SampleHelper { ) } + val bytes = Files.readAllBytes(path) + if (bytes.all { it == 0.toByte() }) { + return Result.failure( + RuntimeException("file all zeroes") + ) + } + return Result.success(Unit) } } \ No newline at end of file diff --git a/sample/src/main/kotlin/vipsffm/SampleRunner.kt b/sample/src/main/kotlin/vipsffm/SampleRunner.kt index 7a43a82..411b5c5 100644 --- a/sample/src/main/kotlin/vipsffm/SampleRunner.kt +++ b/sample/src/main/kotlin/vipsffm/SampleRunner.kt @@ -9,6 +9,7 @@ import vipsffm.sample.VImageCachingSample import vipsffm.sample.VImageChainSample import vipsffm.sample.VImageCopyWriteSample import vipsffm.sample.VImageCreateThumbnailSample +import vipsffm.sample.VImageFromBytesSample import vipsffm.sample.VImageJoinSample import vipsffm.sample.VOptionHyphenSample import vipsffm.sample.VSourceTargetSample @@ -36,7 +37,8 @@ object SampleRunner { VImageArrayJoinSample, VBlobByteBufferSample, VTargetToFileSample, - VImageJoinSample + VImageJoinSample, + VImageFromBytesSample ) val sampleParentRunPath = Paths.get("sample_run") if (Files.exists(sampleParentRunPath)) { diff --git a/sample/src/main/kotlin/vipsffm/VTargetMemorySample.kt b/sample/src/main/kotlin/vipsffm/VTargetMemorySample.kt index 9914faa..e5b822a 100644 --- a/sample/src/main/kotlin/vipsffm/VTargetMemorySample.kt +++ b/sample/src/main/kotlin/vipsffm/VTargetMemorySample.kt @@ -11,7 +11,7 @@ object VTargetToFileSample: RunnableSample { override fun run(arena: Arena, workingDirectory: Path): Result { val source = VSource.newFromFile(arena, "sample/src/main/resources/sample_images/fox.jpg") - val sourceImage = VImage.newFromSource(arena, source, "") + val sourceImage = VImage.newFromSource(arena, source) val target = VTarget.newToMemory(arena) sourceImage.writeToTarget(target, ".jpg") @@ -19,7 +19,7 @@ object VTargetToFileSample: RunnableSample { val blob = target.blob val outputSource = VSource.newFromBlob(arena, blob) val outputPath = workingDirectory.resolve("fox_copy.jpg") - VImage.newFromSource(arena, outputSource, "") + VImage.newFromSource(arena, outputSource) .writeToFile(outputPath.absolutePathString()) return SampleHelper.validate( diff --git a/sample/src/main/kotlin/vipsffm/sample/VBlobByteBufferSample.kt b/sample/src/main/kotlin/vipsffm/sample/VBlobByteBufferSample.kt index 5767420..2847c26 100644 --- a/sample/src/main/kotlin/vipsffm/sample/VBlobByteBufferSample.kt +++ b/sample/src/main/kotlin/vipsffm/sample/VBlobByteBufferSample.kt @@ -16,7 +16,7 @@ object VBlobByteBufferSample: RunnableSample { arena, "sample/src/main/resources/sample_images/rabbit.jpg" ) - .thumbnailImage(400) + .thumbnailImage(400) val blob = image.jpegsaveBuffer() val bytes = blob.asClonedByteBuffer() diff --git a/sample/src/main/kotlin/vipsffm/sample/VImageFromBytesSample.kt b/sample/src/main/kotlin/vipsffm/sample/VImageFromBytesSample.kt new file mode 100644 index 0000000..94cdb5f --- /dev/null +++ b/sample/src/main/kotlin/vipsffm/sample/VImageFromBytesSample.kt @@ -0,0 +1,29 @@ +package vipsffm.sample + +import app.photofox.vipsffm.VImage +import app.photofox.vipsffm.VSource +import vipsffm.RunnableSample +import vipsffm.SampleHelper +import java.lang.foreign.Arena +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +object VImageFromBytesSample: RunnableSample { + + // note that this is generally inefficient, use a "VImage.newFrom" method if available + override fun run(arena: Arena, workingDirectory: Path): Result { + val path = Path.of("sample/src/main/resources/sample_images/rabbit.jpg") + val bytes = Files.readAllBytes(path) + val image = VImage.newFromBytes(arena, bytes) + .thumbnailImage(400) + + val outputPath = workingDirectory.resolve("rabbit_copy.jpg") + image.writeToFile(outputPath.absolutePathString()) + + return SampleHelper.validate( + outputPath, + expectedSizeBoundsKb = 20L..100L + ) + } +} \ No newline at end of file diff --git a/sample/src/main/kotlin/vipsffm/sample/VSourceTargetSample.kt b/sample/src/main/kotlin/vipsffm/sample/VSourceTargetSample.kt index 6e8b67b..a5c433a 100644 --- a/sample/src/main/kotlin/vipsffm/sample/VSourceTargetSample.kt +++ b/sample/src/main/kotlin/vipsffm/sample/VSourceTargetSample.kt @@ -16,7 +16,7 @@ object VSourceTargetSample: RunnableSample { override fun run(arena: Arena, workingDirectory: Path): Result { val source = VSource.newFromFile(arena, "sample/src/main/resources/sample_images/fox.jpg") - val sourceImage = VImage.newFromSource(arena, source, "") + val sourceImage = VImage.newFromSource(arena, source) val sourceWidth = sourceImage.width val sourceHeight = sourceImage.height