Skip to content

Commit

Permalink
feat(jxl): refactor the plugin to follow the TwelveMonkeys standard
Browse files Browse the repository at this point in the history
image with orientation are now correctly decoded
support for subsampling, source and destination region
  • Loading branch information
gotson committed Oct 5, 2023
1 parent b5585d2 commit d1d0772
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 181 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This lets you add the dependencies in your project whatever the JDK used, and st

| Plugin | Format | Read | Write | Metadata | TwelveMonkeys Tests | Notes |
|----------------------|----------------------------------------------------------------------------------------------------------------------|------|-------|----------|---------------------|------------------------------------|
| [jxl](imageio-jxl) | [Jpeg XL](https://jpeg.org/jpegxl/) || - | - | - | |
| [jxl](imageio-jxl) | [Jpeg XL](https://jpeg.org/jpegxl/) || - | - | | See limitations in the plugin page |
| [webp](imageio-webp) | [WebP](https://developers.google.com/speed/webp) || - | - || See limitations in the plugin page |
| [heif](imageio-heif) | [HEIF](https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format) & [AVIF](https://en.wikipedia.org/wiki/AVIF) || - | - || See limitations in the plugin page |

Expand Down

This file was deleted.

This file was deleted.

7 changes: 7 additions & 0 deletions imageio-jxl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@

- Homebrew (Mac & Linux): `brew install jpeg-xl`

## Features

- Decode Jpeg XL images

## Limitations

- Animations are not supported. A single image is returned.
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.github.gotson.nightmonkeys.jxl;

import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;

public record BasicInfo(
int width,
int height,
boolean hasAlpha,
boolean hasAnimation,
int colorChannels,
ICC_ColorSpace iccSpace) {
ICC_Profile iccProfile,
int frameCount) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageReadParam;
import javax.imageio.stream.ImageInputStream;
import java.awt.color.ICC_ColorSpace;
import java.awt.Rectangle;
import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import static com.github.gotson.nightmonkeys.common.imageio.IIOUtil.byteArrayFromStream;
import static com.github.gotson.nightmonkeys.jxl.lib.panama.decode_h.C_CHAR;
import static com.github.gotson.nightmonkeys.jxl.lib.panama.decode_h.C_INT;
import static com.github.gotson.nightmonkeys.jxl.lib.panama.decode_h.C_LONG;

Expand All @@ -33,7 +33,9 @@
*/
public class JpegXl {

private static final Logger LOG = LoggerFactory.getLogger(JpegXl.class);
private static final Logger LOGGER = LoggerFactory.getLogger(JpegXl.class);

private static final ValueLayout.OfInt pixelLayout = C_INT.withOrder(ByteOrder.BIG_ENDIAN);

public static boolean canDecode(final ImageInputStream stream) throws JxlException {
try (var arena = Arena.ofConfined()) {
Expand Down Expand Up @@ -69,7 +71,9 @@ public static BasicInfo getBasicInfo(final ImageInputStream stream) throws JxlEx
throw new JxlException("JxlDecoderCreate failed");
}

decode_h.JxlDecoderSetKeepOrientation(dec, 1);
// we want the image with its transformations
// this will return the correct width/height
decode_h.JxlDecoderSetKeepOrientation(dec, 0);

if (JxlDecoderStatus.JXL_DEC_SUCCESS != JxlDecoderStatus.fromId(
decode_h.JxlDecoderSubscribeEvents(dec, JxlDecoderStatus.JXL_DEC_BASIC_INFO.intValue() | JxlDecoderStatus.JXL_DEC_COLOR_ENCODING.intValue()))) {
Expand Down Expand Up @@ -125,15 +129,15 @@ public static BasicInfo getBasicInfo(final ImageInputStream stream) throws JxlEx
var iccData = new byte[iccProfile.remaining()];
iccProfile.get(iccData);
var icc = ICC_Profile.getInstance(iccData);
var iccSpace = new ICC_ColorSpace(icc);

return new BasicInfo(
JxlBasicInfo.xsize$get(info),
JxlBasicInfo.ysize$get(info),
JxlBasicInfo.alpha_bits$get(info) > 0,
JxlBasicInfo.have_animation$get(info) > 0,
JxlBasicInfo.num_color_channels$get(info),
iccSpace
icc,
1 // no support for animations
);

} catch (IOException e) {
Expand All @@ -144,7 +148,7 @@ public static BasicInfo getBasicInfo(final ImageInputStream stream) throws JxlEx
}
}

public static BufferedImage decode(final ImageInputStream stream, final BasicInfo info) throws JxlException {
public static void decode(final ImageInputStream stream, final BasicInfo info, WritableRaster raster, ImageReadParam param, int imageIndex) throws JxlException {
var dec = decode_h.JxlDecoderCreate(MemorySegment.NULL.NULL);

try {
Expand All @@ -158,6 +162,9 @@ public static BufferedImage decode(final ImageInputStream stream, final BasicInf
throw new JxlException("JxlDecoderSubscribeEvents failed");
}

// we want the image with its transformations
decode_h.JxlDecoderSetKeepOrientation(dec, 0);

try (var arena = Arena.ofConfined()) {
stream.mark();
var jxlData = byteArrayFromStream(stream);
Expand All @@ -169,12 +176,12 @@ public static BufferedImage decode(final ImageInputStream stream, final BasicInf

var dataType = JxlDataType.JXL_TYPE_UINT8;
var format = JxlPixelFormat.allocate(arena);
JxlPixelFormat.num_channels$set(format, info.colorChannels() + (info.hasAlpha() ? 1 : 0));
JxlPixelFormat.num_channels$set(format, 4);
JxlPixelFormat.data_type$set(format, dataType.intValue());
JxlPixelFormat.endianness$set(format, JxlEndianness.JXL_NATIVE_ENDIAN.intValue());
JxlPixelFormat.endianness$set(format, JxlEndianness.JXL_BIG_ENDIAN.intValue());
JxlPixelFormat.align$set(format, 0);

ByteBuffer pixels = ByteBuffer.allocate(0);
MemorySegment pixels = MemorySegment.NULL;

while (true) {
var status = JxlDecoderStatus.fromId(decode_h.JxlDecoderProcessInput(dec));
Expand All @@ -199,9 +206,9 @@ public static BufferedImage decode(final ImageInputStream stream, final BasicInf
"Invalid out buffer size. Got: " + bufferSizeBytesValue + ", expected: " + bufferSizeExpected);
}

pixels = ByteBuffer.allocateDirect(bufferSizeBytesValue);
pixels = arena.allocateArray(C_CHAR, bufferSizeBytesValue);
if (JxlDecoderStatus.JXL_DEC_SUCCESS !=
JxlDecoderStatus.fromId(decode_h.JxlDecoderSetImageOutBuffer(dec, format, MemorySegment.ofBuffer(pixels), bufferSizeBytes.get(C_LONG, 0)))) {
JxlDecoderStatus.fromId(decode_h.JxlDecoderSetImageOutBuffer(dec, format, pixels, bufferSizeBytesValue))) {
throw new JxlException("JxlDecoderSetImageOutBuffer failed");
}
} else if (status == JxlDecoderStatus.JXL_DEC_FULL_IMAGE) {
Expand All @@ -218,14 +225,31 @@ public static BufferedImage decode(final ImageInputStream stream, final BasicInf
}
}

var colorModel = new ComponentColorModel(info.iccSpace(), info.hasAlpha(), false, ComponentColorModel.TRANSLUCENT, DataBuffer.TYPE_BYTE);
var sampleModel = colorModel.createCompatibleSampleModel(info.width(), info.height());
var pixelsRaster = new int[Math.min(info.width(), raster.getWidth()) * Math.min(info.height(), raster.getHeight())];

var sourceRegion = param != null ? param.getSourceRegion() : null;
if (sourceRegion == null) sourceRegion = new Rectangle(0, 0, info.width(), info.height());
var ssX = param != null ? param.getSourceXSubsampling() : 1;
var ssY = param != null ? param.getSourceYSubsampling() : 1;
var ssOffX = param != null ? param.getSubsamplingXOffset() : 0;
var ssOffY = param != null ? param.getSubsamplingYOffset() : 0;

var offset = 0;
for (int row = sourceRegion.y + ssOffY; row < sourceRegion.y + sourceRegion.height; row += ssY) {
if (offset >= pixelsRaster.length) break;
for (int col = sourceRegion.x + ssOffX; col < sourceRegion.x + sourceRegion.width; col += ssX) {
if (offset >= pixelsRaster.length) break;
int pixel = pixels.get(pixelLayout, (((long) row * info.width()) + col) * pixelLayout.byteSize());
pixelsRaster[offset++] = pixel;
}
}

var pixelsArray = new byte[pixels.capacity()];
pixels.get(pixelsArray);
var db = new DataBufferByte(pixelsArray, info.width() * info.height());
var raster = WritableRaster.createWritableRaster(sampleModel, db, null);
return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);
if (param != null && param.getDestinationOffset() != null) {
raster.setDataElements(param.getDestinationOffset().x, param.getDestinationOffset().y, raster.getWidth() - param.getDestinationOffset().x,
raster.getHeight() - param.getDestinationOffset().y, pixelsRaster);
} else {
raster.setDataElements(0, 0, raster.getWidth(), raster.getHeight(), pixelsRaster);
}
} catch (IOException e) {
throw new JxlException("Couldn't get stream content", e);
}
Expand Down
Loading

0 comments on commit d1d0772

Please sign in to comment.