mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 03:58:21 +00:00
8374377: PNGImageDecoder Slow For 8-bit PNGs
Reviewed-by: jdv, prr
This commit is contained in:
parent
a855224305
commit
2a965dffdd
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 1999, 2021, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 1999, 2026, 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
|
||||
@ -302,8 +302,16 @@ public class PNGImageDecoder extends ImageDecoder
|
||||
int bitsPerPixel = samplesPerPixel*bitDepth;
|
||||
int bytesPerPixel = (bitsPerPixel+7)>>3;
|
||||
int pass, passLimit;
|
||||
if(interlaceMethod==0) { pass = -1; passLimit = 0; }
|
||||
else { pass = 0; passLimit = 7; }
|
||||
boolean isDirectByteCopy;
|
||||
if(interlaceMethod==0) {
|
||||
pass = -1;
|
||||
passLimit = 0;
|
||||
isDirectByteCopy = bPixels != null && bitDepth == 8;
|
||||
} else {
|
||||
pass = 0;
|
||||
passLimit = 7;
|
||||
isDirectByteCopy = false;
|
||||
}
|
||||
while(++pass<=passLimit) {
|
||||
int row = startingRow[pass];
|
||||
int rowInc = rowIncrement[pass];
|
||||
@ -334,7 +342,11 @@ public class PNGImageDecoder extends ImageDecoder
|
||||
int spos=0;
|
||||
int pixel = 0;
|
||||
while (col < width) {
|
||||
if(wPixels !=null) {
|
||||
if (isDirectByteCopy) {
|
||||
System.arraycopy(rowByteBuffer, spos, bPixels, col + rowOffset, width);
|
||||
spos += width;
|
||||
break;
|
||||
} else if(wPixels !=null) {
|
||||
switch(combinedType) {
|
||||
case COLOR|ALPHA|(8<<3):
|
||||
wPixels[col+rowOffset] =
|
||||
|
||||
223
test/jdk/sun/awt/image/png/PngImageDecoder8BitTest.java
Normal file
223
test/jdk/sun/awt/image/png/PngImageDecoder8BitTest.java
Normal file
@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright (c) 2026, 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
|
||||
* @bug 8374377
|
||||
* @summary This test confirms the PNGImageProducer decodes 8-bit interlaced
|
||||
* and non-interlaced PNGs correctly.
|
||||
*/
|
||||
|
||||
import javax.imageio.IIOImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.ImageConsumer;
|
||||
import java.awt.image.IndexColorModel;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Iterator;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* The proposed change for 8374377 affects how 8-bit PNGs are decoded.
|
||||
* So this test confirms that 8-bit PNGs (both interlaced and non-interlaced)
|
||||
* are still decoded by the PNGImageDecoder so they match what ImageIO decodes.
|
||||
*
|
||||
* This test has never failed.
|
||||
*/
|
||||
public class PngImageDecoder8BitTest {
|
||||
|
||||
static BufferedImage createBufferedImage(Image img)
|
||||
throws ExecutionException, InterruptedException {
|
||||
CompletableFuture<BufferedImage> future = new CompletableFuture<>();
|
||||
img.getSource().startProduction(new ImageConsumer() {
|
||||
private int imageWidth, imageHeight;
|
||||
private BufferedImage bi;
|
||||
|
||||
@Override
|
||||
public void setDimensions(int width, int height) {
|
||||
imageWidth = width;
|
||||
imageHeight = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperties(Hashtable<?, ?> props) {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorModel(ColorModel model) {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHints(int hintflags) {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int x, int y, int w, int h, ColorModel model,
|
||||
byte[] pixels, int off, int scansize) {
|
||||
if (bi == null) {
|
||||
bi = new BufferedImage(imageWidth, imageHeight,
|
||||
BufferedImage.TYPE_BYTE_INDEXED,
|
||||
(IndexColorModel) model);
|
||||
}
|
||||
|
||||
if (w == imageWidth && h == imageHeight) {
|
||||
// this is how interlaced PNGs are decoded:
|
||||
bi.getRaster().setDataElements(0, 0,
|
||||
imageWidth, imageHeight, pixels);
|
||||
return;
|
||||
}
|
||||
|
||||
if (h != 1) {
|
||||
throw new UnsupportedOperationException(
|
||||
"this test requires h = 1");
|
||||
}
|
||||
if (off != 0) {
|
||||
throw new UnsupportedOperationException(
|
||||
"this test requires off = 0");
|
||||
}
|
||||
|
||||
bi.getRaster().setDataElements(x, y, w, 1, pixels);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int x, int y, int w, int h, ColorModel model,
|
||||
int[] pixels, int off, int scansize) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void imageComplete(int status) {
|
||||
future.complete(bi);
|
||||
}
|
||||
});
|
||||
return future.get();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
BufferedImage expected = createImageData();
|
||||
for (boolean interlace : new boolean[] { false, true} ) {
|
||||
System.out.println("Testing interlacing = "+ interlace);
|
||||
byte[] imageData = encodePNG(expected, interlace);
|
||||
|
||||
Image i = Toolkit.getDefaultToolkit().createImage(imageData);
|
||||
BufferedImage actual = createBufferedImage(i);
|
||||
|
||||
testCorrectness(expected, actual);
|
||||
}
|
||||
System.out.println("Confirmed that 8-bit PNGs decode correctly " +
|
||||
"whether we use interlacing or not.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a large sample image stored as an 8-bit PNG.
|
||||
*/
|
||||
private static BufferedImage createImageData() {
|
||||
BufferedImage bi = new BufferedImage(6000, 6000,
|
||||
BufferedImage.TYPE_BYTE_INDEXED);
|
||||
Random r = new Random(0);
|
||||
Graphics2D g = bi.createGraphics();
|
||||
for (int a = 0; a < 20000; a++) {
|
||||
g.setColor(new Color(r.nextInt(0xffffff)));
|
||||
int radius = 10 + r.nextInt(90);
|
||||
g.fillOval(r.nextInt(bi.getWidth()), r.nextInt(bi.getHeight()),
|
||||
radius, radius);
|
||||
}
|
||||
g.dispose();
|
||||
return bi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an image as 8-bit PNG.
|
||||
*/
|
||||
private static byte[] encodePNG(BufferedImage bi, boolean interlace)
|
||||
throws IOException {
|
||||
Iterator<ImageWriter> writers =
|
||||
ImageIO.getImageWritersByFormatName("png");
|
||||
if (!writers.hasNext()) {
|
||||
throw new IllegalStateException("No PNG writers found");
|
||||
}
|
||||
ImageWriter writer = writers.next();
|
||||
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
if (interlace) {
|
||||
param.setProgressiveMode(ImageWriteParam.MODE_DEFAULT);
|
||||
}
|
||||
|
||||
try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
||||
ImageOutputStream imageOut =
|
||||
ImageIO.createImageOutputStream(byteOut)) {
|
||||
writer.setOutput(imageOut);
|
||||
writer.write(null, new IIOImage(bi, null, null), param);
|
||||
return byteOut.toByteArray();
|
||||
} finally {
|
||||
writer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This throws an Error if the two images are not identical.
|
||||
* <p>
|
||||
* This unit test is intended to accompany a performance enhancement for
|
||||
* PNGImageDecoder. This method makes sure the enhancement didn't cost us
|
||||
* any accuracy.
|
||||
*/
|
||||
private static void testCorrectness(BufferedImage expected,
|
||||
BufferedImage actual) {
|
||||
if (expected.getWidth() != actual.getWidth()) {
|
||||
throw new RuntimeException("expected.getWidth() = " +
|
||||
expected.getWidth() + ", actual.getWidth() = " +
|
||||
actual.getWidth());
|
||||
}
|
||||
if (expected.getHeight() != actual.getHeight()) {
|
||||
throw new RuntimeException("expected.getHeight() = " +
|
||||
expected.getHeight() + ", actual.getHeight() = " +
|
||||
actual.getHeight());
|
||||
}
|
||||
for (int y = 0; y < expected.getHeight(); y++) {
|
||||
for (int x = 0; x < expected.getWidth(); x++) {
|
||||
int argb1 = expected.getRGB(x, y);
|
||||
int argb2 = actual.getRGB(x, y);
|
||||
if (argb1 != argb2) {
|
||||
throw new RuntimeException("x = " + x + ", y = " + y +
|
||||
" argb1 = " + Integer.toUnsignedString(argb1, 16) +
|
||||
" argb2 = " + Integer.toUnsignedString(argb2, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (c) 2026, 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.
|
||||
*/
|
||||
package org.openjdk.bench.java.awt.image;
|
||||
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.Fork;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
import org.openjdk.jmh.infra.Blackhole;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.ImageConsumer;
|
||||
import java.awt.image.IndexColorModel;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
@Warmup(iterations = 5, time = 1)
|
||||
@Measurement(iterations = 5, time = 20)
|
||||
@Fork(3)
|
||||
@State(Scope.Thread)
|
||||
public class PNGImageDecoder_8bit_uninterlaced {
|
||||
|
||||
byte[] pngImageData;
|
||||
|
||||
@Setup
|
||||
public void setup() throws Exception {
|
||||
pngImageData = createImageData(2_500);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void measurePNGImageDecoder(Blackhole bh) throws Exception {
|
||||
Image img = Toolkit.getDefaultToolkit().createImage(pngImageData);
|
||||
BufferedImage bi = createBufferedImage(img);
|
||||
bi.flush();
|
||||
bh.consume(bi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a large sample image stored as an 8-bit PNG.
|
||||
*
|
||||
* @return the byte representation of the PNG image.
|
||||
*/
|
||||
private static byte[] createImageData(int squareSize) throws Exception {
|
||||
BufferedImage bi = new BufferedImage(squareSize, squareSize,
|
||||
BufferedImage.TYPE_BYTE_INDEXED);
|
||||
Random r = new Random(0);
|
||||
Graphics2D g = bi.createGraphics();
|
||||
for (int a = 0; a < 20000; a++) {
|
||||
g.setColor(new Color(r.nextInt(0xffffff)));
|
||||
int radius = 10 + r.nextInt(90);
|
||||
g.fillOval(r.nextInt(bi.getWidth()), r.nextInt(bi.getHeight()),
|
||||
radius, radius);
|
||||
}
|
||||
g.dispose();
|
||||
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(bi, "png", out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
static BufferedImage createBufferedImage(Image img)
|
||||
throws ExecutionException, InterruptedException {
|
||||
CompletableFuture<BufferedImage> future = new CompletableFuture<>();
|
||||
img.getSource().startProduction(new ImageConsumer() {
|
||||
private int imageWidth, imageHeight;
|
||||
private BufferedImage bi;
|
||||
|
||||
@Override
|
||||
public void setDimensions(int width, int height) {
|
||||
imageWidth = width;
|
||||
imageHeight = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperties(Hashtable<?, ?> props) {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorModel(ColorModel model) {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHints(int hintflags) {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int x, int y, int w, int h, ColorModel model,
|
||||
byte[] pixels, int off, int scansize) {
|
||||
if (bi == null) {
|
||||
bi = new BufferedImage(imageWidth, imageHeight,
|
||||
BufferedImage.TYPE_BYTE_INDEXED,
|
||||
(IndexColorModel) model);
|
||||
}
|
||||
if (h != 1)
|
||||
throw new UnsupportedOperationException(
|
||||
"this test expects sequential rows of pixels");
|
||||
if (off != 0)
|
||||
throw new UnsupportedOperationException(
|
||||
"this test expects the incoming pixels to start " +
|
||||
"at index zero");
|
||||
|
||||
bi.getRaster().setDataElements(x, y, w, 1, pixels);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int x, int y, int w, int h, ColorModel model,
|
||||
int[] pixels, int off, int scansize) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void imageComplete(int status) {
|
||||
future.complete(bi);
|
||||
}
|
||||
});
|
||||
return future.get();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user