From 4b2bb8619090475da40a8eac6cd691f426cf9f6b Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Mon, 9 Mar 2026 21:52:37 +0700 Subject: [PATCH 01/22] Add WebP codec module with FFM-based platform backends and bundled artifact New `webp` module providing WebP encode/decode via Java 25 FFM API: - libwebp backend (cross-platform, if library available) - macOS ImageIO (CoreGraphics) backend - Windows WIC backend - Java ImageIO fallback (works with TwelveMonkeys plugin) - Bundled artifact with shaded+ProGuard-optimized TwelveMonkeys (~146K) - Testing UI with backend probing, encode/decode tests, and file preview - Updated README with WebP documentation and usage examples Co-Authored-By: Claude Opus 4.6 --- README.MD | 48 ++++ build.gradle | 27 +- settings.gradle | 1 + testing/build.gradle | 6 + testing/src/main/java/module-info.java | 1 + .../platformtools/testing/TestingApp.java | 204 ++++++++++++++++ webp/build.gradle | 66 +++++ webp/proguard.pro | 2 + webp/src/main/java/module-info.java | 12 + .../webp/decoder/PlatformWebPDecoder.java | 60 +++++ .../webp/encoder/PlatformWebPEncoder.java | 57 +++++ .../webp/impl/PlatformWebPDecoderImpl.java | 72 ++++++ .../webp/impl/PlatformWebPEncoderImpl.java | 72 ++++++ .../webp/impl/imageio/JavaImageIODecoder.java | 105 ++++++++ .../webp/impl/imageio/JavaImageIOEncoder.java | 91 +++++++ .../webp/impl/libwebp/LibWebPDecoder.java | 109 +++++++++ .../webp/impl/libwebp/LibWebPEncoder.java | 108 ++++++++ .../webp/impl/libwebp/LibWebPLibrary.java | 74 ++++++ .../webp/impl/macos/MacOSFrameworks.java | 112 +++++++++ .../webp/impl/macos/MacOSImageIODecoder.java | 202 +++++++++++++++ .../webp/impl/macos/MacOSImageIOEncoder.java | 221 +++++++++++++++++ .../impl/windows/WindowsCodecsDecoder.java | 216 ++++++++++++++++ .../impl/windows/WindowsCodecsEncoder.java | 231 ++++++++++++++++++ .../webp/impl/windows/WindowsComHelper.java | 151 ++++++++++++ 24 files changed, 2244 insertions(+), 4 deletions(-) create mode 100644 webp/build.gradle create mode 100644 webp/proguard.pro create mode 100644 webp/src/main/java/module-info.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPDecoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPEncoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPLibrary.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java diff --git a/README.MD b/README.MD index 2d76fe0..64e67d1 100644 --- a/README.MD +++ b/README.MD @@ -13,6 +13,7 @@ The library is split into independent modules so you can include only what you n | **referer** | `referer` | File origin/referrer metadata (Windows & macOS) | | **favorites** | `favorites` | Finder Sidebar favorites (macOS) | | **progress** | `progress` | Taskbar/dock progress bars (macOS) | +| **webp** | `webp` | WebP image encoding/decoding (macOS, Windows, libwebp) | ## ✨ Features @@ -25,6 +26,10 @@ The library is split into independent modules so you can include only what you n * **[Taskbar Progress Bars](#4-taskbar-progress-bars)** (`progress`): Display progress indicators on the taskbar/dock icon. * Supports multiple stacked progress bars (up to 8). * *Note: Windows taskbar support is planned for future releases.* +* **[WebP Codec](#5-webp-codec)** (`webp`): Encode and decode WebP images using platform-native APIs (Java 25+, FFM API). + * **macOS:** ImageIO (CoreGraphics) + * **Windows:** WIC (Windows Imaging Component) + * **Cross-platform:** libwebp (if found on library path) * **Java 8 Compatible:** Built on Java 17 but distributed with a Java 8 compatible version (Multi-Release JAR). ## 📦 Installation @@ -43,6 +48,7 @@ dependencies { implementation "org.redlance.platformtools:referer:3.3.0" implementation "org.redlance.platformtools:favorites:3.3.0" implementation "org.redlance.platformtools:progress:3.3.0" + implementation "org.redlance.platformtools:webp:3.3.0" // For Java 8 (Downgraded version), add classifier: // implementation("org.redlance.platformtools:accent:3.3.0:java8") @@ -187,6 +193,48 @@ public class ProgressExample { } ``` +### 5. WebP Codec +Encode and decode WebP images using platform-native APIs. No bundled native libraries required — uses macOS ImageIO, Windows WIC, or system-installed libwebp. + +> **Note:** Requires Java 25+ (uses the FFM API). Other modules remain Java 8 compatible. + +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 [TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys) WebP codec (~146K). Works on any platform without native libraries as a pure Java fallback. + +```groovy +// Standard — requires platform support or libwebp on library path +implementation "org.redlance.platformtools:webp:3.3.0" + +// Bundled — self-contained, works everywhere +implementation "org.redlance.platformtools:webp:3.3.0:bundled" +``` + +```java +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; + +public class WebPExample { + public void decodeWebP(byte[] webpData) { + // Get image dimensions without full decode + int[] info = PlatformWebPDecoder.INSTANCE.getInfo(webpData); + System.out.println("Size: " + info[0] + "x" + info[1]); + + // Full decode to RGBA pixels + PlatformWebPDecoder.DecodedImage image = PlatformWebPDecoder.INSTANCE.decode(webpData); + byte[] rgba = image.rgba(); // straight alpha, width * height * 4 bytes + } + + public void encodeWebP(byte[] rgba, int width, int height) { + // Lossless encoding + byte[] lossless = PlatformWebPEncoder.INSTANCE.encodeLossless(rgba, width, height); + + // Lossy encoding (0.0 = smallest, 1.0 = best quality) + byte[] lossy = PlatformWebPEncoder.INSTANCE.encodeLossy(rgba, width, height, 0.75f); + } +} +``` + ## 🛠 System Requirements * **Java:** 8 or higher. * **Operating Systems:** diff --git a/build.gradle b/build.gradle index a89c370..9f0c00f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,15 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "com.guardsquare:proguard-gradle:7.8.2" + } +} + plugins { id "xyz.wagyourtail.jvmdowngrader" version "1.3.6" apply false + id "com.gradleup.shadow" version "9.3.2" apply false } allprojects { @@ -58,6 +68,8 @@ configure(publishedModules) { } shadeDowngradedApi { + onlyIf { !downgradeJar.state.skipped } + downgradeTo = JavaVersion.VERSION_1_8 shadePath { return "org/redlance/platformtools/" @@ -72,10 +84,6 @@ configure(publishedModules) { publishing { publications { mavenJava(MavenPublication) { - artifact(tasks.shadeDowngradedApi) { - classifier = "java8" - extension = "jar" - } groupId = "org.redlance.platformtools" artifactId = project.name @@ -111,6 +119,17 @@ configure(publishedModules) { } } + afterEvaluate { + publications.mavenJava { + if (tasks.downgradeJar.enabled) { + artifact(tasks.shadeDowngradedApi) { + classifier = "java8" + extension = "jar" + } + } + } + } + repositories { maven { name = "RedlanceMinecraft" diff --git a/settings.gradle b/settings.gradle index 0e05e5b..f7e769c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,3 +20,4 @@ include "referer" include "favorites" include "progress" include "testing" +include "webp" diff --git a/testing/build.gradle b/testing/build.gradle index 803b902..bb3a47f 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -3,4 +3,10 @@ dependencies { implementation(project(":referer")) implementation(project(":favorites")) implementation(project(":progress")) + implementation(project(":webp")) +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = "UTF-8" + options.release.set(25) } diff --git a/testing/src/main/java/module-info.java b/testing/src/main/java/module-info.java index 39b3d62..85cfe7f 100644 --- a/testing/src/main/java/module-info.java +++ b/testing/src/main/java/module-info.java @@ -3,5 +3,6 @@ requires platformtools.referer; requires platformtools.favorites; requires platformtools.progress; + requires platformtools.webp; requires java.desktop; } 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 430286a..d46bd55 100644 --- a/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java +++ b/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java @@ -4,12 +4,24 @@ import org.redlance.platformtools.favorites.PlatformFinderFavorites; import org.redlance.platformtools.progress.PlatformProgressBars; import org.redlance.platformtools.referer.PlatformFileReferer; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; +import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; +import org.redlance.platformtools.webp.impl.imageio.JavaImageIOEncoder; +import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; +import org.redlance.platformtools.webp.impl.libwebp.LibWebPEncoder; +import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; +import org.redlance.platformtools.webp.impl.macos.MacOSImageIOEncoder; +import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; +import org.redlance.platformtools.webp.impl.windows.WindowsCodecsEncoder; import javax.swing.*; import java.awt.*; import java.awt.event.*; +import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.Collection; public class TestingApp extends JFrame { @@ -41,6 +53,10 @@ public TestingApp(Color initialColor) { recreateButton.addActionListener(this::onRecreate); controlsPanel.add(recreateButton); + JButton webpButton = new JButton("WebP"); + webpButton.addActionListener(this::onWebP); + controlsPanel.add(webpButton); + JButton chooseFileButton = new JButton("Select file"); chooseFileButton.addActionListener(this::onChooseFile); controlsPanel.add(chooseFileButton); @@ -167,6 +183,10 @@ private void onRecreate(ActionEvent e) { PlatformAccent.INSTANCE.resubscribe(); } + private void onWebP(ActionEvent e) { + showWebPDialog(); + } + private void onProgressBar(ActionEvent e) { showProgressDialog(); } @@ -225,4 +245,188 @@ private void showProgressDialog() { dialog.add(addButton, BorderLayout.SOUTH); dialog.setVisible(true); } + + private void showWebPDialog() { + JDialog dialog = new JDialog(this, "WebP Test", false); + dialog.setSize(500, 400); + dialog.setLocationRelativeTo(this); + dialog.setLayout(new BorderLayout()); + + JTextArea log = new JTextArea(); + log.setEditable(false); + log.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + dialog.add(new JScrollPane(log), BorderLayout.CENTER); + + // Status + boolean decodeAvailable = PlatformWebPDecoder.INSTANCE.isAvailable(); + boolean encodeAvailable = PlatformWebPEncoder.INSTANCE.isAvailable(); + log.append("Decoder: " + (decodeAvailable + ? "available (" + PlatformWebPDecoder.INSTANCE.backendName() + ")" + : "unavailable") + "\n"); + log.append("Encoder: " + (encodeAvailable + ? "available (" + PlatformWebPEncoder.INSTANCE.backendName() + ")" + : "unavailable") + "\n\n"); + + JPanel buttons = new JPanel(new FlowLayout()); + + // Probe each backend individually + JButton probeBtn = new JButton("Probe Backends"); + probeBtn.addActionListener(event -> { + log.append("--- Probing backends ---\n"); + + try { + PlatformWebPDecoder libDec = LibWebPDecoder.tryCreate(); + PlatformWebPEncoder libEnc = LibWebPEncoder.tryCreate(); + log.append(" libwebp: decode " + (libDec != null ? "OK" : "not found") + + ", encode " + (libEnc != null ? "OK" : "not found") + "\n"); + } catch (Throwable t) { + log.append(" libwebp: ERROR " + t + "\n"); + } + + try { + MacOSImageIODecoder macosDec = MacOSImageIODecoder.create(); + log.append(" macOS ImageIO: decode OK\n"); + } catch (Throwable t) { + log.append(" macOS ImageIO decode: " + t + "\n"); + } + try { + MacOSImageIOEncoder macosEnc = MacOSImageIOEncoder.tryCreate(); + log.append(" macOS ImageIO: encode " + (macosEnc != null ? "OK" : "not available") + "\n"); + } catch (Throwable t) { + log.append(" macOS ImageIO encode: ERROR " + t + "\n"); + } + + try { + WindowsCodecsDecoder wicDec = WindowsCodecsDecoder.tryCreate(); + WindowsCodecsEncoder wicEnc = WindowsCodecsEncoder.tryCreate(); + log.append(" Windows WIC: decode " + (wicDec != null ? "OK" : "not available") + + ", encode " + (wicEnc != null ? "OK" : "not available") + "\n"); + } catch (Throwable t) { + log.append(" Windows WIC: ERROR " + t + "\n"); + } + + try { + JavaImageIODecoder iioDec = JavaImageIODecoder.tryCreate(); + JavaImageIOEncoder iioEnc = JavaImageIOEncoder.tryCreate(); + log.append(" Java ImageIO: decode " + (iioDec != null ? "OK" : "no WebP plugin") + + ", encode " + (iioEnc != null ? "OK" : "no WebP plugin") + "\n"); + } catch (Throwable t) { + log.append(" Java ImageIO: ERROR " + t + "\n"); + } + + log.append("\n"); + }); + buttons.add(probeBtn); + + // Encode test + JButton encodeBtn = new JButton("Encode Test"); + encodeBtn.setEnabled(encodeAvailable); + encodeBtn.addActionListener(event -> { + int w = 256, h = 256; + byte[] rgba = new byte[w * h * 4]; + java.util.Random rng = new java.util.Random(42); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int i = (y * w + x) * 4; + rgba[i] = (byte) Math.min(255, (x + rng.nextInt(32)) & 0xFF); + rgba[i + 1] = (byte) Math.min(255, (y + rng.nextInt(32)) & 0xFF); + rgba[i + 2] = (byte) (rng.nextInt(256)); + rgba[i + 3] = (byte) 255; + } + } + + log.append("Encode 256x256 noisy gradient:\n"); + log.append(" Raw RGBA: " + rgba.length + " bytes\n"); + + byte[] lossless = null; + try { + lossless = PlatformWebPEncoder.INSTANCE.encodeLossless(rgba, w, h); + log.append(" Lossless: " + lossless.length + " bytes\n"); + } catch (Exception ex) { + log.append(" Lossless: FAILED: " + ex.getMessage() + "\n"); + } + + try { + byte[] lossy75 = PlatformWebPEncoder.INSTANCE.encodeLossy(rgba, w, h, 0.75f); + log.append(" Lossy 75%%: " + lossy75.length + " bytes\n"); + } catch (Exception ex) { + log.append(" Lossy 75%%: FAILED: " + ex.getMessage() + "\n"); + } + + try { + byte[] lossy50 = PlatformWebPEncoder.INSTANCE.encodeLossy(rgba, w, h, 0.50f); + log.append(" Lossy 50%%: " + lossy50.length + " bytes\n"); + } catch (Exception ex) { + log.append(" Lossy 50%%: FAILED: " + ex.getMessage() + "\n"); + } + + // Roundtrip test + if (lossless != null && decodeAvailable) { + try { + PlatformWebPDecoder.DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(lossless); + log.append(" Roundtrip: " + decoded.width() + "x" + decoded.height() + + " (" + decoded.rgba().length + " bytes RGBA)\n"); + } catch (Exception ex) { + log.append(" Roundtrip: FAILED: " + ex.getMessage() + "\n"); + } + } + log.append("\n"); + }); + buttons.add(encodeBtn); + + // Decode file + JButton decodeBtn = new JButton("Decode File"); + decodeBtn.setEnabled(decodeAvailable); + decodeBtn.addActionListener(event -> { + JFileChooser fc = new JFileChooser(); + fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("WebP files", "webp")); + if (fc.showOpenDialog(dialog) != JFileChooser.APPROVE_OPTION) return; + + try { + byte[] webpData = Files.readAllBytes(fc.getSelectedFile().toPath()); + log.append("File: " + fc.getSelectedFile().getName() + " (" + webpData.length + " bytes)\n"); + + try { + int[] info = PlatformWebPDecoder.INSTANCE.getInfo(webpData); + log.append(" Info: " + info[0] + "x" + info[1] + "\n"); + } catch (Exception ex) { + log.append(" Info: FAILED: " + ex.getMessage() + "\n"); + } + + try { + PlatformWebPDecoder.DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(webpData); + log.append(" Decoded: " + decoded.width() + "x" + decoded.height() + "\n"); + + // Show decoded image + BufferedImage img = new BufferedImage(decoded.width(), decoded.height(), BufferedImage.TYPE_INT_ARGB); + byte[] pixels = decoded.rgba(); + for (int y = 0; y < decoded.height(); y++) { + for (int x = 0; x < decoded.width(); x++) { + int idx = (y * decoded.width() + x) * 4; + int r = pixels[idx] & 0xFF; + int g = pixels[idx + 1] & 0xFF; + int b = pixels[idx + 2] & 0xFF; + int a = pixels[idx + 3] & 0xFF; + img.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b); + } + } + + JDialog preview = new JDialog(dialog, "Preview: " + fc.getSelectedFile().getName(), false); + preview.add(new JLabel(new ImageIcon(img))); + preview.pack(); + preview.setLocationRelativeTo(dialog); + preview.setVisible(true); + } catch (Exception ex) { + log.append(" Decode: FAILED: " + ex.getMessage() + "\n"); + } + } catch (IOException ex) { + log.append(" Error: " + ex.getMessage() + "\n"); + } + log.append("\n"); + }); + buttons.add(decodeBtn); + + dialog.add(buttons, BorderLayout.SOUTH); + dialog.setVisible(true); + } } diff --git a/webp/build.gradle b/webp/build.gradle new file mode 100644 index 0000000..49417c5 --- /dev/null +++ b/webp/build.gradle @@ -0,0 +1,66 @@ +import proguard.gradle.ProGuardTask + +plugins { + id "com.gradleup.shadow" +} + +configurations { + bundled + implementation.extendsFrom(bundled) +} + +dependencies { + // api(project(":common")) + + bundled("com.twelvemonkeys.imageio:imageio-webp:3.13.1") +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = "UTF-8" + options.release.set(25) +} + +shadowJar { + archiveClassifier.set("shadow") + configurations = [project.configurations.bundled] + + relocate("com.twelvemonkeys", "org.redlance.platformtools.webp.shaded.twelvemonkeys") + mergeServiceFiles() + exclude("META-INF/maven/**") + exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi") +} + +tasks.register("proguardJar", ProGuardTask) { + dependsOn shadowJar + + injars(shadowJar.archiveFile) + outjars(layout.buildDirectory.file("libs/${base.archivesName.get()}-${version}-bundled.jar")) + libraryjars("${System.getProperty('java.home')}/jmods") + + // Keep all our classes, only shrink shaded dependencies + keep("class !org.redlance.platformtools.webp.shaded.**, org.redlance.** { *; }") + + // Keep ImageIO SPI entry points (loaded via ServiceLoader) + keep("class * extends javax.imageio.spi.ImageReaderSpi { *; }") + keep("class * extends javax.imageio.spi.ImageWriterSpi { *; }") + + dontnote() + ignorewarnings() + dontobfuscate() + optimizationpasses(10) + configuration(file("proguard.pro")) +} + +assemble.dependsOn proguardJar +downgradeJar.enabled = false + +shadow { + addShadowVariantIntoJavaComponent = false +} + +publishing.publications.named("mavenJava") { + artifact(proguardJar.getOutJarFileCollection().singleFile) { + classifier = "bundled" + builtBy(tasks.named("proguardJar")) + } +} diff --git a/webp/proguard.pro b/webp/proguard.pro new file mode 100644 index 0000000..bafe5a8 --- /dev/null +++ b/webp/proguard.pro @@ -0,0 +1,2 @@ +-optimizeaggressively +-allowaccessmodification \ No newline at end of file diff --git a/webp/src/main/java/module-info.java b/webp/src/main/java/module-info.java new file mode 100644 index 0000000..3e3a22b --- /dev/null +++ b/webp/src/main/java/module-info.java @@ -0,0 +1,12 @@ +module platformtools.webp { + exports org.redlance.platformtools.webp.decoder; + exports org.redlance.platformtools.webp.encoder; + + exports org.redlance.platformtools.webp.impl.libwebp to platformtools.testing; + exports org.redlance.platformtools.webp.impl.macos to platformtools.testing; + exports org.redlance.platformtools.webp.impl.windows to platformtools.testing; + exports org.redlance.platformtools.webp.impl.imageio to platformtools.testing; + + requires java.desktop; + requires static org.jetbrains.annotations; +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java new file mode 100644 index 0000000..524621c --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java @@ -0,0 +1,60 @@ +package org.redlance.platformtools.webp.decoder; + +import org.redlance.platformtools.webp.impl.PlatformWebPDecoderImpl; + +/** + * Platform-native WebP image decoder. + * + *

Automatically selects the best available backend: + *

+ * + *

All methods throw if no backend is available; check {@link #isAvailable()} first + * or handle {@link UnsupportedOperationException}. + */ +@SuppressWarnings("unused") // API +public interface PlatformWebPDecoder { + PlatformWebPDecoder INSTANCE = new PlatformWebPDecoderImpl(); + + /** + * Decoded image with raw pixel data. + * + * @param rgba pixel data in RGBA order, straight (non-premultiplied) alpha, + * length is always {@code width * height * 4} + * @param width image width in pixels + * @param height image height in pixels + */ + record DecodedImage(byte[] rgba, int width, int height) {} + + /** + * Returns the name of the active backend (e.g. "libwebp", "macOS ImageIO", "Windows WIC"). + * + * @throws UnsupportedOperationException if no backend is available + */ + String backendName(); + + /** + * Decodes a WebP image into raw RGBA pixels. + * + * @param webpData raw WebP file bytes + * @return decoded image with RGBA pixel data + * @throws IllegalStateException if the data is invalid or decoding fails + * @throws UnsupportedOperationException if no backend is available + */ + DecodedImage decode(byte[] webpData); + + /** + * Returns the dimensions of a WebP image without fully decoding it. + * + * @param webpData raw WebP file bytes + * @return {@code int[]{width, height}} + * @throws IllegalStateException if the data is invalid + * @throws UnsupportedOperationException if no backend is available + */ + int[] getInfo(byte[] webpData); + + boolean isAvailable(); +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java new file mode 100644 index 0000000..01aecf7 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java @@ -0,0 +1,57 @@ +package org.redlance.platformtools.webp.encoder; + +import org.redlance.platformtools.webp.impl.PlatformWebPEncoderImpl; + +/** + * Platform-native WebP image encoder. + * + *

Automatically selects the best available backend: + *

+ * + *

All methods throw if no backend is available; check {@link #isAvailable()} first + * or handle {@link UnsupportedOperationException}. + */ +@SuppressWarnings("unused") // API +public interface PlatformWebPEncoder { + PlatformWebPEncoder INSTANCE = new PlatformWebPEncoderImpl(); + + /** + * Returns the name of the active backend (e.g. "libwebp", "macOS ImageIO", "Windows WIC"). + * + * @throws UnsupportedOperationException if no backend is available + */ + String backendName(); + + /** + * Encodes raw RGBA pixels into a lossless WebP image. + * + * @param rgba pixel data in RGBA order, straight (non-premultiplied) alpha, + * length must be {@code width * height * 4} + * @param width image width in pixels + * @param height image height in pixels + * @return WebP file bytes + * @throws IllegalStateException if encoding fails + * @throws UnsupportedOperationException if no backend is available + */ + byte[] encodeLossless(byte[] rgba, int width, int height); + + /** + * Encodes raw RGBA pixels into a lossy WebP image. + * + * @param rgba pixel data in RGBA order, straight (non-premultiplied) alpha, + * length must be {@code width * height * 4} + * @param width image width in pixels + * @param height image height in pixels + * @param quality compression quality, {@code 0.0f} (smallest) to {@code 1.0f} (best) + * @return WebP file bytes + * @throws IllegalStateException if encoding fails + * @throws UnsupportedOperationException if no backend is available + */ + byte[] encodeLossy(byte[] rgba, int width, int height, float quality); + + boolean isAvailable(); +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java new file mode 100644 index 0000000..e22f775 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java @@ -0,0 +1,72 @@ +package org.redlance.platformtools.webp.impl; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; +import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; +import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; +import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; + +public final class PlatformWebPDecoderImpl implements PlatformWebPDecoder { + private final @Nullable PlatformWebPDecoder delegate = createBackend(); + + private PlatformWebPDecoder requireDelegate() { + if (this.delegate == null) throw new UnsupportedOperationException("No WebP decoder backend available"); + return this.delegate; + } + + @Override + public String backendName() { + return requireDelegate().backendName(); + } + + @Override + public DecodedImage decode(byte[] webpData) { + return requireDelegate().decode(webpData); + } + + @Override + public int[] getInfo(byte[] webpData) { + return requireDelegate().getInfo(webpData); + } + + @Override + public boolean isAvailable() { + return this.delegate != null && this.delegate.isAvailable(); + } + + private static @Nullable PlatformWebPDecoder createBackend() { + // 1. libwebp (all platforms) + try { + PlatformWebPDecoder backend = LibWebPDecoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + + // 2. System frameworks + String os = System.getProperty("os.name", "").toLowerCase(); + + if (os.contains("mac")) { + try { + PlatformWebPDecoder backend = MacOSImageIODecoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + } else if (os.contains("win")) { + try { + PlatformWebPDecoder backend = WindowsCodecsDecoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + } + + // 3. Java ImageIO fallback (requires a WebP ImageIO plugin on classpath) + try { + PlatformWebPDecoder backend = JavaImageIODecoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + + return null; + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java new file mode 100644 index 0000000..a0d2415 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java @@ -0,0 +1,72 @@ +package org.redlance.platformtools.webp.impl; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; +import org.redlance.platformtools.webp.impl.imageio.JavaImageIOEncoder; +import org.redlance.platformtools.webp.impl.libwebp.LibWebPEncoder; +import org.redlance.platformtools.webp.impl.macos.MacOSImageIOEncoder; +import org.redlance.platformtools.webp.impl.windows.WindowsCodecsEncoder; + +public class PlatformWebPEncoderImpl implements PlatformWebPEncoder { + private final @Nullable PlatformWebPEncoder delegate = createBackend(); + + private PlatformWebPEncoder requireDelegate() { + if (this.delegate == null) throw new UnsupportedOperationException("No WebP encoder backend available"); + return this.delegate; + } + + @Override + public String backendName() { + return requireDelegate().backendName(); + } + + @Override + public byte[] encodeLossless(byte[] rgba, int width, int height) { + return requireDelegate().encodeLossless(rgba, width, height); + } + + @Override + public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + return requireDelegate().encodeLossy(rgba, width, height, quality); + } + + @Override + public boolean isAvailable() { + return this.delegate != null && this.delegate.isAvailable(); + } + + private static @Nullable PlatformWebPEncoder createBackend() { + // 1. libwebp (all platforms) + try { + PlatformWebPEncoder backend = LibWebPEncoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + + // 2. System frameworks + String os = System.getProperty("os.name", "").toLowerCase(); + + if (os.contains("mac")) { + try { + PlatformWebPEncoder backend = MacOSImageIOEncoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + } else if (os.contains("win")) { + try { + PlatformWebPEncoder backend = WindowsCodecsEncoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + } + + // 3. Java ImageIO fallback (requires a WebP ImageIO plugin on classpath) + try { + PlatformWebPEncoder backend = JavaImageIOEncoder.tryCreate(); + if (backend != null) return backend; + } catch (Throwable ignored) { + } + + return null; + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java new file mode 100644 index 0000000..43cea7e --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java @@ -0,0 +1,105 @@ +package org.redlance.platformtools.webp.impl.imageio; + +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Iterator; + +/** + * Fallback WebP decoder using {@link javax.imageio.ImageIO}. + * + *

Works when a WebP ImageIO plugin is available on the classpath + * (e.g. TwelveMonkeys, or a JRE with built-in WebP support). + */ +public final class JavaImageIODecoder implements PlatformWebPDecoder { + private JavaImageIODecoder() { + } + + public static JavaImageIODecoder tryCreate() { + Iterator readers = ImageIO.getImageReadersByMIMEType("image/webp"); + return readers.hasNext() ? new JavaImageIODecoder() : null; + } + + @Override + public String backendName() { + return "Java ImageIO"; + } + + @Override + public DecodedImage decode(byte[] webpData) { + try { + BufferedImage img = ImageIO.read(new ByteArrayInputStream(webpData)); + if (img == null) { + throw new IllegalStateException("ImageIO returned null: unsupported or invalid WebP data"); + } + + int w = img.getWidth(); + int h = img.getHeight(); + byte[] rgba = toRGBA(img, w, h); + + return new DecodedImage(rgba, w, h); + } catch (IOException e) { + throw new RuntimeException("Java ImageIO decode failed", e); + } + } + + @Override + public int[] getInfo(byte[] webpData) { + try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(webpData))) { + Iterator readers = ImageIO.getImageReaders(iis); + if (!readers.hasNext()) { + throw new IllegalStateException("No ImageIO reader for WebP data"); + } + + ImageReader reader = readers.next(); + try { + reader.setInput(iis); + return new int[]{reader.getWidth(0), reader.getHeight(0)}; + } finally { + reader.dispose(); + } + } catch (IOException e) { + throw new RuntimeException("Java ImageIO getInfo failed", e); + } + } + + @Override + public boolean isAvailable() { + return true; + } + + private static byte[] toRGBA(BufferedImage img, int w, int h) { + if (img.getType() == BufferedImage.TYPE_4BYTE_ABGR) { + byte[] abgr = ((DataBufferByte) img.getRaster().getDataBuffer()).getData(); + byte[] rgba = new byte[w * h * 4]; + for (int i = 0; i < w * h; i++) { + int si = i * 4; + rgba[si] = abgr[si + 3]; // R + rgba[si + 1] = abgr[si + 2]; // G + rgba[si + 2] = abgr[si + 1]; // B + rgba[si + 3] = abgr[si]; // A + } + return rgba; + } + + // Generic path: convert any type via getRGB + byte[] rgba = new byte[w * h * 4]; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int argb = img.getRGB(x, y); + int i = (y * w + x) * 4; + rgba[i] = (byte) ((argb >> 16) & 0xFF); // R + rgba[i + 1] = (byte) ((argb >> 8) & 0xFF); // G + rgba[i + 2] = (byte) (argb & 0xFF); // B + rgba[i + 3] = (byte) ((argb >> 24) & 0xFF); // A + } + } + return rgba; + } +} \ No newline at end of file diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java new file mode 100644 index 0000000..e666106 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java @@ -0,0 +1,91 @@ +package org.redlance.platformtools.webp.impl.imageio; + +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Iterator; + +/** + * Fallback WebP encoder using {@link javax.imageio.ImageIO}. + * + *

Works when a WebP ImageIO plugin is available on the classpath + * (e.g. TwelveMonkeys, or a JRE with built-in WebP support). + */ +public final class JavaImageIOEncoder implements PlatformWebPEncoder { + private JavaImageIOEncoder() { + } + + public static JavaImageIOEncoder tryCreate() { + Iterator writers = ImageIO.getImageWritersByMIMEType("image/webp"); + return writers.hasNext() ? new JavaImageIOEncoder() : null; + } + + @Override + public String backendName() { + return "Java ImageIO"; + } + + @Override + public byte[] encodeLossless(byte[] rgba, int width, int height) { + return encode(rgba, width, height, null); + } + + @Override + public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + return encode(rgba, width, height, quality); + } + + @Override + public boolean isAvailable() { + return true; + } + + private byte[] encode(byte[] rgba, int width, int height, Float quality) { + BufferedImage img = fromRGBA(rgba, width, height); + + Iterator writers = ImageIO.getImageWritersByMIMEType("image/webp"); + if (!writers.hasNext()) { + throw new IllegalStateException("No ImageIO writer for WebP"); + } + + ImageWriter writer = writers.next(); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writer.setOutput(ImageIO.createImageOutputStream(baos)); + + ImageWriteParam param = writer.getDefaultWriteParam(); + if (quality != null && param.canWriteCompressed()) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(param.getCompressionTypes()[0]); + param.setCompressionQuality(quality); + } + + writer.write(null, new IIOImage(img, null, null), param); + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Java ImageIO encode failed", e); + } finally { + writer.dispose(); + } + } + + private static BufferedImage fromRGBA(byte[] rgba, int width, int height) { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); + byte[] abgr = ((DataBufferByte) img.getRaster().getDataBuffer()).getData(); + for (int i = 0; i < width * height; i++) { + int si = i * 4; + abgr[si] = rgba[si + 3]; // A + abgr[si + 1] = rgba[si + 2]; // B + abgr[si + 2] = rgba[si + 1]; // G + abgr[si + 3] = rgba[si]; // R + } + return img; + } +} \ 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 new file mode 100644 index 0000000..23a73a2 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPDecoder.java @@ -0,0 +1,109 @@ +package org.redlance.platformtools.webp.impl.libwebp; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +public final class LibWebPDecoder implements PlatformWebPDecoder { + private final LibWebPLibrary lib; + + // uint8_t* WebPDecodeRGBA(const uint8_t* data, size_t size, int* w, int* h) + private final MethodHandle webPDecodeRGBA; + // int WebPGetInfo(const uint8_t* data, size_t size, int* w, int* h) + private final MethodHandle webPGetInfo; + + private LibWebPDecoder(LibWebPLibrary lib) { + this.lib = lib; + + Linker linker = Linker.nativeLinker(); + + this.webPDecodeRGBA = linker.downcallHandle( + lib.lookup.find("WebPDecodeRGBA").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + this.webPGetInfo = linker.downcallHandle( + lib.lookup.find("WebPGetInfo").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + } + + public static @Nullable LibWebPDecoder tryCreate() { + LibWebPLibrary lib = LibWebPLibrary.getInstance(); + return lib != null ? new LibWebPDecoder(lib) : null; + } + + @Override + public String backendName() { + return "libwebp"; + } + + @Override + public DecodedImage decode(byte[] webpData) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment wPtr = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment hPtr = arena.allocate(ValueLayout.JAVA_INT); + + MemorySegment dataSeg = arena.allocate(webpData.length); + dataSeg.copyFrom(MemorySegment.ofArray(webpData)); + + MemorySegment result = (MemorySegment) this.webPDecodeRGBA.invokeExact( + dataSeg, (long) webpData.length, wPtr, hPtr + ); + if (result.equals(MemorySegment.NULL)) { + throw new IllegalStateException("WebPDecodeRGBA failed: invalid or unsupported WebP data"); + } + + int w = wPtr.get(ValueLayout.JAVA_INT, 0); + int h = hPtr.get(ValueLayout.JAVA_INT, 0); + + byte[] pixels = result.reinterpret((long) w * h * 4).toArray(ValueLayout.JAVA_BYTE); + this.lib.webPFree.invokeExact(result); + + return new DecodedImage(pixels, w, h); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("WebP decode failed", t); + } + } + + @Override + public int[] getInfo(byte[] webpData) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment wPtr = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment hPtr = arena.allocate(ValueLayout.JAVA_INT); + + MemorySegment dataSeg = arena.allocate(webpData.length); + dataSeg.copyFrom(MemorySegment.ofArray(webpData)); + + int ok = (int) this.webPGetInfo.invokeExact( + dataSeg, (long) webpData.length, wPtr, hPtr + ); + if (ok == 0) { + throw new IllegalStateException("WebPGetInfo failed: invalid or unsupported WebP data"); + } + + return new int[] { + wPtr.get(ValueLayout.JAVA_INT, 0), + hPtr.get(ValueLayout.JAVA_INT, 0) + }; + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("WebP getInfo failed", t); + } + } + + @Override + public boolean isAvailable() { + return true; + } +} 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 new file mode 100644 index 0000000..3331bbb --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPEncoder.java @@ -0,0 +1,108 @@ +package org.redlance.platformtools.webp.impl.libwebp; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +public final class LibWebPEncoder implements PlatformWebPEncoder { + private final LibWebPLibrary lib; + + // size_t WebPEncodeLosslessRGBA(const uint8_t* rgba, int w, int h, int stride, uint8_t** output) + private final MethodHandle webPEncodeLosslessRGBA; + // size_t WebPEncodeRGBA(const uint8_t* rgba, int w, int h, int stride, float quality, uint8_t** output) + private final MethodHandle webPEncodeRGBA; + + private LibWebPEncoder(LibWebPLibrary lib) { + this.lib = lib; + + Linker linker = Linker.nativeLinker(); + + this.webPEncodeLosslessRGBA = linker.downcallHandle( + lib.lookup.find("WebPEncodeLosslessRGBA").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, + ValueLayout.ADDRESS + ) + ); + this.webPEncodeRGBA = linker.downcallHandle( + lib.lookup.find("WebPEncodeRGBA").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, + ValueLayout.JAVA_FLOAT, ValueLayout.ADDRESS + ) + ); + } + + public static @Nullable LibWebPEncoder tryCreate() { + LibWebPLibrary lib = LibWebPLibrary.getInstance(); + return lib != null ? new LibWebPEncoder(lib) : null; + } + + @Override + public String backendName() { + return "libwebp"; + } + + @Override + public byte[] encodeLossless(byte[] rgba, int width, int height) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment outputPtr = arena.allocate(ValueLayout.ADDRESS); + + MemorySegment rgbaSeg = arena.allocate(rgba.length); + rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + + long size = (long) this.webPEncodeLosslessRGBA.invokeExact( + rgbaSeg, width, height, width * 4, outputPtr + ); + if (size == 0) { + throw new IllegalStateException("WebPEncodeLosslessRGBA failed"); + } + + MemorySegment encoded = outputPtr.get(ValueLayout.ADDRESS, 0).reinterpret(size); + byte[] result = encoded.toArray(ValueLayout.JAVA_BYTE); + + this.lib.webPFree.invokeExact(outputPtr.get(ValueLayout.ADDRESS, 0)); + return result; + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("WebP lossless encode failed", t); + } + } + + @Override + public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment outputPtr = arena.allocate(ValueLayout.ADDRESS); + + MemorySegment rgbaSeg = arena.allocate(rgba.length); + rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + + long size = (long) this.webPEncodeRGBA.invokeExact( + rgbaSeg, width, height, width * 4, quality * 100.0f, outputPtr + ); + if (size == 0) { + throw new IllegalStateException("WebPEncodeRGBA failed"); + } + + MemorySegment encoded = outputPtr.get(ValueLayout.ADDRESS, 0).reinterpret(size); + byte[] result = encoded.toArray(ValueLayout.JAVA_BYTE); + + this.lib.webPFree.invokeExact(outputPtr.get(ValueLayout.ADDRESS, 0)); + return result; + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("WebP lossy encode failed", t); + } + } + + @Override + public boolean isAvailable() { + return true; + } +} 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 new file mode 100644 index 0000000..26dc574 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/libwebp/LibWebPLibrary.java @@ -0,0 +1,74 @@ +package org.redlance.platformtools.webp.impl.libwebp; + +import org.jetbrains.annotations.Nullable; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.nio.file.Path; + +public final class LibWebPLibrary { + final SymbolLookup lookup; + final MethodHandle webPFree; + + private LibWebPLibrary(SymbolLookup lookup) { + this.lookup = lookup; + + this.webPFree = Linker.nativeLinker().downcallHandle( + lookup.find("WebPFree").orElseThrow(), + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) + ); + } + + private static class Holder { + static final @Nullable LibWebPLibrary INSTANCE = tryLoad(); + } + + public static @Nullable LibWebPLibrary getInstance() { + return Holder.INSTANCE; + } + + private static @Nullable LibWebPLibrary tryLoad() { + for (String name : new String[]{"webp", "libwebp", "libwebp.so.7"}) { + try { + return new LibWebPLibrary(SymbolLookup.libraryLookup(name, Arena.global())); + } catch (Throwable ignored) { + } + } + + String os = System.getProperty("os.name", "").toLowerCase(); + String[] paths; + + if (os.contains("mac")) { + paths = new String[]{ + "/opt/homebrew/lib/libwebp.dylib", + "/usr/local/lib/libwebp.dylib", + "/opt/homebrew/lib/libwebp.7.dylib", + "/usr/local/lib/libwebp.7.dylib" + }; + } else if (os.contains("win")) { + String programFiles = System.getenv("ProgramFiles"); + String localAppData = System.getenv("LOCALAPPDATA"); + paths = new String[]{ + "libwebp.dll", + programFiles + "\\libwebp\\bin\\libwebp.dll", + localAppData + "\\libwebp\\bin\\libwebp.dll" + }; + } else { + paths = new String[]{ + "/usr/lib/libwebp.so", + "/usr/lib/x86_64-linux-gnu/libwebp.so", + "/usr/lib/aarch64-linux-gnu/libwebp.so", + "/usr/local/lib/libwebp.so" + }; + } + + for (String path : paths) { + try { + return new LibWebPLibrary(SymbolLookup.libraryLookup(Path.of(path), Arena.global())); + } catch (Throwable ignored) { + } + } + + return null; + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java new file mode 100644 index 0000000..9686508 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java @@ -0,0 +1,112 @@ +package org.redlance.platformtools.webp.impl.macos; + +import org.jetbrains.annotations.Nullable; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +public final class MacOSFrameworks { + static final int kCFStringEncodingUTF8 = 0x08000100; + static final int kCGImageAlphaLast = 3; + static final int kCGImageAlphaPremultipliedLast = 1; + static final int kCFNumberFloat64Type = 13; + + final SymbolLookup lookup; + final SymbolLookup imageIO; + + final MethodHandle cfRelease; + final MethodHandle cgColorSpaceCreateDeviceRGB; + final MethodHandle cgColorSpaceRelease; + final MethodHandle cgImageRelease; + private final MethodHandle cfStringCreateWithCString; + + final MethodHandle cfDataCreateMutable; + final MethodHandle cgImageDestCreateWithData; + + private MacOSFrameworks(SymbolLookup combined, SymbolLookup imageIO) { + this.lookup = combined; + this.imageIO = imageIO; + + Linker linker = Linker.nativeLinker(); + + this.cfRelease = linker.downcallHandle( + combined.find("CFRelease").orElseThrow(), + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) + ); + this.cfStringCreateWithCString = linker.downcallHandle( + combined.find("CFStringCreateWithCString").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT + ) + ); + this.cgColorSpaceCreateDeviceRGB = linker.downcallHandle( + combined.find("CGColorSpaceCreateDeviceRGB").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS) + ); + this.cgColorSpaceRelease = linker.downcallHandle( + combined.find("CGColorSpaceRelease").orElseThrow(), + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) + ); + this.cgImageRelease = linker.downcallHandle( + combined.find("CGImageRelease").orElseThrow(), + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) + ); + + this.cfDataCreateMutable = linker.downcallHandle( + combined.find("CFDataCreateMutable").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG + ) + ); + this.cgImageDestCreateWithData = linker.downcallHandle( + imageIO.find("CGImageDestinationCreateWithData").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS + ) + ); + } + + private static class Holder { + static final @Nullable MacOSFrameworks INSTANCE = tryCreate(); + } + + public static @Nullable MacOSFrameworks getInstance() { + return Holder.INSTANCE; + } + + private static @Nullable MacOSFrameworks tryCreate() { + try { + return create(); + } catch (Throwable t) { + return null; + } + } + + public static MacOSFrameworks create() { + SymbolLookup defaultLookup = Linker.nativeLinker().defaultLookup(); + SymbolLookup io = SymbolLookup.libraryLookup( + "/System/Library/Frameworks/ImageIO.framework/ImageIO", Arena.global() + ); + SymbolLookup combined = name -> io.find(name).or(() -> defaultLookup.find(name)); + return new MacOSFrameworks(combined, io); + } + + MemorySegment createCFString(Arena arena, String s) throws Throwable { + return (MemorySegment) cfStringCreateWithCString.invokeExact( + MemorySegment.NULL, arena.allocateFrom(s), kCFStringEncodingUTF8 + ); + } + + static void unpremultiplyAlpha(byte[] rgba) { + for (int i = 0; i < rgba.length; i += 4) { + int a = rgba[i + 3] & 0xFF; + if (a == 0) { + rgba[i] = rgba[i + 1] = rgba[i + 2] = 0; + } else if (a < 255) { + rgba[i] = (byte) Math.min(255, ((rgba[i] & 0xFF) * 255 + a / 2) / a); + rgba[i + 1] = (byte) Math.min(255, ((rgba[i + 1] & 0xFF) * 255 + a / 2) / a); + rgba[i + 2] = (byte) Math.min(255, ((rgba[i + 2] & 0xFF) * 255 + a / 2) / a); + } + } + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java new file mode 100644 index 0000000..2db566d --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java @@ -0,0 +1,202 @@ +package org.redlance.platformtools.webp.impl.macos; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +public final class MacOSImageIODecoder implements PlatformWebPDecoder { + private final MacOSFrameworks fw; + + private final MethodHandle cfDataCreate; + private final MethodHandle cgImageSourceCreateWithData; + private final MethodHandle cgImageSourceCreateImageAtIndex; + private final MethodHandle cgImageGetWidth; + private final MethodHandle cgImageGetHeight; + private final MethodHandle cgBitmapContextCreate; + private final MethodHandle cgBitmapContextGetData; + private final MethodHandle cgContextDrawImage; + private final MethodHandle cgContextRelease; + + private MacOSImageIODecoder(MacOSFrameworks fw) { + this.fw = fw; + + Linker linker = Linker.nativeLinker(); + + this.cfDataCreate = linker.downcallHandle( + fw.lookup.find("CFDataCreate").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG + ) + ); + this.cgImageSourceCreateWithData = linker.downcallHandle( + fw.imageIO.find("CGImageSourceCreateWithData").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + this.cgImageSourceCreateImageAtIndex = linker.downcallHandle( + fw.imageIO.find("CGImageSourceCreateImageAtIndex").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS + ) + ); + this.cgImageGetWidth = linker.downcallHandle( + fw.lookup.find("CGImageGetWidth").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) + ); + this.cgImageGetHeight = linker.downcallHandle( + fw.lookup.find("CGImageGetHeight").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) + ); + this.cgBitmapContextCreate = linker.downcallHandle( + fw.lookup.find("CGBitmapContextCreate").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, + ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.JAVA_INT + ) + ); + this.cgBitmapContextGetData = linker.downcallHandle( + fw.lookup.find("CGBitmapContextGetData").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS) + ); + this.cgContextDrawImage = linker.downcallHandle( + fw.lookup.find("CGContextDrawImage").orElseThrow(), + FunctionDescriptor.ofVoid( + ValueLayout.ADDRESS, + ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE, + ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE, + ValueLayout.ADDRESS + ) + ); + this.cgContextRelease = linker.downcallHandle( + fw.lookup.find("CGContextRelease").orElseThrow(), + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) + ); + } + + public static @Nullable MacOSImageIODecoder tryCreate() { + MacOSFrameworks fw = MacOSFrameworks.getInstance(); + return fw != null ? new MacOSImageIODecoder(fw) : null; + } + + public static MacOSImageIODecoder create() throws Throwable { + return new MacOSImageIODecoder(MacOSFrameworks.create()); + } + + @Override + public String backendName() { + return "macOS ImageIO"; + } + + @Override + public DecodedImage decode(byte[] webpData) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment webpSeg = arena.allocate(webpData.length); + webpSeg.copyFrom(MemorySegment.ofArray(webpData)); + + MemorySegment cfData = (MemorySegment) this.cfDataCreate.invokeExact( + MemorySegment.NULL, webpSeg, (long) webpData.length + ); + + MemorySegment source = (MemorySegment) this.cgImageSourceCreateWithData.invokeExact(cfData, MemorySegment.NULL); + if (source.address() == 0) { + this.fw.cfRelease.invokeExact(cfData); + throw new IllegalStateException("CGImageSourceCreateWithData failed"); + } + + MemorySegment cgImage = (MemorySegment) this.cgImageSourceCreateImageAtIndex.invokeExact( + source, 0L, MemorySegment.NULL + ); + if (cgImage.address() == 0) { + this.fw.cfRelease.invokeExact(source); + this.fw.cfRelease.invokeExact(cfData); + throw new IllegalStateException("CGImageSourceCreateImageAtIndex failed"); + } + + long w = (long) this.cgImageGetWidth.invokeExact(cgImage); + long h = (long) this.cgImageGetHeight.invokeExact(cgImage); + + MemorySegment colorSpace = (MemorySegment) this.fw.cgColorSpaceCreateDeviceRGB.invokeExact(); + MemorySegment ctx = (MemorySegment) this.cgBitmapContextCreate.invokeExact( + MemorySegment.NULL, w, h, 8L, w * 4, colorSpace, MacOSFrameworks.kCGImageAlphaPremultipliedLast + ); + + if (ctx.address() == 0) { + this.fw.cgColorSpaceRelease.invokeExact(colorSpace); + this.fw.cgImageRelease.invokeExact(cgImage); + this.fw.cfRelease.invokeExact(source); + this.fw.cfRelease.invokeExact(cfData); + throw new IllegalStateException("CGBitmapContextCreate failed"); + } + + this.cgContextDrawImage.invokeExact(ctx, 0.0, 0.0, (double) w, (double) h, cgImage); + + MemorySegment pixelPtr = (MemorySegment) this.cgBitmapContextGetData.invokeExact(ctx); + byte[] rgbaOut = pixelPtr.reinterpret(w * h * 4).toArray(ValueLayout.JAVA_BYTE); + + MacOSFrameworks.unpremultiplyAlpha(rgbaOut); + + this.cgContextRelease.invokeExact(ctx); + this.fw.cgColorSpaceRelease.invokeExact(colorSpace); + this.fw.cgImageRelease.invokeExact(cgImage); + this.fw.cfRelease.invokeExact(source); + this.fw.cfRelease.invokeExact(cfData); + + return new DecodedImage(rgbaOut, (int) w, (int) h); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("macOS ImageIO decode failed", t); + } + } + + @Override + public int[] getInfo(byte[] webpData) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment webpSeg = arena.allocate(webpData.length); + webpSeg.copyFrom(MemorySegment.ofArray(webpData)); + + MemorySegment cfData = (MemorySegment) this.cfDataCreate.invokeExact( + MemorySegment.NULL, webpSeg, (long) webpData.length + ); + + MemorySegment source = (MemorySegment) this.cgImageSourceCreateWithData.invokeExact( + cfData, MemorySegment.NULL + ); + if (source.address() == 0) { + this.fw.cfRelease.invokeExact(cfData); + throw new IllegalStateException("CGImageSourceCreateWithData failed"); + } + + MemorySegment cgImage = (MemorySegment) this.cgImageSourceCreateImageAtIndex.invokeExact( + source, 0L, MemorySegment.NULL + ); + if (cgImage.address() == 0) { + this.fw.cfRelease.invokeExact(source); + this.fw.cfRelease.invokeExact(cfData); + throw new IllegalStateException("CGImageSourceCreateImageAtIndex failed"); + } + + long w = (long) this.cgImageGetWidth.invokeExact(cgImage); + long h = (long) this.cgImageGetHeight.invokeExact(cgImage); + + this.fw.cgImageRelease.invokeExact(cgImage); + this.fw.cfRelease.invokeExact(source); + this.fw.cfRelease.invokeExact(cfData); + + return new int[] {(int) w, (int) h}; + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("macOS ImageIO getInfo failed", t); + } + } + + @Override + public boolean isAvailable() { + return true; + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java new file mode 100644 index 0000000..0336f45 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java @@ -0,0 +1,221 @@ +package org.redlance.platformtools.webp.impl.macos; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +public final class MacOSImageIOEncoder implements PlatformWebPEncoder { + private final MacOSFrameworks fw; + + private final MethodHandle cfDataGetLength; + private final MethodHandle cfDataGetBytePtr; + private final MethodHandle cfNumberCreate; + private final MethodHandle cfDictionaryCreate; + private final MethodHandle cgImageCreate; + private final MethodHandle cgDataProviderCreateWithData; + private final MethodHandle cgDataProviderRelease; + private final MethodHandle cgImageDestAddImage; + private final MethodHandle cgImageDestFinalize; + private final MemorySegment qualityPropertyKey; + + private MacOSImageIOEncoder(MacOSFrameworks fw) { + this.fw = fw; + + Linker linker = Linker.nativeLinker(); + + this.cfDataGetLength = linker.downcallHandle( + fw.lookup.find("CFDataGetLength").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) + ); + this.cfDataGetBytePtr = linker.downcallHandle( + fw.lookup.find("CFDataGetBytePtr").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS) + ); + this.cfNumberCreate = linker.downcallHandle( + fw.lookup.find("CFNumberCreate").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ) + ); + this.cfDictionaryCreate = linker.downcallHandle( + fw.lookup.find("CFDictionaryCreate").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + this.cgImageCreate = linker.downcallHandle( + fw.lookup.find("CGImageCreate").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, + ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.JAVA_BOOLEAN, ValueLayout.JAVA_INT + ) + ); + this.cgDataProviderCreateWithData = linker.downcallHandle( + fw.lookup.find("CGDataProviderCreateWithData").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS + ) + ); + this.cgDataProviderRelease = linker.downcallHandle( + fw.lookup.find("CGDataProviderRelease").orElseThrow(), + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) + ); + this.cgImageDestAddImage = linker.downcallHandle( + fw.imageIO.find("CGImageDestinationAddImage").orElseThrow(), + FunctionDescriptor.ofVoid( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + this.cgImageDestFinalize = linker.downcallHandle( + fw.imageIO.find("CGImageDestinationFinalize").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS) + ); + this.qualityPropertyKey = fw.imageIO.find("kCGImageDestinationLossyCompressionQuality").orElseThrow() + .reinterpret(ValueLayout.ADDRESS.byteSize()) + .get(ValueLayout.ADDRESS, 0); + } + + public static @Nullable MacOSImageIOEncoder tryCreate() { + MacOSFrameworks fw = MacOSFrameworks.getInstance(); + if (fw == null || !testCanEncode(fw)) return null; + return new MacOSImageIOEncoder(fw); + } + + private static boolean testCanEncode(MacOSFrameworks fw) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment uti = fw.createCFString(arena, "org.webmproject.webp"); + if (uti.address() == 0) return false; + + MemorySegment data = (MemorySegment) fw.cfDataCreateMutable.invokeExact(MemorySegment.NULL, 0L); + MemorySegment dest = (MemorySegment) fw.cgImageDestCreateWithData.invokeExact( + data, uti, 1L, MemorySegment.NULL + ); + boolean result = dest.address() != 0; + if (result) fw.cfRelease.invokeExact(dest); + fw.cfRelease.invokeExact(data); + fw.cfRelease.invokeExact(uti); + return result; + } catch (Throwable t) { + return false; + } + } + + @Override + public String backendName() { + return "macOS ImageIO"; + } + + @Override + public byte[] encodeLossless(byte[] rgba, int width, int height) { + // ImageIO doesn't support true lossless WebP; use max quality lossy + return encodeLossy(rgba, width, height, 1.0f); + } + + @Override + public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + try (Arena arena = Arena.ofConfined()) { + // CGDataProviderCreateWithData stores the pointer — must use arena-allocated memory + MemorySegment rgbaSeg = arena.allocate(rgba.length); + rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + + MemorySegment colorSpace = (MemorySegment) this.fw.cgColorSpaceCreateDeviceRGB.invokeExact(); + MemorySegment provider = (MemorySegment) this.cgDataProviderCreateWithData.invokeExact( + MemorySegment.NULL, rgbaSeg, (long) rgba.length, MemorySegment.NULL + ); + + MemorySegment cgImage = (MemorySegment) this.cgImageCreate.invokeExact( + (long) width, (long) height, + 8L, 32L, (long) width * 4, + colorSpace, MacOSFrameworks.kCGImageAlphaLast, + provider, MemorySegment.NULL, + false, 0 + ); + + if (cgImage.address() == 0) { + this.cgDataProviderRelease.invokeExact(provider); + this.fw.cgColorSpaceRelease.invokeExact(colorSpace); + throw new IllegalStateException("CGImageCreate failed"); + } + + MemorySegment outputData = (MemorySegment) this.fw.cfDataCreateMutable.invokeExact( + MemorySegment.NULL, 0L + ); + + MemorySegment uti = this.fw.createCFString(arena, "org.webmproject.webp"); + MemorySegment dest = (MemorySegment) this.fw.cgImageDestCreateWithData.invokeExact( + outputData, uti, 1L, MemorySegment.NULL + ); + + if (dest.address() == 0) { + this.fw.cfRelease.invokeExact(uti); + this.fw.cfRelease.invokeExact(outputData); + this.fw.cgImageRelease.invokeExact(cgImage); + this.cgDataProviderRelease.invokeExact(provider); + this.fw.cgColorSpaceRelease.invokeExact(colorSpace); + throw new IllegalStateException("CGImageDestinationCreateWithData failed"); + } + + MemorySegment props = createQualityProperties(arena, quality); + this.cgImageDestAddImage.invokeExact(dest, cgImage, props); + boolean ok = (boolean) this.cgImageDestFinalize.invokeExact(dest); + + byte[] result = null; + if (ok) { + long len = (long) this.cfDataGetLength.invokeExact(outputData); + MemorySegment ptr = (MemorySegment) this.cfDataGetBytePtr.invokeExact(outputData); + result = ptr.reinterpret(len).toArray(ValueLayout.JAVA_BYTE); + } + + this.fw.cfRelease.invokeExact(props); + this.fw.cfRelease.invokeExact(dest); + this.fw.cfRelease.invokeExact(uti); + this.fw.cfRelease.invokeExact(outputData); + this.fw.cgImageRelease.invokeExact(cgImage); + this.cgDataProviderRelease.invokeExact(provider); + this.fw.cgColorSpaceRelease.invokeExact(colorSpace); + + if (result == null) throw new IllegalStateException("CGImageDestinationFinalize failed"); + return result; + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("macOS ImageIO encode failed", t); + } + } + + private MemorySegment createQualityProperties(Arena arena, float quality) throws Throwable { + MemorySegment doubleVal = arena.allocate(ValueLayout.JAVA_DOUBLE); + doubleVal.set(ValueLayout.JAVA_DOUBLE, 0, quality); + MemorySegment qualityNum = (MemorySegment) this.cfNumberCreate.invokeExact( + MemorySegment.NULL, MacOSFrameworks.kCFNumberFloat64Type, doubleVal + ); + + MemorySegment keys = arena.allocate(ValueLayout.ADDRESS); + keys.set(ValueLayout.ADDRESS, 0, this.qualityPropertyKey); + + MemorySegment values = arena.allocate(ValueLayout.ADDRESS); + values.set(ValueLayout.ADDRESS, 0, qualityNum); + + MemorySegment dict = (MemorySegment) this.cfDictionaryCreate.invokeExact( + MemorySegment.NULL, keys, values, 1L, + MemorySegment.NULL, MemorySegment.NULL + ); + + this.fw.cfRelease.invokeExact(qualityNum); + return dict; + } + + @Override + public boolean isAvailable() { + return true; + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java new file mode 100644 index 0000000..6bb0511 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java @@ -0,0 +1,216 @@ +package org.redlance.platformtools.webp.impl.windows; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; + +import java.lang.foreign.*; + +import static org.redlance.platformtools.webp.impl.windows.WindowsComHelper.*; + +public final class WindowsCodecsDecoder implements PlatformWebPDecoder { + private final WindowsComHelper com; + + private WindowsCodecsDecoder(WindowsComHelper com) { + this.com = com; + } + + public static @Nullable WindowsCodecsDecoder tryCreate() { + WindowsComHelper com = WindowsComHelper.getInstance(); + return com != null ? new WindowsCodecsDecoder(com) : null; + } + + @Override + public String backendName() { + return "Windows WIC"; + } + + @Override + public DecodedImage decode(byte[] webpData) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment factory = this.com.createFactory(arena); + if (factory == null) throw new IllegalStateException("Failed to create WIC factory"); + + MemorySegment wicStream = null, decoder = null, frame = null, converter = null; + try { + wicStream = comCreateObj( + arena, factory, FACTORY_CREATE_STREAM, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + + // InitializeFromMemory stores the pointer — must use arena-allocated memory + MemorySegment dataSeg = arena.allocate(webpData.length); + dataSeg.copyFrom(MemorySegment.ofArray(webpData)); + + checkHr(comCallInt( + wicStream, WIC_STREAM_INITIALIZE_FROM_MEMORY, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT + ), + dataSeg, webpData.length + ), "InitializeFromMemory"); + + decoder = comCreateObj( + arena, factory, FACTORY_CREATE_DECODER_FROM_STREAM, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + wicStream, MemorySegment.NULL, 0 + ); + + frame = comCreateObj( + arena, decoder, DECODER_GET_FRAME, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + 0 + ); + + converter = comCreateObj( + arena, factory, FACTORY_CREATE_FORMAT_CONVERTER, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + + checkHr(comCallInt( + converter, FORMAT_CONVERTER_INITIALIZE, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_DOUBLE, + ValueLayout.JAVA_INT + ), + frame, guidPixelFormat32bppRGBA(arena), 0, MemorySegment.NULL, 0.0, 0 + ), "FormatConverter.Initialize"); + + MemorySegment wPtr = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment hPtr = arena.allocate(ValueLayout.JAVA_INT); + checkHr(comCallInt( + converter, BITMAP_SOURCE_GET_SIZE, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + wPtr, hPtr + ), "GetSize"); + + int w = wPtr.get(ValueLayout.JAVA_INT, 0); + int h = hPtr.get(ValueLayout.JAVA_INT, 0); + int stride = w * 4; + int bufSize = stride * h; + + MemorySegment buffer = arena.allocate(bufSize); + checkHr(comCallInt( + converter, BITMAP_SOURCE_COPY_PIXELS, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + MemorySegment.NULL, stride, bufSize, buffer + ), "CopyPixels"); + + return new DecodedImage(buffer.toArray(ValueLayout.JAVA_BYTE), w, h); + } finally { + if (converter != null) comRelease(converter); + if (frame != null) comRelease(frame); + if (decoder != null) comRelease(decoder); + if (wicStream != null) comRelease(wicStream); + comRelease(factory); + } + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("WIC decode failed", t); + } + } + + @Override + public int[] getInfo(byte[] webpData) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment factory = this.com.createFactory(arena); + if (factory == null) throw new IllegalStateException("Failed to create WIC factory"); + + MemorySegment wicStream = null, decoder = null, frame = null; + try { + wicStream = comCreateObj( + arena, factory, FACTORY_CREATE_STREAM, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + + // InitializeFromMemory stores the pointer — must use arena-allocated memory + MemorySegment dataSeg = arena.allocate(webpData.length); + dataSeg.copyFrom(MemorySegment.ofArray(webpData)); + + checkHr(comCallInt( + wicStream, WIC_STREAM_INITIALIZE_FROM_MEMORY, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT + ), + dataSeg, webpData.length + ), "InitializeFromMemory"); + + decoder = comCreateObj( + arena, factory, FACTORY_CREATE_DECODER_FROM_STREAM, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + wicStream, MemorySegment.NULL, 0 + ); + + frame = comCreateObj( + arena, decoder, DECODER_GET_FRAME, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + 0 + ); + + MemorySegment wPtr = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment hPtr = arena.allocate(ValueLayout.JAVA_INT); + checkHr(comCallInt( + frame, BITMAP_SOURCE_GET_SIZE, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + wPtr, hPtr + ), "GetSize"); + + return new int[] { + wPtr.get(ValueLayout.JAVA_INT, 0), + hPtr.get(ValueLayout.JAVA_INT, 0) + }; + } finally { + if (frame != null) comRelease(frame); + if (decoder != null) comRelease(decoder); + if (wicStream != null) comRelease(wicStream); + comRelease(factory); + } + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("WIC getInfo failed", t); + } + } + + @Override + public boolean isAvailable() { + return true; + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java new file mode 100644 index 0000000..cb9c706 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java @@ -0,0 +1,231 @@ +package org.redlance.platformtools.webp.impl.windows; + +import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +import static org.redlance.platformtools.webp.impl.windows.WindowsComHelper.*; + +public final class WindowsCodecsEncoder implements PlatformWebPEncoder { + private final WindowsComHelper com; + + // Encoder-only handle + private final MethodHandle createStreamOnHGlobal; + + private WindowsCodecsEncoder(WindowsComHelper com) { + this.com = com; + + this.createStreamOnHGlobal = LINKER.downcallHandle( + com.ole32.find("CreateStreamOnHGlobal").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ) + ); + } + + public static @Nullable WindowsCodecsEncoder tryCreate() { + WindowsComHelper com = WindowsComHelper.getInstance(); + if (com == null) return null; + + // Verify WebP encode support + try (Arena arena = Arena.ofConfined()) { + MemorySegment factory = com.createFactory(arena); + if (factory == null) return null; + + try { + MemorySegment encoder = comCreateObj( + arena, factory, FACTORY_CREATE_ENCODER, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + guidWebpContainer(arena), MemorySegment.NULL + ); + comRelease(encoder); + } finally { + comRelease(factory); + } + } catch (Throwable t) { + return null; + } + + return new WindowsCodecsEncoder(com); + } + + @Override + public String backendName() { + return "Windows WIC"; + } + + @Override + public byte[] encodeLossless(byte[] rgba, int width, int height) { + return encodeLossy(rgba, width, height, 1.0f); + } + + @Override + public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment factory = this.com.createFactory(arena); + if (factory == null) throw new IllegalStateException("Failed to create WIC factory"); + + MemorySegment stream = null, encoder = null, frame = null, props = null; + try { + MemorySegment streamPtr = arena.allocate(ValueLayout.ADDRESS); + checkHr((int) this.createStreamOnHGlobal.invokeExact( + MemorySegment.NULL, 1, streamPtr + ), "CreateStreamOnHGlobal"); + stream = streamPtr.get(ValueLayout.ADDRESS, 0); + + encoder = comCreateObj( + arena, factory, FACTORY_CREATE_ENCODER, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + guidWebpContainer(arena), MemorySegment.NULL + ); + + checkHr(comCallInt( + encoder, ENCODER_INITIALIZE, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT + ), + stream, 2 + ), "Encoder.Initialize"); + + MemorySegment framePtr = arena.allocate(ValueLayout.ADDRESS); + MemorySegment propsPtr = arena.allocate(ValueLayout.ADDRESS); + checkHr(comCallInt( + encoder, ENCODER_CREATE_NEW_FRAME, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + framePtr, propsPtr + ), "CreateNewFrame"); + frame = framePtr.get(ValueLayout.ADDRESS, 0); + props = propsPtr.get(ValueLayout.ADDRESS, 0); + + checkHr(comCallInt( + frame, FRAME_ENCODE_INITIALIZE, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + props + ), "FrameEncode.Initialize"); + + checkHr(comCallInt( + frame, FRAME_ENCODE_SET_SIZE, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT + ), + width, height + ), "SetSize"); + + checkHr(comCallInt( + frame, FRAME_ENCODE_SET_PIXEL_FORMAT, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + guidPixelFormat32bppRGBA(arena) + ), "SetPixelFormat"); + + int stride = width * 4; + + MemorySegment rgbaSeg = arena.allocate(rgba.length); + rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + + checkHr(comCallInt( + frame, FRAME_ENCODE_WRITE_PIXELS, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + height, stride, rgba.length, rgbaSeg + ), "WritePixels"); + + checkHr(comCallInt( + frame, FRAME_ENCODE_COMMIT, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ) + ), "FrameEncode.Commit"); + + checkHr(comCallInt( + encoder, ENCODER_COMMIT, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ) + ), "Encoder.Commit"); + + return readStream(arena, stream); + } finally { + if (props != null && props.address() != 0) comRelease(props); + if (frame != null) comRelease(frame); + if (encoder != null) comRelease(encoder); + if (stream != null) comRelease(stream); + comRelease(factory); + } + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("WIC encode failed", t); + } + } + + private byte[] readStream(Arena arena, MemorySegment stream) throws Throwable { + // Seek to 0 + comCallInt( + stream, ISTREAM_SEEK, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + 0L, 0, MemorySegment.NULL // STREAM_SEEK_SET = 0 + ); + + // Stat to get size + MemorySegment statstg = arena.allocate(72); // sizeof(STATSTG) + checkHr(comCallInt( + stream, ISTREAM_STAT, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT + ), + statstg, 1 // STATFLAG_NONAME = 1 + ), "IStream.Stat"); + + long size = statstg.get(ValueLayout.JAVA_LONG, 8); // cbSize at offset 8 + if (size <= 0 || size > Integer.MAX_VALUE) { + throw new IllegalStateException("Invalid stream size: " + size); + } + + // Read + MemorySegment buffer = arena.allocate(size); + MemorySegment bytesRead = arena.allocate(ValueLayout.JAVA_INT); + checkHr(comCallInt( + stream, ISTREAM_READ, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS + ), + buffer, (int) size, bytesRead + ), "IStream.Read"); + + int read = bytesRead.get(ValueLayout.JAVA_INT, 0); + return buffer.asSlice(0, read).toArray(ValueLayout.JAVA_BYTE); + } + + @Override + public boolean isAvailable() { + return true; + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java new file mode 100644 index 0000000..e2da4dc --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java @@ -0,0 +1,151 @@ +package org.redlance.platformtools.webp.impl.windows; + +import org.jetbrains.annotations.Nullable; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +public final class WindowsComHelper { + static final Linker LINKER = Linker.nativeLinker(); + static final long PTR_SIZE = ValueLayout.ADDRESS.byteSize(); + + // COM vtable indices — shared + static final int IUNKNOWN_RELEASE = 2; + + // Decoder vtable indices + static final int FACTORY_CREATE_DECODER_FROM_STREAM = 4; + static final int FACTORY_CREATE_FORMAT_CONVERTER = 10; + static final int FACTORY_CREATE_STREAM = 14; + static final int DECODER_GET_FRAME = 13; + static final int BITMAP_SOURCE_GET_SIZE = 3; + static final int BITMAP_SOURCE_COPY_PIXELS = 7; + static final int FORMAT_CONVERTER_INITIALIZE = 8; + static final int WIC_STREAM_INITIALIZE_FROM_MEMORY = 16; + + // Encoder vtable indices + static final int FACTORY_CREATE_ENCODER = 8; + static final int ENCODER_INITIALIZE = 3; + static final int ENCODER_CREATE_NEW_FRAME = 10; + static final int ENCODER_COMMIT = 11; + static final int FRAME_ENCODE_INITIALIZE = 3; + static final int FRAME_ENCODE_SET_SIZE = 4; + static final int FRAME_ENCODE_SET_PIXEL_FORMAT = 6; + static final int FRAME_ENCODE_WRITE_PIXELS = 10; + static final int FRAME_ENCODE_COMMIT = 12; + static final int ISTREAM_READ = 3; + static final int ISTREAM_SEEK = 5; + static final int ISTREAM_STAT = 12; + + private final MethodHandle coCreateInstance; + final SymbolLookup ole32; + + private WindowsComHelper(SymbolLookup ole32) { + this.ole32 = ole32; + + this.coCreateInstance = LINKER.downcallHandle(ole32.find("CoCreateInstance").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS + ) + ); + } + + private static class Holder { + static final @Nullable WindowsComHelper INSTANCE = tryInit(); + } + + public static @Nullable WindowsComHelper getInstance() { + return Holder.INSTANCE; + } + + private static @Nullable WindowsComHelper tryInit() { + try { + SymbolLookup ole32 = SymbolLookup.libraryLookup("ole32", Arena.global()); + + MethodHandle coInitEx = LINKER.downcallHandle(ole32.find("CoInitializeEx").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); + coInitEx.invokeExact(MemorySegment.NULL, 0); + + return new WindowsComHelper(ole32); + } catch (Throwable t) { + return null; + } + } + + @Nullable MemorySegment createFactory(Arena arena) throws Throwable { + MemorySegment factoryPtr = arena.allocate(ValueLayout.ADDRESS); + int hr = (int) coCreateInstance.invokeExact( + guidWICImagingFactory(arena), MemorySegment.NULL, + 1 | 4, // CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER + guidIWICImagingFactory(arena), factoryPtr + ); + if (hr < 0) return null; + return factoryPtr.get(ValueLayout.ADDRESS, 0); + } + + static int comCallInt(MemorySegment obj, int vtableIndex, FunctionDescriptor fd, Object... extraArgs) throws Throwable { + MemorySegment vtable = obj.get(ValueLayout.ADDRESS, 0).reinterpret(PTR_SIZE * (vtableIndex + 1)); + MemorySegment funcPtr = vtable.getAtIndex(ValueLayout.ADDRESS, vtableIndex); + MethodHandle mh = LINKER.downcallHandle(funcPtr, fd); + Object[] allArgs = new Object[extraArgs.length + 1]; + allArgs[0] = obj; + System.arraycopy(extraArgs, 0, allArgs, 1, extraArgs.length); + return (int) mh.invokeWithArguments(allArgs); + } + + static MemorySegment comCreateObj(Arena arena, MemorySegment obj, int vtableIndex, FunctionDescriptor fd, Object... extraArgs) throws Throwable { + MemorySegment ptr = arena.allocate(ValueLayout.ADDRESS); + Object[] args = new Object[extraArgs.length + 1]; + System.arraycopy(extraArgs, 0, args, 0, extraArgs.length); + args[extraArgs.length] = ptr; + int hr = comCallInt(obj, vtableIndex, fd, args); + checkHr(hr, "COM call"); + return ptr.get(ValueLayout.ADDRESS, 0); + } + + static void checkHr(int hr, String operation) { + if (hr < 0) throw new IllegalStateException(operation + " failed: HRESULT 0x" + Integer.toHexString(hr)); + } + + static void comRelease(MemorySegment obj) { + try { + comCallInt(obj, IUNKNOWN_RELEASE, FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); + } catch (Throwable ignored) {} + } + + // --- GUID helpers --- + + static MemorySegment writeGuid(Arena arena, int d1, short d2, short d3, byte... d4) { + MemorySegment seg = arena.allocate(16); + seg.set(ValueLayout.JAVA_INT_UNALIGNED, 0, d1); + seg.set(ValueLayout.JAVA_SHORT_UNALIGNED, 4, d2); + seg.set(ValueLayout.JAVA_SHORT_UNALIGNED, 6, d3); + MemorySegment.copy(MemorySegment.ofArray(d4), 0, seg, 8, 8); + return seg; + } + + static MemorySegment guidWICImagingFactory(Arena arena) { + return writeGuid(arena, 0xcacaf262, (short) 0x9370, (short) 0x4615, + (byte) 0xa1, (byte) 0x3b, (byte) 0x9f, (byte) 0x55, + (byte) 0x39, (byte) 0xda, (byte) 0x4c, (byte) 0x0a); + } + + static MemorySegment guidIWICImagingFactory(Arena arena) { + return writeGuid(arena, 0xec5ec8a9, (short) 0xc395, (short) 0x4314, + (byte) 0x9c, (byte) 0x77, (byte) 0x54, (byte) 0xd7, + (byte) 0xa9, (byte) 0x35, (byte) 0xff, (byte) 0x70); + } + + static MemorySegment guidWebpContainer(Arena arena) { + return writeGuid(arena, 0xe094b0e2, (short) 0x67f2, (short) 0x45b3, + (byte) 0xb0, (byte) 0xea, (byte) 0x11, (byte) 0x53, + (byte) 0x37, (byte) 0xca, (byte) 0x7c, (byte) 0xf3); + } + + static MemorySegment guidPixelFormat32bppRGBA(Arena arena) { + return writeGuid(arena, 0xf5c7ad2d, (short) 0x6a8d, (short) 0x43dd, + (byte) 0xa7, (byte) 0xa8, (byte) 0xa2, (byte) 0x99, + (byte) 0x35, (byte) 0x26, (byte) 0x1a, (byte) 0xe9); + } +} From e46f56612015bd2d6f1e085f9b1f582461720190 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Mon, 9 Mar 2026 22:35:46 +0700 Subject: [PATCH 02/22] Fix Windows WIC backend and improve testing module - Fix CoInitializeEx invokeExact return type mismatch (WrongMethodTypeException) - Add libwebp-7 to library search names (MinGW/MSYS2 SONAME convention) - Configure testing module with shadow fat JAR and Main-Class manifest - Use ProGuard-optimized bundled webp artifact in testing Co-Authored-By: Claude Opus 4.6 --- testing/build.gradle | 16 +++++++++++++++- .../webp/impl/libwebp/LibWebPLibrary.java | 2 +- .../webp/impl/windows/WindowsComHelper.java | 11 ++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/testing/build.gradle b/testing/build.gradle index bb3a47f..2ff23bd 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -1,12 +1,26 @@ +plugins { + id "com.gradleup.shadow" +} + dependencies { implementation(project(":accent")) implementation(project(":referer")) implementation(project(":favorites")) implementation(project(":progress")) - implementation(project(":webp")) + compileOnly(project(":webp")) } tasks.withType(JavaCompile).configureEach { options.encoding = "UTF-8" options.release.set(25) } + +jar { + manifest.attributes("Main-Class": "org.redlance.platformtools.testing.TestingApp") +} + +shadowJar { + dependsOn(":webp:proguardJar") + from(zipTree(project(":webp").file("build/libs/PlatformTools-webp-${version}-bundled.jar"))) + mergeServiceFiles() +} 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 26dc574..40a4b8d 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 @@ -28,7 +28,7 @@ private static class Holder { } private static @Nullable LibWebPLibrary tryLoad() { - for (String name : new String[]{"webp", "libwebp", "libwebp.so.7"}) { + for (String name : new String[]{"webp", "libwebp", "libwebp-7", "libwebp.so.7"}) { try { return new LibWebPLibrary(SymbolLookup.libraryLookup(name, Arena.global())); } catch (Throwable ignored) { diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java index e2da4dc..5f36248 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java @@ -63,9 +63,14 @@ private static class Holder { try { SymbolLookup ole32 = SymbolLookup.libraryLookup("ole32", Arena.global()); - MethodHandle coInitEx = LINKER.downcallHandle(ole32.find("CoInitializeEx").orElseThrow(), - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); - coInitEx.invokeExact(MemorySegment.NULL, 0); + MethodHandle coInitEx = LINKER.downcallHandle( + ole32.find("CoInitializeEx").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT + ) + ); + //noinspection unused — invokeExact requires matching return type + int hr = (int) coInitEx.invokeExact(MemorySegment.NULL, 0); return new WindowsComHelper(ole32); } catch (Throwable t) { From 0a6b03e4e17207702e5abc4e08d443912e405f08 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Mon, 9 Mar 2026 23:43:50 +0700 Subject: [PATCH 03/22] Migrate WebP pipeline to int[] ARGB and extract DecodedImage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch all encoder/decoder backends from byte[] RGBA to int[] ARGB — the native Java2D pixel format. This eliminates all manual byte-level pixel conversion and enables trivial interop with BufferedImage. - Extract DecodedImage from inner record to standalone class with toPng(), fromPng(), and isPngCached() methods (PNG caching) - Add isWebP() detection to PlatformWebPDecoder with backend overrides - libwebp: use WebPDecodeARGB/WebPEncode*BGRA (int[] ARGB = BGRA on LE) - macOS: use kCGBitmapByteOrder32Little|kCGImageAlphaFirst for encoding - Windows WIC: use GUID_WICPixelFormat32bppBGRA (native int[] = ARGB) - Java ImageIO: use getRGB()/setRGB() directly Co-Authored-By: Claude Opus 4.6 --- .../platformtools/testing/TestingApp.java | 38 +++----- .../webp/decoder/DecodedImage.java | 91 +++++++++++++++++++ .../webp/decoder/PlatformWebPDecoder.java | 30 +++--- .../webp/encoder/PlatformWebPEncoder.java | 16 ++-- .../webp/impl/PlatformWebPDecoderImpl.java | 1 + .../webp/impl/PlatformWebPEncoderImpl.java | 8 +- .../webp/impl/imageio/JavaImageIODecoder.java | 45 +++------ .../webp/impl/imageio/JavaImageIOEncoder.java | 26 ++---- .../webp/impl/libwebp/LibWebPDecoder.java | 32 +++++-- .../webp/impl/libwebp/LibWebPEncoder.java | 40 ++++---- .../webp/impl/macos/MacOSFrameworks.java | 19 ++-- .../webp/impl/macos/MacOSImageIODecoder.java | 12 ++- .../webp/impl/macos/MacOSImageIOEncoder.java | 15 +-- .../impl/windows/WindowsCodecsDecoder.java | 5 +- .../impl/windows/WindowsCodecsEncoder.java | 15 +-- .../webp/impl/windows/WindowsComHelper.java | 8 +- 16 files changed, 245 insertions(+), 156 deletions(-) create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/decoder/DecodedImage.java 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 d46bd55..bb5a4e9 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.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; @@ -323,38 +324,37 @@ private void showWebPDialog() { encodeBtn.setEnabled(encodeAvailable); encodeBtn.addActionListener(event -> { int w = 256, h = 256; - byte[] rgba = new byte[w * h * 4]; + int[] argb = new int[w * h]; java.util.Random rng = new java.util.Random(42); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - int i = (y * w + x) * 4; - rgba[i] = (byte) Math.min(255, (x + rng.nextInt(32)) & 0xFF); - rgba[i + 1] = (byte) Math.min(255, (y + rng.nextInt(32)) & 0xFF); - rgba[i + 2] = (byte) (rng.nextInt(256)); - rgba[i + 3] = (byte) 255; + int r = Math.min(255, (x + rng.nextInt(32)) & 0xFF); + int g = Math.min(255, (y + rng.nextInt(32)) & 0xFF); + int b = rng.nextInt(256); + argb[y * w + x] = (0xFF << 24) | (r << 16) | (g << 8) | b; } } log.append("Encode 256x256 noisy gradient:\n"); - log.append(" Raw RGBA: " + rgba.length + " bytes\n"); + log.append(" Raw ARGB: " + argb.length + " pixels\n"); byte[] lossless = null; try { - lossless = PlatformWebPEncoder.INSTANCE.encodeLossless(rgba, w, h); + lossless = PlatformWebPEncoder.INSTANCE.encodeLossless(argb, w, h); log.append(" Lossless: " + lossless.length + " bytes\n"); } catch (Exception ex) { log.append(" Lossless: FAILED: " + ex.getMessage() + "\n"); } try { - byte[] lossy75 = PlatformWebPEncoder.INSTANCE.encodeLossy(rgba, w, h, 0.75f); + byte[] lossy75 = PlatformWebPEncoder.INSTANCE.encodeLossy(argb, w, h, 0.75f); log.append(" Lossy 75%%: " + lossy75.length + " bytes\n"); } catch (Exception ex) { log.append(" Lossy 75%%: FAILED: " + ex.getMessage() + "\n"); } try { - byte[] lossy50 = PlatformWebPEncoder.INSTANCE.encodeLossy(rgba, w, h, 0.50f); + byte[] lossy50 = PlatformWebPEncoder.INSTANCE.encodeLossy(argb, w, h, 0.50f); log.append(" Lossy 50%%: " + lossy50.length + " bytes\n"); } catch (Exception ex) { log.append(" Lossy 50%%: FAILED: " + ex.getMessage() + "\n"); @@ -363,9 +363,9 @@ private void showWebPDialog() { // Roundtrip test if (lossless != null && decodeAvailable) { try { - PlatformWebPDecoder.DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(lossless); + DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(lossless); log.append(" Roundtrip: " + decoded.width() + "x" + decoded.height() - + " (" + decoded.rgba().length + " bytes RGBA)\n"); + + " (" + decoded.argb().length + " pixels)\n"); } catch (Exception ex) { log.append(" Roundtrip: FAILED: " + ex.getMessage() + "\n"); } @@ -394,22 +394,12 @@ private void showWebPDialog() { } try { - PlatformWebPDecoder.DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(webpData); + DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(webpData); log.append(" Decoded: " + decoded.width() + "x" + decoded.height() + "\n"); // Show decoded image BufferedImage img = new BufferedImage(decoded.width(), decoded.height(), BufferedImage.TYPE_INT_ARGB); - byte[] pixels = decoded.rgba(); - for (int y = 0; y < decoded.height(); y++) { - for (int x = 0; x < decoded.width(); x++) { - int idx = (y * decoded.width() + x) * 4; - int r = pixels[idx] & 0xFF; - int g = pixels[idx + 1] & 0xFF; - int b = pixels[idx + 2] & 0xFF; - int a = pixels[idx + 3] & 0xFF; - img.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b); - } - } + img.setRGB(0, 0, decoded.width(), decoded.height(), decoded.argb(), 0, decoded.width()); JDialog preview = new JDialog(dialog, "Preview: " + fc.getSelectedFile().getName(), false); preview.add(new JLabel(new ImageIcon(img))); diff --git a/webp/src/main/java/org/redlance/platformtools/webp/decoder/DecodedImage.java b/webp/src/main/java/org/redlance/platformtools/webp/decoder/DecodedImage.java new file mode 100644 index 0000000..c263886 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/decoder/DecodedImage.java @@ -0,0 +1,91 @@ +package org.redlance.platformtools.webp.decoder; + +import org.jetbrains.annotations.Nullable; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Decoded image with raw pixel data. + */ +@SuppressWarnings("unused") // API +public final class DecodedImage { + private final int[] argb; + private final int width; + private final int height; + private byte @Nullable [] png; + + /** + * Creates a decoded image from packed ARGB pixels. + * + * @param argb pixel data as packed ARGB integers ({@code (a << 24) | (r << 16) | (g << 8) | b}), + * length must be {@code width * height} + * @param width image width in pixels + * @param height image height in pixels + */ + public DecodedImage(int[] argb, int width, int height) { + this.argb = argb; + this.width = width; + this.height = height; + } + + /** + * Parses a PNG image into a {@link DecodedImage}. + * + * @param pngData raw PNG file bytes + * @return decoded image with cached PNG bytes + * @throws IOException if the data is not a valid PNG or decoding fails + */ + public static DecodedImage fromPng(byte[] pngData) throws IOException { + try (ByteArrayInputStream is = new ByteArrayInputStream(pngData)) { + BufferedImage img = ImageIO.read(is); + if (img == null) throw new IOException("Failed to decode PNG"); + + int w = img.getWidth(); + int h = img.getHeight(); + DecodedImage decoded = new DecodedImage(img.getRGB(0, 0, w, h, null, 0, w), w, h); + decoded.png = pngData; + return decoded; + } + } + + public int[] argb() { + return this.argb; + } + + public int width() { + return this.width; + } + + public int height() { + return this.height; + } + + /** + * Returns {@code true} if PNG bytes are already cached. + */ + public boolean isPngCached() { + return this.png != null; + } + + /** + * Converts this image to PNG format. Cached after the first call. + * + * @return PNG file bytes + * @throws IOException if encoding fails + */ + public byte[] toPng() throws IOException { + if (this.png != null) return this.png; + + BufferedImage img = new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_ARGB); + img.setRGB(0, 0, this.width, this.height, this.argb, 0, this.width); + + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + ImageIO.write(img, "png", out); + return this.png = out.toByteArray(); + } + } +} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java index 524621c..ed2ffab 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/decoder/PlatformWebPDecoder.java @@ -19,16 +19,6 @@ public interface PlatformWebPDecoder { PlatformWebPDecoder INSTANCE = new PlatformWebPDecoderImpl(); - /** - * Decoded image with raw pixel data. - * - * @param rgba pixel data in RGBA order, straight (non-premultiplied) alpha, - * length is always {@code width * height * 4} - * @param width image width in pixels - * @param height image height in pixels - */ - record DecodedImage(byte[] rgba, int width, int height) {} - /** * Returns the name of the active backend (e.g. "libwebp", "macOS ImageIO", "Windows WIC"). * @@ -37,10 +27,10 @@ record DecodedImage(byte[] rgba, int width, int height) {} String backendName(); /** - * Decodes a WebP image into raw RGBA pixels. + * Decodes a WebP image into packed ARGB pixels. * * @param webpData raw WebP file bytes - * @return decoded image with RGBA pixel data + * @return decoded image with ARGB pixel data * @throws IllegalStateException if the data is invalid or decoding fails * @throws UnsupportedOperationException if no backend is available */ @@ -56,5 +46,21 @@ record DecodedImage(byte[] rgba, int width, int height) {} */ int[] getInfo(byte[] webpData); + /** + * Returns {@code true} if the given data is a valid WebP image. + * + * @param data raw bytes to check + */ + default boolean isWebP(byte[] data) { + if (data == null || data.length == 0) return false; + + try { + int[] info = getInfo(data); + return info[0] > 0 && info[1] > 0; + } catch (Exception e) { + return false; + } + } + boolean isAvailable(); } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java index 01aecf7..3001fd5 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/encoder/PlatformWebPEncoder.java @@ -27,23 +27,23 @@ public interface PlatformWebPEncoder { String backendName(); /** - * Encodes raw RGBA pixels into a lossless WebP image. + * Encodes ARGB pixels into a lossless WebP image. * - * @param rgba pixel data in RGBA order, straight (non-premultiplied) alpha, - * length must be {@code width * height * 4} + * @param argb pixel data as packed ARGB integers, + * length must be {@code width * height} * @param width image width in pixels * @param height image height in pixels * @return WebP file bytes * @throws IllegalStateException if encoding fails * @throws UnsupportedOperationException if no backend is available */ - byte[] encodeLossless(byte[] rgba, int width, int height); + byte[] encodeLossless(int[] argb, int width, int height); /** - * Encodes raw RGBA pixels into a lossy WebP image. + * Encodes ARGB pixels into a lossy WebP image. * - * @param rgba pixel data in RGBA order, straight (non-premultiplied) alpha, - * length must be {@code width * height * 4} + * @param argb pixel data as packed ARGB integers, + * length must be {@code width * height} * @param width image width in pixels * @param height image height in pixels * @param quality compression quality, {@code 0.0f} (smallest) to {@code 1.0f} (best) @@ -51,7 +51,7 @@ public interface PlatformWebPEncoder { * @throws IllegalStateException if encoding fails * @throws UnsupportedOperationException if no backend is available */ - byte[] encodeLossy(byte[] rgba, int width, int height, float quality); + byte[] encodeLossy(int[] argb, int width, int height, float quality); boolean isAvailable(); } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java index e22f775..4475983 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java @@ -1,6 +1,7 @@ package org.redlance.platformtools.webp.impl; import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java index a0d2415..7d48e80 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java @@ -21,13 +21,13 @@ public String backendName() { } @Override - public byte[] encodeLossless(byte[] rgba, int width, int height) { - return requireDelegate().encodeLossless(rgba, width, height); + public byte[] encodeLossless(int[] argb, int width, int height) { + return requireDelegate().encodeLossless(argb, width, height); } @Override - public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { - return requireDelegate().encodeLossy(rgba, width, height, quality); + public byte[] encodeLossy(int[] argb, int width, int height, float quality) { + return requireDelegate().encodeLossy(argb, width, height, quality); } @Override diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java index 43cea7e..8aa8b08 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java @@ -2,11 +2,12 @@ import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.decoder.DecodedImage; + import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Iterator; @@ -41,9 +42,8 @@ public DecodedImage decode(byte[] webpData) { int w = img.getWidth(); int h = img.getHeight(); - byte[] rgba = toRGBA(img, w, h); - return new DecodedImage(rgba, w, h); + return new DecodedImage(img.getRGB(0, 0, w, h, null, 0, w), w, h); } catch (IOException e) { throw new RuntimeException("Java ImageIO decode failed", e); } @@ -70,36 +70,19 @@ public int[] getInfo(byte[] webpData) { } @Override - public boolean isAvailable() { - return true; - } + public boolean isWebP(byte[] data) { + if (data == null || data.length == 0) return false; - private static byte[] toRGBA(BufferedImage img, int w, int h) { - if (img.getType() == BufferedImage.TYPE_4BYTE_ABGR) { - byte[] abgr = ((DataBufferByte) img.getRaster().getDataBuffer()).getData(); - byte[] rgba = new byte[w * h * 4]; - for (int i = 0; i < w * h; i++) { - int si = i * 4; - rgba[si] = abgr[si + 3]; // R - rgba[si + 1] = abgr[si + 2]; // G - rgba[si + 2] = abgr[si + 1]; // B - rgba[si + 3] = abgr[si]; // A - } - return rgba; + try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) { + return iis != null && ImageIO.getImageReaders(iis).hasNext(); + } catch (IOException e) { + return false; } + } - // Generic path: convert any type via getRGB - byte[] rgba = new byte[w * h * 4]; - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int argb = img.getRGB(x, y); - int i = (y * w + x) * 4; - rgba[i] = (byte) ((argb >> 16) & 0xFF); // R - rgba[i + 1] = (byte) ((argb >> 8) & 0xFF); // G - rgba[i + 2] = (byte) (argb & 0xFF); // B - rgba[i + 3] = (byte) ((argb >> 24) & 0xFF); // A - } - } - return rgba; + @Override + public boolean isAvailable() { + return true; } + } \ No newline at end of file diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java index e666106..af9f128 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java @@ -7,7 +7,6 @@ import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Iterator; @@ -33,13 +32,13 @@ public String backendName() { } @Override - public byte[] encodeLossless(byte[] rgba, int width, int height) { - return encode(rgba, width, height, null); + public byte[] encodeLossless(int[] argb, int width, int height) { + return encode(argb, width, height, null); } @Override - public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { - return encode(rgba, width, height, quality); + public byte[] encodeLossy(int[] argb, int width, int height, float quality) { + return encode(argb, width, height, quality); } @Override @@ -47,8 +46,9 @@ public boolean isAvailable() { return true; } - private byte[] encode(byte[] rgba, int width, int height, Float quality) { - BufferedImage img = fromRGBA(rgba, width, height); + private byte[] encode(int[] argb, int width, int height, Float quality) { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + img.setRGB(0, 0, width, height, argb, 0, width); Iterator writers = ImageIO.getImageWritersByMIMEType("image/webp"); if (!writers.hasNext()) { @@ -76,16 +76,4 @@ private byte[] encode(byte[] rgba, int width, int height, Float quality) { } } - private static BufferedImage fromRGBA(byte[] rgba, int width, int height) { - BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); - byte[] abgr = ((DataBufferByte) img.getRaster().getDataBuffer()).getData(); - for (int i = 0; i < width * height; i++) { - int si = i * 4; - abgr[si] = rgba[si + 3]; // A - abgr[si + 1] = rgba[si + 2]; // B - abgr[si + 2] = rgba[si + 1]; // G - abgr[si + 3] = rgba[si]; // R - } - return img; - } } \ 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 23a73a2..8e2aabe 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 @@ -1,16 +1,18 @@ package org.redlance.platformtools.webp.impl.libwebp; import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; +import java.nio.ByteOrder; public final class LibWebPDecoder implements PlatformWebPDecoder { private final LibWebPLibrary lib; - // uint8_t* WebPDecodeRGBA(const uint8_t* data, size_t size, int* w, int* h) - private final MethodHandle webPDecodeRGBA; + // uint8_t* WebPDecodeARGB(const uint8_t* data, size_t size, int* w, int* h) + private final MethodHandle webPDecodeARGB; // int WebPGetInfo(const uint8_t* data, size_t size, int* w, int* h) private final MethodHandle webPGetInfo; @@ -19,8 +21,8 @@ private LibWebPDecoder(LibWebPLibrary lib) { Linker linker = Linker.nativeLinker(); - this.webPDecodeRGBA = linker.downcallHandle( - lib.lookup.find("WebPDecodeRGBA").orElseThrow(), + this.webPDecodeARGB = linker.downcallHandle( + lib.lookup.find("WebPDecodeARGB").orElseThrow(), FunctionDescriptor.of( ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.ADDRESS @@ -54,17 +56,19 @@ public DecodedImage decode(byte[] webpData) { MemorySegment dataSeg = arena.allocate(webpData.length); dataSeg.copyFrom(MemorySegment.ofArray(webpData)); - MemorySegment result = (MemorySegment) this.webPDecodeRGBA.invokeExact( + MemorySegment result = (MemorySegment) this.webPDecodeARGB.invokeExact( dataSeg, (long) webpData.length, wPtr, hPtr ); if (result.equals(MemorySegment.NULL)) { - throw new IllegalStateException("WebPDecodeRGBA failed: invalid or unsupported WebP data"); + throw new IllegalStateException("WebPDecodeARGB failed: invalid or unsupported WebP data"); } int w = wPtr.get(ValueLayout.JAVA_INT, 0); int h = hPtr.get(ValueLayout.JAVA_INT, 0); - byte[] pixels = result.reinterpret((long) w * h * 4).toArray(ValueLayout.JAVA_BYTE); + int[] pixels = result.reinterpret((long) w * h * 4).toArray( + ValueLayout.JAVA_INT.withOrder(ByteOrder.BIG_ENDIAN) + ); this.lib.webPFree.invokeExact(result); return new DecodedImage(pixels, w, h); @@ -102,6 +106,20 @@ public int[] getInfo(byte[] webpData) { } } + @Override + public boolean isWebP(byte[] data) { + if (data == null || data.length == 0) return false; + + try (Arena arena = Arena.ofConfined()) { + MemorySegment dataSeg = arena.allocate(data.length); + dataSeg.copyFrom(MemorySegment.ofArray(data)); + + return (int) this.webPGetInfo.invokeExact(dataSeg, (long) data.length, MemorySegment.NULL, MemorySegment.NULL) != 0; + } catch (Throwable t) { + return false; + } + } + @Override public boolean isAvailable() { return true; 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 3331bbb..6c86b6d 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 @@ -9,26 +9,26 @@ public final class LibWebPEncoder implements PlatformWebPEncoder { private final LibWebPLibrary lib; - // size_t WebPEncodeLosslessRGBA(const uint8_t* rgba, int w, int h, int stride, uint8_t** output) - private final MethodHandle webPEncodeLosslessRGBA; - // size_t WebPEncodeRGBA(const uint8_t* rgba, int w, int h, int stride, float quality, uint8_t** output) - private final MethodHandle webPEncodeRGBA; + // 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 LibWebPEncoder(LibWebPLibrary lib) { this.lib = lib; Linker linker = Linker.nativeLinker(); - this.webPEncodeLosslessRGBA = linker.downcallHandle( - lib.lookup.find("WebPEncodeLosslessRGBA").orElseThrow(), + this.webPEncodeLosslessBGRA = linker.downcallHandle( + lib.lookup.find("WebPEncodeLosslessBGRA").orElseThrow(), FunctionDescriptor.of( ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS ) ); - this.webPEncodeRGBA = linker.downcallHandle( - lib.lookup.find("WebPEncodeRGBA").orElseThrow(), + this.webPEncodeBGRA = linker.downcallHandle( + lib.lookup.find("WebPEncodeBGRA").orElseThrow(), FunctionDescriptor.of( ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, @@ -48,18 +48,18 @@ public String backendName() { } @Override - public byte[] encodeLossless(byte[] rgba, int width, int height) { + public byte[] encodeLossless(int[] argb, int width, int height) { try (Arena arena = Arena.ofConfined()) { MemorySegment outputPtr = arena.allocate(ValueLayout.ADDRESS); - MemorySegment rgbaSeg = arena.allocate(rgba.length); - rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + MemorySegment argbSeg = arena.allocate((long) argb.length * 4); + argbSeg.copyFrom(MemorySegment.ofArray(argb)); - long size = (long) this.webPEncodeLosslessRGBA.invokeExact( - rgbaSeg, width, height, width * 4, outputPtr + long size = (long) this.webPEncodeLosslessBGRA.invokeExact( + argbSeg, width, height, width * 4, outputPtr ); if (size == 0) { - throw new IllegalStateException("WebPEncodeLosslessRGBA failed"); + throw new IllegalStateException("WebPEncodeLosslessBGRA failed"); } MemorySegment encoded = outputPtr.get(ValueLayout.ADDRESS, 0).reinterpret(size); @@ -75,18 +75,18 @@ public byte[] encodeLossless(byte[] rgba, int width, int height) { } @Override - public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + public byte[] encodeLossy(int[] argb, int width, int height, float quality) { try (Arena arena = Arena.ofConfined()) { MemorySegment outputPtr = arena.allocate(ValueLayout.ADDRESS); - MemorySegment rgbaSeg = arena.allocate(rgba.length); - rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + MemorySegment argbSeg = arena.allocate((long) argb.length * 4); + argbSeg.copyFrom(MemorySegment.ofArray(argb)); - long size = (long) this.webPEncodeRGBA.invokeExact( - rgbaSeg, width, height, width * 4, quality * 100.0f, outputPtr + long size = (long) this.webPEncodeBGRA.invokeExact( + argbSeg, width, height, width * 4, quality * 100.0f, outputPtr ); if (size == 0) { - throw new IllegalStateException("WebPEncodeRGBA failed"); + throw new IllegalStateException("WebPEncodeBGRA failed"); } MemorySegment encoded = outputPtr.get(ValueLayout.ADDRESS, 0).reinterpret(size); diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java index 9686508..f404d3f 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java @@ -8,7 +8,10 @@ public final class MacOSFrameworks { static final int kCFStringEncodingUTF8 = 0x08000100; static final int kCGImageAlphaLast = 3; + static final int kCGImageAlphaFirst = 4; + static final int kCGImageAlphaPremultipliedFirst = 2; static final int kCGImageAlphaPremultipliedLast = 1; + static final int kCGBitmapByteOrder32Little = 2 << 12; static final int kCFNumberFloat64Type = 13; final SymbolLookup lookup; @@ -97,15 +100,17 @@ MemorySegment createCFString(Arena arena, String s) throws Throwable { ); } - static void unpremultiplyAlpha(byte[] rgba) { - for (int i = 0; i < rgba.length; i += 4) { - int a = rgba[i + 3] & 0xFF; + static void unpremultiplyAlpha(int[] argb) { + for (int i = 0; i < argb.length; i++) { + int pixel = argb[i]; + int a = (pixel >> 24) & 0xFF; if (a == 0) { - rgba[i] = rgba[i + 1] = rgba[i + 2] = 0; + argb[i] = 0; } else if (a < 255) { - rgba[i] = (byte) Math.min(255, ((rgba[i] & 0xFF) * 255 + a / 2) / a); - rgba[i + 1] = (byte) Math.min(255, ((rgba[i + 1] & 0xFF) * 255 + a / 2) / a); - rgba[i + 2] = (byte) Math.min(255, ((rgba[i + 2] & 0xFF) * 255 + a / 2) / a); + int r = Math.min(255, (((pixel >> 16) & 0xFF) * 255 + a / 2) / a); + int g = Math.min(255, (((pixel >> 8) & 0xFF) * 255 + a / 2) / a); + int b = Math.min(255, ((pixel & 0xFF) * 255 + a / 2) / a); + argb[i] = (a << 24) | (r << 16) | (g << 8) | b; } } } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java index 2db566d..86754d4 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java @@ -1,10 +1,12 @@ package org.redlance.platformtools.webp.impl.macos; import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; +import java.nio.ByteOrder; public final class MacOSImageIODecoder implements PlatformWebPDecoder { private final MacOSFrameworks fw; @@ -121,7 +123,7 @@ public DecodedImage decode(byte[] webpData) { MemorySegment colorSpace = (MemorySegment) this.fw.cgColorSpaceCreateDeviceRGB.invokeExact(); MemorySegment ctx = (MemorySegment) this.cgBitmapContextCreate.invokeExact( - MemorySegment.NULL, w, h, 8L, w * 4, colorSpace, MacOSFrameworks.kCGImageAlphaPremultipliedLast + MemorySegment.NULL, w, h, 8L, w * 4, colorSpace, MacOSFrameworks.kCGImageAlphaPremultipliedFirst ); if (ctx.address() == 0) { @@ -135,9 +137,11 @@ public DecodedImage decode(byte[] webpData) { this.cgContextDrawImage.invokeExact(ctx, 0.0, 0.0, (double) w, (double) h, cgImage); MemorySegment pixelPtr = (MemorySegment) this.cgBitmapContextGetData.invokeExact(ctx); - byte[] rgbaOut = pixelPtr.reinterpret(w * h * 4).toArray(ValueLayout.JAVA_BYTE); + int[] argbOut = pixelPtr.reinterpret(w * h * 4).toArray( + ValueLayout.JAVA_INT.withOrder(ByteOrder.BIG_ENDIAN) + ); - MacOSFrameworks.unpremultiplyAlpha(rgbaOut); + MacOSFrameworks.unpremultiplyAlpha(argbOut); this.cgContextRelease.invokeExact(ctx); this.fw.cgColorSpaceRelease.invokeExact(colorSpace); @@ -145,7 +149,7 @@ public DecodedImage decode(byte[] webpData) { this.fw.cfRelease.invokeExact(source); this.fw.cfRelease.invokeExact(cfData); - return new DecodedImage(rgbaOut, (int) w, (int) h); + return new DecodedImage(argbOut, (int) w, (int) h); } catch (RuntimeException | Error e) { throw e; } catch (Throwable t) { diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java index 0336f45..405c277 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java @@ -115,27 +115,28 @@ public String backendName() { } @Override - public byte[] encodeLossless(byte[] rgba, int width, int height) { + public byte[] encodeLossless(int[] argb, int width, int height) { // ImageIO doesn't support true lossless WebP; use max quality lossy - return encodeLossy(rgba, width, height, 1.0f); + return encodeLossy(argb, width, height, 1.0f); } @Override - public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + public byte[] encodeLossy(int[] argb, int width, int height, float quality) { try (Arena arena = Arena.ofConfined()) { // CGDataProviderCreateWithData stores the pointer — must use arena-allocated memory - MemorySegment rgbaSeg = arena.allocate(rgba.length); - rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + int bufSize = argb.length * 4; + MemorySegment argbSeg = arena.allocate(bufSize); + argbSeg.copyFrom(MemorySegment.ofArray(argb)); MemorySegment colorSpace = (MemorySegment) this.fw.cgColorSpaceCreateDeviceRGB.invokeExact(); MemorySegment provider = (MemorySegment) this.cgDataProviderCreateWithData.invokeExact( - MemorySegment.NULL, rgbaSeg, (long) rgba.length, MemorySegment.NULL + MemorySegment.NULL, argbSeg, (long) bufSize, MemorySegment.NULL ); MemorySegment cgImage = (MemorySegment) this.cgImageCreate.invokeExact( (long) width, (long) height, 8L, 32L, (long) width * 4, - colorSpace, MacOSFrameworks.kCGImageAlphaLast, + colorSpace, MacOSFrameworks.kCGBitmapByteOrder32Little | MacOSFrameworks.kCGImageAlphaFirst, provider, MemorySegment.NULL, false, 0 ); diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java index 6bb0511..f848e5a 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java @@ -1,6 +1,7 @@ package org.redlance.platformtools.webp.impl.windows; import org.jetbrains.annotations.Nullable; +import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import java.lang.foreign.*; @@ -88,7 +89,7 @@ public DecodedImage decode(byte[] webpData) { ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_INT ), - frame, guidPixelFormat32bppRGBA(arena), 0, MemorySegment.NULL, 0.0, 0 + frame, guidPixelFormat32bppBGRA(arena), 0, MemorySegment.NULL, 0.0, 0 ), "FormatConverter.Initialize"); MemorySegment wPtr = arena.allocate(ValueLayout.JAVA_INT); @@ -118,7 +119,7 @@ frame, guidPixelFormat32bppRGBA(arena), 0, MemorySegment.NULL, 0.0, 0 MemorySegment.NULL, stride, bufSize, buffer ), "CopyPixels"); - return new DecodedImage(buffer.toArray(ValueLayout.JAVA_BYTE), w, h); + return new DecodedImage(buffer.toArray(ValueLayout.JAVA_INT), w, h); } finally { if (converter != null) comRelease(converter); if (frame != null) comRelease(frame); diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java index cb9c706..8685a05 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java @@ -61,12 +61,12 @@ public String backendName() { } @Override - public byte[] encodeLossless(byte[] rgba, int width, int height) { - return encodeLossy(rgba, width, height, 1.0f); + public byte[] encodeLossless(int[] argb, int width, int height) { + return encodeLossy(argb, width, height, 1.0f); } @Override - public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { + public byte[] encodeLossy(int[] argb, int width, int height, float quality) { try (Arena arena = Arena.ofConfined()) { MemorySegment factory = this.com.createFactory(arena); if (factory == null) throw new IllegalStateException("Failed to create WIC factory"); @@ -134,13 +134,14 @@ public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS ), - guidPixelFormat32bppRGBA(arena) + guidPixelFormat32bppBGRA(arena) ), "SetPixelFormat"); int stride = width * 4; - MemorySegment rgbaSeg = arena.allocate(rgba.length); - rgbaSeg.copyFrom(MemorySegment.ofArray(rgba)); + int bufSize = argb.length * 4; + MemorySegment argbSeg = arena.allocate(bufSize); + argbSeg.copyFrom(MemorySegment.ofArray(argb)); checkHr(comCallInt( frame, FRAME_ENCODE_WRITE_PIXELS, @@ -149,7 +150,7 @@ public byte[] encodeLossy(byte[] rgba, int width, int height, float quality) { ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS ), - height, stride, rgba.length, rgbaSeg + height, stride, bufSize, argbSeg ), "WritePixels"); checkHr(comCallInt( diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java index 5f36248..0eee137 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java @@ -148,9 +148,9 @@ static MemorySegment guidWebpContainer(Arena arena) { (byte) 0x37, (byte) 0xca, (byte) 0x7c, (byte) 0xf3); } - static MemorySegment guidPixelFormat32bppRGBA(Arena arena) { - return writeGuid(arena, 0xf5c7ad2d, (short) 0x6a8d, (short) 0x43dd, - (byte) 0xa7, (byte) 0xa8, (byte) 0xa2, (byte) 0x99, - (byte) 0x35, (byte) 0x26, (byte) 0x1a, (byte) 0xe9); + static MemorySegment guidPixelFormat32bppBGRA(Arena arena) { + return writeGuid(arena, 0x6fddc324, (short) 0x4e03, (short) 0x4bfe, + (byte) 0xb1, (byte) 0x85, (byte) 0x3d, (byte) 0x77, + (byte) 0x76, (byte) 0x8d, (byte) 0xc9, (byte) 0x0e); } } From 4f657a161bcd3392cb02adcbafe422a160fef566 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 00:43:27 +0700 Subject: [PATCH 04/22] Tests --- .github/workflows/test-webp.yml | 51 ++++++++++++++ testing/build.gradle | 6 +- webp/build.gradle | 13 ++++ .../platformtools/webp/AutoBackendTest.java | 26 +++++++ .../platformtools/webp/CrossBackendTest.java | 66 ++++++++++++++++++ .../platformtools/webp/DecodedImageTest.java | 42 +++++++++++ .../platformtools/webp/JavaImageIOTest.java | 30 ++++++++ .../platformtools/webp/LibWebPTest.java | 59 ++++++++++++++++ .../platformtools/webp/MacOSImageIOTest.java | 44 ++++++++++++ .../platformtools/webp/TestUtils.java | 27 +++++++ .../platformtools/webp/WindowsWICTest.java | 39 +++++++++++ webp/src/test/resources/test.webp | Bin 0 -> 4080 bytes 12 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-webp.yml create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/AutoBackendTest.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/DecodedImageTest.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java create mode 100644 webp/src/test/resources/test.webp diff --git a/.github/workflows/test-webp.yml b/.github/workflows/test-webp.yml new file mode 100644 index 0000000..4cf89fd --- /dev/null +++ b/.github/workflows/test-webp.yml @@ -0,0 +1,51 @@ +name: Test WebP + +on: + push: + branches: ['main', 'webp'] + pull_request: + workflow_dispatch: + +jobs: + test-webp: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + name: Linux (libwebp) + install: sudo apt-get update && sudo apt-get install -y libwebp-dev + expected-decode: libwebp + expected-encode: libwebp + - os: ubuntu-latest + name: Linux (TwelveMonkeys) + expected-decode: Java ImageIO + - os: macos-latest + name: macOS (ImageIO) + expected-decode: macOS ImageIO + - os: windows-latest + name: Windows (WIC) + expected-decode: Windows WIC + + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 25 + + - name: Install libwebp + if: matrix.install + run: ${{ matrix.install }} + + - name: Make gradlew executable + if: runner.os != 'Windows' + run: chmod +x ./gradlew + + - name: Run WebP tests + shell: bash + run: ./gradlew :webp:test -Dexpected.decode="${{ matrix.expected-decode }}" -Dexpected.encode="${{ matrix.expected-encode }}" \ No newline at end of file diff --git a/testing/build.gradle b/testing/build.gradle index 2ff23bd..d2cb2fe 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -7,7 +7,7 @@ dependencies { implementation(project(":referer")) implementation(project(":favorites")) implementation(project(":progress")) - compileOnly(project(":webp")) + implementation(project(":webp")) } tasks.withType(JavaCompile).configureEach { @@ -20,6 +20,10 @@ jar { } shadowJar { + dependencies { + exclude(project(":webp")) + } + dependsOn(":webp:proguardJar") from(zipTree(project(":webp").file("build/libs/PlatformTools-webp-${version}-bundled.jar"))) mergeServiceFiles() diff --git a/webp/build.gradle b/webp/build.gradle index 49417c5..7563b6f 100644 --- a/webp/build.gradle +++ b/webp/build.gradle @@ -13,6 +13,19 @@ dependencies { // api(project(":common")) bundled("com.twelvemonkeys.imageio:imageio-webp:3.13.1") + + testImplementation(platform("org.junit:junit-bom:6.0.3")) + testImplementation("org.junit.jupiter:junit-jupiter-api") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +test { + useJUnitPlatform() + jvmArgs("--enable-native-access=ALL-UNNAMED") + modularity.inferModulePath.set(false) + systemProperty("expected.decode", System.getProperty("expected.decode", "")) + systemProperty("expected.encode", System.getProperty("expected.encode", "")) } tasks.withType(JavaCompile).configureEach { diff --git a/webp/src/test/java/org/redlance/platformtools/webp/AutoBackendTest.java b/webp/src/test/java/org/redlance/platformtools/webp/AutoBackendTest.java new file mode 100644 index 0000000..78f854f --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/AutoBackendTest.java @@ -0,0 +1,26 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +class AutoBackendTest { + @Test + void autoDecoderBackend() { + String expected = System.getProperty("expected.decode", ""); + assumeFalse(expected.isEmpty(), "No expected decoder specified"); + assertTrue(PlatformWebPDecoder.INSTANCE.isAvailable(), "Decoder should be available"); + assertEquals(expected, PlatformWebPDecoder.INSTANCE.backendName()); + } + + @Test + void autoEncoderBackend() { + String expected = System.getProperty("expected.encode", ""); + assumeFalse(expected.isEmpty(), "No expected encoder specified"); + assertTrue(PlatformWebPEncoder.INSTANCE.isAvailable(), "Encoder should be available"); + assertEquals(expected, PlatformWebPEncoder.INSTANCE.backendName()); + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java b/webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java new file mode 100644 index 0000000..96a584d --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java @@ -0,0 +1,66 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; +import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; +import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +class CrossBackendTest { + @Test + void allBackendsDecodeSamePixels() throws IOException { + byte[] webp = TestUtils.loadTestWebP(); + + PlatformWebPDecoder[] decoders = { + LibWebPDecoder.tryCreate(), + MacOSImageIODecoder.tryCreate(), + WindowsCodecsDecoder.tryCreate() + }; + + DecodedImage reference = null; + String referenceName = null; + int tested = 0; + + for (PlatformWebPDecoder dec : decoders) { + if (dec == null) continue; + + DecodedImage decoded = dec.decode(webp); + assertTrue(decoded.width() > 0, "Invalid width: " + dec.backendName()); + assertTrue(decoded.height() > 0, "Invalid height: " + dec.backendName()); + assertEquals(decoded.width() * decoded.height(), decoded.argb().length, + "Pixel count mismatch: " + dec.backendName()); + + if (reference == null) { + reference = decoded; + referenceName = dec.backendName(); + } else { + assertEquals(reference.width(), decoded.width(), + "Width mismatch: " + referenceName + " vs " + dec.backendName()); + assertEquals(reference.height(), decoded.height(), + "Height mismatch: " + referenceName + " vs " + dec.backendName()); + assertPixelsEqual(reference.argb(), decoded.argb(), + referenceName + " vs " + dec.backendName()); + } + tested++; + } + + assertTrue(tested > 0, "At least one decoder should be available"); + } + + // RGB is undefined when alpha=0 — skip those pixels + private static void assertPixelsEqual(int[] expected, int[] actual, String label) { + assertEquals(expected.length, actual.length, "Pixel count mismatch: " + label); + for (int i = 0; i < expected.length; i++) { + int e = expected[i], a = actual[i]; + if (e == a) continue; + if ((e >>> 24) == 0 && (a >>> 24) == 0) continue; + fail("Pixel [" + i + "]: expected 0x" + Integer.toHexString(e) + + " but was 0x" + Integer.toHexString(a) + " (" + label + ")"); + } + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/DecodedImageTest.java b/webp/src/test/java/org/redlance/platformtools/webp/DecodedImageTest.java new file mode 100644 index 0000000..71e15e6 --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/DecodedImageTest.java @@ -0,0 +1,42 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +class DecodedImageTest { + @Test + void pngCaching() throws IOException { + DecodedImage decoded = PlatformWebPDecoder.INSTANCE.decode(TestUtils.loadTestWebP()); + assertFalse(decoded.isPngCached()); + + byte[] png = decoded.toPng(); + assertTrue(png.length > 0); + assertTrue(decoded.isPngCached()); + assertSame(png, decoded.toPng(), "toPng() should return cached instance"); + } + + @Test + void fromPngPreservesPixels() throws IOException { + DecodedImage original = PlatformWebPDecoder.INSTANCE.decode(TestUtils.loadTestWebP()); + byte[] png = original.toPng(); + + DecodedImage fromPng = DecodedImage.fromPng(png); + assertEquals(original.width(), fromPng.width()); + assertEquals(original.height(), fromPng.height()); + assertTrue(fromPng.isPngCached()); + assertArrayEquals(original.argb(), fromPng.argb()); + } + + @Test + void isWebPDetection() throws IOException { + assertTrue(PlatformWebPDecoder.INSTANCE.isWebP(TestUtils.loadTestWebP())); + assertFalse(PlatformWebPDecoder.INSTANCE.isWebP(new byte[]{1, 2, 3})); + assertFalse(PlatformWebPDecoder.INSTANCE.isWebP(new byte[0])); + assertFalse(PlatformWebPDecoder.INSTANCE.isWebP(null)); + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java b/webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java new file mode 100644 index 0000000..420b5d0 --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java @@ -0,0 +1,30 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class JavaImageIOTest { + @Test + void decoderAvailable() { + JavaImageIODecoder dec = JavaImageIODecoder.tryCreate(); + assumeTrue(dec != null, "No WebP ImageIO plugin on classpath"); + assertEquals("Java ImageIO", dec.backendName()); + } + + @Test + void decodeTestFile() throws IOException { + JavaImageIODecoder dec = JavaImageIODecoder.tryCreate(); + assumeTrue(dec != null, "No WebP ImageIO plugin on classpath"); + + DecodedImage decoded = dec.decode(TestUtils.loadTestWebP()); + assertTrue(decoded.width() > 0); + assertTrue(decoded.height() > 0); + assertEquals(decoded.width() * decoded.height(), decoded.argb().length); + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java b/webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java new file mode 100644 index 0000000..565a120 --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/LibWebPTest.java @@ -0,0 +1,59 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; +import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; +import org.redlance.platformtools.webp.impl.libwebp.LibWebPEncoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +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"); + } + + @Test + void losslessRoundtripExact() { + PlatformWebPDecoder dec = LibWebPDecoder.tryCreate(); + PlatformWebPEncoder enc = LibWebPEncoder.tryCreate(); + assumeTrue(dec != null, "libwebp not available"); + + int[] original = TestUtils.generateTestImage(); + byte[] encoded = enc.encodeLossless(original, TestUtils.W, TestUtils.H); + DecodedImage decoded = dec.decode(encoded); + assertArrayEquals(original, decoded.argb(), "Lossless roundtrip pixels must match exactly"); + } + + @Test + void lossyRoundtripDimensions() { + PlatformWebPDecoder dec = LibWebPDecoder.tryCreate(); + PlatformWebPEncoder enc = LibWebPEncoder.tryCreate(); + assumeTrue(dec != null, "libwebp not available"); + + int[] original = TestUtils.generateTestImage(); + byte[] encoded = enc.encodeLossy(original, TestUtils.W, TestUtils.H, 0.75f); + DecodedImage decoded = dec.decode(encoded); + assertEquals(TestUtils.W, decoded.width()); + assertEquals(TestUtils.H, decoded.height()); + assertEquals(original.length, decoded.argb().length); + } + + @Test + void decodeTestFile() throws IOException { + PlatformWebPDecoder dec = LibWebPDecoder.tryCreate(); + assumeTrue(dec != null, "libwebp not available"); + + DecodedImage decoded = dec.decode(TestUtils.loadTestWebP()); + assertTrue(decoded.width() > 0); + assertTrue(decoded.height() > 0); + assertEquals(decoded.width() * decoded.height(), decoded.argb().length); + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java b/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java new file mode 100644 index 0000000..fcc0ba8 --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java @@ -0,0 +1,44 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; +import org.redlance.platformtools.webp.impl.macos.MacOSImageIOEncoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@EnabledOnOs(OS.MAC) +class MacOSImageIOTest { + @Test + void decoderAvailable() { + PlatformWebPDecoder dec = MacOSImageIODecoder.tryCreate(); + assertNotNull(dec, "macOS ImageIO decoder should be available"); + assertEquals("macOS ImageIO", dec.backendName()); + } + + @Test + void decodeTestFile() throws IOException { + PlatformWebPDecoder dec = MacOSImageIODecoder.tryCreate(); + assertNotNull(dec); + + DecodedImage decoded = dec.decode(TestUtils.loadTestWebP()); + assertTrue(decoded.width() > 0); + assertTrue(decoded.height() > 0); + assertEquals(decoded.width() * decoded.height(), decoded.argb().length); + } + + @Test + void encoderMayBeUnavailable() { + // macOS currently does not support WebP encoding via ImageIO + MacOSImageIOEncoder enc = MacOSImageIOEncoder.tryCreate(); + if (enc != null) { + byte[] encoded = enc.encodeLossy(TestUtils.generateTestImage(), TestUtils.W, TestUtils.H, 0.75f); + assertTrue(encoded.length > 0); + } + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java b/webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java new file mode 100644 index 0000000..1926fa7 --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java @@ -0,0 +1,27 @@ +package org.redlance.platformtools.webp; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +final class TestUtils { + static final int W = 64, H = 64; + + private TestUtils() { + } + + static int[] generateTestImage() { + int[] argb = new int[W * H]; + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + argb[y * W + x] = 0xFF000000 | ((x * 4) << 16) | ((y * 4) << 8) | 128; + return argb; + } + + static byte[] loadTestWebP() throws IOException { + try (var is = TestUtils.class.getResourceAsStream("/test.webp")) { + assertNotNull(is, "test.webp resource not found"); + return is.readAllBytes(); + } + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java b/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java new file mode 100644 index 0000000..f67af37 --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java @@ -0,0 +1,39 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; +import org.redlance.platformtools.webp.impl.windows.WindowsCodecsEncoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@EnabledOnOs(OS.WINDOWS) +class WindowsWICTest { + @Test + void decoderAvailable() { + PlatformWebPDecoder dec = WindowsCodecsDecoder.tryCreate(); + assertNotNull(dec, "WIC decoder should be available on Windows"); + assertEquals("Windows WIC", dec.backendName()); + } + + @Test + void decodeTestFile() throws IOException { + PlatformWebPDecoder dec = WindowsCodecsDecoder.tryCreate(); + assertNotNull(dec); + + DecodedImage decoded = dec.decode(TestUtils.loadTestWebP()); + assertTrue(decoded.width() > 0); + assertTrue(decoded.height() > 0); + assertEquals(decoded.width() * decoded.height(), decoded.argb().length); + } + + @Test + void noEncoder() { + assertNull(WindowsCodecsEncoder.tryCreate(), "WIC should not have WebP encoder"); + } +} diff --git a/webp/src/test/resources/test.webp b/webp/src/test/resources/test.webp new file mode 100644 index 0000000000000000000000000000000000000000..c2ffe1719c2c9aa5b29ec54aa7308ffafbaa4254 GIT binary patch literal 4080 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 literal 0 HcmV?d00001 From 852b0b9cbe0ea168a60a79a14146d2cd04585cda Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 00:44:55 +0700 Subject: [PATCH 05/22] Update MacOSImageIODecoder.java --- .../webp/impl/macos/MacOSImageIODecoder.java | 106 +++++++++++------- 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java index 86754d4..5b3ba03 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java @@ -16,10 +16,11 @@ public final class MacOSImageIODecoder implements PlatformWebPDecoder { private final MethodHandle cgImageSourceCreateImageAtIndex; private final MethodHandle cgImageGetWidth; private final MethodHandle cgImageGetHeight; - private final MethodHandle cgBitmapContextCreate; - private final MethodHandle cgBitmapContextGetData; - private final MethodHandle cgContextDrawImage; - private final MethodHandle cgContextRelease; + private final MethodHandle cgImageGetBitmapInfo; + private final MethodHandle cgImageGetDataProvider; + private final MethodHandle cgDataProviderCopyData; + private final MethodHandle cfDataGetBytePtr; + private final MethodHandle cfDataGetLength; private MacOSImageIODecoder(MacOSFrameworks fw) { this.fw = fw; @@ -52,30 +53,25 @@ private MacOSImageIODecoder(MacOSFrameworks fw) { fw.lookup.find("CGImageGetHeight").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) ); - this.cgBitmapContextCreate = linker.downcallHandle( - fw.lookup.find("CGBitmapContextCreate").orElseThrow(), - FunctionDescriptor.of( - ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, - ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.JAVA_INT - ) + this.cgImageGetBitmapInfo = linker.downcallHandle( + fw.lookup.find("CGImageGetBitmapInfo").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS) ); - this.cgBitmapContextGetData = linker.downcallHandle( - fw.lookup.find("CGBitmapContextGetData").orElseThrow(), + this.cgImageGetDataProvider = linker.downcallHandle( + fw.lookup.find("CGImageGetDataProvider").orElseThrow(), FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS) ); - this.cgContextDrawImage = linker.downcallHandle( - fw.lookup.find("CGContextDrawImage").orElseThrow(), - FunctionDescriptor.ofVoid( - ValueLayout.ADDRESS, - ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE, - ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE, - ValueLayout.ADDRESS - ) + this.cgDataProviderCopyData = linker.downcallHandle( + fw.lookup.find("CGDataProviderCopyData").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS) + ); + this.cfDataGetBytePtr = linker.downcallHandle( + fw.lookup.find("CFDataGetBytePtr").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS) ); - this.cgContextRelease = linker.downcallHandle( - fw.lookup.find("CGContextRelease").orElseThrow(), - FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) + this.cfDataGetLength = linker.downcallHandle( + fw.lookup.find("CFDataGetLength").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) ); } @@ -120,36 +116,30 @@ public DecodedImage decode(byte[] webpData) { long w = (long) this.cgImageGetWidth.invokeExact(cgImage); long h = (long) this.cgImageGetHeight.invokeExact(cgImage); + int bitmapInfo = (int) this.cgImageGetBitmapInfo.invokeExact(cgImage); - MemorySegment colorSpace = (MemorySegment) this.fw.cgColorSpaceCreateDeviceRGB.invokeExact(); - MemorySegment ctx = (MemorySegment) this.cgBitmapContextCreate.invokeExact( - MemorySegment.NULL, w, h, 8L, w * 4, colorSpace, MacOSFrameworks.kCGImageAlphaPremultipliedFirst - ); - - if (ctx.address() == 0) { - this.fw.cgColorSpaceRelease.invokeExact(colorSpace); + // Read raw pixel data from CGImage + MemorySegment provider = (MemorySegment) this.cgImageGetDataProvider.invokeExact(cgImage); + MemorySegment rawData = (MemorySegment) this.cgDataProviderCopyData.invokeExact(provider); + if (rawData.address() == 0) { this.fw.cgImageRelease.invokeExact(cgImage); this.fw.cfRelease.invokeExact(source); this.fw.cfRelease.invokeExact(cfData); - throw new IllegalStateException("CGBitmapContextCreate failed"); + throw new IllegalStateException("CGDataProviderCopyData failed"); } - this.cgContextDrawImage.invokeExact(ctx, 0.0, 0.0, (double) w, (double) h, cgImage); - - MemorySegment pixelPtr = (MemorySegment) this.cgBitmapContextGetData.invokeExact(ctx); - int[] argbOut = pixelPtr.reinterpret(w * h * 4).toArray( - ValueLayout.JAVA_INT.withOrder(ByteOrder.BIG_ENDIAN) - ); + long len = (long) this.cfDataGetLength.invokeExact(rawData); + MemorySegment ptr = (MemorySegment) this.cfDataGetBytePtr.invokeExact(rawData); + MemorySegment pixels = ptr.reinterpret(len); - MacOSFrameworks.unpremultiplyAlpha(argbOut); + int[] argb = readPixels(pixels, (int) w, (int) h, bitmapInfo); - this.cgContextRelease.invokeExact(ctx); - this.fw.cgColorSpaceRelease.invokeExact(colorSpace); + this.fw.cfRelease.invokeExact(rawData); this.fw.cgImageRelease.invokeExact(cgImage); this.fw.cfRelease.invokeExact(source); this.fw.cfRelease.invokeExact(cfData); - return new DecodedImage(argbOut, (int) w, (int) h); + return new DecodedImage(argb, (int) w, (int) h); } catch (RuntimeException | Error e) { throw e; } catch (Throwable t) { @@ -157,6 +147,38 @@ public DecodedImage decode(byte[] webpData) { } } + private static int[] readPixels(MemorySegment pixels, int w, int h, int bitmapInfo) { + int alphaInfo = bitmapInfo & 0x1F; // kCGBitmapAlphaInfoMask + int byteOrder = bitmapInfo & 0x7000; // kCGBitmapByteOrderMask + boolean premultiplied = alphaInfo == MacOSFrameworks.kCGImageAlphaPremultipliedFirst + || alphaInfo == MacOSFrameworks.kCGImageAlphaPremultipliedLast; + + int[] argb; + if (byteOrder == MacOSFrameworks.kCGBitmapByteOrder32Little) { + // Native LE int = BGRA bytes = ARGB int + argb = pixels.toArray(ValueLayout.JAVA_INT); + } else { + // Big-endian / default: ARGB bytes → read as BE int + argb = pixels.toArray(ValueLayout.JAVA_INT.withOrder(ByteOrder.BIG_ENDIAN)); + } + + if (premultiplied) { + MacOSFrameworks.unpremultiplyAlpha(argb); + } + + // Handle AlphaLast formats (RGBA) → convert to ARGB + if (alphaInfo == MacOSFrameworks.kCGImageAlphaPremultipliedLast + || alphaInfo == MacOSFrameworks.kCGImageAlphaLast) { + for (int i = 0; i < argb.length; i++) { + int px = argb[i]; + // RGBA → ARGB: rotate right by 8 + argb[i] = (px << 24) | (px >>> 8); + } + } + + return argb; + } + @Override public int[] getInfo(byte[] webpData) { try (Arena arena = Arena.ofConfined()) { From 8a45ad091d238eb0c1d18338fa7b7e85956ec4df Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 00:55:36 +0700 Subject: [PATCH 06/22] Fixes --- .../platformtools/webp/impl/imageio/JavaImageIODecoder.java | 1 + .../platformtools/webp/impl/imageio/JavaImageIOEncoder.java | 1 + .../platformtools/webp/impl/windows/WindowsComHelper.java | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java index 8aa8b08..8456348 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java @@ -23,6 +23,7 @@ private JavaImageIODecoder() { } public static JavaImageIODecoder tryCreate() { + ImageIO.scanForPlugins(); Iterator readers = ImageIO.getImageReadersByMIMEType("image/webp"); return readers.hasNext() ? new JavaImageIODecoder() : null; } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java index af9f128..9a7793d 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java @@ -22,6 +22,7 @@ private JavaImageIOEncoder() { } public static JavaImageIOEncoder tryCreate() { + ImageIO.scanForPlugins(); Iterator writers = ImageIO.getImageWritersByMIMEType("image/webp"); return writers.hasNext() ? new JavaImageIOEncoder() : null; } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java index 0eee137..565f612 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java @@ -90,7 +90,7 @@ private static class Holder { } static int comCallInt(MemorySegment obj, int vtableIndex, FunctionDescriptor fd, Object... extraArgs) throws Throwable { - MemorySegment vtable = obj.get(ValueLayout.ADDRESS, 0).reinterpret(PTR_SIZE * (vtableIndex + 1)); + MemorySegment vtable = obj.reinterpret(PTR_SIZE).get(ValueLayout.ADDRESS, 0).reinterpret(PTR_SIZE * (vtableIndex + 1)); MemorySegment funcPtr = vtable.getAtIndex(ValueLayout.ADDRESS, vtableIndex); MethodHandle mh = LINKER.downcallHandle(funcPtr, fd); Object[] allArgs = new Object[extraArgs.length + 1]; From cf91f200ff2f6586ffb8aa5b7df6878b47600bcd Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 00:59:08 +0700 Subject: [PATCH 07/22] Update test-webp.yml --- .github/workflows/test-webp.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-webp.yml b/.github/workflows/test-webp.yml index 4cf89fd..70205b9 100644 --- a/.github/workflows/test-webp.yml +++ b/.github/workflows/test-webp.yml @@ -2,7 +2,8 @@ name: Test WebP on: push: - branches: ['main', 'webp'] + branches: + - 'main' pull_request: workflow_dispatch: From 0162e553af0872196cb6372419ec20c7f0784d5f Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 01:06:59 +0700 Subject: [PATCH 08/22] Update build.gradle --- webp/build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/webp/build.gradle b/webp/build.gradle index 7563b6f..3ae5dd5 100644 --- a/webp/build.gradle +++ b/webp/build.gradle @@ -26,6 +26,15 @@ test { modularity.inferModulePath.set(false) systemProperty("expected.decode", System.getProperty("expected.decode", "")) systemProperty("expected.encode", System.getProperty("expected.encode", "")) + + testLogging { + events("passed", "skipped", "failed") + + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat = "full" + } } tasks.withType(JavaCompile).configureEach { From 4c770227da58221476678331a3dea06b2882b546 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 01:12:20 +0700 Subject: [PATCH 09/22] Fix linux --- .github/workflows/test-webp.yml | 3 ++- .../impl/windows/WindowsCodecsDecoder.java | 26 ++++++++++++++++++- .../webp/impl/windows/WindowsComHelper.java | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-webp.yml b/.github/workflows/test-webp.yml index 70205b9..89d1d72 100644 --- a/.github/workflows/test-webp.yml +++ b/.github/workflows/test-webp.yml @@ -20,6 +20,7 @@ jobs: expected-encode: libwebp - os: ubuntu-latest name: Linux (TwelveMonkeys) + install: sudo apt-get remove -y 'libwebp*' || true expected-decode: Java ImageIO - os: macos-latest name: macOS (ImageIO) @@ -39,7 +40,7 @@ jobs: distribution: temurin java-version: 25 - - name: Install libwebp + - name: Setup native libraries if: matrix.install run: ${{ matrix.install }} diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java index f848e5a..9b6c82c 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsDecoder.java @@ -17,7 +17,31 @@ private WindowsCodecsDecoder(WindowsComHelper com) { public static @Nullable WindowsCodecsDecoder tryCreate() { WindowsComHelper com = WindowsComHelper.getInstance(); - return com != null ? new WindowsCodecsDecoder(com) : null; + if (com == null) return null; + + // Verify WebP decode support (codec may not be installed) + try (Arena arena = Arena.ofConfined()) { + MemorySegment factory = com.createFactory(arena); + if (factory == null) return null; + + try { + MemorySegment decoder = comCreateObj( + arena, factory, FACTORY_CREATE_DECODER, + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS + ), + guidWebpContainer(arena), MemorySegment.NULL + ); + comRelease(decoder); + } finally { + comRelease(factory); + } + } catch (Throwable t) { + return null; + } + + return new WindowsCodecsDecoder(com); } @Override diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java index 565f612..0dfd43b 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsComHelper.java @@ -13,6 +13,7 @@ public final class WindowsComHelper { static final int IUNKNOWN_RELEASE = 2; // Decoder vtable indices + static final int FACTORY_CREATE_DECODER = 7; static final int FACTORY_CREATE_DECODER_FROM_STREAM = 4; static final int FACTORY_CREATE_FORMAT_CONVERTER = 10; static final int FACTORY_CREATE_STREAM = 14; From 8e98665dbb953fc7be29a8f0c9fbf7d0b4733586 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 01:15:26 +0700 Subject: [PATCH 10/22] Fix windows & linux --- .github/workflows/test-webp.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-webp.yml b/.github/workflows/test-webp.yml index 89d1d72..8a92162 100644 --- a/.github/workflows/test-webp.yml +++ b/.github/workflows/test-webp.yml @@ -20,13 +20,14 @@ jobs: expected-encode: libwebp - os: ubuntu-latest name: Linux (TwelveMonkeys) - install: sudo apt-get remove -y 'libwebp*' || true + install: sudo find /usr -name 'libwebp*.so*' -exec mv {} {}.disabled \; expected-decode: Java ImageIO - os: macos-latest name: macOS (ImageIO) expected-decode: macOS ImageIO - os: windows-latest name: Windows (WIC) + install: winget install --id 9PG2DK419DRG --accept-package-agreements --accept-source-agreements --source msstore --silent expected-decode: Windows WIC name: ${{ matrix.name }} From d87c97907de37dd6d58bca3ab24fcd5ab9c7d9c8 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 02:32:16 +0700 Subject: [PATCH 11/22] Images --- webp/src/test/resources/checkerboard.webp | Bin 0 -> 544 bytes webp/src/test/resources/checkerboard_ref.png | Bin 0 -> 709 bytes webp/src/test/resources/circle_alpha.webp | Bin 0 -> 2310 bytes webp/src/test/resources/circle_alpha_ref.png | Bin 0 -> 22680 bytes webp/src/test/resources/gradient_alpha.webp | Bin 0 -> 144 bytes webp/src/test/resources/gradient_alpha_ref.png | Bin 0 -> 562 bytes webp/src/test/resources/gradient_rgb.webp | Bin 0 -> 252 bytes webp/src/test/resources/gradient_rgb_ref.png | Bin 0 -> 4475 bytes webp/src/test/resources/noise.webp | Bin 0 -> 1602 bytes webp/src/test/resources/noise_ref.png | Bin 0 -> 6837 bytes webp/src/test/resources/solid.webp | Bin 0 -> 78 bytes webp/src/test/resources/solid_ref.png | Bin 0 -> 196 bytes webp/src/test/resources/tall_gradient.webp | Bin 0 -> 266 bytes webp/src/test/resources/tall_gradient_ref.png | Bin 0 -> 5546 bytes webp/src/test/resources/test_ref.png | Bin 0 -> 20007 bytes webp/src/test/resources/wide_gradient.webp | Bin 0 -> 248 bytes webp/src/test/resources/wide_gradient_ref.png | Bin 0 -> 6472 bytes 17 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 webp/src/test/resources/checkerboard.webp create mode 100644 webp/src/test/resources/checkerboard_ref.png create mode 100644 webp/src/test/resources/circle_alpha.webp create mode 100644 webp/src/test/resources/circle_alpha_ref.png create mode 100644 webp/src/test/resources/gradient_alpha.webp create mode 100644 webp/src/test/resources/gradient_alpha_ref.png create mode 100644 webp/src/test/resources/gradient_rgb.webp create mode 100644 webp/src/test/resources/gradient_rgb_ref.png create mode 100644 webp/src/test/resources/noise.webp create mode 100644 webp/src/test/resources/noise_ref.png create mode 100644 webp/src/test/resources/solid.webp create mode 100644 webp/src/test/resources/solid_ref.png create mode 100644 webp/src/test/resources/tall_gradient.webp create mode 100644 webp/src/test/resources/tall_gradient_ref.png create mode 100644 webp/src/test/resources/test_ref.png create mode 100644 webp/src/test/resources/wide_gradient.webp create mode 100644 webp/src/test/resources/wide_gradient_ref.png diff --git a/webp/src/test/resources/checkerboard.webp b/webp/src/test/resources/checkerboard.webp new file mode 100644 index 0000000000000000000000000000000000000000..952c2ad9b9f7ddff1a4f1d73ada3042eabac8e09 GIT binary patch literal 544 zcmV+*0^j{oNk&E(0ssJ4MM6+kP&gnA0ssJT3jmz~DnI~006vX2lt-l^q9HNp0H6_x zYz2?=Ls#|R?Z4`Pe*pXdrZq(Sen39}J%K;hC;0#N|Kfmu|MdUe0RCb81NaB~PwJol z{6Ihd`T&0b|Nr~`8bEY3B9cmO`wRd8{`yBM9si!UPtfH%-hZk7c8$mjSpTulS5*`L zt|%^ux_>yJx+3ZP;)3Xlr~iBE=--c!|3x7@)GoJU=1MVT*sCZ3E{M23Q*=^P6}3q& zZxJLOWnNgbdS&=(*U*!OgWfEe$n4Wyc0k3KX8fV)$O}-G&jk2fuOz4}{U(E1>{q*I zb#+gRcbW^2K_Yg&8!M!HySJEm0{-sp(~Z!QgS*V6sN7mhiE)x+Men*VebIaD#lZ`3 zwB|M|MgUmn_TN7Ieb?jezaMqZ@Fj0<-271X5_nl<=+&RDto?Oo>#IKm@diZ^E#K^2|O&a_GGypLP`sP2Rwv1? i$>SbR7-pkov1u+P&JLk@2`DZE9P$w7kcT{kBme*bE(dV{ literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/checkerboard_ref.png b/webp/src/test/resources/checkerboard_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..57941349e4419c79b3e64ba8342281ae718061f4 GIT binary patch literal 709 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLH@YF!gx4IEG|2zP)bfFXAZC@bLTZ zyqm^}x-QbsR-DIo4BgmV||0beE`!k)dro3ie>0aZ`%Z28$bLX1h zS1+teWxpoAZ>N#DF8||_#~CXXOtswi?JKxbGru;lT50kx_eAfRN|!~qeV@Gb^sjRU zl`mgbz2wQai%n&>F^Wyhp0Oum-(vgquNr1f^I(1c>9^PccdwQk@h`;UZ#Evb_&enb z>#@RZM>qmr7)q7Dce)(=P>S!%tIr#by!iT7e{a!~#5-rr`lii#uO{`@Q$9fJ&w;7( zVSa2*rar$|`@b!zuUyX0_2pFgcmJ{l$%|O~XTM(%UV5NMXzc+J8wYmzm9B1AYmOhk zxalnuQ{SGB%ZZo7&s&_kllkuNgY)-%@4jT*w%Vny=FVB45M8ZDS0~WaZTPeC|${3A#Ogq%=X2mxcA#{Zocx{Kxn-< eo5^*@ddA|jGB)`c4-Ny9CxfS}pUXO@geCw*%0-?4 literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/circle_alpha.webp b/webp/src/test/resources/circle_alpha.webp new file mode 100644 index 0000000000000000000000000000000000000000..538f47b33a8109d882ce8ab930b9951309694e9e GIT binary patch literal 2310 zcmV+h3HkO?Nk&Hg2mk2B{MnnW^egh@hwynmIaL9eZ2ojL{7D9bx1oZ0zOrQeKe+>7Ya~@gb0Pc1!c*lfFV!VJBi?b1={>#Mf$r;abNOp8DBn&B(t!y`0*Y7DokR#*>=HqZ19;U`6ECepp8YuP8&QhoX|X$9FMvT9CDKWQU}tFBz@Z1Bm}{1RN%X4pEF6F$6&)x(Tla2o zMD5bufiJ!waHlzC%>orGN-Ed6XMTP6{_#h#NUVY>Cw7husR=416N zM+ZhNap^Ny^hGU$$iYWAoFqQff^h}stTY^O1St6M1PNL&=tN>QB`7Muj!8_z^)G-6 z-}7`jL$OQMkmb0!utdrP3y`$8n8v%6gD{9`5+`awwt_0tdNVJT(5gURP(jV>xIH{zT zn3a|x03R%b2uV5|%vF586WnyVXN5`&n&u!xlG*^~Bu%K1G^c~GE76l+u8j%Yembex zNPr>PI*>4+M4+~Tf;q9Ex)W9hlaK~1&)E#&#S9PPm>%^3q$Z3dLGvu+3Q@odA%fHz zvumu%9kc+oWF^sPetiCAZaDvdcRw)nGUXdA@o5=5pL2zbrhFVF4agSGJHlb>&^MpU zz6tMfzLM%bSInJ6X8(`PzQ*ILt4# zH=cg){yzQQA5Z_g^ZWF_d*kVUcgNHJ?*BghyJI~4yJtN8yK6lCyKg-GyK_ACyB9d| zyPG)myB|3HcgJ|BU;QHbl#(I=71vS3?|{vAMLfbZ5=MHJn<64oxyE=_NGf#%Kv5ri7r( zN_zFEp__^>qHe1K?-}ln#OWra{VuiiJq86)XBaI82S^CBEA*kpY;bfH2`vPg63L-_1ILMORWctOLD!Q=PR7Kv41vh6Dr2EiW zkZ-(n2Jx20S()MW{e>jW(cR_&Tt+iZH=+lk=k9KhZ&N~CeJL7Oj*Dmri;KpV026d$T)*$Gqy(jlX+-Jh2LXA4EwL4-@I z3|w7&s4r(QULrn3(UqKe5yYg8s`NZYsGg4H>d^yraC5(#2% z;4o)~a0_Nk52|x}wyL`>mGP2QACu=lZSN={Z$4!Kz5D=I`$n19Q`%{wc1$=f)&KsJ z_t+xeSWgH4F}wT=fB%;OJZBlBM1(mV zQ7%hlmRWw*e;*f%;D+#Tn}}IC%(4KtvRAZIjjg0$!Qk`<1)Bhwyh5+`sJp8vv%V_s zU*a_J2`%9f*Rxsidj3j@6XJ0+uFX7o5oh+K;rFMss9oC43Nd_uM`I+NZyyJ zw@Twe!#)I;B0vS*qYXUgj{d!Gfa1kPco|s`l0&!w&ih(c$%On+sZC(|m3_e$RIgcg zFo^xBMWl`V(3`l8MVMRS6oBZiM6_}%LkF7f!{Vjl@|MbRnAO<-2$^0fPiva}yA_^$ zDMDC<5BT2A$GTm_4^7!aM>JvDj;)V0wOwZ31?9Rb66-X0L}^t_UBbF`3(dXM&i3PDTyYM-0Pg!-U;qFB literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/circle_alpha_ref.png b/webp/src/test/resources/circle_alpha_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e76b6f3a4e2507a887b03b7f2a6306b1134b7c GIT binary patch literal 22680 zcmV(;K-<5GP)rycFL2uK#if=J!vZTM9299FCU*b5{9)@Zcc;iUa^k0{|Qlfbj4k{DAy75P$IC zKS2CDi2oq*?|%4iApb!7K>J|$2eLdE0Kg*|00uMwkdFaaAY@ork6%AH9=smd!fRqJ zydIuDZ;xNo|GCirG4Wp(|7+p@wD8vtb|1ec|7(T69==QL4sCgN49{X8Y6+}X2Cy0} zfF0yfdKS8OMb;X}Nve$+G@!txNz!d-iWGzN8`aKBfo&N8?=KqgJ{y)C{A3qGh zd1LIG{@2HU{k-2SHe(s_%UKO2KHyaw068b&@h^7Z%8uel2>`}*#C zGl1>q-{TTg-qGskdq3}#hzb15mHqnIe*Cxp9*TeoQosr%yT&mj%7Txs2LSw!Mb<#V zh_DEXG-->Zx*>VLul!8D1_|;XUw1Xb=MFyci92JqbWOpl#LzP|nT=4XoLP7hrNhuk-V^c%0Wf7TnKm`k3$vhgci3 z1ukI{TtXDQBuQ{hk>b|MXmf3VF&oO*X9NHca^39W#S`XQvj|*(|7z<0;sLmKSL8Lh z0^WLnBUnGXo|FlhBNzntdoJJK+*7PfZ~@6+zFrxzYyhg2tGkf~XhQrScK$NE`Vbs| z2f(neL0|!j-A-BafBAO+EWy`c9S?!UAFLMgdSPUTb)Qk)`3B;63Cr8jJ?v>Q-85P_woO^%YXg*zkcTNb#dI- zHna1G7Hut7!fHtg5Jiy^4Ur@G6-dD=Obk9Az#>h{k}bhBE#!4cK#En@Ys4eKcb8w! z@qh3BNu=jsa26bro1G7Ul;Z(tz#P&5vztKI1H>8uzXWO504M=I@kuMk6TntXVC%}I zxWl-4m^A2P;bf=N29N1Aa7>;B4uNjzw0IAW;mPe0s`DMG1<@5h?fp~yLY>*3>z)Fq_4Hj6!pErnSi817Unm&MEFW0<_KVI5iEaf#|o4bppu=b7Z z;!dtVftaLqjetZi7bbVHlHf%R_AYpK3~KM+D&OsY!y9(*XAkCjQ~-dq0e9w+)vboktY@cQWz3juQ1e- z#1a&xhuYd@y(9pLLmBZG7KX_d1^_W24AJnDd(9w%n2;9Y7TphIm=m<$tzz_MJd}w| zjNf>KEYp~Vr6o-dXn0JXl}W5kct;NjEd5f}3Z|f?2ILab<}QHr1S=5G)UE!1;1L=2*6n-?OQ^F#C#gs>@Te(;)ob6sqqRpT&%!^ z4m92`p?Xnn-W7$!`FMmZy#3)u89}IqnXUeiJcK5D_+o|z3IreZv)CI++THgej0biu zU;%JUJ1*RKK@*E7p^$6B*gk*JkpP3o3t=V8>!nR?KKC{bt~V&(G(1KB3z*C{B;x^u z?sjMmWd~CWiNHhB%lIh#iiaD|joeA(mEgebJkbyFGYIo~4DaJ{A{vim3U&Zrenlpw z>{h8DkEG1M$qWpwj~3Lx+Fgk&K_Wqmd0jY0aFIU?CLQO(fdU&OVv<5YvYdDSdF`Q? zkUJoEw=b8ZVleT65*gG_8L<8k7VHOufjJ-twkX(^Asf&^V;z{So=0uro?ye8AOb8d zD6|pCFS{Y(OZyCYVbl4M6-DA%>|YwRIg}k-0IfLyO$iIU5KUREg|D8*^kXEBO#Rn% z&8HARfEmzYniN<}a4|p-JgLQ-{C+i$_AZc?(8E7+kbI0ln(glAc?n6o5S46zT&*Fo zcmS)#%2+N|)=Jrc%I<{r#(CGvCzO#Gt%HiT0B9JP#o3^H{Rs{$=ra`?%g+@xgvQuz zpMl_Anvbo`(qI9-*np`HOk-#<#aB_dX;9xG;kT z;xR&oCw5;dn3iGRWwEbWKj?`V8|aK zi~mqq$e|SF>>IJJpLk^)Jutxy!1VdZJ{y7`(w6E0;|Z4VG#O*UdsDy@zKlPKuL^X3 zWd%-h%bWTez!~trPgB%kG`D3`T&d|EMG>jo^;w+f> z4|qf4V?pxl#ezZQi-+{PUp5DF3XzY ze_?by7c5+wMAt8<22c+vg-U7M2ZxQL^iI za^qD-DNF48UKB*Jbk*JU%?gCYEe_oh&gf5BD3Q#*Q-4BONW}mKQvFk6|D}IbX7K(e z1yY0;SjHfon7gGE8t>(uIH~=R8o6uZDU=Jr{Hu7Q7OTSI)MW{K4hZ6bYK#5(7-*oP zn-Dmx77|lU2XTZ)D)*tLexSct&0Ihh57%H$aYI8zJ;@51y*S~wM^IBh%V8IJ4elcU zR54SVwwUKRgdt1hw(JwHA0kmtoev0_iJb+5ftD~QnOx}PuV7MF&ISBXFb(3(Z3~Et zxfn$t`6?7~_a;yxTh?MXVIWL*qR}S0dM}9+Qi}_G-2F-ughy`DSI6nPB|-|uz|0zo zlgAP4J|7)IxM&3gwn|$9VI5@XItSrl{2dX>Ggwh3?K`? z2)lGy|E%W|&tl+Y_Y?I1*&#x(KtLTZA2;1l(CYkLbNw*DwBk3U&q|^keulx71hAZe zUG|H!^nwH?gjU?DK~BQ~MZ4)~jag-$!dOlc&=iRbFs1EaVO38MTfzE)Li<{A)ebPz z(h^=J-QCm!yuko$u_qy>9ziit#tU56zu)r=$=wCL7-4@x4=sxL6Z%f|d5t<@@*qD6 zV`W-6@fi7@o7i-i=(1yNv)7f6trM zz`z@C;k6l5^I*;3LxiXajfNF>rErYV2d?`ZCp3MliFpZ$f8$xQ@TvHK z0;dFG{@#UlaqT0A+SR7ct7Yo}7)dFgiwYk=a`*5O0$Hw*a8Ga=c~wCG;98sj_{6Eo z5DvhTkVoZKHYHrp4>jYusZ85(e*2M9bV1aX$V1Gm1U?(FPvt}U$o zT(ptUuAj=iKpZkP(mg+z9!b>#dW~GT0KQ&X1Tm0pX}x*XcV4F4o1IwJj=mV+asW)E zvsOz-13-#8A}FWGqrK@P^t0oGU%yZhly5B_87oRm=3`+W8AEVXk%wq;Z+V$;yC5tg`K<~hMll% zOI~1SyUAp8wJTGrUA>H?!dyJmkG7A9onHY<5-<*K5^DZ9T-<$M?o2*;2?8OSf*n^h z@QON3qJb1XR|{fnFPK_D^Jl@}I4P{ycuz~d3ZebU?{&3-C5VDXE)J0Dy9JJn@&k&h zD0y!%M1)3x^cJmAY6Eo^O+G^ZLFQ8dh`8bR7((eGy&N*TpG*3oZwbgRt_z?6`%OZX zqLIPB;RkdRt*BQJz!WTiFV6>4D72<)gt#1UvH=SVFNImplALWVZd;WnU`o%bMH^cX zCf+~~(Yi>KJo6dA0x==OdLF=AE}w=s9!xgyROx0zCm1<@)=s_M&rymrVG~zjr&rq@ zPck=}mbir9P2 zke#j&Kwgy^j+C^#L9Y07Jje}AZk6l{h~)gQ!Nh(x9W;-_3c~QhlGoQM!&_n~1Wy9} zI;eP!U5Q%lsRu9*7uay@(ltM>g03BT7*5zg9bN+vAm~rye>{Su$&OvICFP3K zq?VuKt*aM^)Qp#AwhD}QWQt}UCv31T{~ypiBwn@z*@|FPAX#$rYI9S-zv;R!vHD3T zKqKizio@b&$Lll%w1hB8^CIpDpFf3H zuHb?|kQju28_3>;xlImMgfJ~%hpj9n$h>XPrU}8OttX^$^876V88?%`hN;6UnBbN0 zM%Bx_vvo8B;M<)a5V)+nXJjo8L-w>vp%6W(B}DUA*6?m%N!~})!>cce51fELQUlRd zQ^F6;pW0ZkLV$OO6{7WagoMqKYnO{;0a1QeM$h%~#pU!D1h}aH63M7yJzho;lp7X5 zx#7KOc-Nx1Tl{sbQ+|l zh3(4PMbHl>H@3wGHPKO16pi0fux2G*WDO$4nn$1KF!Z<>K=`zT(Gz5+hoRw6Q4=Y# zPSQk31Vw6&r>}iW4^MRjN!ZE*NJ{M2M}C^)tNs+I(^$IY1xl>kzKt1@C(5`IyGe&& zSrD`H=jsCR6@-EZXh(IJ1&7uGb|Q;kK`G~jiHl;HTya1Xlh{sa7!344`v+`IuAE%H zPWlNNmmFEuayJRv(S4J#2W?ui%IB2CW|m;MK<)U(t*a z+JI=CMfh~|&AFq1=%SX^>+T1j>H@>{J8J4615ZSHp5TFH;LL&A$PE&G~o#Q*sawO8wdp@mx(-XkhsR(xgO{TB+J7Pd0KMH)xa85FRj zbODKOE+1M&4Bcxic}RO3ewI2VTg1^rX_pwR7o)~|3v#rIjcz}LPXrte9UAwZy5>R~ zHbYe`Z1R8MI?L46^l72O|C^v5=q`S%dV5>AMDi#vePqS;$ITu|CE0xXyD8ovRR}F# zPFu!O*5X)wS1tYTS3+(Gz{KS{5d?ThJfg2^_|nx~cv8FOGvfMCGYr)KvS(5!kpnNg zhOG#;%=X@}nF0%M@&I1w7~Dr&ShxPX_?on?0Y#QjWEo0?Pig?%!anSnAWxXRWa0sR zDz#CUFAi=?W-dI*pgv^F006^&M&8#eN*K$DQeeLhg>U9QJE8bBopw*sClf|<%)KM!)Jsx)LV14|gCXCN(P0JNO8lKUskGqd0=EIC1M z7?+R=VnR^Ff6KCr$tL_$LHuw)Md4cbrz<9%W;A|6fDs8sZ*hVoc{}2a?iJyH&_|q9 z^=2o1`aA*de{^dyauTU@?4~Tmrr&wfq2l}lSRH?SOH@CZfIc^G#{gJ}-;i)aR@f&+ zW-tuX*G2yx2ei%Xe1;B+=ck{WPyD*6Kc6$w2;A)8*K>m_HL2deAwe|`h}z24NQdm& zfa@3&^-zP8W>I|I$N%leKi(Tj@fnhoZ-P&L&82BJU9PV>9*y-E{uLg?^)fpo;4F=# z>t>17qsbf8+yZTUDk5%Z2*EN@H)URVv5SO(T(P1;Y5iy*J=l}L@pE(gK9Xfz?$@E{ zD{50}ixg!aJK({)tK}MDY`s{`En%f8*OAH~MvT>SO^xj0eWaB0r?R1lKHr^uRlMTU zK9FVuObB36{f~Z7C3r5dYZ!o7(&=k2ub!}F_cDkPdi=%!tJ0~JogIch-bV9kmg#2P zKl?1WtQcT7q$-J)$U>Fgs)18M2*Re9$1)v4Ct3m-YYrEi#Rqn&a+X{}A1n{HxCsrA z2=d{K)ED#K?2=NEP{e1b9N22Bc;AT1v7X5fx8XFJqL0(@7f^1aF3n~E)tIMuuVgV8 zQGMfbi00f!Mq{j$WxLEA1*(*L9cg1gX&Xajb9UH4{)vVF5H~G&+#};pL5N#usxDFb zg#lJGqewj{?%^Yn`?8vaj}st`hyh&;kfptp&pbo%cuxX_O@vrZH4VP8bXuH+H;oR2 z$D>B(D2AAv5oGXm%nO7t@C@ilMZJK5S^C>S6VSB=bS(SobIWI7#Va7$lD9<5sHqV4O4`E@Z6dxHm4M zpTw{$^G`kzSMPd?D+;}jP49TU26N>R3q>VNLG-D@N-{aX$sOX6H>41e#uNU+08U9d zQM|PZ;}ir|)`<<2PHQ&9(VQ4UvR-(L?Knt*^bblcz<58mkrvV*f1~-4xw|K%k1lEm z21qG;NY9e<^)?fPK4z~Zvrds*95gFUN?SzJTD8QlR`k*vWMM@dmtb?=>WW+&%ke5| z;H2|uaN8CyS5FpnO9sXLZ;BS8zi^t_D#ZW++UEIbz7v_>(@nE=72iSsAX7n%j^hUm zZhE_ytGSJ-q|8%cX^180d>)|4S&krukhNa^;m1=jLhu+syKqP!C^rJ;8)eW8zd}j* zzzUAwqUW%IzJ*p#eD`h)KgFMD{8HI-nmd=NBuuEh6q!HXXF4rDK@a z1B6T}hY&@JNJ*ZA5t4#5Na%0BzLZ;AsUWC~2aw7e5aB<=0Hy*Nl520zLNEsZ#KI<- z$O9ylp;El)0K*w#$=yra@K;)Zew}#IoCTl2pslc46%$%@zMd*~9V@)?WT? z@F1@;Xfsvj6Ks(UfYAduFqe%66_J^SxNBO8$^Aaio@=k?k$CmbI6?T{0A^I2Vo6xX ztdY>HHH=`V-;+rS90~Gv$myqE#i(>!fGiE-Gg)=+bpv2MFn+UWM zX=S`JM{9}j1XHAO{$>~=Q$L7{*TNEBE==JSm>OMN#d5XaE)(-p^IyCWUZPs*{UL?@ zefIOYla67>)kme))#+XJ`fUq>hI<=1Yenm(dzo?a(a%xqF49H(YWKnNO_C&elQnAr z4@Rv`@L~Yt5j@zkxDIo`wK5Y0ggE zKPh;zt~v<|G<;f=Y$-Qtc!DGHj~s2EcKI(uD4Hqgi0kRb{3L5J776^T2q>(n49`3f zvfz-3eKAT`Ad`|kC&h{^n{Ri?ll4=yy_>s3ncfB%k1a zB1swJ!vPFT`s$I{GKH8-P+E&Y@@X-knWu)y1w>F+UU6rsxqtMQIaV0eK>-$D8`QFL42S3w*f9%%UiRH$dE0bOJ4$ zOd0IP(WilwF1|7=~MohA-*CCu-}tZh$O@o~%){pJWHE7}_Yh zP6h61>uo4cYRVnk}ur1o-e_qKAMhuuQR{ zI^y~dMFT?&yeW6lc8D~9BWsy1vc;8j8F5ywwxfj3VY4kyPjdD0)C z)nbIJPvo{Z1X;cqVB#^ zte@zCy^3IKIa|(f4+bedGjg@&gJ6rqt4=m}9G%~QWk3ix^HZYGS*-`-wC1h35{fli ze6ul(&LYxRN!&~QCIlqo$%aU4d4w=ry<8jyD=ndATf<03QrbB0sP;ZgF#JG=QISrj z?3|4hos3^hyFPfCc3=n(GYcTydz@r)x_tqZs6(%^32Z^<2TZykx{H6Y!thR#TDd7u zJjF6CWZSnEZXlDzPC_{r8EbHe%l8pjPnqwQVLa}zsHnIiY0mv5xd$aVw4c~C@GB@Q zEV&OKsf2>SDt43(a6_I`fyY)=tkge&NaALbA>!45u3m!JH+pF zNE*y;l3;)U*Z_rFNz?qvO*5iQNcPm);@0w7a!Q+X!YagS1mf{9vb&Zc%BoJLIu)Z? z2E+$he4KLC-RaE&X!LuRo~CvK0xt$&tCDO8AZtjAFoirYHi5{kuNMD?ig$&UNtRue zk9CR_qBrZ5w#9=mej@D55ep-qGK@p~m>t+aQ<)vayliEGkpQn#mW433ee$_va<80> zTh|>-m>gnmx{Iz4{TV%tvIk4!)P}bqb9h^4001BWNkl*j%t-c_AdwcBz)gdS}wWturtLyy7x7907g1VB&5Hpz)Jsqb!+mo^{O0Ir9&jfWQt zh53DMK8RaJy}~Dej$V$yQv-+sH~Id2Sq?W*+jnGJ%4agb4FSXjWOr^wva0)c@AwKr z)cS3kCE2DEMueh&bQD9EdcN9e2)J_$+R22L;qo^b?c?qI-xh{ZrzfPQR;I7*-YM`f zWf^B$7rrWOXFgwsKJKzbo768!da)^IdbPA?nbK_NS1}wzrW(*z+6GmP;T;4SPeHb< z_#`j#RsV_msiHqE8S%{?$~G`WLzmXU z6SG9Q+$M-5RzpRz!R&2+xWrSOm6$Cjag45!LOYNkr5g(IfO4Ex|q;7AM zVeZM!S7lrwri9k9aW~n86u9a5+eMC}#E`c;I^(IbVKZeF1mA%Da(6I0Obb9qC$Kqj zEry=~B+;0VT~QAe<2ALwec4}6WCm?Y^k_`tM^V;lZ*ix4CCXziU*)f6q)euWY+Gcm zN?zUd*8`{qa35Ju%&Q7WLom}zh@XG7ZoQqYmfm8|O{9jGCrJ9inf|eCpPHv$TJ{4r0`rLyW~d8UQQGWI0zRuCF(zXHr9CS~5K5Q}U=#`{ zsw{rkLGb|is&PbkU!Byloa_N*f&wfLw@1f-hd=HRc{KRV9}LF$<_lz=LBZkeOyW=d zpyh_j#ZdIGUSsWDRi_Dd#=z~06lPP>K#ORoPu^Di;B^{DoaUEgducmY2ZAuDlr zqJWK(GBpgEFPCpnPi1YoU4)GBHe=-`x6d3ws#uHVVUjs7++yE=j!;bZU=Vz zr2d*p@Sl2v>8^kB8|cN4zPa@+m?qOI0$c;~5)sNU)X`>(Vf9A^D|$=50Kh7D@y3@KNKdsK(o@}Weyu+k~N47wVIG9P)PuY@ch}^QMenL_E^uZ$h|Js zVn!l1cqa8TY5;2jhBLq9)lHn!By9(PCBjj+NOKDAkA%N2lUEOr)J`ob6QLpE6-4{Z z>xCr>(G(9=?mr5#czAbhD<1&x?A-Vp3kqE;pyS3xrdX;$Ccih_`=^GGbh~xt04uQh zXxi7_qRSFZG_DK798!mwA^Z}0DZd0ys(c}IBCIpQSTi#QClD{JW2o^Ct^Vmu`c%Y3 zS;`HgS$~`{YvazMGE95N=FrkE{&1+>QoCXEBD*9O-qaeCJFK>r7!Pn;R;1!TUO_)E zOSvzBw_Pp=A!^hd@rsx#*JV zR~oqZyZvkT--A~B@!2GT<2lm3QSIPE1zv{UD_ zGb!};{DuB~I@Dx-s^=Z#5Ogo7OyLf-OE1i{)3c0&)38F{L!Nk}XzCbC_PSZj6UJ|0 zEpCURP_et1*XOO%743|t z^hcP*yQ$d76&HOYy^yh#^l}u#pSuguBJdsj_hqhuux@SLA;*3Qt}ZN)hSZW6xtQ%n z>O9S+fz`s594Kar2FMv!5JT&fDk7W>TICW!Jj4f9@QF!)&>ng07$xLeiJ=I)+Ka7- zu@~d7tNLsGLUzgF@=5KS*8?JmpQVyBoDbIU2{|M;qjKRsOGki%aVZlcPK4lOR@^-@ zh7Y2xPX0(zOIsb^#-R|Yz%8uC6XM4!>Sl4-@3~$in5^^0H#&tc6kaaC%qX0T)riha zPQb6CeXH(GA;^r5sDRV_RgY#@704+ehb>U@^4&C;v7$F#mRLgC($f^V0;gj| z8H|v4C73xh$+9w#VTp&>XW4#N=B&^N7%qiVDHcu6JjvD3+}-<0D@u$3o+5_({&@=` z%n-}Hc!!yGavEw%21XJUY1EGoxWk>kn4Y{cUytHlOBkj2v-5M0o7Fr!td&$EvGoMJ z-YVpq6+hq-<1U4K*~7z9mIZGrp|t&fhl`Ju<34V=hs#+3@IiU{kGr2LTFMcy^9fqc z0Hf84+=Ugku6oD>NOq8w%L}x@mAB^6$dwUSfhqxe{LTny0Q<-T-#C8XXOqe9*8TEy z0Ke2pH%{jjN^r!J;yU{~Yj!!;wR6AxKKSXneblbYZaR!#{Hbt{$pqhAkN&1w zI1glaaC=99^sQ=@Ry3akYa0)ri*8jckR9JaMtQ;8$l#lXh72+QDu<{Hd3Cc`ATSRyAA| za&Nl6z_XKkkkPLwxd>7Tf&8T7(nrPwIfs!KQkZ>ZcXd{ht|9F zxo|^tt}2xrFm8^>vrOsdDv$-*w-6^Lut7 zaPxLx?m#;RGhJ7YUz;&Dd3V(B$9!bCUe~vtR`vMLw&r|K=WNedeNy9WqikIuO>w4x zXD@3fF2m+c8Znx#;ODhEZy>;?4B*|dA6I?%Qmn~vg^c-L%gxqH2f6h}LBJZb#_K!HZKo8UO={PJwaRHr#g9T18g@Vmr->-PIFI`+z zsrKa#`gRka2(`4)-mtJ3lS~&s4In@K?TjBheJT_$Tv;o={WcK=R#-KLdXE@u*3f^fI zpo7(2{}6;71l*r-ZWK1V-s`Np-(>WnXCL9+NO@pA)sV|>Ap zYFzg=cFJYayT*NfKhyUE0=G)w9ALQ2qj4puDbg@iQCo@^sl^ksJxnsi02#0yYV^%+ z5|*6qkh4{b89ecjr4+NnawB5gi4D>Lk?YTOAAtRy7lV8b9Hdj(iPOm~zjbJ^!%h4I zUXfUP&tn76rXxbl#K;_xmCt|Uj%nJLVqaGdfbjV4RD=hJ{LukT^44WV8L5hX#DB+u}Y>f{h}8ETm!)TeCUVg31rBNJr~Vi?D+A~yaSU` zkG*Ua>f+HLQ?K{&%)lFWNTVmE;vU*`0c{kqn?XhLRZO%})X#}5vc_Fe+~Qz*p5kvV z^}|4}~IL>hM1HO0{yX!~#0IutowKi~e%_V17aJmG>`P?+OIqffsvA2o@H@2 zc3$aW83M8(y{uo3C`<252%q?fR#R-`@Z`}C=%Uwjj!;)s^^rgJMwK7Nml_EC=JuBw z6)+a4lPo*p<${`&DAw@9#GMFj;7pTGFt$NPEL^k~&2q|!^#yJCz^6 z*PdjJ>ST~io+zlf(0vR4vet&piP!p!I}GuUo3G7GJ43J$m|P+*aH;MX4dK^K8&;XF zdD`;)w8J9z*-2gh;P%vJFy;IT3$fD&MV@zOf^YiK+vM7F&BB|=9@;-E_tBTZm6Ad~8R%viER}={gT^!o8nOPmC zffutFH__EJnvvh$#RmPYb}jA(YP?H$#aW<7fjZ5UXg07etluBcX40J?_IX%p?Tl~B zV&?3gdX~~Ul#d@scYfUMImo626b#_JDmw@-+{pFO0FumhzQ~R7>-H3WnC9`^-#9Uz zx-sXhT%g$iW(x99T~5J?UrZOqgyE|V3q@l3i;i{^N^#Qa~Kj ze*mM0L}7lESuyM>VOP}Otm~sp>H;3Y=jg_iPeTAu>lGq!Qzbfd%rV2$-M+NbpXC(ri>7L%`s zINd1Cb81;wkc)j9^31t%_0rBCA)czL)Ou}pbVt{QTOkZS)iKxGFeEe!=N~+I^16hK zK@zA_SCUJfqWa2D>;k}rs3`gMjE9nW20*>wGnpAtE5!$J>e zj^=jlWesq}hxBfR8KoXFY+1nPLYW$e!*8=vwKN$5{+c&sg+&&VE*eXmg}k9Xd5Mcv zL{}M4f(^o3>^SELvguVY5`KBcn>7Tu>hlga?pI5M2dd6El-}MP#{j)vKU6DTk*|Fb z;PV81ymbb4`HEp4^G%Fuh*x7F_2;vs#Le88_Ny6*aX-9>UCL_9NR>qlv^TvrUZ*Gj z?Zw0@Pmf#!@)C#9<;(DLBisWrdll&rj8p_1cuy$T4bg$iP~{yo`|NR8H!>BGrfX5Rwe>QSx&GM*K$xJj@JUNi-g*tmbB5qto6GUX=Zkz4OZ)5kCrF~mv7PG%ke0GEr7xG`;r$LuyI z;aP0cy#>9?UDB(Jgk$4?>m)M*NH`+$dc4o9Wf4k^>7ZKF29Jx8c z%}{#AMdxgRx@`UcL?b+cH<%=_wK#%hCiA&QMOq{Y}y|DD;NDF#oAAHquI z7+%_$&BOFTUuJ~l)frTm67+Z9ufj?x48XcNLzdr})=&g1C^n(W#VGa~JM`v0Z4s@C z1^PAr+^@ZuRfa+>Fy07uH=TL$QPUhRTtf)1>uK#CSKqsD-s)vCgeG+P+(_<5&`Q~>vA9uuy0() zlp@8Outn!O?xOd4I^c72Dr|SYvB0|v>4~)xvn|kbTiQ<;Q@7+QxV%JTf7fg*W-pG?e21Bn(g!y ztIn?*l0$BePv0(evlQ;fd`U$54teP$`HK)MgjE4CyzC%NbQVt$_yOU{hmFn;T&085 z0+v2Dy7SQi2AN8o$yCN_)}mJ49Xv)l$vVyyEk|bqiO<^Wo(Z(D){J`G)Am#Cpr99!59ge zjAJ8;f*kf5uf1421fi4RH8^3;@s^i)Q@M12-2xUjzf2~!GY2g$yozWqDNl00`vx}N zPdm={(E}RqCI}3QhY2ez+gNwHh;Q+gfrbQqh!t~8NN~X0&rxPRu!dij))FA-h=||^ z^S3C}29AZl*~vVu?xAT+F@jMPD~t9zX~n8a8#G(E60saOL{iSgW;2y7Zri1oBK@70 z!A*;ub}bMOgU&ObG?M0QH5PU^J$1?@m*HK~Bk*<7gv0B{L=iZ6K)m?NVGMvcfL|L6 z-0R2ngaL2S;b5uRg64J5eCh^dCQ?&1l#>dOxU#+SW*Fohp5Pimd+9R@aXX;GAomh+ z0DLmw$cK*yKQg!tyHN_b)cM`K7eu0|;SB|*pM5S7WDBR3acrIt5?f>mda*qZH=4b& zmmQm*H(+HSR9U{OBz_`yB@*7vgqlni1lU2?T}^VyWm>gp%-X`1n5t9HM}{F9cLJ9} zWV4G;zP=;y%=y_z1(qA-Qn&{#5H*j9H(~Rpu+4%(Z4|R4tz7JBGhZr+Op0_|129`Y z78iDgm+Gtn+%{z3>}oxpVhXx5i87Sl+G_yv>@h4wxs(At1@AQgfxC(a`#CG;)>K#; zK2-F>xin;41ARS8t!XQMyf0ptlF}wNF*GB)X>ZwVv*T2w;lWzX;XMS-reEF690y|b z1Y(`A<2f8fbZ74SS06&MhOx04KBYA16s>7;1+49d83!+tIYYRo>fKP;ldXaQfbcK+ zVNipEoB5RISm830@JxvzG&F!a)}~=Rt-na8!OC99E2r}l@McmlEKNKG#+k7rN5>G* zN6w~JLi>gwt7YU3YQ>xM&}y*_1(L7R9(9>Xg`5NY9M#P+u0HkbQ$k`tULZ8wQffV0 zN{Hh(7;cx~jrm)}r_dfbVX@-gz|EH1^@PTt)6G6^a3Fe8cnZPQ2VCV+sLjwiO2Q}j zYHb6HncOHsDYg#K*%kW{N0(mSXz-&}0(m@}a_$wp+g4j8lRC;31AUpY&Ii1t(Kd3#B$Virthzrpg zOy!6?v-3+c+!x%Tj8M#O@t9)>BLbtWorAb{8SW9IDOdj#!T^!V9MS6H;Q zpLi;Z(>mnbk~tS_pv4L1Y-HXHL9|P2ZyW|a@%y2pT~9CjF)#b)FnQJMACnQP?Fk`>ER#E@*6D*`T^V_(2UZDv{3y)29 zxEsx%!;=Cp3RUdu0X}gEQRsfTs#N8+I;K$j${~xGy>*PLGtwhd;Kd4zrjw-O);IpB z9>7cMm?ekEa0cZKjChEUL)^HevGr_v7-k7>1R~h%Qta^AZMx`;Wi$OY`C^Hl0KXgW zKMVlh85XV{pbZ-tPP)}i-1>D~y;9X`=aAzs_y23`inim(aUDRF=l{QV(*o`Tz-rH& z*?ZQz8K)h`u}cy~f&j=TBWX1gbCt1D?@uoSOf9<)g5nOCal?uh@!|ZkT(`pgSy5`~ z%&v2`+uRA~(V3sOmB}*8PO{%L{p+&>-UUWm3ndE7WNx5|S8Mn7-!b^&A>Y0T_VmM= zXFRr^w6Du0MBE*E1yBj`+eDf?IG?GizPTGNmf)@{Y>uL0EhLKoqu_r!9TbYIf5sxO_!QcQux{82KRWa$0tXX2d zR_rK7z}T~kj^CT${TTcutZt-2O1Is(+74%O0m<7Ae#!qyo8O)vDJ6bhhY@p0T$_KA zYRA-HxPM4KDd*CW>hGjC-C}^f^PjW zx;aCvk8~~xcVOU_m&N?kmmvKVi%I#Mbxi||+zlf)0R(SmV$EI5mALUlfj^4)5rBvF zANY2o#}x-KbW(=s^;Cg;cq)+YsI^+O9RK7i_6iu(&}bUkxk5ou<3rBRIb zv^HnW&?rS;fLI4$pN3tCiW>p2IsfhjlOPVF(uwU-tAr%U z7{^5UxHI2Dd`zRWg5CPuJ`X*Ebp?dl!+XCmxI;X@1$0T%l~B zRt4D5t$BL~vb{jmHv(X)aYzJE4O>%TG|DFew2hm`Ao`#HUNt z1Jmpv&D8$s$EA#jBLIJ${w_(;r6n}<{lRz1dL4KpIm{3Odk=#_(6~MRN+uHpvI!zB zC9&mSP~RWdsoaT!wd2_Eyx2(E#m(wNc-k>#8M30JPm~Rv zjEoXe3MT=JyOSJdZ4yE9O)&ejtJDcGXqApBhLx-Kh-!vJ7ou*(#J0ET3Paumg8%rv zzK`w*GzV9cL6$mv(t0Z?W7{v=)}3@XX=OOfxC?&5+6~U%49o?3h+3%pdW-Y$e$W$p zfYcM$7R(>sfwK7M_1g4~-&8X1^BH~aMuDJiw*DJ=KD3bmQc1kC;PC-=_;hNrF!PH$ zIQA$8h&bkCF~|K!J6>t#kt7|f#FDiTAQsV#tFQglTm~FDsf4Ipx-KCx>q@iu@wkUAJwvCHLKeYIjE>!@hPV@s3xZ zn&(&obhG#Nn1by$410Rg7Rk#obs$jcAP{V#g6}4aOru`edm&quye{*lkja^}(L}`x z#moY4Uguk?XsOp$lG~J}?c+Ra;4r=KLz=Z-wcsKrL>8Yn1H}fi4`(3X3qM1tc$Crh zZ8Pq3kny55j%qqf2+#9ZUbeM3#ozrRyuo0CLm(6yV4ibqm_NgTuZLvVJ!ZfsGcMel zWOx0VyQOvvvV&0IiaiK6eek5iIDql-#Vf8(Nf=blKXal5ImZw!vI?Rjoz~n<_H6b& zGzEkRG)G|C$tRV8=eBAX4J^DphbDnzr7vL(6m%BXmUVF54VCx2xf)(#>dj_>{d5{{ z?s-k3W2B_2Gm?lW9AJKkvgyp8zfhUk-5gZbSf2r+&OOEE6IA}@4;1}iI;jR$6E_#D z^rP=A!GZ5|4HM2t>C>MzhRdGl@6|4Dvwwp01#t;qEb?~_NR0g*_)su2u}pm*&K{i> z3k1RsipBF}#Lo$@Q9yy8VEbZ5S0`nfEc95mba=}mxYT=QpW$GS?PJAO_7XsHsFHR;(Z1OV ziKsL(ALyHo9#d<_W)wdMn1UUoPk+4RmJw(eDTyGicvg!yMJJKkn>g{3CVXIJIB-hB zhtZP=TO^O*_>E%C)d}GP@<9AVERjE2Wr_t1 z6LO~+_@oP2cKXophnoP3n*{zzw2bKhEI~nOp}ByC2uf4#aII)o0LMWG-W$mrTt2ta zW=M*sRSD507TEK>^TUWvr_jkf1w{3fcDRZT-fsk8n@FI5D^GTATE@_ss7`}0N;3hO z=-VnG_kXS#8@>u;-YO%pOi!}O!?ZI+zNO$6Z@JewyTNlAB!m%9CX0ZZ1g_GJ58-Ol zW7Sx=_XK4qw9j)2tSG=v8mbY+q=7Z1>x=@p zlSX&LjuGY>vJ?=0RRobr(@z8zQh4`$l3tp9(!Svzr)gibi{4{x-S-xR=4v7jW9e@2j6mD*$E)uvfFrZRLlM|Rq zXGSWXqRAN98dn^KFV{v+Ptk&ZId+|@+2ZWhedOSzA%UHkv#>90emuSPvwP>2b}1P+ z!I`jkhex33`ghkBEt>#4e_$pFyc_0w4F~mxEh#myTLZ_%6*>K<>1=+YDXd5oTdyuJ zDG8ft76mv-2m z!fFrF%yT#&I=wPd1_E3JFm)_xD6iet+BBh~$T>yP85;#AEj&A+%DJfC2;jYK`<)Jf zLmVF%aq!C3X^VGYBZC&18W7i>ClsPc!v-85B=ZjyAzjcUn@RpUUM8o!3NW;?cq@Jc z0^c5B^F@4JHf)fH&$eFm@PT6&qr$4B4^#6udU#YuPv#G()GUEV=}}U)Vn;*ikQ!Za zEQcnSBA(L2(dc|n^Ngk}B)ZZyq{%d>in*386@my+o2gmVRsp@C!f)animFUjmGiRHn@x<=LcBdz>-Bt3-lpi;(s;{U@C`4RKukrbdRJ z+MSc2jHz383(S-pCC`G1@G!m+z^zTRg40r&a~cj_Bb`uM5W>ciL{s1fYP||SVI4W1 zHyKP9?=%_gNkr2gO2&~0MSqY@fJx851%@uILrR#7`Dl=1ypAQ!;LJ}BFB8N!0u&icqBk{h^EL78(Ygnx-Zx z9xRYKZ|(SaAUp%F{}Dqz&vkJA3f~UR0p@!JB%H;C7nU#W8${{ z&lQ)Mg89z_5O~sW1f97aJ489xL(bJw*xzAIcvb z-WLg!^rFSJtjx0{J&tKa{+W{;Isf>^OY44e>U}cmpuH5XUqXo^>B5By&N6veiy0fn zxee*Jz33I6E`=;4)6HsUg(X_;;pPIRqtDqMK%7h#DZ~vxl>MWJQRU~+20w8E}Cu5G|EJDob zYBjdGYQ;u`H$Y9_XR{>LPlv{>IiRG_lCnhdy=C&P#9JX@s_4gaZ5YIa8Txe_#U#h<^~R?xIYu)xa9 zqwtw@^m=0So*b%A)co_=)G>;?!95TAqI3k#3>c|C55^q5eme$mKiUSaq| zxR56vn?;@z>^|^zA%~v&RAXqgXjOo=6H4de5LDF1nE)ySpD{iODlEYo%7{%zi24&>8rmFsfeZ_FCyb!mz>LRcIiIvR{)E*ooo&I5qv=i`sz zXVN9y8>z9!-efSFMD-pONl(L>3(91{KL$U&(^abZ=hYf=!K7sw$2J6o~ICLE;n+XOyZX$;-tviP*xH$&3n(JLkd{UT? zM+SbJ1#Uu}2oNKwp8RxAvvbT6T=Ma?5$C^c01*5^QrBEvo$$d}LK1v^1l4ajUr7Cq zpSOVAv|(&y(J$a6t?)Tx={i!bkAuW*8CYwBvW0UAg{n1#H~}I6B{yKZcXYdhVfsLI zDf4^(e@}j8X`pY}*A=Sr*Ppi!K`2@V1ava=^sZ=dhl2M3oF^gQPafYp7od)FuU50! z{tTj3i9K;zywimbAoU^zw$Fhn&UxUg>+_s}|I_e)U7^ubZA|h`zfMnmu7cTiW9VqL ztHe&qTCh}whoH}eaB?4=Ln|dUTp?LA1)=f!2HB+_jXMrMvCkvmW zB!Wc`se;a>gpDyOB4qZ)10rkkPGL=$cz|hZG(Y&So^SYb6WHa0J$EbSt~-uY(@(AaS+5WAY{nL*#xnowQWlAQBryjSA4Gm8~6myjTqw10aA~Sg2)9z zjy@smVL6K|;*Lh9bZTvDJOP1Pjy~0}CCD03)V6#w2`3ML2b?^}BuLHpTk0)UQfTwD zW#ITA=F!B7LD7J>YjZsCn16zxj!fL|6)-ojby+|_093T=IVlUXMidV_7^UpvP)CCl zw6_q{$oIXr4ojho*Kj9orFU@{H=fDxY;y_;@np&0+?Jg5};?c?CaGq#RJMD+V^Cy z=ra>P!E{ZX>|a@1QksMGQ`=Sm`noD43_2y2g9E20IK>P{q{XXc#ijotPqT3&jJ@b=M(ip@Bf@MH)^KrcdqGbOvL&qSKs%&{0 z>cRxO$W$;DDFTKf2N<8CMwvS_lX(F%(I<=|N*(?nzpMV}#PN?jK?N5Ij#W)ajLszS z==eY3>%(46L9+TJgIf*`%Y~bS|01|m5-Hb(Tjv{MtoU?r5(?W8@+3n>(M29y1b9jg z@KzP+C<=)PPh1E$bS@M+1fR^K(W%s;ryASr2#}M29%Rg-QGV{U-`3+56}Wp%!|#kS zQPZe4YKEVI&)0<}C@O@g#HT!m*85^+d@drGb`~IhdZr6gg=hzZxPv6o2Fwmm-v10? zHS$dcNyL?Fz!^7c_*>=yK&FSB^-iQsr{NX06}^%>l=K$1kag$dg#K$GxP4vA+9ED{ zsPwSK0cu|#g>WHYkvDyU_5={OGGk<}xNp$Px>hzlw zRrEbQqws6eqJ0)sDfJ=b>80z8+*GDBCnBArG3=_orx75Slye&_3H^kD4 z;{NqU0M8Aw<@IGyeEQd~^&_Z#U6e(tD9P)2Tzo*bVM zdsdP&9bgXSCUUL&+%k|mlO~(LXL^#eCA)1XbV;4`7T!)k9U*R+p}eyk(r%8dZ;{m! zJ89+^C=8%1@Y@5Z2N+MX9Au2;fCwZq{4i6qlc1{R-()Rn+0$RDsJHA3MC|?c%XV1m z7hc{|9XGxkp`pH(xX8X;Kq$?j{Cp8|B>ne5ia&!k0vMJw)X}1Vh)Aj7|8L13Sy1RI zI*eQAl2s?Gn0!9};Zt;;Cv4u;<)i^90F+Ja#9W9KV9hbyXdO>A?2nBhVCnPBQ)Y z`v@Kn(gO%S5N=;)DkKSn5DA|8N!qaV*OqoIWQ4aXr_!9gGehLu}9yR$5lPN5O zZQ_Uke+jkRkByry6wj|hW*)p$YuEm_5x`5?D5S@60&0jfC?|t^kpw|DLEyv4>rmRa z+Gw1124=f|p@H^D0)1i#auqyb5z)WBhlK?RA zxXF-scHzUV_2e!zKpyPeydeW`;tXf;tyYS_yTHE|+wQ4v*`9urCxE@XpVm1pO%+B| zx}>h_0dn0*9o4S734ge64axi_wLy?rpl}n}X_Gm8MUtECqp=ZpEb&vKUh%aXn38U;-^LNY{~?>P3Hcz$ui!HXNey(0jl zO6&;GyaCVvq7)G%vq0Xr@y}s^p!*{HaU~vlrgDZ(%R`WHaxn=zYK9YjBCVGbeTsfNBYq1hD;E z66P?1Pa+EJvOWTUqPXxZlw*Ror#)wGvL*R@f%k3Zutrmx6<`f4_OtsOn)FR+;n_aK zvQT&7>sRz^1MKTQ8}(VVlw^&em#_DlJVnqrt9!d;5@l2D!k$yF0oY3)+a3a{Nkm)4 zV7KU=G4lW20PiOF$J_5aZ~o=u^DnRem-p|<|J& zzkh#wae)8wv;On<|Gew}`Aq(oU-~bv|M6bG-xnHA#}6;zlizkPp^VFTzsB!h|N8cS XYwz!6yw|jy00000NkvXXu0mjf;DZ3$ literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/gradient_alpha.webp b/webp/src/test/resources/gradient_alpha.webp new file mode 100644 index 0000000000000000000000000000000000000000..4eeae251b46c189c9fded60700c7e45e926c56bd GIT binary patch literal 144 zcmWIYbaU%qU|V<0pX2X8M-zY(SD-fvHPrZz)bp6BiD+#Uf2hZA6JfHi1y%_MQP zJdSCf0aE>h``S5*_r1ah0Pz|Nj6ti8GUhoA5-pnNW2HwE=6lq(!$H~G^E>sM777V1 z!4wc6EeMH#m4t+Y$|xP>xqz2z31Pj9!v6PS83HK)@A#T=u83=hDoWvss#OFV!^ZIX z9E9<6j6WIPI1dPEABj?q;JHD|B}dV3%D)IsfVBbEpcMmCtd|&kjmJ5*c_{L%+j^$P z=L4-LLc=S-2@p@5H0<;SaUPH`lh8V*kD_(#7YrW)^l9a#7w?}5Q2`@#9w4u)f5cU5 zb5`&i@a-Pf;0n+xoHhQof~H{2{^1qi1Xvqjja3d%W9H=Ne+_#_Ep63!cm+5C)&^KZ zod;+OBi1>g7OnuTg7y#1-8|0W3g8E{ansu4+#Y`xsDzpWY8mbwt^lnY%;%r z31l;YfB#r9Jg|U0{=*1o=Ww(ETceHyI_loL+#IDynOzV6w7CEO%Z)@Ap{gJM`d|S5 z^Fzn*!$;=-|HknY)D#j<7oFWf{?zUSJmBaV`UI~#VHn3x9UX(sAJrY01`g9{hTBen zdbx2Dz6W)U*IC#qI{x@vPC(T|8N+oTQdO(#Yig)mVBlBSAVP9`O*^Ul!aB` z!Mh4j7Y0ZhKnmdb$-9b8I=Y2RT8$GuyzzBkMAm&@CF%z$<}W3|(yxGcJdRzt86W^^ C>T_%W literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/gradient_rgb_ref.png b/webp/src/test/resources/gradient_rgb_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8b5f0b2e44b40225657661036d2ef555d615c4 GIT binary patch literal 4475 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 literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/noise.webp b/webp/src/test/resources/noise.webp new file mode 100644 index 0000000000000000000000000000000000000000..ace1c4e71b7da23af8fc5f14c0c885d297019c00 GIT binary patch literal 1602 zcmV-I2EF-GNk&FG1^@t8MM6+kP&gni1^@u?7XY0BDlh;r00ICsC9nc?$`C+ao9j{J zxWtGQAU42xfb)R#0q6n!r_ry}1NwLS-(U|k-}PU+KLDS$Kf0d)AGKfq`oKK^H-T6S zlCT482T&^GvIKB90zM~553OA9_k(=j`?s(M=zmi$)_>)Hbn^xIFZK`oKi_@;f0Tbu z|B?N_*-O<&>c_Cp>fimc)Ms5ysP))Q=foZ#2ZKe`@sPhw8}3gYGNH1&O2~E!aoHmJ zgpq=W;@>OyB6@(cZ`@y&rzt=H`)#FWEH7n;k-p8z)uT&buDyCi8CN`dkxU97=egQc z=E6c8hajn6gQ6Pug!>l@PgVV9(#Jsv+VH2qk7f8ghLf{&GX2x;`_bBGHI(>HXW&Xw zkGFa}>Yn|$^ae`jn5&+;EBhP|s!m}SLw$smUJfk2<+hlk&GN=dPF<|40i74qD48N9 zIY6+Kd{pwnK6697WA+e|P)C;XhBlE~EL!&)5$KKNJKa1b=M<7U`rHYh&1lAmw>Vs#VnAoEMdET;*WQ+KXTWhNj-@%ABg}4QPHwRYpACS zi*KISnYqWBX~vtV<=cp;4+PtmbTedo@@Lnuflg{#)}m`11xOxipAiVSZ|xQYsTk>& z^)S{@BpN8VjtFO>we5Y_B|QGTBA9+U9d3s*tT&BFpGflA-2)fQouvl5AX8 z!$F6d0Sb#|XTQZ_lt z5xaQzw13x8!Ft4bA5qX%C2YKSz159_23gMVkPG@in*VhuxMMV-_TAKPV!&`X7IU48 zU0)S|;GUS8Q0ucNWOEcj++TN&WyTnGy72UApJ|Qy@5W(OiiIVmX0j#KKpsq#i&~bk zdDx3rW=Xyx{G#2fP#pe6`X(VvyTK5YYM@B(@6l9A@-|nM=ml=sV1nn8-FbiaWS1S9RgM37QVMocF(bAYpjD4-f+j&<9xx1QehN+p4MoIm@=b%E1C%cK;hkN9 zo_KidQ+8q3+7kA8uGPH;qPns4T@pOy zMq_TI?cy32e&c4gv2Q>Pb&$C$dft|4{M84h{lRW*WP5#Ef@!OGaR>i>dw9ojV^8f? zgor&}lM`>>0P9z5^aDcn@%7+<&09jzaBoDT^jFO3`TMF7dH}(bb!f|yK*qQWEn3Ry zVV4#V;!y(y+e@0f#iEvjP*8#%EJ6k-gcO)hLVVcc=#<7}#rmW4M;B)-)gf@4k)JCK_r`z{@=Cc_=XBcl%?i+gbNPm4--qb)v z*jQvw4cacu?CKJ+A2qz>2Y#IGy~+YQ-f7IBIPy`Is|SM#6PQ%;26h+NWO7v{ioN z`K&aH&pIU;9x%RE{2BpHOU@a3G9dP_;)7WQSd_wZS}52lSU~2ricp)?#h`ejE#dNN zZtjFffqcILkSvzJbbc5IUlK``lH@&$=MDL7r_z&dpQ7@)@HKR=BeKj9&f@JN%Os|? ztt)`+y*o;mK)9sc^w6h+n^7oDltT7j)gR)V1JetKFW9{4@daf4uGsRV86}_q0PW8k A3;+NC literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/noise_ref.png b/webp/src/test/resources/noise_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..38cc28805015a26016a415f4249d455d63e0527b GIT binary patch literal 6837 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 literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/solid.webp b/webp/src/test/resources/solid.webp new file mode 100644 index 0000000000000000000000000000000000000000..4fdace68c0335adec3c6addb2e0e9ca94f5b205a GIT binary patch literal 78 zcmWIYbaQiKU|~;$rMh_0!WA408-zGh8P5D` iU`Pnr!NBnMxcrleubu_?|DU1$`uq8Ldw;3{bpilcPaAgt literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/solid_ref.png b/webp/src/test/resources/solid_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..b0d535335b9295a5558ef754eaf1c4903f6ac606 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={Wx;mmQWVsWi+f>iEbg zw4j~8!}GnsH~SU~z50YFlNQeRc{!)6V4AVZb7r2K3*}u(I+%q{mj@kenC_}F&1Ob| uLyg16)e{dk9A{)+mhToVx>#%hBg4K!Vqa$(pT7rm7K5j&pUXO@geCx*Zb>`< literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/tall_gradient.webp b/webp/src/test/resources/tall_gradient.webp new file mode 100644 index 0000000000000000000000000000000000000000..14926d6cd1494219189bbfa48393527363537003 GIT binary patch literal 266 zcmV+l0rmb;Nk&Ej0RRA3MM6+kP&gp=0000m2>_h|Dj)y=0X~sHol2#nqM@P~3;?ha z31$XxW&llyz)w=G{xH(su4j4VS?ZIkz!Tr7#PG}J|JKjD?~G4AzQw)Y`?|8y9gSx41&M*{*mH6iqgBG4#K Qzu895Gu~zguVT;u07W%{Bme*a literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/tall_gradient_ref.png b/webp/src/test/resources/tall_gradient_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..172c74864ee706a032bdfbce012e1dcdce99b886 GIT binary patch literal 5546 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 literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/test_ref.png b/webp/src/test/resources/test_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..7b0332eee41b7b2adf7d9baa31f54704c9112ac4 GIT binary patch literal 20007 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 literal 0 HcmV?d00001 diff --git a/webp/src/test/resources/wide_gradient_ref.png b/webp/src/test/resources/wide_gradient_ref.png new file mode 100644 index 0000000000000000000000000000000000000000..41d9e39b102fe8e1f7634a37c9a34011d79a8bc0 GIT binary patch literal 6472 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 ip8xNiuRi=IdHsLs!S2tSpi9pH0000 Date: Tue, 10 Mar 2026 02:48:00 +0700 Subject: [PATCH 12/22] Replace TwelveMonkeys with ngengine image-webp-java, add parameterized test suite - Replace buggy TwelveMonkeys VP8 decoder (43% pixel errors, issue #785) with ngengine image-webp-java pure Java WebP decoder - pixel-exact on all test images - Add 9 diverse test images (gradients, alpha, noise, checkerboard, solid, tall/wide) with libwebp reference PNGs for pixel-exact verification - Parameterize all backend tests (NgEngineDecoderTest, MacOSImageIOTest, WindowsWICTest, CrossBackendTest) over the full image set - Fix macOS ImageIO decoder bug: handle kCGImageAlphaNoneSkipLast (RGBX) format for opaque WebP images - Add NgEngineDecoder to CrossBackendTest for cross-backend pixel comparison - Remove JavaImageIODecoder, JavaImageIOEncoder and related ImageIO SPI config Co-Authored-By: Claude Opus 4.6 --- webp/build.gradle | 12 +-- webp/src/main/java/module-info.java | 2 +- .../webp/impl/PlatformWebPDecoderImpl.java | 6 +- .../webp/impl/PlatformWebPEncoderImpl.java | 9 +- .../webp/impl/imageio/JavaImageIODecoder.java | 89 ------------------- .../webp/impl/imageio/JavaImageIOEncoder.java | 80 ----------------- .../webp/impl/macos/MacOSFrameworks.java | 2 + .../webp/impl/macos/MacOSImageIODecoder.java | 5 +- .../webp/impl/ngengine/NgEngineDecoder.java | 69 ++++++++++++++ .../platformtools/webp/CrossBackendTest.java | 43 +++++---- .../platformtools/webp/JavaImageIOTest.java | 30 ------- .../platformtools/webp/MacOSImageIOTest.java | 21 ++++- .../webp/NgEngineDecoderTest.java | 45 ++++++++++ .../platformtools/webp/TestUtils.java | 42 ++++++++- .../platformtools/webp/WindowsWICTest.java | 21 ++++- 15 files changed, 230 insertions(+), 246 deletions(-) delete mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java delete mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java create mode 100644 webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java delete mode 100644 webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java create mode 100644 webp/src/test/java/org/redlance/platformtools/webp/NgEngineDecoderTest.java diff --git a/webp/build.gradle b/webp/build.gradle index 3ae5dd5..0db5fbf 100644 --- a/webp/build.gradle +++ b/webp/build.gradle @@ -12,10 +12,11 @@ configurations { dependencies { // api(project(":common")) - bundled("com.twelvemonkeys.imageio:imageio-webp:3.13.1") + bundled("org.ngengine:image-webp-java:0.1.1") testImplementation(platform("org.junit:junit-bom:6.0.3")) testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-params") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } @@ -40,16 +41,15 @@ test { tasks.withType(JavaCompile).configureEach { options.encoding = "UTF-8" options.release.set(25) + options.compilerArgs.addAll(["--add-reads", "platformtools.webp=ALL-UNNAMED"]) } shadowJar { archiveClassifier.set("shadow") configurations = [project.configurations.bundled] - relocate("com.twelvemonkeys", "org.redlance.platformtools.webp.shaded.twelvemonkeys") - mergeServiceFiles() + relocate("org.ngengine", "org.redlance.platformtools.webp.shaded.ngengine") exclude("META-INF/maven/**") - exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi") } tasks.register("proguardJar", ProGuardTask) { @@ -62,10 +62,6 @@ tasks.register("proguardJar", ProGuardTask) { // Keep all our classes, only shrink shaded dependencies keep("class !org.redlance.platformtools.webp.shaded.**, org.redlance.** { *; }") - // Keep ImageIO SPI entry points (loaded via ServiceLoader) - keep("class * extends javax.imageio.spi.ImageReaderSpi { *; }") - keep("class * extends javax.imageio.spi.ImageWriterSpi { *; }") - dontnote() ignorewarnings() dontobfuscate() diff --git a/webp/src/main/java/module-info.java b/webp/src/main/java/module-info.java index 3e3a22b..8a42be3 100644 --- a/webp/src/main/java/module-info.java +++ b/webp/src/main/java/module-info.java @@ -5,7 +5,7 @@ exports org.redlance.platformtools.webp.impl.libwebp to platformtools.testing; exports org.redlance.platformtools.webp.impl.macos to platformtools.testing; exports org.redlance.platformtools.webp.impl.windows to platformtools.testing; - exports org.redlance.platformtools.webp.impl.imageio to platformtools.testing; + exports org.redlance.platformtools.webp.impl.ngengine to platformtools.testing; requires java.desktop; requires static org.jetbrains.annotations; diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java index 4475983..dbc258f 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPDecoderImpl.java @@ -3,7 +3,7 @@ import org.jetbrains.annotations.Nullable; import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; -import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; +import org.redlance.platformtools.webp.impl.ngengine.NgEngineDecoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; @@ -61,9 +61,9 @@ public boolean isAvailable() { } } - // 3. Java ImageIO fallback (requires a WebP ImageIO plugin on classpath) + // 3. Pure-Java fallback (ngengine image-webp-java) try { - PlatformWebPDecoder backend = JavaImageIODecoder.tryCreate(); + PlatformWebPDecoder backend = NgEngineDecoder.tryCreate(); if (backend != null) return backend; } catch (Throwable ignored) { } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java index 7d48e80..c483ae0 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java @@ -2,7 +2,6 @@ import org.jetbrains.annotations.Nullable; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; -import org.redlance.platformtools.webp.impl.imageio.JavaImageIOEncoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPEncoder; import org.redlance.platformtools.webp.impl.macos.MacOSImageIOEncoder; import org.redlance.platformtools.webp.impl.windows.WindowsCodecsEncoder; @@ -60,13 +59,7 @@ public boolean isAvailable() { } } - // 3. Java ImageIO fallback (requires a WebP ImageIO plugin on classpath) - try { - PlatformWebPEncoder backend = JavaImageIOEncoder.tryCreate(); - if (backend != null) return backend; - } catch (Throwable ignored) { - } - + // No pure-Java fallback encoder available return null; } } diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java deleted file mode 100644 index 8456348..0000000 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIODecoder.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.redlance.platformtools.webp.impl.imageio; - -import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; - -import org.redlance.platformtools.webp.decoder.DecodedImage; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.stream.ImageInputStream; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Iterator; - -/** - * Fallback WebP decoder using {@link javax.imageio.ImageIO}. - * - *

Works when a WebP ImageIO plugin is available on the classpath - * (e.g. TwelveMonkeys, or a JRE with built-in WebP support). - */ -public final class JavaImageIODecoder implements PlatformWebPDecoder { - private JavaImageIODecoder() { - } - - public static JavaImageIODecoder tryCreate() { - ImageIO.scanForPlugins(); - Iterator readers = ImageIO.getImageReadersByMIMEType("image/webp"); - return readers.hasNext() ? new JavaImageIODecoder() : null; - } - - @Override - public String backendName() { - return "Java ImageIO"; - } - - @Override - public DecodedImage decode(byte[] webpData) { - try { - BufferedImage img = ImageIO.read(new ByteArrayInputStream(webpData)); - if (img == null) { - throw new IllegalStateException("ImageIO returned null: unsupported or invalid WebP data"); - } - - int w = img.getWidth(); - int h = img.getHeight(); - - return new DecodedImage(img.getRGB(0, 0, w, h, null, 0, w), w, h); - } catch (IOException e) { - throw new RuntimeException("Java ImageIO decode failed", e); - } - } - - @Override - public int[] getInfo(byte[] webpData) { - try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(webpData))) { - Iterator readers = ImageIO.getImageReaders(iis); - if (!readers.hasNext()) { - throw new IllegalStateException("No ImageIO reader for WebP data"); - } - - ImageReader reader = readers.next(); - try { - reader.setInput(iis); - return new int[]{reader.getWidth(0), reader.getHeight(0)}; - } finally { - reader.dispose(); - } - } catch (IOException e) { - throw new RuntimeException("Java ImageIO getInfo failed", e); - } - } - - @Override - public boolean isWebP(byte[] data) { - if (data == null || data.length == 0) return false; - - try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) { - return iis != null && ImageIO.getImageReaders(iis).hasNext(); - } catch (IOException e) { - return false; - } - } - - @Override - public boolean isAvailable() { - return true; - } - -} \ No newline at end of file diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java deleted file mode 100644 index 9a7793d..0000000 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/imageio/JavaImageIOEncoder.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.redlance.platformtools.webp.impl.imageio; - -import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; - -import javax.imageio.IIOImage; -import javax.imageio.ImageIO; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Iterator; - -/** - * Fallback WebP encoder using {@link javax.imageio.ImageIO}. - * - *

Works when a WebP ImageIO plugin is available on the classpath - * (e.g. TwelveMonkeys, or a JRE with built-in WebP support). - */ -public final class JavaImageIOEncoder implements PlatformWebPEncoder { - private JavaImageIOEncoder() { - } - - public static JavaImageIOEncoder tryCreate() { - ImageIO.scanForPlugins(); - Iterator writers = ImageIO.getImageWritersByMIMEType("image/webp"); - return writers.hasNext() ? new JavaImageIOEncoder() : null; - } - - @Override - public String backendName() { - return "Java ImageIO"; - } - - @Override - public byte[] encodeLossless(int[] argb, int width, int height) { - return encode(argb, width, height, null); - } - - @Override - public byte[] encodeLossy(int[] argb, int width, int height, float quality) { - return encode(argb, width, height, quality); - } - - @Override - public boolean isAvailable() { - return true; - } - - private byte[] encode(int[] argb, int width, int height, Float quality) { - BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - img.setRGB(0, 0, width, height, argb, 0, width); - - Iterator writers = ImageIO.getImageWritersByMIMEType("image/webp"); - if (!writers.hasNext()) { - throw new IllegalStateException("No ImageIO writer for WebP"); - } - - ImageWriter writer = writers.next(); - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writer.setOutput(ImageIO.createImageOutputStream(baos)); - - ImageWriteParam param = writer.getDefaultWriteParam(); - if (quality != null && param.canWriteCompressed()) { - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionType(param.getCompressionTypes()[0]); - param.setCompressionQuality(quality); - } - - writer.write(null, new IIOImage(img, null, null), param); - return baos.toByteArray(); - } catch (IOException e) { - throw new RuntimeException("Java ImageIO encode failed", e); - } finally { - writer.dispose(); - } - } - -} \ No newline at end of file diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java index f404d3f..c8bb2ea 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java @@ -9,6 +9,8 @@ public final class MacOSFrameworks { static final int kCFStringEncodingUTF8 = 0x08000100; static final int kCGImageAlphaLast = 3; static final int kCGImageAlphaFirst = 4; + static final int kCGImageAlphaNoneSkipLast = 5; + static final int kCGImageAlphaNoneSkipFirst = 6; static final int kCGImageAlphaPremultipliedFirst = 2; static final int kCGImageAlphaPremultipliedLast = 1; static final int kCGBitmapByteOrder32Little = 2 << 12; diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java index 5b3ba03..30274c6 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIODecoder.java @@ -166,9 +166,10 @@ private static int[] readPixels(MemorySegment pixels, int w, int h, int bitmapIn MacOSFrameworks.unpremultiplyAlpha(argb); } - // Handle AlphaLast formats (RGBA) → convert to ARGB + // Handle AlphaLast / SkipLast formats (RGBA/RGBX) → convert to ARGB if (alphaInfo == MacOSFrameworks.kCGImageAlphaPremultipliedLast - || alphaInfo == MacOSFrameworks.kCGImageAlphaLast) { + || alphaInfo == MacOSFrameworks.kCGImageAlphaLast + || alphaInfo == MacOSFrameworks.kCGImageAlphaNoneSkipLast) { for (int i = 0; i < argb.length; i++) { int px = argb[i]; // RGBA → ARGB: rotate right by 8 diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java new file mode 100644 index 0000000..2ac3984 --- /dev/null +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java @@ -0,0 +1,69 @@ +package org.redlance.platformtools.webp.impl.ngengine; + +import org.jetbrains.annotations.Nullable; +import org.ngengine.webp.decoder.WebPDecoder; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; + +import java.nio.ByteBuffer; + +/** + * Fallback WebP decoder using ngengine image-webp-java (pure-Java). + */ +public final class NgEngineDecoder implements PlatformWebPDecoder { + private NgEngineDecoder() { + } + + public static @Nullable NgEngineDecoder tryCreate() { + try { + // Verify the library is on the classpath + Class.forName("org.ngengine.webp.decoder.WebPDecoder"); + return new NgEngineDecoder(); + } catch (ClassNotFoundException e) { + return null; + } + } + + @Override + public String backendName() { + return "Java (ngengine)"; + } + + @Override + public DecodedImage decode(byte[] webpData) { + try { + var decoded = WebPDecoder.decode(webpData); + ByteBuffer rgba = decoded.rgba; + rgba.rewind(); + + int pixelCount = decoded.width * decoded.height; + int[] argb = new int[pixelCount]; + for (int i = 0; i < pixelCount; i++) { + int r = rgba.get() & 0xFF; + int g = rgba.get() & 0xFF; + int b = rgba.get() & 0xFF; + int a = rgba.get() & 0xFF; + argb[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + + return new DecodedImage(argb, decoded.width, decoded.height); + } catch (Exception e) { + throw new IllegalStateException("ngengine WebP decode failed", e); + } + } + + @Override + public int[] getInfo(byte[] webpData) { + try { + var decoded = WebPDecoder.decode(webpData); + return new int[]{decoded.width, decoded.height}; + } catch (Exception e) { + throw new IllegalStateException("ngengine WebP getInfo failed", e); + } + } + + @Override + public boolean isAvailable() { + return true; + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java b/webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java index 96a584d..5288818 100644 --- a/webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java +++ b/webp/src/test/java/org/redlance/platformtools/webp/CrossBackendTest.java @@ -1,25 +1,40 @@ package org.redlance.platformtools.webp; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; +import org.redlance.platformtools.webp.impl.ngengine.NgEngineDecoder; import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; import java.io.IOException; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; class CrossBackendTest { - @Test - void allBackendsDecodeSamePixels() throws IOException { - byte[] webp = TestUtils.loadTestWebP(); + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "test", // original test image (with alpha) + "gradient_rgb", // horizontal RGB gradient, opaque + "gradient_alpha", // alpha gradient + "checkerboard", // high-frequency pattern + "solid", // solid color + "circle_alpha", // circular alpha gradient 128x128 + "tall_gradient", // 32x256, tests vertical accumulation + "wide_gradient", // 256x32 + "noise", // pseudo-random noise pattern + }) + void allBackendsDecodeSamePixels(String name) throws IOException { + byte[] webp = TestUtils.loadWebP(name); PlatformWebPDecoder[] decoders = { LibWebPDecoder.tryCreate(), MacOSImageIODecoder.tryCreate(), - WindowsCodecsDecoder.tryCreate() + WindowsCodecsDecoder.tryCreate(), + NgEngineDecoder.tryCreate(), }; DecodedImage reference = null; @@ -30,26 +45,24 @@ void allBackendsDecodeSamePixels() throws IOException { if (dec == null) continue; DecodedImage decoded = dec.decode(webp); - assertTrue(decoded.width() > 0, "Invalid width: " + dec.backendName()); - assertTrue(decoded.height() > 0, "Invalid height: " + dec.backendName()); + assertTrue(decoded.width() > 0, name + ": invalid width from " + dec.backendName()); + assertTrue(decoded.height() > 0, name + ": invalid height from " + dec.backendName()); assertEquals(decoded.width() * decoded.height(), decoded.argb().length, - "Pixel count mismatch: " + dec.backendName()); + name + ": pixel count mismatch from " + dec.backendName()); if (reference == null) { reference = decoded; referenceName = dec.backendName(); } else { - assertEquals(reference.width(), decoded.width(), - "Width mismatch: " + referenceName + " vs " + dec.backendName()); - assertEquals(reference.height(), decoded.height(), - "Height mismatch: " + referenceName + " vs " + dec.backendName()); - assertPixelsEqual(reference.argb(), decoded.argb(), - referenceName + " vs " + dec.backendName()); + String label = name + ": " + referenceName + " vs " + dec.backendName(); + assertEquals(reference.width(), decoded.width(), label + " width"); + assertEquals(reference.height(), decoded.height(), label + " height"); + assertPixelsEqual(reference.argb(), decoded.argb(), label); } tested++; } - assertTrue(tested > 0, "At least one decoder should be available"); + assumeTrue(tested >= 2, "Need at least 2 backends to cross-check"); } // RGB is undefined when alpha=0 — skip those pixels diff --git a/webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java b/webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java deleted file mode 100644 index 420b5d0..0000000 --- a/webp/src/test/java/org/redlance/platformtools/webp/JavaImageIOTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.redlance.platformtools.webp; - -import org.junit.jupiter.api.Test; -import org.redlance.platformtools.webp.decoder.DecodedImage; -import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; - -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -class JavaImageIOTest { - @Test - void decoderAvailable() { - JavaImageIODecoder dec = JavaImageIODecoder.tryCreate(); - assumeTrue(dec != null, "No WebP ImageIO plugin on classpath"); - assertEquals("Java ImageIO", dec.backendName()); - } - - @Test - void decodeTestFile() throws IOException { - JavaImageIODecoder dec = JavaImageIODecoder.tryCreate(); - assumeTrue(dec != null, "No WebP ImageIO plugin on classpath"); - - DecodedImage decoded = dec.decode(TestUtils.loadTestWebP()); - assertTrue(decoded.width() > 0); - assertTrue(decoded.height() > 0); - assertEquals(decoded.width() * decoded.height(), decoded.argb().length); - } -} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java b/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java index fcc0ba8..114fc94 100644 --- a/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java +++ b/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; @@ -21,15 +23,28 @@ void decoderAvailable() { assertEquals("macOS ImageIO", dec.backendName()); } - @Test - void decodeTestFile() throws IOException { + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "test", // original test image (with alpha) + "gradient_rgb", // horizontal RGB gradient, opaque + "gradient_alpha", // alpha gradient + "checkerboard", // high-frequency pattern + "solid", // solid color + "circle_alpha", // circular alpha gradient 128x128 + "tall_gradient", // 32x256, tests vertical accumulation + "wide_gradient", // 256x32 + "noise", // pseudo-random noise pattern + }) + void pixelExactDecode(String name) throws IOException { PlatformWebPDecoder dec = MacOSImageIODecoder.tryCreate(); assertNotNull(dec); - DecodedImage decoded = dec.decode(TestUtils.loadTestWebP()); + DecodedImage decoded = dec.decode(TestUtils.loadWebP(name)); assertTrue(decoded.width() > 0); assertTrue(decoded.height() > 0); assertEquals(decoded.width() * decoded.height(), decoded.argb().length); + + TestUtils.assertMatchesReference(decoded, name); } @Test diff --git a/webp/src/test/java/org/redlance/platformtools/webp/NgEngineDecoderTest.java b/webp/src/test/java/org/redlance/platformtools/webp/NgEngineDecoderTest.java new file mode 100644 index 0000000..d55c1cc --- /dev/null +++ b/webp/src/test/java/org/redlance/platformtools/webp/NgEngineDecoderTest.java @@ -0,0 +1,45 @@ +package org.redlance.platformtools.webp; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.redlance.platformtools.webp.decoder.DecodedImage; +import org.redlance.platformtools.webp.impl.ngengine.NgEngineDecoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class NgEngineDecoderTest { + @Test + void decoderAvailable() { + NgEngineDecoder dec = NgEngineDecoder.tryCreate(); + assumeTrue(dec != null, "No Java WebP decoder on classpath"); + assertEquals("Java (ngengine)", dec.backendName()); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "test", // original test image (with alpha) + "gradient_rgb", // horizontal RGB gradient, opaque + "gradient_alpha", // alpha gradient + "checkerboard", // high-frequency pattern + "solid", // solid color + "circle_alpha", // circular alpha gradient 128x128 + "tall_gradient", // 32x256, tests vertical accumulation + "wide_gradient", // 256x32 + "noise", // pseudo-random noise pattern + }) + void pixelExactDecode(String name) throws IOException { + NgEngineDecoder dec = NgEngineDecoder.tryCreate(); + assumeTrue(dec != null, "No Java WebP decoder on classpath"); + + DecodedImage decoded = dec.decode(TestUtils.loadWebP(name)); + assertTrue(decoded.width() > 0); + assertTrue(decoded.height() > 0); + assertEquals(decoded.width() * decoded.height(), decoded.argb().length); + + TestUtils.assertMatchesReference(decoded, name); + } +} diff --git a/webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java b/webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java index 1926fa7..8070874 100644 --- a/webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java +++ b/webp/src/test/java/org/redlance/platformtools/webp/TestUtils.java @@ -1,8 +1,10 @@ package org.redlance.platformtools.webp; +import org.redlance.platformtools.webp.decoder.DecodedImage; + import java.io.IOException; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; final class TestUtils { static final int W = 64, H = 64; @@ -18,10 +20,42 @@ static int[] generateTestImage() { return argb; } - static byte[] loadTestWebP() throws IOException { - try (var is = TestUtils.class.getResourceAsStream("/test.webp")) { - assertNotNull(is, "test.webp resource not found"); + static byte[] loadWebP(String name) throws IOException { + try (var is = TestUtils.class.getResourceAsStream("/" + name + ".webp")) { + assertNotNull(is, name + ".webp resource not found"); return is.readAllBytes(); } } + + static byte[] loadTestWebP() throws IOException { + return loadWebP("test"); + } + + static DecodedImage loadReference(String name) throws IOException { + try (var is = TestUtils.class.getResourceAsStream("/" + name + "_ref.png")) { + assertNotNull(is, name + "_ref.png resource not found"); + return DecodedImage.fromPng(is.readAllBytes()); + } + } + + /** + * Compares decoded image against libwebp reference PNG (pixel-exact). + * Skips pixels where both have alpha=0 (RGB undefined for transparent pixels). + */ + static void assertMatchesReference(DecodedImage decoded, String name) throws IOException { + DecodedImage ref = loadReference(name); + assertEquals(ref.width(), decoded.width(), name + ": width mismatch vs reference"); + assertEquals(ref.height(), decoded.height(), name + ": height mismatch vs reference"); + + int[] ep = ref.argb(), ap = decoded.argb(); + assertEquals(ep.length, ap.length, name + ": pixel count mismatch vs reference"); + for (int i = 0; i < ep.length; i++) { + int e = ep[i], a = ap[i]; + if (e == a) continue; + if ((e >>> 24) == 0 && (a >>> 24) == 0) continue; + fail(name + " pixel [" + i + "] (" + (i % ref.width()) + "," + (i / ref.width()) + + "): expected 0x" + Integer.toHexString(e) + + " but was 0x" + Integer.toHexString(a)); + } + } } diff --git a/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java b/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java index f67af37..d961f78 100644 --- a/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java +++ b/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; @@ -21,15 +23,28 @@ void decoderAvailable() { assertEquals("Windows WIC", dec.backendName()); } - @Test - void decodeTestFile() throws IOException { + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "test", // original test image (with alpha) + "gradient_rgb", // horizontal RGB gradient, opaque + "gradient_alpha", // alpha gradient + "checkerboard", // high-frequency pattern + "solid", // solid color + "circle_alpha", // circular alpha gradient 128x128 + "tall_gradient", // 32x256, tests vertical accumulation + "wide_gradient", // 256x32 + "noise", // pseudo-random noise pattern + }) + void pixelExactDecode(String name) throws IOException { PlatformWebPDecoder dec = WindowsCodecsDecoder.tryCreate(); assertNotNull(dec); - DecodedImage decoded = dec.decode(TestUtils.loadTestWebP()); + DecodedImage decoded = dec.decode(TestUtils.loadWebP(name)); assertTrue(decoded.width() > 0); assertTrue(decoded.height() > 0); assertEquals(decoded.width() * decoded.height(), decoded.argb().length); + + TestUtils.assertMatchesReference(decoded, name); } @Test From eadd70f12ffca40e26d3cadfea007fce612e2a88 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 02:50:48 +0700 Subject: [PATCH 13/22] Update test module --- .../redlance/platformtools/testing/TestingApp.java | 11 ++++------- .../webp/impl/ngengine/NgEngineDecoder.java | 7 ++++--- 2 files changed, 8 insertions(+), 10 deletions(-) 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 bb5a4e9..536cd31 100644 --- a/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java +++ b/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java @@ -7,8 +7,7 @@ import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; -import org.redlance.platformtools.webp.impl.imageio.JavaImageIODecoder; -import org.redlance.platformtools.webp.impl.imageio.JavaImageIOEncoder; +import org.redlance.platformtools.webp.impl.ngengine.NgEngineDecoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPEncoder; import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; @@ -307,12 +306,10 @@ private void showWebPDialog() { } try { - JavaImageIODecoder iioDec = JavaImageIODecoder.tryCreate(); - JavaImageIOEncoder iioEnc = JavaImageIOEncoder.tryCreate(); - log.append(" Java ImageIO: decode " + (iioDec != null ? "OK" : "no WebP plugin") - + ", encode " + (iioEnc != null ? "OK" : "no WebP plugin") + "\n"); + NgEngineDecoder ngDec = NgEngineDecoder.tryCreate(); + log.append(" ngengine: decode " + (ngDec != null ? "OK" : "not found") + "\n"); } catch (Throwable t) { - log.append(" Java ImageIO: ERROR " + t + "\n"); + log.append(" ngengine: ERROR " + t + "\n"); } log.append("\n"); diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java index 2ac3984..960add9 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/ngengine/NgEngineDecoder.java @@ -1,6 +1,7 @@ package org.redlance.platformtools.webp.impl.ngengine; import org.jetbrains.annotations.Nullable; +import org.ngengine.webp.decoder.DecodedWebP; import org.ngengine.webp.decoder.WebPDecoder; import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; @@ -32,7 +33,7 @@ public String backendName() { @Override public DecodedImage decode(byte[] webpData) { try { - var decoded = WebPDecoder.decode(webpData); + DecodedWebP decoded = WebPDecoder.decode(webpData); ByteBuffer rgba = decoded.rgba; rgba.rewind(); @@ -55,8 +56,8 @@ public DecodedImage decode(byte[] webpData) { @Override public int[] getInfo(byte[] webpData) { try { - var decoded = WebPDecoder.decode(webpData); - return new int[]{decoded.width, decoded.height}; + DecodedWebP decoded = WebPDecoder.decode(webpData); + return new int[] {decoded.width, decoded.height}; } catch (Exception e) { throw new IllegalStateException("ngengine WebP getInfo failed", e); } From d5293bbf1721cc1cfcce00579f2f94af287d052c Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 02:51:19 +0700 Subject: [PATCH 14/22] Update test-webp.yml --- .github/workflows/test-webp.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-webp.yml b/.github/workflows/test-webp.yml index 8a92162..6cba822 100644 --- a/.github/workflows/test-webp.yml +++ b/.github/workflows/test-webp.yml @@ -19,9 +19,9 @@ jobs: expected-decode: libwebp expected-encode: libwebp - os: ubuntu-latest - name: Linux (TwelveMonkeys) + name: Linux (NgEngine) install: sudo find /usr -name 'libwebp*.so*' -exec mv {} {}.disabled \; - expected-decode: Java ImageIO + expected-decode: Java (ngengine) - os: macos-latest name: macOS (ImageIO) expected-decode: macOS ImageIO From b2732ebc641f271f2e3228a5bcdb7ce87165f455 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 02:55:14 +0700 Subject: [PATCH 15/22] Update test-webp.yml --- .github/workflows/test-webp.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-webp.yml b/.github/workflows/test-webp.yml index 6cba822..04395ef 100644 --- a/.github/workflows/test-webp.yml +++ b/.github/workflows/test-webp.yml @@ -4,6 +4,9 @@ on: push: branches: - 'main' + paths: + - 'webp/**' + - '.github/workflows/test-webp.yml' pull_request: workflow_dispatch: From f528fcc666a86101dec3f3e6b9a53ec774201294 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 02:58:33 +0700 Subject: [PATCH 16/22] Update README.MD --- README.MD | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/README.MD b/README.MD index 64e67d1..3ad9e91 100644 --- a/README.MD +++ b/README.MD @@ -13,7 +13,7 @@ The library is split into independent modules so you can include only what you n | **referer** | `referer` | File origin/referrer metadata (Windows & macOS) | | **favorites** | `favorites` | Finder Sidebar favorites (macOS) | | **progress** | `progress` | Taskbar/dock progress bars (macOS) | -| **webp** | `webp` | WebP image encoding/decoding (macOS, Windows, libwebp) | +| **webp** | `webp` | Compact WebP codec (libwebp, macOS ImageIO, Windows WIC, pure-Java ngengine fallback) | ## ✨ Features @@ -26,10 +26,11 @@ The library is split into independent modules so you can include only what you n * **[Taskbar Progress Bars](#4-taskbar-progress-bars)** (`progress`): Display progress indicators on the taskbar/dock icon. * Supports multiple stacked progress bars (up to 8). * *Note: Windows taskbar support is planned for future releases.* -* **[WebP Codec](#5-webp-codec)** (`webp`): Encode and decode WebP images using platform-native APIs (Java 25+, FFM API). +* **[WebP Codec](#5-webp-codec)** (`webp`): Compact, pixel-exact WebP decoding/encoding (Java 25+, FFM API). * **macOS:** ImageIO (CoreGraphics) * **Windows:** WIC (Windows Imaging Component) * **Cross-platform:** libwebp (if found on library path) + * **Pure-Java fallback:** [ngengine image-webp-java](https://github.com/NostrGameEngine/image-webp-java) (~92 KB bundled) * **Java 8 Compatible:** Built on Java 17 but distributed with a Java 8 compatible version (Multi-Release JAR). ## 📦 Installation @@ -194,24 +195,31 @@ public class ProgressExample { ``` ### 5. WebP Codec -Encode and decode WebP images using platform-native APIs. No bundled native libraries required — uses macOS ImageIO, Windows WIC, or system-installed libwebp. +Compact, pixel-exact WebP decoding and encoding module. The goal is **minimal footprint** and **zero native dependencies** out of the box — the bundled pure-Java decoder adds only ~92 KB (after ProGuard) while producing bit-identical output to libwebp. > **Note:** Requires Java 25+ (uses the FFM API). Other modules remain Java 8 compatible. +Decoder priority (first available wins): +1. **libwebp** — system-installed native library (fastest, encode + decode) +2. **macOS ImageIO** — CoreGraphics via FFM (decode only, no extra dependencies) +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 [TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys) WebP codec (~146K). Works on any platform without native libraries as a pure Java fallback. +- **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. ```groovy // Standard — requires platform support or libwebp on library path implementation "org.redlance.platformtools:webp:3.3.0" -// Bundled — self-contained, works everywhere +// Bundled — self-contained, works everywhere (decode only) implementation "org.redlance.platformtools:webp:3.3.0:bundled" ``` ```java import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; +import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; public class WebPExample { @@ -220,17 +228,17 @@ public class WebPExample { int[] info = PlatformWebPDecoder.INSTANCE.getInfo(webpData); System.out.println("Size: " + info[0] + "x" + info[1]); - // Full decode to RGBA pixels - PlatformWebPDecoder.DecodedImage image = PlatformWebPDecoder.INSTANCE.decode(webpData); - byte[] rgba = image.rgba(); // straight alpha, width * height * 4 bytes + // Full decode to ARGB pixels + DecodedImage image = PlatformWebPDecoder.INSTANCE.decode(webpData); + int[] argb = image.argb(); // width * height pixels } - public void encodeWebP(byte[] rgba, int width, int height) { - // Lossless encoding - byte[] lossless = PlatformWebPEncoder.INSTANCE.encodeLossless(rgba, width, height); + public void encodeWebP(int[] argb, int width, int height) { + // Lossless encoding (requires libwebp or platform encoder) + byte[] lossless = PlatformWebPEncoder.INSTANCE.encodeLossless(argb, width, height); // Lossy encoding (0.0 = smallest, 1.0 = best quality) - byte[] lossy = PlatformWebPEncoder.INSTANCE.encodeLossy(rgba, width, height, 0.75f); + byte[] lossy = PlatformWebPEncoder.INSTANCE.encodeLossy(argb, width, height, 0.75f); } } ``` From e608576c403b907f2b6c9536c764f53df4180c9b Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 03:03:34 +0700 Subject: [PATCH 17/22] Create TestingApp.run.xml --- .run/TestingApp.run.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .run/TestingApp.run.xml diff --git a/.run/TestingApp.run.xml b/.run/TestingApp.run.xml new file mode 100644 index 0000000..5982232 --- /dev/null +++ b/.run/TestingApp.run.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file From 8cde7f3baabec7649f0e02c7de38259f8a4c26be Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 03:10:59 +0700 Subject: [PATCH 18/22] Comment unused codes --- .../webp/impl/PlatformWebPEncoderImpl.java | 6 ++---- .../webp/impl/macos/MacOSFrameworks.java | 16 ++++++++-------- .../webp/impl/macos/MacOSImageIOEncoder.java | 4 ++-- .../webp/impl/windows/WindowsCodecsEncoder.java | 4 ++-- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java index c483ae0..2dbf633 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/PlatformWebPEncoderImpl.java @@ -3,8 +3,6 @@ import org.jetbrains.annotations.Nullable; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPEncoder; -import org.redlance.platformtools.webp.impl.macos.MacOSImageIOEncoder; -import org.redlance.platformtools.webp.impl.windows.WindowsCodecsEncoder; public class PlatformWebPEncoderImpl implements PlatformWebPEncoder { private final @Nullable PlatformWebPEncoder delegate = createBackend(); @@ -43,7 +41,7 @@ public boolean isAvailable() { } // 2. System frameworks - String os = System.getProperty("os.name", "").toLowerCase(); + /*String os = System.getProperty("os.name", "").toLowerCase(); if (os.contains("mac")) { try { @@ -57,7 +55,7 @@ public boolean isAvailable() { if (backend != null) return backend; } catch (Throwable ignored) { } - } + }*/ // No pure-Java fallback encoder available return null; diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java index c8bb2ea..c61c9d5 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSFrameworks.java @@ -23,10 +23,10 @@ public final class MacOSFrameworks { final MethodHandle cgColorSpaceCreateDeviceRGB; final MethodHandle cgColorSpaceRelease; final MethodHandle cgImageRelease; - private final MethodHandle cfStringCreateWithCString; + /*private final MethodHandle cfStringCreateWithCString; final MethodHandle cfDataCreateMutable; - final MethodHandle cgImageDestCreateWithData; + final MethodHandle cgImageDestCreateWithData;*/ private MacOSFrameworks(SymbolLookup combined, SymbolLookup imageIO) { this.lookup = combined; @@ -38,12 +38,12 @@ private MacOSFrameworks(SymbolLookup combined, SymbolLookup imageIO) { combined.find("CFRelease").orElseThrow(), FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) ); - this.cfStringCreateWithCString = linker.downcallHandle( + /*this.cfStringCreateWithCString = linker.downcallHandle( combined.find("CFStringCreateWithCString").orElseThrow(), FunctionDescriptor.of( ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT ) - ); + );*/ this.cgColorSpaceCreateDeviceRGB = linker.downcallHandle( combined.find("CGColorSpaceCreateDeviceRGB").orElseThrow(), FunctionDescriptor.of(ValueLayout.ADDRESS) @@ -57,7 +57,7 @@ private MacOSFrameworks(SymbolLookup combined, SymbolLookup imageIO) { FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) ); - this.cfDataCreateMutable = linker.downcallHandle( + /*this.cfDataCreateMutable = linker.downcallHandle( combined.find("CFDataCreateMutable").orElseThrow(), FunctionDescriptor.of( ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG @@ -68,7 +68,7 @@ private MacOSFrameworks(SymbolLookup combined, SymbolLookup imageIO) { FunctionDescriptor.of( ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS ) - ); + );*/ } private static class Holder { @@ -96,11 +96,11 @@ public static MacOSFrameworks create() { return new MacOSFrameworks(combined, io); } - MemorySegment createCFString(Arena arena, String s) throws Throwable { + /*MemorySegment createCFString(Arena arena, String s) throws Throwable { return (MemorySegment) cfStringCreateWithCString.invokeExact( MemorySegment.NULL, arena.allocateFrom(s), kCFStringEncodingUTF8 ); - } + }*/ static void unpremultiplyAlpha(int[] argb) { for (int i = 0; i < argb.length; i++) { diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java index 405c277..834a807 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/macos/MacOSImageIOEncoder.java @@ -1,4 +1,4 @@ -package org.redlance.platformtools.webp.impl.macos; +/*package org.redlance.platformtools.webp.impl.macos; import org.jetbrains.annotations.Nullable; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; @@ -219,4 +219,4 @@ private MemorySegment createQualityProperties(Arena arena, float quality) throws public boolean isAvailable() { return true; } -} +}*/ diff --git a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java index 8685a05..a4bfaf9 100644 --- a/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java +++ b/webp/src/main/java/org/redlance/platformtools/webp/impl/windows/WindowsCodecsEncoder.java @@ -1,4 +1,4 @@ -package org.redlance.platformtools.webp.impl.windows; +/*package org.redlance.platformtools.webp.impl.windows; import org.jetbrains.annotations.Nullable; import org.redlance.platformtools.webp.encoder.PlatformWebPEncoder; @@ -229,4 +229,4 @@ private byte[] readStream(Arena arena, MemorySegment stream) throws Throwable { public boolean isAvailable() { return true; } -} +}*/ From f76e155b9bbe1669fd3d739b774934a8be20f8c4 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 03:12:45 +0700 Subject: [PATCH 19/22] Update TestingApp.java --- .../org/redlance/platformtools/testing/TestingApp.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 536cd31..5913426 100644 --- a/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java +++ b/testing/src/main/java/org/redlance/platformtools/testing/TestingApp.java @@ -11,9 +11,7 @@ import org.redlance.platformtools.webp.impl.libwebp.LibWebPDecoder; import org.redlance.platformtools.webp.impl.libwebp.LibWebPEncoder; import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; -import org.redlance.platformtools.webp.impl.macos.MacOSImageIOEncoder; import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; -import org.redlance.platformtools.webp.impl.windows.WindowsCodecsEncoder; import javax.swing.*; import java.awt.*; @@ -290,17 +288,17 @@ private void showWebPDialog() { log.append(" macOS ImageIO decode: " + t + "\n"); } try { - MacOSImageIOEncoder macosEnc = MacOSImageIOEncoder.tryCreate(); - log.append(" macOS ImageIO: encode " + (macosEnc != null ? "OK" : "not available") + "\n"); + // MacOSImageIOEncoder macosEnc = MacOSImageIOEncoder.tryCreate(); + log.append(" macOS ImageIO: encode " + (/*macosEnc != null ? "OK" :*/ "not available") + "\n"); } catch (Throwable t) { log.append(" macOS ImageIO encode: ERROR " + t + "\n"); } try { WindowsCodecsDecoder wicDec = WindowsCodecsDecoder.tryCreate(); - WindowsCodecsEncoder wicEnc = WindowsCodecsEncoder.tryCreate(); + // WindowsCodecsEncoder wicEnc = WindowsCodecsEncoder.tryCreate(); log.append(" Windows WIC: decode " + (wicDec != null ? "OK" : "not available") - + ", encode " + (wicEnc != null ? "OK" : "not available") + "\n"); + + ", encode " + (/*wicEnc != null ? "OK" :*/ "not available") + "\n"); } catch (Throwable t) { log.append(" Windows WIC: ERROR " + t + "\n"); } From 65400f7d845f1e75cd8e6927b133a615bd81c432 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 03:13:56 +0700 Subject: [PATCH 20/22] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9f0c00f..ecf4512 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ plugins { allprojects { group = "org.redlance" - version = "3.3.1" + version = "4.0.0" repositories { mavenCentral() From 671ff61d865b5a5a17c2e1b8cae08c72accf9c8b Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 03:15:24 +0700 Subject: [PATCH 21/22] Java 22 --- README.MD | 4 ++-- webp/build.gradle | 2 +- .../org/redlance/platformtools/webp/MacOSImageIOTest.java | 5 ++--- .../java/org/redlance/platformtools/webp/WindowsWICTest.java | 5 ++--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.MD b/README.MD index 3ad9e91..5a2fb6e 100644 --- a/README.MD +++ b/README.MD @@ -26,7 +26,7 @@ The library is split into independent modules so you can include only what you n * **[Taskbar Progress Bars](#4-taskbar-progress-bars)** (`progress`): Display progress indicators on the taskbar/dock icon. * Supports multiple stacked progress bars (up to 8). * *Note: Windows taskbar support is planned for future releases.* -* **[WebP Codec](#5-webp-codec)** (`webp`): Compact, pixel-exact WebP decoding/encoding (Java 25+, FFM API). +* **[WebP Codec](#5-webp-codec)** (`webp`): Compact, pixel-exact WebP decoding/encoding (Java 22+, FFM API). * **macOS:** ImageIO (CoreGraphics) * **Windows:** WIC (Windows Imaging Component) * **Cross-platform:** libwebp (if found on library path) @@ -197,7 +197,7 @@ public class ProgressExample { ### 5. WebP Codec Compact, pixel-exact WebP decoding and encoding module. The goal is **minimal footprint** and **zero native dependencies** out of the box — the bundled pure-Java decoder adds only ~92 KB (after ProGuard) while producing bit-identical output to libwebp. -> **Note:** Requires Java 25+ (uses the FFM API). Other modules remain Java 8 compatible. +> **Note:** Requires Java 22+ (uses the FFM API). Other modules remain Java 8 compatible. Decoder priority (first available wins): 1. **libwebp** — system-installed native library (fastest, encode + decode) diff --git a/webp/build.gradle b/webp/build.gradle index 0db5fbf..0067d7c 100644 --- a/webp/build.gradle +++ b/webp/build.gradle @@ -40,7 +40,7 @@ test { tasks.withType(JavaCompile).configureEach { options.encoding = "UTF-8" - options.release.set(25) + options.release.set(22) options.compilerArgs.addAll(["--add-reads", "platformtools.webp=ALL-UNNAMED"]) } diff --git a/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java b/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java index 114fc94..0f8bca0 100644 --- a/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java +++ b/webp/src/test/java/org/redlance/platformtools/webp/MacOSImageIOTest.java @@ -8,7 +8,6 @@ import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.impl.macos.MacOSImageIODecoder; -import org.redlance.platformtools.webp.impl.macos.MacOSImageIOEncoder; import java.io.IOException; @@ -47,7 +46,7 @@ void pixelExactDecode(String name) throws IOException { TestUtils.assertMatchesReference(decoded, name); } - @Test + /*@Test void encoderMayBeUnavailable() { // macOS currently does not support WebP encoding via ImageIO MacOSImageIOEncoder enc = MacOSImageIOEncoder.tryCreate(); @@ -55,5 +54,5 @@ void encoderMayBeUnavailable() { byte[] encoded = enc.encodeLossy(TestUtils.generateTestImage(), TestUtils.W, TestUtils.H, 0.75f); assertTrue(encoded.length > 0); } - } + }*/ } diff --git a/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java b/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java index d961f78..6c8b44e 100644 --- a/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java +++ b/webp/src/test/java/org/redlance/platformtools/webp/WindowsWICTest.java @@ -8,7 +8,6 @@ import org.redlance.platformtools.webp.decoder.DecodedImage; import org.redlance.platformtools.webp.decoder.PlatformWebPDecoder; import org.redlance.platformtools.webp.impl.windows.WindowsCodecsDecoder; -import org.redlance.platformtools.webp.impl.windows.WindowsCodecsEncoder; import java.io.IOException; @@ -47,8 +46,8 @@ void pixelExactDecode(String name) throws IOException { TestUtils.assertMatchesReference(decoded, name); } - @Test + /*@Test void noEncoder() { assertNull(WindowsCodecsEncoder.tryCreate(), "WIC should not have WebP encoder"); - } + }*/ } From 9d537e4c970b91c4153c2395a96ef0efa1acd459 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Tue, 10 Mar 2026 03:16:36 +0700 Subject: [PATCH 22/22] Update README.MD --- README.MD | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.MD b/README.MD index 5a2fb6e..2de7926 100644 --- a/README.MD +++ b/README.MD @@ -45,14 +45,14 @@ repositories { dependencies { // Include only the modules you need: - implementation "org.redlance.platformtools:accent:3.3.0" - implementation "org.redlance.platformtools:referer:3.3.0" - implementation "org.redlance.platformtools:favorites:3.3.0" - implementation "org.redlance.platformtools:progress:3.3.0" - implementation "org.redlance.platformtools:webp:3.3.0" + implementation "org.redlance.platformtools:accent:4.0.0" + implementation "org.redlance.platformtools:referer:4.0.0" + implementation "org.redlance.platformtools:favorites:4.0.0" + implementation "org.redlance.platformtools:progress:4.0.0" + implementation "org.redlance.platformtools:webp:4.0.0" // For Java 8 (Downgraded version), add classifier: - // implementation("org.redlance.platformtools:accent:3.3.0:java8") + // implementation("org.redlance.platformtools:accent:4.0.0:java8") } ``` @@ -69,7 +69,7 @@ dependencies { org.redlance.platformtools accent - 3.3.0 + 4.0.0 ``` @@ -211,10 +211,10 @@ The `webp` module has two variants: ```groovy // Standard — requires platform support or libwebp on library path -implementation "org.redlance.platformtools:webp:3.3.0" +implementation "org.redlance.platformtools:webp:4.0.0" // Bundled — self-contained, works everywhere (decode only) -implementation "org.redlance.platformtools:webp:3.3.0:bundled" +implementation "org.redlance.platformtools:webp:4.0.0:bundled" ``` ```java