Skip to content

Commit

Permalink
Tidy up README ready for a first release (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
lopcode authored Aug 20, 2024
1 parent 35c1375 commit f63c65f
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Generate helpers" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="vipsffm.GenerateHelpers" />
<module name="vips-ffm.helper-generator.main" />
<configuration default="false" name="Generate Vips class" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="vipsffm.GenerateVipsClass" />
<module name="vips-ffm.generator.main" />
<shortenClasspath name="NONE" />
<option name="VM_PARAMETERS" value="--enable-preview" />
<method v="2">
Expand Down
2 changes: 1 addition & 1 deletion .run/SampleRunner.run.xml → .run/Run samples.run.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SampleRunner" type="JetRunConfigurationType">
<configuration default="false" name="Run samples" type="JetRunConfigurationType">
<envs>
<env name="DYLD_LIBRARY_PATH" value="native_libs" />
</envs>
Expand Down
125 changes: 77 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,89 @@ of libvips, FFM, and auto-generated helpers makes for performant, safe, and ergo
Supports a vast range of image formats, including HEIC, JXL, WebP, PNG, JPEG, and more. Pronounced "vips (like zips)
eff-eff-emm".

Bindings are automatically generated using `jextract`, and enhanced with some helpful wrapper functions to make usage
more idiomatic.
Raw bindings are automatically generated using `jextract`, then processed and wrapped with some lightweight "helpers" to
allow for safe, idiomatic usage.

Incubating in [Photo Fox](https://github.com/lopcode/photo-fox).
Like what you've read so far? Please give the repo a star 🌟️!

## Usage

This is still a work-in-progress, and there aren't any packages published yet. As the project uses the FFM API, it must
target JDK 22+.

The libvips API is exposed via a Java helper class, `Vips`. You must provide an [Arena][1] during initialisation. This
Arena constrains the lifetime of objects generated during usage of the vips-ffm API, so be careful to only keep it in
scope for as long as you need to. If the arena doesn't close, your memory usage will grow forever. Constructing the
`Vips` object is cheap, as it's just a wrapper, so make them as you need.

This library **does not** include `libvips` in the download, you must add it to the system you're building on, then make
sure it's available in `DYLD_LIBRARY_PATH` (on macOS) or `LD_LIBRARY_PATH` (on Linux).

🚨 Raw bindings generated by `jextract` are available in `VipsRaw` as static methods. It's very difficult to use
`VipsRaw` functions without accidentally causing memory leaks, or even segfaults! If what you want to do is available in
`Vips`, use that instead. If you notice something missing, open a GitHub Issue.

### Thumbnail sample

To get a feeling for the bindings, here's an indicative sample written in Kotlin (using the Java bindings) that:
* Loads an original JPEG image from disk
* Writes a copy of it to disk
* Creates a 400px thumbnail from the original, and writes that to disk

<img align="right" width="200" src="sample/src/main/resources/sample_images/rabbit.jpg">

```kotlin
import app.photofox.vipsffm.generated.Vips
import java.lang.foreign.Arena

// ...

Arena.ofConfined().use { arena ->
val vips = Vips(arena)

val version = vips.versionString()
logger.info("vips version: $version")

val sourceImage = vips.imageNewFromFile(
"sample/src/main/resources/sample_images/rabbit.jpg",
VipsIntOption("access", VipsRaw.VIPS_ACCESS_SEQUENTIAL())
)
val sourceWidth = VipsImage.Xsize(sourceImage)
val sourceHeight = VipsImage.Ysize(sourceImage)
logger.info("source image size: $sourceWidth x $sourceHeight")

val outputPath = workingDirectory.resolve("rabbit_copy.jpg")
vips.imageWriteToFile(sourceImage, outputPath.absolutePathString())

val outputImagePointer = VipsOutputPointer(arena)
vips.thumbnail("sample/src/main/resources/sample_images/rabbit.jpg", outputImagePointer, 400)
val thumbnailImage = outputImagePointer.dereferencedOrThrow()
val thumbnailPath = workingDirectory.resolve("rabbit_thumbnail_400.jpg")
vips.imageWriteToFile(thumbnailImage, thumbnailPath.absolutePathString())

val thumbnailWidth = VipsImage.Xsize(thumbnailImage)
val thumbnailHeight = VipsImage.Ysize(thumbnailImage)
logger.info("thumbnail image size: $thumbnailWidth x $thumbnailHeight")
}
```

The project has several samples, [described below](#samples).

## Project goals

* Publish to GitHub Packages via Gradle, so that using the library is as easy as
`implementation("app.photofox:vips-ffm-core:$version")` (TODO)
* Automate as much as possible, including API generation via [jextract](https://github.com/openjdk/jextract), so
upstream changes can be rapidly integrated ✅
* Core module should only include this automated output, with a minimum of extra things that can break as upstreams
change ✅
* If helpers are required, publish in another artifact ✅
* Generate these too, if possible
* Keep generated APIs as similar to the original as possible
* Incubate in Photo Fox with some "real world" usage
Ideas and suggestions are welcome, but please make sure they fit in to these goals, or you have a good argument about
why a goal should change!

* Avoid manual work by automating as much as possible. This means upstream changes can be rapidly integrated.
* Keep generated APIs as similar to the original as is sensible. Small changes to make the API nicer, or make correct
usage easier, are ok (eg `gboolean` -> `boolean`, or `VipsOutputPointer` to manage tricky pointer translations).
* Provide access to the raw bindings (`VipsRaw`), so users aren't blocked by helper bugs or API annoyances.
* Incubate in [Photo Fox](https://github.com/lopcode/photo-fox) with some "real world" usage.

## Samples

Samples are included that show various usages of the `libvips` bindings. They include validations, and run on GitHub
Actions to validate new versions during development.
Actions as "end-to-end tests" during development.

To get set up to run samples (on macOS):
* `brew install vips`
Expand All @@ -36,7 +97,7 @@ To get set up to run samples (on macOS):
* (Optional, to regenerate bindings) `./generate_ffm_bindings.sh`
* Then either:
* Run `./run_samples.sh` in your terminal
* Run `VipsFfm` in IntelliJ to run samples and validations
* Run `SampleRunner` in IntelliJ to run samples and validations

```
[main] INFO vipsffm.SampleRunner - clearing sample run directory at path "sample_run"
Expand All @@ -59,36 +120,4 @@ memory: high-water mark 36.55 MB
[main] INFO vipsffm.SampleRunner - all samples ran successfully 🎉
```

### Thumbnail sample

To get a feeling for the bindings, here's an indicative sample written in Kotlin (using the Java bindings) that:
* Loads an original JPEG image from disk
* Writes a copy of it to disk
* Creates a 400px thumbnail from the original, and writes that to disk

```kt
Arena.ofConfined().use { arena ->
val vips = Vips(arena)

val sourceImage = vips.imageNewFromFile(
"sample/src/main/resources/sample_images/rabbit.jpg",
VipsIntOption("access", VipsRaw.VIPS_ACCESS_SEQUENTIAL())
)
val sourceWidth = VipsImage.Xsize(sourceImage)
val sourceHeight = VipsImage.Ysize(sourceImage)
logger.info("source image size: $sourceWidth x $sourceHeight")

val outputPath = workingDirectory.resolve("rabbit_copy.jpg")
vips.imageWriteToFile(sourceImage, outputPath.absolutePathString())

val outputImagePointer = VipsOutputPointer(arena)
vips.thumbnail("sample/src/main/resources/sample_images/rabbit.jpg", outputImagePointer, 400)
val thumbnailImage = outputImagePointer.dereferencedOrThrow()
val thumbnailPath = workingDirectory.resolve("rabbit_thumbnail_400.jpg")
vips.imageWriteToFile(thumbnailImage, thumbnailPath.absolutePathString())

val thumbnailWidth = VipsImage.Xsize(thumbnailImage)
val thumbnailHeight = VipsImage.Ysize(thumbnailImage)
logger.info("thumbnail image size: $thumbnailWidth x $thumbnailHeight")
}
```
[1]: https://docs.oracle.com/en/java/javase/22/core/memory-segments-and-arenas.html
2 changes: 1 addition & 1 deletion generate_ffm_bindings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ set -x
@includes_filtered.txt \
"$LIBVIPS_ENTRY_PATH"

./gradlew clean helper-generator:run
./gradlew clean generator:run
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ tasks.withType<Test> {
}

application {
mainClass = "vipsffm.GenerateHelpers"
mainClass = "vipsffm.GenerateVipsClass"
applicationDefaultJvmArgs = listOf("--enable-preview")
tasks.run.get().workingDir = rootProject.projectDir
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import java.util.Locale
import javax.lang.model.SourceVersion
import javax.lang.model.element.Modifier

object GenerateHelpers {
object GenerateVipsClass {

private val logger = LoggerFactory.getLogger(GenerateHelpers::class.java)
private val logger = LoggerFactory.getLogger(GenerateVipsClass::class.java)
private val vipsValidatorType = ClassName.get("app.photofox.vipsffm.helper", "VipsValidation")
private val vipsInvokerType = ClassName.get("app.photofox.vipsffm.helper", "VipsInvoker")
private val vipsErrorType = ClassName.get("app.photofox.vipsffm.helper", "VipsError")
Expand Down Expand Up @@ -372,7 +372,7 @@ object GenerateHelpers {
.build()
val javaFile = JavaFile.builder("app.photofox.vipsffm.generated", vipsClass)
.build()
val targetGeneratedSourceRoot = Path.of("helper/src/main/java/")
val targetGeneratedSourceRoot = Path.of("core/src/main/java/")
javaFile.writeToPath(targetGeneratedSourceRoot, Charsets.UTF_8)
}

Expand Down
52 changes: 0 additions & 52 deletions helper/build.gradle.kts

This file was deleted.

4 changes: 3 additions & 1 deletion run_samples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ echo "checking for leaks..."
if grep --quiet "objects alive:" sample_output.log; then
echo "failure - detected a memory leak!"
exit 1
fi
fi

echo "no leaks detected 🎉"
1 change: 0 additions & 1 deletion sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ repositories {

dependencies {
implementation(project(":core"))
implementation(project(":helper"))
implementation(platform("org.slf4j:slf4j-bom:2.0.16"))
implementation("org.slf4j:slf4j-api")
implementation("org.slf4j:slf4j-simple")
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ plugins {
}

rootProject.name = "vips-ffm"
include("core", "helper", "helper-generator", "sample")
include("core", "generator", "sample")

0 comments on commit f63c65f

Please sign in to comment.