I need to read static and animated WebP images in Java. There is no DLL offered for libwepb, so I've downloaded the source and compiled it in the Native Tools Command Prompt for VS 2022 on Windows like this:
..\libwebp-1.2.2>nmake /f Makefile.vc CFG=release-dynamic RTLIBCFG=dynamic OBJDIR=output
This resulted in the files libwebp.dll
, libwebpdecoder.dll
and libwebpdemux.dll
. I'm using jna-5.12.1.jar
for JNA. I was able to decode a static image using the libwebp.dll
and the function WebPDecodeRGBA
, however decoding an animated image doesn't work with that. From what I understand the demux functions in libwebpdemux.dll
are required for that.
I tried to implement the WebPAnimDecoder API, but I'm already stuck at creating the decoder:
WebPAnimDecoder* dec = WebPAnimDecoderNew(webp_data, &dec_options);
From the documentation it looks like dec_options
can just be null to get a default, but I'm not sure how it wants the webp_data
. I tried to implement the WebPData struct, but I'm not sure if that is even correct.
@Structure.FieldOrder({ "bytes", "length" })
public static class WebPData extends Structure {
public byte[] bytes;
public int length;
}
I've gotten an "Invalid memory access" exception by just assigning the bytes directly data.bytes = rawdata;
. At the WebP struct definition it says "'bytes' memory must be allocated using WebPMalloc() and such.", so I assume that is required somehow, but I don't know how that would work, how do I get the data in there? The way it is now throws an exception complaining about the return type.
What also confuses me is that the example calls WebPAnimDecoderNew
, but that function is not exported, so I can't access it. Instead, there is a function called WebPAnimDecoderNewInternal
with an additional int parameter that apparently is supposed to receive the WEBP_DEMUX_ABI_VERSION
constant. I'm not quite sure what the signficance of that is. Am I really supposed to call the "Internal" function?
This is what I've been testing with (more information in some comments):
package webptest;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.ptr.IntByReference;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Hashtable;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
public class WebPDecodingTest {
public static void main(String[] args) throws Exception {
System.setProperty("jna.library.path", "<path>\\lib\\x64");
// This only shows the first image
ImageIcon staticImage = decodeStatic("https://www.gstatic.com/webp/gallery/4.sm.webp");
ImageIcon animatedImage = decodeStatic("https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp");
showImages(staticImage, animatedImage);
// This throws an error (and it's incomplete anyway)
ImageIcon[] animatedImage2 = decodeAnimated("https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp");
showImages(animatedImage2);
}
//==========================
// Animated image
//==========================
/**
* Decode the frames of an animated image. Doesn't work and incomplete.
*/
private static ImageIcon[] decodeAnimated(String url) throws Exception {
byte[] rawData = getBytesFromURL(new URL(url));
System.out.println("Bytes length: "+rawData.length);
/**
* I assume that this is already not correct. In a C example
* (examples/anim_util.c) it uses "WebPDataInit(&webp_data);" first, but
* that function isn't exported. It's in "src/webp/mux_types.h":
*
// Initializes the contents of the 'webp_data' object with default values.
static WEBP_INLINE void WebPDataInit(WebPData* webp_data) {
if (webp_data != NULL) {
memset(webp_data, 0, sizeof(*webp_data));
}
}
*
* I tried just assigning the data direclty like "data.bytes = rawdata;"
* however that resulted in a "Invalid memory access" exception when
* calling "WebPAnimDecoderNewInternal".
*
* At the definition of the WebPData struct (see above) it says
* "'bytes' memory must be allocated using WebPMalloc() and such."
* so I assume that might be required to be used, but I'm not sure how.
*
* The way it is now throws a "Unsupported return type class [B in
* function WebPMalloc" exception.
*/
LibWebPDemux.WebPData data = new LibWebPDemux.WebPData();
data.bytes = LibWebP.INSTANCE.WebPMalloc(rawData.length);
data.length = rawData.length;
// This may not be required, instead "null" can be provided for default options
// LibWebPDemux.WebPAnimDecoderOptions options = new LibWebPDemux.WebPAnimDecoderOptions();
// int initResult = LibWebPDemux.INSTANCE.WebPAnimDecoderOptionsInitInternal(options, LibWebPDemux.WEBP_DEMUX_ABI_VERSION);
// System.out.println(initResult);
Pointer dec = LibWebPDemux.INSTANCE.WebPAnimDecoderNewInternal(data, null, LibWebPDemux.WEBP_DEMUX_ABI_VERSION);
System.out.println(dec);
// Just testing if accessing the lib works at all (it appears to)
// int version = LibWebPDemux.INSTANCE.WebPGetDemuxVersion();
// System.out.println(version);
return null;
}
//==========================
// libwebp
//==========================
public interface LibWebP extends Library {
LibWebP INSTANCE = Native.load("libwebp", LibWebP.class);
/*
[webp/decode.h]
// Retrieve basic header information: width, height.
// This function will also validate the header, returning true on success,
// false otherwise. '*width' and '*height' are only valid on successful return.
// Pointers 'width' and 'height' can be passed NULL if deemed irrelevant.
// Note: The following chunk sequences (before the raw VP8/VP8L data) are
// considered valid by this function:
// RIFF + VP8(L)
// RIFF + VP8X + (optional chunks) + VP8(L)
// ALPH + VP8 <-- Not a valid WebP format: only allowed for internal purpose.
// VP8(L) <-- Not a valid WebP format: only allowed for internal purpose.
WEBP_EXTERN int WebPGetInfo(const uint8_t* data, size_t data_size,
int* width, int* height);
*/
public int WebPGetInfo(byte[] data, int data_size, IntByReference width, IntByReference height);
/*
[webp/decode.h]
// Decodes WebP images pointed to by 'data' and returns RGBA samples, along
// with the dimensions in *width and *height. The ordering of samples in
// memory is R, G, B, A, R, G, B, A... in scan order (endian-independent).
// The returned pointer should be deleted calling WebPFree().
// Returns NULL in case of error.
WEBP_EXTERN uint8_t* WebPDecodeRGBA(const uint8_t* data, size_t data_size,
int* width, int* height);
*/
public Pointer WebPDecodeRGBA(byte[] data, int data_size, IntByReference width, IntByReference height);
/*
[webp/types.h]
// Allocates 'size' bytes of memory. Returns NULL upon error. Memory
// must be deallocated by calling WebPFree(). This function is made available
// by the core 'libwebp' library.
WEBP_EXTERN void* WebPMalloc(size_t size);
*/
public byte[] WebPMalloc(int size);
/*
[webp/types.h]
// Releases memory returned by the WebPDecode*() functions (from decode.h).
WEBP_EXTERN void WebPFree(void* ptr);
*/
public void WebPFree(Pointer pointer);
}
//==========================
// libwebpdemux
//==========================
public interface LibWebPDemux extends Library {
LibWebPDemux INSTANCE = Native.load("libwebpdemux", LibWebPDemux.class);
static final int WEBP_DEMUX_ABI_VERSION = 0x0107;
/*
[webp/demux.h]
// Internal, version-checked, entry point.
WEBP_EXTERN WebPAnimDecoder* WebPAnimDecoderNewInternal(
const WebPData*, const WebPAnimDecoderOptions*, int);
// Creates and initializes a WebPAnimDecoder object.
// Parameters:
// webp_data - (in) WebP bitstream. This should remain unchanged during the
// lifetime of the output WebPAnimDecoder object.
// dec_options - (in) decoding options. Can be passed NULL to choose
// reasonable defaults (in particular, color mode MODE_RGBA
// will be picked).
// Returns:
// A pointer to the newly created WebPAnimDecoder object, or NULL in case of
// parsing error, invalid option or memory error.
static WEBP_INLINE WebPAnimDecoder* WebPAnimDecoderNew(
const WebPData* webp_data, const WebPAnimDecoderOptions* dec_options) {
return WebPAnimDecoderNewInternal(webp_data, dec_options,
WEBP_DEMUX_ABI_VERSION);
}
*/
public Pointer WebPAnimDecoderNewInternal(WebPData webp_data, Structure dec_options, int version);
/*
[webp/mux_types.h]
// Data type used to describe 'raw' data, e.g., chunk data
// (ICC profile, metadata) and WebP compressed image data.
// 'bytes' memory must be allocated using WebPMalloc() and such.
struct WebPData {
const uint8_t* bytes;
size_t size;
};
*/
@Structure.FieldOrder({ "bytes", "length" })
public static class WebPData extends Structure {
public byte[] bytes;
public int length;
}
/*
[webp/demux.h]
// Returns the version number of the demux library, packed in hexadecimal using
// 8bits for each of major/minor/revision. E.g: v2.5.7 is 0x020507.
WEBP_EXTERN int WebPGetDemuxVersion(void);
*/
public int WebPGetDemuxVersion();
/*
[webp/demux.h]
// Internal, version-checked, entry point.
WEBP_EXTERN int WebPAnimDecoderOptionsInitInternal(
WebPAnimDecoderOptions*, int);
// Should always be called, to initialize a fresh WebPAnimDecoderOptions
// structure before modification. Returns false in case of version mismatch.
// WebPAnimDecoderOptionsInit() must have succeeded before using the
// 'dec_options' object.
static WEBP_INLINE int WebPAnimDecoderOptionsInit(
WebPAnimDecoderOptions* dec_options) {
return WebPAnimDecoderOptionsInitInternal(dec_options,
WEBP_DEMUX_ABI_VERSION);
}
*/
public int WebPAnimDecoderOptionsInitInternal(WebPAnimDecoderOptions options, int version);
/*
[webp/demux.h]
// Global options.
struct WebPAnimDecoderOptions {
// Output colorspace. Only the following modes are supported:
// MODE_RGBA, MODE_BGRA, MODE_rgbA and MODE_bgrA.
WEBP_CSP_MODE color_mode;
int use_threads; // If true, use multi-threaded decoding.
uint32_t padding[7]; // Padding for later use.
};
[webp/decode.h]
typedef enum WEBP_CSP_MODE {
MODE_RGB = 0, MODE_RGBA = 1,
MODE_BGR = 2, MODE_BGRA = 3,
MODE_ARGB = 4, MODE_RGBA_4444 = 5,
MODE_RGB_565 = 6,
// RGB-premultiplied transparent modes (alpha value is preserved)
MODE_rgbA = 7,
MODE_bgrA = 8,
MODE_Argb = 9,
MODE_rgbA_4444 = 10,
// YUV modes must come after RGB ones.
MODE_YUV = 11, MODE_YUVA = 12, // yuv 4:2:0
MODE_LAST = 13
} WEBP_CSP_MODE;
*/
@Structure.FieldOrder({ "color_mode", "use_threads", "padding" })
public static class WebPAnimDecoderOptions extends Structure {
public int color_mode; // enum
public int use_threads;
public int[] padding = new int[7];
}
/*
[webp/demux.h]
// Global information about the animation..
struct WebPAnimInfo {
uint32_t canvas_width;
uint32_t canvas_height;
uint32_t loop_count;
uint32_t bgcolor;
uint32_t frame_count;
uint32_t pad[4]; // padding for later use
};
*/
@Structure.FieldOrder({ "canvas_width", "canvas_height", "loop_count", "bgcolor", "frame_count", "pad" })
public static class WebPAnimInfo extends Structure {
public int canvas_width;
public int canvas_height;
public int loop_count;
public int bgcolor;
public int frame_count;
public int[] pad = new int[4];
}
}
//==========================
// Static image
//==========================
/**
* Decoding a static WebP image. I'm not sure if it's entirely correct, but
* at least an image comes out of it.
*/
private static ImageIcon decodeStatic(String url) throws Exception {
byte[] rawData = getBytesFromURL(new URL(url));
IntByReference widthRef = new IntByReference();
IntByReference heightRef = new IntByReference();
int result = LibWebP.INSTANCE.WebPGetInfo(rawData, rawData.length, widthRef, heightRef);
if (result == 1) {
// System.out.println(widthRef.getValue() + " " + heightRef.getValue());
Pointer pixelData = LibWebP.INSTANCE.WebPDecodeRGBA(rawData, rawData.length, widthRef, heightRef);
if (pixelData != null) {
int width = widthRef.getValue();
int height = heightRef.getValue();
int[] pixels = pixelData.getIntArray(0, width * height);
ColorModel colorModel;
colorModel = new DirectColorModel(32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
SampleModel sampleModel = colorModel.createCompatibleSampleModel(width, height);
DataBufferInt db = new DataBufferInt(pixels, width * height);
WritableRaster raster = WritableRaster.createWritableRaster(sampleModel, db, null);
Image img = new BufferedImage(colorModel, raster, false, new Hashtable<Object, Object>());
return new ImageIcon(img);
}
}
System.out.println("Not a valid WebP");
return null;
}
//==========================
// General Helpers
//==========================
private static void showImages(ImageIcon... icons) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame();
frame.setLayout(new FlowLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
for (ImageIcon icon : icons) {
frame.add(new JLabel(icon), BorderLayout.CENTER);
}
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
public static byte[] getBytesFromURL(URL url) throws Exception {
URLConnection c = url.openConnection();
try (InputStream input = c.getInputStream()) {
byte[] imageData = readAllBytes(input);
return imageData;
}
}
private static byte[] readAllBytes(InputStream input) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = input.read(buffer, 0, buffer.length)) != -1) {
result.write(buffer, 0, length);
}
return result.toByteArray();
}
}