Merge pull request #1 from haraldk/master
Update from haraldk/TwelveMonkeys
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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 + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public class IFFImageReader extends ImageReaderBase {
|
||||
private DataInputStream byteRunStream;
|
||||
|
||||
public IFFImageReader() {
|
||||
super(IFFImageReaderSpi.sharedProvider());
|
||||
super(new IFFImageReaderSpi());
|
||||
}
|
||||
|
||||
protected IFFImageReader(ImageReaderSpi pProvider) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Adobe d<> <01><> <20>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#"""#''''''''''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
!! !!''''''''''<27><> &<01>" <01><><01>
|
||||
|
||||
s !1AQa"q<>2<><32><15>B#<23>R<EFBFBD><52>3b<>$r<><72>%C4S<34><53><EFBFBD>cs<63>5D'<27><><EFBFBD>6Tdt<64><74><EFBFBD>&<26>
|
||||
<19><>EF<45><46>V<EFBFBD>U(<1A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>eu<65><75><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>fv<66><76><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>7GWgw<67><77><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>8HXhx<68><78><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)9IYiy<69><79><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*:JZjz<6A><7A><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> m !1AQa"q<><71>2<EFBFBD><32><EFBFBD><14><><EFBFBD>#BRbr<62>3$4C<34><16>S%<25>c<EFBFBD><63>s<>5<EFBFBD>D<EFBFBD>T<>
|
||||
&6E'dtU7<55><37><EFBFBD><EFBFBD>()<29><><EFBFBD><F38494A4><EFBFBD><EFBFBD><EFBFBD>eu<65><75><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>FVfv<66><76><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>GWgw<67><77><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>8HXhx<68><78><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>9IYiy<69><79><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*:JZjz<6A><7A><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ? <20><>Zo<5A>,<01><>eu<65>"<22>p0DS=p<><13><><EFBFBD>S5<53>-<2D><><EFBFBD>!#<0B>;<3B><> <20>M1B<31>϶,<2C> <14><><EFBFBD>m<EFBFBD><6D><EFBFBD>a<EFBFBD>U8<55>Š<>)<15><><EFBFBD><EFBFBD> Jb<4A><62>"<22>lNi<4E><69><EFBFBD><EFBFBD><EFBFBD>Eۉ <15>^<5E><><EFBFBD>SY<53>}͈<><CD88><0F><><EFBFBD><EFBFBD>g!<21>U<EFBFBD>o$<24><>*<2A>d<EFBFBD><64>~j<>F<EFBFBD>hn~(.<15>p<EFBFBD><70><EFBFBD>L<EFBFBD><4C><0F>:AC <20>U<EFBFBD>9<EFBFBD>8<EFBFBD><38><EFBFBD><EFBFBD><EFBFBD>Q<EFBFBD><51><EFBFBD>E<EFBFBD><13><><EFBFBD><EFBFBD>wo~<7E><>A<EFBFBD><41>O<EFBFBD><1A><>T<EFBFBD><54><EFBFBD><EFBFBD>$V=93n><3E><>s<EFBFBD>#sL<D883>z<EFBFBD><7A>M<13><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><1D>l<EFBFBD><6C><12><><EFBFBD><EFBFBD>`<06>2<EFBFBD>0C<30><43>*<2A><>6<EFBFBD>h(d"<22>?<3F>F<EFBFBD>+Y|<7C><><EFBFBD><EFBFBD><EFBFBD>?R<><52>n@N<><15><><EFBFBD><EFBFBD>Ei?<1C>P\<5C><>c<EFBFBD> <20><>lù<6C>$%<25><18>T^<5E>%<25>"<22><1F>
|
||||
MX<><58><EFBFBD>nISڇ"<22><>Cv<><76>m<14><18>"<22><><7F><EFBFBD><EFBFBD>쟻8*<2A><>Cpi<70>f<06><>s;<1F><0F><>=b<><62>&<26>e%U'<27>Ν<EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD>0<EFBFBD>N<EFBFBD>،<EFBFBD>X<><58>v<EFBFBD><76><EFBFBD>s<EFBFBD>Z<0F>hr»<72>Z<EFBFBD><5A>̬<EFBFBD>ly<<3C>X<EFBFBD>(<28>p<EFBFBD>^<5E>˨<EFBFBD><CBA8>dm<64>9=<3D><1E>q6<71>bIf<49> nI<6E><49><17><><EFBFBD>(<28>ڼ<EFBFBD>M*`<60>o<>G<47>s[C-<2D><><EFBFBD>yk#<23>pDq<44>s<1C><><EFBFBD><EFBFBD><EFBFBD>es1<73><10><>A<EFBFBD>o<EFBFBD>9<EFBFBD>-4<>-i<>Zl<5A><6C><EFBFBD>r<EFBFBD><72>><3E>^?<3F>:<3A>̶P7{A<>u<EFBFBD>B}<14>b<EFBFBD><62><EFBFBD>8y{RQ,<2C><><EFBFBD> C<>'<17>59ER&o<><6F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>m<EFBFBD>'<27><18> <0C><><EFBFBD>d<EFBFBD>Z<EFBFBD>dyDlF<6C><46><EFBFBD><EFBFBD><><7F><16><>~<7E>-OՑ<4F><D591>_<EFBFBD>H<EFBFBD>N<EFBFBD><18> ?K<>f<EFBFBD>^<5E><18><>ȣ<EFBFBD>
|
||||
<EFBFBD>R<EFBFBD><EFBFBD>4X<EFBFBD><EFBFBD>`Nz=G<7F><Vu<56><75>_<0F>J<EFBFBD>1<1A>M<EFBFBD>=z<11><><EFBFBD>8<EFBFBD>sK<73><4B>L%<25><>?<3F><><EFBFBD><EFBFBD>M<EFBFBD>cZ<63>.>jG<6A><47>ˍ<0E>
|
||||
<EFBFBD><EFBFBD><EFBFBD>g<EFBFBD><EFBFBD><EFBFBD><EFBFBD>C\<5C>k66<36>W<EFBFBD><57><EFBFBD>m<EFBFBD>q<03>J6<18>Hˬ<48>a<EFBFBD><1C><19><><EFBFBD><0F><><EFBFBD><EFBFBD>']<5D><1F>(E <20>JFu<46><75><EFBFBD>2<EFBFBD><02>'<27>R?<3F><02>H<EFBFBD>}<7D>!<21><18>F<<3C>ɢ><3E><><EFBFBD>X<EFBFBD><58><EFBFBD><EFBFBD>Wl<02><>1<EFBFBD><31>ի<EFBFBD>b<EFBFBD>Nˠ<>[=@<40><>8<EFBFBD>`<60>s<EFBFBD><73><EFBFBD><EFBFBD>ͷ<01><><EFBFBD>X<EFBFBD><58><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Nh<4E><68>H<0C>8e<05><0C><>`<60>P:p#n<>
|
||||
{1H<>&U<><55><EFBFBD>Z<EFBFBD><18> t<>lM<6C>`<60><11><1D>$ņء<C586> QA<><41><EFBFBD>\<5C>\R<>D<EFBFBD><44>-Z<><5A><EFBFBD>OA<4F><41><EFBFBD>lUҎ<>G<EFBFBD><47><EFBFBD>*<2A><><EFBFBD><EFBFBD>b<EFBFBD><62>lg<12>E,<2C>!<21>(<28>!<1B><0C><>b<EFBFBD>ʇ<02>;⨲<14>p;<3B>7<EFBFBD><37><EFBFBD><EFBFBD>04<30><34><EFBFBD>D<17>4<EFBFBD><34>;`5<><35><EFBFBD>g<EFBFBD><67><EFBFBD>*r=[;<3B>]<5D>,<2C>5<EFBFBD>\+\YV<59><56>W b<><62>E3ez<65><7A>lU,<2C><>S<EFBFBD><53><EFBFBD><EFBFBD>D<>1E <0B><>/<2F><>Q<EFBFBD><51>ؼr<D8BC><72>C<><EFBFBD><7F>1T<31>;<3B><><EFBFBD><13>A<1C><>-f<><66>MRA<52>l<16><>o<EFBFBD>KpF <09>螸<EFBFBD>l<EFBFBD><05><><EFBFBD>. ]<5D> .<2E><><EFBFBD><EFBFBD>[SH<>PhA<68><41><EFBFBD>sx<73><78><EFBFBD>/<2F><>r<EFBFBD><72><EFBFBD>k<>o<EFBFBD><6F> <20>G;/<2F><>?W<><07><>ٓ<EFBFBD>&<26><0E> |Ak<41>0~3<>a<EFBFBD><61><EFBFBD>4gj9<6A>y'<27><>V6<56>]<5D> <20><><EFBFBD>H><3E>Gj<47><07>[<5B><>q<EFBFBD><71><EFBFBD><0F><> <20>~<7E>q<EFBFBD><71>E<EFBFBD><45><EFBFBD><19><>\<5C>ϼU?͖J<CD96>L<EFBFBD><4C>qn>H<1C>{<7B><17>A *<2A>.<2E><>3F$z<><7A><EFBFBD><EFBFBD>5++<2B>*<2A>[<1B><>S<EFBFBD><53>YXS<58>I<<3C><>Ki2p<32>At<>$KC<><43><EFBFBD>Q|<7C><>_K<5F><4B><EFBFBD>]iw<69>EVxD<78><44> <20><>^y<><79> D<><44><EFBFBD>w<EFBFBD>yNz<0F>䗖<n<>g<EFBFBD><67>i<><69>\<0B>iI/w<<0C><><EFBFBD>j*H<>.<2E>1,y=]ջV\<5C>ㄇ/<2F><><<3C><><EFBFBD>b<EFBFBD>d<EFBFBD>Y<><1F>C<EFBFBD><43><EFBFBD><1D><>h<EFBFBD>M<EFBFBD>@<40>X<EFBFBD>3<01><><EFBFBD>yțy<C89B><79>ސ<EFBFBD>Y<EFBFBD><59>>RBP <20><13><><EFBFBD><EFBFBD><14><>Pr
|
||||
<EFBFBD>W<EFBFBD>bKU#*`<60>Gi<47><69>2<EFBFBD>c9<13>D<EFBFBD><44>r}t<><19>91<39>n<EFBFBD>@[}<7D>j<><6A>k<12><><EFBFBD><EFBFBD><EFBFBD>Y<EFBFBD><59>玧b<E78EA7>W4<57>><3E><>i<EFBFBD><69>_<EFBFBD>m<EFBFBD><6D>!<21>䮀/?<3F>
|
||||
;<3B>FfR<66>=/<2F><><1E>4<><34>B <20><19>Re<><65><EFBFBD>l<02><><EFBFBD><EFBFBD><14><>I=<3D><15><>?w<><77>d<EFBFBD>/<2F><>Ġ<>l
|
||||
<EFBFBD><EFBFBD>EkЩ
|
||||
<EFBFBD>0X<EFBFBD>6s<36>i<EFBFBD>x%!!<21>.?sW<73>M<EFBFBD>gf<67><66>1<EFBFBD><31><EFBFBD>
|
||||
G<EFBFBD> p<><70><EFBFBD>v(o<16><07>K-/ྏԁ<E0BE8F><06><1D><><1C><>8(<11>i<EFBFBD>8<EFBFBD>6<EFBFBD><36>N<EFBFBD><06>I<EFBFBD><49><EFBFBD>k<EFBFBD>T<EFBFBD>*<2A>1k?8\D1<>rq<72><71>S_<53><5F>3<EFBFBD>&<26>4 <20><><EFBFBD>5<EFBFBD><35>#<23>dZm<5A><6D><EFBFBD><EFBFBD><EFBFBD>J<1B><>2<<3C>W<><57><EFBFBD><EFBFBD><EFBFBD><1C><>,<2C><>!<21><><EFBFBD><EFBFBD>t<0C><><EFBFBD>D<EFBFBD><44><EFBFBD>+<2B><><EFBFBD><7F><EFBFBD>8<EFBFBD><38>=<3D>|e`<01><>)<29><>c<EFBFBD><63>Ү<EFBFBD>Y"<22><><EFBFBD><EFBFBD><EFBFBD> <1F>"^e<>Z<EFBFBD>p<EFBFBD><70>Ȯ9<C8AE><39>ޅk<DE85><6B>9<EFBFBD>sɫ<73>)d<>XKk"<22><><EFBFBD>H<EFBFBD>LiRV<><56><EFBFBD><EFBFBD>v<EFBFBD><76><EFBFBD> <09>x<EFBFBD>5?H<>o.<2E>Y.[<5B>P<><50><EFBFBD>n4-<2D>.$&<26><><0C><01>$ہ=>i<>f<EFBFBD>1<18><><EFBFBD>G<EFBFBD><02>q<EFBFBD>(qIB<49><0E>no$<24><>=ʸH<CAB8>CF8<19><>4KA?Tx<54>\zZB<5A><42>, <1F><><EFBFBD><EFBFBD>;<3B>m<EFBFBD><6D><EFBFBD><EFBFBD>đHAV<41><56>}<7D>LNQ<4E><51>t<EFBFBD><74>V<EFBFBD>m<EFBFBD><11><><EFBFBD><10><10>\<5C>8>+<2B><> <20><><EFBFBD><EFBFBD>̞?<eŻ<65><C5BB><r<><72><17><>k<EFBFBD>j"(ԺıۋE<DB8B><45><EFBFBD>
|
||||
n<EFBFBD>)<29>%O Uy9'1<>4F<>H)<29>W;a\<5C><><EFBFBD>B<>#<1E><><EFBFBD><14><>GN*<2A>7Z`<60><>f<EFBFBD><66>-k<02><15>(m֕C<>n<EFBFBD>S=8Ќx<>u<18>U:ҧ<><13><>c<EFBFBD>w<EFBFBD> <20><><EFBFBD><13><><EFBFBD><0C><>7<EFBFBD>^<5E><><13><><EFBFBD>Q!<21>s;`R<7F>FJ⪛W7*b<<3C>r<EFBFBD>U<EFBFBD><55>
|
||||
<EFBFBD><EFBFBD><EFBFBD>6<EFBFBD><EFBFBD>"<22><><EFBFBD>>B<><42>T>^YCo,<1C>eUP66<36><36>+<2B>\U<1B><>z<EFBFBD>z<EFBFBD>1t<31><74>U6<><10>xؾ.\P<><50>N6A<36>)<29>:<3A>n<EFBFBD><6E>U
|
||||
<EFBFBD>I<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
=<<3C><>K<EFBFBD>Z<EFBFBD>q<EFBFBD>t*<2A><><EFBFBD>s<EFBFBD><73><EFBFBD><EFBFBD><EFBFBD> ߉<06><>M<1A>/7y<1B><><EFBFBD>Ml<4D>J<EFBFBD><4A>v;f^<5E> y<>"^Q<>ֹ<EFBFBD><1E>^<5E><>y<EFBFBD>Q<1A><><ác<C3A1>
|
||||
<20><><EFBFBD><EFBFBD>;<3B>g1<67><0C><>F
|
||||
<EFBFBD><EFBFBD>eӵ <09><>ƕ<>4]<16>U<EFBFBD>H<EFBFBD>B܈ <01>')<29>)<0C><><EFBFBD><D883>,<2C>Ǐ<EFBFBD>G`<1E><><EFBFBD><EFBFBD>hhF̺rZ<><5A>X<EFBFBD>z<EFBFBD><7A>:<3A><><EFBFBD>-tؒ<74>U@<40>!<07><><EFBFBD> <20><0E><><EFBFBD>m<14><>k<EFBFBD>v<12>U|~xs<78><73>]]X<>6;J<>iU<69><55>V\<5C>|&<26><><EFBFBD>:<3A><>'<27>f<12><>F;<3B><><EFBFBD>w<EFBFBD>ίXӁE<D381>e@<40><><EFBFBD><02>5eG<1B>9<EFBFBD>yg\U?<08>߈q<DF88>Ykz<6B><7A>p<EFBFBD><70><EFBFBD><EFBFBD>_<EFBFBD><5F><0F><1A>ʸ<EFBFBD><CAB8><EFBFBD><EFBFBD>p<EFBFBD><70>3<EFBFBD>Q<>X<EFBFBD><58>%<25><><EFBFBD>]}<7D>i<EFBFBD><69>i<EFBFBD>^g<><67>f<EFBFBD><66>D<EFBFBD>|<7C>t<EFBFBD><74><EFBFBD>@<40><13><>>{o<><6F>լ<EFBFBD><D5AC><<><F3AA8EB4><EFBFBD><EFBFBD><EFBFBD>7s<37>{w.<2E><>
|
||||
<EFBFBD><01><19>1h<31><68><EFBFBD>c<19><>$o<>H<1B>ӈ [<5B><>/<0F><><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>e<EFBFBD><65>L̅<4C>'<27><>G<EFBFBD><47><EFBFBD><EFBFBD>4<EFBFBD><34><EFBFBD><EFBFBD>Lx<4C><78><EFBFBD>r-5<><35><1C><>eX<65><58>wܪ<77><DCAA><EFBFBD>Ⴤ><3E><01><><EFBFBD><EFBFBD>L<EFBFBD>.?w<7F><77>#W֜%<25>,<2C>><3E>Q<EFBFBD><51><EFBFBD>^a<>aq$/<2F>v
|
||||
<EFBFBD><07>gO<67><4F><EFBFBD><EFBFBD><EFBFBD>6<><36>Y<><59><13><><EFBFBD><EFBFBD>B<EFBFBD>)ene$n<><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_̘1<>÷<><08>U<EFBFBD>Ht<48><74><EFBFBD>><3E><><EFBFBD>BG<42>oî;Q<><0C><12><>4<EFBFBD>˾F<CBBE><46>gZ<67><5A>=<3D>Ԛ<EFBFBD>d<EFBFBD>
|
||||
m<EFBFBD><<3C><><EFBFBD><EFBFBD>t<EFBFBD><74>X<EFBFBD><58>?
|
||||
<EFBFBD><EFBFBD>|<7C><><EFBFBD> <20>*˅Y<>81<38><31><04><>fOÐG<C390><47>u֬<75><D6AC><EFBFBD><EFBFBD><EFBFBD><14>Z}8><3E><>3<EFBFBD><17>o/<2F>˻''<27><><EFBFBD><0E><><EFBFBD>KT<4B>Z!<0B>ƾM<><4D><EFBFBD>%<25><>><3E>
|
||||
<EFBFBD><EFBFBD>v<0C>!̧&-&p<>&yޓ<>?w<>-<2D>ծt<D5AE>h~
|
||||
<EFBFBD>H<EFBFBD>+<2B>.|<7C>n<EFBFBD>b<><62><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,q4<17><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӧ<EFBFBD><D3A7><EFBFBD>a<EFBFBD><EFBFBD>,<2C>kO
|
||||
<EFBFBD><EFBFBD>2<EFBFBD>и<><D0B8> <20>嘏p<17><>L<1F>/s2<73><04>f4<66><34> }<7D>
|
||||
R<EFBFBD>HLR<EFBFBD>*<2A>'<27>0<EFBFBD>"<22><>OZ*<2A>;<3B> -<2D>5<EFBFBD><35>2P<32>eaU&<26><>N2<1C>܍~<7E>U=FA<46>O9<4F>F<EFBFBD>R<EFBFBD><52>Od]<5D> <20>E<EFBFBD><45>;T<>ԁh<D481><08>b<EFBFBD><62><EFBFBD>"<22>PI<50>z<EFBFBD><7A><EFBFBD><EFBFBD>VU-<2D>N<EFBFBD><4E>
|
||||
g<04>H<EFBFBD>s<11>z^<5E> b5<16><>><07>]Mwh<77> <20>d<EFBFBD>_Y<5F>r<EFBFBD><18>w<EFBFBD>3<EFBFBD>[<5B>ڮ<01>q<EFBFBD><71><EFBFBD>Jt<4A>Q<EFBFBD>$Z<><5A>E$<24>(*yFz<46>t<EFBFBD><74>*<2A><><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>'
|
||||
VDqU;a<><61>`#>A<1C>xb`9<><39><EFBFBD>I<EFBFBD><49>`<60> d<><64><EFBFBD><0B>H
|
||||
F<EFBFBD>u<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>#<23><10><>;<3B>,<2C><><EFBFBD><EFBFBD><EFBFBD> L<>/b<><62>?<3F><><EFBFBD>q<EFBFBD><71><EFBFBD><12>c<07><06><1D><06>*<2A>3;SaE<61>]<5D><>
|
||||
H<EFBFBD>
|
||||
<EFBFBD><12><><EFBFBD>/#<23><>8<1C>Z<EFBFBD>U7<55><37>5<>G-<2D>Ep<45>+<2B><0B>)V<>vWۦZ<DBA6>
|
||||
<EFBFBD><EFBFBD><EFBFBD>
|
||||
N<EFBFBD>B<><42>*<2A><>ND<4E> ec\$<24><>p<1C> <09><>P<EFBFBD>)<29>1<EFBFBD>]<5D><03>*<2A>9Yy<59>V<EFBFBD>W6lU<6C><55><EFBFBD>x<>uڙ<75>f<EFBFBD>W<EFBFBD>\<5C><><EFBFBD><EFBFBD><EFBFBD>N*<2A><>N8<4E>b<EFBFBD>lqT<>\1<>q<EFBFBD><71>qTLr<4C>7<13><><EFBFBD><EFBFBD>R{<7B>i
|
||||
02<EFBFBD>l<EFBFBD>lU<EFBFBD>rs<EFBFBD><EFBFBD>I$<24><><EFBFBD>vbPG@<40><>9<EFBFBD>UY<55>"<22><><EFBFBD>z3<7A><33>O/<2F>}G<>f<EFBFBD>4<03> <20>ܿ <11>I<1D><01>-9<>՞V~<01><><EFBFBD>V<EFBFBD><56>7<EFBFBD>n<11>4cɺ($<24><>J<EFBFBD><4A>|<7C>(<28><>Kx<07> ]<5D><><EFBFBD><EFBFBD><1C>Gykap<61>0<EFBFBD>%<25><>4oݏ<6F><DD8F><18>ol<6F>ZhQ<68><51>*<2A> 9<><39>g$<24><>_+<2B>:l<>&k1<6B>8<EFBFBD>.D\c<>]<5D>iU<69><55>M<EFBFBD><4D><EFBFBD>Y[<5B><><EFBFBD>O<10><><EFBFBD><EFBFBD><12>*<2A><>
|
||||
~#<23>#<23>y<EFBFBD><79>~<02>(<28>[~<0B><><EFBFBD><EFBFBD>a<><61>Kg<4B><67><EFBFBD>VU<56>N<EFBFBD><4E> <20>|<7C><0C><04>W<1E><0E>Lg<4C>W<EFBFBD><57>'<18>FO<46><4F><EFBFBD><EFBFBD>/0Y<30>~<7E>-<2D>T<<3C><><EFBFBD>iP:<1A>
|
||||
<EFBFBD><EFBFBD>}<7D>KI=p y<08>?<3F>8<><38>[86`! <20><>SV<53>o<10><><1A><>0<04><>L<EFBFBD>rGH<47>ґ<EFBFBD><D291>N<14><><13>n<EFBFBD><6E>¿S<7F><53><EFBFBD>Bѐ<42>h<EFBFBD>a<EFBFBD>Sw?><3E><>o7<6F><37>ly<6C>rI,Gv<47>9q<39>Pl3<6C><33><EFBFBD><EFBFBD>K0I<30>rkҧ <><C298><EFBFBD>/1<>ݙ<EFBFBD>)<29><>k<EFBFBD><6B><EFBFBD>0<EFBFBD><30>S<05>+ <09>E<EFBFBD><45><EFBFBD><EFBFBD>W<EFBFBD><57>HM<48>ٹ$<24><><EFBFBD>xS<78>0<EFBFBD>B<EFBFBD><42><EFBFBD>ՒL<D592>A <09><>9<1A>Q<><51>Y<EFBFBD>i3<69>Bj<42><6A><EFBFBD>o<EFBFBD>#껫<03>ל<EFBFBD><D79C><EFBFBD><1A><><1D><><EFBFBD><EFBFBD>wy=IT<><54>#r}<7D><><EFBFBD>ϥ<EFBFBD>1<EFBFBD>$<24><>z<EFBFBD><7A><EFBFBD><EFBFBD><EFBFBD><EFBFBD>q<18><>CFGq<><71>
|
||||
<EFBFBD><EFBFBD>B<EFBFBD>`<60><>:<3A><><1F><><EFBFBD><EFBFBD>'<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><14><1F> m+<2B><1D><04><>5<EFBFBD>Ef)2<1E><>_<EFBFBD><5F>j<EFBFBD>6<EFBFBD>F#<23>⨂<EFBFBD><E2A882>:<0F>F<EFBFBD><46><EFBFBD><0F><><EFBFBD><EFBFBD><15><>jF[V.@<40>p<EFBFBD>v<EFBFBD>Q.X<><58>ǟ<EFBFBD><C79F>w<EFBFBD>{<7B>)55댔<35><EB8C94><EFBFBD>i"U<><55>ͽoc\
|
||||
#<23><05><>
|
||||
<EFBFBD><EFBFBD><EFBFBD>30G<30><47>wN<11>Jɳv}<7D>+<2B><><EFBFBD>M<EFBFBD> #/}<7D>0e<30>ѻ<EFBFBD>KLj=0<04><1F>d<EFBFBD>Q<EFBFBD><51><EFBFBD><EFBFBD>g
|
||||
R4<EFBFBD>B
|
||||
<EFBFBD>( <20>2<EFBFBD>r߫<72><DFAB>Î8ǂ<38><C782> <11><>>+<2B>`;<3B>/<2F><11>6S<36><11>`<60><><EFBFBD><04>a<EFBFBD><12>$%Dr!Nc<><63>b<EFBFBD>B<EFBFBD><42> <20>3W<1A>=\<5C><>M<EFBFBD><4D>B^<5E>IfU<66><55><EFBFBD><EFBFBD>}<7D>>;<3B>/b"6<>۸ř#<23>
|
||||
<EFBFBD><0C><>aL<61>{<7B>?<3F>lIJ<49>;<3B><><EFBFBD><EFBFBD>"ߌc<DF8C>xO<78>c<EFBFBD><63>}<7D><>6K<36>е[<5B><>د$<24><><EFBFBD>~<7E>8p<38><70>bj{<7B>6<EFBFBD><14>خ<EFBFBD>'<1E><><EFBFBD><EFBFBD>8<>og<6F>S%<25><>A<>0 <20><>"<22>j<EFBFBD><0F><>ᅮ<EFBFBD>$`<16><><1C><>ò3R<33>.<04>6=p<>O<EFBFBD>P<EFBFBD><50>)<29>U<EFBFBD>۩<1B> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>d%{;^<5E><>q<EFBFBD>i<EFBFBD><69>q_<><04><>N<07><><EFBFBD><EFBFBD>,k\Bg<42><67>&<26><>$>8<><38>jR<6A><08>La<4C><14>d<EFBFBD><64><EFBFBD>=;<3B>F<EFBFBD><46>ac<61><63><EFBFBD>rO\M<><4D>9f<39>*<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]<5D><><EFBFBD>vlٱV<D9B1>8h<>R<EFBFBD><52>\sb<73>͊<EFBFBD><CD8A><EFBFBD>Q]<5D>͔t<>T<EFBFBD><54>\q<1B><1C><><EFBFBD>s<18><><EFBFBD><EFBFBD>f<>*<2A>Ylw<6C><77>W<01><><EFBFBD><EFBFBD>1j<31><6A>c<EFBFBD>X<EFBFBD>u8<75><38>Ɓ<EFBFBD><C681><EFBFBD>ʒ>pB<><42>e<EFBFBD>,r<>3<EFBFBD>E<EFBFBD>~T<>YV<59><56>IMi<4D><69><0C>6<EFBFBD>J<EFBFBD>of<6F><0E> A<>a<EFBFBD>tQ<74>|rIk|!F<19>w<EFBFBD>e+<2B><><EFBFBD>J<EFBFBD> <20><><EFBFBD><EFBFBD>'<17><><EFBFBD><0E><><EFBFBD>h<EFBFBD>.<2E><>!1<> m<>(<28>j<EFBFBD>-<2D>dx<64>Z%P<><50>i<>`[<5B>2^<5E>J,<2C><01>S<EFBFBD><01><><EFBFBD>N<EFBFBD><4E>k<EFBFBD><6B><0F><>K4<4B>;<3B><>v<EFBFBD><76><06><>Y;<3B><>Y<>H <20>C[<5B>FL<46>y<04>YD<><44>,<2C>J<EFBFBD>e<EFBFBD><65>o/<1B>U<EFBFBD>Fݞ<46><DD9E>$:<3A><>.<2E><p h<><68><EFBFBD>:<3A>!ߤ<><DFA4><EFBFBD>k+*vPi<50>m5<6D>b<EFBFBD><62><EFBFBD><11><><EFBFBD>}<7D>u<EFBFBD>/i|<7C><>`<60><><EFBFBD>9b1<62>|H<>)<29>6B<06><>[<1B>4[<5B>چ<EFBFBD><DA86>&vZ<76><5A><EFBFBD>`<60><>ߴ<EFBFBD><DFB4>Ӑ*Ԛ<1E><><EFBFBD>t9΅^MZ}^\#<23>O8<4F><38>O<EFBFBD>[ eya<>
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>f<EFBFBD>ys<1D><>zs<7A>
|
||||
P<02>o<EFBFBD><03>8<EFBFBD>K<EFBFBD><4B>bV<62><06>3<EFBFBD>D<06><>g<EFBFBD><67><EFBFBD>[<5B><>sd<73><64><EFBFBD><EFBFBD>X翚<E7BF9A>jkc<1B><><EFBFBD>&<26>1MGZ<47><5A><EFBFBD><EFBFBD>$F<><46><EFBFBD>F<EFBFBD><46>#W7<57>ۡf5<66>a<EFBFBD>-G]<5D><>B<EFBFBD><42><11><><EFBFBD><EFBFBD>x<EFBFBD><78>V̴<56><CCB4>Y<EFBFBD><59>D<EFBFBD><44>v:^<5E><>Ϧ_<CFA6><5F><EFBFBD>h<EFBFBD>
|
||||
7<1E>[<5B><>2 <20>"<22><>S<EFBFBD>qm3G<33><47><EFBFBD>9<EFBFBD><39>ԖAȫ<1D>=69<>M)<29>
|
||||
<EFBFBD><EFBFBD><EFBFBD>#<23><>2<EFBFBD>"
|
||||
<EFBFBD><EFBFBD><EFBFBD>3h <18><><EFBFBD><EFBFBD>I<>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD>zs<7A><73><EFBFBD><EFBFBD>08<30><38>V,*.S<>48O,i<>R<EFBFBD><52><EFBFBD><06><>]?<3F><><EFBFBD><EFBFBD>p<0F>`G<><47>&:NC<4E><43>]<5D>3<EFBFBD><33><EFBFBD><EFBFBD> X_ܛ\y<>N<EFBFBD><1F><><EFBFBD><EFBFBD>F]H<>2<EFBFBD>{`V><3E><>~<7E><>M<EFBFBD><4D>q<EFBFBD>#x<><78>!U<><55>k<EFBFBD>2c<32>i?<3F><>!<21>4<EFBFBD>OP<4F><50><EFBFBD>2<><32><EFBFBD><1F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>bœ<><C593>/<2F>{<1E><>+kh<6B><68>
|
||||
<EFBFBD>I<EFBFBD>8<EFBFBD>;<3B><>ƒ <20><><EFBFBD>0@ {<7B>9<EFBFBD>I&<26>o6j~e<><65><EFBFBD>6<EFBFBD>%<25>lX<6C><58><EFBFBD><EFBFBD>Ү<>/-<2D><><13><>i<EFBFBD>F<05>"zL<7A><4C>3N<1<>+<2B>~i57<35><37>0<EFBFBD>*<2A><><EFBFBD>za<7A>:J<><4A><EFBFBD>V<EFBFBD>p&<26><><EFBFBD><EFBFBD>$<03>U<EFBFBD><0F><0F>F<EFBFBD><46>N<EFBFBD>O<EFBFBD>6<EFBFBD><02><1B>)<29>}<7D>T$6<><><11><1D>c<1E><><EFBFBD><EFBFBD><EFBFBD>dv<64>~G{v;<3B> <20>pZ<70>H<EFBFBD><48><EFBFBD>S<EFBFBD>b$Wn<>:d<<3C><>zM<7A><4D><1A>
|
||||
8<EFBFBD><EFBFBD><EFBFBD>G<EFBFBD><EFBFBD><11>I=H<><48><EFBFBD>!<21><> <10>l<EFBFBD>*<2A><> }k<1C>A<EFBFBD><41><EFBFBD>y<EFBFBD>\|iC<69><43>mx<6D><78>S<EFBFBD>B@<40>:V9ى<39>R<><52>E<><45><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Vj<><6A><EFBFBD><EFBFBD>7<EFBFBD>E'<27><> <09><>P<EFBFBD>
|
||||
<EFBFBD><U<><55><EFBFBD>&<26>x<EFBFBD>G<EFBFBD><47><EFBFBD>m~<7E>E
|
||||
i<EFBFBD><02>"^@@<40>i<EFBFBD><69>Kޤ<4B><DEA4><EFBFBD>u<EFBFBD>Q<EFBFBD><51><EFBFBD><EFBFBD> $n\<R<> G!<21>8<EFBFBD><07><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>%}<7D>I<EFBFBD><EFBFBD>Y<EFBFBD>잣<EFBFBD>"<22><>e<EFBFBD><65>z<EFBFBD>.<2E>9<EFBFBD> <09>|<7C><>*LĜms<13>\.C<><43>͛vlٱWf͗LU<4C><55>t<EFBFBD>LU<4C><55>Lx.<2E><><EFBFBD>c<EFBFBD><63>1 b<><62><EFBFBD>\ت<>><18>&<26><><EFBFBD>N<EFBFBD><4E><EFBFBD>-z<><7A>S2WR*q27<32>-@1o<><6F>2q<>i<EFBFBD>)<29>+<2B><><11><>'<1B>⫭<EFBFBD>i<EFBFBD>XΓ<D7B9><CE93><EFBFBD><EFBFBD>ګR<DAAB>G\<5C>yv<79>4<EFBFBD><34><EFBFBD><EFBFBD>r\<5C>aUcNBN<42><4E>{g<<3C>Li<4C><69>L~<7E>z=%M6<03>=<3D>ej<p<>o<EFBFBD><6F><04> <20><><EFBFBD>W<EFBFBD><57><EFBFBD>H<EFBFBD><48>O<EFBFBD>e`<18>C<EFBFBD><43><EFBFBD>BQ<42>}H<><48><18><><EFBFBD>OrO<72><12>c<> c<><1C><><EFBFBD><EFBFBD><EFBFBD>稜Fi؊<69><D88A><EFBFBD><EFBFBD>l<EFBFBD><6C><EFBFBD>\|j<><6A><1E>ڹ!<21>
|
||||
<EFBFBD><EFBFBD><EFBFBD>fCN\ד<>X<EFBFBD><58>|<7C><>2
|
||||
F<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><1D>:<13>v6<>X<EFBFBD><58><EFBFBD>G<EFBFBD><47>_k<0C><><EFBFBD><EFBFBD>`:d}<7D><><EFBFBD>´<EFBFBD><C2B4>I<EFBFBD>CJx<4A><11>r<EFBFBD>#<1E><>'<27>g<11><08><><1D>w2<77><32>1<EFBFBD>W?<0B><>|<7C><><EFBFBD><EFBFBD>j<EFBFBD>'I<0F> ]ݱ<><14>6<EFBFBD>2<13>8DCvh<><11><>G]j<>R<EFBFBD><19>+<2B>Ė<EFBFBD>i<1F>/<2F>}<7D><>L<EFBFBD> f<><66>6f
|
||||
+<2B><><EFBFBD>!@<40>.<2E><><EFBFBD>g<EFBFBD><67><EFBFBD>hsXG<1C>($<24> <20>s<EFBFBD>0<13><><EFBFBD>T;<3B><>g.x<>R<18>
|
||||
<EFBFBD>ٞC}<02>(:S<><53><EFBFBD>L<7F>¹
|
||||
[<5B>QI<12><>[<5B><>K<1A>'<27><><EFBFBD><02><EFBFBD><7F>D<EFBFBD>eB"<22><>^(<28><><1A><>F<><46><08><>S<EFBFBD>k<EFBFBD><6B><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>^<5E>Ɵۃ<C69F>uq<0E> 3<>;<3B><>{l:<3A>.<2E><><EFBFBD>Q<EFBFBD>)<29><>
|
||||
<EFBFBD>@<40><>D<EFBFBD><44>3<EFBFBD>KA<4B>p<EFBFBD><70><EFBFBD>VҘ<56>3<>?m7<6D><37><EFBFBD><EFBFBD>" <20>E<>=6<0C><>j<>N1@<40><>+<2B>T<EFBFBD>K<EFBFBD>h<EFBFBD><68><EFBFBD>Ϋ<EFBFBD><CEAB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>'bé<62>[<5B>f<EFBFBD>t<EFBFBD><74>|4<>ͬD<CDAC><44><EFBFBD>(<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><08>;<3B><><EFBFBD><EFBFBD><EFBFBD>q<EFBFBD>F<EFBFBD><46>c<EFBFBD><63>r'<27>#K<><4B>%n<><6E>b<1F><>nf1-QP(<18><>m<EFBFBD><6D> θ<>l<EFBFBD>6<EFBFBD>〓v\ٳd<D9B3>ɜ<13>@&9u<39>9c<39>{v<><76>!<21>Fo<46>Z<EFBFBD><EFBFBD>.<2E> N_i}<7D>[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>=Z4q<34><71>9 El7s<37><73><07><><rLQ<4C>u<EFBFBD>3<EFBFBD>ҡv<D2A1><1A>4<EFBFBD><34><EFBFBD><EFBFBD>U<>͇"<22>&;<1D><>]5(TR<54><52>;b,r
|
||||
'<27>O.<p<>y<EFBFBD>Z<5A>e<EFBFBD>{<7B>⣶<1F><><EFBFBD><02> <1D> u<10> <20> ]s<>"<><D789><EFBFBD>y<EFBFBD><79><EFBFBD><EFBFBD>es<65>Q<>#<23><><EFBFBD><EFBFBD>HT <20>df<64><06><><EFBFBD><1A>c<EFBFBD><63><EFBFBD><EFBFBD>Ɖ_<C689>*\H<><48><EFBFBD>{<7B><><EFBFBD><EFBFBD><EFBFBD>c<EFBFBD>S<><53><EFBFBD>j<><6A><EFBFBD>(<18><>S<EFBFBD>c<>l<EFBFBD><6C><EFBFBD>ja<6A><61><EFBFBD><03><19>t<EFBFBD><74><EFBFBD><ӈ<><0E><04><><EFBFBD>"<22><><EFBFBD>ZL <09><1C><>fPH<15><><<3C>h&<26>,<2C><><16>Glc*.6<EFBFBD>U<EFBFBD>jDyFf<EFBFBD>:_B<5F><42>V8<56>2<>ףk6l<36>U<EFBFBD>ٳb<D9B3>ǁ<EFBFBD><18>)8<>@e<15>,<10><>+<2B><>t<><
|
||||
<0C>b<EFBFBD>V<EFBFBD><18>\Q<>$<24>b<EFBFBD>W6VlUT!<21>*<12>{<0C>⫆<EFBFBD>|<7C><>3Ӧ$X<>E
|
||||
<EFBFBD>
|
||||
<EFBFBD>:<3A><><EFBFBD>8lUqL<71><11>,<2C>b<EFBFBD><62><EFBFBD>ت<EFBFBD><D8AA><EFBFBD>L<EFBFBD>ڽ<EFBFBD>XZz H<1E><><EFBFBD><EFBFBD>{ŵ<>j6-<2D><><EFBFBD><EFBFBD>A<07>sD<73><07><> J<><4A><EFBFBD>Xt<58>^`ǎvy<76>E<EFBFBD><45><EFBFBD><EFBFBD>/<2F><><EFBFBD><EFBFBD><16>JJ<><4A><EFBFBD><7F><EFBFBD>db[<5B>"<22><02>Ӟ<EFBFBD><D39E><EFBFBD>t<EFBFBD>R<EFBFBD>9<EFBFBD><39><EFBFBD>]<5D>i<EFBFBD>V<EFBFBD><56><EFBFBD>,rSgO<67>2%=<3D><><EFBFBD><EFBFBD><0F>þ'<27><>W<EFBFBD><57><EFBFBD><EFBFBD>3<EFBFBD><33><EFBFBD>cZ<63><5A>H<EFBFBD><48><EFBFBD><EFBFBD>i3b)<29>?<3F>z{<7B>py
|
||||
<EFBFBD>ڌ<><DA8C><EFBFBD>c<EFBFBD><0E>Vk<56>-d<>ɞ<EFBFBD><1C><>6<EFBFBD><36>,<2C>8<EFBFBD>5<EFBFBD><35><EFBFBD>
|
||||
2<EFBFBD><EFBFBD>8<EFBFBD><08><08><>3L@<40>qkr<19><>C<EFBFBD><43> <09><><EFBFBD><EFBFBD> 5<>}8<><38><EFBFBD><EFBFBD><EFBFBD>3<EFBFBD>[<5B><><EFBFBD><EFBFBD>` .<2E><>'<18><><EFBFBD>-<2D><>/0<02>*<2A>^<0E>:<3A><0E>.+\kf'<10>#<23>8<EFBFBD>↬i<E286AC>v<EFBFBD><76>P(v<>ɑ<EFBFBD><C991>Y|.Mf7<66>k<05> 3@p<>)պ<><D5BA><<17>B<EFBFBD><42>&<26><><EFBFBD><EFBFBD>r<16>r<EFBFBD>d<EFBFBD><fKy<4B>i<EFBFBD><69>b<EFBFBD>$?<3F><>}<7D>"z<><7A>Մ<EFBFBD><D584><13>}<7D>+B0<42>
|
||||
{<7B><>4şY<C59F><59>dF<64><46>!<21>1<EFBFBD>1a<><61>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD><EFBFBD>#<23>>b<><62><EFBFBD>8<EFBFBD>1uz;
|
||||
<EFBFBD>m<EFBFBD>{<7B>1<EFBFBD><31>-#j7<6A><37><EFBFBD>aί!%A<><17><>-~<7E><$<24>A<EFBFBD><41>3<EFBFBD>9fw<<3C>@;<3B><>-<2D>|Q<><51><EFBFBD><EFBFBD>?<3F><>)于B<E4BA8E>rV<72>
|
||||
Aû
|
||||
NEjփ<EFBFBD>ݭ<><DDAD><04>qYPu?<3F><><06><>+s<><73>L1<4C><31>(<28><><EFBFBD><EFBFBD>c<EFBFBD>sП<73>ޑP<DE91><50>5<><35>TE@G<><47><EFBFBD><EFBFBD>\<1B><>9|s<><73><EFBFBD>(<28><><EFBFBD><EFBFBD><EFBFBD>1<EFBFBD><31>Q5ġ+@N<><4E><EFBFBD><EFBFBD> <09>*<2A><>n<17><><EFBFBD><EFBFBD><EFBFBD>ck<63>%<25>~<7E><19><>2M<><4D>@F<><46>w=q/QсY$<24><>N&<18><>:[<5B>H<EFBFBD>#<23><><0C><><EFBFBD><EFBFBD>8<EFBFBD>1<D882> <20><><EFBFBD><EFBFBD>.<2E><><EFBFBD> <20><>L<EFBFBD>u<07><>N<EFBFBD>v$[<5B>w^<5E><><EFBFBD>ApHv8wb<77> <20><1F> ]><3E>8Ǫ <20>@<40>{<7B><>s:<3A><><EFBFBD>8y<38>?B<><42>n<12>-ý<><C3BD>qC <20><02><><EFBFBD>9<EFBFBD><16>S52<35><32>T'iWD<><44>,d`<60><>b<><62>Pb.<2E>`<60>$
|
||||
<EFBFBD>4<EFBFBD>\UI<55><49><EFBFBD>1leqU<71><55>g/(<28><>S6_lت <20><><18><>|Uc<55>es<1C><><EFBFBD><16><>@c<><11><><EFBFBD>aM<61>Tt<54><15><>m<EFBFBD>,<2C><0E><>:W
|
||||
6;<3B>)1<>)<29><>ZRNh<4E><68>(<28>Ō<EFBFBD>s'<27><><10><><18>'<27>Kv<4B><76><EFBFBD>n1<6E>
|
||||
E<0F><>P<EFBFBD>C<EFBFBD><43><EFBFBD>b<EFBFBD>'<27><>o<EFBFBD>R<EFBFBD><52>qX<><58><EFBFBD><EFBFBD><EFBFBD>k<>l<EFBFBD><6C>
|
||||
/!<21>&<26><><EFBFBD>><3E><><EFBFBD><EFBFBD>
|
||||
Sn<><6E><EFBFBD>f<EFBFBD><66>ZQ<5A><51>t<EFBFBD><74>p:Y<>Hi>¸<><C2B8>}C<>/A<><41>8vY <20><><07>.K<>s<EFBFBD><73><EFBFBD><EFBFBD> IП<>J<EFBFBD><4A><EFBFBD><EFBFBD><EFBFBD>}<7D><>rL`45<34>g`5́du<>0?P*2-<04><>[ާ<>lL}<7D>x<EFBFBD><78>7F"<22><>\-<2D><><EFBFBD>U<1A>1<EFBFBD><31>~<7E>><3E>T<EFBFBD>pq<70>k<EFBFBD>4;<3B><>0_B<5F><42><EFBFBD>3<EFBFBD> <20>52<35><32><EFBFBD><EFBFBD><EFBFBD>cG<1A><><05><><07>C<EFBFBD><43>ޣle(D<><44><1D><>d<EFBFBD><64><EFBFBD><1D><> OL{<7B>Tヲ<54> S` <01><02><>W4POцV<D186><56>m4<6D>`|(7<><37> <20><1F>1<EFBFBD>F<14><><EFBFBD>k<EFBFBD><1C>6":_#Չ<><D589>Ě<0C><>)6`Ol[c<11>H<EFBFBD><14>(fڻ<66>+<2B>2
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,13 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Adobe d<> <01><> <20>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#"""#''''''''''
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 687 B |
@@ -0,0 +1,13 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Adobe d<> <01><> <20>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#"""#''''''''''
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
BIN
imageio/imageio-jpeg/src/test/resources/broken-jpeg/broken-sos-before-sof.jpg
Executable file
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 299 B |
BIN
imageio/imageio-jpeg/src/test/resources/jpeg/jfif-16bit-dqt.jpg
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 768 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
imageio/imageio-pict/src/test/resources/pict/FC10.PCT
Normal 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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||