Skip to content

Commit 73465b9

Browse files
micklenessprrace
authored andcommitted
8160327: Support for thumbnails present in APP1 marker for JPEG
Reviewed-by: prr
1 parent dbdbbd4 commit 73465b9

File tree

14 files changed

+711
-23
lines changed

14 files changed

+711
-23
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/*
2+
* Copyright (c) 2001, 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. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package com.sun.imageio.plugins.jpeg;
27+
28+
import com.sun.imageio.plugins.tiff.TIFFImageReader;
29+
30+
import javax.imageio.ImageIO;
31+
import javax.imageio.ImageReader;
32+
import javax.imageio.stream.ImageInputStream;
33+
import javax.imageio.stream.MemoryCacheImageInputStream;
34+
import java.awt.image.BufferedImage;
35+
import java.io.ByteArrayInputStream;
36+
import java.io.IOException;
37+
import java.io.InputStream;
38+
import java.nio.ByteOrder;
39+
import java.nio.charset.StandardCharsets;
40+
import java.time.LocalDateTime;
41+
import java.time.format.DateTimeFormatter;
42+
import java.util.LinkedHashMap;
43+
import java.util.LinkedList;
44+
import java.util.List;
45+
import java.util.Map;
46+
47+
/**
48+
* An Exif (Exchangeable Image File Format) APP1 (Application-Specific)
49+
* marker segment. This implementation only supports reading thumbnails
50+
* and the image creation time.
51+
*/
52+
class ExifMarkerSegment extends MarkerSegment {
53+
54+
static class ImageFileDirectory implements Cloneable {
55+
static class Entry implements Cloneable {
56+
final int tagNumber, dataFormat;
57+
final long componentCount, fieldValue;
58+
59+
Entry(ImageInputStream in) throws IOException {
60+
tagNumber = in.readUnsignedShort();
61+
dataFormat = in.readUnsignedShort();
62+
componentCount = in.readUnsignedInt();
63+
fieldValue = in.readUnsignedInt();
64+
}
65+
66+
@Override
67+
public String toString() {
68+
return "Entry[ tagNumber: " + tagNumber +
69+
", dataFormat: " + dataFormat +
70+
", componentCount: " + componentCount +
71+
", fieldValue: " + fieldValue + "]";
72+
}
73+
}
74+
static final int[] bytesPerComponent = new int[] {1, 1, 1, 2, 4, 8, 1};
75+
76+
Map<Integer, Entry> entriesByTag = new LinkedHashMap<>();
77+
long nextIFD;
78+
79+
ImageFileDirectory(ImageInputStream in, long pos) throws IOException {
80+
in.seek(pos);
81+
int entryCount = in.readUnsignedShort();
82+
for (int a = 0; a < entryCount; a++) {
83+
Entry e = new Entry(in);
84+
entriesByTag.put(e.tagNumber, e);
85+
}
86+
87+
// The next 4 bytes SHOULD be the position of the next IFD.
88+
89+
// However in rare cases: the position of the next IFD header is missing. We can detect
90+
// this by checking to see if any of the IFD entries we just read appear where the
91+
// next IFD position *should* be:
92+
93+
long streamPos = in.getStreamPosition();
94+
for (Entry e : entriesByTag.values()) {
95+
int byteLength = e.dataFormat < bytesPerComponent.length ?
96+
(int) (e.componentCount * bytesPerComponent[e.dataFormat]) :
97+
// this is an unknown data format, so let's just assume its 1 byte
98+
1;
99+
if (byteLength > 4) {
100+
long valuePos = e.fieldValue;
101+
if (valuePos <= streamPos) {
102+
nextIFD = 0;
103+
return;
104+
}
105+
}
106+
}
107+
108+
nextIFD = in.readUnsignedInt();
109+
}
110+
111+
int getTagValueAsInt(int tagID) {
112+
ImageFileDirectory.Entry e = entriesByTag.get(tagID);
113+
if (e == null) {
114+
return NO_VALUE;
115+
}
116+
return (int) e.fieldValue;
117+
}
118+
}
119+
120+
private static final int NO_VALUE = -1;
121+
122+
private static final int TIFF_BIG_ENDIAN = 0x4d4d;
123+
private static final int TIFF_MAGIC = 42;
124+
private static final int TIFF_TYPE_SHORT = 3;
125+
private static final int TAG_IMAGE_WIDTH = 256;
126+
private static final int TAG_IMAGE_HEIGHT = 257;
127+
private static final int TAG_DATE_TIME = 306;
128+
private static final int TAG_JPEG_INTERCHANGE_FORMAT = 513;
129+
private static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
130+
131+
int thumbnailPos = -1;
132+
int thumbnailLength = -1;
133+
boolean isThumbnailJPEG;
134+
135+
int thumbnailWidth = -1;
136+
int thumbnailHeight = -1;
137+
138+
final long firstIFDOffset;
139+
final List<ImageFileDirectory> imageFileDirectories = new LinkedList<>();
140+
141+
ExifMarkerSegment(MarkerSegment originalSegment) throws IOException {
142+
super(originalSegment.tag);
143+
this.length = originalSegment.length;
144+
this.data = originalSegment.data;
145+
146+
ByteArrayInputStream in = new ByteArrayInputStream(data, 6, data.length - 6);
147+
148+
// we aren't actually going to read anything as an image yet, but ImageInputStream
149+
// has useful helper methods:
150+
ImageInputStream input = new MemoryCacheImageInputStream(in);
151+
input.setByteOrder(input.readUnsignedShort() == TIFF_BIG_ENDIAN ?
152+
ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
153+
if (input.readUnsignedShort() != TIFF_MAGIC) {
154+
throw new IllegalArgumentException("Bad magic number");
155+
}
156+
157+
firstIFDOffset = input.readUnsignedInt();
158+
ImageFileDirectory ifd1 = null;
159+
ImageFileDirectory ifd2 = null;
160+
if (firstIFDOffset != 0) {
161+
ifd1 = new ImageFileDirectory(input, firstIFDOffset);
162+
imageFileDirectories.add(ifd1);
163+
164+
long secondIFDOffset = ifd1.nextIFD;
165+
if (secondIFDOffset != 0) {
166+
ifd2 = new ImageFileDirectory(input, secondIFDOffset);
167+
imageFileDirectories.add(ifd2);
168+
}
169+
}
170+
171+
if (ifd2 != null) {
172+
// the thumbnail should always be described in the 2nd IFD (if it exists at all)
173+
174+
thumbnailPos = ifd2.getTagValueAsInt(TAG_JPEG_INTERCHANGE_FORMAT);
175+
thumbnailLength = ifd2.getTagValueAsInt(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
176+
if (thumbnailPos != NO_VALUE && thumbnailLength != NO_VALUE) {
177+
// The `compression` tag (259) should also help inform whether we read this
178+
// image as a JPEG or TIFF. But in reality this is tricky: the docs say
179+
// the value for a JPEG encoding is 0x0006, but the `jdk_8160327-plastic-wrap.jpg`
180+
// file shows it can also sometimes be 0x60000. I've also observed it to be
181+
// undefined, 0x0007, or several variations of 0x????0006. Similarly the same
182+
// tag should be 0x0001 for TIFFs, but I also observed a case where it as 0x10000.
183+
isThumbnailJPEG = true;
184+
} else {
185+
thumbnailWidth = ifd2.getTagValueAsInt(TAG_IMAGE_WIDTH);
186+
thumbnailHeight = ifd2.getTagValueAsInt(TAG_IMAGE_HEIGHT);
187+
thumbnailPos = 0;
188+
thumbnailLength = data.length - 6;
189+
isThumbnailJPEG = false;
190+
}
191+
}
192+
}
193+
194+
LocalDateTime getImageCreationTime() {
195+
LocalDateTime imageCreationTime = null;
196+
197+
if (!imageFileDirectories.isEmpty()) {
198+
ImageFileDirectory ifd = imageFileDirectories.get(0);
199+
int dateTimeOffset = ifd.getTagValueAsInt(TAG_DATE_TIME);
200+
if (dateTimeOffset != NO_VALUE) {
201+
try {
202+
String dateTime = new String(data, dateTimeOffset + 6, 19, StandardCharsets.US_ASCII);
203+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu:MM:dd HH:mm:ss");
204+
imageCreationTime = LocalDateTime.parse(dateTime, formatter);
205+
} catch(Exception e) {
206+
// intentionally empty
207+
}
208+
}
209+
}
210+
211+
return imageCreationTime;
212+
}
213+
214+
@Override
215+
void print() {
216+
printTag("Exif APP1");
217+
for (int a = 0; a < imageFileDirectories.size(); a++) {
218+
System.out.println("ImageFileDirectory #" + a + ", offset = " + firstIFDOffset);
219+
int entryCtr = 0;
220+
for (ImageFileDirectory.Entry entry : imageFileDirectories.get(a).entriesByTag.values()) {
221+
System.out.println("Entry #" + (entryCtr++) + ": " + entry.toString());
222+
}
223+
System.out.println("next directory: " + imageFileDirectories.get(a).nextIFD);
224+
}
225+
}
226+
227+
int getNumThumbnails() {
228+
return thumbnailPos >= 0 && thumbnailLength > 0 ? 1 : 0;
229+
}
230+
231+
int getThumbnailWidth() throws IOException {
232+
// this should only be called if there is a thumbnail
233+
234+
if (thumbnailWidth == -1) {
235+
populateJPEGThumbnailDimensions();
236+
}
237+
return thumbnailWidth;
238+
}
239+
240+
int getThumbnailHeight() throws IOException {
241+
// this should only be called if there is a thumbnail
242+
243+
if (thumbnailHeight == -1) {
244+
populateJPEGThumbnailDimensions();
245+
}
246+
return thumbnailHeight;
247+
}
248+
249+
/**
250+
* Use a JPEGImageReader to identify the size of the thumbnail. This
251+
* populates the `thumbnailWidth` and `thumbnailHeight` fields.
252+
*/
253+
private void populateJPEGThumbnailDimensions() throws IOException {
254+
// this method will never be invoked for TIFF thumbnails, because TIFF
255+
// thumbnails clearly define their thumbnail size via IFD entries.
256+
JPEGImageReader reader = new JPEGImageReader(null);
257+
try {
258+
reader.setInput(ImageIO.createImageInputStream(new ByteArrayInputStream(
259+
data, thumbnailPos + 6, thumbnailLength)));
260+
thumbnailWidth = reader.getWidth(0);
261+
thumbnailHeight = reader.getHeight(0);
262+
} finally {
263+
reader.dispose();
264+
}
265+
}
266+
267+
BufferedImage getThumbnail(JPEGImageReader callbackReader) throws IOException {
268+
// this should only be called if there is a thumbnail
269+
270+
callbackReader.thumbnailStarted(0);
271+
ImageReader thumbReader;
272+
int imageIndex = 0;
273+
if (isThumbnailJPEG) {
274+
thumbReader = new JPEGImageReader(null);
275+
imageIndex = 0;
276+
} else {
277+
thumbReader = new TIFFImageReader(null);
278+
imageIndex = 1;
279+
}
280+
try {
281+
InputStream byteIn = new ByteArrayInputStream(data, thumbnailPos + 6, thumbnailLength);
282+
ImageInputStream input = new MemoryCacheImageInputStream(byteIn);
283+
thumbReader.setInput(input);
284+
thumbReader.addIIOReadProgressListener(new JFIFMarkerSegment.JFIFThumbJPEG.ThumbnailReadListener(callbackReader));
285+
BufferedImage thumbnailImage = thumbReader.read(imageIndex);
286+
thumbnailWidth = thumbnailImage.getWidth();
287+
thumbnailHeight = thumbnailImage.getHeight();
288+
callbackReader.thumbnailComplete();
289+
return thumbnailImage;
290+
} finally {
291+
thumbReader.dispose();
292+
}
293+
}
294+
}

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

Lines changed: 2 additions & 2 deletions
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
@@ -1234,7 +1234,7 @@ int getHeight() {
12341234
return retval;
12351235
}
12361236

1237-
private static class ThumbnailReadListener
1237+
static class ThumbnailReadListener
12381238
implements IIOReadProgressListener {
12391239
JPEGImageReader reader = null;
12401240
ThumbnailReadListener (JPEGImageReader reader) {

0 commit comments

Comments
 (0)