-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from Vinrobot/features/imageio
Implement ImageIO
- Loading branch information
Showing
22 changed files
with
821 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
217 changes: 217 additions & 0 deletions
217
libwebp-api/src/main/java/net/vinrobot/webp/imageio/BaseImageReader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) { | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
libwebp-api/src/main/java/net/vinrobot/webp/imageio/BaseImageReaderSpi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
16
libwebp-api/src/main/java/net/vinrobot/webp/imageio/WebP.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
48 changes: 48 additions & 0 deletions
48
libwebp-api/src/main/java/net/vinrobot/webp/imageio/WebPImageMetadata.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
Oops, something went wrong.