Skip to content

Commit b6eb8ce

Browse files
cost0muchPaul Hohensee
authored andcommitted
8337681: PNGImageWriter uses much more memory than necessary
Backport-of: 89a15f1414f89d2dd32eac791e9155fcb4207e56
1 parent e904358 commit b6eb8ce

File tree

2 files changed

+199
-5
lines changed

2 files changed

+199
-5
lines changed

src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGImageWriter.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
package com.sun.imageio.plugins.png;
2727

2828
import java.awt.Rectangle;
29+
import java.awt.image.BufferedImage;
2930
import java.awt.image.IndexColorModel;
3031
import java.awt.image.Raster;
3132
import java.awt.image.RenderedImage;
@@ -919,17 +920,28 @@ private void encodePass(ImageOutputStream os,
919920

920921
int bitDepth = metadata.IHDR_bitDepth;
921922
for (int row = minY + yOffset; row < minY + height; row += ySkip) {
922-
Rectangle rect = new Rectangle(minX, row, width, 1);
923-
Raster ras = image.getData(rect);
923+
Raster ras;
924+
if (image instanceof BufferedImage bi) {
925+
// Use the raster directly (no copy).
926+
ras = bi.getRaster();
927+
} else if (image.getNumXTiles() == 1 && image.getNumYTiles() == 1 &&
928+
image.getTileWidth() == width && image.getTileHeight() == height) {
929+
// Use the single tile directly (no copy).
930+
ras = image.getTile(image.getMinTileX(), image.getMinTileY());
931+
} else {
932+
// Make a copy of the raster data.
933+
Rectangle rect = new Rectangle(minX, row, width, 1);
934+
ras = image.getData(rect);
935+
}
936+
924937
if (sourceBands != null) {
925-
ras = ras.createChild(minX, row, width, 1, minX, row,
926-
sourceBands);
938+
ras = ras.createChild(minX, row, width, 1, minX, row, sourceBands);
927939
}
928940

929941
ras.getPixels(minX, row, width, 1, samples);
930942

931943
if (image.getColorModel().isAlphaPremultiplied()) {
932-
WritableRaster wr = ras.createCompatibleWritableRaster();
944+
WritableRaster wr = ras.createCompatibleWritableRaster(minX, row, width, 1);
933945
wr.setPixels(wr.getMinX(), wr.getMinY(),
934946
wr.getWidth(), wr.getHeight(),
935947
samples);
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8337681
27+
* @summary Test that raster use optimization does not cause any regressions.
28+
*/
29+
30+
import java.awt.Color;
31+
import java.awt.Graphics2D;
32+
import java.awt.geom.AffineTransform;
33+
import java.awt.image.BufferedImage;
34+
import java.awt.image.RenderedImage;
35+
import java.io.ByteArrayInputStream;
36+
import java.io.ByteArrayOutputStream;
37+
import javax.imageio.IIOImage;
38+
import javax.imageio.ImageIO;
39+
import javax.imageio.ImageReader;
40+
import javax.imageio.ImageWriteParam;
41+
import javax.imageio.ImageWriter;
42+
import javax.imageio.stream.ImageInputStream;
43+
import javax.imageio.stream.ImageOutputStream;
44+
import javax.imageio.stream.MemoryCacheImageOutputStream;
45+
46+
public class RasterReuseWriteTest {
47+
48+
public static void main(String[] args) throws Exception {
49+
test(BufferedImage.TYPE_INT_RGB);
50+
test(BufferedImage.TYPE_INT_ARGB);
51+
test(BufferedImage.TYPE_INT_ARGB_PRE);
52+
test(BufferedImage.TYPE_4BYTE_ABGR);
53+
test(BufferedImage.TYPE_4BYTE_ABGR_PRE);
54+
}
55+
56+
private static void test(int type) throws Exception {
57+
58+
// swaps blue and red
59+
int bands = (type == BufferedImage.TYPE_INT_RGB ? 3 : 4);
60+
int[] sourceBands = bands == 3 ? new int[] { 2, 1, 0 } :
61+
new int[] { 2, 1, 0, 3 };
62+
63+
// test writing a BufferedImage without source bands
64+
BufferedImage img1 = createImage(256, 256, type);
65+
byte[] bytes1 = writePng(img1, null);
66+
BufferedImage img2 = ImageIO.read(new ByteArrayInputStream(bytes1));
67+
compare(img1, img2, false);
68+
69+
// test writing a BufferedImage with source bands
70+
BufferedImage img3 = createImage(256, 256, type);
71+
byte[] bytes3 = writePng(img3, sourceBands);
72+
BufferedImage img4 = ImageIO.read(new ByteArrayInputStream(bytes3));
73+
compare(img3, img4, true);
74+
75+
// test writing a non-BufferedImage with source bands and one tile
76+
RenderedImage img5 = toTiledImage(img1, 256);
77+
byte[] bytes5 = writePng(img5, sourceBands);
78+
BufferedImage img6 = ImageIO.read(new ByteArrayInputStream(bytes5));
79+
compare(img5, img6, true);
80+
81+
// test writing a non-BufferedImage with source bands and multiple tiles
82+
RenderedImage img7 = toTiledImage(img1, 128);
83+
byte[] bytes7 = writePng(img7, sourceBands);
84+
BufferedImage img8 = ImageIO.read(new ByteArrayInputStream(bytes7));
85+
compare(img7, img8, true);
86+
}
87+
88+
private static BufferedImage createImage(int w, int h, int type) throws Exception {
89+
BufferedImage img = new BufferedImage(w, h, type);
90+
Graphics2D g2d = img.createGraphics();
91+
g2d.setColor(Color.WHITE);
92+
g2d.fillRect(0, 0, w, h);
93+
g2d.setColor(Color.GREEN);
94+
g2d.drawRect(20, 20, 100, 50);
95+
g2d.setColor(Color.RED);
96+
g2d.drawRect(80, 10, 100, 40);
97+
g2d.setColor(Color.BLUE);
98+
g2d.fillRect(40, 60, 120, 30);
99+
g2d.dispose();
100+
return img;
101+
}
102+
103+
private static byte[] writePng(RenderedImage img, int[] sourceBands) throws Exception {
104+
ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
105+
ImageWriteParam param = writer.getDefaultWriteParam();
106+
param.setSourceBands(sourceBands);
107+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
108+
ImageOutputStream stream = new MemoryCacheImageOutputStream(baos);
109+
writer.setOutput(stream);
110+
writer.write(null, new IIOImage(img, null, null), param);
111+
writer.dispose();
112+
stream.flush();
113+
return baos.toByteArray();
114+
}
115+
116+
private static void compare(RenderedImage img1, RenderedImage img2, boolean blueAndRedSwapped) {
117+
int[] pixels1 = getRgbPixels(img1);
118+
int[] pixels2 = getRgbPixels(img2);
119+
for (int i = 0; i < pixels1.length; i++) {
120+
int expected;
121+
if (blueAndRedSwapped && pixels1[i] == 0xFFFF0000) {
122+
expected = 0xFF0000FF; // red -> blue
123+
} else if (blueAndRedSwapped && pixels1[i] == 0xFF0000FF) {
124+
expected = 0xFFFF0000; // blue -> red
125+
} else {
126+
expected = pixels1[i]; // no change
127+
}
128+
int actual = pixels2[i];
129+
if (actual != expected) {
130+
throw new RuntimeException("Pixel " + i + ": expected " +
131+
Integer.toHexString(expected) + ", but got " +
132+
Integer.toHexString(actual));
133+
}
134+
}
135+
}
136+
137+
private static int[] getRgbPixels(RenderedImage img) {
138+
int w = img.getWidth();
139+
int h = img.getHeight();
140+
if (img instanceof BufferedImage bi) {
141+
return bi.getRGB(0, 0, w, h, null, 0, w);
142+
} else {
143+
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
144+
Graphics2D g2d = bi.createGraphics();
145+
g2d.drawRenderedImage(img, new AffineTransform());
146+
g2d.dispose();
147+
return bi.getRGB(0, 0, w, h, null, 0, w);
148+
}
149+
}
150+
151+
private static RenderedImage toTiledImage(BufferedImage img, int tileSize) throws Exception {
152+
153+
// write to TIFF
154+
ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
155+
ImageWriteParam param = writer.getDefaultWriteParam();
156+
param.setTilingMode(ImageWriteParam.MODE_EXPLICIT);
157+
param.setTiling(tileSize, tileSize, 0, 0);
158+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
159+
ImageOutputStream stream = new MemoryCacheImageOutputStream(baos);
160+
writer.setOutput(stream);
161+
writer.write(null, new IIOImage(img, null, null), param);
162+
writer.dispose();
163+
stream.flush();
164+
byte[] bytes = baos.toByteArray();
165+
166+
// read from TIFF
167+
ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
168+
ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes));
169+
reader.setInput(input);
170+
RenderedImage ri = reader.readAsRenderedImage(0, null);
171+
if (ri instanceof BufferedImage) {
172+
throw new RuntimeException("Unexpected BufferedImage");
173+
}
174+
int tw = ri.getTileWidth();
175+
int th = ri.getTileHeight();
176+
if (tw != tileSize || th != tileSize) {
177+
throw new RuntimeException("Expected tile size " + tileSize +
178+
", but found " + tw + "x" + th);
179+
}
180+
return ri;
181+
}
182+
}

0 commit comments

Comments
 (0)