Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8337681

PNGImageWriter uses much more memory than necessary

XMLWordPrintable

    • b11
    • x86_64
    • linux

        A DESCRIPTION OF THE PROBLEM :
        At some point the JPEGImageWriter was optimized to reduce duplication / copying of rasters and data buffers (see JDK-6266748). However, PNGImageWriter never received a similar optimization, and is making unnecessary copies of raster and data buffers whenever PNG images are written.

        See also mailing list discussion: https://mail.openjdk.org/pipermail/client-libs-dev/2024-July/021480.html

        STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
        Use ImageIO to write PNG files.

        EXPECTED VERSUS ACTUAL BEHAVIOR :
        EXPECTED -
        Unnecessary copies of rasters and data buffers are not created.
        ACTUAL -
        Rasters and data buffers are copied, row by row.

        ---------- BEGIN SOURCE ----------
        /*
         * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
         * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
         *
         * This code is free software; you can redistribute it and/or modify it
         * under the terms of the GNU General Public License version 2 only, as
         * published by the Free Software Foundation.
         *
         * This code is distributed in the hope that it will be useful, but WITHOUT
         * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
         * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
         * version 2 for more details (a copy is included in the LICENSE file that
         * accompanied this code).
         *
         * You should have received a copy of the GNU General Public License version
         * 2 along with this work; if not, write to the Free Software Foundation,
         * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
         *
         * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
         * or visit www.oracle.com if you need additional information or have any
         * questions.
         */

        /*
         * @test
         * @summary Test that raster use optimization does not cause any regressions.
         */

        import java.awt.Color;
        import java.awt.Graphics2D;
        import java.awt.geom.AffineTransform;
        import java.awt.image.BufferedImage;
        import java.awt.image.RenderedImage;
        import javax.imageio.IIOImage;
        import javax.imageio.ImageIO;
        import javax.imageio.ImageReader;
        import javax.imageio.ImageWriter;
        import javax.imageio.ImageWriteParam;
        import javax.imageio.stream.ImageInputStream;
        import javax.imageio.stream.ImageOutputStream;
        import javax.imageio.stream.MemoryCacheImageOutputStream;
        import java.io.ByteArrayInputStream;
        import java.io.ByteArrayOutputStream;

        public class RasterReuseWriteTest {

            public static void main(String[] args) throws Exception {
                test(BufferedImage.TYPE_INT_ARGB);
                test(BufferedImage.TYPE_INT_ARGB_PRE);
            }

            private static void test(int type) throws Exception {

                // test writing a BufferedImage without source bands
                BufferedImage img1 = createImage(256, 256, type);
                byte[] bytes1 = writePng(img1, null);
                BufferedImage img2 = ImageIO.read(new ByteArrayInputStream(bytes1));
                compare(img1, img2, false);

                // test writing a BufferedImage with source bands
                BufferedImage img3 = createImage(256, 256, type);
                int[] sourceBands = new int[] { 2, 1, 0, 3 }; // swap blue and red
                byte[] bytes3 = writePng(img3, sourceBands);
                BufferedImage img4 = ImageIO.read(new ByteArrayInputStream(bytes3));
                compare(img3, img4, true);

                // test writing a non-BufferedImage with source bands and one tile
                RenderedImage img5 = toTiledImage(img1, 256);
                byte[] bytes5 = writePng(img5, sourceBands);
                BufferedImage img6 = ImageIO.read(new ByteArrayInputStream(bytes5));
                compare(img5, img6, true);

                // test writing a non-BufferedImage with source bands and multiple tiles
                RenderedImage img7 = toTiledImage(img1, 128);
                byte[] bytes7 = writePng(img7, sourceBands);
                BufferedImage img8 = ImageIO.read(new ByteArrayInputStream(bytes7));
                compare(img7, img8, true);
            }

            private static BufferedImage createImage(int w, int h, int type) throws Exception {
                BufferedImage img = new BufferedImage(w, h, type);
                Graphics2D g2d = img.createGraphics();
                g2d.setColor(Color.WHITE);
                g2d.fillRect(0, 0, w, h);
                g2d.setColor(Color.GREEN);
                g2d.drawRect(20, 20, 100, 50);
                g2d.setColor(Color.RED);
                g2d.drawRect(80, 10, 100, 40);
                g2d.setColor(Color.BLUE);
                g2d.fillRect(40, 60, 120, 30);
                g2d.dispose();
                return img;
            }

            private static byte[] writePng(RenderedImage img, int[] sourceBands) throws Exception {
                ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
                ImageWriteParam param = writer.getDefaultWriteParam();
                param.setSourceBands(sourceBands);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ImageOutputStream stream = new MemoryCacheImageOutputStream(baos);
                writer.setOutput(stream);
                writer.write(null, new IIOImage(img, null, null), param);
                writer.dispose();
                stream.flush();
                return baos.toByteArray();
            }

            private static void compare(RenderedImage img1, RenderedImage img2, boolean blueAndRedSwapped) {
                int[] pixels1 = getRgbPixels(img1);
                int[] pixels2 = getRgbPixels(img2);
                for (int i = 0; i < pixels1.length; i++) {
                    int expected;
                    if (blueAndRedSwapped && pixels1[i] == 0xFFFF0000) {
                        expected = 0xFF0000FF; // red -> blue
                    } else if (blueAndRedSwapped && pixels1[i] == 0xFF0000FF) {
                        expected = 0xFFFF0000; // blue -> red
                    } else {
                        expected = pixels1[i]; // no change
                    }
                    int actual = pixels2[i];
                    if (actual != expected) {
                        throw new RuntimeException("Pixel " + i + ": expected " +
                            Integer.toHexString(expected) + ", but got " +
                            Integer.toHexString(actual));
                    }
                }
            }

            private static int[] getRgbPixels(RenderedImage img) {
                int w = img.getWidth();
                int h = img.getHeight();
                if (img instanceof BufferedImage bi) {
                    return bi.getRGB(0, 0, w, h, null, 0, w);
                } else {
                    BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
                    Graphics2D g2d = bi.createGraphics();
                    g2d.drawRenderedImage(img, new AffineTransform());
                    g2d.dispose();
                    return bi.getRGB(0, 0, w, h, null, 0, w);
                }
            }

            private static RenderedImage toTiledImage(BufferedImage img, int tileSize) throws Exception {

                // write to TIFF
                ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
                ImageWriteParam param = writer.getDefaultWriteParam();
                param.setTilingMode(ImageWriteParam.MODE_EXPLICIT);
                param.setTiling(tileSize, tileSize, 0, 0);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ImageOutputStream stream = new MemoryCacheImageOutputStream(baos);
                writer.setOutput(stream);
                writer.write(null, new IIOImage(img, null, null), param);
                writer.dispose();
                stream.flush();
                byte[] bytes = baos.toByteArray();

                // read from TIFF
                ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
                ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes));
                reader.setInput(input);
                RenderedImage ri = reader.readAsRenderedImage(0, null);
                if (ri instanceof BufferedImage) {
                    throw new RuntimeException("Unexpected BufferedImage");
                }
                int tw = ri.getTileWidth();
                int th = ri.getTileHeight();
                if (tw != tileSize || th != tileSize) {
                    throw new RuntimeException("Expected tile size " + tileSize +
                        ", but found " + tw + "x" + th);
                }
                return ri;
            }
        }
        ---------- END SOURCE ----------

        CUSTOMER SUBMITTED WORKAROUND :
        Allocate more memory to the system.

        FREQUENCY : always


              dgredler Daniel Gredler
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              4 Start watching this issue

                Created:
                Updated:
                Resolved: