Skip to content

Commit 3c7f948

Browse files
committed
Fix signature of VCustomSource#SeekCallback (fixes #171)
Previously the offset was mistyped as a `*void`/`MemorySegment`. It was now changed to be a `gint64`/`long`. Additionally, the `int whence` parameter was replaced with a new `SeekWhence` enum that provides more type safety and discoverability for implementers. To test, a sample was added to provide `VCustomSource` based on a `FileHandle` that implements seeking, testing it agains a TIF image.
1 parent 0d498ab commit 3c7f948

File tree

6 files changed

+144
-13
lines changed

6 files changed

+144
-13
lines changed

core/src/main/java/app/photofox/vipsffm/VCustomSource.java

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,55 @@ public final class VCustomSource extends VSource {
1616
@FunctionalInterface
1717
public interface ReadCallback {
1818

19+
/**
20+
* Read data from the input and write it to the provided memory segment
21+
*
22+
* @param dataPointer Pointer to the memory segment where the data should be written
23+
* @param length The maximum number of bytes to read
24+
* @return The number of bytes actually read, or -1 on error
25+
*/
1926
long read(MemorySegment dataPointer, long length);
2027
}
2128

2229
@FunctionalInterface
2330
public interface SeekCallback {
2431

25-
long seek(int whence);
32+
/**
33+
* Seek to a specific position in the input
34+
*
35+
* @param offset Relative offset in bytes from the position specified by {@code whence}
36+
* @param whence Where to seek from, one of {@link SeekWhence}
37+
* @return the new position in the input, or -1 on error
38+
*/
39+
long seek(long offset, SeekWhence whence);
40+
}
41+
42+
public enum SeekWhence {
43+
/** Seek from the beginning of the input */
44+
SEEK_SET(0),
45+
/** Seek from the current position in the input */
46+
SEEK_CUR(1),
47+
/** Seek from the end of the input */
48+
SEEK_END(2);
49+
50+
private final int value;
51+
52+
public static SeekWhence fromValue(int value) {
53+
return switch (value) {
54+
case 0 -> SEEK_SET;
55+
case 1 -> SEEK_CUR;
56+
case 2 -> SEEK_END;
57+
default -> throw new IllegalArgumentException("Unknown seek whence value: " + value);
58+
};
59+
}
60+
61+
SeekWhence(int value) {
62+
this.value = value;
63+
}
64+
65+
public int getValue() {
66+
return value;
67+
}
2668
}
2769

2870
private final VCustomSource.ReadCallback readCallback;
@@ -39,7 +81,7 @@ public VCustomSource(
3981

4082
attachReadSignal(arena, this);
4183
if (seekCallback != null) {
42-
attachSeekSignal(arena, this, seekCallback);
84+
attachSeekSignal(arena, this);
4385
}
4486
}
4587

@@ -78,17 +120,17 @@ public long apply(
78120
}
79121
}
80122

81-
private void attachSeekSignal(Arena arena, VSource source, SeekCallback seekCallback) {
123+
private void attachSeekSignal(Arena arena, VSource source) {
82124
var callback = new CustomStreamSeekCallback.Function() {
83125

84126
@Override
85127
public long apply(
86128
MemorySegment source,
87-
MemorySegment data,
129+
long offset,
88130
int whence,
89131
MemorySegment handle
90132
) {
91-
return seekCallback.seek(whence);
133+
return seekCallback.seek(offset, SeekWhence.fromValue(whence));
92134
}
93135
};
94136
var callbackPointer = CustomStreamSeekCallback.allocate(callback, arena);

core/src/main/java/app/photofox/vipsffm/jextract/CustomStreamSeekCallback.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
/**
1616
* {@snippet lang=c :
17-
* typedef gint64 (*CustomStreamSeekCallback)(VipsSourceCustom *, void *, int, void *)
17+
* typedef gint64 (*CustomStreamSeekCallback)(VipsSourceCustom *, gint64, int, void *)
1818
* }
1919
*/
2020
public class CustomStreamSeekCallback {
@@ -27,13 +27,13 @@ public class CustomStreamSeekCallback {
2727
* The function pointer signature, expressed as a functional interface
2828
*/
2929
public interface Function {
30-
long apply(MemorySegment source, MemorySegment data, int whence, MemorySegment handle);
30+
long apply(MemorySegment source, long offset, int whence, MemorySegment handle);
3131
}
3232

3333
private static final FunctionDescriptor $DESC = FunctionDescriptor.of(
34-
VipsRaw.C_LONG_LONG,
35-
VipsRaw.C_POINTER,
34+
VipsRaw.C_LONG,
3635
VipsRaw.C_POINTER,
36+
VipsRaw.C_LONG,
3737
VipsRaw.C_INT,
3838
VipsRaw.C_POINTER
3939
);
@@ -60,9 +60,9 @@ public static MemorySegment allocate(CustomStreamSeekCallback.Function fi, Arena
6060
/**
6161
* Invoke the upcall stub {@code funcPtr}, with given parameters
6262
*/
63-
public static long invoke(MemorySegment funcPtr,MemorySegment source, MemorySegment data, int whence, MemorySegment handle) {
63+
public static long invoke(MemorySegment funcPtr,MemorySegment source, long offset, int whence, MemorySegment handle) {
6464
try {
65-
return (long) DOWN$MH.invokeExact(funcPtr, source, data, whence, handle);
65+
return (long) DOWN$MH.invokeExact(funcPtr, source, offset, whence, handle);
6666
} catch (Throwable ex$) {
6767
throw new AssertionError("should not reach here", ex$);
6868
}

jextract_entry/callback_typedefs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
typedef gint64 (*CustomStreamReadCallback)(VipsSourceCustom *source, void *data, gint64 length, void *handle);
22

3-
typedef gint64 (*CustomStreamSeekCallback)(VipsSourceCustom *source, void *data, int whence, void *handle);
3+
typedef gint64 (*CustomStreamSeekCallback)(VipsSourceCustom *source, gint64 offset, int whence, void *handle);
44

55
typedef gint64 (*CustomStreamWriteCallback)(VipsTargetCustom *source, void *data, gint64 length, void *handle);
66

sample/src/main/kotlin/vipsffm/SampleRunner.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import vipsffm.sample.VImageCachingSample
1313
import vipsffm.sample.VImageChainSample
1414
import vipsffm.sample.VImageCopyWriteSample
1515
import vipsffm.sample.VImageCreateThumbnailSample
16+
import vipsffm.sample.VImageCustomSourceTargetSample
1617
import vipsffm.sample.VImageFindTrimSample
1718
import vipsffm.sample.VImageFromBytesSample
1819
import vipsffm.sample.VImageGetPagesSample
@@ -55,7 +56,8 @@ object SampleRunner {
5556
HelperGetSetMetadataSample,
5657
VImageGetSetSample,
5758
VImageFindTrimSample,
58-
VImageFromMemoryToMemorySample
59+
VImageFromMemoryToMemorySample,
60+
VImageCustomSourceTargetSample
5961
)
6062
val sampleParentRunPath = Paths.get("sample_run")
6163
if (Files.exists(sampleParentRunPath)) {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package vipsffm.sample
2+
3+
import app.photofox.vipsffm.VCustomSource
4+
import app.photofox.vipsffm.VCustomTarget
5+
import app.photofox.vipsffm.VImage
6+
import vipsffm.RunnableSample
7+
import vipsffm.SampleHelper
8+
import java.io.IOException
9+
import java.lang.foreign.Arena
10+
import java.lang.foreign.MemorySegment
11+
import java.nio.channels.FileChannel
12+
import java.nio.file.Files
13+
import java.nio.file.Path
14+
import java.nio.file.StandardOpenOption
15+
import java.util.function.Consumer
16+
17+
/**
18+
* Sample showing loading an image from a custom source and writing it to a custom source.
19+
*
20+
* Implemented using `FileChannel` for reading and writing, which is of little practical use but
21+
* demonstrates the use of custom sources and targets quite well.
22+
*
23+
* See https://www.libvips.org/2019/11/29/True-streaming-for-libvips.html
24+
*/
25+
object VImageCustomSourceTargetSample: RunnableSample {
26+
internal class FileChannelSource {
27+
val input: FileChannel
28+
29+
constructor(path: Path) {
30+
this.input = FileChannel.open(path, StandardOpenOption.READ)
31+
}
32+
33+
fun read(buffer: MemorySegment, length: Long): Long {
34+
val byteBuffer = buffer.asSlice(0, length).asByteBuffer()
35+
return input.read(byteBuffer).toLong()
36+
}
37+
38+
fun seek(offset: Long, whence: VCustomSource.SeekWhence): Long {
39+
return when (whence) {
40+
VCustomSource.SeekWhence.SEEK_SET -> input.position(offset)
41+
VCustomSource.SeekWhence.SEEK_CUR -> input.position(input.position() + offset)
42+
VCustomSource.SeekWhence.SEEK_END -> input.position(input.size() + offset)
43+
}.position()
44+
}
45+
46+
fun close() {
47+
input.close()
48+
}
49+
}
50+
51+
class FileChannelTarget {
52+
private val output: FileChannel
53+
54+
constructor(path: Path) {
55+
this.output = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)
56+
}
57+
58+
fun write(buffer: MemorySegment): Long {
59+
return output.write(buffer.asByteBuffer()).toLong()
60+
}
61+
62+
fun end(): Int {
63+
output.close()
64+
return 0
65+
}
66+
}
67+
68+
override fun run(arena: Arena, workingDirectory: Path): Result<Unit> {
69+
val outputPath = workingDirectory.resolve("fox_copy.jpg")
70+
val inputPath = workingDirectory.resolve("fox_input.tif")
71+
Files.copy(ClassLoader.getSystemResourceAsStream("sample_images/fox.tif"), inputPath)
72+
73+
val src = FileChannelSource(inputPath)
74+
val vipsSrc = VCustomSource(arena, src::read, src::seek)
75+
76+
val target = FileChannelTarget(outputPath)
77+
val vipsTarget = VCustomTarget(arena, target::write, target::end)
78+
79+
VImage.thumbnailSource(arena, vipsSrc, 800)
80+
.writeToTarget(vipsTarget, ".jpg")
81+
82+
return SampleHelper.validate(
83+
outputPath,
84+
expectedSizeBoundsKb = 20..100L
85+
)
86+
}
87+
}
34.3 MB
Binary file not shown.

0 commit comments

Comments
 (0)