Skip to content

Commit a9b396f

Browse files
rm-gh-8Paul Hohensee
authored andcommitted
8351110: ImageIO.write for JPEG can write corrupt JPEG for certain thumbnail dimensions
Backport-of: 60f3d607412dfe289f33dd922dfc1c9ff766810f
1 parent 505a371 commit a9b396f

File tree

3 files changed

+168
-1
lines changed

3 files changed

+168
-1
lines changed

src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JFIFMarkerSegment.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,15 @@ void write(ImageOutputStream ios,
376376
}
377377
thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
378378
thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
379+
380+
int maxArea = (0xffff - DATA_SIZE - LENGTH_SIZE) / thumb.getSampleModel().getNumBands();
381+
if (thumbWidth * thumbHeight > maxArea) {
382+
writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
383+
double scale = Math.sqrt( ((double)maxArea) / (double)(thumbWidth * thumbHeight) );
384+
thumbWidth = (int) (scale * thumbWidth);
385+
thumbHeight = (int) (scale * thumbHeight);
386+
}
387+
379388
thumbData = thumb.getRaster().getPixels(0, 0,
380389
thumbWidth, thumbHeight,
381390
(int []) null);

src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/MarkerSegment.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2001, 2021, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -190,6 +190,9 @@ void write(ImageOutputStream ios) throws IOException {
190190

191191
static void write2bytes(ImageOutputStream ios,
192192
int value) throws IOException {
193+
if (value < 0 || value > 0xffff) {
194+
throw new IIOException("Invalid 2-byte value: " + value);
195+
}
193196
ios.write((value >> 8) & 0xff);
194197
ios.write(value & 0xff);
195198

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright (c) 2025, 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 8351110
27+
* @summary Test verifies that when a JFIF thumbnail may exceed 65535 bytes
28+
* we still write a valid JPEG file by clipping the thumbnail.
29+
* @run main WriteJPEGThumbnailTest
30+
*/
31+
32+
import javax.imageio.IIOImage;
33+
import javax.imageio.ImageIO;
34+
import javax.imageio.ImageReader;
35+
import javax.imageio.ImageWriter;
36+
import javax.imageio.stream.ImageInputStream;
37+
import javax.imageio.stream.ImageOutputStream;
38+
import java.awt.geom.AffineTransform;
39+
import java.awt.image.BufferedImage;
40+
import java.awt.Color;
41+
import java.awt.Graphics2D;
42+
import java.io.ByteArrayInputStream;
43+
import java.io.ByteArrayOutputStream;
44+
import java.io.IOException;
45+
import java.io.OutputStream;
46+
import java.util.Iterator;
47+
import java.util.List;
48+
49+
public class WriteJPEGThumbnailTest {
50+
51+
private static void assertEquals(int expected, int observed) {
52+
if (expected != observed) {
53+
throw new Error("expected " + expected + ", but observed " + observed);
54+
}
55+
}
56+
57+
public static void main(String[] args) throws Exception {
58+
// this always passed. 21800 * 3 = 65400, which fits in a 65535-byte segment.
59+
boolean b1 = new WriteJPEGThumbnailTest(100, 218).run();
60+
61+
// this failed prior to resolving 8351110. 21900 * 3 = 65700, which is too large
62+
// for a JPEG segment. Now we clip the thumbnail to make it fit. (Previously
63+
// we wrote a corrupt JPEG file.)
64+
boolean b2 = new WriteJPEGThumbnailTest(100, 219).run();
65+
66+
if (!(b1 && b2)) {
67+
System.err.println("Test failed");
68+
throw new Error("Test failed");
69+
}
70+
}
71+
72+
final int thumbWidth;
73+
final int thumbHeight;
74+
75+
public WriteJPEGThumbnailTest(int thumbWidth, int thumbHeight) {
76+
this.thumbWidth = thumbWidth;
77+
this.thumbHeight = thumbHeight;
78+
}
79+
80+
public boolean run() throws Exception {
81+
System.out.println("Testing thumbnail " + thumbWidth + "x" + thumbHeight + "...");
82+
try {
83+
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
84+
BufferedImage thumbnail = writeImage(byteOut);
85+
byte[] jpegData = byteOut.toByteArray();
86+
ImageReader reader = getJPEGImageReader();
87+
ImageInputStream stream = ImageIO.createImageInputStream(new ByteArrayInputStream(jpegData));
88+
reader.setInput(stream);
89+
assertEquals(1, reader.getNumThumbnails(0));
90+
91+
// we may have a subset of our original thumbnail, that's OK
92+
BufferedImage readThumbnail = reader.readThumbnail(0, 0);
93+
for (int y = 0; y < readThumbnail.getHeight(); y++) {
94+
for (int x = 0; x < readThumbnail.getWidth(); x++) {
95+
int rgb1 = thumbnail.getRGB(x, y);
96+
int rgb2 = readThumbnail.getRGB(x, y);
97+
assertEquals(rgb1, rgb2);
98+
}
99+
}
100+
System.out.println("\tTest passed");
101+
} catch (Throwable e) {
102+
e.printStackTrace();
103+
return false;
104+
}
105+
return true;
106+
}
107+
108+
private BufferedImage writeImage(OutputStream out) throws IOException {
109+
BufferedImage thumbnail = createImage(thumbWidth, thumbHeight);
110+
BufferedImage bi = createImage(thumbnail.getWidth() * 10, thumbnail.getHeight() * 10);
111+
112+
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
113+
114+
try (ImageOutputStream outputStream = ImageIO.createImageOutputStream(out)) {
115+
writer.setOutput(outputStream);
116+
117+
// Write the main image
118+
IIOImage img = new IIOImage(bi, List.of(thumbnail), null);
119+
writer.write(null, img, null);
120+
} finally {
121+
writer.dispose();
122+
}
123+
return thumbnail;
124+
}
125+
126+
private static BufferedImage createImage(int width, int height) {
127+
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
128+
Graphics2D g = bi.createGraphics();
129+
double sx = ((double)width) / 1000.0;
130+
double sy = ((double)height) / 1000.0;
131+
g.transform(AffineTransform.getScaleInstance(sx, sy));
132+
g.setColor(Color.RED);
133+
g.fillRect(0, 0, 100, 100);
134+
g.setColor(Color.GREEN);
135+
g.fillRect(900, 0, 900, 100);
136+
g.setColor(Color.ORANGE);
137+
g.fillRect(0, 900, 100, 100);
138+
g.setColor(Color.MAGENTA);
139+
g.fillRect(900, 900, 100, 100);
140+
g.dispose();
141+
return bi;
142+
}
143+
144+
private static ImageReader getJPEGImageReader() {
145+
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpeg");
146+
ImageReader reader;
147+
while (readers.hasNext()) {
148+
reader = readers.next();
149+
if (reader.canReadRaster()) {
150+
return reader;
151+
}
152+
}
153+
return null;
154+
}
155+
}

0 commit comments

Comments
 (0)