#202, #433: Fixes offset issues when reading multiple JPEGs from single stream + embedded case (ie. TIFF).

This commit is contained in:
Harald Kuhr 2018-08-18 13:08:17 +02:00
parent 27fcd495db
commit 2235f6c911
4 changed files with 217 additions and 69 deletions

View File

@ -39,6 +39,8 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.lang.Validate;
@ -126,6 +128,9 @@ public final class JPEGImageReader extends ImageReaderBase {
/** Cached list of JPEG segments we filter from the underlying stream */ /** Cached list of JPEG segments we filter from the underlying stream */
private List<Segment> segments; private List<Segment> segments;
private int currentStreamIndex = 0;
private List<Long> streamOffsets = new ArrayList<>();
protected JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { protected JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
super(provider); super(provider);
@ -142,6 +147,10 @@ public final class JPEGImageReader extends ImageReaderBase {
@Override @Override
protected void resetMembers() { protected void resetMembers() {
delegate.reset(); delegate.reset();
currentStreamIndex = 0;
streamOffsets.clear();
segments = null; segments = null;
thumbnails = null; thumbnails = null;
@ -171,29 +180,11 @@ public final class JPEGImageReader extends ImageReaderBase {
return delegate.getFormatName(); return delegate.getFormatName();
} }
@Override
public int getNumImages(boolean allowSearch) throws IOException {
if (allowSearch) {
if (isLossless()) {
return 1;
}
}
try {
return delegate.getNumImages(allowSearch);
}
catch (ArrayIndexOutOfBoundsException ignore) {
// This will happen if we find a "tables only" image, with no more images in stream.
return 0;
}
}
private boolean isLossless() throws IOException { private boolean isLossless() throws IOException {
assertInput(); assertInput();
try { try {
Frame sof = getSOF(); if (getSOF().marker == JPEG.SOF3) {
if (sof.marker == JPEG.SOF3) {
return true; return true;
} }
} }
@ -210,32 +201,27 @@ public final class JPEGImageReader extends ImageReaderBase {
@Override @Override
public int getWidth(int imageIndex) throws IOException { public int getWidth(int imageIndex) throws IOException {
checkBounds(imageIndex); checkBounds(imageIndex);
initHeader(imageIndex);
Frame sof = getSOF(); return getSOF().samplesPerLine;
if (sof.marker == JPEG.SOF3) {
return sof.samplesPerLine;
}
return delegate.getWidth(imageIndex);
} }
@Override @Override
public int getHeight(int imageIndex) throws IOException { public int getHeight(int imageIndex) throws IOException {
checkBounds(imageIndex); checkBounds(imageIndex);
initHeader(imageIndex);
Frame sof = getSOF(); return getSOF().lines;
if (sof.marker == JPEG.SOF3) {
return sof.lines;
}
return delegate.getHeight(imageIndex);
} }
@Override @Override
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException { public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
checkBounds(imageIndex);
initHeader(imageIndex);
Iterator<ImageTypeSpecifier> types; Iterator<ImageTypeSpecifier> types;
try { try {
types = delegate.getImageTypes(imageIndex); types = delegate.getImageTypes(0);
} }
catch (IndexOutOfBoundsException | NegativeArraySizeException ignore) { catch (IndexOutOfBoundsException | NegativeArraySizeException ignore) {
types = null; types = null;
@ -301,9 +287,12 @@ public final class JPEGImageReader extends ImageReaderBase {
@Override @Override
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException { public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
checkBounds(imageIndex);
initHeader(imageIndex);
// If delegate can determine the spec, we'll just go with that // If delegate can determine the spec, we'll just go with that
try { try {
ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex); ImageTypeSpecifier rawType = delegate.getRawImageType(0);
if (rawType != null) { if (rawType != null) {
return rawType; return rawType;
@ -332,25 +321,10 @@ public final class JPEGImageReader extends ImageReaderBase {
} }
} }
@Override
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata);
// JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments
delegate.setInput(imageInput != null
? new JPEGSegmentImageInputStream(imageInput, new JPEGSegmentStreamWarningDelegate())
: null, seekForwardOnly, ignoreMetadata);
}
@Override
public boolean isRandomAccessEasy(int imageIndex) throws IOException {
return delegate.isRandomAccessEasy(imageIndex);
}
@Override @Override
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
assertInput();
checkBounds(imageIndex); checkBounds(imageIndex);
initHeader(imageIndex);
Frame sof = getSOF(); Frame sof = getSOF();
ICC_Profile profile = getEmbeddedICCProfile(false); ICC_Profile profile = getEmbeddedICCProfile(false);
@ -392,17 +366,17 @@ public final class JPEGImageReader extends ImageReaderBase {
return bufferedImage; return bufferedImage;
} }
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) // We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
// - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream. // - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream.
else if (delegate.canReadRaster() && ( else if (delegate.canReadRaster() && (
bogusAdobeDCT || bogusAdobeDCT ||
sourceCSType == JPEGColorSpace.CMYK || sourceCSType == JPEGColorSpace.CMYK ||
sourceCSType == JPEGColorSpace.YCCK || sourceCSType == JPEGColorSpace.YCCK ||
profile != null && !ColorSpaces.isCS_sRGB(profile) || profile != null && !ColorSpaces.isCS_sRGB(profile) ||
(long) sof.lines * sof.samplesPerLine > Integer.MAX_VALUE || (long) sof.lines * sof.samplesPerLine > Integer.MAX_VALUE ||
!delegate.getImageTypes(imageIndex).hasNext() || !delegate.getImageTypes(0).hasNext() ||
sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null)) { // TODO: Issue warning? sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null)) { // TODO: Issue warning?
if (DEBUG) { if (DEBUG) {
System.out.println("Reading using raster and extra conversion"); System.out.println("Reading using raster and extra conversion");
System.out.println("ICC color profile: " + profile); System.out.println("ICC color profile: " + profile);
@ -416,7 +390,7 @@ public final class JPEGImageReader extends ImageReaderBase {
System.out.println("Reading using delegate"); System.out.println("Reading using delegate");
} }
return delegate.read(imageIndex, param); return delegate.read(0, param);
} }
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, Frame startOfFrame, JPEGColorSpace csType, ICC_Profile profile) throws IOException { private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, Frame startOfFrame, JPEGColorSpace csType, ICC_Profile profile) throws IOException {
@ -519,7 +493,7 @@ public final class JPEGImageReader extends ImageReaderBase {
// for each iteration, so we'll read all at once. // for each iteration, so we'll read all at once.
try { try {
param.setSourceRegion(srcRegion); param.setSourceRegion(srcRegion);
Raster raster = delegate.readRaster(imageIndex, param); // non-converted Raster raster = delegate.readRaster(0, param); // non-converted
// Apply source color conversion from implicit color space // Apply source color conversion from implicit color space
if (csType == JPEGColorSpace.YCbCr) { if (csType == JPEGColorSpace.YCbCr) {
@ -642,7 +616,7 @@ public final class JPEGImageReader extends ImageReaderBase {
} }
} }
protected ICC_Profile ensureDisplayProfile(final ICC_Profile profile) { private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) {
// NOTE: This is probably not the right way to do it... :-P // NOTE: This is probably not the right way to do it... :-P
// TODO: Consider moving method to ColorSpaces class or new class in imageio.color package // TODO: Consider moving method to ColorSpaces class or new class in imageio.color package
@ -681,6 +655,30 @@ public final class JPEGImageReader extends ImageReaderBase {
array[index + 3] = (byte) (value ); array[index + 3] = (byte) (value );
} }
@Override
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata);
try {
if (imageInput != null) {
streamOffsets.add(imageInput.getStreamPosition());
}
initDelegate(seekForwardOnly, ignoreMetadata);
}
catch (IOException e) {
// TODO: This should ideally be reported as an IOException, but I don't see how
throw new IllegalStateException(e.getMessage(), e);
}
}
private void initDelegate(boolean seekForwardOnly, boolean ignoreMetadata) throws IOException {
// JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments
delegate.setInput(imageInput != null
? new JPEGSegmentImageInputStream(new SubImageInputStream(imageInput, Long.MAX_VALUE), new JPEGSegmentStreamWarningDelegate())
: null, seekForwardOnly, ignoreMetadata);
}
private void initHeader() throws IOException { private void initHeader() throws IOException {
if (segments == null) { if (segments == null) {
long start = DEBUG ? System.currentTimeMillis() : 0; long start = DEBUG ? System.currentTimeMillis() : 0;
@ -714,11 +712,127 @@ public final class JPEGImageReader extends ImageReaderBase {
} }
} }
private void initHeader(final int imageIndex) throws IOException {
if (imageIndex < 0) {
throw new IllegalArgumentException("imageIndex < 0: " + imageIndex);
}
if (imageIndex == currentStreamIndex) {
return;
}
gotoImage(imageIndex);
// Reset segments and re-init the header
segments = null;
thumbnails = null;
initDelegate(seekForwardOnly, ignoreMetadata);
initHeader();
}
private void gotoImage(final int imageIndex) throws IOException {
if (imageIndex < streamOffsets.size()) {
imageInput.seek(streamOffsets.get(imageIndex));
}
else {
long lastKnownSOIOffset = streamOffsets.get(streamOffsets.size() - 1);
imageInput.seek(lastKnownSOIOffset);
try (ImageInputStream stream = new BufferedImageInputStream(imageInput)) { // Extreme (10s -> 50ms) speedup if imageInput is FileIIS
for (int i = streamOffsets.size() - 1; i < imageIndex; i++) {
long start = 0;
if (DEBUG) {
start = System.currentTimeMillis();
System.out.println(String.format("Start seeking for image index %d", i + 1));
}
// Need to skip over segments, as they may contain JPEG markers (eg. JFXX or EXIF thumbnail)
JPEGSegmentUtil.readSegments(stream, Collections.<Integer, List<String>>emptyMap());
// Now, search for EOI and following SOI...
int marker;
while ((marker = stream.read()) != -1) {
if (marker == 0xFF && (0xFF00 | stream.readUnsignedByte()) == JPEG.EOI) {
// Found EOI, now the SOI should be nearby...
while ((marker = stream.read()) != -1) {
if (marker == 0xFF && (0xFF00 | stream.readUnsignedByte()) == JPEG.SOI) {
long nextSOIOffset = stream.getStreamPosition() - 2;
imageInput.seek(nextSOIOffset);
streamOffsets.add(nextSOIOffset);
break;
}
}
// ...or we may have missed it, but at least we tried
break;
}
}
if (DEBUG) {
System.out.println(String.format("Seek in %d ms", System.currentTimeMillis() - start));
}
}
}
catch (EOFException eof) {
IndexOutOfBoundsException ioobe = new IndexOutOfBoundsException("Image index " + imageIndex + " not found in stream");
ioobe.initCause(eof);
throw ioobe;
}
if (imageIndex >= streamOffsets.size()) {
throw new IndexOutOfBoundsException("Image index " + imageIndex + " not found in stream");
}
}
currentStreamIndex = imageIndex;
}
@Override
public int getNumImages(boolean allowSearch) throws IOException {
assertInput();
if (allowSearch) {
if (seekForwardOnly) {
throw new IllegalStateException("seekForwardOnly and allowSearch are both true");
}
int index = 0;
int count = 0;
while (true) {
try {
gotoImage(index++);
}
catch (IndexOutOfBoundsException e) {
break;
}
// TODO: We should probably optimize this
try {
getSOF(); // No SOF, no image
count++;
}
catch (IIOException ignore) {}
}
currentStreamIndex = -1;
return count;
}
// We can't possibly know without searching
return -1;
}
private List<JPEGSegment> readSegments() throws IOException { private List<JPEGSegment> readSegments() throws IOException {
imageInput.mark(); imageInput.mark();
try { try {
imageInput.seek(0); // TODO: Seek to wanted image, skip images on the way imageInput.seek(streamOffsets.get(currentStreamIndex));
return JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS); return JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS);
} }
@ -794,7 +908,7 @@ public final class JPEGImageReader extends ImageReaderBase {
processWarningOccurred("Exif chunk has no data."); processWarningOccurred("Exif chunk has no data.");
} }
else { else {
ImageInputStream stream = ImageIO.createImageInputStream(data); ImageInputStream stream = new MemoryCacheImageInputStream(data);
return (CompoundDirectory) new TIFFReader().read(stream); return (CompoundDirectory) new TIFFReader().read(stream);
// TODO: Directory offset of thumbnail is wrong/relative to container stream, causing trouble for the TIFFReader... // TODO: Directory offset of thumbnail is wrong/relative to container stream, causing trouble for the TIFFReader...
@ -933,6 +1047,7 @@ public final class JPEGImageReader extends ImageReaderBase {
@Override @Override
public Raster readRaster(final int imageIndex, final ImageReadParam param) throws IOException { public Raster readRaster(final int imageIndex, final ImageReadParam param) throws IOException {
checkBounds(imageIndex); checkBounds(imageIndex);
initHeader(imageIndex);
if (isLossless()) { if (isLossless()) {
// TODO: What about stream position? // TODO: What about stream position?
@ -941,7 +1056,7 @@ public final class JPEGImageReader extends ImageReaderBase {
} }
try { try {
return delegate.readRaster(imageIndex, param); return delegate.readRaster(0, param);
} }
catch (IndexOutOfBoundsException knownIssue) { catch (IndexOutOfBoundsException knownIssue) {
// com.sun.imageio.plugins.jpeg.JPEGBuffer doesn't do proper sanity check of input data. // com.sun.imageio.plugins.jpeg.JPEGBuffer doesn't do proper sanity check of input data.
@ -973,6 +1088,7 @@ public final class JPEGImageReader extends ImageReaderBase {
private void readThumbnailMetadata(int imageIndex) throws IOException { private void readThumbnailMetadata(int imageIndex) throws IOException {
checkBounds(imageIndex); checkBounds(imageIndex);
initHeader(imageIndex);
if (thumbnails == null) { if (thumbnails == null) {
thumbnails = new ArrayList<>(); thumbnails = new ArrayList<>();
@ -1098,6 +1214,7 @@ public final class JPEGImageReader extends ImageReaderBase {
public IIOMetadata getImageMetadata(int imageIndex) throws IOException { public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
// checkBounds needed, as we catch the IndexOutOfBoundsException below. // checkBounds needed, as we catch the IndexOutOfBoundsException below.
checkBounds(imageIndex); checkBounds(imageIndex);
initHeader(imageIndex);
IIOMetadata imageMetadata; IIOMetadata imageMetadata;
@ -1106,7 +1223,7 @@ public final class JPEGImageReader extends ImageReaderBase {
} }
else { else {
try { try {
imageMetadata = delegate.getImageMetadata(imageIndex); imageMetadata = delegate.getImageMetadata(0);
} }
catch (IndexOutOfBoundsException knownIssue) { catch (IndexOutOfBoundsException knownIssue) {
// TMI-101: com.sun.imageio.plugins.jpeg.JPEGBuffer doesn't do proper sanity check of input data. // TMI-101: com.sun.imageio.plugins.jpeg.JPEGBuffer doesn't do proper sanity check of input data.
@ -1179,22 +1296,22 @@ public final class JPEGImageReader extends ImageReaderBase {
@Override @Override
public void imageComplete(ImageReader source) { public void imageComplete(ImageReader source) {
processImageComplete(); processImageComplete();
} }
@Override @Override
public void imageProgress(ImageReader source, float percentageDone) { public void imageProgress(ImageReader source, float percentageDone) {
processImageProgress(percentageDone); processImageProgress(percentageDone);
} }
@Override @Override
public void imageStarted(ImageReader source, int imageIndex) { public void imageStarted(ImageReader source, int imageIndex) {
processImageStarted(imageIndex); processImageStarted(currentStreamIndex);
} }
@Override @Override
public void readAborted(ImageReader source) { public void readAborted(ImageReader source) {
processReadAborted(); processReadAborted();
} }
@Override @Override
@ -1219,7 +1336,7 @@ public final class JPEGImageReader extends ImageReaderBase {
@Override @Override
public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {
processThumbnailStarted(imageIndex, thumbnailIndex); processThumbnailStarted(currentStreamIndex, thumbnailIndex);
} }
public void passStarted(ImageReader source, BufferedImage theImage, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) { public void passStarted(ImageReader source, BufferedImage theImage, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) {
@ -1274,6 +1391,7 @@ public final class JPEGImageReader extends ImageReaderBase {
processWarningOccurred(warning); processWarningOccurred(warning);
} }
} }
protected static void showIt(final BufferedImage pImage, final String pTitle) { protected static void showIt(final BufferedImage pImage, final String pTitle) {
ImageReaderBase.showIt(pImage, pTitle); ImageReaderBase.showIt(pImage, pTitle);
} }

View File

@ -172,6 +172,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
} }
} }
else if (isSOFMarker(marker)) { else if (isSOFMarker(marker)) {
// TODO: Warning + ignore if we already have a SOF
// Replace duplicate SOFn component ids // Replace duplicate SOFn component ids
byte[] data = readReplaceDuplicateSOFnComponentIds(marker, length); byte[] data = readReplaceDuplicateSOFnComponentIds(marker, length);
segment = new ReplacementSegment(marker, realPosition, segment.end(), length, data); segment = new ReplacementSegment(marker, realPosition, segment.end(), length, data);
@ -272,7 +273,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
processWarningOccured(String.format("Duplicate component ID %d in SOF", id)); processWarningOccured(String.format("Duplicate component ID %d in SOF", id));
id++; id++;
while (!componentIds.add(id)) { while (!componentIds.add(id) && componentIds.size() <= 16) {
id++; id++;
} }

View File

@ -100,6 +100,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
new TestData(getClassLoaderResource("/jpeg/app-marker-missing-null-term.jpg"), new Dimension(200, 150)), new TestData(getClassLoaderResource("/jpeg/app-marker-missing-null-term.jpg"), new Dimension(200, 150)),
new TestData(getClassLoaderResource("/jpeg/jfif-16bit-dqt.jpg"), new Dimension(204, 131)), new TestData(getClassLoaderResource("/jpeg/jfif-16bit-dqt.jpg"), new Dimension(204, 131)),
new TestData(getClassLoaderResource("/jpeg/jfif-grayscale-thumbnail.jpg"), new Dimension(2547, 1537)), // Non-compliant JFIF with 8 bit grayscale thumbnail new TestData(getClassLoaderResource("/jpeg/jfif-grayscale-thumbnail.jpg"), new Dimension(2547, 1537)), // Non-compliant JFIF with 8 bit grayscale thumbnail
new TestData(getClassLoaderResource("/jpeg/jfif-with-preview-as-second-image.jpg"), new Dimension(3968, 2976), new Dimension(640, 480)), // JFIF, full size + preview
new TestData(getClassLoaderResource("/jpeg-lossless/8_ls.jpg"), new Dimension(800, 535)), // Lossless gray, 8 bit new TestData(getClassLoaderResource("/jpeg-lossless/8_ls.jpg"), new Dimension(800, 535)), // Lossless gray, 8 bit
new TestData(getClassLoaderResource("/jpeg-lossless/16_ls.jpg"), new Dimension(800, 535)), // Lossless gray, 16 bit new TestData(getClassLoaderResource("/jpeg-lossless/16_ls.jpg"), new Dimension(800, 535)), // Lossless gray, 16 bit
new TestData(getClassLoaderResource("/jpeg-lossless/24_ls.jpg"), new Dimension(800, 535)), // Lossless RGB, 8 bit per component (24 bit) new TestData(getClassLoaderResource("/jpeg-lossless/24_ls.jpg"), new Dimension(800, 535)), // Lossless RGB, 8 bit per component (24 bit)
@ -1778,4 +1779,32 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
reader.dispose(); reader.dispose();
} }
} }
@Test
public void testReadSequenceInverse() throws IOException {
JPEGImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-with-preview-as-second-image.jpg")));
BufferedImage image = reader.read(1, null);
assertNotNull(image);
assertEquals(640, image.getWidth());
assertEquals(480, image.getHeight());
assertEquals(ColorSpace.TYPE_RGB, image.getColorModel().getColorSpace().getType());
image = reader.read(0, null);
assertNotNull(image);
assertEquals(3968, image.getWidth());
assertEquals(2976, image.getHeight());
assertEquals(ColorSpace.TYPE_RGB, image.getColorModel().getColorSpace().getType());
assertEquals(2, reader.getNumImages(true));
}
finally {
reader.dispose();
}
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB