Skip to content

Commit

Permalink
Merge pull request #3 from Vinrobot/features/imageio
Browse files Browse the repository at this point in the history
Implement ImageIO
  • Loading branch information
Vinrobot authored Jul 14, 2024
2 parents 33c386a + 0d937d0 commit 740e4e3
Show file tree
Hide file tree
Showing 22 changed files with 821 additions and 9 deletions.
3 changes: 3 additions & 0 deletions libwebp-api/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module net.vinrobot.webp {
requires java.desktop;

exports net.vinrobot.webp;
exports net.vinrobot.webp.imageio;

uses net.vinrobot.webp.WebPReaderSpi;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package net.vinrobot.webp.imageio;

import net.vinrobot.webp.WebPDecoderException;
import net.vinrobot.webp.WebPFrame;
import net.vinrobot.webp.WebPMetadata;
import net.vinrobot.webp.WebPReader;

import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
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.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

public abstract class BaseImageReader extends ImageReader {
private final List<Frame> frames = new ArrayList<>();
private WebPReader reader;
private WebPMetadata metadata;

public BaseImageReader(final ImageReaderSpi originatingProvider) {
super(originatingProvider);
}

protected BufferedImage createImage(final int[] pixels, final int width, final int height) {
assert pixels.length == width * height;
final ColorModel colorModel = new DirectColorModel(32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
final SampleModel sampleModel = colorModel.createCompatibleSampleModel(width, height);
final DataBufferInt dataBufferInt = new DataBufferInt(pixels, width * height);
final WritableRaster writableRaster = WritableRaster.createWritableRaster(sampleModel, dataBufferInt, null);
return new BufferedImage(colorModel, writableRaster, false, null);
}

@Override
public void setInput(final Object input, final boolean seekForwardOnly, final boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata);
this.resetInternalState();
}

@Override
public void dispose() {
super.dispose();
this.setInput(null);
}

protected synchronized void resetInternalState() {
this.metadata = null;
this.frames.clear();
if (this.reader != null) {
try {
this.reader.close();
this.reader = null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

protected WebPReader getReader(final Object input) throws WebPDecoderException, IOException {
if (input instanceof ImageInputStream stream) {
return this.getReader(stream);
} else if (input instanceof byte[]) {
return this.getReader((byte[]) input);
} else {
final String inputClassName = input.getClass().getName();
throw new IllegalStateException("Unsupported input type: " + inputClassName);
}
}

protected abstract WebPReader getReader(ImageInputStream stream) throws WebPDecoderException, IOException;

protected abstract WebPReader getReader(byte[] data) throws WebPDecoderException, IOException;

protected WebPReader getReader() throws WebPDecoderException, IOException {
final Object input = Objects.requireNonNull(this.getInput());
if (this.reader == null) {
synchronized (this) {
if (this.reader == null) {
this.reader = this.getReader(input);
}
}
}
return Objects.requireNonNull(this.reader);
}

protected WebPMetadata getMetadata() throws WebPDecoderException, IOException {
if (this.metadata == null) {
final WebPReader reader = this.getReader();
this.metadata = reader.getMetadata();
}
return this.metadata;
}

protected void readAllFrames() throws IOException {
try {
this.read(Integer.MAX_VALUE);
} catch (final IndexOutOfBoundsException ex) {
// Ignore
}
}

@Override
public int getNumImages(final boolean allowSearch) throws IOException {
if (allowSearch) {
this.readAllFrames();
return this.frames.size();
}
try {
final WebPMetadata metadata = this.getMetadata();
return metadata.frameCount();
} catch (WebPDecoderException e) {
throw new IOException(e.getMessage(), e);
}
}

@Override
public int getWidth(final int imageIndex) throws IOException {
if (imageIndex < 0) {
throw new IndexOutOfBoundsException("imageIndex < 0");
}
try {
final WebPMetadata metadata = this.getMetadata();
if (imageIndex >= metadata.frameCount()) {
throw new IndexOutOfBoundsException("imageIndex >= frameCount");
}
return metadata.canvasWidth();
} catch (WebPDecoderException e) {
throw new IOException(e.getMessage(), e);
}
}

@Override
public int getHeight(final int imageIndex) throws IOException {
if (imageIndex < 0) {
throw new IndexOutOfBoundsException("imageIndex < 0");
}
try {
final WebPMetadata metadata = this.getMetadata();
if (imageIndex >= metadata.frameCount()) {
throw new IndexOutOfBoundsException("imageIndex >= frameCount");
}
return metadata.canvasHeight();
} catch (WebPDecoderException e) {
throw new IOException(e.getMessage(), e);
}
}

@Override
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
return null;
}

@Override
public IIOMetadata getStreamMetadata() throws IOException {
try {
final WebPMetadata metadata = this.getMetadata();
return new WebPStreamMetadata(metadata.canvasWidth(), metadata.canvasHeight(), metadata.loopCount(), metadata.frameCount(), metadata.backgroundColor());
} catch (WebPDecoderException e) {
throw new IOException(e.getMessage(), e);
}
}

@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
this.read(imageIndex);
final int timestamp = this.frames.get(imageIndex).timestamp;
return new WebPImageMetadata(timestamp);
}

@Override
public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
if (param != null) {
throw new UnsupportedOperationException("ImageReadParam not supported");
}
if (imageIndex < 0) {
throw new IndexOutOfBoundsException("imageIndex < 0");
}
if (imageIndex < this.frames.size()) {
return this.frames.get(imageIndex).image;
}

try {
final WebPReader reader = this.getReader();
final WebPMetadata metadata = this.getMetadata();

while (reader.hasMoreFrame()) {
final WebPFrame frame = reader.nextFrame();
final BufferedImage image = createImage(frame.pixels(), metadata.canvasWidth(), metadata.canvasHeight());
this.frames.add(new Frame(image, frame.timestamp()));
if (this.frames.size() - 1 == imageIndex) {
return image;
}
}
} catch (WebPDecoderException e) {
throw new IOException(e.getMessage(), e);
}

// Should never happen since we check imageIndex < info.frameCount() unless the webp file is malformed.
throw new IndexOutOfBoundsException("imageIndex >= frameCount");
}

protected record Frame(
BufferedImage image,
int timestamp
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package net.vinrobot.webp.imageio;

import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Locale;

public abstract class BaseImageReaderSpi extends ImageReaderSpi {
private static final String VENDOR_NAME = "Vinrobot";
private static final String VERSION = "1.0";
private static final String[] NAMES = {"webp", "WEBP", "wbp", "WBP"};
private static final String[] SUFFIXES = {"wbp", "webp"};
private static final String[] MIME_TYPES = {"image/webp", "image/x-webp"};

public BaseImageReaderSpi(final Class<? extends ImageReader> readerClass, final Class<?>[] inputTypes) {
this(VENDOR_NAME, VERSION, readerClass, inputTypes);
}

public BaseImageReaderSpi(final String vendorName, final String version, final Class<? extends ImageReader> readerClass, final Class<?>[] inputTypes) {
this(vendorName, version, NAMES, SUFFIXES, MIME_TYPES, readerClass.getName(), inputTypes, null, false, null, null, null, null, false, null, null, null, null);
}

public BaseImageReaderSpi(final String vendorName, final String version, final String[] names, final String[] suffixes, final String[] MIMETypes, final String readerClassName, final Class<?>[] inputTypes, final String[] writerSpiNames, final boolean supportsStandardStreamMetadataFormat, final String nativeStreamMetadataFormatName, final String nativeStreamMetadataFormatClassName, final String[] extraStreamMetadataFormatNames, final String[] extraStreamMetadataFormatClassNames, final boolean supportsStandardImageMetadataFormat, final String nativeImageMetadataFormatName, final String nativeImageMetadataFormatClassName, final String[] extraImageMetadataFormatNames, final String[] extraImageMetadataFormatClassNames) {
super(vendorName, version, names, suffixes, MIMETypes, readerClassName, inputTypes, writerSpiNames, supportsStandardStreamMetadataFormat, nativeStreamMetadataFormatName, nativeStreamMetadataFormatClassName, extraStreamMetadataFormatNames, extraStreamMetadataFormatClassNames, supportsStandardImageMetadataFormat, nativeImageMetadataFormatName, nativeImageMetadataFormatClassName, extraImageMetadataFormatNames, extraImageMetadataFormatClassNames);
}

protected static int readInt(final byte[] buffer, final int offset) {
return buffer[offset] | buffer[offset + 1] << 8 | buffer[offset + 2] << 16 | buffer[offset + 3] << 24;
}

@Override
public boolean canDecodeInput(final Object source) throws IOException {
if (source instanceof ImageInputStream) {
return canDecodeInput((ImageInputStream) source);
} else if (source instanceof byte[]) {
return canDecodeInput((byte[]) source);
} else {
return false;
}
}

public boolean canDecodeInput(final ImageInputStream stream) throws IOException {
final ByteOrder originalOrder = stream.getByteOrder();
stream.mark();

try {
// RIFF native order is Little Endian
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);

// Check file header
// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header

if (stream.readInt() != WebP.RIFF_MAGIC) {
return false;
}

stream.readInt(); // Skip file size

if (stream.readInt() != WebP.WEBP_MAGIC) {
return false;
}

// Check first chunk type
switch (stream.readInt()) {
case WebP.CHUNK_VP8L, WebP.CHUNK_VP8X, WebP.CHUNK_VP8_:
break;
default:
return false;
}

return true;
} finally {
stream.setByteOrder(originalOrder);
stream.reset();
}
}

public boolean canDecodeInput(final byte[] data) {
// Check file header
// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header

if (data.length < 16 || readInt(data, 0) != WebP.RIFF_MAGIC || readInt(data, 8) != WebP.WEBP_MAGIC) {
return false;
}

switch (readInt(data, 12)) {
case WebP.CHUNK_VP8L, WebP.CHUNK_VP8X, WebP.CHUNK_VP8_:
break;
default:
return false;
}

return true;
}

@Override
public String getDescription(final Locale locale) {
return "Google WebP File Format (WebP) Reader";
}
}
16 changes: 16 additions & 0 deletions libwebp-api/src/main/java/net/vinrobot/webp/imageio/WebP.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.vinrobot.webp.imageio;

/**
* WebP container format constants.
*
* @see <a href="https://developers.google.com/speed/webp/docs/riff_container">WebP Container
* Specification</a>
*/
public class WebP {
public static final int RIFF_MAGIC = 'R' | 'I' << 8 | 'F' << 16 | 'F' << 24;
public static final int WEBP_MAGIC = 'W' | 'E' << 8 | 'B' << 16 | 'P' << 24;

public static final int CHUNK_VP8_ = 'V' | 'P' << 8 | '8' << 16 | ' ' << 24;
public static final int CHUNK_VP8L = 'V' | 'P' << 8 | '8' << 16 | 'L' << 24;
public static final int CHUNK_VP8X = 'V' | 'P' << 8 | '8' << 16 | 'X' << 24;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.vinrobot.webp.imageio;

import org.w3c.dom.Node;

import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;

public final class WebPImageMetadata extends IIOMetadata {
public static final String NATIVE_METADATA_FORMAT_NAME = "net_vinrobot_imageio_webp_image_1.0";
public static final String NATIVE_METADATA_FORMAT_CLASS_NAME = WebPImageMetadata.class.getName();

private final int timestamp;

public WebPImageMetadata(final int timestamp) {
super(false, NATIVE_METADATA_FORMAT_NAME, NATIVE_METADATA_FORMAT_CLASS_NAME, null, null);
this.timestamp = timestamp;
}

@Override
public boolean isReadOnly() {
return true;
}

@Override
public Node getAsTree(final String formatName) {
if (formatName.equals(nativeMetadataFormatName)) {
return getNativeTree();
} else {
throw new IllegalArgumentException("Unsupported format name: " + formatName);
}
}

public Node getNativeTree() {
final IIOMetadataNode documentNode = new IIOMetadataNode("Document");
documentNode.setAttribute("Timestamp", String.valueOf(this.timestamp));
return documentNode;
}

@Override
public void mergeTree(final String formatName, final Node root) {
throw new IllegalStateException("Metadata is read-only");
}

@Override
public void reset() {
throw new IllegalStateException("Metadata is read-only");
}
}
Loading

0 comments on commit 740e4e3

Please sign in to comment.