Merge pull request #1 from haraldk/master

Update from haraldk/TwelveMonkeys
This commit is contained in:
Tyler 2015-04-01 14:18:53 -07:00
commit e7d2cad8ff
105 changed files with 6028 additions and 1519 deletions

View File

@ -706,30 +706,15 @@ public final class ImageUtil {
AffineTransform transform = AffineTransform.getTranslateInstance((newW - w) / 2.0, (newH - h) / 2.0);
transform.rotate(pAngle, w / 2.0, h / 2.0);
//AffineTransformOp transformOp = new AffineTransformOp(
// transform, fast ? AffineTransformOp.TYPE_NEAREST_NEIGHBOR : 3 // 3 == TYPE_BICUBIC
//);
//
//return transformOp.filter(pSource, null);
// TODO: Figure out if this is correct
BufferedImage dest = createTransparent(newW, newH);
//ColorModel cm = pSource.getColorModel();
//new BufferedImage(cm,
// createCompatibleWritableRaster(pSource, cm, newW, newH),
// cm.isAlphaPremultiplied(), null);
// See: http://weblogs.java.net/blog/campbell/archive/2007/03/java_2d_tricker_1.html
Graphics2D g = dest.createGraphics();
try {
g.transform(transform);
if (!fast) {
// Clear with all transparent
//Composite normal = g.getComposite();
//g.setComposite(AlphaComposite.Clear);
//g.fillRect(0, 0, newW, newH);
//g.setComposite(normal);
// Max quality
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);

View File

@ -1,178 +0,0 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.io.enc;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* Decoder implementation for 16 bit-chunked Apple PackBits-like run-length
* encoding.
* <p/>
* This version of the decoder decodes chunk of 16 bit, instead of 8 bit.
* This format is used in certain PICT files.
*
* @see PackBitsDecoder
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java#2 $
*/
public final class PackBits16Decoder implements Decoder {
// TODO: Refactor this into an option for the PackBitsDecoder (bytesPerSample, default == 1)?
private final boolean disableNoop;
private int leftOfRun;
private boolean splitRun;
private boolean reachedEOF;
/**
* Creates a {@code PackBitsDecoder}.
*/
public PackBits16Decoder() {
this(false);
}
/**
* Creates a {@code PackBitsDecoder}.
* <p/>
* As some implementations of PackBits-like encoders treat {@code -128} as length of
* a compressed run, instead of a no-op, it's possible to disable no-ops
* for compatibility.
* Should be used with caution, even though, most known encoders never write
* no-ops in the compressed streams.
*
* @param pDisableNoop {@code true} if {@code -128} should be treated as a compressed run, and not a no-op
*/
public PackBits16Decoder(final boolean pDisableNoop) {
disableNoop = pDisableNoop;
}
/**
* Decodes bytes from the given input stream, to the given buffer.
*
* @param stream the stream to decode from
* @param buffer a byte array, minimum 128 (or 129 if no-op is disabled)
* bytes long
* @return The number of bytes decoded
*
* @throws java.io.IOException
*/
public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
if (reachedEOF) {
return -1;
}
while (buffer.hasRemaining()) {
int n;
if (splitRun) {
// Continue run
n = leftOfRun;
splitRun = false;
}
else {
// Start new run
int b = stream.read();
if (b < 0) {
reachedEOF = true;
break;
}
n = (byte) b;
}
// Split run at or before max
if (n >= 0 && 2 * (n + 1) > buffer.remaining()) {
leftOfRun = n;
splitRun = true;
break;
}
else if (n < 0 && 2 * (-n + 1) > buffer.remaining()) {
leftOfRun = n;
splitRun = true;
break;
}
try {
if (n >= 0) {
// Copy next n + 1 shorts literally
readFully(stream, buffer, 2 * (n + 1));
}
// Allow -128 for compatibility, see above
else if (disableNoop || n != -128) {
// Replicate the next short -n + 1 times
byte value1 = readByte(stream);
byte value2 = readByte(stream);
for (int i = -n + 1; i > 0; i--) {
buffer.put(value1);
buffer.put(value2);
}
}
// else NOOP (-128)
}
catch (IndexOutOfBoundsException e) {
throw new DecodeException("Error in PackBits decompression, data seems corrupt", e);
}
}
return buffer.position();
}
private static byte readByte(final InputStream pStream) throws IOException {
int read = pStream.read();
if (read < 0) {
throw new EOFException("Unexpected end of PackBits stream");
}
return (byte) read;
}
private static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException {
if (pLength < 0) {
throw new IndexOutOfBoundsException();
}
int total = 0;
while (total < pLength) {
int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + total, pLength - total);
if (count < 0) {
throw new EOFException("Unexpected end of PackBits stream");
}
total += count;
}
pBuffer.position(pBuffer.position() + total);
}
}

View File

@ -66,7 +66,8 @@ import java.nio.ByteBuffer;
public final class PackBitsDecoder implements Decoder {
// TODO: Look at ICNSImageReader#unpackbits... What is this weirdness?
private final boolean disableNoop;
private final boolean disableNoOp;
private final byte[] sample;
private int leftOfRun;
private boolean splitRun;
@ -74,7 +75,7 @@ public final class PackBitsDecoder implements Decoder {
/** Creates a {@code PackBitsDecoder}. */
public PackBitsDecoder() {
this(false);
this(1, false);
}
/**
@ -84,10 +85,24 @@ public final class PackBitsDecoder implements Decoder {
* a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility.
* Should be used with caution, even though, most known encoders never write no-ops in the compressed streams.
*
* @param pDisableNoop {@code true} if {@code -128} should be treated as a compressed run, and not a no-op
* @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op
*/
public PackBitsDecoder(final boolean pDisableNoop) {
disableNoop = pDisableNoop;
public PackBitsDecoder(final boolean disableNoOp) {
this(1, disableNoOp);
}
/**
* Creates a {@code PackBitsDecoder}, with optional compatibility mode.
* <p/>
* As some implementations of PackBits-like encoders treat {@code -128} as length of
* a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility.
* Should be used with caution, even though, most known encoders never write no-ops in the compressed streams.
*
* @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op
*/
public PackBitsDecoder(int sampleSize, final boolean disableNoOp) {
this.sample = new byte[sampleSize];
this.disableNoOp = disableNoOp;
}
/**
@ -138,15 +153,17 @@ public final class PackBitsDecoder implements Decoder {
try {
if (n >= 0) {
// Copy next n + 1 bytes literally
readFully(stream, buffer, n + 1);
readFully(stream, buffer, sample.length * (n + 1));
}
// Allow -128 for compatibility, see above
else if (disableNoop || n != -128) {
else if (disableNoOp || n != -128) {
// Replicate the next byte -n + 1 times
byte value = readByte(stream);
for (int s = 0; s < sample.length; s++) {
sample[s] = readByte(stream);
}
for (int i = -n + 1; i > 0; i--) {
buffer.put(value);
buffer.put(sample);
}
}
// else NOOP (-128)

View File

@ -28,8 +28,7 @@
package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
@ -47,30 +46,9 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: BMPImageReaderSpi.java,v 1.0 25.feb.2006 00:29:44 haku Exp$
*/
public final class BMPImageReaderSpi extends ImageReaderSpi {
public final class BMPImageReaderSpi extends ImageReaderSpiBase {
public BMPImageReaderSpi() {
this(IIOUtil.getProviderInfo(BMPImageReaderSpi.class));
}
private BMPImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"bmp", "BMP"},
new String[]{"bmp", "rle"},
new String[]{
"image/bmp",
"image/x-bmp"
// "image/vnd.microsoft.bitmap", // TODO: Official IANA MIME
},
"com.twelvemonkeys.imageio.plugins.bmp.BMPImageReader",
new Class[]{ImageInputStream.class},
new String[]{"com.sun.imageio.plugins.bmp.BMPImageWriterSpi"}, // We support the same native metadata format
false, null, null, null, null,
true,
BMPMetadata.nativeMetadataFormatName, "com.sun.imageio.plugins.bmp.BMPMetadataFormat",
null, null
);
super(new BMPProviderInfo());
}
static ImageReaderSpi lookupDefaultProvider(final ServiceRegistry registry) {

View File

@ -0,0 +1,31 @@
package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* BMPProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: BMPProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class BMPProviderInfo extends ReaderWriterProviderInfo {
protected BMPProviderInfo() {
super(
BMPProviderInfo.class,
new String[] {"bmp", "BMP"},
new String[] {"bmp", "rle"},
new String[] {
"image/bmp",
"image/x-bmp"
// "image/vnd.microsoft.bitmap", // TODO: Official IANA MIME
},
"com.twelvemonkeys.imageio.plugins.bmp.BMPImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.bmp.BMPImageReaderSpi"},
"com.sun.imageio.plugins.bmp.BMPImageWriter",
new String[]{"com.sun.imageio.plugins.bmp.BMPImageWriterSpi"}, // We support the same native metadata format
false, null, null, null, null,
true, BMPMetadata.nativeMetadataFormatName, "com.sun.imageio.plugins.bmp.BMPMetadataFormat", null, null
);
}
}

View File

@ -28,11 +28,9 @@
package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Locale;
@ -43,31 +41,10 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: CURImageReaderSpi.java,v 1.0 25.feb.2006 00:29:44 haku Exp$
*/
public final class CURImageReaderSpi extends ImageReaderSpi {
public final class CURImageReaderSpi extends ImageReaderSpiBase {
public CURImageReaderSpi() {
this(IIOUtil.getProviderInfo(CURImageReaderSpi.class));
}
private CURImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"cur", "CUR"},
new String[]{"cur"},
new String[]{
"image/vnd.microsoft.cursor", // Official IANA MIME
"image/x-cursor", // Common extension MIME
"image/cursor" // Unofficial, but common
},
"com.twelvemonkeys.imageio.plugins.bmp.CURImageReader",
new Class[] {ImageInputStream.class},
null,
true, null, null, null, null,
true,
null, null,
null, null
);
super(new CURProviderInfo());
}
public boolean canDecodeInput(final Object pSource) throws IOException {

View File

@ -0,0 +1,30 @@
package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* CURProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: CURProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class CURProviderInfo extends ReaderWriterProviderInfo {
protected CURProviderInfo() {
super(
CURProviderInfo.class,
new String[]{"cur", "CUR"},
new String[]{"cur"},
new String[]{
"image/vnd.microsoft.cursor", // Official IANA MIME
"image/x-cursor", // Common extension MIME
"image/cursor" // Unofficial, but common
},
"com.twelvemonkeys.imageio.plugins.bmp.CURImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.bmp.CURImageReaderSpi"},
null, null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -28,11 +28,9 @@
package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Locale;
@ -43,31 +41,10 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ICOImageReaderSpi.java,v 1.0 25.feb.2006 00:29:44 haku Exp$
*/
public final class ICOImageReaderSpi extends ImageReaderSpi {
public final class ICOImageReaderSpi extends ImageReaderSpiBase {
public ICOImageReaderSpi() {
this(IIOUtil.getProviderInfo(ICOImageReaderSpi.class));
}
private ICOImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"ico", "ICO"},
new String[]{"ico"},
new String[]{
"image/vnd.microsoft.icon", // Official IANA MIME
"image/x-icon", // Common extension MIME
"image/ico" // Unofficial, but common
},
"com.twelvemonkeys.imageio.plugins.bmp.ICOImageReader",
new Class[] {ImageInputStream.class},
null,
true, null, null, null, null,
true,
null, null,
null, null
);
super(new ICOProviderInfo());
}
public boolean canDecodeInput(final Object pSource) throws IOException {

View File

@ -0,0 +1,30 @@
package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* CURProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: CURProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class ICOProviderInfo extends ReaderWriterProviderInfo {
protected ICOProviderInfo() {
super(
ICOProviderInfo.class,
new String[]{"ico", "ICO"},
new String[]{"ico"},
new String[]{
"image/vnd.microsoft.icon", // Official IANA MIME
"image/x-icon", // Common extension MIME
"image/ico" // Unofficial, but common
},
"com.twelvemonkeys.imageio.plugins.bmp.ICOImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.bmp.ICOImageReaderSpi"},
null, null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -6,6 +6,8 @@ import org.mockito.InOrder;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.event.IIOReadProgressListener;
@ -23,9 +25,7 @@ import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.*;
/**
* BMPImageReaderTest
@ -171,6 +171,21 @@ public class BMPImageReaderTest extends ImageReaderAbstractTestCase<BMPImageRead
}
}
@Test(expected = IIOException.class)
public void testReadCorruptCausesIIOException() throws IOException {
// See https://bugs.openjdk.java.net/browse/JDK-8066904
// NullPointerException when calling ImageIO.read(InputStream) with corrupt BMP
BMPImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/broken-bmp/corrupted-bmp.bmp")));
reader.read(0);
}
finally {
reader.dispose();
}
}
@Test
public void testAddIIOReadProgressListenerCallbacksJPEG() {
ImageReader reader = createReader();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -179,8 +179,10 @@ public abstract class ImageReaderBase extends ImageReader {
if (index < getMinIndex()) {
throw new IndexOutOfBoundsException("index < minIndex");
}
else if (getNumImages(false) != -1 && index >= getNumImages(false)) {
throw new IndexOutOfBoundsException("index >= numImages (" + index + " >= " + getNumImages(false) + ")");
int numImages = getNumImages(false);
if (numImages != -1 && index >= numImages) {
throw new IndexOutOfBoundsException("index >= numImages (" + index + " >= " + numImages + ")");
}
}

View File

@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.spi;
import javax.imageio.spi.ImageReaderSpi;
/**
* ImageReaderSpiBase.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: ImageReaderSpiBase.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
public abstract class ImageReaderSpiBase extends ImageReaderSpi {
protected ImageReaderSpiBase(final ReaderWriterProviderInfo info) {
super(
info.getVendorName(), info.getVersion(),
info.formatNames(), info.suffixes(), info.mimeTypes(),
info.readerClassName(), info.inputTypes(),
info.writerSpiClassNames(),
info.supportsStandardStreamMetadataFormat(),
info.nativeStreamMetadataFormatName(), info.nativeStreamMetadataFormatClassName(),
info.extraStreamMetadataFormatNames(), info.extraStreamMetadataFormatClassNames(),
info.supportsStandardImageMetadataFormat(),
info.nativeImageMetadataFormatName(), info.nativeImageMetadataFormatClassName(),
info.extraImageMetadataFormatNames(), info.extraImageMetadataFormatClassNames()
);
}
}

View File

@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.spi;
import javax.imageio.spi.ImageWriterSpi;
/**
* ImageWriterSpiBase.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: ImageWriterSpiBase.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
public abstract class ImageWriterSpiBase extends ImageWriterSpi {
protected ImageWriterSpiBase(final ReaderWriterProviderInfo info) {
super(
info.getVendorName(), info.getVersion(),
info.formatNames(), info.suffixes(), info.mimeTypes(),
info.writerClassName(), info.outputTypes(),
info.readerSpiClassNames(),
info.supportsStandardStreamMetadataFormat(),
info.nativeStreamMetadataFormatName(), info.nativeStreamMetadataFormatClassName(),
info.extraStreamMetadataFormatNames(), info.extraStreamMetadataFormatClassNames(),
info.supportsStandardImageMetadataFormat(),
info.nativeImageMetadataFormatName(), info.nativeImageMetadataFormatClassName(),
info.extraImageMetadataFormatNames(), info.extraImageMetadataFormatClassNames()
);
}
}

View File

@ -0,0 +1,157 @@
package com.twelvemonkeys.imageio.spi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* ReaderWriterProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: ReaderWriterProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
public abstract class ReaderWriterProviderInfo extends ProviderInfo {
private final String[] formatNames;
private final String[] suffixes;
private final String[] mimeTypes;
private final String readerClassName;
private final String[] readerSpiClassNames;
private final Class[] inputTypes = new Class[] {ImageInputStream.class};
private final String writerClassName;
private final String[] writerSpiClassNames;
private final Class[] outputTypes = new Class[] {ImageOutputStream.class};
private final boolean supportsStandardStreamMetadata;
private final String nativeStreameMetadataFormatName;
private final String nativeStreamMetadataFormatClassName;
private final String[] extraStreamMetadataFormatNames;
private final String[] extraStreamMetadataFormatClassNames;
private final boolean supportsStandardImageMetadata;
private final String nativeImageMetadataFormatName;
private final String nativeImageMetadataFormatClassName;
private final String[] extraImageMetadataFormatNames;
private final String[] extraImageMetadataFormatClassNames;
/**
* Creates a provider information instance based on the given class.
*
* @param infoClass the class to get provider information from.
* The provider info will be taken from the class' package. @throws IllegalArgumentException if {@code pPackage == null}
*/
protected ReaderWriterProviderInfo(final Class<? extends ReaderWriterProviderInfo> infoClass,
final String[] formatNames,
final String[] suffixes,
final String[] mimeTypes,
final String readerClassName,
final String[] readerSpiClassNames,
final String writerClassName,
final String[] writerSpiClassNames,
final boolean supportsStandardStreamMetadata,
final String nativeStreameMetadataFormatName,
final String nativeStreamMetadataFormatClassName,
final String[] extraStreamMetadataFormatNames,
final String[] extraStreamMetadataFormatClassNames,
final boolean supportsStandardImageMetadata,
final String nativeImageMetadataFormatName,
final String nativeImageMetadataFormatClassName,
final String[] extraImageMetadataFormatNames,
final String[] extraImageMetadataFormatClassNames) {
super(notNull(infoClass).getPackage());
this.formatNames = formatNames;
this.suffixes = suffixes;
this.mimeTypes = mimeTypes;
this.readerClassName = readerClassName;
this.readerSpiClassNames = readerSpiClassNames;
this.writerClassName = writerClassName;
this.writerSpiClassNames = writerSpiClassNames;
this.supportsStandardStreamMetadata = supportsStandardStreamMetadata;
this.nativeStreameMetadataFormatName = nativeStreameMetadataFormatName;
this.nativeStreamMetadataFormatClassName = nativeStreamMetadataFormatClassName;
this.extraStreamMetadataFormatNames = extraStreamMetadataFormatNames;
this.extraStreamMetadataFormatClassNames = extraStreamMetadataFormatClassNames;
this.supportsStandardImageMetadata = supportsStandardImageMetadata;
this.nativeImageMetadataFormatName = nativeImageMetadataFormatName;
this.nativeImageMetadataFormatClassName = nativeImageMetadataFormatClassName;
this.extraImageMetadataFormatNames = extraImageMetadataFormatNames;
this.extraImageMetadataFormatClassNames = extraImageMetadataFormatClassNames;
}
public String[] formatNames() {
return formatNames;
}
public String[] suffixes() {
return suffixes;
}
public String[] mimeTypes() {
return mimeTypes;
}
public String readerClassName() {
return readerClassName;
}
public String[] readerSpiClassNames() {
return readerSpiClassNames;
}
public Class[] inputTypes() {
return inputTypes;
}
public String writerClassName() {
return writerClassName;
}
public String[] writerSpiClassNames() {
return writerSpiClassNames;
}
public Class[] outputTypes() {
return outputTypes;
}
public boolean supportsStandardStreamMetadataFormat() {
return supportsStandardStreamMetadata;
}
public String nativeStreamMetadataFormatName() {
return nativeStreameMetadataFormatName;
}
public String nativeStreamMetadataFormatClassName() {
return nativeStreamMetadataFormatClassName;
}
public String[] extraStreamMetadataFormatNames() {
return extraStreamMetadataFormatNames;
}
public String[] extraStreamMetadataFormatClassNames() {
return extraStreamMetadataFormatClassNames;
}
public boolean supportsStandardImageMetadataFormat() {
return supportsStandardImageMetadata;
}
public String nativeImageMetadataFormatName() {
return nativeImageMetadataFormatName;
}
public String nativeImageMetadataFormatClassName() {
return nativeImageMetadataFormatClassName;
}
public String[] extraImageMetadataFormatNames() {
return extraImageMetadataFormatNames;
}
public String[] extraImageMetadataFormatClassNames() {
return extraImageMetadataFormatClassNames;
}
}

View File

@ -0,0 +1,55 @@
package com.twelvemonkeys.imageio.stream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageOutputStreamImpl;
import java.io.IOException;
/**
* SubImageOutputStream.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: SubImageOutputStream.java,v 1.0 30/03/15 harald.kuhr Exp$
*/
public class SubImageOutputStream extends ImageOutputStreamImpl {
private final ImageOutputStream stream;
public SubImageOutputStream(final ImageOutputStream stream) {
this.stream = stream;
}
@Override
public void write(int b) throws IOException {
stream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
stream.write(b, off, len);
}
@Override
public int read() throws IOException {
return stream.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return stream.read(b, off, len);
}
@Override
public boolean isCached() {
return stream.isCached();
}
@Override
public boolean isCachedMemory() {
return stream.isCachedMemory();
}
@Override
public boolean isCachedFile() {
return stream.isCachedFile();
}
}

View File

@ -70,6 +70,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
ImageIO.setUseCache(false);
}
protected abstract List<TestData> getTestData();

View File

@ -28,11 +28,9 @@
package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Locale;
@ -44,28 +42,9 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: ICNSImageReaderSpi.java,v 1.0 25.10.11 18:41 haraldk Exp$
*/
public final class ICNSImageReaderSpi extends ImageReaderSpi{
public final class ICNSImageReaderSpi extends ImageReaderSpiBase {
public ICNSImageReaderSpi() {
this(IIOUtil.getProviderInfo(ICNSImageReaderSpi.class));
}
private ICNSImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"icns", "ICNS"},
new String[]{"icns"},
new String[]{
"image/x-apple-icons", // Common extension MIME
},
"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader",
new Class[] {ImageInputStream.class},
null,
true, null, null, null, null,
true,
null, null,
null, null
);
super(new ICNSProviderInfo());
}
@Override

View File

@ -0,0 +1,28 @@
package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* ICNSProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: ICNSProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class ICNSProviderInfo extends ReaderWriterProviderInfo {
protected ICNSProviderInfo() {
super(
ICNSProviderInfo.class,
new String[]{"icns", "ICNS"},
new String[]{"icns"},
new String[]{
"image/x-apple-icons", // Common extension MIME
},
"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.ics.ICNImageReaderSpi"},
null, null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -113,7 +113,7 @@ public class IFFImageReader extends ImageReaderBase {
private DataInputStream byteRunStream;
public IFFImageReader() {
super(IFFImageReaderSpi.sharedProvider());
super(new IFFImageReaderSpi());
}
protected IFFImageReader(ImageReaderSpi pProvider) {

View File

@ -28,11 +28,9 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Locale;
@ -44,34 +42,13 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: IFFImageWriterSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$
*/
public class IFFImageReaderSpi extends ImageReaderSpi {
static IFFImageReaderSpi mSharedInstance;
public class IFFImageReaderSpi extends ImageReaderSpiBase {
/**
* Creates an {@code IFFImageReaderSpi}.
*/
public IFFImageReaderSpi() {
this(IIOUtil.getProviderInfo(IFFImageReaderSpi.class));
}
private IFFImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"iff", "IFF"},
new String[]{"iff", "lbm", "ham", "ham8", "ilbm"},
new String[]{"image/iff", "image/x-iff"},
"com.twelvemonkeys.imageio.plugins.iff.IFFImageReader",
new Class[] {ImageInputStream.class},
new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageWriterSpi"},
true, null, null, null, null,
true, null, null, null, null
);
if (mSharedInstance == null) {
mSharedInstance = this;
}
super(new IFFProviderInfo());
}
public boolean canDecodeInput(Object pSource) throws IOException {
@ -102,7 +79,6 @@ public class IFFImageReaderSpi extends ImageReaderSpi {
return false;
}
public ImageReader createReaderInstance(Object pExtension) throws IOException {
return new IFFImageReader(this);
}
@ -110,12 +86,4 @@ public class IFFImageReaderSpi extends ImageReaderSpi {
public String getDescription(Locale pLocale) {
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image reader";
}
public static ImageReaderSpi sharedProvider() {
if (mSharedInstance == null) {
new IFFImageReaderSpi();
}
return mSharedInstance;
}
}

View File

@ -28,12 +28,10 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.ImageWriterSpi;
import java.io.IOException;
import java.util.Locale;
@ -44,28 +42,13 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: IFFImageWriterSpi.java,v 1.0 02.mar.2006 19:21:05 haku Exp$
*/
public class IFFImageWriterSpi extends ImageWriterSpi {
public class IFFImageWriterSpi extends ImageWriterSpiBase {
/**
* Creates an {@code IFFImageWriterSpi}.
*/
public IFFImageWriterSpi() {
this(IIOUtil.getProviderInfo(IFFImageWriterSpi.class));
}
private IFFImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"iff", "IFF"},
new String[]{"iff", "lbm", "ham", "ham8", "ilbm"},
new String[]{"image/iff", "image/x-iff"},
"com.twelvemonkeys.imageio.plugins.iff.IFFImageWriter",
STANDARD_OUTPUT_TYPE,
new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageReaderSpi"},
true, null, null, null, null,
true, null, null, null, null
);
super(new IFFProviderInfo());
}
public boolean canEncodeImage(final ImageTypeSpecifier pType) {

View File

@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* IFFProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: IFFProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class IFFProviderInfo extends ReaderWriterProviderInfo {
protected IFFProviderInfo() {
super(
IFFProviderInfo.class,
new String[] {"iff", "IFF"},
new String[] {"iff", "lbm", "ham", "ham8", "ilbm"},
new String[] {"image/iff", "image/x-iff"},
"com.twelvemonkeys.imageio.plugins.iff.IFFImageReader",
new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageReaderSpi"},
"com.twelvemonkeys.imageio.plugins.iff.IFFImageWriter",
new String[] {"com.twelvemonkeys.imageio.plugins.iff.IFFImageWriterSpi"},
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -156,10 +156,12 @@ final class JPEGImage10MetadataCleaner {
}
// Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF
if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) {
if (adobeDCT != null && (adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4 ||
adobeDCT.getTransform() == AdobeDCTSegment.YCC && sof.componentsInFrame() < 3)) {
reader.processWarningOccurred(String.format(
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
"Invalid Adobe App14 marker. Indicates %s data, but SOF%d has %d color component(s). " +
"Ignoring Adobe App14 marker.",
adobeDCT.getTransform() == AdobeDCTSegment.YCCK ? "YCCK/CMYK" : "YCC/RGB",
sof.marker & 0xf, sof.componentsInFrame()
));
@ -231,6 +233,26 @@ final class JPEGImage10MetadataCleaner {
markerSequence.insertBefore(unknown, next);
}
// Known issues in the com.sun classes, if sof/sos component id or selector is negative,
// setFromTree will fail. We'll fix the range from -128...127 to be 0...255.
NodeList sofs = markerSequence.getElementsByTagName("sof");
if (sofs.getLength() > 0) {
NodeList components = sofs.item(0).getChildNodes();
for (int i = 0; i < components.getLength(); i++) {
forceComponentIdInRange((IIOMetadataNode) components.item(i), "componentId");
}
}
NodeList sos = markerSequence.getElementsByTagName("sos");
for (int i = 0; i < sos.getLength(); i++) {
NodeList specs = sos.item(i).getChildNodes();
for (int j = 0; j < specs.getLength(); j++) {
forceComponentIdInRange((IIOMetadataNode) specs.item(j), "componentSelector");
}
}
// Inconsistency issue in the com.sun classes, it can read metadata with dht containing
// more than 4 children, but will not allow setting such a tree...
// We'll split AC/DC tables into separate dht nodes.
@ -239,7 +261,12 @@ final class JPEGImage10MetadataCleaner {
Node dht = dhts.item(j);
NodeList dhtables = dht.getChildNodes();
if (dhtables.getLength() > 4) {
if (dhtables.getLength() < 1) {
// Why is there an empty DHT node?
dht.getParentNode().removeChild(dht);
reader.processWarningOccurred("Metadata contains empty dht node. Ignoring.");
}
else if (dhtables.getLength() > 4) {
IIOMetadataNode acTables = new IIOMetadataNode("dht");
dht.getParentNode().insertBefore(acTables, dht.getNextSibling());
@ -260,6 +287,8 @@ final class JPEGImage10MetadataCleaner {
}
catch (IIOInvalidTreeException e) {
if (JPEGImageReader.DEBUG) {
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0), false);
System.out.println("-- 8< --");
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false);
}
@ -268,4 +297,29 @@ final class JPEGImage10MetadataCleaner {
return imageMetadata;
}
private void forceComponentIdInRange(final IIOMetadataNode component, final String attributeName) {
String attribute = component.getAttribute(attributeName);
if (attribute != null) {
try {
int componentId = Integer.parseInt(attribute);
if (componentId < 0) {
// Metadata doesn't like negative component ids/specs
// We'll convert to the positive value it probably should have been
componentId = ((byte) componentId) & 0xff;
component.setAttribute(attributeName, String.valueOf(componentId));
}
}
catch (NumberFormatException ignore) {
if ("scanComponentSpec".equals(component.getNodeName())) {
reader.processWarningOccurred("Bad SOS component selector: " + attribute);
}
else {
reader.processWarningOccurred("Bad SOF component id: " + attribute);
}
}
}
}
}

View File

@ -49,6 +49,7 @@ import javax.imageio.event.IIOReadUpdateListener;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
@ -100,6 +101,7 @@ import java.util.List;
public class JPEGImageReader extends ImageReaderBase {
// TODO: Allow automatic rotation based on EXIF rotation field?
// TODO: Create a simplified native metadata format that is closer to the actual JPEG stream AND supports EXIF in a sensible way
// TODO: As we already parse the SOF segments, maybe we should stop delegating getWidth/getHeight etc?
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
@ -197,7 +199,13 @@ public class JPEGImageReader extends ImageReaderBase {
@Override
public int getNumImages(boolean allowSearch) throws IOException {
return delegate.getNumImages(allowSearch);
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;
}
}
@Override
@ -316,9 +324,22 @@ public class JPEGImageReader extends ImageReaderBase {
// }
// }
SOFSegment sof = getSOF();
ICC_Profile profile = getEmbeddedICCProfile(false);
AdobeDCTSegment adobeDCT = getAdobeDCT();
SOFSegment sof = getSOF();
if (adobeDCT != null && (adobeDCT.getTransform() == AdobeDCTSegment.YCC && sof.componentsInFrame() != 3 ||
adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() != 4)) {
processWarningOccurred(String.format(
"Invalid Adobe App14 marker. Indicates %s data, but SOF%d has %d color component(s). " +
"Ignoring Adobe App14 marker.",
adobeDCT.getTransform() == AdobeDCTSegment.YCCK ? "YCCK/CMYK" : "YCC/RGB",
sof.marker & 0xf, sof.componentsInFrame()
));
adobeDCT = null;
}
JPEGColorSpace sourceCSType = getSourceCSType(getJFIF(), adobeDCT, sof);
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
@ -334,8 +355,8 @@ public class JPEGImageReader extends ImageReaderBase {
System.out.println("ICC color profile: " + profile);
}
// TODO: Possible to optimize slightly, to avoid readAsRaster for non-CMyK and other good types?
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, adobeDCT, ensureDisplayProfile(profile));
// TODO: Possible to optimize slightly, to avoid readAsRaster for non-CMYK and other good types?
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, ensureDisplayProfile(profile));
}
if (DEBUG) {
@ -345,7 +366,7 @@ public class JPEGImageReader extends ImageReaderBase {
return delegate.read(imageIndex, param);
}
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, SOFSegment startOfFrame, JPEGColorSpace csType, AdobeDCTSegment adobeDCT, ICC_Profile profile) throws IOException {
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, SOFSegment startOfFrame, JPEGColorSpace csType, ICC_Profile profile) throws IOException {
int origWidth = getWidth(imageIndex);
int origHeight = getHeight(imageIndex);
@ -366,27 +387,16 @@ public class JPEGImageReader extends ImageReaderBase {
else if (intendedCS != null) {
// Handle inconsistencies
if (startOfFrame.componentsInFrame() != intendedCS.getNumComponents()) {
if (startOfFrame.componentsInFrame() < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
processWarningOccurred(String.format(
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
"Ignoring Adobe App14 marker, assuming YCbCr/RGB data.",
startOfFrame.marker & 0xf, startOfFrame.componentsInFrame()
));
// If ICC profile number of components and startOfFrame does not match, ignore ICC profile
processWarningOccurred(String.format(
"Embedded ICC color profile is incompatible with image data. " +
"Profile indicates %d components, but SOF%d has %d color components. " +
"Ignoring ICC profile, assuming source color space %s.",
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType
));
csType = JPEGColorSpace.YCbCr;
}
else {
// If ICC profile number of components and startOfFrame does not match, ignore ICC profile
processWarningOccurred(String.format(
"Embedded ICC color profile is incompatible with image data. " +
"Profile indicates %d components, but SOF%d has %d color components. " +
"Ignoring ICC profile, assuming source color space %s.",
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType
));
if (csType == JPEGColorSpace.CMYK && image.getColorModel().getColorSpace().getType() != ColorSpace.TYPE_CMYK) {
convert = new ColorConvertOp(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), image.getColorModel().getColorSpace(), null);
}
if (csType == JPEGColorSpace.CMYK && image.getColorModel().getColorSpace().getType() != ColorSpace.TYPE_CMYK) {
convert = new ColorConvertOp(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), image.getColorModel().getColorSpace(), null);
}
}
// NOTE: Avoid using CCOp if same color space, as it's more compatible that way
@ -437,75 +447,46 @@ public class JPEGImageReader extends ImageReaderBase {
Rectangle dstRegion = new Rectangle();
computeRegions(param, origWidth, origHeight, image, srcRegion, dstRegion);
// We're ready to go
processImageStarted(imageIndex);
// Need to undo the subsampling offset translations, as they are applied again in delegate.readRaster
int gridX = param.getSubsamplingXOffset();
int gridY = param.getSubsamplingYOffset();
srcRegion.translate(-gridX, -gridY);
srcRegion.width += gridX;
srcRegion.height += gridY;
// Unfortunately looping is slower than reading all at once, but
// that requires 2 x memory or more, so a few steps is an ok compromise I guess
// Unfortunately, reading the image in steps, is increasingly slower
// for each iteration, so we'll read all at once.
try {
final int step = Math.max(1024, srcRegion.height / 10); // TODO: Using a multiple of 8 is probably a good idea for JPEG
final int srcMaxY = srcRegion.y + srcRegion.height;
int destY = dstRegion.y;
param.setSourceRegion(srcRegion);
Raster raster = delegate.readRaster(imageIndex, param); // non-converted
for (int y = srcRegion.y; y < srcMaxY; y += step) {
int scan = Math.min(step, srcMaxY - y);
// Apply source color conversion from implicit color space
if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.YCbCrA) {
YCbCrConverter.convertYCbCr2RGB(raster);
}
else if (csType == JPEGColorSpace.YCCK) {
YCbCrConverter.convertYCCK2CMYK(raster);
}
else if (csType == JPEGColorSpace.CMYK) {
invertCMYK(raster);
}
// ...else assume the raster is already converted
// Let the progress delegator handle progress, using corrected range
progressDelegator.updateProgressRange(100f * (y + scan) / srcRegion.height);
WritableRaster dest = destination.createWritableChild(dstRegion.x, dstRegion.y, raster.getWidth(), raster.getHeight(), 0, 0, param.getDestinationBands());
// Make sure subsampling is within bounds
if (scan <= param.getSubsamplingYOffset()) {
param.setSourceSubsampling(param.getSourceXSubsampling(), param.getSourceYSubsampling(), param.getSubsamplingXOffset(), scan - 1);
}
Rectangle subRegion = new Rectangle(srcRegion.x, y, srcRegion.width, scan);
param.setSourceRegion(subRegion);
Raster raster = delegate.readRaster(imageIndex, param); // non-converted
// Apply source color conversion from implicit color space
if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.YCbCrA) {
YCbCrConverter.convertYCbCr2RGB(raster);
}
else if (csType == JPEGColorSpace.YCCK) {
YCbCrConverter.convertYCCK2CMYK(raster);
}
else if (csType == JPEGColorSpace.CMYK) {
invertCMYK(raster);
}
// ...else assume the raster is already converted
int destHeight = Math.min(raster.getHeight(), dstRegion.height - destY); // Avoid off-by-one
Raster src = raster.createChild(0, 0, raster.getWidth(), destHeight, 0, 0, param.getSourceBands());
WritableRaster dest = destination.createWritableChild(dstRegion.x, destY, raster.getWidth(), destHeight, 0, 0, param.getDestinationBands());
// Apply further color conversion for explicit color space, or just copy the pixels into place
if (convert != null) {
convert.filter(src, dest);
// WritableRaster filtered = convert.filter(src, null);
// new AffineTransformOp(AffineTransform.getRotateInstance(2 * Math.PI, filtered.getWidth() / 2.0, filtered.getHeight() / 2.0), null).filter(filtered, dest);
}
else {
dest.setRect(0, 0, src);
}
destY += raster.getHeight();
if (abortRequested()) {
processReadAborted();
break;
}
// Apply further color conversion for explicit color space, or just copy the pixels into place
if (convert != null) {
convert.filter(raster, dest);
}
else {
dest.setRect(0, 0, raster);
}
}
finally {
// Restore normal read progress processing
progressDelegator.resetProgressRange();
// NOTE: Would be cleaner to clone the param, unfortunately it can't be done easily...
param.setSourceRegion(origSourceRegion);
}
processImageComplete();
return image;
}
@ -553,10 +534,16 @@ public class JPEGImageReader extends ImageReaderBase {
if (adobeDCT != null) {
switch (adobeDCT.getTransform()) {
case AdobeDCTSegment.YCC:
// TODO: Verify that startOfFrame has 3 components, otherwise issue warning and ignore adobeDCT
if (startOfFrame.components.length != 3) {
// This probably means the Adobe marker is bogus
break;
}
return JPEGColorSpace.YCbCr;
case AdobeDCTSegment.YCCK:
// TODO: Verify that startOfFrame has 4 components, otherwise issue warning and ignore adobeDCT
if (startOfFrame.components.length != 4) {
// This probably means the Adobe marker is bogus
break;
}
return JPEGColorSpace.YCCK;
case AdobeDCTSegment.Unknown:
if (startOfFrame.components.length == 1) {
@ -573,6 +560,7 @@ public class JPEGImageReader extends ImageReaderBase {
}
}
// TODO: We should probably allow component ids out of order (ie. BGR or KMCY)...
switch (startOfFrame.components.length) {
case 1:
return JPEGColorSpace.Gray;
@ -580,6 +568,7 @@ public class JPEGImageReader extends ImageReaderBase {
return JPEGColorSpace.GrayA;
case 3:
if (startOfFrame.components[0].id == 1 && startOfFrame.components[1].id == 2 && startOfFrame.components[2].id == 3) {
// NOTE: Due to a bug in JPEGMetadata, standard format will report RGB for non-subsampled, non-JFIF files
return JPEGColorSpace.YCbCr;
}
else if (startOfFrame.components[0].id == 'R' && startOfFrame.components[1].id == 'G' && startOfFrame.components[2].id == 'B') {
@ -600,6 +589,7 @@ public class JPEGImageReader extends ImageReaderBase {
}
case 4:
if (startOfFrame.components[0].id == 1 && startOfFrame.components[1].id == 2 && startOfFrame.components[2].id == 3 && startOfFrame.components[3].id == 4) {
// NOTE: Due to a bug in JPEGMetadata, standard format will report RGBA for non-subsampled, non-JFIF files
return JPEGColorSpace.YCbCrA;
}
else if (startOfFrame.components[0].id == 'R' && startOfFrame.components[1].id == 'G' && startOfFrame.components[2].id == 'B' && startOfFrame.components[3].id == 'A') {
@ -720,6 +710,8 @@ public class JPEGImageReader extends ImageReaderBase {
}
SOFSegment getSOF() throws IOException {
initHeader();
for (JPEGSegment segment : segments) {
if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 ||
JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 ||
@ -752,7 +744,7 @@ public class JPEGImageReader extends ImageReaderBase {
}
}
return null;
throw new IIOException("No SOF segment in stream");
}
AdobeDCTSegment getAdobeDCT() throws IOException {
@ -779,7 +771,13 @@ public class JPEGImageReader extends ImageReaderBase {
if (!jfif.isEmpty()) {
JPEGSegment segment = jfif.get(0);
return JFIFSegment.read(segment.data());
if (segment.length() >= 9) {
return JFIFSegment.read(segment.data());
}
else {
processWarningOccurred("Bogus JFIF segment, ignoring");
}
}
return null;
@ -790,7 +788,12 @@ public class JPEGImageReader extends ImageReaderBase {
if (!jfxx.isEmpty()) {
JPEGSegment segment = jfxx.get(0);
return JFXXSegment.read(segment.data(), segment.length());
if (segment.length() >= 1) {
return JFXXSegment.read(segment.data(), segment.length());
}
else {
processWarningOccurred("Bogus JFXX segment, ignoring");
}
}
return null;
@ -1052,7 +1055,22 @@ public class JPEGImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
IIOMetadata imageMetadata = delegate.getImageMetadata(imageIndex);
// checkBounds needed, as we catch the IndexOutOfBoundsException below.
checkBounds(imageIndex);
IIOMetadata imageMetadata;
try {
imageMetadata = delegate.getImageMetadata(imageIndex);
}
catch (IndexOutOfBoundsException knownIssue) {
// TMI-101: com.sun.imageio.plugins.jpeg.JPEGBuffer doesn't do proper sanity check of input data.
throw new IIOException("Corrupt JPEG data: Bad segment length", knownIssue);
}
catch (NegativeArraySizeException knownIssue) {
// Most likely from com.sun.imageio.plugins.jpeg.SOSMarkerSegment
throw new IIOException("Corrupt JPEG data: Bad component count", knownIssue);
}
if (imageMetadata != null && Arrays.asList(imageMetadata.getMetadataFormatNames()).contains(JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0)) {
if (metadataCleaner == null) {
@ -1085,6 +1103,9 @@ public class JPEGImageReader extends ImageReaderBase {
/**
* Static inner class for lazy-loading of conversion tables.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author Original code by Werner Randelshofer
*/
static final class YCbCrConverter {
/** Define tables for YCC->RGB color space conversion. */
@ -1182,54 +1203,25 @@ public class JPEGImageReader extends ImageReaderBase {
}
private class ProgressDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener {
float readProgressStart = -1;
float readProgressStop = -1;
void resetProgressRange() {
readProgressStart = -1;
readProgressStop = -1;
}
private boolean isProgressRangeCorrected() {
return readProgressStart == -1 && readProgressStop == -1;
}
void updateProgressRange(float limit) {
Validate.isTrue(limit >= 0, limit, "Negative range limit");
readProgressStart = readProgressStop != -1 ? readProgressStop : 0;
readProgressStop = limit;
}
@Override
public void imageComplete(ImageReader source) {
if (isProgressRangeCorrected()) {
processImageComplete();
}
}
@Override
public void imageProgress(ImageReader source, float percentageDone) {
if (isProgressRangeCorrected()) {
processImageProgress(percentageDone);
}
else {
processImageProgress(readProgressStart + (percentageDone * (readProgressStop - readProgressStart) / 100f));
}
}
@Override
public void imageStarted(ImageReader source, int imageIndex) {
if (isProgressRangeCorrected()) {
processImageStarted(imageIndex);
}
}
@Override
public void readAborted(ImageReader source) {
if (isProgressRangeCorrected()) {
processReadAborted();
}
}
@Override
@ -1362,6 +1354,14 @@ public class JPEGImageReader extends ImageReaderBase {
reader.setInput(input);
// For a tables-only image, we can't read image, but we should get metadata.
if (reader.getNumImages(true) == 0) {
IIOMetadata streamMetadata = reader.getStreamMetadata();
IIOMetadataNode streamNativeTree = (IIOMetadataNode) streamMetadata.getAsTree(streamMetadata.getNativeMetadataFormatName());
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(streamNativeTree, false);
continue;
}
try {
ImageReadParam param = reader.getDefaultReadParam();
// if (args.length > 1) {
@ -1369,9 +1369,20 @@ public class JPEGImageReader extends ImageReaderBase {
// int sub = 4;
// param.setSourceSubsampling(sub, sub, 0, 0);
// }
BufferedImage image = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0));
param.setDestination(image);
// long start = System.currentTimeMillis();
BufferedImage image = reader.read(0, param);
try {
image = reader.read(0, param);
}
catch (IOException e) {
e.printStackTrace();
if (image == null) {
continue;
}
}
// System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
// System.err.println("image: " + image);
@ -1380,8 +1391,6 @@ public class JPEGImageReader extends ImageReaderBase {
int maxW = 1280;
int maxH = 800;
// int maxW = 400;
// int maxH = 400;
if (image.getWidth() > maxW || image.getHeight() > maxH) {
// start = System.currentTimeMillis();
float aspect = reader.getAspectRatio(0);

View File

@ -28,7 +28,7 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.Validate;
@ -36,7 +36,6 @@ import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Locale;
@ -48,7 +47,7 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: JPEGImageReaderSpi.java,v 1.0 24.01.11 22.12 haraldk Exp$
*/
public class JPEGImageReaderSpi extends ImageReaderSpi {
public class JPEGImageReaderSpi extends ImageReaderSpiBase {
private ImageReaderSpi delegateProvider;
/**
@ -56,22 +55,7 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
* The instance created will not work without being properly registered.
*/
public JPEGImageReaderSpi() {
this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class));
}
private JPEGImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{"JPEG", "jpeg", "JPG", "jpg"},
new String[]{"jpg", "jpeg"},
new String[]{"image/jpeg"},
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReader",
new Class[] {ImageInputStream.class},
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriterSpi"},
true, null, null, null, null,
true, null, null, null, null
);
super(new JPEGProviderInfo());
}
/**
@ -80,7 +64,7 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
* @param delegateProvider a {@code ImageReaderSpi} that can read JPEG.
*/
protected JPEGImageReaderSpi(final ImageReaderSpi delegateProvider) {
this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class));
this();
this.delegateProvider = Validate.notNull(delegateProvider);
}
@ -128,12 +112,12 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
}
@Override
public ImageReader createReaderInstance(Object extension) throws IOException {
public ImageReader createReaderInstance(final Object extension) throws IOException {
return new JPEGImageReader(this, delegateProvider.createReaderInstance(extension));
}
@Override
public boolean canDecodeInput(Object source) throws IOException {
public boolean canDecodeInput(final Object source) throws IOException {
return delegateProvider.canDecodeInput(source);
}
@ -183,17 +167,17 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
}
@Override
public IIOMetadataFormat getStreamMetadataFormat(String formatName) {
public IIOMetadataFormat getStreamMetadataFormat(final String formatName) {
return delegateProvider.getStreamMetadataFormat(formatName);
}
@Override
public IIOMetadataFormat getImageMetadataFormat(String formatName) {
public IIOMetadataFormat getImageMetadataFormat(final String formatName) {
return delegateProvider.getImageMetadataFormat(formatName);
}
@Override
public String getDescription(Locale locale) {
public String getDescription(final Locale locale) {
return delegateProvider.getDescription(locale);
}

View File

@ -28,7 +28,7 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.Validate;
@ -37,7 +37,6 @@ import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.Iterator;
@ -50,7 +49,7 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: JPEGImageWriterSpi.java,v 1.0 06.02.12 16:09 haraldk Exp$
*/
public class JPEGImageWriterSpi extends ImageWriterSpi {
public class JPEGImageWriterSpi extends ImageWriterSpiBase {
private ImageWriterSpi delegateProvider;
/**
@ -58,22 +57,7 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
* The instance created will not work without being properly registered.
*/
public JPEGImageWriterSpi() {
this(IIOUtil.getProviderInfo(JPEGImageWriterSpi.class));
}
private JPEGImageWriterSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{"JPEG", "jpeg", "JPG", "jpg"},
new String[]{"jpg", "jpeg"},
new String[]{"image/jpeg"},
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter",
new Class[] { ImageOutputStream.class },
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"},
true, null, null, null, null,
true, null, null, null, null
);
super(new JPEGProviderInfo());
}
/**
@ -82,7 +66,7 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
* @param delegateProvider a {@code ImageWriterSpi} that can write JPEG.
*/
protected JPEGImageWriterSpi(final ImageWriterSpi delegateProvider) {
this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class));
this();
this.delegateProvider = Validate.notNull(delegateProvider);
}
@ -110,7 +94,7 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
}
if (delegateProvider != null) {
// Order before com.sun provider, to aid ImageIO in selecting our reader
// Order before com.sun provider, to aid ImageIO in selecting our writer
registry.setOrdering((Class<ImageWriterSpi>) category, this, delegateProvider);
}
else {
@ -130,7 +114,7 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
}
@Override
public ImageWriter createWriterInstance(Object extension) throws IOException {
public ImageWriter createWriterInstance(final Object extension) throws IOException {
return new JPEGImageWriter(this, delegateProvider.createWriterInstance(extension));
}
@ -180,27 +164,27 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
}
@Override
public IIOMetadataFormat getStreamMetadataFormat(String formatName) {
public IIOMetadataFormat getStreamMetadataFormat(final String formatName) {
return delegateProvider.getStreamMetadataFormat(formatName);
}
@Override
public IIOMetadataFormat getImageMetadataFormat(String formatName) {
public IIOMetadataFormat getImageMetadataFormat(final String formatName) {
return delegateProvider.getImageMetadataFormat(formatName);
}
@Override
public boolean canEncodeImage(ImageTypeSpecifier type) {
public boolean canEncodeImage(final ImageTypeSpecifier type) {
return delegateProvider.canEncodeImage(type);
}
@Override
public boolean canEncodeImage(RenderedImage im) {
public boolean canEncodeImage(final RenderedImage im) {
return delegateProvider.canEncodeImage(im);
}
@Override
public String getDescription(Locale locale) {
public String getDescription(final Locale locale) {
return delegateProvider.getDescription(locale);
}

View File

@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* JPEGProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: JPEGProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class JPEGProviderInfo extends ReaderWriterProviderInfo {
protected JPEGProviderInfo() {
super(
JPEGProviderInfo.class,
new String[] {"JPEG", "jpeg", "JPG", "jpg"},
new String[] {"jpg", "jpeg"},
new String[] {"image/jpeg"},
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"},
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter",
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriterSpi"},
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -33,15 +33,17 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageInputStreamImpl;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* JPEGSegmentImageInputStream.
* ImageInputStream implementation that filters out certain JPEG segments.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
@ -74,11 +76,13 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
if (streamPos >= segment.end()) {
// Go forward in cache
while (++currentSegment < segments.size()) {
int cachedSegment = currentSegment;
while (++cachedSegment < segments.size()) {
currentSegment = cachedSegment;
segment = segments.get(currentSegment);
if (streamPos >= segment.start && streamPos < segment.end()) {
stream.seek(segment.realStart + streamPos - segment.start);
segment.seek(stream, streamPos);
return segment;
}
@ -114,9 +118,13 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
marker = 0xff00 | stream.readUnsignedByte();
}
// TODO: Optionally skip JFIF only for non-JFIF conformant streams
// TODO: Refactor to make various segments optional, we probably only want the "Adobe" APP14 segment, 'Exif' APP1 and very few others
if (isAppSegmentMarker(marker) && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) {
// We are now handling all important segments ourselves, except APP1/Exif and APP14/Adobe,
// as these segments affects image decoding.
boolean appSegmentMarker = isAppSegmentMarker(marker);
boolean isApp14Adobe = marker == JPEG.APP14 && isAppSegmentWithId("Adobe", stream);
boolean isApp1Exif = marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream);
if (appSegmentMarker && !(isApp1Exif || isApp14Adobe)) {
int length = stream.readUnsignedShort(); // Length including length field itself
stream.seek(realPosition + 2 + length); // Skip marker (2) + length
}
@ -130,21 +138,42 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
if (marker == JPEG.SOS) {
// Treat rest of stream as a single segment (scanning for EOI is too much work)
// TODO: For progressive, there will be more than one SOS...
length = Long.MAX_VALUE - realPosition;
}
else {
// Length including length field itself
length = stream.readUnsignedShort() + 2;
length = 2 + stream.readUnsignedShort();
}
if (isApp14Adobe && length != 16) {
// Need to rewrite this segment, so that it gets length 16 and discard the remaining bytes...
segment = new AdobeAPP14Replacement(realPosition, segment.end(), length, stream);
}
else if (marker == JPEG.DQT) {
// TODO: Do we need to know SOF precision before determining if the DQT precision is bad?
// Inspect segment, see if we have 16 bit precision (assuming segments will not contain
// multiple quality tables with varying precision)
int qtInfo = stream.read();
if ((qtInfo & 0x10) == 0x10) {
// TODO: Warning!
segment = new DownsampledDQTReplacement(realPosition, segment.end(), length, qtInfo, stream);
}
else {
segment = new Segment(marker, realPosition, segment.end(), length);
}
}
else {
segment = new Segment(marker, realPosition, segment.end(), length);
}
segment = new Segment(marker, realPosition, segment.end(), length);
segments.add(segment);
}
currentSegment = segments.size() - 1;
if (streamPos >= segment.start && streamPos < segment.end()) {
stream.seek(segment.realStart + streamPos - segment.start);
segment.seek(stream, streamPos);
break;
}
@ -157,20 +186,22 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
}
else if (streamPos < segment.start) {
// Go back in cache
while (--currentSegment >= 0) {
int cachedSegment = currentSegment;
while (--cachedSegment >= 0) {
currentSegment = cachedSegment;
segment = segments.get(currentSegment);
if (streamPos >= segment.start && streamPos < segment.end()) {
stream.seek(segment.realStart + streamPos - segment.start);
segment.seek(stream, streamPos);
break;
}
}
}
else {
stream.seek(segment.realStart + streamPos - segment.start);
segment.seek(stream, streamPos);
}
return segment;
}
@ -182,7 +213,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
try {
int length = stream.readUnsignedShort(); // Length including length field itself
byte[] data = new byte[Math.max(20, length - 2)];
byte[] data = new byte[Math.min(segmentId.length() + 1, length - 2)];
stream.readFully(data);
return segmentId.equals(asNullTerminatedAsciiString(data, 0));
@ -227,7 +258,15 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
private void repositionAsNecessary() throws IOException {
if (segment == null || streamPos < segment.start || streamPos >= segment.end()) {
fetchSegment();
try {
fetchSegment();
}
catch (EOFException ignore) {
// This might happen if the segment lengths in the stream are bad.
// We MUST leave internal state untouched in this case.
// We ignore this exception here, but client code will get
// an EOFException (or -1 return code) on subsequent reads.
}
}
}
@ -237,7 +276,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
repositionAsNecessary();
int read = stream.read();
int read = segment.read(stream);
if (read != -1) {
streamPos++;
@ -257,7 +296,8 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
while (total < len) {
repositionAsNecessary();
int count = stream.read(b, off + total, (int) Math.min(len - total, segment.end() - streamPos));
long bytesLeft = segment.end() - streamPos; // If no more bytes after reposition, we're at EOF
int count = bytesLeft == 0 ? -1 : segment.read(stream, b, off + total, (int) Math.min(len - total, bytesLeft));
if (count == -1) {
// EOF
@ -283,7 +323,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
}
static class Segment {
private final int marker;
final int marker;
final long realStart;
final long start;
@ -304,9 +344,128 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
return start + length;
}
public void seek(final ImageInputStream stream, final long newPos) throws IOException {
stream.seek(realStart + newPos - start);
}
public int read(final ImageInputStream stream) throws IOException {
return stream.read();
}
public int read(final ImageInputStream stream, byte[] b, int off, int len) throws IOException {
return stream.read(b, off, len);
}
@Override
public String toString() {
return String.format("0x%04x[%d-%d]", marker, realStart, realEnd());
}
}
/**
* Workaround for a known bug in com.sun.imageio.plugins.jpeg.AdobeMarkerSegment, leaving the buffer in an
* inconsistent state, if the length of the APP14/Adobe is not exactly 16 bytes.
*
* @see <a href="http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6355567">Bug report</a>
*/
static final class AdobeAPP14Replacement extends ReplacementSegment {
AdobeAPP14Replacement(final long realStart, final long start, final long realLength, final ImageInputStream stream) throws IOException {
super(JPEG.APP14, realStart, start, realLength, createMarkerFixedLength(stream));
}
private static byte[] createMarkerFixedLength(final ImageInputStream stream) throws IOException {
byte[] segmentData = new byte[16];
segmentData[0] = (byte) ((JPEG.APP14 >> 8) & 0xff);
segmentData[1] = (byte) (JPEG.APP14 & 0xff);
segmentData[2] = (byte) 0;
segmentData[3] = (byte) 14;
stream.readFully(segmentData, 4, segmentData.length - 4);
return segmentData;
}
}
/**
* Workaround for a known bug in com.sun.imageio.plugins.jpeg.DQTMarkerSegment, throwing exception,
* if the DQT precision is 16 bits (not 8 bits). Native reader seems to cope fine though.
* This downsampling of the quality tables, creates visually same results, with no exceptions thrown.
*/
static final class DownsampledDQTReplacement extends ReplacementSegment {
DownsampledDQTReplacement(final long realStart, final long start, final long realLength, final int qtInfo, final ImageInputStream stream) throws IOException {
super(JPEG.DQT, realStart, start, realLength, createMarkerFixedLength((int) realLength, qtInfo, stream));
}
private static byte[] createMarkerFixedLength(final int length, final int qtInfo, final ImageInputStream stream) throws IOException {
byte[] replacementData = new byte[length];
int numQTs = length / 128;
int newSegmentLength = 2 + 1 + 64 * numQTs;
replacementData[0] = (byte) ((JPEG.DQT >> 8) & 0xff);
replacementData[1] = (byte) (JPEG.DQT & 0xff);
replacementData[2] = (byte) ((newSegmentLength >> 8) & 0xff);
replacementData[3] = (byte) (newSegmentLength & 0xff);
replacementData[4] = (byte) (qtInfo & 0x0f);
stream.readFully(replacementData, 5, replacementData.length - 5);
// Downsample tables to 8 bits by discarding lower 8 bits...
int newOff = 4;
int oldOff = 4;
for (int q = 0; q < numQTs; q++) {
replacementData[newOff++] = (byte) (replacementData[oldOff++] & 0x0f);
for (int i = 0; i < 64; i++) {
replacementData[newOff + i] = replacementData[oldOff + 1 + i * 2];
}
newOff += 64;
oldOff += 128;
}
return Arrays.copyOfRange(replacementData, 0, newSegmentLength + 2);
}
}
static class ReplacementSegment extends Segment {
final long realLength;
final byte[] data;
int pos;
ReplacementSegment(final int marker, final long realStart, final long start, final long realLength, final byte[] replacementData) {
super(marker, realStart, start, replacementData.length);
this.realLength = realLength;
this.data = replacementData;
}
@Override
long realEnd() {
return realStart + realLength;
}
@Override
public void seek(final ImageInputStream stream, final long newPos) throws IOException {
pos = (int) (newPos - start);
super.seek(stream, newPos);
}
@Override
public int read(final ImageInputStream stream) {
return data[pos++] & 0xff;
}
@Override
public int read(final ImageInputStream stream, byte[] b, int off, int len) {
int length = Math.min(data.length - pos, len);
System.arraycopy(data, pos, b, off, length);
pos += length;
return length;
}
}
}

View File

@ -53,6 +53,7 @@ import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.EOFException;
import java.io.IOException;
import java.util.*;
import java.util.List;
@ -80,6 +81,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
@Override
protected List<TestData> getTestData() {
// While a lot of these files don't conform to any spec (Exif/JFIF), we will read these.
return Arrays.asList(
new TestData(getClassLoaderResource("/jpeg/cmm-exception-adobe-rgb.jpg"), new Dimension(626, 76)),
new TestData(getClassLoaderResource("/jpeg/cmm-exception-srgb.jpg"), new Dimension(1800, 1200)),
@ -88,7 +90,23 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(2707, 3804)),
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480)),
new TestData(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg"), new Dimension(20, 45)),
new TestData(getClassLoaderResource("/jpeg/0x00-to-0xFF-between-segments.jpg"), new Dimension(16, 16))
new TestData(getClassLoaderResource("/jpeg/0x00-to-0xFF-between-segments.jpg"), new Dimension(16, 16)),
new TestData(getClassLoaderResource("/jpeg/jfif-bogus-empty-jfif-segment.jpg"), new Dimension(942, 714)),
new TestData(getClassLoaderResource("/jpeg/jfif-16bit-dqt.jpg"), new Dimension(204, 131))
);
// More test data in specific tests below
}
protected List<TestData> getBrokenTestData() {
// These files are considered too broken to be read (ie. most other software does not read them either).
return Arrays.asList(
new TestData(getClassLoaderResource("/broken-jpeg/broken-bogus-segment-length.jpg"), new Dimension(467, 612)), // Semi-readable, parts missing
new TestData(getClassLoaderResource("/broken-jpeg/broken-adobe-marker-bad-length.jpg"), new Dimension(1800, 1200)), // Unreadable, segment lengths are wrong
new TestData(getClassLoaderResource("/broken-jpeg/broken-invalid-adobe-ycc-gray.jpg"), new Dimension(11, 440)), // Image readable, broken metadata (fixable?)
new TestData(getClassLoaderResource("/broken-jpeg/broken-no-sof-ascii-transfer-mode.jpg"), new Dimension(-1, -1)), // Unreadable, can't find SOFn marker
new TestData(getClassLoaderResource("/broken-jpeg/broken-sos-before-sof.jpg"), new Dimension(-1, -1)), // Unreadable, can't find SOFn marker
new TestData(getClassLoaderResource("/broken-jpeg/broken-adobe-segment-length-beyond-eof.jpg"), new Dimension(-1, -1)) // Unreadable, no EOI
);
// More test data in specific tests below
@ -146,7 +164,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(800, 800, 64, 8));
param.setSourceSubsampling(8, 8, 1, 1);
param.setSourceSubsampling(8, 8, 2, 2);
BufferedImage image = reader.read(0, param);
assertNotNull(image);
@ -163,7 +181,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
private static void assertJPEGPixelsEqual(byte[] expected, byte[] actual, int actualOffset) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], actual[i + actualOffset], 5);
assertEquals(String.format("Difference in pixel %d", i), expected[i], actual[i + actualOffset], 5);
}
}
@ -396,7 +414,209 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals(384, image.getHeight());
reader.dispose();
}
}
@Ignore("Known issue in com.sun...JPEGMetadata")
@Test
public void testStandardMetadataColorSpaceTypeRGBForYCbCr() {
// These reports RGB in standard metadata, while the data is really YCbCr.
// Exif files are always YCbCr AFAIK.
fail("/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg");
fail("/jpeg/exif-pspro-13-inverted-colors.jpg");
// Not Exif, but same issue: SOF comp ids are JFIF standard 1-3 and
// *should* be interpreted as YCbCr but isn't.
// Possible fix for this, is to insert a fake JFIF segment, as this image
// conforms to the JFIF spec (but it won't work for the Exif samples)
fail("/jpeg/no-jfif-ycbcr.jpg");
}
@Test
public void testBrokenReadRasterAfterGetMetadataException() throws IOException {
// See issue 107, from PDFBox team
JPEGImageReader reader = createReader();
try {
for (TestData broken : getBrokenTestData()) {
reader.setInput(broken.getInputStream());
try {
reader.getImageMetadata(0);
}
catch (IOException ignore) {
// Expected IOException here, due to broken file
// ignore.printStackTrace();
}
try {
reader.readRaster(0, null);
}
catch (IOException expected) {
// Should not throw anything other than IOException here
if (!(expected instanceof EOFException)) {
assertNotNull(expected.getMessage());
}
}
}
}
finally {
reader.dispose();
}
}
@Test
public void testBrokenRead() throws IOException {
JPEGImageReader reader = createReader();
try {
for (TestData broken : getBrokenTestData()) {
reader.setInput(broken.getInputStream());
try {
reader.read(0);
}
catch (IIOException expected) {
assertNotNull(expected.getMessage());
}
catch (IOException expected) {
if (!(expected instanceof EOFException)) {
assertNotNull(expected.getMessage());
}
}
}
}
finally {
reader.dispose();
}
}
@Test
public void testBrokenGetDimensions() throws IOException {
JPEGImageReader reader = createReader();
try {
for (TestData broken : getBrokenTestData()) {
reader.setInput(broken.getInputStream());
Dimension exptectedSize = broken.getDimension(0);
try {
assertEquals(exptectedSize.width, reader.getWidth(0));
assertEquals(exptectedSize.height, reader.getHeight(0));
}
catch (IIOException expected) {
assertNotNull(expected.getMessage());
}
catch (IOException expected) {
if (!(expected instanceof EOFException)) {
assertNotNull(expected.getMessage());
}
}
}
}
finally {
reader.dispose();
}
}
@Test
public void testBrokenGetImageMetadata() throws IOException {
JPEGImageReader reader = createReader();
try {
for (TestData broken : getBrokenTestData()) {
reader.setInput(broken.getInputStream());
try {
reader.getImageMetadata(0);
}
catch (IIOException expected) {
assertNotNull(expected.getMessage());
}
catch (IOException expected) {
if (!(expected instanceof EOFException)) {
assertNotNull(expected.getMessage());
}
}
}
}
finally {
reader.dispose();
}
}
@Test
public void testImageMetadata1ChannelGrayWithBogusAdobeYCC() throws IOException {
JPEGImageReader reader = createReader();
try {
// Any sample should do here
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-adobe-ycc-gray-with-metadata.jpg")));
IIOMetadata metadata = reader.getImageMetadata(0);
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
IIOMetadataNode chroma = getSingleElementByName(root, "Chroma");
IIOMetadataNode numChannels = getSingleElementByName(chroma, "NumChannels");
assertEquals("1", numChannels.getAttribute("value"));
IIOMetadataNode colorSpaceType = getSingleElementByName(chroma, "ColorSpaceType");
assertEquals("GRAY", colorSpaceType.getAttribute("name"));
}
finally {
reader.dispose();
}
}
private IIOMetadataNode getSingleElementByName(final IIOMetadataNode root, final String name) {
NodeList elements = root.getElementsByTagName(name);
assertEquals(1, elements.getLength());
return (IIOMetadataNode) elements.item(0);
}
@Test(expected = IndexOutOfBoundsException.class)
public void testGetImageMetadataOutOfBounds() throws IOException {
JPEGImageReader reader = createReader();
try {
// Any sample should do here
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/gray-sample.jpg")));
reader.getImageMetadata(-1);
}
finally {
reader.dispose();
}
}
@Test(expected = IIOException.class)
public void testBrokenBogusSegmentLengthReadWithDestination() throws IOException {
JPEGImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/broken-bogus-segment-length.jpg")));
assertEquals(467, reader.getWidth(0));
assertEquals(612, reader.getHeight(0));
ImageTypeSpecifier type = reader.getImageTypes(0).next();
BufferedImage image = type.createBufferedImage(reader.getWidth(0), reader.getHeight(0));
ImageReadParam param = reader.getDefaultReadParam();
param.setDestination(image);
try {
reader.read(0, param);
}
catch (IOException e) {
// Even if we get an exception here, the image should contain 10-15% of the image
assertRGBEquals(0xffffffff, image.getRGB(0, 0)); // white area
assertRGBEquals(0xff0000ff, image.getRGB(67, 22)); // blue area
assertRGBEquals(0xffff00ff, image.getRGB(83, 22)); // purple area
throw e;
}
}
finally {
reader.dispose();
}
}
@Test
public void testHasThumbnailNoIFD1() throws IOException {
@ -728,7 +948,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertNotNull(image);
}
@Ignore
@Test
public void testReadSubsamplingNotSkippingLines1028() throws IOException {
JPEGImageReader reader = createReader();
@ -793,7 +1012,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertNotNull(image);
}
@Ignore
@Test
public void testReadSubsamplingNotSkippingLines1025() throws IOException {
JPEGImageReader reader = createReader();
@ -935,16 +1153,9 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
public void testReadMetadataEqualReference() throws IOException {
// Compares the metadata for JFIF-conformant files with metadata from com.sun...JPEGImageReader
JPEGImageReader reader = createReader();
ImageReader referenceReader;
ImageReader referenceReader = createReferenceReader();
try {
@SuppressWarnings("unchecked")
Class<ImageReaderSpi> spiClass = (Class<ImageReaderSpi>) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi");
ImageReaderSpi provider = spiClass.newInstance();
referenceReader = provider.createReaderInstance();
}
catch (Throwable t) {
System.err.println("WARNING: Could not create ImageReader for reference (missing dependency): " + t.getMessage());
if (referenceReader == null) {
return;
}
@ -982,6 +1193,21 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
}
}
private ImageReader createReferenceReader() {
try {
@SuppressWarnings("unchecked")
Class<ImageReaderSpi> spiClass = (Class<ImageReaderSpi>) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi");
ImageReaderSpi provider = spiClass.newInstance();
return provider.createReaderInstance();
}
catch (Throwable t) {
System.err.println("WARNING: Could not create ImageReader for reference (missing dependency): " + t.getMessage());
return null;
}
}
private void assertTreesEquals(String message, Node expectedTree, Node actualTree) {
if (expectedTree == actualTree) {
return;
@ -1132,4 +1358,138 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
return sortedNodes;
}
@Test
public void testGetNumImagesBogusDataPrepended() throws IOException {
// The JPEGImageReader (incorrectly) interprets this image to be a "tables only" image.
JPEGImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/broken-bogus-data-prepended-real-jfif-start-at-4801.jpg")));
assertEquals(-1, reader.getNumImages(false)); // Ok
assertEquals(0, reader.getNumImages(true)); // Should throw IIOException or return 0
}
finally {
reader.dispose();
}
}
@Test
public void testNegativeSOSComponentCount() throws IOException {
// The data in the stream looks like this:
// FF DA 00 08 01 01 01 06 3F 02 0E 70 9A A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 A2 64 05 5D ...
// ..but the JPEGBuffer class contains:
// FF DA 00 08 A2 A2 A2 A2 A2 64 05 5D 02 87 FC 5B 5C E1 0E BD ...
// *****************??
// 15 bytes missing in action! Why?
// There's a bug in com.sun.imageio.plugins.jpeg.AdobeMarkerSegment when parsing non-standard length
// APP14/Adobe segments (i.e. lengths other than 14) that causes the
// com.sun.imageio.plugins.jpeg.JPEGBuffer#loadBuf() method to overwrite parts of the input data
// (the difference between the real length and 14, at the end of the stream). This can cause all
// sorts of weird problems later, and is a pain to track down (it is probably the real cause for
// many of the other issues we've found in the set).
// See also: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6355567
JPEGImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-exif-xmp-adobe-progressive-negative-component-count.jpg")));
IIOMetadata metadata = reader.getImageMetadata(0);
assertNotNull(metadata);
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
assertNotNull(tree);
assertThat(tree, new IsInstanceOf(IIOMetadataNode.class));
}
catch (IIOException knownIssue) {
// This shouldn't fail, but the bug is most likely in the JPEGBuffer class
assertNotNull(knownIssue.getCause());
assertThat(knownIssue.getCause(), new IsInstanceOf(NegativeArraySizeException.class));
}
finally {
reader.dispose();
}
}
@Test
public void testInconsistentSOSBandCountExceedsSOFBandCount() throws IOException {
// Last SOS segment contains (FF DA) 00 08 01 03 03 01 3F 10 (... 18 more ... F0 7D FB FB 6D)
// (14th) (SOS) len 8 | | | | | approx high: 1, approx low: 0
// | | | | end spectral selection:
// | | | start spectral selection: 1
// | | dc: 0, ac: 3
// | selector: 3
// 1 component
// Metadata reads completely different values...
// FF DA 00 08 01 F0 7D FB FB 6D
// \_ there's 24 bytes MIA (skipped) here, between the length and the actual data read...
// Seems to be a bug in the AdobeMarkerSegment, it reads 12 bytes always,
// then subtracting length from bufferAvail, but *does not update bufPtr to skip the remaining*.
// This causes trouble for subsequent JPEGBuffer.loadBuf() calls, because it will overwrite the same
// number of bytes *at the end* of the buffer.
// This image has a 38 (36) byte App14/Adobe segment.
// The length 36 - 12 = 24 (the size of the missing bytes!)
// TODO: Report bug!
ImageReader reader = createReader();
// ImageReader reader = createReferenceReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/progressive-adobe-sof-bands-dont-match-sos-band-count.jpg")));
IIOMetadata metadata = reader.getImageMetadata(0);
assertNotNull(metadata);
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
assertNotNull(tree);
assertThat(tree, new IsInstanceOf(IIOMetadataNode.class));
}
finally {
reader.dispose();
}
}
@Test
public void testInvalidDHTIssue() throws IOException {
// Image has empty (!) DHT that is okay on read, but not when you set back from tree...
JPEGImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-progressive-invalid-dht.jpg")));
IIOMetadata metadata = reader.getImageMetadata(0);
assertNotNull(metadata);
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
assertNotNull(tree);
assertThat(tree, new IsInstanceOf(IIOMetadataNode.class));
}
finally {
reader.dispose();
}
}
@Test
public void testComponentIdOutOfRange() throws IOException {
// Image has SOF and SOS component ids that are negative, setFromTree chokes on this...
JPEGImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-component-id-out-of-range.jpg")));
IIOMetadata metadata = reader.getImageMetadata(0);
assertNotNull(metadata);
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
assertNotNull(tree);
assertThat(tree, new IsInstanceOf(IIOMetadataNode.class));
}
finally {
reader.dispose();
}
}
}

View File

@ -56,6 +56,7 @@ import static org.junit.Assert.*;
public class JPEGSegmentImageInputStreamTest {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
ImageIO.setUseCache(false);
}
protected URL getClassLoaderResource(final String pName) {
@ -152,4 +153,28 @@ public class JPEGSegmentImageInputStreamTest {
assertEquals(1061L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
}
@Test
public void testEOFExceptionInSegmentParsingShouldNotCreateBadState() throws IOException {
ImageInputStream iis = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/broken-no-sof-ascii-transfer-mode.jpg")));
byte[] buffer = new byte[4096];
// NOTE: This is a simulation of how the native parts of com.sun...JPEGImageReader would read the image...
assertEquals(2, iis.read(buffer, 0, buffer.length));
assertEquals(2, iis.getStreamPosition());
iis.seek(0x2012); // bad segment length, should have been 0x0012, not 0x2012
assertEquals(0x2012, iis.getStreamPosition());
// So far, so good (but stream position is now really beyond EOF)...
// This however, will blow up with an EOFException internally (but we'll return -1 to be good)
assertEquals(-1, iis.read(buffer, 0, buffer.length));
assertEquals(0x2012, iis.getStreamPosition());
// Again, should just continue returning -1 for ever
assertEquals(-1, iis.read(buffer, 0, buffer.length));
assertEquals(0x2012, iis.getStreamPosition());
}
}

View File

@ -0,0 +1,88 @@
ÿØÿî Adobe d€ ÿÛ „   

     #"""#'''''''''' 
   !! !!''''''''''ÿÀ &å" ÿÄ¢   
   
 s !1AQa"q<>2¡±B#ÁRÑá3bð$rñ%C4S¢²csÂ5D'“£³6TdtÃÒâ
„”EF¤´VÓU(òãóÄÔäôeu…•¥µÅÕåõfv†¦¶ÆÖæö7GWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø)9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêú m !1AQa"q<>2¡±ðÁÑá#BRbrñ3$4CS%¢c²ÂsÒ5âDƒT“
&6E'dtU7ò£³Ã()Óã󄔤´ÄÔäôeu…•¥µÅÕåõFVfv†¦¶ÆÖæöGWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêúÿÚ   ? âµZoŒ,ÄËeuß"ãp0DS=p¸¡ÅS5¸-Ôâ©!# ã;ïƒ âM1B¡϶,ˆ Ëà m˜£aŠU8ñÅ ”)Àà–Û Jb©ô"§lNiÂôÀ<C3B4>ÉEÛ‰ Å^­å™SYò}͈þòŠƒÀŠg!ÔU­o$‰¶*Äd¯È~jƒF½hn~(.ìpëÎþL†êÓ:AC çUþ9Ÿ8Œ°ÛøªQþ·ñE¤îØûžwo~ð<><41>ìßT½¼” $V=93n>ìâs™#s؃Lèzåàò¿´Mûë±õû§Ëlƒè ‘Œ±Ï`ú2—0C×õ*ÇÌ6Æh(d"¥?¦F´+Y|»ª½<C2AA>À?R»¬n@NÙò·<C3B2>ÞEi?êP\éÞc· <20>·lùË$%ŒŸT%¿"ñÌ MX—ˆ¤nISÚ‡"ÐÎCvïÌmê¹"·º”øÓ쟻8*ËÂCpi”fá”s;ëÖÊ=bôï&êe%U'¸Î<C2B8>¯Bš†ƒê0©N‡ØŒâX¸ãv½ÅsºZ®hr»±Z<C2B1>ò̬‡ly<ÃXþ(¾pó^…˨ñÂdm·9=óq6¤bIf  nIÃÝòòÎ(ÄÚ¼¥M*`ˆoöGÔs[C-Ž¢Üyk#ŒpDqÈs€÷òes1øâ£A¿oØ9Ý-4ï-ißZl²ûrÄì>æ^?†:ç̶P7 {AÄuøB}¦bú¹Õ8y{RQ,¸±÷ '•59ER&o§õ‡Ùmä'Ùÿ î¶ÞdÒZÝdyDlFñž£èâ<7F>´õ~ž-OÕâó_ÏH€N¯¿ ?KÂfò^³«ÛÈ£Ý
žRÔä4X˜Ÿ`Nz=G¸<Vu¯ù_üJ˜1ÚMã=z¸ÆË8êsKû½L%îˆ?¥óœžMÕcZ´.>jGðÂË<C382>ú
òŒšg§¤·¯C\<5C>k66«W¹<57><C2B9>mƒqïJ6HˬÕa<C395>¥<19>ÎÇÚùá­']¸»(E ûJFuÙôÝ2¿©'±R?Ž—H±}š!óÊF<ÇÉ¢>ÐÆêX<C3AA>À¼ÑWlŽ™1¹òÕ«bøNË Ý[=@ä¾8Æ`òsôý­¥Í·îÈX<C388>ôÁ¶ö<C2B6>ÍNh­<68>H ´8e» ›°`ØP:p#n˜
{1H½&UÂëèZ•¥ tßlMÐ`‰Ã°$ņء¥ QAÀ…\ƒ\R¸-Z„ÑOA­çlUÒŽùGâÀ«*±ÅÕûb­ŸlgÃE,ô!à(µ!ã ûb<C3BB>ʇ³;⨲¥p;€7Æúã<C3BA>04žÇDß4Ž;`5”“¾g”ôÅ*r=[;ã]¾,µ5Å\+\YV¸W bª¼E3ezƒŽlU,å<>SˆãÔÓD¥1E  ÏÃ/™ÅQÜÆØ¼rñß ¯µ1Tî;¥¦ç©A†¸-f¢ÓMRAËlŒ„o„KpF Šèž¸ªlÒé€ï. ]± .ˆÀ²Ì[SH¯PhAÎãùsxú§/ìårì«ÈkÓo㜠žG;/äÙ?W¿ýôÙ“¦&²€ |AkŸ0~3×aú¾©4gj9ýy'üÌV6¾]¸ ˜šÁH>ÍGjá<07>[޵qþ±É”¯ ó~<7E>qä½EÀ¼ˆ´‰\ïϼU?ÍJŽL¸ùqn>H¢{˜ÒA *Ù׿.õ§3F$zšŽùÆ5++Í*ö[ØÌSÂÅYXS¦I<«­Ki2p­AtÓ$KCîµÉQ|€æ_KùÔ]iwüEVxD€ÿ ”½^y¦ÿ D»µ¹wàyNzËä—–<n£g<C2A3>Çiãá\ ªiI/w< è»új*Hù.ù1,y=]Õ»V\Ñㄇ/ƒÉ<§åÍbædY”ŠC²<43>³·hñM¦@¿X<C2BF>3ºŽŸyÈyÆÞÞ<C39E>ÚY•<59>>RBP âù½ÞàްPr ³W¾bKU#*`êGiá‰2c9ÈDˆ<44>r}tÚÔ91<39>n<EFBFBD>@[}üjáœkŠª<C5A0>œ÷YŽÖ玧bõW4š>áüi†Ú_šmÖÙ!»ä®€/?´ ;åFfRõ=/¹¯¾4ã”B ïŽRe´ lº¶±¼¹·I=Èûò?wæÛdÚ/îßÄ ól ýôEkЩ ý0Xï6s×i¤x%!!æ.?sWžMŠgf¶¹1¡èŒ G· pµü…v(o¾K-/à¾<C3A0>Ô<EFBFBD>ªÄˆÁøô8(Ói¤8 6—óNÌóI¿ÒÀkTí*î1k?8\D1´rqÛâS_ÀŒ3ó&¢ 4 ïÉÈ5éÛ#údZm¥ÁâÜJðú2<<3C>W¿“­óŽ,<>!ÅÒúÝt ßÎD<C38E>õˆ+âÈ<C388>þ¸8ëú=Ü|e`ê’)þ„c ²Ò®¡Y"·‰‘†Ä «"^eÒZÎp¶ŽÈ®9 ðÞ…k†ä9¹sÉ«Å)d†XKk"ùòåHýLiRV®¢¿Èv¯ÈÓ ™xô5?HÐo.¢Y.[PÝÏË n4-ã.$&ƒ¾ èï$Û<>=>iñfŽ1óôúG¼Çq¤(qIB«à no$<24>û=ʸHCF8š<>4KA?Tx¯\zZBã÷, ³ÐýØ;õmÎÇðÅÄHAVâã}<7D>LNQæìtÚíVÐîˆÐÃ8>+<2B>ÿ ºœÐþÌž?<eÅ»õê<rè̤Òkñj"(Ժı۠ß
ní)Ó%O Uy9'1<>4H)‡W;a\°š×B©#›œ¾íŠGN*­7Z`Øãf ¦-kÃ(mÖ•CÅnÊS=8ÐŒuªU:Ò§¶Îôc‡w´ ŒŽÜÈâ« ×7©^¸ƒŒ©ÅQ!…s;`R™FJâªW7*b<órÅUæ ˆ˜6øª"¦™±>B™±T>^YCo,ªeUP66»ã+—\Uà”zázµ1t<31>ŒUàxؾ.\P½ªN6A¶)É:ânàŒU
½I Î÷ä =<»åK<C3A5>Zàqt*•ðês¬¾ŒÉ ߉™èMâ/7yêñšMlœJ®Ûv;f^˜ yÊ"^QþÖ¹óã^÷…yQ†­<ácÓ
»žÎâ;g1Í Œ»F
ÖìeÓµ ­äÆ•Å4]çU¹H£B܈ ¹')Ï) ²‘؃ý‹,<2C>Ç<EFBFBD>ŠG`ŒÐÚþhhFï€ÌºrZŸX<C5B8>z·» :òŸ<C3B2>-tØóU@Î!‡úÿ Ó¼«å m•€k¶v½U|~xs¬Ú]]X¼6;JÔiU®ùV\¼|&¨<>½:Ìù'f¨F;ú»¬w¢á½·XÓ<58>Ee@´ ð˜5eG×9ãyg\U? ßˆqŠYkzp¶·àð_…ƒˆ÷ʸ·­÷ïpáÚ3<C39A>Q†X<E280A0> %ÒþÆ]}¢i·Õi¡^gö×fûÆDï|¡t²ÒÎ@ñÕÏ>{oÕ¬¡Œ<󪎴¨Àñë¶7s{w.ç 
æê1hòÆc“µ$oïHÉÓˆ [Áë/‡ï¯ðÈF±eªéLÌ…¹'÷ˆGãòÎÇ4ñÚÂÓLx¢Šr-5áóæÎeXÆæwܪøíãჄЏåþLŽ.?w½ç#WÖœ%¼,î>ÐQ†ƒË^aaq$/Åv
¿ÈgO°Òìô¸6Y©»€µ<C2B5>BÝ)ene$nà<6E>‡ý‰Ã_̘1™÷ÓñUôHt•¾²>´¨ÑBGÆoî;Qó ¿Ââ4é˾FõÍgZ<67>©=»ÔšŽdÓ
m Þët¾ÔX¥¸?
<EFBFBD>|½°Ý ä*Ë… Y¸81ÈÆò°fOÃ<4F>GéÚuÖ¬åÐñŽ¿ÏZ}8>ïË3Á©o/®Ë»''ýŽçÚîÃKT´Z! ðƾM¨ë·%<25>·>š ýð v <0C>!̧&-&pä&yÞ“¸?wÍ-²Õ®t²h~
üHÝ+ƒ.|ánÀb‡ýç,q4àˆÀ¾Ó§ÃËýa¸ÂÑ,kO °Æ2«иøõ Àå˜<C3A5>p÷òLÎ/s2Á´f4ýØ }ç RâHLR¿*õ'­0"ÎÞOZ*‡;ÿ -Ö5<35>2P«eaU&¸ñN2ŽÜ<C5BD>~†U=FA<46>O9FüR«ïOd ]± †Eâè­;T½Ô<C2BD>ƒbæ´Á"ßPIƒz¼»îVU-äNÑò g±HÂs”z^ÿ bÛ<62>5€ñ>¦]Mwhô ŠdÇ_YÅrŸÜwÆ3è[´Ú®”qÄ÷óJtíQ§$Zö®E$±(*yFz£tþÌ*µŽÖÕèFþ' VDqU;a£`#>Aœxb`9Úù I£õ`ÿ d½Á þH FøuÌÄüÐÐ÷ð#ß½‚;´<C692>Êÿ L²/bîû?µ„„qçØòïc⸢¸*â3;SaEË]ÝÚ
H”
Ó<12>¶/#Óá8<1C>ZŒU7´<37>5…׎G-æEpÖ+¥ ¾)VšvWÛ¦ZÎ
ï…×
B”®*¾òNDÓ ec\$¢½pÌ ¨ÅP®)ˆ1Å]±¾*Ñ9Yy±V³W6lUºæÊx«uÚ™³fÅW<>òÀÌN*°ãN8åb«lqTÅ\1êq˜àqTLrÓ7 ˜“Š£R{æi 02¿l¶lU¶rs·þI$ÊþvbPG@½·9ÃUYØ"Š“¶z3òëO/”}Gøf¼4½ þÜ¿ ŽIó-9§ÕžV~ê¾V¼ó7™n Ö4cɺ($ä£J²Ñ|¥(…¹KxÇ ]£¢<C2A3>Gykap0¥%â4<6F>‰¦ËolèZhQÙÅ*Ê 9<>šg$Ìù_+î:l‡&k1Ê8¡.D\cå]þiU¿˜MýÊÛY[³ŽîO£ÄõÉ«*¨­ ~#×#¯y¥ù~<02>(õ[~ ö˜ûœa¨ÜKgõËÅVUðNÄ× º|¤ ¹òW˜ëLgŽWä¯'ÏFO…½÷Â/0Yý~Ù-íT<ÈâŒiP:Ÿ
ºÕÞ³}õKI= p y«?”8·±[8 6`! <20>ïSVñoÄΘÎ0ÀÜLïrGH<47>ÒÙùN£ê—nñ§Â¿S†¢çBÑ<42>…hãaÕSw?>­<>o7§šly¼rI,Gv™9qùPl3¾¥©K0I¤rkÒ§ ˜é°í/1˜Ý™ž)ü¿k×õ¯0þ<30>S°+ ûE¾ÑÁ¾W»±HM¿Ù¹$¯íxSä0ƒBòÕÍÕL÷A ý9øQ‰ÞYÞi3ÖBj»£<C2BB>#껫¹×œ™ñæ©<><1D>­¨÷wy=IT¹û#r}°<>üÏ¥‡1ó$ŽÁzü°ºËÌÉqŠíCFGqÔý
îÓB»`ËÊ:õ¥ãøäÀ'é¢äæ×Ø£ç m+÷×ú5äEf)2ªË_ÀŒjê6±F#µâ¨‚Šª:FšÆÚŠÞ踔ãjF[ V.@Šp²vŽQ.Xî¹ÇŸÎËw×{£)55댔´®i"U£òͽoc\ #…ÜÐ ÉÊç30G×ÈwN¹Jɳv}å+¿ÔÛM” #/}0e•Ñ»ˆˆ=0ÖÓd¤Qš†ïþg R4‰B
ª( è2rß«“ÃŽ8Dzÿ ¾<>>+û`;ëÎ6SÈÔ`ºœ¬ïa¢”$%Dr!N<63>b‰B¢ì Å3WÌ=\ŠîM<C3AE>ÉB^ÞIfUøòˆì}Ž>;ˆ/b"6ë۸ř#™
° <0C>ÔaLº{Ù?¯lIJî;Œ˜£·"ߌc”xO¢cè}ê÷6Kéе[±Âد$µ“Ó~ž8p¤Ïbj{á6©°Ø®ã'âäéÏ8²ogäS%¸õ0 Ž£"ÐjÍÀÛá…®ª$`ìš<1C>³Ã²3R¶.È6=pžO„Päš)ÒUàÛ©Œ Õàú¼„<C2BC>ðž™d%{;^ËÖqÄiòýq±èN—áéŽ,k\Bg©ß&íœ$>8¨¹jR¸ÉLa£dž½ñ=;àF<C3A0>œacŠ¢šrO\M¤Ä9f®*¹š¸ÊæÊÅ]—•›vlÙ±VÆ8 hÅR˜«\sb”ÍŠ¹‡†Q]±Í”tÅT÷Ë \q׸ªÑséŠôßf®*¢Â™YlwÊÅW˜ŒªÓ1jâ­c…XÐu8ÚáÆ<C3A1>¦µíÊ>pBÚóeŽ,rÉ3´E§~TÐYVêðIMiÜû ì6³J­of´ê Aöa„tQî|rIk|!Féwíe+ÎÀÈJñ Ÿ‘ÈÃ'¢¹Ù÷ŸhÏ. Ï!1‰ mÒ(íjé-õdx<64>Z%PÞÄi÷`[Ÿ2^ÊJ,<2C>ØSíáýNîòkÞÎK4žvâØ½Y;±þYÈH <20>C[ÓFL“yÌYDóó,‡JÒe½o/÷UåFÝžŸÃ$:²É.™<p hµãì:Ó!ߤîÂúk+*vPiƒm5Éb<C389>¢œ”©ß}þu /i|ú¶`Ôá†9b1”|H)ó6BÆò[<1B>4[°Ú†¿Ã&vZå½Ü`òâß´§·Ó<C2B7>*Ôš˜àÄt9Î…^MZ}^\#¼O8— O¨[ ey
Œ<EFBFBD>ëºf‡ysíšzs† PšoÓÍ8<C38D>KÈÛbV×à3<C3A0>DñÂg¶Ãâ[²ësd‰<64>ˆX翚¤jkc¬„ñ&ª1MGZ¶½€Å$F£ì½FÇÇ#W7ÐÛ¡f5§a-G]žæB±ô ïŒxˆØVÌ´˜µYááD<C3A1>Œv:^©©Ï¦_Ÿª¸
7ã[ù¶2 ž"§¹Sýqm3G³Ú9î“ÔAÈ«€=69´M)Å ²<>#øà2<C3A0>" Ž­™3h ²âœåIœvºø­Ìzs<7A>ïŠû08ºëV,*.Sé48O,iïR…ãö£ñ]?”¤ÜÁp€`GêÂ&:NCâÕ]3ýìñÿ X_Ü\y<>NˆÞú‡ÁF]Hë2{`V>¦½~œ<>M êqÌ#x˜ò!U†êkî2c£i?¢à!˜4<CB9C>OP<4F>·È2¡½ñ¨Á¢ÓbÅ“ÄÉ/§{ý“+khí£
¿Iñ8ö;æ©Æ’ ©Êå0@ {Ý9²I&Éo6j~eŠÕÌ6ª%<25>lXýò¦Ò®î/-„÷øiµF—"zLÐÄ3N<1—+æ~i57³»0Î*•ëìza²:J<>ÐòVèp&¥§¥ü$ÆUûü¶F´íNçOº6³Ê)ì}²T$6æá§Ž£²c¨÷ŽðÉdv²~G{v;ÿ pZºH¡<48>†SÜb$WnŽ:d<ê×zMÓĨ 8ž”ÄGÞƒI=H<>†Ù!Ðÿ ýlà*¨  }kÈAë„Ñy­\|iCíŽmx¿ÙS„B@¦:V9Ù‰‰R<52>Ež‰º±ë‡Vj ¹ø°7éE'“­ ´×<U³Ÿ&¶xÄGð<47>ém~®E iÓß"^@@ûiú°KÞ¤¢ âuˆQÆÍßà $n\<RÉ G!±8õ°éÁ<C3A9>Š<EFBFBD>©€%}òI­Ø¿Y‡ìž£Ã"òÔeàØz½.¢9ñ Ž|ˆî*LÄœms•\.C‰ÊÍvlÙ±WfÍ—LU¬ÙtÍLUÔÍLx.˜ªÊc†Ç1 b®å\تö>˜&¹‰ËN¸ª²-zã¸S2WR*q27Á-@1oЍ2q¼i)<29>+Ѝ•‹'Àâ«­ iåX׹Γ¢éñØÚ«RŒG\ŠyvÈ4ž»<C5BE>…r\÷aUcNBNÁç{g<²LiñŸL~¯z=%M6¡=ñej<p‰o¹Üè ©Á×Wž•³HÉO„e`<18>C¥žžBQ<42>}H†êìÁOrO†êcŽ c¶äñíÉï¥FiØŠž˜lþµë\|jªíÚ¹!Œ ÎîÓfCN\ד„Xˆä|“½2 FíÅíôŒÕ:ôv6÷X·´øGÄÝ_k Ëû½‡`:d}ÅÕÅ´ŠÜIëCJxá³rù#<1E>ê'âgÇ<08>¡¨w2³¨1µW? Éñ|<7C>é…Òj³'IË ]ݱøì6¦2¹8DCv h“¹ºG]j—R­É+ôÄöiˆ/Ä}ÎøLÿ f½Ž6f +ò¸¤!@ß.«çÔgªÌhsXG÷($û òsì0âöŒT;ä«g.x£ RÇ ŠÙžC}­(: Sô•¸L<7F>¹ [©QIµ–[ Kõ'å•ðéâDäeB"ÙÛ^(éÓ·ñFÛß…ÃS<C383>k<EFBFBD>ŠÖæéÀŒ…^ìÆŸÛƒ€uq 3Â;ÓÓ{l:È.«­Qƒ)ðß
¿@ÀÈD·3ûKAýpžêÏVÒ˜É3Ä?m7Ûü¡Û" þE†=6 §ƒj—N1@û‹+T‰KÈh£©ÈΫ®´õµ´û'béÂ[Ífötôå|4òͬDµíÍ(¿ÝòññÉ;¹Ðìøéq<C3A9>F¢¦côÄr'¢#Kòà%n¯¾bù«nf1-QP(×Ôm<C394>¸ θ¬l¬6£〓v\Ù³d˜ÉœÝ@&9uæ9cå{v¯†!¢Fo¯Zæˆ.ÿ N_i}Õ[‡¢ýÊý“<C3BD>ì=Z4q¿Ú9 El7sãåå< rLQ÷uÝ3<C39D>Ò¡vðÕ4Ùï¦åU†Í‡"ò&;¾ü]5(TR¾;b,r 'O.<p¹y¤Ze¨{–⣶­–Ÿ ñ uÿ ´ ]sª"׉©ÇÔy³™ÕêesQØ#®íìÝHT ûdfêµ—’¯cµõÄÆ‰_£*\H‡šš{Œ<>ÙÎÓcžS<>ƒÌj÷Ì(á¤S™c¿lŒ«ðja<6A><61>ÝÓêt¢¸¢<Óˆä­û«þ"ú½“ZLŸ ܘÛfPHï€<Áh&´,¢­ßGlc*.6UájDyFf¤:_BÁÎV8Š×£k6l¼U¬Ù³b®Ç<C2AE><C387>ª)8«@e¦,<10><>+Ьt¦<
ªb­Í\Q†$Ûb­W6VlUT!®*™{ Ä⫆Ù|é‰3Ó¦$X×E ˜ ð:¾øº8lUqL±®,€bª…ŽØªŠÁ·LÆÚ½°XZ z Hø <C3B8>“{ŵœj6-¹Åä•AÛsD‰òÿ Jìñ<C3AC>XtÊ^`ÇŽvyÊE¨ŸŒÏ/ŽÃ凷JJÀÈ ùdb[ž"ƒ½Óžø˜Ût´RÌ9˜ù†]¨iºV ,rSgOâ2%=©±™ã²Ã¾'õ©Wì±ûð3ÎÎõcZáŒHÚíËÒi3b)œ?šz{ py
œÚŒŽž<C5BD>˜VkÛ-d©ÉžŸ¨<>6ä«,µ8Ô5ë‰È 2Ðü8¶<08>‡Þ3L@qkrø·C±® ¾²ú­ 5¨}8´Œ„Æ3Î[„µ<E2809E>Å` .øÞ'§·†-çÕ/0ƒ*Ö^Ì:œÇ.+\kf'à#½8Šâ†¬i†vú€P(vÈɲÖY|.Mf7¦kÿ 3@pÂ)Õº÷È<²B¿Ç&°àõürërödÉ<fKy¢i×õb<C3B5>$?¶›}ã"z<>½Õ„æÐÀ}+B0Ê {‡Ú4ÅŸY¶½dF!ª1ê1ažœjôò©ÄåÆ#ü>b×èº8Š1uz;
¤mÐ{œ1¸£-#j7ìÐÐaί!%A¦¾¥-~Ö<$AÒê3ä9fw<‡@;‘·-­|Qˆäãâ?ˆÂ)于B²rVî Aû NEjÖƒÛÝ­ž¤²qYPu?µ’¹‡+s§ŸL1áë(<28>ïÜÅcsП£ÞP¯÷5ÂÛTE@G€Àë©\áø9|s˜â†(<28>æ¢æâ1ñòQ5Ä¡+@Näá£Ü ¢*ûÔnÀ¾Œ¤øck<63>%Â~<19>µ2M­”@F ·w=q/QÑ<51>Þ¸Y$ìýN&à§:[õH“#½” ç÷¦ž8èØ1Ó çÈåŽ.ŽÒÇ ÚöL­u„€NØv$[»w^¼”þApHv8wb<77> ¡« ]>®8Ǫ Æ@°{¨Ìs:Ðâ8yæ ?Bè¸ nÓ-ýÓåqC êÜÔË9†×S52éŽT'iWD•Æ,d`ˆÁb­²Pb.¤`$ ð4<C3B0>\UI‰Æ×1leqUäâg/(â­S6_lت ŒÕãÓ|Ucœes¡Š®Œâ@cÔŠ£âaMðTtꬄm¸ª:W
6;â)1ä)ã<>ZRNhž²(÷ÅŒ¹s'<27>ü·ùÙ'ŽKvŒŸˆn1Ó
Eú˜PìCœ©ÐbÆ'åÂoíR”îqX¤†¸ÉkŒl´œ /!¸&ª»õ>øœŒÒ
Sn‰£Ëf¢¹ZQñítöp:YÜHi>¸¿è}C<>/Aéò8vY ú¤.K…sÄâ¾Ã IП<C390>J<4A>Šà×rL`45Åg`<35>duË0?P*2-Âý[Þ§ËlL}ºxâ†7F"àƒ\-®<E28098>U<1A>1ˆÜ~˜>ÎT”pq¾k4;¡ú0_BÕã<C395>3<EFBFBD> ¯52ðñ¨ÜøcGò“ÆÃÅCšÞ£l e(Dðñú¡d”¹ð€Æ OL{ÄTãƒ²Ž S ` À˜ŠW4POцVËm4`|(7ûÎ õß1Fíá‰kÉ6":_#Õ‰ÀîÄš ³Ë)6`Ol[cßH¡°(fÚ»Ó+ë2 <0A>ÄÚf}»e¤%<25>cÐcïjáy@µŽÌ£|loÃs×ñüUìqlW ä­Õ` Á³Çl± ë!<1B><EFBFBD>8òéšg„p×0:¶^§lz°k“¹ÌW””ðÅá;wÓ R¸k\5i<>jpŽLñADG°­pÎÂჅ®*ø*Ö« #quŒ£.©®¯iõ»"Ôýä_«!L¥XƒÛ: £!º0*~<>}F/NêE‰Æ˜Oï0Kø}CãÍFV;, ±Ü9pZGJJÃR»SR¥
†Ú»œ^87ÅŠ”ªâalà î$$Ó åª ²—®>A¾'ÓVâ)‰6X'¸ªžlwÊ™±TYmˆ={à°¸UðÅP§,f#, Uz{â¼q4÷Å+Š­"ƒÈå“ÛНã£4~c®)j¾¥Â'‰3 FDôM¼0·Štùa4Õ pæíx¬h:"<22>÷ï„÷ FT&“Ÿ¾Ô|H­qJí<4A>É;!²Ñ'~˜ehÖVà™aYnUý]0l2@å&D#IÕ¾ $š <04>z*Š “ØÝÁ*qæµð9‰ŠGQÔæ[‰ÕXŒ‰<C592>¸ŽÍ¬Dð×&o©i6wñ1
¢JlÃ9Ég+)‡6ú½ÂÑY‰a$)¨Ã¸ i×¾1¸ó6ÇJsèeÃ\xÏØÃb¸x<C2B8>ˆÃ{]B"(øÿ J݉Q¶JiÐäènc‡Q ï¨eKyl *Dza2Ï*ôlwÖ¤ÁÃæ×!<21>ô̦„’ùR\ûáY¸“Çiºœ<-Ÿ—\R6SDœr­qf½zQFÂÄ8ß ”Tb@c“ q¸™Ž0Í7†+ÇÛð¨Ü`ø"â9@!<21>Ó¯UǦ  üJ3IÀöÀ¯8vf!ŽCxÒ8ÞÀÃuÄ«š¯LbðÅíÔ<C3AD>Ž4áB ˜ßÍ:õ‡e Øf[Vs°Ãm/áæûS!ÇÍŸ @2ÜŽIQ¨8 <—~Øa=¬}@û°)„ÐñSØÇ<&E0è1HÁmÛŽÖVr@í$ 1+šxà6"Ê‹ ¶25 r=pd­P¯€Ì-€ÀãŒ±á«æ<C3A6>roÍH£G·L7³dr^3öôŽôŽ ×¬­Zbºl-#o¾´)ÔŠŸ ‰.». Â\d¡mÔ§úND5b ì§ß%óJ°Fó¶ÁAærq/­3¹îrPçngeDœ¹2tªø¨ß2æ¦Zœ±Ý"" 8co"+…hqtzwÅS£2\ -߆i«ßi+Š¢nF¸G[lM·ÅTÛs+¾*¤S|UËÕÅV*ãCS<17>×¾*³ÑÍŽõ“Ô§lØ«\Ç|BMñçlM<6C>qU;ã‚å•®<)ªÂ(6Æò¦(Ã8«e«ŽøÁ<C3B8>«DàÝ9ßF=ð\1ÐÅ/“ç€ò-:“X2èQrŠK3xíôaÉ_Pá½Ô•棨'õá Ü‹¬:m æz)9Û)§êk‰rNÙ'j#b<>D±Üã•‹ ñÁwÅG§dÑ@Æcø=0@‡Ò^N1`gù“Ñ ¸ÁV÷@~sjÿ káùe4P~Ë<>§J3<>ø„ÝnẌ€WÇ
îô¥jºûc•û-Š,ò¨ã\yrqáŽX‰P=
\tð:µ1§OÚªØ.^D׫ •—:3ÈEñ –Ò'¾$Apʼn#ºTt tdO42špÚÑ@+…EpM¤Þ²3Dʹ§!¡NªOÑ<4F>åo)âOô®) ,«šK4&½2·V%H”O}©=ˆ¥PÔ`Y-™{aA,e<7F>Ë --šái$`Ó¸Û ÒËVq 2
ŠÄ‡<C384> ÉCèHí°¦
‡C†%äÔø Ãö¶¶z%Ö0ü ‘¿Ë ý´_Žåmi²£IO·ã™µkf 8áN<C3A1>†D“Ð:¼“É’\P<>#Ÿà4špqR:â£ÁÔ¨úr¤ÔÆLRb)Œ·Ô}ABßê0zšÿ ÂH$<•?FDŠ cAb¹ð©<>O]ñ¦U¦Øî<C398>,ŸÄIAsÈ»u8Ã,·,I軜•·Frê”ê;qŒ|Î'kl_¨ÁM žbì>C `·Xã.Ä(Î6åK8ÇŒ@sý%Öi¬M,„*ާÝyŠÉ  âp¿\ÕEúµ¹øGR;äq«Œ/räi;4eˆr<>-¼Ó-KY÷à_…;…C®^6»äÀ®Nã(bˆ†1¯V8ÁÄ•üqdp0³s€¸ÐÕÛ.F®1'mª3Ç•c°ãD´l~Œ"2<<3C>?¶ÉbdöËnJh„v8Þ¸ªeTãé\°•8ªÔ'©¦Øá1Aتz×6 ôwÍŠ¬lH<6C>ðC®8««Lz°ÄŽ]qUÏCÓeÅ+RTPbo×yx¾Zá5p^uõ[¤“µwÀy<16>L ðdŒy˜šdº§îÑ™~Ñ&¸D&5ßï××<C397>fMÑ÷? %hAjd­ÑJ#n[å®(Ü#ë”fŽ”Âæ¨0#qŽŽä.Î1üã~ùFÙdèF>öDƪb°ÜZñ­>, ywËá]ð;Àcï<63>Ž2ç|hsa ļK2÷¨RF5®XI|pÒ+u¨Æ½”‰½:aâgùœwÃa‰!ý¬YmîHä»ÓÃ+ƒ/lUUW¢s5qáø© †O†A_ž/¶—¯ÂqKIöÅp#¡ªjü”cøÙ3k0~Ã1 mŒcz`1w2lqâw˜npÑLqå‰Þ@<40>rÆ@N$Ñ‘¸ÅÈÊ=0·ºÚõ¢4n˜ukwÀ
Ôøde·c-Ë!­vÀbÓŸM €žG½šÚۤǘdóYÙÆ˜/ü£s_£"k¨Ê¶â(Iý¦Å¬ZÏ©;Õ»(Üåf=î<>6ŠfåF<E28099>Ú nSè幓â/é§eP?_\»‰ÄH^F4ɩ—ÔÝÜ$a>ä}£ý0A¸Y…OÒ0SIÓÈeè9¡_SmâqV¤ñ<E280B9>¨ˆöÉ7ž ¸#ÃI|Ñ´UxØ<78>í‰Áræ@Ý~#Í
ŒA4÷â¤
0ÅÈŽLfN¯•¦QKÍAÅ9cŒµ·e÷íგ§Æh0]P‰5¸òAzÒÈxB´ÌqOE†Äòsö<73>ðÅÞöÆÚ¡œ)ûÏá…Óë°¯Ãl•?ÌßÓýa ¹?»Ä@ïý©¤ƒ©ÂmRDCk ßöŽùr;ÍfôB<C3B4><42>™X<E284A2>ë‚ÿ åWkš<6B>û~å’:õ>‡Nr. (õÙÊÐé µY½B«—58ø­n.$1'Àgs²ü¢Ñì_V¼Š6iIþf~]ùeLÁ…Ô©ÐlFeGM²3òˆý%ßñžê÷¼b!ë×ßXKvãò8]så<73>bÒ¦[v {ë·¿<C2B7>[\l-ã[UÛ€QLgù«åÍNjV1Û øež3· ÿ 6~¯8<E28099>Øð'·<>ƒFÁ¼)†:~…©ß8H`c_c<5F>èÙþ[j4¿æ" »G±®#qç¿&h
SJ´G~˵Di  ðÌùHø•ã=ãïa±¨F%<25>LkâÛ “ÚþNiöÔkûØ<C398>A`Ur/¯~mkƒ¶scì…Ø ˆÜùÃ\¹'Ô¹mýÎK<C38E>6¸GÊ1â?4Q=çÞií)å iƒý*ñd#¯_×<5F>Tü³…ˆ“Jü=ó6¥{9¬“1úr½Y‰T{äN¦ÎÈ~QOîg×ÿ *ôÝ^¾òôË*Ëý¡ôg Ö|»¨h³´W10
iZaç—¼ï«ès¡IXƧ¥s­[ë¾SóÅ<C3B3>MX%½Õ 2€7>ã$D2ú¿¤®>ñÕcååÑó²ŠâÊžÛ¯(´ËÕ2iqËQUU;Ÿ£9¦¹å=OC¸h牸ƒÖ™<C396>-9¢`Dë˜å/“.>ñL}T×|¨Ôå¬ Mßú³ñß(d†økíôO.9±J Áßaƒ^!ˆ´“”+<}±œiŠ´¸êeR™E銴Ƙå±®0œUØ¥´fYs‰`Ý,®Ç_O&%à Ht²¹¾¯ vËû ó8 7"NÔ…n[çü02še]V<18>ˆHsç↸˜[Äàù78Ñ9 \èO†!é¸èr¹Ì˜<Cã—õ`pÚ|xu¤,lò}¬1¶¶%kLuµ¢Ö§ bUQÄ ‰.§T>˜$ÓúñŸ„<C5B8>†T7ò)ã/Ä=ðâkupv»^<1B>ˆ¤aË àœE÷¢xÇ"sNøÄ<C3B8>"n(ãã€íç15ì÷ÁÓ@³ÇÉ:फ़1 pÎÌO#ܽÚÁ…A ®"ÇÒÜbOm*·|^rv8yunŒ!ˆq †^D žݳC 0Õí(Ë¿Ð0¬¹õ*¸A¶üy¼@xyª<>­1# SÇWÜæ™TR¸ÚR$"RïF〦+¦Æ¸ƒ[µza¶c06 ªR.xÐbY£5©Ã8,ùšuÕŠ¤dœl1üÆ!.v—Ç2ô&¸¸Ôgm™É^žø$GQ„€Ù,X¹˜<C2B9>k± `zÔø†¦ ÿ Ù<>ýÑÿ Ó#rÆW¦Ó-žfÈ?2ÉŸÌpþÄ#é?Ù<>ßÌS¶È~Cúá 2èqá ŽƒMà¿y¿½3“Y¼“¬­òŸ«[ë†ý¶ûðF˜% ¾ˆø/Híñá<>­¡Œz`À 0ëOqNuäÊ~þ89<38>9[‰«™„==<3D>ò¯@XÜßRîe
lV5ßþï<>¼áæ¯3i"H#ž©SBv?†
ü¹ÕÏ(Ñ<>€#þdéAÑäQ±ç›,8£'†@6,:¶à‡×>fºð­SÍšÝì­ë\5{îp[Ë™«êJÍ^µ8+T·1\0÷Âíó<ò‰ÊF<>åÉʈ`6½wÇV<C387>1™‰Ê+Û¤R+=«ˆ™<1D>Y‰>øÓ›$g"(ÈŸyZ
NÙ«S‰ŒQäU^1ƒ¢—lFÞ0i\51N˜ª­À¦hýxw‰ŠŸl30Õq&ˆ×¦#q$$U¢´Ÿ3ë:eÂH“±
zTçXÒüç¡ùžÕlüÁÄMAËéñÎ7éoÓ[++m·<6D>Ëã¨?å=UÊCií¼º=/Xü²Še7ºqÞ‰Ô|ÆC.¼½¨ZËèI&<26>0ÓEó­¦`<60>¸ŽÕÉ”_˜PI;û(æ¸O²å{ûå§ÃɾÒóú%ñFãËí;ÿ <6A>Ö¾®Ü§C'?ò³/~² ŸW¡ÇîÍ<C3AE>/æÃ»êün·/?“ÄŸ58«î1-ÁÌ&kxåÁ8·Šã({bª2EL ÊkƒŸ¦ø€'¡ŠãÁ^<5E>q)˜ª†±“Ó¹<C393>½ñ ´4`|(<28>â‰<C3A2>x!”߯)=AÑ”0û©…¬ä5'Õ²<C395>úìTœ*O,¬:­)ØÀÿ ß-—(å¾SHTÒ˜¤ Å¤<C385>~ÑÆÛ%"U!a^SÓ0yF<0F>#8æHFØÛÌDmÀ„Šñ“í ¥ݺâ-fŽ*¸ìöqÙ‰lœî%4KØå‡¦+,BUùá,°=wûK•@' 4gÁáÔñéEݳDÕ펲¼ôƒýœ:žÙfJaÍŒ1 m„Ø·áÏ<C3A1>4<<¦Št`I—šPûŒ ñ4B n0•ìÖ¬·NàáìRAvœ<76>Š÷Î6XäÀwõC¡c·X™¨Õ§†)žÍB˜zÖÈ7¦"êæÃlÒcÃŒ ÍºD¸OzÌÌBtÃÉ#wøiŒ]/Ô7 3ÁžÏCe ŠY£=ðj^íI´IÙÄ_Eœ~ÎAr§K“™òn-F×ìïZå¨6_ U´¹×ªœu¾™+°ü¬ È$ ó6•¼<07>Á TdŠ)ÞÝÂóÚÇê,"²
Šô'ás°í<C2B0>D1@Ê•øhy±É­€àÝѮ佬CŠÓMeÄôÉzX,‘ò]ü0ÍMœ}9'h*»òb†
˜ðöK@•à7´uýœ—°Ç«„ºÒ^±Ó(í‹-¬Œv\<0E>) ° Ä•ɨÆò
­Øéâ:Jz׳± ¦+u•¹jÐÓámÔgÕO,ü,{Ùª ·•µsm«¥¬;³¸<51>«ÍV©É•Ÿ<>pÏÊÝ9µo1%ʪµjs»ÙëV···št¤Ô_j
Ú™HÇëxÆÏ¹Ý`‡1Œž@æ3iÒÃrç‰ ‘£ùèß7yÜ»ÉUNàŽùÉu¯)ÏbYЏc÷˜ÈºÜ6F\>™0—ZbtÁ—”b­Ô`p»æâ<E2809A>F_
¦˜¼KS<4B> ¾,€
SV<>á4Ã;g$€0ps ᥔi\U3 w  màR¦.ð/lU)ÞØå· ôÃ^Ûe1NÛb¨xª†<C2AA>°VÇ®!ð“×Ò«|SŸÑ}ayõÍŠ±YF"ԦؼãT¸¡r¹˜Ú1;eÔ`„QƸª
àr(pÂF¡ ©Zâ«FØÉ 8âã&¸¥ëC<C3AB>³ HŒU g¶?h|IóœëÅ·W§ÜµµÂ¸é]òCwÈdû2
<EFBFBD>Ÿ|®BªÏPOðäÜ{ú c5éÝŠÐeªqq¥N³0$$wK¢yU·­0CÂòK
½qëq
í<EFBFBD>¦YI<P†èXžhMqƒc•XTíí<C3AD>kr78]sx¡¿w°ÇŽY<C5BD>ÑÂ{ú'æŒ@Ø<mÊ#€mµ0¿háœZ¤ @[ïÆ‹\ñê1 $|Â*B€ýqv<71>dŠà'/C £ÛÝ<C39B> <20>q?†pŸÔ6=ÝP×Z6ê(p!<21>ìk/*S¥0íœIº>šÓëÞ5F6Ï¢uÃ<75>úz„¶°®xN´ÿ (azRŽHA n˜ê×<C3AA>5c¨ùaÛ£l±iòoŠ|¸òdËw†ää 0ž)u&øj~d QôË©þ)d'4œ J²å ynÝΰÇà·͉[jWÇ©ñ/q ÒBˆr8%4±]— ¥“K˜ߙ扆KyÔTnp\6¨»<C2A8>O|žSاc5'bÙ{‹®Ë!uŽF<C5BD>ØœiS+95(Û÷kpSäµ'éä1msȺ¦Ÿ7˜t
è=[«
mOÚxHéþ¯Ýá„HðÃ%¤lB8 ê:¦ çFü¶œK –ïñ+£)SÜÓ3±FÓïÎïw]šqæÃàÑcÖÍîOn·0¯ ]ÔŽP,ƒ®HuÈ!ÑõéôÉþœúÒuøu¯Ëáòk:T×v7|ï#^qÀPp:¯.F‡Ã*ɦÉU\zH~§ i5',¯ð“d½w<ñ!§m±æ¦à0ú¬°†D*èJ²°¡ue}S(·çÜÙª@¥²×áZ`¤·UÜá<C39C>†ž÷·QÙÀUe<55>Ðs4Ç+Í6RùVSáY$vŽ¥kìX ˜ÇE~A1†|±â„%(Ý_š[us¤%ÞŠ<41><C3A3>KPúc¿ÁZSÕ¥¾s½°À1š2Ÿq„7ÕÞv~€a&As?ì^÷ùM§®—¢^kŠpŒñ'ÄŠd^Óͯo¬ÝMÏíJHÉ·—Íùqt©±P+OÎ1t¼—s^G6È1q,q\Uî·Òžb»U†r$ µ|æ­ÛR±úÕ²Ž,+AØçÑuG¶•±Û;o•|Ùm-°¶º`êÀ ÁÂ(fÃñ ¿á“Ä<ÁåÛˆ&rÆ™ "j8¦z¿Vò¾<C3B2>«ÀÓZQª*W¾q<C2BE>5y>KYªl+ÛcÇœ ŽáæÂ”ß,œZâÒH« KŽÙ<C5BD>8˜
Ý`&¸¼[°…)@hÃ"”úÕ 8Yk!Øa”U'|U6¶bS9 rÀò@0KJ¥1V¹òÀ“c˜Ç϶*‡QS\Ò_
qÛ7Õ<37>kŠ¥¾<C2A5>å ý1Ë<31>¶lU„\U†Ø ¹W5'ku­H
…ÅhsqQˆÈÁwª¼üp¾cLQ¥'®"渪ƒ18à*1¥wÛŠ2Ø¥K<C2A5>sN X(w¿¤¼qT §‡:eò2}VãìþËx4í<>•;`"Ú³áŽhpËn ÷L`(A4*z0èqÓGûª©ß íµKqÄükàpÆ Z ÈŠXÂrÚ«¶@ĺŒºmF3f<B=Gw¹.‘š´Äž£¾ ¾„Á%:×u>ÇÇ®!ÍÂD¢$*”Ù©ˆ°-‚Ä$ŒaŽ<61>²VßÄrAúdwÇ/1½pAQŒã\m³Š×%Ì©Ðà˜õY”Püð' µ<>}ñÙ®XñKêˆ)”z¢·ÛZ÷Šäq—è8J-IÞ˜õµ»dH\yG‡í ¾Þ.ô>ø·ÕCvÈÅÍ Ó‘¦}Qà¦äý9GU—K<E28094>N±ÈèZ*îqÆ3Ða*k<><6B>ÄH8&=R'?o‰iµy*7(©ñÅ#‰Óâð¹uþ|FïYXÐ*nͰÈðŸËæáŸ2áýi€ðNÞø´#â¯a€,ÞIâzá¬P•?v2Ûde¨à‚¸<³1í˽{мUåAË!Þc½[xL*hÍ×
ü·«5<EFBFBD>Ò5{æ~‹”á-¸<>ßdb<64>Ç,Çn*Ü<1E>ùŧ•Ôô­JÜ|FËQìy<C3AC>øI,îåeHœìEÁZq²ó<1D>Íx=kJúF'åÛeÑõ_A‡ƒ3 ±ª.ÜýB]_Ì%¶´ºàF<04>ØlOÏ9ußcލñ ë_™vH²H7qžyÔ £jæ>|äÔ<>Òáäìí6lÇ&HïåµûÓ{6^ZêÞ†§<13>Κoæ_—€IÄ”íVÛõçš0ûËþiÔt •í¤* Ô€p`Ê ‘᯦]ÞGÉÌŽc<>8€;/Ì^KÕt)Ýe…¸w¦F÷SB(GlôGüï£ùÚÜišÚ'®ËÅg T˜>B¹Ðnžâånß°èFO& w€©s¡¼d;âÈK¿—ã8ü©Ö-uM&/NB™Škû]³ùÏB—BÕæ<C395><C3A6>…äi…W׿Ðu(îP<C3AE>…Fwmó+Fõáâº'Ä»U¨?^NãžR®/èÌu÷4AÛ§/0ð¸)B0ëMÖç¶`Uˆ¦Ö´‰t{ù,åꄌ
¶Ù<EFBFBD>™0LÇ»˜èÈ<C3A8>!okò<6B><C3B2>]]GûÎOµk=sLk¸<6B>s§ÄëÏ8é7ŠÀô9Ý|<7C>­Å5¸·<C2B8>¾H9•"'Äs FÇ„¼_ÍzKZÜ¿ÃMÎDŠÒµÏCy×Ê^°yÑy+nÎ'¬i2YÊÀ­)Ï<E28098>ÅˆË È…‰á<% e±ð§Ä1Á7ßD€°¦`6&±<>ŽF´ ÷ÀVÊB<C38A>°zì1J.ß®ô<>ÀVÌ+†±(plUGêõÇöÛaŠ©*T.eøzãÊ :b¨?¼¯lØ;êý©¾lU„´`tÄ¥*3<ê®äW®([+•8IsMÈW3â•å±¼«<C2BC>ãA\ # Àè‡B<>Jâ«<11>R\P¡-L
È0\´±U*¾%êq[Àâ²l0#uÅH±]ìšxÅͼw ½Eæ0¿<30>®ãaŠh·èµœü °'¶ ¹¶ô÷ìz0èFVv4éîX2K »ý'¼ UkòÆÈ b¬xì1Éaɰ2£ÄM B{ezg Þ˜Œƒ¸Ãm£P
Ò8匃SÖmñâ^˜Ú¡h‰"€z÷Á¾ŒJ*vÂôW<C3B4>ìâ73ËГLÇ,“ôÊ<C3B4>D;ĬB§ÌÜ›rpk\]*ãrF¥v±<76>f<EFBFBD><18>±JoŽCC<43>²3<5Í
<EFBFBD>N):%c'LJ“?¶]Ë<>UžâãQ3çÐòe:\`ÇϵK|©¦Újº²ÛÝ·ÀªXF?i©ðƒí^¹‡Rú¥‰”öØaÿ åeôÚŸ™RHPÛ žŸÉ<>™rˆ'äâi42˪ñ2Dpàõî ÖK=rxeäh2-­O¿7¥Wó P|G¦s°s+9áËÄ6$ñ§¦ÇoMò—œIâÜ·3²Þúz…½—˜,Eb¹E”ñìHø‡ßžRI]zì_“þxe”ySS¬¶÷ýT<C3BD>ø·òý9`Î$A¤9ÿ HuG mÑê·ö<C2B7>yÀ(5VŒ½öÎ+æß'IlîÊoÛ;LÙIõÍ=ýH ¯Ãür¯,íuËvVP³Óâ_·ÄE}P=;<3B>A>D>M»³ÙʰÀÔΩç_,¥‹¹ œÆX¸9_˜úœ8}2e ^ǘVÓon,.Rh©tÏCŸÌŸ—O$ÇÔžöŽæ„gœ•sºþRjjm΃9þù
¨?ÍÛ%§™á#ù„H{¿‰f7÷ì𻨌W2'ò±ÎÓù#žá‰!#b+ìsÿ 8èRišä°q g }ù×¼™jž[òUÎ¥ ã$©Á ë¸ß'f2ËÝ*Œ|ø·A•ˆùn~$óÌæo0\µhþ¼"‰‰Ûj×F÷Pšv5,Çõâ1.cj%ye] |™Gé <09>´œiN¹+е™l¥B<18>\‰[\5€PƒÁ˜ã4w‰æQ±æ÷ý^µÕí¥Ñ¢€žÇ#sòj•y£Z©Ü0ÈN<C388>«Ëg:jo<6A>ŸL¸MoFh\ò<>-V¿Žeá‘’Ó.a‡Õé<ÃæMKO6“²J
ÕÎ<EFBFBD>çM
H®$eZ
œ€IÄÔ¦SªÅDdˆÚI„º‰´´Á±¨ë„ÖìÕ â ÌFÄBÈPaÅr &UaµžÀU7@:cŒ*wÆDãl]™iPqJ è•FÙr°ÜàF˜.õÅ <09> ¾º8Ò¹±W—É1•À­%qIØÖ¸ª§©]±­¾4cÂ×r×Á<Ec¦ø>Ñ aŠ«%µm¢q¦ UR¢¸Ò«]±T9ޏÓQˆÎÀ)ÅRù¶8ƒ0*ihNø¥®*ºG <0A>Øe—Æ“\Uhb¦ ÐŒ“hׯÂsñ£cØäf˜µ´Ïo2L†Œ¦¸b"d8¹^þæ<C3BE>Fž9¡Âv#xž ³MsËz†‡$-yô®œ3.èþ>Ø=Å3®yRêÓÎÞU“K¼ç<> Æ[~ƒ|æwÖ gw-ªÈ9FiÄŸãŽlÆIæçäy:½F3Š®ÌO#çÜP|+üN7Ñì>œ¶ïà>ñŠ­»oðôùeÌ"X}±U‡ò<>úm‰Ür€ E+ÓaâñÊÐ(÷Àr N .[Bp·@˜ïiL<69>ÐãíRaƒŸUÇÖÖÊ5õ…ú{ m¾Z; ùT+PŒL ðmükE=Ñ…TàšbËâˆ?Q(M†<04>É”Uwa\Jà~ðb± 'ï0œòÒZ½<5A>ðÉ<C3B0>ä•?MžD
—Ó@©¨ Î¡ùA凰<E280A1>µ»š¢ ªûž¹—¤²K§ ɳCË$Û-çÿ š€ÿ ˆ§¯ó’Ï>ëk:ÜÓÀ(G¸9é?½>@<0F>9ðú\£&¿•ñWÍóñË1?ê#7ðÈX;äûòé>¯kæ \ÿ Ç­ˆ§ü©iüm<C3BC>Â.cÜS.LóÊzÖ[gz­x°;ŠgD¹<44>Rxuoîe¡ t¸Ï1h·ímt¬zç¢<™©®«¦9­Æ©ó—.Xò;I€»á?Ïúxž•EC
<EFBFBD>§8§mé\0§|õ·mõ<6D>5•‡Ä•Sž}óE<C3B3>¡tûw99G<39>NGX¢ê~ö(«“ïÊïXy…i<E280A6>Jrkùq©E¦ë°É(¨,:æ&›ûÏ|HûÏ—Ä2?ÍÇCæ%
!…ióÉ]ïúGå¬gùkú†þkhÌÚŒ:¤d´W]Omð÷S#Oü¹†3±§îš>œgÎ5𵀺RVùœZ6U8œ¦²1ñ'½s[/¨ûËpä™G*-)ƒâŸáÂhÁ;àøZ´"”ʈ<>5zgMò_™>¬é6Ý7Îa¨ÁwÆÖ`Êԡ̽>@AÅ>G“\ÁúƒÝõý
rÓë6ª añØç(Ö<—< Ç<> öÉ&ƒç¦· >Ãn¹>·¸´ó<>Š%ãPG|ºçˆT‡;Ñ´·Î2Øi
B1h‰-óV%µÃñS×!SÈКŽQ¨ÃGŽÒ{™B]4_­Ä† d`OÈÖ¸ikrùŠÍ’, ¸Ù.¨(×óßL$œ²õÂÉ®Î_®B€®°ÅWýd×®l QÇß6*Å^½1;à†¥1"qU=<3D>ǧ\N»âˆ1TR @ÁvôäSLZ'£W'¾  Hp<Rc™˜
â—K!M뀦¹¨¦:wf_!5Å
RµMqØ£œH⫎•\UR¹cs‰W\Uï?“±z:UõÇòÂ߈¦rÿ 1NÏ­\¸$|gõçSü¡nZ ŸñK~¬äÞcVM^çj|g¯Ï3³ÿ w?ó>TÔ $¿4¼¸8¬z…Èý³<C3BD>£PÝpTq(ß0()ÁˆóÇ%5;±ÕއQõ§qñ)ü0P|±<>â11 SÑiå8O|v!<X÷‰ÃhqU¶#¨üF%Ô‘š9SßLFÎFG€÷¸3ìÌ·éÊó§³Ü[Z!28$tQMOQk¹¹ ”t”ó<Ÿi‰À‡<08>9Z]pž99÷ôæi¦ÙÜêz$·0¡qf9ÉAR££©Â‚$d¯ò¯_ŠÊù´ë Úä¥CÑÇæk:\ºn©u`õ¬2iAø_äËCgÆ"!(ò”Ùm91 R4v'ÝÕ-SCŠI)œŽÝ 1¤Šß„“9­2˜ÄÊB1Ü“MÉëâ‘è§<>¼¦5Åîë÷vì=Iì’NNSÌšdú—ø_F¢ÅµÃ+òŠ‚~üˆü·ªþíçßmºíÎEä]hXyÎÎúéɉ¤1ÈOò¿Ã(Dc€„·õÇ»‹¿ô; >/9ŸT<C5B8>™c7<63>ºÝÎí —θ<C38E>¦rWçMMÌwÖ®*­!&
ÈùM¶ÌL§!ç÷¹@ìahs¢~_¯«åß5Ããfîu9ÏÜS:'“Ð|‡æ}LìeT¶Cþ±©ýYfë÷Ò>L 8=)<29>`6³F9wǹ£$ÞU½hîPW¡vŽBFX¥È<C2A5>˜äêOÝG¡§½Å¸ÝÅ]GŽp<C5BD>:i3¬îÅO|ì~\¾—ôwª¦¼@${c5½3NÖôù®Ñ8ÉøÖ›|ÆY xfP<66>¸“V8¨Žo¦<E28093>£j6Ø"Âv·<76>&SB¤6ó=ˆ·ºp¢€„P‡f¢¬z ÅÉ<03>(à÷†@Üwø¾†Ò&¶ó¯•Ùˆk»1Ér\&üÉÔc²òõއtŒrç+ò×I¿Ñ¬$Ö®Ó<>Hn‡Û |áåË?8é<38>©é.  ¬‘ºÿ ffò•rÇtrHr,9<>Æà>f ”ªÄâ÷vRZ\½¼Ÿi dŽƒ|ÖJ&21—1Í´Ü*ÀÀlzà„~-€IâqU—mð*cõ<63>3Þ»àŸõ¼*A}$r ö®uO#ù<>ƒm³Œ 6ë¾ µÔ§µ`ÊÄS2ðf‰S±äK Dß_O]Xižb€É+
•ñùg5ó'e<E28098><65>Ý…\óĶÅCÈ~üêÚOšôý^%†ïÔS—|¾²bÞ>¸1¸ËžÅóõî<C3B5>qhä2L <OCž€×¼<C397>k}\Z êwÛøç!×ôS¦ÌÀ¯C•Ï< ËÒÂA1Ú\<5C>0ËQ¾*G.˜7.9`<60>¶m]éµvÄ包ÁˆÂ•ÀóA¦*€á¾lSÖ´ÍŠ±/ž$Ø[GwÅ sb©¸Åâ§s„9±Vaj<12>qg ã<>œØ¡”MƇŽÉ×
sb”sâGfÅUÎlC6*®1ë×l {gäüúäw$[Û¬©ûãÉñï^L;`/Ìè4ª»é÷Jd&¯WØøW<C3B8>3<EFBFBD><66>úMÿ ©‹âÿ }_e4õÿ ;§èO<C3A8>
õÛ§LfÍ{k(p)¹ÄO.Ý2;O¾*ôÄ宿Å(é+\KfÅSm%¯þ#d¥æ 8¨4¯ßž‰Ñ®}}1aó<61>Ìc<1E>˼ 8^ÇŠ3IÇèÏ0f̼_Üÿ <20>×ýçF^Ã˯/‹Ô<å ¼8Ðn>³Nꬻ³ òúÛËòjˆúýèŠ`AºHj|*<14>Ç9~l˜þüý7ÁÓëý\N.š½Uàóÿ 'Š} ù©s®¶<C2AE>YÙpÑÂÒUã*G<>WðÎ!m_Y=?ï94ëZáNl†~X¹õåïÿ uÞçGøžÃù†UôíM@õŸ«º«pݳ¨Ý}òðS®G3eZ<65>ï> ‡$ânë“í+ÿ %>±^Ÿ\<>ÜÙÊsaÓó—õHYt÷£°ß@¯ÖÖž#õäk6'÷ÑYý%õ¿”iú!ù¾ûàÝ·¾YE âjç§¶xï6_“ü§¼1<C2BC>ðûžµçX´ñu!jvâßÓy"×Ë2jQ<6A>^ùa¶Rÿ À©̳eŸÅW_çÿ ›Ñ‡¿áñ}+çû<C3A7>oô*Å¡YÑ QgG<67>r¡¹~WùBú¯­:ÝÄÆÐ£z®ÄP-74®yû6?ä¥Î¨ÿ Wã׉?Ä?/'£yÝ,“]¸úœEäz?Xs/aüÙ‡©þôû‡ÝÕœ>”íÓqŒ¬žO(f1 `åá…™±Tæ2þ¡-N˜EO¡7!‡¤ É~<7E>.¾®¾„Þ<12>Ös™fÍŽø<C3B8>ã\-9>¥õß“¦×YG×­Ùb§ÅVR)ô6F¿0#ÒùÈVaË}¨ßÓ<Ó› ?¿<¹/ŠŸ£õ³Ù9+ä¾=?^Zëör0sy.\ú6Ç<36>zdf~;®ßF8Wö…3˜æÊÒõ ©9~lUÿÙ

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,13 @@
ÿØÿî Adobe d€ ÿÛ „   

     #"""#'''''''''' 
   !! !!''''''''''ÿÀ  5 )" ÿÄ¢   
   
 s !1AQa"q<>2¡±B#ÁRÑá3bð$rñ%C4S¢²csÂ5D'“£³6TdtÃÒâ
„”EF¤´VÓU(òãóÄÔäôeu…•¥µÅÕåõfv†¦¶ÆÖæö7GWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø)9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêú m !1AQa"q<>2¡±ðÁÑá#BRbrñ3$4CS%¢c²ÂsÒ5âDƒT“
&6E'dtU7ò£³Ã()Óã󄔤´ÄÔäôeu…•¥µÅÕåõFVfv†¦¶ÆÖæöGWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêúÿÚ   ? áÚF“}­êiš|f[™Ø*/aâÄö¾z#@òO—¼·e žÝ.æ0g½Çê´"Zxô¨È?åF<C3A5><C2A7>|ˆVoB¦b¡Õ”òT*ƒ¸Éǘtëûý$ 2x£Ä$6Å<E28093>&b*¥˜u>Ùƒ©Èrê±haªQ1cûÒr}8¡ÜH¢OŸÎ¹“Y2œ2È#.ØýÖßTæzÑÚ)åí/Pº<50>W»Ò­Z´6ñ¢Ñ¨ÄêŠøEI«yʾcYcÙlî<6C>8ýk~*êA¦ñ<C2A6>E;í„·^aóG—­m,/­OÖŠñ¶H<C2B6>8<EFBFBD>.Óo¿Žo/\yŽûV“U¿±fH˜
¬<EFBFBD>èG­އ{3´à3f=¡<11>O凉âC(¿Drqú}\®îÜÌà:UþòUÃ!üã<C3BC>‡}¹Õ<SÌ~_¾òέ6•~œdŒÕ³¡û.¾Ç
sÐ?šº$—”P£5îŸ"˜åž©‰Ër‰¸<E280B0>èÝ3ÏÙgçcù_Ípo|ÿ âààâîâëÜåVü[>¿ƒ‡<E280A1>‡ÝÓ½ôo/´Í/ÊZ}œÓGª9˜wi¥M”w®Ù –êøÜÛ/ªBKÖu R8ú-EG":áEŸt/¨Ùꙣ•-âw•_<E280A2>> ví„w>b½ž ,^éî! óQ—FÙ¯ÇÙíNÒÔeÓÇ æ9¥<39>pÔw<C394>|˜(ÏGŠ#,å<dã<02>ª_Å;÷윧™&¹¼†þÚËž<C38B>nÖu¨
$¨«Èb)Û'<27>5m>itË“]¨hî#$¨Ú¿Æ™°óÔlßG¹<47>dK°ÍlªS¡ý¾¿O3ɧéGD»[ÈÁà¼èHëí™úþÂÏ©†A¦ÃàKKš8°ÄO÷sÓ}w(<28>¬Hñ8šÑðr‰ÊG/NY\}qÌ=;wì OõÝ=­ü©©Û½Ì·­" _Q½ONE Ÿ‹²øçÿ
Ÿùo·ÿ ƒÏAØ-æ£f`ÔdHEÂpX­ÀƒUy7.GâùÊ¿åY/ùðKšAâÒ ÃÆ†¬cã†òDÊ;Õs<C395>÷mùÂ2<C382>Q„½Pc.ø` eèÿ :&ÞŸ¢IkæO/Ú\\8ºˆÇÁ£CÄ5Ú ùd6/&]ß”Wœ­âW"BåY˜¯*m<6D>ÉÝ~ 3P¹Óð ìA"£·ígcuÒêKÛ©coQRYË© ë™íV»³²äæÉ!V|\Qæ!33Çû?"lzM>{9ˆ‰Ó<E280B0>YåÇA>ïÐ^TÖˆm­:ÏC<‡ì:7„Rµ£m¾N5O+Û\iöúP$ê/9§cð€´æõ¥íˆZÜØùX r1¢FäÑ) =iÞ™-¶<>/”\GR¼—÷ÊU€Ø·ß—ö¯löŽ—S¥,s†Ë&/à3œxb%__Râè;?lYòãœ1ÉU â°ÒHBXzº6<C2BA>'×ç3 ¤”ÏPv‰Íhh3ƒÿ ÊÁÔß<7F>÷ä¿óCÏ6Éb¾_Ñåy êß[˜
'vˆt­3Œd† hÀuƒ?7,ãTpðmp‰„cÁüÿ Q<>~—'‡ xFRðD|!’÷à'ˆËú»û¼ZwÖ¾½oõ:ýcÔ_K<5F>kÊ»}<7D>ó°ù<C2B0>þV‡øb?­ˆ½?é<ÞŸöYijfÛ~,5áÿ yüuÇÿ Z÷þSô0Ë[ÝrëìëøKÕÿ -ÿ KV/«ˆÍÇ«'§ë>{©ôäßÎgÎßSoª,?Tâ}o©—çOÚçÈW<C388><3Î9²½Oç°‰qpCëâüýµÿ ÿ 7«N/«/Õõÿ Ö­ÐþóñIޝê}q¹P.Ü8Ö”ÿ e€1¹²Ã×ôÿ k³ßÇé|^\?«‡ô?ÿÙ

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,13 @@
ÿØÿî Adobe d€ ÿÛ „   

     #"""#'''''''''' 
   !! !!''''''''''ÿÀ  5 )" ÿÄ¢   
   
 s !1AQa"q<>2¡±B#ÁRÑá3bð$rñ%C4S¢²csÂ5D'“£³6TdtÃÒâ
„”EF¤´VÓU(òãóÄÔäôeu…•¥µÅÕåõfv†¦¶ÆÖæö7GWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø)9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêú m !1AQa"q<>2¡±ðÁÑá#BRbrñ3$4CS%¢c²ÂsÒ5âDƒT“
&6E'dtU7ò£³Ã()Óã󄔤´ÄÔäôeu…•¥µÅÕåõFVfv†¦¶ÆÖæöGWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêúÿÚ   ? áÚF“}­êiš|f[™Ø*/aâÄö¾z#@òO—¼·e žÝ.æ0g½Çê´"Zxô¨È?åF<C3A5><C2A7>|ˆVoB¦b¡Õ”òT*ƒ¸Éǘtëûý$ 2x£Ä$6Å<E28093>&b*¥˜u>Ùƒ©Èrê±haªQ1cûÒr}8¡ÜH¢OŸÎ¹“Y2œ2È#.ØýÖßTæzÑÚ)åí/Pº<50>W»Ò­Z´6ñ¢Ñ¨ÄêŠøEI«yʾcYcÙlî<6C>8ýk~*êA¦ñ<C2A6>E;í„·^aóG—­m,/­OÖŠñ¶H<C2B6>8<EFBFBD>.Óo¿Žo/\yŽûV“U¿±fH˜
¬<EFBFBD>èG­އ{3´à3f=¡<11>O凉âC(¿Drqú}\®îÜÌà:UþòUÃ!üã<C3BC>‡}¹Õ<SÌ~_¾òέ6•~œdŒÕ³¡û.¾Ç
sÐ?šº$—”P£5îŸ"˜åž©‰Ër‰¸<E280B0>èÝ3ÏÙgçcù_Ípo|ÿ âààâîâëÜåVü[>¿ƒ‡<E280A1>‡ÝÓ½ôo/´Í/ÊZ}œÓGª9˜wi¥M”w®Ù –êøÜÛ/ªBKÖu R8ú-EG":áEŸt/¨Ùꙣ•-âw•_<E280A2>> ví„w>b½ž ,^éî! óQ—FÙ¯ÇÙíNÒÔeÓÇ æ9¥<39>pÔw<C394>|˜(ÏGŠ#,å<dã<02>ª_Å;÷윧™&¹¼†þÚËž<C38B>nÖu¨
$¨«Èb)Û'<27>5m>itË“]¨hî#$¨Ú¿Æ™°óÔlßG¹<47>dK°ÍlªS¡ý¾¿O3ɧéGD»[ÈÁà¼èHëí™úþÂÏ©†A¦ÃàKKš8°ÄO÷sÓ}w(<28>¬Hñ8šÑðr‰ÊG/NY\}qÌ=;wì OõÝ=­ü©©Û½Ì·­" _Q½ONE Ÿ‹²øçÿ
Ÿùo·ÿ ƒÏAØ-æ£f`ÔdHEÂpX­ÀƒUy7.GâùÊ¿åY/ùðKšAâÒ ÃÆ†¬cã†òDÊ;Õs<C395>÷mùÂ2<C382>Q„½Pc.ø` eèÿ :&ÞŸ¢IkæO/Ú\\8ºˆÇÁ£CÄ5Ú ùd6/&]ß”Wœ­âW"BåY˜¯*m<6D>ÉÝ~ 3P¹Óð ìA"£·ígcuÒêKÛ©coQRYË© ë™íV»³²äæÉ!V|\Qæ!33Çû?"lzM>{9ˆ‰Ó<E280B0>YåÇA>ïÐ^TÖˆm­:ÏC<‡ì:7„Rµ£m¾N5O+Û\iöúP$ê/9§cð€´æõ¥íˆZÜØùX r1¢FäÑ) =iÞ™-¶<>/”\GR¼—÷ÊU€Ø·ß—ö¯löŽ—S¥,s†Ë&/à3œxb%__Râè;?lYòãœ1ÉU â°ÒHBXzº6<C2BA>'×ç3 ¤”ÏPv‰Íhh3ƒÿ ÊÁÔß<7F>÷ä¿óCÏ6Éb¾_Ñåy êß[˜
'vˆt­3Œd† hÀuƒ?7,ãTpðmp‰„cÁüÿ Q<>~—'‡ xFRðD|!’÷à'ˆËú»û¼ZwÖ¾½oõ:ýcÔ_K<5F>kÊ»}<7D>ó°ù<C2B0>þV‡øb?­ˆ½?é<ÞŸöYijfÛ~,5áÿ yüuÇÿ Z÷þSô0Ë[ÝrëìëøKÕÿ -ÿ KV/«ˆÍÇ«'§ë>{©ôäßÎgÎßSoª,?Tâ}o©—çOÚçÈW<C388><3Î9²½Oç°‰qpCëâüýµÿ ÿ 7«N/«/Õõÿ Ö­ÐþóñIޝê}q¹P.Ü8Ö”ÿ e€1¹²Ã×ôÿ k³ßÇé|^\?«‡ô?ÿÙ

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -174,10 +174,57 @@ public abstract class AbstractEntry implements Entry {
AbstractEntry other = (AbstractEntry) pOther;
return identifier.equals(other.identifier) && (
value == null && other.value == null || value != null && value.equals(other.value)
value == null && other.value == null || value != null && valueEquals(other)
);
}
private boolean valueEquals(final AbstractEntry other) {
return value.getClass().isArray() ? arrayEquals(value, other.value) : value.equals(other.value);
}
static boolean arrayEquals(final Object thisArray, final Object otherArray) {
// TODO: This is likely a utility method, and should be extracted
if (thisArray == otherArray) {
return true;
}
if (otherArray == null || thisArray == null || thisArray.getClass() != otherArray.getClass()) {
return false;
}
Class<?> componentType = thisArray.getClass().getComponentType();
if (componentType.isPrimitive()) {
if (thisArray instanceof byte[]) {
return Arrays.equals((byte[]) thisArray, (byte[]) otherArray);
}
if (thisArray instanceof char[]) {
return Arrays.equals((char[]) thisArray, (char[]) otherArray);
}
if (thisArray instanceof short[]) {
return Arrays.equals((short[]) thisArray, (short[]) otherArray);
}
if (thisArray instanceof int[]) {
return Arrays.equals((int[]) thisArray, (int[]) otherArray);
}
if (thisArray instanceof long[]) {
return Arrays.equals((long[]) thisArray, (long[]) otherArray);
}
if (thisArray instanceof boolean[]) {
return Arrays.equals((boolean[]) thisArray, (boolean[]) otherArray);
}
if (thisArray instanceof float[]) {
return Arrays.equals((float[]) thisArray, (float[]) otherArray);
}
if (thisArray instanceof double[]) {
return Arrays.equals((double[]) thisArray, (double[]) otherArray);
}
throw new AssertionError("Unsupported type:" + componentType);
}
return Arrays.equals((Object[]) thisArray, (Object[]) otherArray);
}
@Override
public String toString() {
String name = getFieldName();

View File

@ -38,6 +38,7 @@ import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteOrder;
@ -52,6 +53,9 @@ import java.util.*;
* @version $Id: EXIFReader.java,v 1.0 Nov 13, 2009 5:42:51 PM haraldk Exp$
*/
public final class EXIFReader extends MetadataReader {
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.metadata.exif.debug"));
static final Collection<Integer> KNOWN_IFDS = Collections.unmodifiableCollection(Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD, TIFF.TAG_SUB_IFD));
@Override
@ -80,16 +84,25 @@ public final class EXIFReader extends MetadataReader {
long directoryOffset = input.readUnsignedInt();
return readDirectory(input, directoryOffset);
return readDirectory(input, directoryOffset, true);
}
public Directory readDirectory(final ImageInputStream pInput, final long pOffset) throws IOException {
// TODO: Consider re-writing so that the linked IFD parsing is done externally to the method
protected Directory readDirectory(final ImageInputStream pInput, final long pOffset, final boolean readLinked) throws IOException {
List<IFD> ifds = new ArrayList<IFD>();
List<Entry> entries = new ArrayList<Entry>();
pInput.seek(pOffset);
long nextOffset = -1;
int entryCount = pInput.readUnsignedShort();
int entryCount;
try {
entryCount = pInput.readUnsignedShort();
}
catch (EOFException e) {
// Treat EOF here as empty Sub-IFD
entryCount = 0;
}
for (int i = 0; i < entryCount; i++) {
EXIFEntry entry = readEntry(pInput);
@ -104,27 +117,24 @@ public final class EXIFReader extends MetadataReader {
entries.add(entry);
}
if (nextOffset == -1) {
nextOffset = pInput.readUnsignedInt();
}
if (readLinked) {
if (nextOffset == -1) {
nextOffset = pInput.readUnsignedInt();
}
// Read linked IFDs
if (nextOffset != 0) {
CompoundDirectory next = (CompoundDirectory) readDirectory(pInput, nextOffset);
for (int i = 0; i < next.directoryCount(); i++) {
ifds.add((IFD) next.getDirectory(i));
// Read linked IFDs
if (nextOffset != 0) {
CompoundDirectory next = (CompoundDirectory) readDirectory(pInput, nextOffset, true);
for (int i = 0; i < next.directoryCount(); i++) {
ifds.add((IFD) next.getDirectory(i));
}
}
}
// TODO: Make what sub-IFDs to parse optional? Or leave this to client code? At least skip the non-TIFF data?
// TODO: Put it in the constructor?
// TODO: Consider leaving to client code what sub-IFDs to parse (but always parse TAG_SUB_IFD).
readSubdirectories(pInput, entries,
Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD, TIFF.TAG_SUB_IFD
// , TIFF.TAG_IPTC, TIFF.TAG_XMP
// , TIFF.TAG_ICC_PROFILE
// , TIFF.TAG_PHOTOSHOP
// ,TIFF.TAG_MODI_OLE_PROPERTY_SET
)
Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD, TIFF.TAG_SUB_IFD)
);
ifds.add(0, new IFD(entries));
@ -149,7 +159,7 @@ public final class EXIFReader extends MetadataReader {
List<IFD> subIFDs = new ArrayList<IFD>(pointerOffsets.length);
for (long pointerOffset : pointerOffsets) {
CompoundDirectory subDirectory = (CompoundDirectory) readDirectory(input, pointerOffset);
CompoundDirectory subDirectory = (CompoundDirectory) readDirectory(input, pointerOffset, false);
for (int j = 0; j < subDirectory.directoryCount(); j++) {
subIFDs.add((IFD) subDirectory.getDirectory(j));
@ -221,20 +231,24 @@ public final class EXIFReader extends MetadataReader {
// Invalid tag, this is just for debugging
long offset = pInput.getStreamPosition() - 8l;
System.err.printf("Bad EXIF");
System.err.println("tagId: " + tagId + (tagId <= 0 ? " (INVALID)" : ""));
System.err.println("type: " + type + " (INVALID)");
System.err.println("count: " + count);
if (DEBUG) {
System.err.printf("Bad EXIF data @%08x\n", pInput.getStreamPosition());
System.err.println("tagId: " + tagId + (tagId <= 0 ? " (INVALID)" : ""));
System.err.println("type: " + type + " (INVALID)");
System.err.println("count: " + count);
}
pInput.mark();
pInput.seek(offset);
try {
byte[] bytes = new byte[8 + Math.max(20, count)];
byte[] bytes = new byte[8 + Math.min(120, Math.max(24, count))];
int len = pInput.read(bytes);
System.err.print(HexDump.dump(offset, bytes, 0, len));
System.err.println(len < count ? "[...]" : "");
if (DEBUG) {
System.err.print(HexDump.dump(offset, bytes, 0, len));
System.err.println(len < count ? "[...]" : "");
}
}
finally {
pInput.reset();
@ -276,6 +290,8 @@ public final class EXIFReader extends MetadataReader {
private static Object readValue(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
// TODO: Review value "widening" for the unsigned types. Right now it's inconsistent. Should we leave it to client code?
// TODO: New strategy: Leave data as is, instead perform the widening in EXIFEntry.getValue.
// TODO: Add getValueByte/getValueUnsignedByte/getValueShort/getValueUnsignedShort/getValueInt/etc... in API.
long pos = pInput.getStreamPosition();
@ -461,7 +477,7 @@ public final class EXIFReader extends MetadataReader {
Directory directory;
if (args.length > 1) {
directory = reader.readDirectory(stream, pos);
directory = reader.readDirectory(stream, pos, false);
}
else {
directory = reader.read(stream);

View File

@ -0,0 +1,412 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.metadata.exif;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.*;
/**
* EXIFWriter
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFWriter.java,v 1.0 17.07.13 10:20 haraldk Exp$
*/
public class EXIFWriter {
static final int WORD_LENGTH = 2;
static final int LONGWORD_LENGTH = 4;
static final int ENTRY_LENGTH = 12;
public boolean write(final Collection<Entry> entries, final ImageOutputStream stream) throws IOException {
return write(new IFD(entries), stream);
}
public boolean write(final Directory directory, final ImageOutputStream stream) throws IOException {
Validate.notNull(directory);
Validate.notNull(stream);
// TODO: Should probably validate that the directory contains only valid TIFF entries...
// the writer will crash on non-Integer ids and unsupported types
// TODO: Implement the above validation in IFD constructor?
writeTIFFHeader(stream);
if (directory instanceof CompoundDirectory) {
CompoundDirectory compoundDirectory = (CompoundDirectory) directory;
for (int i = 0; i < compoundDirectory.directoryCount(); i++) {
writeIFD(compoundDirectory.getDirectory(i), stream, false);
}
}
else {
writeIFD(directory, stream, false);
}
// Offset to next IFD (EOF)
stream.writeInt(0);
return true;
}
public void writeTIFFHeader(final ImageOutputStream stream) throws IOException {
// Header
ByteOrder byteOrder = stream.getByteOrder();
stream.writeShort(byteOrder == ByteOrder.BIG_ENDIAN ? TIFF.BYTE_ORDER_MARK_BIG_ENDIAN : TIFF.BYTE_ORDER_MARK_LITTLE_ENDIAN);
stream.writeShort(42);
}
public long writeIFD(final Collection<Entry> entries, ImageOutputStream stream) throws IOException {
return writeIFD(new IFD(entries), stream, false);
}
private long writeIFD(final Directory original, ImageOutputStream stream, boolean isSubIFD) throws IOException {
// TIFF spec says tags should be in increasing order, enforce that when writing
Directory ordered = ensureOrderedDirectory(original);
// Compute space needed for extra storage first, then write the offset to the IFD, so that the layout is:
// IFD offset
// <data including sub-IFDs>
// IFD entries (values/offsets)
long dataOffset = stream.getStreamPosition();
long dataSize = computeDataSize(ordered);
// Offset to this IFD
final long ifdOffset = stream.getStreamPosition() + dataSize + LONGWORD_LENGTH;
if (!isSubIFD) {
stream.writeInt(assertIntegerOffset(ifdOffset));
dataOffset += LONGWORD_LENGTH;
// Seek to offset
stream.seek(ifdOffset);
}
else {
dataOffset += WORD_LENGTH + ordered.size() * ENTRY_LENGTH;
}
// Write directory
stream.writeShort(ordered.size());
for (Entry entry : ordered) {
// Write tag id
stream.writeShort((Integer) entry.getIdentifier());
// Write tag type
stream.writeShort(getType(entry));
// Write value count
stream.writeInt(getCount(entry));
// Write value
if (entry.getValue() instanceof Directory) {
// TODO: This could possibly be a compound directory, in which case the count should be > 1
stream.writeInt(assertIntegerOffset(dataOffset));
long streamPosition = stream.getStreamPosition();
stream.seek(dataOffset);
Directory subIFD = (Directory) entry.getValue();
writeIFD(subIFD, stream, true);
dataOffset += computeDataSize(subIFD);
stream.seek(streamPosition);
}
else {
dataOffset += writeValue(entry, dataOffset, stream);
}
}
return ifdOffset;
}
public long computeIFDSize(final Collection<Entry> directory) {
return WORD_LENGTH + computeDataSize(new IFD(directory)) + directory.size() * ENTRY_LENGTH;
}
private long computeDataSize(final Directory directory) {
long dataSize = 0;
for (Entry entry : directory) {
int length = EXIFReader.getValueLength(getType(entry), getCount(entry));
if (length < 0) {
throw new IllegalArgumentException(String.format("Unknown size for entry %s", entry));
}
if (length > LONGWORD_LENGTH) {
dataSize += length;
}
if (entry.getValue() instanceof Directory) {
Directory subIFD = (Directory) entry.getValue();
long subIFDSize = WORD_LENGTH + subIFD.size() * ENTRY_LENGTH + computeDataSize(subIFD);
dataSize += subIFDSize;
}
}
return dataSize;
}
private Directory ensureOrderedDirectory(final Directory directory) {
if (!isSorted(directory)) {
List<Entry> entries = new ArrayList<Entry>(directory.size());
for (Entry entry : directory) {
entries.add(entry);
}
Collections.sort(entries, new Comparator<Entry>() {
public int compare(Entry left, Entry right) {
return (Integer) left.getIdentifier() - (Integer) right.getIdentifier();
}
});
return new IFD(entries);
}
return directory;
}
private boolean isSorted(final Directory directory) {
int lastTag = 0;
for (Entry entry : directory) {
int tag = ((Integer) entry.getIdentifier()) & 0xffff;
if (tag < lastTag) {
return false;
}
lastTag = tag;
}
return true;
}
private long writeValue(Entry entry, long dataOffset, ImageOutputStream stream) throws IOException {
short type = getType(entry);
int valueLength = EXIFReader.getValueLength(type, getCount(entry));
if (valueLength <= LONGWORD_LENGTH) {
writeValueInline(entry.getValue(), type, stream);
// Pad
for (int i = valueLength; i < LONGWORD_LENGTH; i++) {
stream.write(0);
}
return 0;
}
else {
writeValueAt(dataOffset, entry.getValue(), type, stream);
return valueLength;
}
}
private int getCount(Entry entry) {
Object value = entry.getValue();
return value instanceof String ? ((String) value).getBytes(Charset.forName("UTF-8")).length + 1 : entry.valueCount();
}
private void writeValueInline(Object value, short type, ImageOutputStream stream) throws IOException {
if (value.getClass().isArray()) {
switch (type) {
case TIFF.TYPE_BYTE:
stream.write((byte[]) value);
break;
case TIFF.TYPE_SHORT:
short[] shorts;
if (value instanceof short[]) {
shorts = (short[]) value;
}
else if (value instanceof int[]) {
int[] ints = (int[]) value;
shorts = new short[ints.length];
for (int i = 0; i < ints.length; i++) {
shorts[i] = (short) ints[i];
}
}
else if (value instanceof long[]) {
long[] longs = (long[]) value;
shorts = new short[longs.length];
for (int i = 0; i < longs.length; i++) {
shorts[i] = (short) longs[i];
}
}
else {
throw new IllegalArgumentException("Unsupported type for TIFF SHORT: " + value.getClass());
}
stream.writeShorts(shorts, 0, shorts.length);
break;
case TIFF.TYPE_LONG:
int[] ints;
if (value instanceof int[]) {
ints = (int[]) value;
}
else if (value instanceof long[]) {
long[] longs = (long[]) value;
ints = new int[longs.length];
for (int i = 0; i < longs.length; i++) {
ints[i] = (int) longs[i];
}
}
else {
throw new IllegalArgumentException("Unsupported type for TIFF SHORT: " + value.getClass());
}
stream.writeInts(ints, 0, ints.length);
break;
case TIFF.TYPE_RATIONAL:
Rational[] rationals = (Rational[]) value;
for (Rational rational : rationals) {
stream.writeInt((int) rational.numerator());
stream.writeInt((int) rational.denominator());
}
// TODO: More types
default:
throw new IllegalArgumentException("Unsupported TIFF type: " + type);
}
}
// else if (value instanceof Directory) {
// writeIFD((Directory) value, stream, false);
// }
else {
switch (type) {
case TIFF.TYPE_BYTE:
stream.writeByte((Integer) value);
break;
case TIFF.TYPE_ASCII:
byte[] bytes = ((String) value).getBytes(Charset.forName("UTF-8"));
stream.write(bytes);
stream.write(0);
break;
case TIFF.TYPE_SHORT:
stream.writeShort((Integer) value);
break;
case TIFF.TYPE_LONG:
stream.writeInt(((Number) value).intValue());
break;
case TIFF.TYPE_RATIONAL:
Rational rational = (Rational) value;
stream.writeInt((int) rational.numerator());
stream.writeInt((int) rational.denominator());
break;
// TODO: More types
default:
throw new IllegalArgumentException("Unsupported TIFF type: " + type);
}
}
}
private void writeValueAt(long dataOffset, Object value, short type, ImageOutputStream stream) throws IOException {
stream.writeInt(assertIntegerOffset(dataOffset));
long position = stream.getStreamPosition();
stream.seek(dataOffset);
writeValueInline(value, type, stream);
stream.seek(position);
}
private short getType(Entry entry) {
if (entry instanceof EXIFEntry) {
EXIFEntry exifEntry = (EXIFEntry) entry;
return exifEntry.getType();
}
Object value = Validate.notNull(entry.getValue());
boolean array = value.getClass().isArray();
if (array) {
value = Array.get(value, 0);
}
// Note: This "narrowing" is to keep data consistent between read/write.
// TODO: Check for negative values and use signed types?
if (value instanceof Byte) {
return TIFF.TYPE_BYTE;
}
if (value instanceof Short) {
if (!array && (Short) value < Byte.MAX_VALUE) {
return TIFF.TYPE_BYTE;
}
return TIFF.TYPE_SHORT;
}
if (value instanceof Integer) {
if (!array && (Integer) value < Short.MAX_VALUE) {
return TIFF.TYPE_SHORT;
}
return TIFF.TYPE_LONG;
}
if (value instanceof Long) {
if (!array && (Long) value < Integer.MAX_VALUE) {
return TIFF.TYPE_LONG;
}
}
if (value instanceof Rational) {
return TIFF.TYPE_RATIONAL;
}
if (value instanceof String) {
return TIFF.TYPE_ASCII;
}
// TODO: More types
throw new UnsupportedOperationException(String.format("Method getType not implemented for entry of type %s/value of type %s", entry.getClass(), value.getClass()));
}
private int assertIntegerOffset(long offset) throws IIOException {
if (offset > Integer.MAX_VALUE - (long) Integer.MIN_VALUE) {
throw new IIOException("Integer overflow for TIFF stream");
}
return (int) offset;
}
}

View File

@ -76,7 +76,6 @@ public interface TIFF {
11 = FLOAT Single precision (4-byte) IEEE format.
12 = DOUBLE Double precision (8-byte) IEEE format.
TODO: Verify IFD type
See http://www.awaresystems.be/imaging/tiff/tifftags/subifds.html
13 = IFD, same as LONG

View File

@ -68,13 +68,13 @@ public interface JPEG {
int APP15 = 0xFFEF;
// Start of Frame segment markers (SOFn).
/** SOF0: Baseline DCT, Huffman encoded. */
/** SOF0: Baseline DCT, Huffman coding. */
int SOF0 = 0xFFC0;
/** SOF0: Extended DCT, Huffman encoded. */
/** SOF0: Extended DCT, Huffman coding. */
int SOF1 = 0xFFC1;
/** SOF2: Progressive DCT, Huffman encoded. */
/** SOF2: Progressive DCT, Huffman coding. */
int SOF2 = 0xFFC2;
/** SOF3: Lossless sequential, Huffman encoded. */
/** SOF3: Lossless sequential, Huffman coding. */
int SOF3 = 0xFFC3;
/** SOF5: Sequential DCT, differential Huffman coding. */
int SOF5 = 0xFFC5;
@ -86,7 +86,7 @@ public interface JPEG {
int SOF9 = 0xFFC9;
/** SOF10: Progressive DCT, arithmetic coding. */
int SOF10 = 0xFFCA;
/** SOF11: Lossless sequential, arithmetic encoded. */
/** SOF11: Lossless sequential, arithmetic coding. */
int SOF11 = 0xFFCB;
/** SOF13: Sequential DCT, differential arithmetic coding. */
int SOF13 = 0xFFCD;

View File

@ -232,7 +232,7 @@ public final class JPEGQuality {
throw new IIOException("Duplicate DQT table index: " + num);
}
if (bits > 1) {
if (bits < 0 || bits > 1) {
throw new IIOException("Bad DQT bit info: " + bits);
}
@ -247,11 +247,13 @@ public final class JPEGQuality {
for (int j = 0, qtDataLength = qtData.length; j < qtDataLength; j++) {
tables[num][j] = (short) (qtData[j] & 0xff);
}
break;
case 1:
for (int j = 0, qtDataLength = qtData.length; j < qtDataLength; j += 2) {
tables[num][j / 2] = (short) ((qtData[j] & 0xff) << 8 | (qtData[j + 1] & 0xff));
}
break;
}
}

View File

@ -40,9 +40,8 @@ import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
/**
* ReaderAbstractTest
@ -54,6 +53,7 @@ import static org.junit.Assert.*;
public abstract class MetadataReaderAbstractTest {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
ImageIO.setUseCache(false);
}
protected final URL getResource(final String name) throws IOException {
@ -96,46 +96,7 @@ public abstract class MetadataReaderAbstractTest {
}
private static boolean valueEquals(final Object expected, final Object actual) {
return expected.getClass().isArray() ? arrayEquals(expected, actual) : expected.equals(actual);
}
private static boolean arrayEquals(final Object expected, final Object actual) {
Class<?> componentType = expected.getClass().getComponentType();
if (actual == null || !actual.getClass().isArray() || actual.getClass().getComponentType() != componentType) {
return false;
}
return componentType.isPrimitive() ? primitiveArrayEquals(componentType, expected, actual) : Arrays.equals((Object[]) expected, (Object[]) actual);
}
private static boolean primitiveArrayEquals(Class<?> componentType, Object expected, Object actual) {
if (componentType == boolean.class) {
return Arrays.equals((boolean[]) expected, (boolean[]) actual);
}
else if (componentType == byte.class) {
return Arrays.equals((byte[]) expected, (byte[]) actual);
}
else if (componentType == char.class) {
return Arrays.equals((char[]) expected, (char[]) actual);
}
else if (componentType == double.class) {
return Arrays.equals((double[]) expected, (double[]) actual);
}
else if (componentType == float.class) {
return Arrays.equals((float[]) expected, (float[]) actual);
}
else if (componentType == int.class) {
return Arrays.equals((int[]) expected, (int[]) actual);
}
else if (componentType == long.class) {
return Arrays.equals((long[]) expected, (long[]) actual);
}
else if (componentType == short.class) {
return Arrays.equals((short[]) expected, (short[]) actual);
}
throw new AssertionError("Unsupported type:" + componentType);
return expected.getClass().isArray() ? AbstractEntry.arrayEquals(expected, actual) : expected.equals(actual);
}
public void describeTo(final Description description) {

View File

@ -190,4 +190,90 @@ public class EXIFReaderTest extends MetadataReaderAbstractTest {
assertNotNull(exif);
assertEquals(0, exif.size()); // EXIFTool reports "Warning: Bad ExifIFD directory"
}
@Test
public void testReadExifJPEGWithInteropSubDirR98() throws IOException {
ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-R98.jpg"));
stream.seek(30);
CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 1360));
assertEquals(17, directory.size());
assertEquals(2, directory.directoryCount());
Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue();
assertNotNull(exif);
assertEquals(23, exif.size());
// The interop IFD is empty (entry count is 0)
Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue();
assertNotNull(interop);
assertEquals(2, interop.size());
assertNotNull(interop.getEntryById(1)); // InteropIndex
assertEquals("ASCII", interop.getEntryById(1).getTypeName());
assertEquals("R98", interop.getEntryById(1).getValue()); // Known values: R98, THM or R03
assertNotNull(interop.getEntryById(2)); // InteropVersion
assertEquals("UNDEFINED", interop.getEntryById(2).getTypeName());
assertArrayEquals(new byte[] {'0', '1', '0', '0'}, (byte[]) interop.getEntryById(2).getValue());
}
@Test
public void testReadExifJPEGWithInteropSubDirEmpty() throws IOException {
ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-empty.jpg"));
stream.seek(30);
CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 1360));
assertEquals(11, directory.size());
assertEquals(1, directory.directoryCount());
Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue();
assertNotNull(exif);
assertEquals(24, exif.size());
// The interop IFD is empty (entry count is 0)
Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue();
assertNotNull(interop);
assertEquals(0, interop.size());
}
@Test
public void testReadExifJPEGWithInteropSubDirEOF() throws IOException {
ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-eof.jpg"));
stream.seek(30);
CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 236));
assertEquals(8, directory.size());
assertEquals(1, directory.directoryCount());
Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue();
assertNotNull(exif);
assertEquals(5, exif.size());
// The interop IFD isn't there (offset points to outside the TIFF structure)...
// Have double-checked using ExifTool, which says "Warning : Bad InteropOffset SubDirectory start"
Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue();
assertNotNull(interop);
assertEquals(0, interop.size());
}
@Test
public void testReadExifJPEGWithInteropSubDirBad() throws IOException {
ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-bad.jpg"));
stream.seek(30);
CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 12185));
assertEquals(16, directory.size());
assertEquals(2, directory.directoryCount());
Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue();
assertNotNull(exif);
assertEquals(26, exif.size());
// JPEG starts at offset 1666 and length 10519, interop IFD points to offset 1900...
// Have double-checked using ExifTool, which says "Warning : Bad InteropIFD directory"
Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue();
assertNotNull(interop);
assertEquals(0, interop.size());
}
}

View File

@ -0,0 +1,312 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.metadata.exif;
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageOutputStreamImpl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* EXIFWriterTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFWriterTest.java,v 1.0 18.07.13 09:53 haraldk Exp$
*/
public class EXIFWriterTest {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
ImageIO.setUseCache(false);
}
protected final URL getResource(final String name) throws IOException {
return getClass().getResource(name);
}
protected final ImageInputStream getDataAsIIS() throws IOException {
return ImageIO.createImageInputStream(getData());
}
// @Override
protected InputStream getData() throws IOException {
return getResource("/exif/exif-jpeg-segment.bin").openStream();
}
// @Override
protected EXIFReader createReader() {
return new EXIFReader();
}
protected EXIFWriter createWriter() {
return new EXIFWriter();
}
@Test
public void testWriteReadSimple() throws IOException {
ArrayList<Entry> entries = new ArrayList<Entry>();
entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT));
entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT));
entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {});
entries.add(new EXIFEntry(TIFF.TAG_ARTIST, "Harald K.", TIFF.TYPE_ASCII));
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
Directory directory = new AbstractDirectory(entries) {};
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
new EXIFWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
byte[] data = output.toByteArray();
assertEquals(106, data.length);
assertEquals('M', data[0]);
assertEquals('M', data[1]);
assertEquals(0, data[2]);
assertEquals(42, data[3]);
Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data));
assertNotNull(read);
assertEquals(5, read.size());
// TODO: Assert that the tags are written in ascending order (don't test the read directory, but the file structure)!
assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
assertEquals(1600, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_HEIGHT));
assertEquals(900, read.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_ORIENTATION));
assertEquals(1, read.getEntryById(TIFF.TAG_ORIENTATION).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_ARTIST));
assertEquals("Harald K.", read.getEntryById(TIFF.TAG_ARTIST).getValue());
}
@Test
public void testWriteMotorola() throws IOException {
ArrayList<Entry> entries = new ArrayList<Entry>();
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG));
Directory directory = new AbstractDirectory(entries) {};
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
imageStream.setByteOrder(ByteOrder.BIG_ENDIAN); // BE = Motorola
new EXIFWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
byte[] data = output.toByteArray();
assertEquals(60, data.length);
assertEquals('M', data[0]);
assertEquals('M', data[1]);
assertEquals(0, data[2]);
assertEquals(42, data[3]);
Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data));
assertNotNull(read);
assertEquals(2, read.size());
assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
}
@Test
public void testWriteIntel() throws IOException {
ArrayList<Entry> entries = new ArrayList<Entry>();
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG));
Directory directory = new AbstractDirectory(entries) {};
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
imageStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // LE = Intel
new EXIFWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
byte[] data = output.toByteArray();
assertEquals(60, data.length);
assertEquals('I', data[0]);
assertEquals('I', data[1]);
assertEquals(42, data[2]);
assertEquals(0, data[3]);
Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data));
assertNotNull(read);
assertEquals(2, read.size());
assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
}
@Test
public void testNesting() throws IOException {
EXIFEntry artist = new EXIFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO", TIFF.TYPE_ASCII);
EXIFEntry subSubSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(artist)), TIFF.TYPE_LONG);
EXIFEntry subSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubSubIFD)), TIFF.TYPE_LONG);
EXIFEntry subSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubIFD)), TIFF.TYPE_LONG);
EXIFEntry subIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubIFD)), TIFF.TYPE_LONG);
Directory directory = new IFD(Collections.<Entry>singletonList(subIFD));
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
new EXIFWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
Directory read = new EXIFReader().read(new ByteArrayImageInputStream(output.toByteArray()));
assertNotNull(read);
assertEquals(1, read.size());
assertEquals(subIFD, read.getEntryById(TIFF.TAG_SUB_IFD)); // Recursively tests content!
}
@Test
public void testReadWriteRead() throws IOException {
Directory original = createReader().read(getDataAsIIS());
ByteArrayOutputStream output = new FastByteArrayOutputStream(256);
ImageOutputStream imageOutput = ImageIO.createImageOutputStream(output);
try {
createWriter().write(original, imageOutput);
}
finally {
imageOutput.close();
}
Directory read = createReader().read(new ByteArrayImageInputStream(output.toByteArray()));
assertEquals(original, read);
}
@Test
public void testComputeIFDSize() throws IOException {
ArrayList<Entry> entries = new ArrayList<Entry>();
entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT));
entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT));
entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {});
entries.add(new EXIFEntry(TIFF.TAG_ARTIST, "Harald K.", TIFF.TYPE_ASCII));
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
EXIFWriter writer = createWriter();
ImageOutputStream stream = new NullImageOutputStream();
writer.write(new IFD(entries), stream);
assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12);
}
@Test
public void testComputeIFDSizeNested() throws IOException {
EXIFEntry artist = new EXIFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO", TIFF.TYPE_ASCII);
EXIFEntry subSubSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(artist)), TIFF.TYPE_LONG);
EXIFEntry subSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubSubIFD)), TIFF.TYPE_LONG);
EXIFEntry subSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubIFD)), TIFF.TYPE_LONG);
EXIFEntry subIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubIFD)), TIFF.TYPE_LONG);
List<Entry> entries = Collections.<Entry>singletonList(subIFD);
EXIFWriter writer = createWriter();
ImageOutputStream stream = new NullImageOutputStream();
writer.write(new IFD(entries), stream);
assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12);
}
private static class NullImageOutputStream extends ImageOutputStreamImpl {
@Override
public void write(int b) throws IOException {
streamPos++;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
streamPos += len;
}
@Override
public int read() throws IOException {
throw new UnsupportedOperationException("Method read not implemented");
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
throw new UnsupportedOperationException("Method read not implemented");
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,33 @@
package com.twelvemonkeys.imageio.plugins.dcx;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* DCXProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: DCXProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class DCXProviderInfo extends ReaderWriterProviderInfo {
protected DCXProviderInfo() {
super(
DCXProviderInfo.class,
new String[]{
"dcx",
"DCX"
},
new String[]{"dcx"},
new String[]{
// No official IANA record exists
"image/dcx",
"image/x-dcx",
},
"com.twelvemkonkeys.imageio.plugins.dcx.DCXImageReader",
new String[] {"com.twelvemkonkeys.imageio.plugins.dcx.DCXImageReaderSpi"},
null, null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -28,48 +28,20 @@
package com.twelvemonkeys.imageio.plugins.pcx;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Locale;
public final class PCXImageReaderSpi extends ImageReaderSpi {
public final class PCXImageReaderSpi extends ImageReaderSpiBase {
/**
* Creates a {@code PCXImageReaderSpi}.
*/
public PCXImageReaderSpi() {
this(IIOUtil.getProviderInfo(PCXImageReaderSpi.class));
}
private PCXImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{
"pcx",
"PCX"
},
new String[]{"pcx"},
new String[]{
// No official IANA record exists
"image/pcx",
"image/x-pcx",
},
"com.twelvemkonkeys.imageio.plugins.pcx.PCXImageReader",
new Class[] {ImageInputStream.class},
null,
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
null, null,
null, null // extra image metadata formats
);
super(new PCXProviderInfo());
}
@Override public boolean canDecodeInput(final Object source) throws IOException {

View File

@ -0,0 +1,33 @@
package com.twelvemonkeys.imageio.plugins.pcx;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* PCXProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: PCXProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class PCXProviderInfo extends ReaderWriterProviderInfo {
protected PCXProviderInfo() {
super(
PCXProviderInfo.class,
new String[]{
"pcx",
"PCX"
},
new String[]{"pcx"},
new String[]{
// No official IANA record exists
"image/pcx",
"image/x-pcx",
},
"com.twelvemkonkeys.imageio.plugins.pcx.PCXImageReader",
new String[] {"com.twelvemkonkeys.imageio.plugins.pcx.PCXImageReaderSpi"},
null, null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -0,0 +1,11 @@
package com.twelvemonkeys.imageio.plugins.pict;
/**
* BitMap.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: BitMap.java,v 1.0 20/02/15 harald.kuhr Exp$
*/
final class BitMap {
}

View File

@ -29,10 +29,9 @@
package com.twelvemonkeys.imageio.plugins.pict;
import java.awt.*;
import java.awt.image.WritableRaster;
import java.awt.image.DataBufferByte;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
/**
* BitMapPattern
@ -43,22 +42,46 @@ import java.awt.image.Raster;
*/
final class BitMapPattern extends Pattern {
private final byte[] pattern;
BitMapPattern(final Paint pColor) {
super(pColor);
this(pColor, null);
}
public BitMapPattern(final byte[] pPattern) {
this(create8x8Pattern(pPattern));
this(create8x8Pattern(pPattern), pPattern);
}
private BitMapPattern(final Paint pColor, final byte[] pPattern) {
super(pColor);
pattern = pPattern;
}
// TODO: Refactor, don't need both BitMapPattern constructors and create8x8Pattern methods?
public BitMapPattern(final byte[] pPattern, Color fg, Color bg) {
this(create8x8Pattern(pPattern, fg, bg));
}
BitMapPattern(final int pPattern) {
this(create8x8Pattern(pPattern));
}
private static TexturePaint create8x8Pattern(final int pPattern) {
// TODO: Creating a special purpose Pattern might be faster than piggy-backing on TexturePaint
WritableRaster raster = QuickDraw.MONOCHROME.createCompatibleWritableRaster(8, 8);
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
private static Paint create8x8Pattern(final int pPattern) {
// // TODO: Creating a special purpose Pattern might be faster than piggy-backing on TexturePaint
// WritableRaster raster = QuickDraw.MONOCHROME.createCompatibleWritableRaster(8, 8);
// byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
//
// for (int i = 0; i < data.length; i += 4) {
// data[i ] = (byte) ((pPattern >> 24) & 0xFF);
// data[i + 1] = (byte) ((pPattern >> 16) & 0xFF);
// data[i + 2] = (byte) ((pPattern >> 8) & 0xFF);
// data[i + 3] = (byte) ((pPattern ) & 0xFF);
// }
//
// BufferedImage img = new BufferedImage(QuickDraw.MONOCHROME, raster, false, null);
// return new TexturePaint(img, new Rectangle(8, 8));
byte[] data = new byte[8];
for (int i = 0; i < data.length; i += 4) {
data[i ] = (byte) ((pPattern >> 24) & 0xFF);
@ -67,13 +90,57 @@ final class BitMapPattern extends Pattern {
data[i + 3] = (byte) ((pPattern ) & 0xFF);
}
BufferedImage img = new BufferedImage(QuickDraw.MONOCHROME, raster, false, null);
return new TexturePaint(img, new Rectangle(8, 8));
return create8x8Pattern(data);
}
private static TexturePaint create8x8Pattern(final byte[] pPattern) {
private static Paint create8x8Pattern(final byte[] pPattern) {
WritableRaster raster = Raster.createPackedRaster(new DataBufferByte(pPattern, 8), 8, 8, 1, new Point());
BufferedImage img = new BufferedImage(QuickDraw.MONOCHROME, raster, false, null);
return new TexturePaint(img, new Rectangle(8, 8));
}
private static Paint create8x8Pattern(final byte[] pPattern, Color fg, Color bg) {
switch (isSolid(pPattern)) {
case 0: // 0x00
return bg;
case -1: // 0xff
return fg;
default:
// Fall through
}
WritableRaster raster = Raster.createPackedRaster(new DataBufferByte(pPattern, 8), 8, 8, 1, new Point());
IndexColorModel cm = new IndexColorModel(1, 2, new int[] {bg.getRGB(), fg.getRGB()}, 0, false, -1, DataBuffer.TYPE_BYTE);
BufferedImage img = new BufferedImage(cm, raster, false, null);
return new TexturePaint(img, new Rectangle(8, 8));
}
private static int isSolid(byte[] pPattern) {
int prev = pPattern[0];
for (int i = 1; i < pPattern.length; i++) {
if (prev != pPattern[i]) {
return 1;
}
}
return prev;
}
@Override
public PaintContext createContext(ColorModel pModel, Rectangle pDeviceBounds, Rectangle2D pUserBounds, AffineTransform pTransform, RenderingHints pHints) {
// switch (isSolid(pattern)) {
// }
return super.createContext(pModel, pDeviceBounds, pUserBounds, pTransform, pHints);
}
@Override
public Pattern derive(final Color foreground, final Color background) {
if (paint instanceof Color) {
// TODO: This only holds for patterns that are already foregrounds...
return new BitMapPattern(foreground);
}
return null;
}
}

View File

@ -175,4 +175,41 @@ interface PICT {
int OP_UNCOMPRESSED_QUICKTIME = 0x8201;
String APPLE_USE_RESERVED_FIELD = "Reserved for Apple use.";
/*
* Picture comment 'kind' codes from: http://developer.apple.com/technotes/qd/qd_10.html
int TextBegin = 150;
int TextEnd = 151;
int StringBegin = 152;
int StringEnd = 153;
int TextCenter = 154;
int LineLayoutOff = 155;
int LineLayoutOn = 156;
int ClientLineLayout = 157;
int PolyBegin = 160;
int PolyEnd = 161;
int PolyIgnore = 163;
int PolySmooth = 164;
int PolyClose = 165;
int DashedLine = 180;
int DashedStop = 181;
int SetLineWidth = 182;
int PostScriptBegin = 190;
int PostScriptEnd = 191;
int PostScriptHandle = 192;
int PostScriptFile = 193;
int TextIsPostScript = 194;
int ResourcePS = 195;
int PSBeginNoSave = 196;
int SetGrayLevel = 197;
int RotateBegin = 200;
int RotateEnd = 201;
int RotateCenter = 202;
int FormsPrinting = 210;
int EndFormsPrinting = 211;
int ICC_Profile = 224;
int Photoshop_Data = 498;
int BitMapThinningOff = 1000;
int BitMapThinningOn = 1001;
*/
}

View File

@ -28,14 +28,12 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.io.EOFException;
import java.io.IOException;
import java.util.Locale;
/**
@ -45,28 +43,13 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: PICTImageReaderSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$
*/
public class PICTImageReaderSpi extends ImageReaderSpi {
public class PICTImageReaderSpi extends ImageReaderSpiBase {
/**
* Creates a {@code PICTImageReaderSpi}.
*/
public PICTImageReaderSpi() {
this(IIOUtil.getProviderInfo(PICTImageReaderSpi.class));
}
private PICTImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"pct", "PCT", "pict", "PICT"},
new String[]{"pct", "pict"},
new String[]{"image/pict", "image/x-pict"},
"com.twelvemkonkeys.imageio.plugins.pict.PICTImageReader",
new Class[] {ImageInputStream.class},
new String[]{"com.twelvemkonkeys.imageio.plugins.pict.PICTImageWriterSpi"},
true, null, null, null, null,
true, null, null, null, null
);
super(new PICTProviderInfo());
}
public boolean canDecodeInput(final Object pSource) throws IOException {
@ -85,7 +68,7 @@ public class PICTImageReaderSpi extends ImageReaderSpi {
else {
// Skip header 512 bytes for file-based streams
stream.reset();
PICTImageReader.skipNullHeader(stream);
skipNullHeader(stream);
}
return isPICT(stream);
@ -98,6 +81,12 @@ public class PICTImageReaderSpi extends ImageReaderSpi {
}
}
static void skipNullHeader(final ImageInputStream pStream) throws IOException {
// NOTE: Only skip if FILE FORMAT, not needed for Mac OS DnD
// Spec says "platofrm dependent", may not be all nulls..
pStream.skipBytes(PICT.PICT_NULL_HEADER_SIZE);
}
private boolean isPICT(final ImageInputStream pStream) throws IOException {
// Size may be 0, so we can't use this for validation...
pStream.readUnsignedShort();

View File

@ -258,7 +258,7 @@ public class PICTImageWriter extends ImageWriterBase {
// Treat the scanline.
for (int j = 0; j < w; j++) {
if (model instanceof ComponentColorModel && model.getColorSpace().getType() == ColorSpace.TYPE_RGB) {
// NOTE: Assumes component order always (A)BGR
// NOTE: Assumes component order always (A)BGR and sRGB
// TODO: Alpha support
scanlineBytes[x + j] = pixels[off + i * scansize * components + components * j + components - 1];
scanlineBytes[x + w + j] = pixels[off + i * scansize * components + components * j + components - 2];

View File

@ -28,12 +28,10 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.ImageWriterSpi;
import java.io.IOException;
import java.util.Locale;
@ -44,29 +42,13 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: PICTImageWriterSpi.java,v 1.0 02.mar.2006 19:21:05 haku Exp$
*/
public class PICTImageWriterSpi extends ImageWriterSpi {
public class PICTImageWriterSpi extends ImageWriterSpiBase {
/**
* Creates a {@code PICTImageWriterSpi}.
*/
public PICTImageWriterSpi() {
this(IIOUtil.getProviderInfo(PICTImageWriterSpi.class));
}
private PICTImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"pct", "PCT",
"pict", "PICT"},
new String[]{"pct", "pict"},
new String[]{"image/pict", "image/x-pict"},
"com.twelvemonkeys.imageio.plugins.pict.PICTImageWriter",
STANDARD_OUTPUT_TYPE,
new String[]{"com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi"},
true, null, null, null, null,
true, null, null, null, null
);
super(new PICTProviderInfo());
}
public boolean canEncodeImage(ImageTypeSpecifier pType) {

View File

@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* PICTProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: PICTProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class PICTProviderInfo extends ReaderWriterProviderInfo {
protected PICTProviderInfo() {
super(
PICTProviderInfo.class,
new String[] {"pct", "PCT", "pict", "PICT"},
new String[] {"pct", "pict"},
new String[] {"image/pict", "image/x-pict"},
"com.twelvemkonkeys.imageio.plugins.pict.PICTImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi"},
"com.twelvemonkeys.imageio.plugins.pict.PICTImageWriter",
new String[] {"com.twelvemkonkeys.imageio.plugins.pict.PICTImageWriterSpi"},
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -34,7 +34,8 @@ import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.io.DataInput;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
/**
* PICTUtil
@ -47,15 +48,14 @@ final class PICTUtil {
private static final String ENC_MAC_ROMAN = "MacRoman";
public static final String ENCODING = initEncoding();
public static final Charset ENCODING = initEncoding();
private static String initEncoding() {
private static Charset initEncoding() {
try {
new String("\uF8FF".getBytes(), ENC_MAC_ROMAN);
return ENC_MAC_ROMAN;
return Charset.forName(ENC_MAC_ROMAN);
}
catch (UnsupportedEncodingException e) {
return "ISO-8859-1";
catch (UnsupportedCharsetException e) {
return Charset.forName("ISO-8859-1");
}
}
@ -86,9 +86,9 @@ final class PICTUtil {
* @throws java.io.IOException if an I/O error occurs during read
*/
public static Dimension readDimension(final DataInput pStream) throws IOException {
final int h = pStream.readShort() ;
final int v = pStream.readShort() ;
return new Dimension(h,v);
int h = pStream.readShort();
int v = pStream.readShort();
return new Dimension(h, v);
}
/**
@ -102,8 +102,8 @@ final class PICTUtil {
* @throws IOException if an I/O exception occurs during reading
*/
public static String readStr31(final DataInput pStream) throws IOException {
String text = readPascalString(pStream);
int length = 31 - text.length();
String text = readPascalString(pStream);
int length = 31 - text.length();
if (length < 0) {
throw new IOException("String length exceeds maximum (31): " + text.length());
}
@ -112,7 +112,7 @@ final class PICTUtil {
}
/**
* Reads a Pascal String from the given strean.
* Reads a Pascal String from the given stream.
* The input stream must be positioned at the length byte of the text,
* which can thus be a maximum of 255 characters long.
*
@ -146,6 +146,14 @@ final class PICTUtil {
return new BitMapPattern(data);
}
// TODO: Refactor, don't need both readPattern methods
public static Pattern readPattern(final DataInput pStream, final Color fg, final Color bg) throws IOException {
// Get the data (8 bytes)
byte[] data = new byte[8];
pStream.readFully(data);
return new BitMapPattern(data, fg, bg);
}
/**
* Reads a variable width {@link Pattern color pattern} from the given stream
*
@ -221,7 +229,7 @@ final class PICTUtil {
/**
* Reads a {@code ColorTable} data structure from the given stream.
*
* @param pStream the input stream
* @param pStream the input stream
* @param pPixelSize the pixel size
* @return the indexed color model created from the {@code ColorSpec} records read.
*
@ -252,7 +260,7 @@ final class PICTUtil {
int[] colors = new int[size];
for (int i = 0; i < size ; i++) {
for (int i = 0; i < size; i++) {
// Read ColorSpec records
int index = pStream.readUnsignedShort();
Color color = readRGBColor(pStream);

View File

@ -29,9 +29,9 @@
package com.twelvemonkeys.imageio.plugins.pict;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.util.Collections;
/**
@ -42,7 +42,7 @@ import java.util.Collections;
* @version $Id: Pattern.java,v 1.0 Oct 9, 2007 1:21:38 AM haraldk Exp$
*/
abstract class Pattern implements Paint {
private final Paint paint;
protected final Paint paint;
Pattern(final Paint pPaint) {
paint = pPaint;
@ -60,5 +60,7 @@ abstract class Pattern implements Paint {
public int getTransparency() {
return paint.getTransparency();
}
}
public abstract Pattern derive(Color foreground, Color background);
}

View File

@ -0,0 +1,11 @@
package com.twelvemonkeys.imageio.plugins.pict;
/**
* PixMap.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: PixMap.java,v 1.0 20/02/15 harald.kuhr Exp$
*/
final class PixMap {
}

View File

@ -48,7 +48,12 @@ final class PixMapPattern extends Pattern {
/**
* @return the fallback B/W pattern
*/
public Pattern getPattern() {
public Pattern getFallbackPattern() {
return fallback;
}
@Override
public Pattern derive(final Color foreground, final Color background) {
return getFallbackPattern().derive(foreground, background);
}
}

View File

@ -31,8 +31,11 @@ package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.lang.Validate;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
/**
* Emulates an Apple QuickDraw rendering context, backed by a Java {@link Graphics2D}.
@ -121,9 +124,20 @@ class QuickDrawContext {
private Dimension2D penSize = new Dimension();
private int penMode;
QuickDrawContext(Graphics2D pGraphics) {
// TODO: Make sure setting bgColor/fgColor does not reset pattern, and pattern not resetting bg/fg!
private Color bgColor = Color.WHITE;
private Color fgColor = Color.BLACK;
private int textMode;
private Pattern textPattern = new BitMapPattern(Color.BLACK);
private Pattern fillPattern;
QuickDrawContext(final Graphics2D pGraphics) {
graphics = Validate.notNull(pGraphics, "graphics");
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
setPenNormal();
}
@ -144,18 +158,34 @@ class QuickDrawContext {
// Font number (sic), integer
void setTextFont(int fontFamily) {
// ..?
System.err.println("QuickDrawContext.setTextFont");
System.err.println("QuickDrawContext.setTextFont: " + fontFamily);
}
public void setTextFont(final String fontName) {
// TODO: Need mapping between known QD font names and Java font names?
Font current = graphics.getFont();
graphics.setFont(Font.decode(fontName).deriveFont(current.getStyle(), (float) current.getSize()));
}
// Sets the text's font style (0..255)
void setTextFace(int face) {
// int?
System.err.println("QuickDrawContext.setTextFace");
void setTextFace(final int face) {
int style = 0;
if ((face & QuickDraw.TX_BOLD_MASK) > 0) {
style |= Font.BOLD;
}
if ((face & QuickDraw.TX_ITALIC_MASK) > 0) {
style |= Font.ITALIC;
}
// TODO: Other face options, like underline, shadow, etc...
graphics.setFont(graphics.getFont().deriveFont(style));
}
void setTextMode(int pSourceMode) {
// ..?
System.err.println("QuickDrawContext.setTextMode");
textMode = pSourceMode;
}
public void setTextSize(int pSize) {
@ -175,15 +205,24 @@ class QuickDrawContext {
graphics.translate(pOrigin.getX(), pOrigin.getY());
}
public void setForeground(Color pColor) {
// TODO: Is this really correct? Or does it depend on pattern mode?
public void setForeground(final Color pColor) {
fgColor = pColor;
penPattern = new BitMapPattern(pColor);
}
public void setBackground(Color pColor) {
Color getForeground() {
return fgColor;
}
public void setBackground(final Color pColor) {
bgColor = pColor;
background = new BitMapPattern(pColor);
}
Color getBackground() {
return bgColor;
}
/*
// Pen management:
// NOTE: The HidePen procedure is called by the OpenRgn, OpenPicture, and OpenPoly routines so that you can create regions, pictures, and polygons without drawing on the screen.
@ -306,10 +345,14 @@ class QuickDrawContext {
BackPat // Used by the Erase* methods
*BackPixPat
*/
public void setBackgroundPattern(Pattern pPaint) {
public void setBackgroundPattern(final Pattern pPaint) {
background = pPaint;
}
public void setFillPattern(final Pattern fillPattern) {
this.fillPattern = fillPattern;
}
private Composite getCompositeFor(final int pMode) {
switch (pMode & ~QuickDraw.DITHER_COPY) {
// Boolean source transfer modes
@ -321,9 +364,10 @@ class QuickDrawContext {
return AlphaComposite.Xor;
case QuickDraw.SRC_BIC:
return AlphaComposite.Clear;
case QuickDraw.NOT_SRC_XOR:
return new NotSrcXor();
case QuickDraw.NOT_SRC_COPY:
case QuickDraw.NOT_SRC_OR:
case QuickDraw.NOT_SRC_XOR:
case QuickDraw.NOT_SRC_BIC:
throw new UnsupportedOperationException("Not implemented for mode " + pMode);
// return null;
@ -349,6 +393,15 @@ class QuickDrawContext {
}
}
/**
* Sets up context for text drawing.
*/
protected void setupForText() {
graphics.setPaint(textPattern);
graphics.setComposite(getCompositeFor(textMode));
}
/**
* Sets up context for line drawing/painting.
*/
@ -415,9 +468,7 @@ class QuickDrawContext {
if (isPenVisible()) {
// NOTE: Workaround for known Mac JDK bug: Paint, not frame
//graphics.setStroke(getStroke(penSize)); // Make sure we have correct stroke
paintShape(graphics.getStroke().createStrokedShape(line));
}
moveTo(pX, pY);
@ -811,13 +862,18 @@ class QuickDrawContext {
// TODO: All other operations can delegate to these! :-)
private void frameShape(final Shape pShape) {
setupForPaint();
graphics.draw(pShape);
if (isPenVisible()) {
setupForPaint();
Stroke stroke = getStroke(penSize);
Shape shape = stroke.createStrokedShape(pShape);
graphics.draw(shape);
}
}
private void paintShape(final Shape pShape) {
setupForPaint();
graphics.fill(pShape);
graphics.fill(pShape); // Yes, fill
}
private void fillShape(final Shape pShape, final Pattern pPattern) {
@ -878,20 +934,22 @@ class QuickDrawContext {
pSrcRect.y + pSrcRect.height,
null
);
setClipRegion(null);
}
/**
* CopyMask
*/
public void copyMask(BufferedImage pSrcBitmap, BufferedImage pMaskBitmap, Rectangle pSrcRect, Rectangle pMaskRect, Rectangle pDstRect, int pSrcCopy, Shape pMaskRgn) {
throw new UnsupportedOperationException("Method copyBits not implemented"); // TODO: Implement
throw new UnsupportedOperationException("Method copyMask not implemented"); // TODO: Implement
}
/**
* CopyDeepMask -- available to basic QuickDraw only in System 7, combines the functionality of both CopyBits and CopyMask
*/
public void copyDeepMask(BufferedImage pSrcBitmap, BufferedImage pMaskBitmap, Rectangle pSrcRect, Rectangle pMaskRect, Rectangle pDstRect, int pSrcCopy, Shape pMaskRgn) {
throw new UnsupportedOperationException("Method copyBits not implemented"); // TODO: Implement
throw new UnsupportedOperationException("Method copyDeepMask not implemented"); // TODO: Implement
}
/*
@ -926,7 +984,8 @@ class QuickDrawContext {
* @param pString a Pascal string (a string of length less than or equal to 255 chars).
*/
public void drawString(String pString) {
graphics.drawString(pString, (float) getPenPosition().getX(), (float) getPenPosition().getY());
setupForText();
graphics.drawString(pString, (float) getPenPosition().getX(), (float) getPenPosition().getY());
}
/*
@ -1049,4 +1108,41 @@ class QuickDrawContext {
}
}
private static class NotSrcXor implements Composite {
// TODO: Src can probably be any color model that can be encoded in PICT, dst is always RGB/TYPE_INT
public CompositeContext createContext(final ColorModel srcColorModel, final ColorModel dstColorModel, RenderingHints hints) {
{
if (!srcColorModel.getColorSpace().isCS_sRGB() || !dstColorModel.getColorSpace().isCS_sRGB()) {
throw new IllegalArgumentException("Only sRGB supported");
}
}
return new CompositeContext() {
public void dispose() {
}
public void compose(Raster src, Raster dstIn, WritableRaster dstOut) {
// We always work in RGB, using DataBuffer.TYPE_INT transfer type.
int[] srcData = null;
int[] dstData = null;
int[] resData = new int[src.getWidth() - src.getMinX()];
for (int y = src.getMinY(); y < src.getHeight(); y++) {
srcData = (int[]) src.getDataElements(src.getMinX(), y, src.getWidth(), 1, srcData);
dstData = (int[]) dstIn.getDataElements(src.getMinX(), y, src.getWidth(), 1, dstData);
for (int x = src.getMinX(); x < src.getWidth(); x++) {
// TODO: Decide how to handle alpha (if at all)
resData[x] = 0xff000000 | ((~ srcData[x] ^ dstData[x])) & 0xffffff ;
// resData[x] = ~ srcData[x] ^ dstData[x];
}
dstOut.setDataElements(src.getMinX(), y, src.getWidth(), 1, resData);
}
}
};
}
}
}

View File

@ -1,15 +1,19 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStreamSpi;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
import org.junit.Test;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
/**
* ICOImageReaderTestCase
@ -20,6 +24,10 @@ import static org.junit.Assert.*;
*/
public class PICTImageReaderTest extends ImageReaderAbstractTestCase<PICTImageReader> {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new ByteArrayImageInputStreamSpi());
}
static ImageReaderSpi sProvider = new PICTImageReaderSpi();
// TODO: Should also test the clipboard format (without 512 byte header)
@ -32,8 +40,20 @@ public class PICTImageReaderTest extends ImageReaderAbstractTestCase<PICTImageRe
new TestData(getClassLoaderResource("/pict/u2.pict"), new Dimension(160, 159)),
// Obsolete V2 format with weird header
new TestData(getClassLoaderResource("/pict/FLAG_B24.PCT"), new Dimension(124, 124)),
// PixMap
new TestData(getClassLoaderResource("/pict/FC10.PCT"), new Dimension(2265, 2593)),
// 1000 DPI with bounding box not matching DPI
new TestData(getClassLoaderResource("/pict/oom.pict"), new Dimension(1713, 1263))
new TestData(getClassLoaderResource("/pict/oom.pict"), new Dimension(1713, 1263)),
// Sample data from http://developer.apple.com/documentation/mac/QuickDraw/QuickDraw-458.html
new TestData(DATA_V1, new Dimension(168, 108)),
new TestData(DATA_V2, new Dimension(168, 108)),
new TestData(DATA_EXT_V2, new Dimension(168, 108)),
// Examples from http://developer.apple.com/technotes/qd/qd_14.html
new TestData(DATA_V1_COPY_BITS, new Dimension(100, 165)),
new TestData(DATA_V1_OVAL_RECT, new Dimension(100, 165)),
new TestData(DATA_V1_OVERPAINTED_ARC, new Dimension(100, 165))
);
}
@ -73,4 +93,182 @@ public class PICTImageReaderTest extends ImageReaderAbstractTestCase<PICTImageRe
new Dimension(386, 396)
)));
}
@Test
public void testDataExtV2() throws IOException, InterruptedException {
PICTImageReader reader = createReader();
reader.setInput(new ByteArrayImageInputStream(DATA_EXT_V2));
reader.read(0);
}
@Test
public void testDataV2() throws IOException, InterruptedException {
PICTImageReader reader = createReader();
reader.setInput(new ByteArrayImageInputStream(DATA_V2));
reader.read(0);
}
@Test
public void testDataV1() throws IOException, InterruptedException {
PICTImageReader reader = createReader();
reader.setInput(new ByteArrayImageInputStream(DATA_V1));
reader.read(0);
}
@Test
public void testDataV1OvalRect() throws IOException, InterruptedException {
PICTImageReader reader = createReader();
reader.setInput(new ByteArrayImageInputStream(DATA_V1_OVAL_RECT));
reader.read(0);
}
@Test
public void testDataV1OverpaintedArc() throws IOException, InterruptedException {
// TODO: Doesn't look right
PICTImageReader reader = createReader();
reader.setInput(new ByteArrayImageInputStream(DATA_V1_OVERPAINTED_ARC));
reader.read(0);
BufferedImage image = reader.read(0);
if (!GraphicsEnvironment.isHeadless()) {
PICTImageReader.showIt(image, "dataV1CopyBits");
Thread.sleep(10000);
}
}
@Test
public void testDataV1CopyBits() throws IOException, InterruptedException {
PICTImageReader reader = createReader();
reader.setInput(new ByteArrayImageInputStream(DATA_V1_COPY_BITS));
reader.read(0);
// BufferedImage image = reader.read(0);
//
// if (!GraphicsEnvironment.isHeadless()) {
// PICTImageReader.showIt(image, "dataV1CopyBits");
// Thread.sleep(10000);
// }
}
private static final byte[] DATA_EXT_V2 = {
0x00, 0x78, /* picture size; don't use this value for picture size */
0x00, 0x00, 0x00, 0x00, 0x00, 0x6C, 0x00, (byte) 0xA8, /* bounding rectangle of picture at 72 dpi */
0x00, 0x11, /* VersionOp opcode; always $0011 for extended version 2 */
0x02, (byte) 0xFF, /* Version opcode; always $02FF for extended version 2 */
0x0C, 0x00, /* HeaderOp opcode; always $0C00 for extended version 2 */
/* next 24 bytes contain header information */
(byte) 0xFF, (byte) 0xFE, /* version; always -2 for extended version 2 */
0x00, 0x00, /* reserved */
0x00, 0x48, 0x00, 0x00, /* best horizontal resolution: 72 dpi */
0x00, 0x48, 0x00, 0x00, /* best vertical resolution: 72 dpi */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* optimal source rectangle for 72 dpi horizontal
and 72 dpi vertical resolutions */
0x00, 0x00, /* reserved */
0x00, 0x1E, /* DefHilite opcode to use default hilite color */
0x00, 0x01, /* Clip opcode to define clipping region for picture */
0x00, 0x0A, /* region size */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for clipping region */
0x00, 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */
0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, /* fill pattern */
0x00, 0x34, /* fillRect opcode; rectangle specified in next 8 bytes */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* rectangle to fill */
0x00, 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */
(byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, /* fill pattern */
0x00, 0x5C, /* fillSameOval opcode */
0x00, 0x08, /* PnMode opcode */
0x00, 0x08, /* pen mode data */
0x00, 0x71, /* paintPoly opcode */
0x00, 0x1A, /* size of polygon */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for polygon */
0x00, 0x6E, 0x00, 0x02, 0x00, 0x02, 0x00, 0x54, 0x00, 0x6E, 0x00, (byte) 0xAA, 0x00, 0x6E, 0x00, 0x02, /* polygon points */
0x00, (byte) 0xFF, /* OpEndPic opcode; end of picture */
};
private static final byte[] DATA_V2 = {
0x00, 0x78, /* picture size; don't use this value for picture size */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle of picture */
0x00, 0x11, /* VersionOp opcode; always $0x00, 0x11, for version 2 */
0x02, (byte) 0xFF, /* Version opcode; always $0x02, 0xFF, for version 2 */
0x0C, 0x00, /* HeaderOp opcode; always $0C00 for version 2 */
/* next 24 bytes contain header information */
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, /* version; always -1 (long) for version 2 */
0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, (byte) 0xAA, 0x00, 0x00, 0x00, 0x6E, 0x00, 0x00, /* fixed-point bounding
rectangle for picture */
0x00, 0x00, 0x00, 0x00, /* reserved */
0x00, 0x1E, /* DefHilite opcode to use default hilite color */
0x00, 0x01, /* Clip opcode to define clipping region for picture */
0x00, 0x0A, /* region size */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for clipping region */
0x00, 0x0A, /* FillPat opcode; fill pattern specifed in next 8 bytes */
0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, /* fill pattern */
0x00, 0x34, /* fillRect opcode; rectangle specified in next 8 bytes */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* rectangle to fill */
0x00, 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */
(byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, /* fill pattern */
0x00, 0x5C, /* fillSameOval opcode */
0x00, 0x08, /* PnMode opcode */
0x00, 0x08, /* pen mode data */
0x00, 0x71, /* paintPoly opcode */
0x00, 0x1A, /* size of polygon */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for polygon */
0x00, 0x6E, 0x00, 0x02, 0x00, 0x02, 0x00, 0x54, 0x00, 0x6E, 0x00, (byte) 0xAA, 0x00, 0x6E, 0x00, 0x02, /* polygon points */
0x00, (byte) 0xFF, /* OpEndPic opcode; end of picture */
};
private static final byte[] DATA_V1 = {
0x00, 0x4F, /* picture size; this value is reliable for version 1 pictures */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle of picture */
0x11, /* picVersion opcode for version 1 */
0x01, /* version number 1 */
0x01, /* ClipRgn opcode to define clipping region for picture */
0x00, 0x0A, /* region size */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for region */
0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */
0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, /* fill pattern */
0x34, /* fillRect opcode; rectangle specified in next 8 bytes */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* rectangle to fill */
0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */
(byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, /* fill pattern */
0x5C, /* fillSameOval opcode */
0x71, /* paintPoly opcode */
0x00, 0x1A, /* size of polygon */
0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for polygon */
0x00, 0x6E, 0x00, 0x02, 0x00, 0x02, 0x00, 0x54, 0x00, 0x6E, 0x00, (byte) 0xAA, 0x00, 0x6E, 0x00, 0x02, /* polygon points */
(byte) 0xFF, /* EndOfPicture opcode; end of picture */
};
private static final byte[] DATA_V1_OVAL_RECT = {
0x00, 0x26, /*size */
0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* picFrame */
0x11, 0x01, /* version 1 */
0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFA, 0x01, (byte) 0x90, /* clipRgn -- 10 byte region */
0x0B, 0x00, 0x04, 0x00, 0x05, /* ovSize point */
0x40, 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* frameRRect rectangle */
(byte) 0xFF, /* fin */
};
private static final byte[] DATA_V1_OVERPAINTED_ARC = {
0x00, 0x36, /* size */
0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* picFrame */
0x11, 0x01, /* version 1 */
0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFA, 0x01, (byte) 0x90, /* clipRgn -- 10 byte region */
0x61, 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, 0x00, 0x03, 0x00, 0x2D, /* paintArc rectangle,startangle,endangle */
0x08, 0x00, 0x0A, /* pnMode patXor -- note that the pnMode comes before the pnPat */
0x09, (byte) 0xAA, 0x55, (byte) 0xAA, 0x55, (byte) 0xAA, 0x55, (byte) 0xAA, 0x55, /* pnPat gray */
0x69, 0x00, 0x03, 0x00, 0x2D, /* paintSameArc startangle,endangle */
(byte) 0xFF, /* fin */
};
private static final byte[] DATA_V1_COPY_BITS = {
0x00, 0x48, /* size */
0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* picFrame */
0x11, 0x01, /* version 1 */
0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFA, 0x01, (byte) 0x90, /* clipRgn -- 10 byte region */
0x31, 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* paintRect rectangle */
(byte) 0x90, 0x00, 0x02, 0x00, 0x0A, 0x00, 0x14, 0x00, 0x0F, 0x00, 0x1C, /* BitsRect rowbytes bounds (note that bounds is wider than smallr) */
0x00, 0x0A, 0x00, 0x14, 0x00, 0x0F, 0x00, 0x19, /* srcRect */
0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x1E, /* dstRect */
0x00, 0x06, /* mode=notSrcXor */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 5 rows of empty bitmap (we copied from a still-blank window) */
(byte) 0xFF, /* fin */
};
}

View File

@ -28,6 +28,7 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase;
import org.junit.Test;
@ -38,14 +39,12 @@ import javax.imageio.stream.ImageOutputStream;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
/**
* PICTImageWriterTest
@ -70,9 +69,9 @@ public class PICTImageWriterTest extends ImageWriterAbstractTestCase {
new BufferedImage(32, 20, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(30, 20, BufferedImage.TYPE_3BYTE_BGR),
new BufferedImage(30, 20, BufferedImage.TYPE_4BYTE_ABGR),
new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_INDEXED),
new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_GRAY)
// new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_BINARY) // Packed does not work
new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_INDEXED)
// new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_GRAY) // With Java8/LittleCMS gray values are way off...
// new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_BINARY) // Packed data does not work
);
}
@ -101,7 +100,7 @@ public class PICTImageWriterTest extends ImageWriterAbstractTestCase {
assertTrue("No image data written", buffer.size() > 0);
ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray()));
ImageInputStream input = new ByteArrayImageInputStream(buffer.toByteArray());
BufferedImage written = ImageIO.read(input);
assertNotNull(written);
@ -113,16 +112,23 @@ public class PICTImageWriterTest extends ImageWriterAbstractTestCase {
int originalRGB = original.getRGB(x, y);
int writtenRGB = written.getRGB(x, y);
int expectedR = (originalRGB & 0xff0000) >> 16;
int actualR = (writtenRGB & 0xff0000) >> 16;
int expectedG = (originalRGB & 0x00ff00) >> 8;
int actualG = (writtenRGB & 0x00ff00) >> 8;
int expectedB = originalRGB & 0x0000ff;
int actualB = writtenRGB & 0x0000ff;
if (original.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY) {
// NOTE: For some reason, gray data seems to be one step off...
assertEquals("Test data " + i + " R(" + x + "," + y + ")", originalRGB & 0xff0000, writtenRGB & 0xff0000, 0x10000);
assertEquals("Test data " + i + " G(" + x + "," + y + ")", originalRGB & 0x00ff00, writtenRGB & 0x00ff00, 0x100);
assertEquals("Test data " + i + " B(" + x + "," + y + ")", originalRGB & 0x0000ff, writtenRGB & 0x0000ff, 0x1);
// ...and vary with different backing CMSs... :-(
assertTrue(String.format("original 0x%08x != gray! (%d,%d)", originalRGB, x, y), expectedR == expectedG && expectedG == expectedB);
assertTrue(String.format("written 0x%08x != gray! (%d,%d)", writtenRGB, x, y), actualR == actualG && actualG == actualB);
}
else {
assertEquals("Test data " + i + " R(" + x + "," + y + ")", originalRGB & 0xff0000, writtenRGB & 0xff0000);
assertEquals("Test data " + i + " G(" + x + "," + y + ")", originalRGB & 0x00ff00, writtenRGB & 0x00ff00);
assertEquals("Test data " + i + " B(" + x + "," + y + ")", originalRGB & 0x0000ff, writtenRGB & 0x0000ff);
assertEquals(String.format("Test data %d R(%d,%d)", i, x, y), expectedR, actualR);
assertEquals(String.format("Test data %d G(%d,%d)", i, x, y), expectedG, actualG);
assertEquals(String.format("Test data %d B(%d,%d)", i, x, y), expectedB, actualB);
}
}
}

Binary file not shown.

View File

@ -29,7 +29,6 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
@ -43,16 +42,16 @@ public final class PAMImageWriterSpi extends ImageWriterSpi {
* Creates a {@code PAMImageWriterSpi}.
*/
public PAMImageWriterSpi() {
this(IIOUtil.getProviderInfo(PAMImageWriterSpi.class));
this(new PNMProviderInfo());
}
private PAMImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"pam", "PAM"},
new String[]{"pam"},
new String[]{
new String[] {"pam", "PAM"},
new String[] {"pam"},
new String[] {
// No official IANA record exists, these are conventional
"image/x-portable-arbitrarymap" // PAM
},
@ -73,7 +72,8 @@ public final class PAMImageWriterSpi extends ImageWriterSpi {
return new PNMImageWriter(this);
}
@Override public String getDescription(final Locale locale) {
@Override
public String getDescription(final Locale locale) {
return "NetPBM Portable Arbitrary Map (PAM) image writer";
}
}

View File

@ -29,7 +29,6 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
@ -43,19 +42,19 @@ public final class PNMImageReaderSpi extends ImageReaderSpi {
* Creates a {@code PNMImageReaderSpi}.
*/
public PNMImageReaderSpi() {
this(IIOUtil.getProviderInfo(PNMImageReaderSpi.class));
this(new PNMProviderInfo());
}
private PNMImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{
"pnm", "pbm", "pgm", "ppm", "pam",
"PNM", "PBM", "PGM", "PPM", "PAM"
new String[] {
"pnm", "pbm", "pgm", "ppm", "pam", "pfm",
"PNM", "PBM", "PGM", "PPM", "PAM", "PFM"
},
new String[]{"pbm", "pgm", "ppm", "pam"},
new String[]{
new String[] {"pbm", "pgm", "ppm", "pam", "pfm"},
new String[] {
// No official IANA record exists, these are conventional
"image/x-portable-pixmap",
"image/x-portable-anymap",
@ -63,7 +62,7 @@ public final class PNMImageReaderSpi extends ImageReaderSpi {
},
"com.twelvemkonkeys.imageio.plugins.pnm.PNMImageReader",
new Class[] {ImageInputStream.class},
new String[]{
new String[] {
"com.twelvemkonkeys.imageio.plugins.pnm.PNMImageWriterSpi",
"com.twelvemkonkeys.imageio.plugins.pnm.PAMImageWriterSpi"
},
@ -76,7 +75,8 @@ public final class PNMImageReaderSpi extends ImageReaderSpi {
);
}
@Override public boolean canDecodeInput(final Object source) throws IOException {
@Override
public boolean canDecodeInput(final Object source) throws IOException {
if (!(source instanceof ImageInputStream)) {
return false;
}
@ -109,11 +109,13 @@ public final class PNMImageReaderSpi extends ImageReaderSpi {
}
}
@Override public ImageReader createReaderInstance(final Object extension) throws IOException {
@Override
public ImageReader createReaderInstance(final Object extension) throws IOException {
return new PNMImageReader(this);
}
@Override public String getDescription(final Locale locale) {
@Override
public String getDescription(final Locale locale) {
return "NetPBM Portable Any Map (PNM and PAM) image reader";
}
}

View File

@ -29,7 +29,6 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
@ -41,23 +40,24 @@ public final class PNMImageWriterSpi extends ImageWriterSpi {
// TODO: Consider one Spi for each sub-format, as it makes no sense for the writer to write PPM if client code requested PBM or PGM format.
// ...Then again, what if user asks for PNM? :-P
/**
* Creates a {@code PNMImageWriterSpi}.
*/
public PNMImageWriterSpi() {
this(IIOUtil.getProviderInfo(PNMImageWriterSpi.class));
this(new PNMProviderInfo());
}
private PNMImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{
new String[] {
"pnm", "pbm", "pgm", "ppm",
"PNM", "PBM", "PGM", "PPM"
},
new String[]{"pbm", "pgm", "ppm"},
new String[]{
new String[] {"pbm", "pgm", "ppm"},
new String[] {
// No official IANA record exists, these are conventional
"image/x-portable-pixmap",
"image/x-portable-anymap"
@ -79,7 +79,8 @@ public final class PNMImageWriterSpi extends ImageWriterSpi {
return new PNMImageWriter(this);
}
@Override public String getDescription(final Locale locale) {
@Override
public String getDescription(final Locale locale) {
return "NetPBM Portable Any Map (PNM) image writer";
}
}

View File

@ -0,0 +1,19 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
/**
* PNMProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: PNMProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
class PNMProviderInfo extends ProviderInfo {
// NOTE: Because the ReaderSpi and the two WriterSpis supports different formats,
// we don't use the standard ImageReaderWriterProviderInfo superclass here.
public PNMProviderInfo() {
super(PNMProviderInfo.class.getPackage());
}
}

View File

@ -29,7 +29,6 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.IIOException;
import java.io.DataInput;
import java.io.IOException;
@ -120,7 +119,7 @@ final class PSDHeader {
case PSD.COLOR_MODE_LAB:
break;
default:
throw new IIOException(String.format("Unsupported mode depth for PSD: %d", mode));
throw new IIOException(String.format("Unsupported color mode for PSD: %d", mode));
}
}

View File

@ -28,11 +28,9 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Locale;
@ -44,39 +42,13 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: PSDImageReaderSpi.java,v 1.0 Apr 29, 2008 4:49:03 PM haraldk Exp$
*/
final public class PSDImageReaderSpi extends ImageReaderSpi {
final public class PSDImageReaderSpi extends ImageReaderSpiBase {
/**
* Creates a {@code PSDImageReaderSpi}.
*/
public PSDImageReaderSpi() {
this(IIOUtil.getProviderInfo(PSDImageReaderSpi.class));
}
private PSDImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[] {"psd", "PSD"},
new String[] {"psd"},
new String[] {
"image/vnd.adobe.photoshop", // Official, IANA registered
"application/vnd.adobe.photoshop", // Used in XMP
"image/x-psd",
"application/x-photoshop",
"image/x-photoshop"
},
"com.twelvemkonkeys.imageio.plugins.psd.PSDImageReader",
new Class[] {ImageInputStream.class},
// new String[] {"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
null,
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
PSDMetadata.NATIVE_METADATA_FORMAT_NAME, PSDMetadata.NATIVE_METADATA_FORMAT_CLASS_NAME,
null, null // extra image metadata formats
);
super(new PSDProviderInfo());
}
public boolean canDecodeInput(final Object pSource) throws IOException {

View File

@ -0,0 +1,33 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* PSDProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: PSDProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class PSDProviderInfo extends ReaderWriterProviderInfo {
protected PSDProviderInfo() {
super(
PSDProviderInfo.class,
new String[] {"psd", "PSD"},
new String[] {"psd"},
new String[] {
"image/vnd.adobe.photoshop", // Official, IANA registered
"application/vnd.adobe.photoshop", // Used in XMP
"image/x-psd",
"application/x-photoshop",
"image/x-photoshop"
},
"com.twelvemkonkeys.imageio.plugins.psd.PSDImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.psd.PSDImageReaderSpi"},
null,
null, // new String[] {"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
false, null, null, null, null,
true, PSDMetadata.NATIVE_METADATA_FORMAT_NAME, PSDMetadata.NATIVE_METADATA_FORMAT_CLASS_NAME, null, null
);
}
}

View File

@ -28,48 +28,20 @@
package com.twelvemonkeys.imageio.plugins.sgi;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.Locale;
public final class SGIImageReaderSpi extends ImageReaderSpi {
public final class SGIImageReaderSpi extends ImageReaderSpiBase {
/**
* Creates a {@code SGIImageReaderSpi}.
*/
public SGIImageReaderSpi() {
this(IIOUtil.getProviderInfo(SGIImageReaderSpi.class));
}
private SGIImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{
"sgi",
"SGI"
},
new String[]{"sgi"},
new String[]{
// No official IANA record exists
"image/sgi",
"image/x-sgi",
},
"com.twelvemkonkeys.imageio.plugins.sgi.SGIImageReader",
new Class[] {ImageInputStream.class},
null,
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
null, null,
null, null // extra image metadata formats
);
super(new SGIProviderInfo());
}
@Override public boolean canDecodeInput(final Object source) throws IOException {

View File

@ -0,0 +1,34 @@
package com.twelvemonkeys.imageio.plugins.sgi;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* SGIProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: SGIProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class SGIProviderInfo extends ReaderWriterProviderInfo {
protected SGIProviderInfo() {
super(
SGIProviderInfo.class,
new String[] {
"sgi",
"SGI"
},
new String[] {"sgi"},
new String[] {
// No official IANA record exists
"image/sgi",
"image/x-sgi",
},
"com.twelvemkonkeys.imageio.plugins.sgi.SGIImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.sgi.SGIImageReaderSpi"},
null,
null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -28,49 +28,21 @@
package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Locale;
public final class TGAImageReaderSpi extends ImageReaderSpi {
public final class TGAImageReaderSpi extends ImageReaderSpiBase {
/**
* Creates a {@code TGAImageReaderSpi}.
*/
public TGAImageReaderSpi() {
this(IIOUtil.getProviderInfo(TGAImageReaderSpi.class));
}
private TGAImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{
"tga", "TGA",
"targa", "TARGA"
},
new String[]{"tga", "tpic"},
new String[]{
// No official IANA record exists
"image/tga", "image/x-tga",
"image/targa", "image/x-targa",
},
"com.twelvemkonkeys.imageio.plugins.tga.TGAImageReader",
new Class[] {ImageInputStream.class},
null,
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
null, null,
null, null // extra image metadata formats
);
super(new TGAProviderInfo());
}
@Override public boolean canDecodeInput(final Object source) throws IOException {

View File

@ -0,0 +1,34 @@
package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* SGIProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: SGIProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class TGAProviderInfo extends ReaderWriterProviderInfo {
protected TGAProviderInfo() {
super(
TGAProviderInfo.class,
new String[]{
"tga", "TGA",
"targa", "TARGA"
},
new String[]{"tga", "tpic"},
new String[]{
// No official IANA record exists
"image/tga", "image/x-tga",
"image/targa", "image/x-targa",
},
"com.twelvemkonkeys.imageio.plugins.tga.TGAImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.tga.TGAImageReaderSpi"},
null,
null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -28,8 +28,7 @@
package com.twelvemonkeys.imageio.plugins.thumbsdb;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import com.twelvemonkeys.io.ole2.CompoundDocument;
import javax.imageio.ImageReader;
@ -48,36 +47,21 @@ import java.util.Locale;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ThumbsDBImageReaderSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$
*/
public class ThumbsDBImageReaderSpi extends ImageReaderSpi {
public class ThumbsDBImageReaderSpi extends ImageReaderSpiBase {
private ImageReaderSpi jpegProvider;
/**
* Creates a {@code ThumbsDBImageReaderSpi}.
*/
public ThumbsDBImageReaderSpi() {
this(IIOUtil.getProviderInfo(ThumbsDBImageReaderSpi.class));
}
private ThumbsDBImageReaderSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"thumbs", "THUMBS", "Thumbs DB"},
new String[]{"db"},
new String[]{"image/x-thumbs-db", "application/octet-stream"}, // TODO: Check IANA et al...
"com.twelvemonkeys.imageio.plugins.thumbsdb.ThumbsDBImageReader",
new Class[] {ImageInputStream.class},
null,
true, null, null, null, null,
true, null, null, null, null
);
super(new ThumbsDBProviderInfo());
}
public boolean canDecodeInput(Object source) throws IOException {
return source instanceof ImageInputStream && canDecode((ImageInputStream) source);
}
public boolean canDecode(ImageInputStream pInput) throws IOException {
public boolean canDecode(final ImageInputStream pInput) throws IOException {
maybeInitJPEGProvider();
// If this is a OLE 2 CompoundDocument, we could try...
// TODO: How do we know it's thumbs.db format (structure), without reading quite a lot?

View File

@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.plugins.thumbsdb;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* ThumbsDBProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: ThumbsDBProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class ThumbsDBProviderInfo extends ReaderWriterProviderInfo {
protected ThumbsDBProviderInfo() {
super(
ThumbsDBProviderInfo.class,
new String[]{"thumbs", "THUMBS", "Thumbs DB"},
new String[]{"db"},
new String[]{"image/x-thumbs-db", "application/octet-stream"}, // TODO: Check IANA et al...
"com.twelvemonkeys.imageio.plugins.thumbsdb.ThumbsDBImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.thumbsdb.ThumbsDBImageReaderSpi"},
null,
null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -0,0 +1,302 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
/**
* A decoder for data converted using "horizontal differencing predictor".
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$
*/
final class HorizontalDifferencingStream extends OutputStream {
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
private final int columns;
// NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1
private final int samplesPerPixel;
private final int bitsPerSample;
private final WritableByteChannel channel;
private final ByteBuffer buffer;
public HorizontalDifferencingStream(final OutputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s");
this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s");
channel = Channels.newChannel(Validate.notNull(stream, "stream"));
buffer = ByteBuffer.allocate((columns * samplesPerPixel * bitsPerSample + 7) / 8).order(byteOrder);
}
private boolean isValidBPS(final int bitsPerSample) {
switch (bitsPerSample) {
case 1:
case 2:
case 4:
case 8:
case 16:
case 32:
case 64:
return true;
default:
return false;
}
}
private boolean flushBuffer() throws IOException {
if (buffer.position() == 0) {
return false;
}
encodeRow();
buffer.flip();
channel.write(buffer);
buffer.clear();
return true;
}
private void encodeRow() throws EOFException {
// Apply horizontal predictor
byte original;
int sample = 0;
int prev;
byte temp;
// Optimization:
// Access array directly for <= 8 bits per sample, as buffer does extra index bounds check for every
// put/get operation... (Measures to about 100 ms difference for 4000 x 3000 image)
final byte[] array = buffer.array();
switch (bitsPerSample) {
case 1:
for (int b = ((columns + 7) / 8) - 1; b > 0; b--) {
// Subtract previous sample from current sample
original = array[b];
prev = array[b - 1] & 0x1;
temp = (byte) ((((original & 0x80) >> 7) - prev) << 7);
sample = ((original & 0x40) >> 6) - ((original & 0x80) >> 7);
temp |= (sample << 6) & 0x40;
sample = ((original & 0x20) >> 5) - ((original & 0x40) >> 6);
temp |= (sample << 5) & 0x20;
sample = ((original & 0x10) >> 4) - ((original & 0x20) >> 5);
temp |= (sample << 4) & 0x10;
sample = ((original & 0x08) >> 3) - ((original & 0x10) >> 4);
temp |= (sample << 3) & 0x08;
sample = ((original & 0x04) >> 2) - ((original & 0x08) >> 3);
temp |= (sample << 2) & 0x04;
sample = ((original & 0x02) >> 1) - ((original & 0x04) >> 2);
temp |= (sample << 1) & 0x02;
sample = (original & 0x01) - ((original & 0x02) >> 1);
array[b] = (byte) (temp & 0xfe | sample & 0x01);
}
// First sample in row as is
original = array[0];
temp = (byte) (original & 0x80);
sample = ((original & 0x40) >> 6) - ((original & 0x80) >> 7);
temp |= (sample << 6) & 0x40;
sample = ((original & 0x20) >> 5) - ((original & 0x40) >> 6);
temp |= (sample << 5) & 0x20;
sample = ((original & 0x10) >> 4) - ((original & 0x20) >> 5);
temp |= (sample << 4) & 0x10;
sample = ((original & 0x08) >> 3) - ((original & 0x10) >> 4);
temp |= (sample << 3) & 0x08;
sample = ((original & 0x04) >> 2) - ((original & 0x08) >> 3);
temp |= (sample << 2) & 0x04;
sample = ((original & 0x02) >> 1) - ((original & 0x04) >> 2);
temp |= (sample << 1) & 0x02;
sample = (original & 0x01) - ((original & 0x02) >> 1);
array[0] = (byte) (temp & 0xfe | sample & 0x01);
break;
case 2:
for (int b = ((columns + 3) / 4) - 1; b > 0; b--) {
// Subtract previous sample from current sample
original = array[b];
prev = array[b - 1] & 0x3;
temp = (byte) ((((original & 0xc0) >> 6) - prev) << 6);
sample = ((original & 0x30) >> 4) - ((original & 0xc0) >> 6);
temp |= (sample << 4) & 0x30;
sample = ((original & 0x0c) >> 2) - ((original & 0x30) >> 4);
temp |= (sample << 2) & 0x0c;
sample = (original & 0x03) - ((original & 0x0c) >> 2);
array[b] = (byte) (temp & 0xfc | sample & 0x03);
}
// First sample in row as is
original = array[0];
temp = (byte) (original & 0xc0);
sample = ((original & 0x30) >> 4) - ((original & 0xc0) >> 6);
temp |= (sample << 4) & 0x30;
sample = ((original & 0x0c) >> 2) - ((original & 0x30) >> 4);
temp |= (sample << 2) & 0x0c;
sample = (original & 0x03) - ((original & 0x0c) >> 2);
array[0] = (byte) (temp & 0xfc | sample & 0x03);
break;
case 4:
for (int b = ((columns + 1) / 2) - 1; b > 0; b--) {
// Subtract previous sample from current sample
original = array[b];
prev = array[b - 1] & 0xf;
temp = (byte) ((((original & 0xf0) >> 4) - prev) << 4);
sample = (original & 0x0f) - ((original & 0xf0) >> 4);
array[b] = (byte) (temp & 0xf0 | sample & 0xf);
}
// First sample in row as is
original = array[0];
sample = (original & 0x0f) - ((original & 0xf0) >> 4);
array[0] = (byte) (original & 0xf0 | sample & 0xf);
break;
case 8:
for (int x = columns - 1; x > 0; x--) {
final int xOff = x * samplesPerPixel;
for (int b = 0; b < samplesPerPixel; b++) {
int off = xOff + b;
array[off] = (byte) (array[off] - array[off - samplesPerPixel]);
}
}
break;
case 16:
for (int x = columns - 1; x > 0; x--) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
buffer.putShort(2 * off, (short) (buffer.getShort(2 * off) - buffer.getShort(2 * (off - samplesPerPixel))));
}
}
break;
case 32:
for (int x = columns - 1; x > 0; x--) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
buffer.putInt(4 * off, buffer.getInt(4 * off) - buffer.getInt(4 * (off - samplesPerPixel)));
}
}
break;
case 64:
for (int x = columns - 1; x > 0; x--) {
for (int b = 0; b < samplesPerPixel; b++) {
int off = x * samplesPerPixel + b;
buffer.putLong(8 * off, buffer.getLong(8 * off) - buffer.getLong(8 * (off - samplesPerPixel)));
}
}
break;
default:
throw new AssertionError(String.format("Unsupported bits per sample value: %d", bitsPerSample));
}
}
@Override
public void write(int b) throws IOException {
buffer.put((byte) b);
if (!buffer.hasRemaining()) {
flushBuffer();
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
while (len > 0) {
int maxLenForRow = Math.min(len, buffer.remaining());
buffer.put(b, off, maxLenForRow);
off += maxLenForRow;
len -= maxLenForRow;
if (!buffer.hasRemaining()) {
flushBuffer();
}
}
}
@Override
public void flush() throws IOException {
flushBuffer();
}
@Override
public void close() throws IOException {
try {
flushBuffer();
super.close();
}
finally {
if (channel.isOpen()) {
channel.close();
}
}
}
}

View File

@ -108,6 +108,10 @@ abstract class LZWDecoder implements Decoder {
break;
}
if (table[code] == null) {
throw new DecodeException(String.format("Corrupted TIFF LZW: code %d (table size: %d)", code, tableLength));
}
table[code].writeTo(buffer);
}
else {
@ -184,8 +188,9 @@ abstract class LZWDecoder implements Decoder {
}
}
public static LZWDecoder create(boolean oldBitReversedStream) {
public static Decoder create(boolean oldBitReversedStream) {
return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder();
// return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWTreeDecoder();
}
static final class LZWSpecDecoder extends LZWDecoder {
@ -282,7 +287,9 @@ abstract class LZWDecoder implements Decoder {
}
}
static final class LZWString {
static final class LZWString implements Comparable<LZWString> {
static final LZWString EMPTY = new LZWString((byte) 0, (byte) 0, 0, null);
final LZWString previous;
final int length;
@ -300,8 +307,12 @@ abstract class LZWDecoder implements Decoder {
this.previous = previous;
}
public final LZWString concatenate(final byte firstChar) {
return new LZWString(firstChar, this.firstChar, length + 1, this);
public final LZWString concatenate(final byte value) {
if (this == EMPTY) {
return new LZWString(value);
}
return new LZWString(value, this.firstChar, length + 1, this);
}
public final void writeTo(final ByteBuffer buffer) {
@ -364,6 +375,35 @@ abstract class LZWDecoder implements Decoder {
result = 31 * result + (int) firstChar;
return result;
}
@Override
public int compareTo(final LZWString other) {
if (other == this) {
return 0;
}
if (length != other.length) {
return other.length - length;
}
if (firstChar != other.firstChar) {
return other.firstChar - firstChar;
}
LZWString t = this;
LZWString o = other;
for (int i = length - 1; i > 0; i--) {
if (t.value != o.value) {
return o.value - t.value;
}
t = t.previous;
o = o.previous;
}
return 0;
}
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.io.enc.Encoder;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.TreeMap;
import static com.twelvemonkeys.imageio.plugins.tiff.LZWDecoder.LZWString;
/**
* LZWEncoder
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: LZWEncoder.java,v 1.0 02.12.13 14:13 haraldk Exp$
*/
final class LZWEncoder implements Encoder {
// TODO: Consider extracting LZWStringTable from LZWDecoder
/** Clear: Re-initialize tables. */
static final int CLEAR_CODE = 256;
/** End of Information. */
static final int EOI_CODE = 257;
private static final int MIN_BITS = 9;
private static final int MAX_BITS = 12;
private static final int TABLE_SIZE = 1 << MAX_BITS;
private int remaining;
private final LZWString[] table = new LZWString[TABLE_SIZE];
// private final Map<LZWString, Integer> reverseTable = new HashMap<>(TABLE_SIZE - 256); // This is foobar
private final Map<LZWString, Integer> reverseTable = new TreeMap<>(); // This is foobar
private int tableLength;
LZWString omega = LZWString.EMPTY;
int bitsPerCode;
private int oldCode = CLEAR_CODE;
private int maxCode;
int bitMask;
int bits;
int bitPos;
protected LZWEncoder(final int length) {
this.remaining = length;
// First 258 entries of table is always fixed
for (int i = 0; i < 256; i++) {
table[i] = new LZWString((byte) i);
}
init();
}
private static int bitmaskFor(final int bits) {
return (1 << bits) - 1;
}
private void init() {
tableLength = 258;
bitsPerCode = MIN_BITS;
bitMask = bitmaskFor(bitsPerCode);
maxCode = maxCode();
// omega = LZWString.EMPTY;
reverseTable.clear();
}
protected int maxCode() {
return bitMask;
}
public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException {
// InitializeStringTable();
// WriteCode(ClearCode);
// Ω = the empty string;
// for each character in the strip {
// K = GetNextCharacter();
// if Ω+K is in the string table {
// Ω = Ω+K;/* string concatenation */
// }
// else{
// WriteCode (CodeFromString( Ω));
// AddTableEntry(Ω+K);
// Ω=K;
// } }/*end of for loop*/
// WriteCode (CodeFromString(Ω));
// WriteCode (EndOfInformation);
if (remaining < 0) {
throw new IOException("Write past end of stream");
}
// TODO: Write 9 bit clear code ONLY first time!
if (oldCode == CLEAR_CODE) {
writeCode(stream, CLEAR_CODE);
}
int len = buffer.remaining();
while (buffer.hasRemaining()) {
byte k = buffer.get();
LZWString string = omega.concatenate(k);
int tableIndex = isInTable(string);
if (tableIndex >= 0) {
omega = string;
oldCode = tableIndex;
}
else {
writeCode(stream, oldCode);
addStringToTable(string);
oldCode = k & 0xff;
omega = table[k & 0xff];
// Handle table (almost) full
if (tableLength >= TABLE_SIZE - 2) {
writeCode(stream, CLEAR_CODE);
init();
}
}
}
remaining -= len;
// Write EOI when er are done (the API isn't very supportive of this)
if (remaining <= 0) {
writeCode(stream, oldCode);
writeCode(stream, EOI_CODE);
if (bitPos > 0) {
writeCode(stream, 0);
}
}
}
private int isInTable(final LZWString string) {
if (string.length == 1) {
return string.value & 0xff;
}
Integer index = reverseTable.get(string);
return index != null ? index : -1;
// TODO: Needs optimization :-)
// for (int i = 258; i < tableLength; i++) {
// if (table[i].equals(string)) {
// return i;
// }
// }
// return -1;
}
private int addStringToTable(final LZWString string) {
// System.err.println("LZWEncoder.addStringToTable: " + string);
final int index = tableLength++;
table[index] = string;
reverseTable.put(string, index);
if (tableLength > maxCode) {
bitsPerCode++;
if (bitsPerCode > MAX_BITS) {
throw new IllegalStateException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS));
}
bitMask = bitmaskFor(bitsPerCode);
maxCode = maxCode();
}
// if (string.length > maxString) {
// maxString = string.length;
// }
return index;
}
private void writeCode(final OutputStream stream, final int code) throws IOException {
// System.err.printf("LZWEncoder.writeCode: 0x%04x\n", code);
bits = (bits << bitsPerCode) | (code & bitMask);
bitPos += bitsPerCode;
while (bitPos >= 8) {
int b = (bits >> (bitPos - 8)) & 0xff;
// System.err.printf("write: 0x%02x\n", b);
stream.write(b);
bitPos -= 8;
}
bits &= bitmaskFor(bitPos);
}
}

View File

@ -29,8 +29,7 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ServiceRegistry;
@ -46,34 +45,12 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageReaderSpi.java,v 1.0 08.05.12 15:14 haraldk Exp$
*/
public class TIFFImageReaderSpi extends ImageReaderSpi {
public class TIFFImageReaderSpi extends ImageReaderSpiBase {
/**
* Creates a {@code TIFFImageReaderSpi}.
*/
public TIFFImageReaderSpi() {
this(IIOUtil.getProviderInfo(TIFFImageReaderSpi.class));
}
private TIFFImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{"tiff", "TIFF"},
new String[]{"tif", "tiff"},
new String[]{
"image/tiff", "image/x-tiff"
},
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
new Class[] {ImageInputStream.class},
// new String[]{"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
null,
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
null, null,
null, null // extra image metadata formats
);
super(new TIFFProviderInfo());
}
@SuppressWarnings("unchecked")

View File

@ -0,0 +1,120 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import javax.imageio.ImageWriteParam;
import java.util.Locale;
/**
* TIFFImageWriteParam
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriteParam.java,v 1.0 18.09.13 12:47 haraldk Exp$
*/
public final class TIFFImageWriteParam extends ImageWriteParam {
// TODO: Support CCITT Modified Huffman compression (2) BASELINE!!
// TODO: Support CCITT T.4 (3)
// TODO: Support CCITT T.6 (4)
// TODO: Support JBIG compression via ImageIO plugin/delegate?
// TODO: Support JPEG2000 compression via ImageIO plugin/delegate?
// TODO: Support tiling
// TODO: Support OPTIONAL predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
// DONE:
// Support no compression (None/1)
// Support ZLIB (/Deflate) compression (8)
// Support PackBits compression (32773)
// Support LZW compression (5)?
// Support JPEG compression (7)
TIFFImageWriteParam() {
this(Locale.getDefault());
}
TIFFImageWriteParam(final Locale locale) {
super(locale);
// NOTE: We use the same spelling/casing as the JAI equivalent to be as compatible as possible
// See: http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html
compressionTypes = new String[] {
"None",
null, null, null,/* "CCITT RLE", "CCITT T.4", "CCITT T.6", */
"LZW", "JPEG", "ZLib", "PackBits", "Deflate",
null/* "EXIF JPEG" */ // A well-defined form of "Old-style JPEG", no tables/process, only 513 (offset) and 514 (length)
};
compressionType = compressionTypes[0];
canWriteCompressed = true;
}
@Override
public float[] getCompressionQualityValues() {
super.getCompressionQualityValues();
// TODO: Special case for JPEG and ZLib/Deflate
return null;
}
@Override
public String[] getCompressionQualityDescriptions() {
super.getCompressionQualityDescriptions();
// TODO: Special case for JPEG and ZLib/Deflate
return null;
}
static int getCompressionType(final ImageWriteParam param) {
// TODO: Support mode COPY_FROM_METADATA (when we have metadata...)
if (param == null || param.getCompressionMode() != MODE_EXPLICIT || param.getCompressionType().equals("None")) {
return TIFFBaseline.COMPRESSION_NONE;
}
else if (param.getCompressionType().equals("PackBits")) {
return TIFFBaseline.COMPRESSION_PACKBITS;
}
else if (param.getCompressionType().equals("ZLib")) {
return TIFFExtension.COMPRESSION_ZLIB;
}
else if (param.getCompressionType().equals("Deflate")) {
return TIFFExtension.COMPRESSION_DEFLATE;
}
else if (param.getCompressionType().equals("LZW")) {
return TIFFExtension.COMPRESSION_LZW;
}
else if (param.getCompressionType().equals("JPEG")) {
return TIFFExtension.COMPRESSION_JPEG;
}
// else if (param.getCompressionType().equals("EXIF JPEG")) {
// return TIFFExtension.COMPRESSION_OLD_JPEG;
// }
throw new IllegalArgumentException(String.format("Unsupported compression type: %s", param.getCompressionType()));
}
}

View File

@ -0,0 +1,767 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter;
import com.twelvemonkeys.imageio.metadata.exif.Rational;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.image.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
/**
* TIFFImageWriter
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/
public final class TIFFImageWriter extends ImageWriterBase {
// Short term
// TODO: Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
// TODO: Use sensible defaults for compression based on input? None is sensible... :-)
// Long term
// TODO: Support tiling
// TODO: Support thumbnails
// TODO: Support ImageIO metadata
// TODO: Support CCITT Modified Huffman compression (2)
// TODO: Full "Baseline TIFF" support
// TODO: Support LZW compression (5)?
// ----
// TODO: Support storing multiple images in one stream (multi-page TIFF)
// TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata
// TODO: Support use-case: Transcode multi-page TIFF to multiple single-page TIFFs with metadata
// TODO: Support use-case: Losslessly transcode JPEG to JPEG in TIFF with (EXIF) metadata (and back)
// Very long term...
// TODO: Support JBIG compression via ImageIO plugin/delegate? Pending support in Reader
// TODO: Support JPEG2000 compression via ImageIO plugin/delegate? Pending support in Reader
// Done
// Create a basic writer that supports most inputs. Store them using the simplest possible format.
// Support no compression (None/1) - BASELINE
// Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
// Support PackBits compression (32773) - easy - BASELINE
// Support ZLIB (/Deflate) compression (8) - easy
public static final Rational STANDARD_DPI = new Rational(72);
TIFFImageWriter(final ImageWriterSpi provider) {
super(provider);
}
static final class TIFFEntry extends AbstractEntry {
TIFFEntry(Object identifier, Object value) {
super(identifier, value);
}
}
@Override
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
// TODO: Validate input
assertOutput();
// TODO: Consider writing TIFF header, offset to IFD0 (leave blank), write image data with correct
// tiling/compression/etc, then write IFD0, go back and update IFD0 offset?
// Write minimal TIFF header (required "Baseline" fields)
// Use EXIFWriter to write leading metadata (TODO: consider rename to TTIFFWriter, again...)
// TODO: Make TIFFEntry and possibly TIFFDirectory? public
RenderedImage renderedImage = image.getRenderedImage();
ColorModel colorModel = renderedImage.getColorModel();
int numComponents = colorModel.getNumComponents();
SampleModel sampleModel = renderedImage.getSampleModel();
int[] bandOffsets;
int[] bitOffsets;
if (sampleModel instanceof ComponentSampleModel) {
bandOffsets = ((ComponentSampleModel) sampleModel).getBandOffsets();
// System.err.println("bandOffsets: " + Arrays.toString(bandOffsets));
bitOffsets = null;
}
else if (sampleModel instanceof SinglePixelPackedSampleModel) {
bitOffsets = ((SinglePixelPackedSampleModel) sampleModel).getBitOffsets();
// System.err.println("bitOffsets: " + Arrays.toString(bitOffsets));
bandOffsets = null;
}
else if (sampleModel instanceof MultiPixelPackedSampleModel) {
bitOffsets = null;
bandOffsets = new int[] {0};
}
else {
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
}
List<Entry> entries = new ArrayList<Entry>();
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
// entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
entries.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
// If numComponents > 3, write ExtraSamples
if (numComponents > 3) {
// TODO: Write per component > 3
if (colorModel.hasAlpha()) {
entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
}
else {
entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
}
}
// Write compression field from param or metadata
int compression = TIFFImageWriteParam.getCompressionType(param);
entries.add(new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
// TODO: Let param control predictor
switch (compression) {
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
case TIFFExtension.COMPRESSION_LZW:
entries.add(new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
default:
}
int photometric = compression == TIFFExtension.COMPRESSION_JPEG ?
TIFFExtension.PHOTOMETRIC_YCBCR :
getPhotometricInterpretation(colorModel);
entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
entries.add(new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel)));
entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
}
else {
entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents));
// Note: Assuming sRGB to be the default RGB interpretation
ColorSpace colorSpace = colorModel.getColorSpace();
if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) {
entries.add(new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
}
}
if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT /* TODO: if (isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
entries.add(new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
}
entries.add(new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer")); // TODO: Get from metadata (optional) + fill in version number
entries.add(new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
entries.add(new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
entries.add(new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
// TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip
entries.add(new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended
// - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?)
TIFFEntry dummyStripByteCounts = new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1);
entries.add(dummyStripByteCounts); // Updated later
// - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???)
TIFFEntry dummyStripOffsets = new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1);
entries.add(dummyStripOffsets); // Updated later
// TODO: If tiled, write tile indexes etc, or always do that?
EXIFWriter exifWriter = new EXIFWriter();
if (compression == TIFFBaseline.COMPRESSION_NONE) {
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
long streamOffset = exifWriter.computeIFDSize(entries) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF
entries.remove(dummyStripByteCounts);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, renderedImage.getWidth() * renderedImage.getHeight() * numComponents));
entries.remove(dummyStripOffsets);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset));
exifWriter.write(entries, imageOutput); // NOTE: Writer takes case of ordering tags
imageOutput.flush();
}
else {
// Unless compression == 1 / COMPRESSION_NONE (and all offsets known), write only TIFF header/magic + leave room for IFD0 offset
exifWriter.writeTIFFHeader(imageOutput);
imageOutput.writeInt(-1); // IFD0 pointer, will be updated later
}
// TODO: Create compressor stream per Tile/Strip
if (compression == TIFFExtension.COMPRESSION_JPEG) {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("JPEG");
if (!writers.hasNext()) {
// This can only happen if someone deliberately uninstalled it
throw new IIOException("No JPEG ImageWriter found!");
}
ImageWriter jpegWriter = writers.next();
try {
jpegWriter.setOutput(new SubImageOutputStream(imageOutput));
jpegWriter.write(renderedImage);
}
finally {
jpegWriter.dispose();
}
}
else {
// Write image data
writeImageData(createCompressorStream(renderedImage, param), renderedImage, numComponents, bandOffsets, bitOffsets);
}
// TODO: Update IFD0-pointer, and write IFD
if (compression != TIFFBaseline.COMPRESSION_NONE) {
long streamPosition = imageOutput.getStreamPosition();
entries.remove(dummyStripOffsets);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8));
entries.remove(dummyStripByteCounts);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8));
long ifdOffset = exifWriter.writeIFD(entries, imageOutput);
imageOutput.writeInt(0); // Next IFD (none)
streamPosition = imageOutput.getStreamPosition();
// Update IFD0 pointer
imageOutput.seek(4);
imageOutput.writeInt((int) ifdOffset);
imageOutput.seek(streamPosition);
imageOutput.flush();
}
}
private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param) {
/*
36 MB test data:
No compression:
Write time: 450 ms
output.length: 36000226
PackBits:
Write time: 688 ms
output.length: 30322187
Deflate, BEST_SPEED (1):
Write time: 1276 ms
output.length: 14128866
Deflate, 2:
Write time: 1297 ms
output.length: 13848735
Deflate, 3:
Write time: 1594 ms
output.length: 13103224
Deflate, 4:
Write time: 1663 ms
output.length: 13380899 (!!)
5
Write time: 1941 ms
output.length: 13171244
6
Write time: 2311 ms
output.length: 12845101
7: Write time: 2853 ms
output.length: 12759426
8:
Write time: 4429 ms
output.length: 12624517
Deflate: DEFAULT_COMPRESSION (6?):
Write time: 2357 ms
output.length: 12845101
Deflate, BEST_COMPRESSION (9):
Write time: 4998 ms
output.length: 12600399
*/
// Use predictor by default for LZW and ZLib/Deflate
// TODO: Unless explicitly disabled in TIFFImageWriteParam
int compression = TIFFImageWriteParam.getCompressionType(param);
OutputStream stream;
switch (compression) {
case TIFFBaseline.COMPRESSION_NONE:
return imageOutput;
case TIFFBaseline.COMPRESSION_PACKBITS:
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new EncoderStream(stream, new PackBitsEncoder(), true);
// NOTE: PackBits + Predictor is possible, but not generally supported, disable it by default
// (and probably not even allow it, see http://stackoverflow.com/questions/20337400/tiff-packbits-compression-with-predictor-step)
// stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
return new DataOutputStream(stream);
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
int deflateSetting = Deflater.BEST_SPEED; // This is consistent with default compression quality being 1.0 and 0 meaning max compression...
if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
// TODO: Determine how to interpret compression quality...
// Docs says:
// A compression quality setting of 0.0 is most generically interpreted as "high compression is important,"
// while a setting of 1.0 is most generically interpreted as "high image quality is important."
// Is this what JAI TIFFImageWriter (TIFFDeflater) does? No, it does:
/*
if (param & compression etc...) {
float quality = param.getCompressionQuality();
deflateLevel = (int)(1 + 8*quality);
} else {
deflateLevel = Deflater.DEFAULT_COMPRESSION;
}
*/
// PS: PNGImageWriter just uses hardcoded BEST_COMPRESSION... :-P
deflateSetting = 9 - Math.round(8 * (param.getCompressionQuality())); // This seems more correct
}
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024);
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
return new DataOutputStream(stream);
case TIFFExtension.COMPRESSION_LZW:
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8));
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
return new DataOutputStream(stream);
}
throw new IllegalArgumentException(String.format("Unsupported TIFF compression: %d", compression));
}
private int getPhotometricInterpretation(final ColorModel colorModel) {
if (colorModel.getNumComponents() == 1 && colorModel.getComponentSize(0) == 1) {
if (colorModel instanceof IndexColorModel) {
if (colorModel.getRGB(0) == 0xFFFFFF && colorModel.getRGB(1) == 0x000000) {
return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO;
}
else if (colorModel.getRGB(0) != 0x000000 || colorModel.getRGB(1) != 0xFFFFFF) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
// Else, fall through to default, BLACK_IS_ZERO
}
return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
}
else if (colorModel instanceof IndexColorModel) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
switch (colorModel.getColorSpace().getType()) {
case ColorSpace.TYPE_GRAY:
return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
case ColorSpace.TYPE_RGB:
return TIFFBaseline.PHOTOMETRIC_RGB;
case ColorSpace.TYPE_CMYK:
return TIFFExtension.PHOTOMETRIC_SEPARATED;
}
throw new IllegalArgumentException("Can't determine PhotometricInterpretation for color model: " + colorModel);
}
private short[] createColorMap(final IndexColorModel colorModel) {
// TIFF6.pdf p. 23:
// A TIFF color map is stored as type SHORT, count = 3 * (2^BitsPerSample)
// "In a TIFF ColorMap, all the Red values come first, followed by the Green values, then the Blue values.
// In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535."
short[] colorMap = new short[(int) (3 * Math.pow(2, colorModel.getPixelSize()))];
for (int i = 0; i < colorModel.getMapSize(); i++) {
int color = colorModel.getRGB(i);
colorMap[i ] = (short) upScale((color >> 16) & 0xff);
colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff);
colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color ) & 0xff);
}
return colorMap;
}
private int upScale(final int color) {
return 257 * color;
}
private short[] asShortArray(final int[] integers) {
short[] shorts = new short[integers.length];
for (int i = 0; i < shorts.length; i++) {
shorts[i] = (short) integers[i];
}
return shorts;
}
private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException {
// Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order)
// Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged
// Store IndexColorModel as is
// Store BYTE_GRAY as is
// Store USHORT_GRAY as is
processImageStarted(0);
final int minTileY = renderedImage.getMinTileY();
final int maxYTiles = minTileY + renderedImage.getNumYTiles();
final int minTileX = renderedImage.getMinTileX();
final int maxXTiles = minTileX + renderedImage.getNumXTiles();
// Use buffer to have longer, better performing writes
final int tileHeight = renderedImage.getTileHeight();
final int tileWidth = renderedImage.getTileWidth();
// TODO: SampleSize may differ between bands/banks
int sampleSize = renderedImage.getSampleModel().getSampleSize(0);
final ByteBuffer buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8);
// System.err.println("tileWidth: " + tileWidth);
for (int yTile = minTileY; yTile < maxYTiles; yTile++) {
for (int xTile = minTileX; xTile < maxXTiles; xTile++) {
final Raster tile = renderedImage.getTile(xTile, yTile);
final DataBuffer dataBuffer = tile.getDataBuffer();
final int numBands = tile.getNumBands();
// final SampleModel sampleModel = tile.getSampleModel();
switch (dataBuffer.getDataType()) {
case DataBuffer.TYPE_BYTE:
// System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth * numBands;
for (int x = 0; x < tileWidth; x++) {
final int xOff = yOff + x * numBands;
for (int s = 0; s < numBands; s++) {
buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff));
}
}
flushBuffer(buffer, stream);
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
}
}
break;
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT:
if (numComponents == 1) {
// TODO: This is foobar...
// System.err.println("Writing USHORT -> " + numBands * 2 + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
final int xOff = yOff + x;
buffer.putShort((short) (dataBuffer.getElem(b, xOff) & 0xffff));
}
flushBuffer(buffer, stream);
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
}
}
}
else {
// for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
// for (int y = 0; y < tileHeight; y++) {
// final int yOff = y * tileWidth;
//
// for (int x = 0; x < tileWidth; x++) {
// final int xOff = yOff + x;
// int element = dataBuffer.getElem(b, xOff);
//
// for (int s = 0; s < numBands; s++) {
// buffer.put((byte) ((element >> bitOffsets[s]) & 0xff));
// }
// }
//
// flushBuffer(buffer, stream);
// if (stream instanceof DataOutputStream) {
// DataOutputStream dataOutputStream = (DataOutputStream) stream;
// dataOutputStream.flush();
// }
// }
// }
throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
}
break;
case DataBuffer.TYPE_INT:
// TODO: This is incorrect for 32 bits/sample, only works for packed (INT_(A)RGB)
// System.err.println("Writing INT -> " + numBands + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
final int xOff = yOff + x;
int element = dataBuffer.getElem(b, xOff);
for (int s = 0; s < numBands; s++) {
buffer.put((byte) ((element >> bitOffsets[s]) & 0xff));
}
}
flushBuffer(buffer, stream);
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
}
}
break;
default:
throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
}
}
// TODO: Need to flush/start new compression for each row, for proper LZW/PackBits/Deflate/ZLib
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
// TODO: Report better progress
processImageProgress((100f * yTile) / maxYTiles);
}
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.close();
}
processImageComplete();
}
// TODO: Would be better to solve this on stream level... But writers would then have to explicitly flush the buffer before done.
private void flushBuffer(final ByteBuffer buffer, final DataOutput stream) throws IOException {
buffer.flip();
stream.write(buffer.array(), buffer.arrayOffset(), buffer.remaining());
buffer.clear();
}
// Metadata
@Override
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
return null;
}
@Override
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
return null;
}
// Param
@Override
public ImageWriteParam getDefaultWriteParam() {
return new TIFFImageWriteParam();
}
// Test
public static void main(String[] args) throws IOException {
int argIdx = 0;
// TODO: Proper argument parsing: -t <type> -c <compression>
int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1;
int compression = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : 0;
if (args.length <= argIdx) {
System.err.println("No file specified");
System.exit(1);
}
File file = new File(args[argIdx++]);
BufferedImage original;
// BufferedImage original = ImageIO.read(file);
ImageInputStream inputStream = ImageIO.createImageInputStream(file);
try {
Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream);
if (!readers.hasNext()) {
System.err.println("No reader for: " + file);
System.exit(1);
}
ImageReader reader = readers.next();
reader.setInput(inputStream);
ImageReadParam param = reader.getDefaultReadParam();
param.setDestinationType(reader.getRawImageType(0));
if (param.getDestinationType() == null) {
Iterator<ImageTypeSpecifier> types = reader.getImageTypes(0);
while (types.hasNext()) {
ImageTypeSpecifier typeSpecifier = types.next();
if (typeSpecifier.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) {
param.setDestinationType(typeSpecifier);
}
}
}
System.err.println("param.getDestinationType(): " + param.getDestinationType());
original = reader.read(0, param);
}
finally {
inputStream.close();
}
System.err.println("original: " + original);
// BufferedImage image = original;
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_RGB);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_BGR);
// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
BufferedImage image;
if (type < 0 || type == original.getType()) {
image = original;
}
else if (type == BufferedImage.TYPE_BYTE_INDEXED) {
// image = ImageUtil.createIndexed(original, 256, null, ImageUtil.COLOR_SELECTION_QUALITY | ImageUtil.DITHER_DIFFUSION_ALTSCANS);
image = ImageUtil.createIndexed(original, 256, null, ImageUtil.COLOR_SELECTION_FAST | ImageUtil.DITHER_DIFFUSION_ALTSCANS);
}
else {
image = new BufferedImage(original.getWidth(), original.getHeight(), type);
Graphics2D graphics = image.createGraphics();
try {
graphics.drawImage(original, 0, 0, null);
}
finally {
graphics.dispose();
}
}
original = null;
File output = File.createTempFile(file.getName().replace('.', '-'), ".tif");
// output.deleteOnExit();
System.err.println("output: " + output);
TIFFImageWriter writer = new TIFFImageWriter(null);
// ImageWriter writer = ImageIO.getImageWritersByFormatName("PNG").next();
// ImageWriter writer = ImageIO.getImageWritersByFormatName("BMP").next();
ImageOutputStream stream = ImageIO.createImageOutputStream(output);
try {
writer.setOutput(stream);
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
// param.setCompressionType("None");
// param.setCompressionType("PackBits");
// param.setCompressionType("ZLib");
param.setCompressionType(param.getCompressionTypes()[compression]);
// if (compression == 2) {
// param.setCompressionQuality(0);
// }
System.err.println("compression: " + param.getLocalizedCompressionTypeName());
long start = System.currentTimeMillis();
writer.write(null, new IIOImage(image, null, null), param);
System.err.println("Write time: " + (System.currentTimeMillis() - start) + " ms");
}
finally {
stream.close();
}
System.err.println("output.length: " + output.length());
// TODO: Support writing multipage TIFF
// ImageOutputStream stream = ImageIO.createImageOutputStream(output);
// try {
// writer.setOutput(stream);
// writer.prepareWriteSequence(null);
// for(int i = 0; i < images.size(); i ++){
// writer.writeToSequence(new IIOImage(images.get(i), null, null), null);
// }
// writer.endWriteSequence();
// }
// finally {
// stream.close();
// }
// writer.dispose();
image = null;
BufferedImage read = ImageIO.read(output);
System.err.println("read: " + read);
TIFFImageReader.showIt(read, output.getName());
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import java.io.IOException;
import java.util.Locale;
/**
* TIFFImageWriterSpi
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriterSpi.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/
public final class TIFFImageWriterSpi extends ImageWriterSpiBase {
// TODO: Implement canEncodeImage better
public TIFFImageWriterSpi() {
super(new TIFFProviderInfo());
}
@Override
public boolean canEncodeImage(final ImageTypeSpecifier type) {
// TODO: Test bit depths compatibility
return true;
}
@Override
public ImageWriter createWriterInstance(final Object extension) throws IOException {
return new TIFFImageWriter(this);
}
@Override
public String getDescription(final Locale locale) {
return "Aldus/Adobe Tagged Image File Format (TIFF) image writer";
}
}

View File

@ -0,0 +1,29 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* TIFFProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: TIFFProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class TIFFProviderInfo extends ReaderWriterProviderInfo {
protected TIFFProviderInfo() {
super(
TIFFProviderInfo.class,
new String[] {"tiff", "TIFF"},
new String[] {"tif", "tiff"},
new String[] {
"image/tiff", "image/x-tiff"
},
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi"},
"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriter",
new String[] {"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@ -0,0 +1,574 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.LittleEndianDataOutputStream;
import org.junit.Test;
import java.io.*;
import java.nio.ByteOrder;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
/**
* HorizontalDifferencingStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: HorizontalDifferencingStreamTest.java,v 1.0 02.12.13 09:50 haraldk Exp$
*/
public class HorizontalDifferencingStreamTest {
@Test
public void testWrite1SPP1BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 24, 1, 1, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x5e);
stream.write(0x1e);
stream.write(0x78);
// 1 sample per pixel, 1 bits per sample (mono/indexed)
byte[] data = {
(byte) 0x80, 0x00, 0x00,
0x71, 0x11, 0x44,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP2BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 16, 1, 2, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x41);
stream.write(0x6b);
stream.write(0x05);
stream.write(0x0f);
// 1 sample per pixel, 2 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xc0, 0x00, 0x00, 0x00,
0x71, 0x11, 0x44, (byte) 0xcc,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP4BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 8, 1, 4, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x77);
stream.write(0x89);
stream.write(0xd1);
stream.write(0xd9);
// Row 3
stream.write(0x00);
stream.write(0x01);
stream.write(0x22);
stream.write(0x00);
// 1 sample per pixel, 4 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xf0, 0x00, 0x00, 0x00,
0x70, 0x11, 0x44, (byte) 0xcc,
0x00, 0x01, 0x10, (byte) 0xe0
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 1, 8, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
stream.write(0xff);
// Row 2
stream.write(0x7f);
stream.write(0x80);
stream.write(0x84);
stream.write(0x80);
// Row 3
stream.write(0x00);
stream.write(0x7f);
stream.write(0xfe);
stream.write(0x7f);
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWriteArray1SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 1, 8, ByteOrder.BIG_ENDIAN);
stream.write(new byte[] {
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
0x7f, (byte) 0x80, (byte) 0x84, (byte) 0x80,
0x00, 0x7f, (byte) 0xfe, 0x7f,
});
// 1 sample per pixel, 8 bits per sample (gray/indexed)
byte[] data = {
(byte) 0xff, 0, 0, 0,
0x7f, 1, 4, -4,
0x00, 127, 127, -127
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite1SPP32BPS() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(16);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 32, ByteOrder.BIG_ENDIAN);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = bytes.createInputStream();
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(0, dataIn.readInt());
assertEquals(-916259688, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite1SPP32BPSLittleEndian() throws IOException {
// 1 sample per pixel, 32 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(16);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 32, ByteOrder.LITTLE_ENDIAN);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeInt(0x00000000);
dataOut.writeInt(305419896);
dataOut.writeInt(305419896);
dataOut.writeInt(-610839792);
InputStream in = bytes.createInputStream();
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readInt());
assertEquals(305419896, dataIn.readInt());
assertEquals(0, dataIn.readInt());
assertEquals(-916259688, dataIn.readInt());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite1SPP64BPS() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(32);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 64, ByteOrder.BIG_ENDIAN);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = bytes.createInputStream();
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
assertEquals(-245956587649460685L, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite1SPP64BPSLittleEndian() throws IOException {
// 1 sample per pixel, 64 bits per sample (gray)
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(32);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 64, ByteOrder.LITTLE_ENDIAN);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeLong(0x00000000);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(81985529216486895L);
dataOut.writeLong(-163971058432973790L);
InputStream in = bytes.createInputStream();
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readLong());
assertEquals(81985529216486895L, dataIn.readLong());
assertEquals(0, dataIn.readLong());
assertEquals(-245956587649460685L, dataIn.readLong());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite3SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 3, 8, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0x00);
stream.write(0x7f);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
stream.write(0xfa);
stream.write(0xfb);
stream.write(0x7a);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
// Row 2
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x84);
stream.write(0x84);
stream.write(0x84);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
// Row 3
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x7f);
stream.write(0x81);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x00);
stream.write(0x7f);
// 3 samples per pixel, 8 bits per sample (RGB)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, -1, -1, -1, -4, -4, -4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 1, 1, 1, 4, 4, 4, -4, -4, -4,
0x00, 0x00, 0x00, 127, -127, 0, -127, 127, 0, 0, 0, 127,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWrite3SPP16BPS() throws IOException {
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(24);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 3, 16, ByteOrder.BIG_ENDIAN);
DataOutput dataOut = new DataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = bytes.createInputStream();
DataInput dataIn = new DataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite3SPP16BPSLittleEndian() throws IOException {
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(24);
OutputStream out = new HorizontalDifferencingStream(bytes, 4, 3, 16, ByteOrder.LITTLE_ENDIAN);
DataOutput dataOut = new LittleEndianDataOutputStream(out);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(4660);
dataOut.writeShort(30292);
dataOut.writeShort(4660);
dataOut.writeShort(-9320);
dataOut.writeShort(-60584);
dataOut.writeShort(-9320);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(0x0000);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(30292);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
dataOut.writeShort(-60584);
InputStream in = bytes.createInputStream();
DataInput dataIn = new LittleEndianDataInputStream(in);
// Row 1
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(4660, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(51556, dataIn.readUnsignedShort());
// Row 2
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(30292, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(0, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
assertEquals(40196, dataIn.readUnsignedShort());
// EOF
assertEquals(-1, in.read());
}
@Test
public void testWrite4SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 4, 8, ByteOrder.BIG_ENDIAN);
// Row 1
stream.write(0xff);
stream.write(0x00);
stream.write(0x7f);
stream.write(0x00);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
stream.write(0xff);
stream.write(0xfa);
stream.write(0xfb);
stream.write(0x7a);
stream.write(0xfb);
stream.write(0xfe);
stream.write(0xff);
stream.write(0x7e);
stream.write(0xff);
// Row 2
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x7f);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x84);
stream.write(0x84);
stream.write(0x84);
stream.write(0x84);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
stream.write(0x80);
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
assertArrayEquals(data, bytes.toByteArray());
}
@Test
public void testWriteArray4SPP8BPS() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 4, 8, ByteOrder.BIG_ENDIAN);
stream.write(
new byte[] {
(byte) 0xff, 0x00, 0x7f, 0x00,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
(byte) 0xfa, (byte) 0xfb, 0x7a, (byte) 0xfb,
(byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
0x7f, 0x7f, 0x7f, 0x7f,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x84, (byte) 0x84, (byte) 0x84, (byte) 0x84,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
}
);
// 4 samples per pixel, 8 bits per sample (RGBA)
byte[] data = {
(byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
};
assertArrayEquals(data, bytes.toByteArray());
}
}

View File

@ -26,15 +26,18 @@ package com.twelvemonkeys.imageio.plugins.tiff;/*
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.io.enc.Decoder;
import com.twelvemonkeys.io.enc.DecoderAbstractTestCase;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.Encoder;
import org.junit.Ignore;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import static org.junit.Assert.*;
@ -47,6 +50,8 @@ import static org.junit.Assert.*;
*/
public class LZWDecoderTest extends DecoderAbstractTestCase {
public static final int SPEED_TEST_ITERATIONS = 1024;
@Test
public void testIsOldBitReversedStreamTrue() throws IOException {
assertTrue(LZWDecoder.isOldBitReversedStream(getClass().getResourceAsStream("/lzw/lzw-short.bin")));
@ -78,13 +83,6 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
int data;
try {
// long toSkip = 3800;
// while ((toSkip -= expected.skip(toSkip)) > 0) {
// }
// toSkip = 3800;
// while ((toSkip -= actual.skip(toSkip)) > 0) {
// }
while ((data = actual.read()) != -1) {
count++;
@ -106,7 +104,28 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
@Override
public Encoder createCompatibleEncoder() {
// Don't have an encoder yet
// TODO: Need to know length of data to compress in advance...
return null;
}
@Ignore
@Test(timeout = 3000)
public void testSpeed() throws IOException {
byte[] bytes = FileUtil.read(getClass().getResourceAsStream("/lzw/lzw-long.bin"));
for (int i = 0; i < SPEED_TEST_ITERATIONS; i++) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayInputStream input = new ByteArrayInputStream(bytes);
LZWDecoder decoder = new LZWDecoder.LZWSpecDecoder();
int read, total = 0;
while((read = decoder.decode(input, buffer)) > 0) {
buffer.clear();
total += read;
}
assertEquals(49152, total);
}
}
}

Some files were not shown because too many files have changed in this diff Show More