diff --git a/.github/workflows/build-libwebp.yml b/.github/workflows/build-libwebp.yml new file mode 100644 index 0000000..147d9e9 --- /dev/null +++ b/.github/workflows/build-libwebp.yml @@ -0,0 +1,165 @@ +name: Build libwebp + +on: + push: + paths: + - '.github/workflows/build-libwebp.yml' + workflow_dispatch: + inputs: + libwebp_version: + description: 'libwebp version tag (e.g. v1.6.0)' + required: false + default: 'v1.6.0' + +env: + LIBWEBP_VERSION: ${{ inputs.libwebp_version || 'v1.6.0' }} + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: linux + artifact: libwebp.so + - os: windows-latest + target: windows + artifact: libwebp.dll + - os: macos-latest + target: macos + artifact: libwebp.dylib + + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v6 + + - name: Clone libwebp + run: git clone --depth 1 --branch ${{ env.LIBWEBP_VERSION }} https://chromium.googlesource.com/webm/libwebp + + - name: Patch out decoder + shell: bash + run: | + cd libwebp + sed -i.bak 's/\$//' CMakeLists.txt + sed -i.bak 's/add_library(sharpyuv /add_library(sharpyuv STATIC /' CMakeLists.txt + + - name: Create export lists + shell: bash + run: | + cat > libwebp-exports.txt <<'EOF' + { global: WebPEncodeLosslessBGRA; WebPFree; local: *; }; + EOF + cat > libwebp-exports-macos.txt <<'EOF' + _WebPEncodeLosslessBGRA + _WebPFree + EOF + cat > libwebp-exports.def <<'EOF' + EXPORTS + WebPEncodeLosslessBGRA + WebPFree + EOF + + - name: Build (Unix) + if: runner.os != 'Windows' + run: | + cd libwebp + mkdir build && cd build + + if [ "${{ matrix.target }}" = "macos" ]; then + EXTRA_FLAGS="-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_SHARED_LINKER_FLAGS=-Wl,-exported_symbols_list,${{ github.workspace }}/libwebp-exports-macos.txt,-dead_strip" + else + EXTRA_FLAGS="-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--version-script=${{ github.workspace }}/libwebp-exports.txt,--gc-sections" + fi + + cmake .. \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_C_FLAGS="-ffunction-sections -fdata-sections -fvisibility=hidden -fno-asynchronous-unwind-tables" \ + -DBUILD_SHARED_LIBS=ON \ + -DWEBP_BUILD_ANIM_UTILS=OFF \ + -DWEBP_BUILD_CWEBP=OFF \ + -DWEBP_BUILD_DWEBP=OFF \ + -DWEBP_BUILD_GIF2WEBP=OFF \ + -DWEBP_BUILD_IMG2WEBP=OFF \ + -DWEBP_BUILD_VWEBP=OFF \ + -DWEBP_BUILD_WEBPINFO=OFF \ + -DWEBP_BUILD_WEBPMUX=OFF \ + -DWEBP_BUILD_EXTRAS=OFF \ + $EXTRA_FLAGS + cmake --build . --target webp --config MinSizeRel + if [ "${{ matrix.target }}" = "macos" ]; then + strip -x libwebp.* 2>/dev/null || true + else + strip -s libwebp.* 2>/dev/null || true + fi + + - name: Build (Windows) + if: runner.os == 'Windows' + run: | + cd libwebp + mkdir build && cd build + cmake .. ` + -DCMAKE_BUILD_TYPE=MinSizeRel ` + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON ` + -DCMAKE_C_FLAGS="/Gw /DWEBP_EXTERN=extern" ` + -DBUILD_SHARED_LIBS=ON ` + -DCMAKE_SHARED_LINKER_FLAGS="/DEF:${{ github.workspace }}/libwebp-exports.def /OPT:REF,ICF" ` + -DWEBP_BUILD_ANIM_UTILS=OFF ` + -DWEBP_BUILD_CWEBP=OFF ` + -DWEBP_BUILD_DWEBP=OFF ` + -DWEBP_BUILD_GIF2WEBP=OFF ` + -DWEBP_BUILD_IMG2WEBP=OFF ` + -DWEBP_BUILD_VWEBP=OFF ` + -DWEBP_BUILD_WEBPINFO=OFF ` + -DWEBP_BUILD_WEBPMUX=OFF ` + -DWEBP_BUILD_EXTRAS=OFF + cmake --build . --target webp --config MinSizeRel + + - name: Copy artifact + shell: bash + run: | + mkdir -p output + if [ "${{ matrix.target }}" = "windows" ]; then + cp libwebp/build/MinSizeRel/${{ matrix.artifact }} output/ + else + cp libwebp/build/${{ matrix.artifact }} output/ + fi + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: libwebp-${{ matrix.target }} + path: output/${{ matrix.artifact }} + + commit: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Place natives + run: | + mkdir -p webp/src/main/resources/natives/{linux,windows,macos} + cp artifacts/libwebp-linux/libwebp.so webp/src/main/resources/natives/linux/ + cp artifacts/libwebp-windows/libwebp.dll webp/src/main/resources/natives/windows/ + cp artifacts/libwebp-macos/libwebp.dylib webp/src/main/resources/natives/macos/ + + - name: Commit + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add webp/src/main/resources/natives/ + git diff --cached --quiet && echo "No changes" && exit 0 + git commit -m "Update bundled libwebp ${{ env.LIBWEBP_VERSION }}" + git push diff --git a/.github/workflows/test-webp.yml b/.github/workflows/test-webp.yml index 8682307..844beeb 100644 --- a/.github/workflows/test-webp.yml +++ b/.github/workflows/test-webp.yml @@ -2,12 +2,12 @@ name: Test WebP on: push: - branches: - - 'main' paths: - 'webp/**' - '.github/workflows/test-webp.yml' - pull_request: + workflow_run: + workflows: [Build libwebp] + types: [completed] workflow_dispatch: jobs: @@ -25,13 +25,16 @@ jobs: name: Linux (NgEngine) install: sudo find /usr -name 'libwebp*.so*' -exec mv {} {}.disabled \; expected-decode: Java (ngengine) + expected-encode: libwebp (lossless-only) - os: macos-latest name: macOS (ImageIO) expected-decode: macOS ImageIO + expected-encode: libwebp (lossless-only) - os: windows-latest name: Windows (WIC) install: winget install --id 9PG2DK419DRG --accept-package-agreements --accept-source-agreements --source msstore --silent expected-decode: Windows WIC + expected-encode: libwebp (lossless-only) name: ${{ matrix.name }} runs-on: ${{ matrix.os }} diff --git a/README.MD b/README.MD index 1ff3edb..d139816 100644 --- a/README.MD +++ b/README.MD @@ -45,14 +45,14 @@ repositories { dependencies { // Include only the modules you need: - implementation "org.redlance.platformtools:accent:4.1.1" - implementation "org.redlance.platformtools:referer:4.1.1" - implementation "org.redlance.platformtools:favorites:4.1.1" - implementation "org.redlance.platformtools:progress:4.1.1" - implementation "org.redlance.platformtools:webp:4.1.1" + implementation "org.redlance.platformtools:accent:4.2.0" + implementation "org.redlance.platformtools:referer:4.2.0" + implementation "org.redlance.platformtools:favorites:4.2.0" + implementation "org.redlance.platformtools:progress:4.2.0" + implementation "org.redlance.platformtools:webp:4.2.0" // For Java 8 (Downgraded version), add classifier: - // implementation("org.redlance.platformtools:accent:4.1.1:java8") + // implementation("org.redlance.platformtools:accent:4.2.0:java8") } ``` @@ -69,7 +69,7 @@ dependencies { org.redlance.platformtools accent - 4.1.1 + 4.2.0 ``` @@ -205,16 +205,27 @@ Decoder priority (first available wins): 3. **Windows WIC** — Windows Imaging Component via FFM (decode only, requires WebP codec from MS Store) 4. **ngengine** — pure-Java fallback ([image-webp-java](https://github.com/NostrGameEngine/image-webp-java), decode only, ~92 KB bundled) -The `webp` module has two variants: -- **Standard** (`webp`) — uses platform-native APIs or system-installed libwebp. Zero bundled dependencies. -- **Bundled** (`webp` with classifier `bundled`) — includes a shaded and ProGuard-optimized [ngengine image-webp-java](https://github.com/NostrGameEngine/image-webp-java) decoder (~92 KB). Pixel-exact on all platforms without native libraries. +Encoder priority: +1. **libwebp** — system-installed native library (lossy + lossless) +2. **libwebp (lossless-only)** — bundled native encoder (lossless only, included in standard and `bundled` variants) + +The `webp` module has three variants: + +| Variant | Classifier | Natives | ngengine | Use case | +|---------|------------|---------|----------|----------| +| **Standard** | — | bundled libwebp (lossless encoder) | — | Platform with native decoder (macOS/Windows) or system libwebp | +| **Bundled** | `bundled` | bundled libwebp (lossless encoder) | shaded + ProGuard | Self-contained, works everywhere | +| **Bundled Lite** | `bundled-lite` | — | shaded + ProGuard | Minimal size, no native binaries | ```groovy -// Standard — requires platform support or libwebp on library path -implementation "org.redlance.platformtools:webp:4.1.1" +// Standard — bundled lossless encoder, requires platform decoder or system libwebp for decoding +implementation "org.redlance.platformtools:webp:4.2.0" + +// Bundled — self-contained with ngengine decoder + bundled lossless encoder +implementation "org.redlance.platformtools:webp:4.2.0:bundled" -// Bundled — self-contained, works everywhere (decode only) -implementation "org.redlance.platformtools:webp:4.1.1:bundled" +// Bundled Lite — ngengine decoder only, no native binaries +implementation "org.redlance.platformtools:webp:4.2.0:bundled-lite" ``` ```java diff --git a/build.gradle b/build.gradle index 245e3da..d28f592 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ plugins { allprojects { group = "org.redlance" - version = "4.1.1" + version = "4.2.0" repositories { mavenCentral() diff --git a/renovate.json b/renovate.json index 5db72dd..48df5de 100644 --- a/renovate.json +++ b/renovate.json @@ -2,5 +2,14 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" + ], + "customManagers": [ + { + "customType": "regex", + "fileMatch": ["\\.github/workflows/build-libwebp\\.yml$"], + "matchStrings": ["'(?v\\d+\\.\\d+\\.\\d+)'"], + "depNameTemplate": "webmproject/libwebp", + "datasourceTemplate": "github-tags" + } ] } diff --git a/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java b/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java index 5913426..f189ecd 100644 --- a/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java +++ b/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java @@ -4,6 +4,7 @@ import org.redlance.platformtools.favorites.PlatformFinderFavorites; import org.redlance.platformtools.progress.PlatformProgressBars; import org.redlance.platformtools.referer.PlatformFileReferer; +import org.redlance.platformtools.webp.WebPDiagnostics; import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; @@ -311,6 +312,10 @@ private void showWebPDialog() { } log.append("\n"); + WebPDiagnostics.summary(s -> { + log.append(s); + log.append("\n"); + }); }); buttons.add(probeBtn); diff --git a/webp/build.gradle b/webp/build.gradle index 0e89768..8d16515 100644 --- a/webp/build.gradle +++ b/webp/build.gradle @@ -70,7 +70,14 @@ tasks.register("proguardJar", ProGuardTask) { configuration(file("proguard.pro")) } -assemble.dependsOn proguardJar +tasks.register("bundledLiteJar", Jar) { + dependsOn proguardJar + from(zipTree(proguardJar.getOutJarFileCollection().singleFile)) + exclude 'natives/**' + archiveClassifier.set("bundled-lite") +} + +assemble.dependsOn proguardJar, bundledLiteJar downgradeJar.enabled = false shadow { @@ -82,4 +89,8 @@ publishing.publications.named("mavenJava") { classifier = "bundled" builtBy(tasks.named("proguardJar")) } + artifact(tasks.named("bundledLiteJar")) { + classifier = "bundled-lite" + builtBy(tasks.named("bundledLiteJar")) + } } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/WebPDiagnostics.java b/webp/src/main/java/org/redlance/platformtools/webp/WebPDiagnostics.java index ce3d87a..6a56168 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/WebPDiagnostics.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/WebPDiagnostics.java @@ -3,6 +3,8 @@ import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; +import java.util.function.Consumer; + /** * Diagnostics utility for WebP backend status. * @@ -15,86 +17,73 @@ private WebPDiagnostics() { } /** - * Returns a human-readable summary of the current WebP backend status, - * including active backends and suggestions for improvement. + * Sends a human-readable summary of the current WebP backend status + * line-by-line to the given consumer. + * + * @param output line consumer (e.g. {@code logger::info}) */ - public static String summary() { - StringBuilder sb = new StringBuilder(); - sb.append("=== WebP Backend Status ===\n"); - - appendDecoderStatus(sb); - sb.append('\n'); - appendEncoderStatus(sb); - sb.append('\n'); - appendSuggestions(sb); + public static void summary(Consumer output) { + output.accept("=== WebP Backend Status ==="); - return sb.toString(); + appendDecoderStatus(output); + appendEncoderStatus(output); + appendSuggestions(output); } - private static void appendDecoderStatus(StringBuilder sb) { + private static void appendDecoderStatus(Consumer output) { PlatformWebPDecoder decoder = PlatformWebPDecoder.INSTANCE; - sb.append("Decoder: "); - if (decoder.isAvailable()) { - sb.append(decoder.backendName()); - } else { - sb.append("UNAVAILABLE"); - } - sb.append('\n'); + output.accept("Decoder: " + (decoder.isAvailable() ? decoder.backendName() : "UNAVAILABLE")); } - private static void appendEncoderStatus(StringBuilder sb) { + private static void appendEncoderStatus(Consumer output) { PlatformWebPEncoder encoder = PlatformWebPEncoder.INSTANCE; - sb.append("Encoder: "); - if (encoder.isAvailable()) { - sb.append(encoder.backendName()); - } else { - sb.append("UNAVAILABLE"); - } - sb.append('\n'); + output.accept("Encoder: " + (encoder.isAvailable() ? encoder.backendName() : "UNAVAILABLE")); } - private static void appendSuggestions(StringBuilder sb) { + private static void appendSuggestions(Consumer output) { PlatformWebPDecoder decoder = PlatformWebPDecoder.INSTANCE; PlatformWebPEncoder encoder = PlatformWebPEncoder.INSTANCE; boolean decoderNative = decoder.isAvailable() && "libwebp".equals(decoder.backendName()); - boolean encoderAvailable = encoder.isAvailable(); + boolean encoderFull = encoder.isAvailable() && "libwebp".equals(encoder.backendName()); - if (decoderNative && encoderAvailable) { - sb.append("Status: optimal (libwebp provides both encode and decode)\n"); + if (decoderNative && encoderFull) { + output.accept("Status: optimal (libwebp provides both encode and decode)"); return; } - sb.append("Suggestions:\n"); + output.accept("Suggestions:"); String os = System.getProperty("os.name", "").toLowerCase(); - sb.append("- Install libwebp for native encode/decode support:\n"); if (os.contains("mac")) { - sb.append(" brew install webp\n"); + output.accept("- Install libwebp for full native encode/decode support: brew install webp"); } else if (os.contains("linux")) { - sb.append(" apt install libwebp-dev (Debian/Ubuntu)\n"); - sb.append(" dnf install libwebp-devel (Fedora/RHEL)\n"); + output.accept("- Install libwebp for full native encode/decode support:"); + output.accept(" apt install libwebp-dev (Debian/Ubuntu)"); + output.accept(" dnf install libwebp-devel (Fedora/RHEL)"); } else if (os.contains("win")) { - sb.append(" Download from https://developers.google.com/speed/webp/download\n"); - sb.append(" and add the bin/ directory to PATH\n"); + output.accept("- Install libwebp for full native encode/decode support:"); + output.accept(" Download from https://developers.google.com/speed/webp/download"); + output.accept(" and add the bin/ directory to PATH"); } if (decoder.isAvailable() && !decoderNative) { String backend = decoder.backendName(); if (backend.contains("ngengine")) { - sb.append("- Decoder uses pure-Java fallback (slower than native libwebp)\n"); + output.accept("- Decoder uses pure-Java fallback (slower than native libwebp)"); } else if (backend.contains("macOS") || backend.contains("Windows")) { - sb.append("- Decoder uses platform API (").append(backend).append(")\n"); - sb.append(" libwebp may offer better performance and consistency\n"); + output.accept("- Decoder uses platform API (" + backend + "), libwebp may offer better performance and consistency"); } } if (!decoder.isAvailable()) { - sb.append("- No decoder available! Add the ngengine bundled dependency as a fallback\n"); + output.accept("- No decoder available! Add the ngengine bundled dependency as a fallback"); } - if (!encoderAvailable) { - sb.append("- No encoder available — libwebp is the only supported encoder backend\n"); + if (encoder.isAvailable() && !encoderFull) { + output.accept("- Encoder supports lossless mode only (bundled libwebp), install full libwebp for lossy encoding support"); + } else if (!encoder.isAvailable()) { + output.accept("- No encoder available — libwebp is the only supported encoder backend"); } } -} \ No newline at end of file +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPDecoder.java index b46470f..9484d34 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPDecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPDecoder.java @@ -39,7 +39,8 @@ private LibWebPDecoder(LibWebPLibrary lib) { public static @Nullable LibWebPDecoder tryCreate() { LibWebPLibrary lib = LibWebPLibrary.getInstance(); - return lib != null ? new LibWebPDecoder(lib) : null; + if (lib == null || lib.lookup.find("WebPDecodeARGB").isEmpty()) return null; + return new LibWebPDecoder(lib); } @Override diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPEncoder.java index 6c86b6d..743179a 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPEncoder.java @@ -12,7 +12,7 @@ public final class LibWebPEncoder implements PlatformWebPEncoder { // size_t WebPEncodeLosslessBGRA(const uint8_t* bgra, int w, int h, int stride, uint8_t** output) private final MethodHandle webPEncodeLosslessBGRA; // size_t WebPEncodeBGRA(const uint8_t* bgra, int w, int h, int stride, float quality, uint8_t** output) - private final MethodHandle webPEncodeBGRA; + private final @Nullable MethodHandle webPEncodeBGRA; private LibWebPEncoder(LibWebPLibrary lib) { this.lib = lib; @@ -27,24 +27,24 @@ private LibWebPEncoder(LibWebPLibrary lib) { ValueLayout.ADDRESS ) ); - this.webPEncodeBGRA = linker.downcallHandle( - lib.lookup.find("WebPEncodeBGRA").orElseThrow(), + this.webPEncodeBGRA = lib.lookup.find("WebPEncodeBGRA").map(symbol -> linker.downcallHandle(symbol, FunctionDescriptor.of( ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_FLOAT, ValueLayout.ADDRESS ) - ); + )).orElse(null); } public static @Nullable LibWebPEncoder tryCreate() { LibWebPLibrary lib = LibWebPLibrary.getInstance(); - return lib != null ? new LibWebPEncoder(lib) : null; + if (lib == null || lib.lookup.find("WebPEncodeLosslessBGRA").isEmpty()) return null; + return new LibWebPEncoder(lib); } @Override public String backendName() { - return "libwebp"; + return this.webPEncodeBGRA != null ? "libwebp" : "libwebp (lossless-only)"; } @Override @@ -76,6 +76,7 @@ public byte[] encodeLossless(int[] argb, int width, int height) { @Override public byte[] encodeLossy(int[] argb, int width, int height, float quality) { + if (this.webPEncodeBGRA == null) return encodeLossless(argb, width, height); try (Arena arena = Arena.ofConfined()) { MemorySegment outputPtr = arena.allocate(ValueLayout.ADDRESS); diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPLibrary.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPLibrary.java index 40a4b8d..ec48953 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPLibrary.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPLibrary.java @@ -2,9 +2,13 @@ import org.jetbrains.annotations.Nullable; +import java.io.IOException; +import java.io.InputStream; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; public final class LibWebPLibrary { final SymbolLookup lookup; @@ -69,6 +73,38 @@ private static class Holder { } } + // 3. Bundled native from JAR + try { + return tryLoadBundled(); + } catch (Throwable ignored) { + } + return null; } + + private static @Nullable LibWebPLibrary tryLoadBundled() throws IOException { + String os = System.getProperty("os.name", "").toLowerCase(); + + String name; + if (os.contains("linux")) { + name = "linux/libwebp.so"; + } else if (os.contains("win")) { + name = "windows/libwebp.dll"; + } else if (os.contains("mac")) { + name = "macos/libwebp.dylib"; + } else { + return null; + } + + try (InputStream in = LibWebPLibrary.class.getResourceAsStream("/natives/" + name)) { + if (in == null) return null; + + String ext = name.substring(name.lastIndexOf('.')); + Path temp = Files.createTempFile("libwebp-", ext); + temp.toFile().deleteOnExit(); + Files.copy(in, temp, StandardCopyOption.REPLACE_EXISTING); + + return new LibWebPLibrary(SymbolLookup.libraryLookup(temp, Arena.global())); + } + } } diff --git a/webp/src/main/resources/natives/linux/libwebp.so b/webp/src/main/resources/natives/linux/libwebp.so new file mode 100644 index 0000000..ca30067 Binary files /dev/null and b/webp/src/main/resources/natives/linux/libwebp.so differ diff --git a/webp/src/main/resources/natives/macos/libwebp.dylib b/webp/src/main/resources/natives/macos/libwebp.dylib new file mode 100644 index 0000000..8a291e2 Binary files /dev/null and b/webp/src/main/resources/natives/macos/libwebp.dylib differ diff --git a/webp/src/main/resources/natives/windows/libwebp.dll b/webp/src/main/resources/natives/windows/libwebp.dll new file mode 100644 index 0000000..4ca9493 Binary files /dev/null and b/webp/src/main/resources/natives/windows/libwebp.dll differ diff --git a/webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java b/webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java index 565a120..c85128d 100644 --- a/webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java +++ b/webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java @@ -14,10 +14,8 @@ class LibWebPTest { @Test - void encoderAvailableWithDecoder() { - PlatformWebPDecoder dec = LibWebPDecoder.tryCreate(); - assumeTrue(dec != null, "libwebp not available"); - assertNotNull(LibWebPEncoder.tryCreate(), "libwebp encoder should be available when decoder is"); + void encoderAvailable() { + assertNotNull(LibWebPEncoder.tryCreate(), "libwebp encoder should be available (bundled or system)"); } @Test @@ -46,6 +44,18 @@ void lossyRoundtripDimensions() { assertEquals(original.length, decoded.argb().length); } + @Test + void bundledLosslessRoundtrip() { + LibWebPEncoder enc = LibWebPEncoder.tryCreate(); + assumeTrue(enc != null, "libwebp encoder not available"); + assumeTrue(PlatformWebPDecoder.INSTANCE.isAvailable(), "No decoder available"); + + int[] original = TestUtils.generateTestImage(); + byte[] encoded = enc.encodeLossless(original, TestUtils.W, TestUtils.H); + DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(encoded); + assertArrayEquals(original, decoded.argb(), "Lossless roundtrip pixels must match exactly"); + } + @Test void decodeTestFile() throws IOException { PlatformWebPDecoder dec = LibWebPDecoder.tryCreate();