Traneptora / jxlatte

Java JPEG XL decoder
MIT License
41 stars 6 forks source link

Reading from Raster vs BufferedImage #14

Closed Ali-RS closed 1 year ago

Ali-RS commented 1 year ago

I have created two jxl loaders for a game engine, first one loads the image from raster (JXLImage.getRaster()), and the second one loads from BufferedImage (JXLImage.asBufferedImage()).

The result looks correct when loading from Raster (the first one) but it looks wrong when loading from BufferedImage (the second one). Seems a transparency issue.

This is how the result looks from the first loader (It is correct):

Screenshot_2023-04-04_20-09-15

and here is the result from the second loader (It is incorrect):

Screenshot_2023-04-04_20-10-11

This is the code for the first loader (using raster):

    public class JpegXlLoader implements AssetLoader {

        private static int hdrToRgb(float hdr) {
            return (int) Math.min(Math.max(Math.pow(hdr, 1.0/2.2) * 255, 0), 255);
        }

        private void writeSample(ByteBuffer data, int x, int y, int c, WritableRaster raster) throws IOException {
            int s = hdrToRgb(raster.getSampleFloat(x, y, c));
            data.put((byte) s);

            /*if (bitDepth == 8)
                data.put((byte)s);
            else
                data.putShort(s);*/
        }

        private Image load(InputStream in, boolean flipY) throws IOException {
            JXLImage image;
            try (JXLDecoder decoder = new JXLDecoder(in)) {
                long startTime = System.nanoTime();
                image = decoder.decode();
                double timePassed = (System.nanoTime() - startTime) / 1000000000d;
                System.out.println("Time passed=" + timePassed);
            }

            WritableRaster raster = image.getRaster();
            int height = image.getHeight();
            int width = image.getWidth();

            boolean hasAlpha = image.hasAlpha();
            int alphaIndex = image.getAlphaIndex();

            switch (image.getColorEncoding()) {
                case ColorFlags.CE_RGB -> {
                    int colorChannels = 3;
                    ByteBuffer data = BufferUtils.createByteBuffer(width * height * (hasAlpha ? 4 : 3));

                    for (int y = 0; y < height; y++) {
                        for (int x = 0; x < width; x++) {
                            int ny = y;
                            if (flipY){
                                ny = height - y - 1;
                            }
                            for (int c = 0; c < colorChannels; c++) {
                                writeSample(data, x, ny, c, raster); // rgb
                            }
                            if (hasAlpha) {
                                writeSample(data, x, ny, colorChannels + alphaIndex, raster); // alpha
                            }
                        }
                    }

                    data.flip();
                    Image.Format format = hasAlpha ? Image.Format.RGBA8 : Image.Format.RGB8;
                    return new Image(format, width, height, data, null, com.jme3.texture.image.ColorSpace.sRGB);
                }

                case ColorFlags.CE_GRAY -> {
                    int colorChannels = 1;
                    ByteBuffer data = BufferUtils.createByteBuffer(width * height * (hasAlpha ? 2 : 1));

                    for (int y = 0; y < height; y++) {
                        for (int x = 0; x < width; x++) {
                            int ny = y;
                            if (flipY){
                                ny = height - y - 1;
                            }
                            for (int c = 0; c < colorChannels; c++) {
                                writeSample(data, x, ny, c, raster); // luminance
                            }
                            if (hasAlpha) {
                                writeSample(data, x, ny, colorChannels + alphaIndex, raster); // alpha
                            }
                        }
                    }

                    data.flip();
                    Image.Format format = hasAlpha ? Image.Format.Luminance8Alpha8 : Image.Format.Luminance8;
                    return new Image(format, width, height, data, null, com.jme3.texture.image.ColorSpace.sRGB);
                }

                default -> throw new UnsupportedOperationException("Unsupported Color Encoding " + image.getColorEncoding());
            }
        }

        @Override
        public Object load(AssetInfo info) throws IOException {
            boolean flip = ((TextureKey) info.getKey()).isFlipY();
            try (InputStream in = info.openStream(); BufferedInputStream bin = new BufferedInputStream(in)) {
                Image img = load(bin, flip);
                if (img == null){
                    throw new AssetLoadException("The given image cannot be loaded " + info.getKey());
                }
                return img;
            }
        }
    }

this is the code for the second loader (using BufferedImage):

    public class JpegXlLoader2 implements AssetLoader {

        public Image load(BufferedImage img, boolean flipY){
            int width = img.getWidth();
            int height = img.getHeight();

            if (img.getTransparency() == Transparency.OPAQUE){
                ByteBuffer data = BufferUtils.createByteBuffer(img.getWidth()*img.getHeight()*3);
                // no alpha
                for (int y = 0; y < height; y++){
                    for (int x = 0; x < width; x++){
                        int ny = y;
                        if (flipY){
                            ny = height - y - 1;
                        }

                        int rgb = img.getRGB(x,ny);
                        byte r = (byte) ((rgb & 0x00FF0000) >> 16);
                        byte g = (byte) ((rgb & 0x0000FF00) >> 8);
                        byte b = (byte) ((rgb & 0x000000FF));
                        data.put(r).put(g).put(b);
                    }
                }
                data.flip();
                return new Image(Image.Format.RGB8, width, height, data, null, com.jme3.texture.image.ColorSpace.sRGB);
            } else {
                ByteBuffer data = BufferUtils.createByteBuffer(img.getWidth()*img.getHeight()*4);
                // alpha
                for (int y = 0; y < height; y++){
                    for (int x = 0; x < width; x++){
                        int ny = y;
                        if (flipY){
                            ny = height - y - 1;
                        }

                        int rgb = img.getRGB(x,ny);
                        byte a = (byte) ((rgb & 0xFF000000) >> 24);
                        byte r = (byte) ((rgb & 0x00FF0000) >> 16);
                        byte g = (byte) ((rgb & 0x0000FF00) >> 8);
                        byte b = (byte) ((rgb & 0x000000FF));
                        data.put(r).put(g).put(b).put(a);
                    }
                }
                data.flip();
                return new Image(Image.Format.RGBA8, width, height, data, null, com.jme3.texture.image.ColorSpace.sRGB);
            }
        }

        public Image load(InputStream in, boolean flipY) throws IOException{
            JXLImage jxlImage;
            try (JXLDecoder decoder = new JXLDecoder(in)) {
                jxlImage = decoder.decode();
            }

            BufferedImage bufferedImage = jxlImage.asBufferedImage();
            if (bufferedImage == null) {
                return null;
            }

            return load(bufferedImage, flipY);
        }

        @Override
        public Object load(AssetInfo info) throws IOException {
            boolean flip = ((TextureKey) info.getKey()).isFlipY();
            try (InputStream in = info.openStream();
                 BufferedInputStream bin = new BufferedInputStream(in)) {
                Image img = load(bin, flip);
                if (img == null){
                    throw new AssetLoadException("The given image cannot be loaded " + info.getKey());
                }
                return img;
            }
        }
    }

I do not know what am I doing wrong in the second loader, do you perhaps have a hint on what might be wrong?

Could be that the alpha value in the BufferedImage is stored in HDR form instead of RGB form?

Here is my sample jxl file

Traneptora commented 1 year ago

I'm not going to debug a mass of your own code for you.

Ali-RS commented 1 year ago

Can you tell me how I can check if the BufferedImage returned by JXLImage.asBufferedImage() is valid? I wonder if the alpha values stored in the BufferedImage are corrupted.

I tried to write it into a png file using ImageIO (see https://github.com/thebombzen/jxlatte/issues/13) and compare the result but it did not work so I am not sure how else I can check this.

Edit: Note that both of the loaders work fine if the image is opaque. This only happens with transparent images.

Traneptora commented 1 year ago

The buffered image is always valid. Whether or not ImageIO's PNG writer accepts it is a different story.