Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5dbf0a6
Merge pull request #1 from openjdk/master
mickleness May 22, 2022
333c87c
Merge pull request #2 from openjdk/master
mickleness May 24, 2022
57346a4
Merge pull request #3 from openjdk/master
mickleness May 29, 2022
69481fc
Merge pull request #4 from openjdk/master
mickleness Mar 11, 2023
81085d4
Merge pull request #5 from openjdk/master
mickleness Oct 13, 2024
93ef96a
Merge branch 'openjdk:master' into master
mickleness Dec 18, 2024
e4f0c8e
8160327: first draft of APP1 Exif thumbnail support
mickleness Dec 30, 2024
a721256
8160327: updating test to include "ImageCreationTime"
mickleness Dec 30, 2024
fdefdcb
8160327: adding a new test case where `compression` = 0x60000
mickleness Dec 30, 2024
c868a89
8160327: adding a new test case where the timestamp is "0000:00..."
mickleness Dec 30, 2024
c6ba396
8160327: clarifying comment
mickleness Jan 1, 2025
9179a88
8160327: wrap LocaleDateTime.parse in try/catch
mickleness Jan 1, 2025
5adfc17
8160327: support malformed IFD
mickleness Jan 1, 2025
3a5bf5f
8160327: avoid ArrayIndexOutOfBoundsE for unknown data format
mickleness Jan 1, 2025
d5a169a
8160327: if thumbnailLength == 0, we don't have a thumbnail
mickleness Jan 1, 2025
15a079e
8160327: fixing typo in comment
mickleness Jan 1, 2025
05ee149
8160327: removed unused field
mickleness Jan 1, 2025
20d44cf
8160327: fixing typo so `thumbnailPos` can be zero
mickleness Jan 2, 2025
9aa5f3b
8160327: moving test + resources to separate directory
mickleness Feb 20, 2025
76adb79
8160327: include support for JFIF *and* EXIF thumbnails together
mickleness Feb 20, 2025
57a5542
8160327: fallback to using MarkerSegment if ExifMarkerSegment fails
mickleness Feb 21, 2025
a67369a
8160327: alphabetize imports
mickleness Feb 21, 2025
b1e4d13
8160327: replace image of unknown origin with my own image
mickleness Feb 21, 2025
91aabca
8160327: remove bug ID from image file names
mickleness Feb 23, 2025
366a8c3
8160327: fix looping ImageFileDirectory vulnerability
mickleness Feb 23, 2025
24b6fee
8160327: replacing the "sony-d700" image
mickleness Feb 24, 2025
4445d6e
8160327: trying to placate PR script
mickleness Feb 25, 2025
52cf81f
8160327: trying to placate PR script
mickleness Feb 26, 2025
8a9ce53
Revert "8160327: trying to placate PR script"
mickleness Mar 3, 2025
026c3db
Merge branch 'openjdk:master' into master
mickleness Mar 3, 2025
b70b080
Merge branch 'master' into JDK-8160327
mickleness Mar 3, 2025
84827ac
8160327: trying to placate PR script
mickleness Mar 3, 2025
eb2283c
8160327: adding "Reading Thumbnail Images" for Exif marker changes
mickleness Mar 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/*
* Copyright (c) 2001, 2025, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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 com.sun.imageio.plugins.jpeg;

import com.sun.imageio.plugins.tiff.TIFFImageReader;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
* An Exif (Exchangeable Image File Format) APP1 (Application-Specific)
* marker segment. This implementation only supports reading thumbnails
* and the image creation time.
*/
class ExifMarkerSegment extends MarkerSegment {

static class ImageFileDirectory implements Cloneable {
static class Entry implements Cloneable {
final int tagNumber, dataFormat;
final long componentCount, fieldValue;

Entry(ImageInputStream in) throws IOException {
tagNumber = in.readUnsignedShort();
dataFormat = in.readUnsignedShort();
componentCount = in.readUnsignedInt();
fieldValue = in.readUnsignedInt();
}

@Override
public String toString() {
return "Entry[ tagNumber: " + tagNumber +
", dataFormat: " + dataFormat +
", componentCount: " + componentCount +
", fieldValue: " + fieldValue + "]";
}
}
static final int[] bytesPerComponent = new int[] {1, 1, 1, 2, 4, 8, 1};

Map<Integer, Entry> entriesByTag = new LinkedHashMap<>();
long nextIFD;

ImageFileDirectory(ImageInputStream in, long pos) throws IOException {
in.seek(pos);
int entryCount = in.readUnsignedShort();
for (int a = 0; a < entryCount; a++) {
Entry e = new Entry(in);
entriesByTag.put(e.tagNumber, e);
}

// The next 4 bytes SHOULD be the position of the next IFD.

// However in rare cases: the position of the next IFD header is missing. We can detect
// this by checking to see if any of the IFD entries we just read appear where the
// next IFD position *should* be:

long streamPos = in.getStreamPosition();
for (Entry e : entriesByTag.values()) {
int byteLength = e.dataFormat < bytesPerComponent.length ?
(int) (e.componentCount * bytesPerComponent[e.dataFormat]) :
// this is an unknown data format, so let's just assume its 1 byte
1;
if (byteLength > 4) {
long valuePos = e.fieldValue;
if (valuePos <= streamPos) {
nextIFD = 0;
return;
}
}
}

nextIFD = in.readUnsignedInt();
}

int getTagValueAsInt(int tagID) {
ImageFileDirectory.Entry e = entriesByTag.get(tagID);
if (e == null) {
return NO_VALUE;
}
return (int) e.fieldValue;
}
}

private static final int NO_VALUE = -1;

private static final int TIFF_BIG_ENDIAN = 0x4d4d;
private static final int TIFF_MAGIC = 42;
private static final int TIFF_TYPE_SHORT = 3;
private static final int TAG_IMAGE_WIDTH = 256;
private static final int TAG_IMAGE_HEIGHT = 257;
private static final int TAG_DATE_TIME = 306;
private static final int TAG_JPEG_INTERCHANGE_FORMAT = 513;
private static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;

int thumbnailPos = -1;
int thumbnailLength = -1;
boolean isThumbnailJPEG;

int thumbnailWidth = -1;
int thumbnailHeight = -1;

final long firstIFDOffset;
final List<ImageFileDirectory> imageFileDirectories = new LinkedList<>();

ExifMarkerSegment(MarkerSegment originalSegment) throws IOException {
super(originalSegment.tag);
this.length = originalSegment.length;
this.data = originalSegment.data;

ByteArrayInputStream in = new ByteArrayInputStream(data, 6, data.length - 6);

// we aren't actually going to read anything as an image yet, but ImageInputStream
// has useful helper methods:
ImageInputStream input = new MemoryCacheImageInputStream(in);
input.setByteOrder(input.readUnsignedShort() == TIFF_BIG_ENDIAN ?
ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
if (input.readUnsignedShort() != TIFF_MAGIC) {
throw new IllegalArgumentException("Bad magic number");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this exception end up ? I would have supposed that if there's an Exif segment we don't like it would be best to just act like the segment isn't there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I concur.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you first asked: this exception would be thrown all the way up to the JPEGImageReader's caller. (That is: calling myJPEGReader.getNumThumbnails would throw this IAE.)

As of this writing: now this exception is ignored. It is consumed in this code in JPEGMetaData:

            case JPEG.APP1:
                newGuy = new MarkerSegment(buffer);
                newGuy.loadData(buffer);

                if (newGuy.data.length > 5 &&
                        newGuy.data[0] == 'E' &&
                        newGuy.data[1] == 'x' &&
                        newGuy.data[2] == 'i' &&
                        newGuy.data[3] == 'f' &&
                        newGuy.data[4] == 0) {
                    try {
                        newGuy = new ExifMarkerSegment(newGuy);
                    } catch(Exception e) {
                        // This is intentionally empty.
                        // Now we fallback to keeping the generic MarkerSegment
                    }
                }
                break;

}

firstIFDOffset = input.readUnsignedInt();
ImageFileDirectory ifd1 = null;
ImageFileDirectory ifd2 = null;
if (firstIFDOffset != 0) {
ifd1 = new ImageFileDirectory(input, firstIFDOffset);
imageFileDirectories.add(ifd1);

long secondIFDOffset = ifd1.nextIFD;
if (secondIFDOffset != 0) {
ifd2 = new ImageFileDirectory(input, secondIFDOffset);
imageFileDirectories.add(ifd2);
}
}

if (ifd2 != null) {
// the thumbnail should always be described in the 2nd IFD (if it exists at all)

thumbnailPos = ifd2.getTagValueAsInt(TAG_JPEG_INTERCHANGE_FORMAT);
thumbnailLength = ifd2.getTagValueAsInt(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
if (thumbnailPos != NO_VALUE && thumbnailLength != NO_VALUE) {
// The `compression` tag (259) should also help inform whether we read this
// image as a JPEG or TIFF. But in reality this is tricky: the docs say
// the value for a JPEG encoding is 0x0006, but the `jdk_8160327-plastic-wrap.jpg`
// file shows it can also sometimes be 0x60000. I've also observed it to be
// undefined, 0x0007, or several variations of 0x????0006. Similarly the same
// tag should be 0x0001 for TIFFs, but I also observed a case where it as 0x10000.
Comment on lines +177 to +182
Copy link

@haraldk haraldk Mar 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this ( 0x0001/0x0006 vs 0x1000/0x6000) just a matter of endianness in the TIFF structure? Some odd writers may also use LONG/32 bit values, even though the TiFF and Exif specs only mention SHORT/16 bit values for the compression tag.

Compression 7 "New JPEG" is not as per the Exif spec, but it can probably safely be treated the same way as "Old JPEG" compression 6 for Exif thumbnails.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it probably is endianness, or endianness-related. My first design question is: should we care? Currently this PR infers whether we're looking for a JPEG or TIFF thumbnail based on other fields. If we strictly rely on the compression tag (250) instead: is that better/desirable? (That is: we could just throw an IOException in the rare case this field is missing/broken.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compression 7 "New JPEG" is not as per the Exif spec, but it can probably safely be treated the same way as "Old JPEG" compression 6 for Exif thumbnails.

Probably the Compression tag should not be relied upon. While the Exif specification strangely states that for compressed thumbnails its value should be 6, there is no harm in its being 7.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this PR infers whether we're looking for a JPEG or TIFF thumbnail based on other fields.

Specifically the JPEGInterchangeFormat and JPEGInterchangeFormatLength fields. In the Exif 3.0 specification these have support level mandatory for compressed thumbnails and disallowed for uncompressed thumbnails.

If we strictly rely on the compression tag (250) instead: is that better/desirable?

I don't think so.

(That is: we could just throw an IOException in the rare case this field is missing/broken.)

This will indeed happen if JPEGInterchangeFormat does not point to a valid JPEG stream (SOI marker).

isThumbnailJPEG = true;
} else {
thumbnailWidth = ifd2.getTagValueAsInt(TAG_IMAGE_WIDTH);
thumbnailHeight = ifd2.getTagValueAsInt(TAG_IMAGE_HEIGHT);
thumbnailPos = 0;
thumbnailLength = data.length - 6;
isThumbnailJPEG = false;
}
}
}

LocalDateTime getImageCreationTime() {
LocalDateTime imageCreationTime = null;

if (!imageFileDirectories.isEmpty()) {
ImageFileDirectory ifd = imageFileDirectories.get(0);
int dateTimeOffset = ifd.getTagValueAsInt(TAG_DATE_TIME);
if (dateTimeOffset != NO_VALUE) {
try {
String dateTime = new String(data, dateTimeOffset + 6, 19, StandardCharsets.US_ASCII);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu:MM:dd HH:mm:ss");
imageCreationTime = LocalDateTime.parse(dateTime, formatter);
} catch(Exception e) {
// intentionally empty
}
}
}

return imageCreationTime;
}

@Override
void print() {
printTag("Exif APP1");
for (int a = 0; a < imageFileDirectories.size(); a++) {
System.out.println("ImageFileDirectory #" + a + ", offset = " + firstIFDOffset);
int entryCtr = 0;
for (ImageFileDirectory.Entry entry : imageFileDirectories.get(a).entriesByTag.values()) {
System.out.println("Entry #" + (entryCtr++) + ": " + entry.toString());
}
System.out.println("next directory: " + imageFileDirectories.get(a).nextIFD);
}
}

int getNumThumbnails() {
return thumbnailPos >= 0 && thumbnailLength > 0 ? 1 : 0;
}

int getThumbnailWidth() throws IOException {
// this should only be called if there is a thumbnail

if (thumbnailWidth == -1) {
populateJPEGThumbnailDimensions();
}
return thumbnailWidth;
}

int getThumbnailHeight() throws IOException {
// this should only be called if there is a thumbnail

if (thumbnailHeight == -1) {
populateJPEGThumbnailDimensions();
}
return thumbnailHeight;
}

/**
* Use a JPEGImageReader to identify the size of the thumbnail. This
* populates the `thumbnailWidth` and `thumbnailHeight` fields.
*/
private void populateJPEGThumbnailDimensions() throws IOException {
// this method will never be invoked for TIFF thumbnails, because TIFF
// thumbnails clearly define their thumbnail size via IFD entries.
JPEGImageReader reader = new JPEGImageReader(null);
try {
reader.setInput(ImageIO.createImageInputStream(new ByteArrayInputStream(
data, thumbnailPos + 6, thumbnailLength)));
thumbnailWidth = reader.getWidth(0);
thumbnailHeight = reader.getHeight(0);
} finally {
reader.dispose();
}
}

BufferedImage getThumbnail(JPEGImageReader callbackReader) throws IOException {
// this should only be called if there is a thumbnail

callbackReader.thumbnailStarted(0);
ImageReader thumbReader;
int imageIndex = 0;
if (isThumbnailJPEG) {
thumbReader = new JPEGImageReader(null);
imageIndex = 0;
} else {
thumbReader = new TIFFImageReader(null);
imageIndex = 1;
}
try {
InputStream byteIn = new ByteArrayInputStream(data, thumbnailPos + 6, thumbnailLength);
ImageInputStream input = new MemoryCacheImageInputStream(byteIn);
thumbReader.setInput(input);
thumbReader.addIIOReadProgressListener(new JFIFMarkerSegment.JFIFThumbJPEG.ThumbnailReadListener(callbackReader));
BufferedImage thumbnailImage = thumbReader.read(imageIndex);
thumbnailWidth = thumbnailImage.getWidth();
thumbnailHeight = thumbnailImage.getHeight();
callbackReader.thumbnailComplete();
return thumbnailImage;
} finally {
thumbReader.dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2001, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2001, 2025, 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
Expand Down Expand Up @@ -1234,7 +1234,7 @@ int getHeight() {
return retval;
}

private static class ThumbnailReadListener
static class ThumbnailReadListener
implements IIOReadProgressListener {
JPEGImageReader reader = null;
ThumbnailReadListener (JPEGImageReader reader) {
Expand Down
Loading