mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 12:05:29 -04:00
TMI-26: TIFF write support sans LZW.
This commit is contained in:
parent
824613b4f1
commit
1505aa651b
@ -174,10 +174,57 @@ public abstract class AbstractEntry implements Entry {
|
|||||||
AbstractEntry other = (AbstractEntry) pOther;
|
AbstractEntry other = (AbstractEntry) pOther;
|
||||||
|
|
||||||
return identifier.equals(other.identifier) && (
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
String name = getFieldName();
|
String name = getFieldName();
|
||||||
|
@ -290,6 +290,8 @@ public final class EXIFReader extends MetadataReader {
|
|||||||
|
|
||||||
private static Object readValue(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
|
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: 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();
|
long pos = pInput.getStreamPosition();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -40,9 +40,8 @@ import javax.imageio.stream.ImageInputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReaderAbstractTest
|
* ReaderAbstractTest
|
||||||
@ -54,6 +53,7 @@ import static org.junit.Assert.*;
|
|||||||
public abstract class MetadataReaderAbstractTest {
|
public abstract class MetadataReaderAbstractTest {
|
||||||
static {
|
static {
|
||||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||||
|
ImageIO.setUseCache(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final URL getResource(final String name) throws IOException {
|
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) {
|
private static boolean valueEquals(final Object expected, final Object actual) {
|
||||||
return expected.getClass().isArray() ? arrayEquals(expected, actual) : expected.equals(actual);
|
return expected.getClass().isArray() ? AbstractEntry.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void describeTo(final Description description) {
|
public void describeTo(final Description description) {
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -300,8 +300,8 @@ abstract class LZWDecoder implements Decoder {
|
|||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final LZWString concatenate(final byte firstChar) {
|
public final LZWString concatenate(final byte value) {
|
||||||
return new LZWString(firstChar, this.firstChar, length + 1, this);
|
return new LZWString(value, this.firstChar, length + 1, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void writeTo(final ByteBuffer buffer) {
|
public final void writeTo(final ByteBuffer buffer) {
|
||||||
|
@ -65,8 +65,7 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
|
|||||||
},
|
},
|
||||||
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
|
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
|
||||||
new Class[] {ImageInputStream.class},
|
new Class[] {ImageInputStream.class},
|
||||||
// new String[]{"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
|
new String[] {"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
|
||||||
null,
|
|
||||||
true, // supports standard stream metadata
|
true, // supports standard stream metadata
|
||||||
null, null, // native stream format name and class
|
null, null, // native stream format name and class
|
||||||
null, null, // extra stream formats
|
null, null, // extra stream formats
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* 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 no compression (None/1)
|
||||||
|
// TODO: Support ZLIB (/Deflate) compression (8)
|
||||||
|
// TODO: Support PackBits compression (32773)
|
||||||
|
// TODO: Support JPEG compression (7)
|
||||||
|
// TODO: Support CCITT Modified Huffman compression (2)
|
||||||
|
// TODO: Support LZW compression (5)?
|
||||||
|
// TODO: Support JBIG compression via ImageIO plugin/delegate?
|
||||||
|
// TODO: Support JPEG2000 compression via ImageIO plugin/delegate?
|
||||||
|
// TODO: Support tiling
|
||||||
|
// TODO: Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
|
||||||
|
|
||||||
|
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", /* "CCITT RLE", "CCITT T.4", "CCITT T.6", */ "LZW", "JPEG", "ZLib", "PackBits", "Deflate", /* "EXIF JPEG" */ };
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException(String.format("Unsupported compression type: %s", param.getCompressionType()));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,734 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.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
|
||||||
|
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 = 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Use predictor only by default for -PackBits,- LZW and ZLib/Deflate, unless explicitly disabled (ImageWriteParam)
|
||||||
|
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,89 @@
|
|||||||
|
/*
|
||||||
|
* 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.ProviderInfo;
|
||||||
|
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||||
|
|
||||||
|
import javax.imageio.ImageTypeSpecifier;
|
||||||
|
import javax.imageio.ImageWriter;
|
||||||
|
import javax.imageio.spi.ImageWriterSpi;
|
||||||
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
|
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 ImageWriterSpi {
|
||||||
|
// TODO: Implement canEncodeImage better
|
||||||
|
|
||||||
|
public TIFFImageWriterSpi() {
|
||||||
|
this(IIOUtil.getProviderInfo(TIFFImageWriterSpi.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TIFFImageWriterSpi(final ProviderInfo providerInfo) {
|
||||||
|
super(
|
||||||
|
providerInfo.getVendorName(), providerInfo.getVersion(),
|
||||||
|
new String[] {"tiff", "TIFF", "tif", "TIFF"},
|
||||||
|
new String[] {"tif", "tiff"},
|
||||||
|
new String[] {"image/tiff", "image/x-tiff"},
|
||||||
|
"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriter",
|
||||||
|
new Class<?>[] {ImageOutputStream.class},
|
||||||
|
new String[] {"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi"},
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canEncodeImage(ImageTypeSpecifier type) {
|
||||||
|
// TODO: Test bit depths compatibility
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImageWriter createWriterInstance(Object extension) throws IOException {
|
||||||
|
return new TIFFImageWriter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription(Locale locale) {
|
||||||
|
return "Aldus/Adobe Tagged Image File Format (TIFF) image writer";
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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.util.ImageWriterAbstractTestCase;
|
||||||
|
|
||||||
|
import javax.imageio.ImageWriter;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.RenderedImage;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TIFFImageWriterTest
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: TIFFImageWriterTest.java,v 1.0 19.09.13 13:22 haraldk Exp$
|
||||||
|
*/
|
||||||
|
public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
|
||||||
|
|
||||||
|
public static final TIFFImageWriterSpi PROVIDER = new TIFFImageWriterSpi();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ImageWriter createImageWriter() {
|
||||||
|
return new TIFFImageWriter(PROVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<? extends RenderedImage> getTestData() {
|
||||||
|
BufferedImage image = new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D graphics = image.createGraphics();
|
||||||
|
try {
|
||||||
|
graphics.setColor(Color.RED);
|
||||||
|
graphics.fillRect(0, 0, 100, 200);
|
||||||
|
graphics.setColor(Color.BLUE);
|
||||||
|
graphics.fillRect(100, 0, 100, 200);
|
||||||
|
graphics.clearRect(200, 0, 100, 200);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
graphics.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(image);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user