mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-03-20 00:00:03 -04:00
Merge remote-tracking branch 'remotes/haraldk/master' into CCITTWriter
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -43,6 +43,7 @@ abstract class BitmapDescriptor {
|
||||
protected final DIBHeader header;
|
||||
|
||||
protected BufferedImage image;
|
||||
protected BitmapMask mask;
|
||||
|
||||
public BitmapDescriptor(final DirectoryEntry pEntry, final DIBHeader pHeader) {
|
||||
Validate.notNull(pEntry, "entry");
|
||||
@@ -69,4 +70,17 @@ abstract class BitmapDescriptor {
|
||||
protected final int getBitCount() {
|
||||
return entry.getBitCount() != 0 ? entry.getBitCount() : header.getBitCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[" + entry + ", " + header + "]";
|
||||
}
|
||||
|
||||
public final void setMask(final BitmapMask mask) {
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
public final boolean hasMask() {
|
||||
return header.getHeight() == getHeight() * 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.bmp;
|
||||
|
||||
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBuffer;
|
||||
import java.awt.image.IndexColorModel;
|
||||
@@ -46,8 +44,6 @@ class BitmapIndexed extends BitmapDescriptor {
|
||||
protected final int[] bits;
|
||||
protected final int[] colors;
|
||||
|
||||
private BitmapMask mask;
|
||||
|
||||
public BitmapIndexed(final DirectoryEntry pEntry, final DIBHeader pHeader) {
|
||||
super(pEntry, pHeader);
|
||||
bits = new int[getWidth() * getHeight()];
|
||||
@@ -65,7 +61,7 @@ class BitmapIndexed extends BitmapDescriptor {
|
||||
// This is slightly obscure, and should probably be moved..
|
||||
Hashtable<String, Object> properties = null;
|
||||
if (entry instanceof DirectoryEntry.CUREntry) {
|
||||
properties = new Hashtable<String, Object>(1);
|
||||
properties = new Hashtable<>(1);
|
||||
properties.put("cursor_hotspot", ((DirectoryEntry.CUREntry) this.entry).getHotspot());
|
||||
}
|
||||
|
||||
@@ -89,8 +85,6 @@ class BitmapIndexed extends BitmapDescriptor {
|
||||
|
||||
raster.setSamples(0, 0, getWidth(), getHeight(), 0, bits);
|
||||
|
||||
//System.out.println("Image: " + image);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -100,40 +94,40 @@ class BitmapIndexed extends BitmapDescriptor {
|
||||
IndexColorModel createColorModel() {
|
||||
// NOTE: This is a hack to make room for transparent pixel for mask
|
||||
int bits = getBitCount();
|
||||
|
||||
|
||||
int colors = this.colors.length;
|
||||
int trans = -1;
|
||||
int transparent = -1;
|
||||
|
||||
// Try to avoid USHORT transfertype, as it results in BufferedImage TYPE_CUSTOM
|
||||
// NOTE: This code assumes icons are small, and is NOT optimized for performance...
|
||||
if (colors > (1 << getBitCount())) {
|
||||
int index = findTransIndexMaybeRemap(this.colors, this.bits);
|
||||
int index = findTransparentIndexMaybeRemap(this.colors, this.bits);
|
||||
|
||||
if (index == -1) {
|
||||
// No duplicate found, increase bitcount
|
||||
bits++;
|
||||
trans = this.colors.length - 1;
|
||||
transparent = this.colors.length - 1;
|
||||
}
|
||||
else {
|
||||
// Found a duplicate, use it as trans
|
||||
trans = index;
|
||||
// Found a duplicate, use it as transparent
|
||||
transparent = index;
|
||||
colors--;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Setting hasAlpha to true, makes things work on 1.2
|
||||
return new InverseColorMapIndexColorModel(
|
||||
bits, colors, this.colors, 0, true, trans,
|
||||
return new IndexColorModel(
|
||||
bits, colors, this.colors, 0, true, transparent,
|
||||
bits <= 8 ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT
|
||||
);
|
||||
}
|
||||
|
||||
private static int findTransIndexMaybeRemap(final int[] pColors, final int[] pBits) {
|
||||
private static int findTransparentIndexMaybeRemap(final int[] colors, final int[] bits) {
|
||||
// Look for unused colors, to use as transparent
|
||||
final boolean[] used = new boolean[pColors.length - 1];
|
||||
for (int pBit : pBits) {
|
||||
if (!used[pBit]) {
|
||||
used[pBit] = true;
|
||||
boolean[] used = new boolean[colors.length - 1];
|
||||
for (int bit : bits) {
|
||||
if (!used[bit]) {
|
||||
used[bit] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,38 +138,35 @@ class BitmapIndexed extends BitmapDescriptor {
|
||||
}
|
||||
|
||||
// Try to find duplicates in colormap, and remap
|
||||
int trans = -1;
|
||||
int transparent = -1;
|
||||
int duplicate = -1;
|
||||
for (int i = 0; trans == -1 && i < pColors.length - 1; i++) {
|
||||
for (int j = i + 1; j < pColors.length - 1; j++) {
|
||||
if (pColors[i] == pColors[j]) {
|
||||
trans = j;
|
||||
for (int i = 0; transparent == -1 && i < colors.length - 1; i++) {
|
||||
for (int j = i + 1; j < colors.length - 1; j++) {
|
||||
if (colors[i] == colors[j]) {
|
||||
transparent = j;
|
||||
duplicate = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trans != -1) {
|
||||
if (transparent != -1) {
|
||||
// Remap duplicate
|
||||
for (int i = 0; i < pBits.length; i++) {
|
||||
if (pBits[i] == trans) {
|
||||
pBits[i] = duplicate;
|
||||
for (int i = 0; i < bits.length; i++) {
|
||||
if (bits[i] == transparent) {
|
||||
bits[i] = duplicate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trans;
|
||||
return transparent;
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
if (image == null) {
|
||||
image = createImageIndexed();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setMask(final BitmapMask pMask) {
|
||||
mask = pMask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,19 +38,19 @@ import java.awt.image.BufferedImage;
|
||||
* @version $Id: BitmapMask.java,v 1.0 25.feb.2006 00:29:44 haku Exp$
|
||||
*/
|
||||
class BitmapMask extends BitmapDescriptor {
|
||||
protected final BitmapIndexed mask;
|
||||
protected final BitmapIndexed bitMask;
|
||||
|
||||
public BitmapMask(final DirectoryEntry pParent, final DIBHeader pHeader) {
|
||||
super(pParent, pHeader);
|
||||
mask = new BitmapIndexed(pParent, pHeader);
|
||||
bitMask = new BitmapIndexed(pParent, pHeader);
|
||||
}
|
||||
|
||||
boolean isTransparent(final int pX, final int pY) {
|
||||
// NOTE: 1: Fully transparent, 0: Opaque...
|
||||
return mask.bits[pX + pY * getWidth()] != 0;
|
||||
return bitMask.bits[pX + pY * getWidth()] != 0;
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
return mask.getImage();
|
||||
return bitMask.getImage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.bmp;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.WritableRaster;
|
||||
|
||||
/**
|
||||
* Describes an RGB/true color bitmap structure (16, 24 and 32 bits per pixel).
|
||||
@@ -43,6 +45,38 @@ class BitmapRGB extends BitmapDescriptor {
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
// Test is mask != null rather than hasMask(), as 32 bit (w/alpha)
|
||||
// might still have bitmask, but we don't read or use it.
|
||||
if (mask != null) {
|
||||
image = createMaskedImage();
|
||||
mask = null;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private BufferedImage createMaskedImage() {
|
||||
BufferedImage masked = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
|
||||
|
||||
Graphics2D graphics = masked.createGraphics();
|
||||
try {
|
||||
graphics.drawImage(image, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
WritableRaster alphaRaster = masked.getAlphaRaster();
|
||||
|
||||
byte[] trans = {0x0};
|
||||
for (int y = 0; y < getHeight(); y++) {
|
||||
for (int x = 0; x < getWidth(); x++) {
|
||||
if (mask.isTransparent(x, y)) {
|
||||
alphaRaster.setDataElements(x, y, trans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return masked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,14 +66,15 @@ import java.util.List;
|
||||
// TODO: Decide whether DirectoryEntry or DIBHeader should be primary source for color count/bit count
|
||||
// TODO: Support loading icons from DLLs, see
|
||||
// <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwui/html/msdn_icons.asp">MSDN</a>
|
||||
// Known issue: 256x256 PNG encoded icons does not have IndexColorModel even if stated in DirectoryEntry (seem impossible as the PNGs are all true color)
|
||||
// Known issue: 256x256 PNG encoded icons does not have IndexColorModel even if stated in DirectoryEntry
|
||||
// (seem impossible as the PNGs are all true color)
|
||||
abstract class DIBImageReader extends ImageReaderBase {
|
||||
// TODO: Consider moving the reading to inner classes (subclasses of BitmapDescriptor)
|
||||
private Directory directory;
|
||||
|
||||
// TODO: Review these, make sure we don't have a memory leak
|
||||
private Map<DirectoryEntry, DIBHeader> headers = new WeakHashMap<DirectoryEntry, DIBHeader>();
|
||||
private Map<DirectoryEntry, BitmapDescriptor> descriptors = new WeakWeakMap<DirectoryEntry, BitmapDescriptor>();
|
||||
private Map<DirectoryEntry, DIBHeader> headers = new WeakHashMap<>();
|
||||
private Map<DirectoryEntry, BitmapDescriptor> descriptors = new WeakWeakMap<>();
|
||||
|
||||
private ImageReader pngImageReader;
|
||||
|
||||
@@ -101,7 +102,7 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
return getImageTypesPNG(entry);
|
||||
}
|
||||
|
||||
List<ImageTypeSpecifier> types = new ArrayList<ImageTypeSpecifier>();
|
||||
List<ImageTypeSpecifier> types = new ArrayList<>();
|
||||
DIBHeader header = getHeader(entry);
|
||||
|
||||
// Use data from header to create specifier
|
||||
@@ -121,10 +122,13 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
specifier = ImageTypeSpecifiers.createFromIndexColorModel(indexed.createColorModel());
|
||||
break;
|
||||
case 16:
|
||||
// TODO: May have mask?!
|
||||
specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB);
|
||||
break;
|
||||
case 24:
|
||||
specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
|
||||
specifier = new BitmapRGB(entry, header).hasMask()
|
||||
? ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR)
|
||||
: ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
|
||||
break;
|
||||
case 32:
|
||||
specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB);
|
||||
@@ -184,6 +188,7 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
}
|
||||
else {
|
||||
Graphics2D g = destination.createGraphics();
|
||||
|
||||
try {
|
||||
g.setComposite(AlphaComposite.Src);
|
||||
g.drawImage(image, 0, 0, null);
|
||||
@@ -335,7 +340,7 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
BitmapMask mask = new BitmapMask(pBitmap.entry, pBitmap.header);
|
||||
readBitmapIndexed1(mask.mask, true);
|
||||
readBitmapIndexed1(mask.bitMask, true);
|
||||
pBitmap.setMask(mask);
|
||||
}
|
||||
|
||||
@@ -370,7 +375,7 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: If we are reading the mask, we don't abort or progress
|
||||
// NOTE: If we are reading the mask, we don't abort or report progress
|
||||
if (!pAsMask) {
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
@@ -455,7 +460,7 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
short[] pixels = new short[pBitmap.getWidth() * pBitmap.getHeight()];
|
||||
|
||||
// TODO: Support TYPE_USHORT_565 and the RGB 444/ARGB 4444 layouts
|
||||
// Will create TYPE_USHORT_555;
|
||||
// Will create TYPE_USHORT_555
|
||||
DirectColorModel cm = new DirectColorModel(16, 0x7C00, 0x03E0, 0x001F);
|
||||
DataBuffer buffer = new DataBufferShort(pixels, pixels.length);
|
||||
WritableRaster raster = Raster.createPackedRaster(
|
||||
@@ -480,6 +485,8 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
|
||||
processImageProgress(100 * y / (float) pBitmap.getHeight());
|
||||
}
|
||||
|
||||
// TODO: Might be mask!?
|
||||
}
|
||||
|
||||
private void readBitmap24(final BitmapDescriptor pBitmap) throws IOException {
|
||||
@@ -494,16 +501,19 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
cs, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE
|
||||
);
|
||||
|
||||
int scanlineStride = pBitmap.getWidth() * 3;
|
||||
// BMP rows are padded to 4 byte boundary
|
||||
int rowSizeBytes = ((8 * scanlineStride + 31) / 32) * 4;
|
||||
|
||||
WritableRaster raster = Raster.createInterleavedRaster(
|
||||
buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), 3, bOffs, null
|
||||
buffer, pBitmap.getWidth(), pBitmap.getHeight(), scanlineStride, 3, bOffs, null
|
||||
);
|
||||
pBitmap.image = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
|
||||
|
||||
for (int y = 0; y < pBitmap.getHeight(); y++) {
|
||||
int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth();
|
||||
imageInput.readFully(pixels, offset, pBitmap.getWidth() * 3);
|
||||
|
||||
// TODO: Possibly read padding byte here!
|
||||
for (int y = 0; y < pBitmap.getHeight(); y++) {
|
||||
int offset = (pBitmap.getHeight() - y - 1) * scanlineStride;
|
||||
imageInput.readFully(pixels, offset, scanlineStride);
|
||||
imageInput.skipBytes(rowSizeBytes - scanlineStride);
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
@@ -512,6 +522,14 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
|
||||
processImageProgress(100 * y / (float) pBitmap.getHeight());
|
||||
}
|
||||
|
||||
// 24 bit icons usually have a bit mask
|
||||
if (pBitmap.hasMask()) {
|
||||
BitmapMask mask = new BitmapMask(pBitmap.entry, pBitmap.header);
|
||||
readBitmapIndexed1(mask.bitMask, true);
|
||||
|
||||
pBitmap.setMask(mask);
|
||||
}
|
||||
}
|
||||
|
||||
private void readBitmap32(final BitmapDescriptor pBitmap) throws IOException {
|
||||
@@ -535,6 +553,9 @@ abstract class DIBImageReader extends ImageReaderBase {
|
||||
}
|
||||
processImageProgress(100 * y / (float) pBitmap.getHeight());
|
||||
}
|
||||
|
||||
// There might be a mask here as well, but we'll ignore it,
|
||||
// and use the 8 bit alpha channel in the ARGB pixel data
|
||||
}
|
||||
|
||||
private Directory getDirectory() throws IOException {
|
||||
|
||||
@@ -39,7 +39,9 @@ public class ICOImageReaderTest extends ImageReaderAbstractTest<ICOImageReader>
|
||||
new Dimension(16, 16), new Dimension(16, 16), new Dimension(32, 32), new Dimension(32, 32),
|
||||
new Dimension(48, 48), new Dimension(48, 48), new Dimension(256, 256), new Dimension(256, 256),
|
||||
new Dimension(16, 16), new Dimension(32, 32), new Dimension(48, 48), new Dimension(256, 256)
|
||||
)
|
||||
),
|
||||
// Problematic icon that reports 24 bit in the descriptor, but has separate 1 bit ''mask (height 2 x icon height)!
|
||||
new TestData(getClassLoaderResource("/ico/rgb24bitmask.ico"), new Dimension(32, 32))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
BIN
imageio/imageio-bmp/src/test/resources/ico/rgb24bitmask.ico
Normal file
BIN
imageio/imageio-bmp/src/test/resources/ico/rgb24bitmask.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
|
||||
@@ -98,15 +98,6 @@ public abstract class AbstractMetadata extends IIOMetadata implements Cloneable
|
||||
if (!root.getNodeName().equals(formatName)) {
|
||||
throw new IIOInvalidTreeException("Root must be " + formatName, root);
|
||||
}
|
||||
|
||||
// TODO: Merge both native and standard!
|
||||
Node node = root.getFirstChild();
|
||||
while (node != null) {
|
||||
// TODO: Merge values from node into this
|
||||
|
||||
// Move to the next sibling
|
||||
node = node.getNextSibling();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -52,7 +52,7 @@ import java.util.Properties;
|
||||
* <p />
|
||||
* Color profiles may be configured by placing a property-file
|
||||
* {@code com/twelvemonkeys/imageio/color/icc_profiles.properties}
|
||||
* on the classpath, specifying the full path to the profile.
|
||||
* on the classpath, specifying the full path to the profiles.
|
||||
* ICC color profiles are probably already present on your system, or
|
||||
* can be downloaded from
|
||||
* <a href="http://www.color.org/profiles2.xalter">ICC</a>,
|
||||
@@ -342,7 +342,7 @@ public final class ColorSpaces {
|
||||
try {
|
||||
return ICC_Profile.getInstance(profilePath);
|
||||
}
|
||||
catch (IOException ignore) {
|
||||
catch (SecurityException | IOException ignore) {
|
||||
if (DEBUG) {
|
||||
ignore.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public abstract class ImageWriterAbstractTestCase {
|
||||
|
||||
static {
|
||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||
ImageIO.setUseCache(false);
|
||||
}
|
||||
|
||||
protected abstract ImageWriter createImageWriter();
|
||||
@@ -120,23 +121,20 @@ public abstract class ImageWriterAbstractTestCase {
|
||||
|
||||
for (RenderedImage testData : getTestData()) {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
ImageOutputStream stream = ImageIO.createImageOutputStream(buffer);
|
||||
writer.setOutput(stream);
|
||||
|
||||
try {
|
||||
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
|
||||
writer.setOutput(stream);
|
||||
writer.write(drawSomething((BufferedImage) testData));
|
||||
}
|
||||
catch (IOException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
finally {
|
||||
stream.close(); // Force data to be written
|
||||
}
|
||||
|
||||
assertTrue("No image data written", buffer.size() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Test
|
||||
public void testWriteNull() throws IOException {
|
||||
ImageWriter writer = createImageWriter();
|
||||
|
||||
30
imageio/imageio-hdr/pom.xml
Normal file
30
imageio/imageio-hdr/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio</artifactId>
|
||||
<version>3.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>imageio-hdr</artifactId>
|
||||
<name>TwelveMonkeys :: ImageIO :: HDR plugin</name>
|
||||
<description>
|
||||
ImageIO plugin for Radiance RGBE High Dynaimc Range format (HDR).
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-metadata</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
/**
|
||||
* HDR.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: HDR.java,v 1.0 27/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
interface HDR {
|
||||
byte[] RADIANCE_MAGIC = new byte[] {'#', '?', 'R', 'A', 'D', 'I', 'A', 'N', 'C', 'E'};
|
||||
byte[] RGBE_MAGIC = new byte[] {'#', '?', 'R', 'G', 'B', 'E'};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* HDRHeader.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: HDRHeader.java,v 1.0 27/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
final class HDRHeader {
|
||||
private static final String KEY_FORMAT = "FORMAT=";
|
||||
private static final String KEY_PRIMARIES = "PRIMARIES=";
|
||||
private static final String KEY_EXPOSURE = "EXPOSURE=";
|
||||
private static final String KEY_GAMMA = "GAMMA=";
|
||||
private static final String KEY_SOFTWARE = "SOFTWARE=";
|
||||
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
private String software;
|
||||
|
||||
public static HDRHeader read(final ImageInputStream stream) throws IOException {
|
||||
HDRHeader header = new HDRHeader();
|
||||
|
||||
while (true) {
|
||||
String line = stream.readLine().trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
// This is the last line before the dimensions
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.startsWith("#?")) {
|
||||
// Program specifier, don't need that...
|
||||
}
|
||||
else if (line.startsWith("#")) {
|
||||
// Comment (ignore)
|
||||
}
|
||||
else if (line.startsWith(KEY_FORMAT)) {
|
||||
String format = line.substring(KEY_FORMAT.length()).trim();
|
||||
|
||||
if (!format.equals("32-bit_rle_rgbe")) {
|
||||
throw new IIOException("Unsupported format \"" + format + "\"(expected \"32-bit_rle_rgbe\")");
|
||||
}
|
||||
// TODO: Support the 32-bit_rle_xyze format
|
||||
}
|
||||
else if (line.startsWith(KEY_PRIMARIES)) {
|
||||
// TODO: We are going to need these values...
|
||||
// Should contain 8 (RGB + white point) coordinates
|
||||
}
|
||||
else if (line.startsWith(KEY_EXPOSURE)) {
|
||||
// TODO: We are going to need these values...
|
||||
}
|
||||
else if (line.startsWith(KEY_GAMMA)) {
|
||||
// TODO: We are going to need these values...
|
||||
}
|
||||
else if (line.startsWith(KEY_SOFTWARE)) {
|
||||
header.software = line.substring(KEY_SOFTWARE.length()).trim();
|
||||
}
|
||||
else {
|
||||
// ...ignore
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Proper parsing of width/height and orientation!
|
||||
String dimensionsLine = stream.readLine().trim();
|
||||
String[] dims = dimensionsLine.split("\\s");
|
||||
|
||||
if (dims[0].equals("-Y") && dims[2].equals("+X")) {
|
||||
header.height = Integer.parseInt(dims[1]);
|
||||
header.width = Integer.parseInt(dims[3]);
|
||||
|
||||
return header;
|
||||
}
|
||||
else {
|
||||
throw new IIOException("Unsupported RGBE orientation (expected \"-Y ... +X ...\")");
|
||||
}
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public String getSoftware() {
|
||||
return software;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
import com.twelvemonkeys.imageio.plugins.hdr.tonemap.DefaultToneMapper;
|
||||
import com.twelvemonkeys.imageio.plugins.hdr.tonemap.ToneMapper;
|
||||
|
||||
import javax.imageio.ImageReadParam;
|
||||
|
||||
/**
|
||||
* HDRImageReadParam.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: HDRImageReadParam.java,v 1.0 28/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public final class HDRImageReadParam extends ImageReadParam {
|
||||
static final ToneMapper DEFAULT_TONE_MAPPER = new DefaultToneMapper(.1f);
|
||||
|
||||
private ToneMapper toneMapper = DEFAULT_TONE_MAPPER;
|
||||
|
||||
public ToneMapper getToneMapper() {
|
||||
return toneMapper;
|
||||
}
|
||||
|
||||
public void setToneMapper(final ToneMapper toneMapper) {
|
||||
this.toneMapper = toneMapper != null ? toneMapper : DEFAULT_TONE_MAPPER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
import com.twelvemonkeys.imageio.ImageReaderBase;
|
||||
import com.twelvemonkeys.imageio.plugins.hdr.tonemap.ToneMapper;
|
||||
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReadParam;
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBuffer;
|
||||
import java.awt.image.Raster;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* HDRImageReader.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: HDRImageReader.java,v 1.0 27/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public final class HDRImageReader extends ImageReaderBase {
|
||||
// Specs: http://radsite.lbl.gov/radiance/refer/filefmts.pdf
|
||||
|
||||
private HDRHeader header;
|
||||
|
||||
protected HDRImageReader(final ImageReaderSpi provider) {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetMembers() {
|
||||
header = null;
|
||||
}
|
||||
|
||||
private void readHeader() throws IOException {
|
||||
if (header == null) {
|
||||
header = HDRHeader.read(imageInput);
|
||||
|
||||
imageInput.flushBefore(imageInput.getStreamPosition());
|
||||
}
|
||||
|
||||
imageInput.seek(imageInput.getFlushedPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth(int imageIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
return header.getWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight(int imageIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
return header.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
|
||||
return Collections.singletonList(ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {0, 1, 2}, DataBuffer.TYPE_FLOAT, false, false)).iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
int width = getWidth(imageIndex);
|
||||
int height = getHeight(imageIndex);
|
||||
|
||||
BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
|
||||
|
||||
Rectangle srcRegion = new Rectangle();
|
||||
Rectangle destRegion = new Rectangle();
|
||||
computeRegions(param, width, height, destination, srcRegion, destRegion);
|
||||
|
||||
WritableRaster raster = destination.getRaster()
|
||||
.createWritableChild(destRegion.x, destRegion.y, destRegion.width, destRegion.height, 0, 0, null);
|
||||
|
||||
int xSub = param != null ? param.getSourceXSubsampling() : 1;
|
||||
int ySub = param != null ? param.getSourceYSubsampling() : 1;
|
||||
|
||||
// Allow pluggable tone mapper via ImageReadParam
|
||||
ToneMapper toneMapper = param instanceof HDRImageReadParam
|
||||
? ((HDRImageReadParam) param).getToneMapper()
|
||||
: HDRImageReadParam.DEFAULT_TONE_MAPPER;
|
||||
|
||||
byte[] rowRGBE = new byte[width * 4];
|
||||
float[] rgb = new float[3];
|
||||
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
// Process one scanline of RGBE data at a time
|
||||
for (int srcY = 0; srcY < height; srcY++) {
|
||||
int dstY = ((srcY - srcRegion.y) / ySub) + destRegion.y;
|
||||
if (dstY >= destRegion.height) {
|
||||
break;
|
||||
}
|
||||
|
||||
RGBE.readPixelsRawRLE(imageInput, rowRGBE, 0, width, 1);
|
||||
|
||||
if (srcY % ySub == 0 && dstY >= destRegion.y) {
|
||||
for (int srcX = srcRegion.x; srcX < srcRegion.x + srcRegion.width; srcX += xSub) {
|
||||
int dstX = ((srcX - srcRegion.x) / xSub) + destRegion.x;
|
||||
if (dstX >= destRegion.width) {
|
||||
break;
|
||||
}
|
||||
|
||||
RGBE.rgbe2float(rgb, rowRGBE, srcX * 4);
|
||||
|
||||
// Map/clamp RGB values into visible range, normally [0...1]
|
||||
toneMapper.map(rgb);
|
||||
|
||||
raster.setDataElements(dstX, dstY, rgb);
|
||||
}
|
||||
}
|
||||
|
||||
processImageProgress(srcY * 100f / height);
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
processImageComplete();
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canReadRaster() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Raster readRaster(final int imageIndex, final ImageReadParam param) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
int width = getWidth(imageIndex);
|
||||
int height = getHeight(imageIndex);
|
||||
|
||||
Rectangle srcRegion = new Rectangle();
|
||||
Rectangle destRegion = new Rectangle();
|
||||
computeRegions(param, width, height, null, srcRegion, destRegion);
|
||||
destRegion = srcRegion; // We don't really care about destination for raster
|
||||
|
||||
BufferedImage destination = new BufferedImage(srcRegion.width, srcRegion.height, BufferedImage.TYPE_4BYTE_ABGR);
|
||||
WritableRaster raster = destination.getRaster();
|
||||
|
||||
int xSub = param != null ? param.getSourceXSubsampling() : 1;
|
||||
int ySub = param != null ? param.getSourceYSubsampling() : 1;
|
||||
|
||||
byte[] rowRGBE = new byte[width * 4];
|
||||
byte[] pixelRGBE = new byte[width];
|
||||
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
// Process one scanline of RGBE data at a time
|
||||
for (int srcY = 0; srcY < height; srcY++) {
|
||||
int dstY = ((srcY - srcRegion.y) / ySub) + destRegion.y;
|
||||
if (dstY >= destRegion.height) {
|
||||
break;
|
||||
}
|
||||
|
||||
RGBE.readPixelsRawRLE(imageInput, rowRGBE, 0, width, 1);
|
||||
|
||||
if (srcY % ySub == 0 && dstY >= destRegion.y) {
|
||||
for (int srcX = srcRegion.x; srcX < srcRegion.x + srcRegion.width; srcX += xSub) {
|
||||
int dstX = ((srcX - srcRegion.x) / xSub) + destRegion.x;
|
||||
if (dstX >= destRegion.width) {
|
||||
break;
|
||||
}
|
||||
|
||||
System.arraycopy(rowRGBE, srcX * 4, pixelRGBE, 0, 4);
|
||||
raster.setDataElements(dstX, dstY, pixelRGBE);
|
||||
}
|
||||
}
|
||||
|
||||
processImageProgress(srcY * 100f / height);
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
processImageComplete();
|
||||
|
||||
return destination.getRaster();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageReadParam getDefaultReadParam() {
|
||||
return new HDRImageReadParam();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
return new HDRMetadata(header);
|
||||
}
|
||||
|
||||
public static void main(final String[] args) throws IOException {
|
||||
File file = new File(args[0]);
|
||||
|
||||
BufferedImage image = ImageIO.read(file);
|
||||
|
||||
showIt(image, file.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
|
||||
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* HDRImageReaderSpi.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: HDRImageReaderSpi.java,v 1.0 27/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public final class HDRImageReaderSpi extends ImageReaderSpiBase {
|
||||
public HDRImageReaderSpi() {
|
||||
super(new HDRProviderInfo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecodeInput(final Object source) throws IOException {
|
||||
if (!(source instanceof ImageInputStream)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ImageInputStream stream = (ImageInputStream) source;
|
||||
|
||||
stream.mark();
|
||||
|
||||
try {
|
||||
// NOTE: All images I have found starts with #?RADIANCE (or has no #? line at all),
|
||||
// although some sources claim that #?RGBE is also used.
|
||||
byte[] magic = new byte[HDR.RADIANCE_MAGIC.length];
|
||||
stream.readFully(magic);
|
||||
|
||||
return Arrays.equals(HDR.RADIANCE_MAGIC, magic)
|
||||
|| Arrays.equals(HDR.RGBE_MAGIC, Arrays.copyOf(magic, 6));
|
||||
}
|
||||
finally {
|
||||
stream.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageReader createReaderInstance(Object extension) throws IOException {
|
||||
return new HDRImageReader(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription(final Locale locale) {
|
||||
return "Radiance RGBE High Dynaimc Range (HDR) image reader";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
import com.twelvemonkeys.imageio.AbstractMetadata;
|
||||
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
|
||||
final class HDRMetadata extends AbstractMetadata {
|
||||
private final HDRHeader header;
|
||||
|
||||
HDRMetadata(final HDRHeader header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IIOMetadataNode getStandardChromaNode() {
|
||||
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
|
||||
|
||||
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
|
||||
chroma.appendChild(csType);
|
||||
csType.setAttribute("name", "RGB");
|
||||
// TODO: Support XYZ
|
||||
|
||||
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
|
||||
numChannels.setAttribute("value", "3");
|
||||
chroma.appendChild(numChannels);
|
||||
|
||||
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
|
||||
blackIsZero.setAttribute("value", "TRUE");
|
||||
chroma.appendChild(blackIsZero);
|
||||
|
||||
return chroma;
|
||||
}
|
||||
|
||||
// No compression
|
||||
|
||||
@Override
|
||||
protected IIOMetadataNode getStandardCompressionNode() {
|
||||
IIOMetadataNode node = new IIOMetadataNode("Compression");
|
||||
|
||||
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
|
||||
compressionTypeName.setAttribute("value", "RLE");
|
||||
node.appendChild(compressionTypeName);
|
||||
|
||||
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
|
||||
lossless.setAttribute("value", "TRUE");
|
||||
node.appendChild(lossless);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IIOMetadataNode getStandardDataNode() {
|
||||
IIOMetadataNode node = new IIOMetadataNode("Data");
|
||||
|
||||
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
|
||||
sampleFormat.setAttribute("value", "UnsignedIntegral");
|
||||
node.appendChild(sampleFormat);
|
||||
|
||||
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
|
||||
bitsPerSample.setAttribute("value", "8 8 8 8");
|
||||
node.appendChild(bitsPerSample);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IIOMetadataNode getStandardDimensionNode() {
|
||||
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
|
||||
|
||||
// TODO: Support other orientations
|
||||
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
|
||||
imageOrientation.setAttribute("value", "Normal");
|
||||
dimension.appendChild(imageOrientation);
|
||||
|
||||
return dimension;
|
||||
}
|
||||
|
||||
// No document node
|
||||
|
||||
@Override
|
||||
protected IIOMetadataNode getStandardTextNode() {
|
||||
if (header.getSoftware() != null) {
|
||||
IIOMetadataNode text = new IIOMetadataNode("Text");
|
||||
|
||||
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
|
||||
textEntry.setAttribute("keyword", "Software");
|
||||
textEntry.setAttribute("value", header.getSoftware());
|
||||
text.appendChild(textEntry);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// No tiling
|
||||
|
||||
// No transparency
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
|
||||
|
||||
/**
|
||||
* HDRProviderInfo.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: HDRProviderInfo.java,v 1.0 27/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
final class HDRProviderInfo extends ReaderWriterProviderInfo {
|
||||
protected HDRProviderInfo() {
|
||||
super(
|
||||
HDRProviderInfo.class,
|
||||
new String[] {"HDR", "hdr", "RGBE", "rgbe"},
|
||||
new String[] {"hdr", "rgbe", "xyze", "pic"},
|
||||
new String[] {"image/vnd.radiance"},
|
||||
"com.twelvemonkeys.imageio.plugins.hdr.HDRImageReader",
|
||||
new String[]{"com.twelvemonkeys.imageio.plugins.hdr.HDRImageReaderSpi"},
|
||||
null,
|
||||
null,
|
||||
false, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
package com.twelvemonkeys.imageio.plugins.hdr;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* This file contains code to read and write four byte rgbe file format
|
||||
* developed by Greg Ward. It handles the conversions between rgbe and
|
||||
* pixels consisting of floats. The data is assumed to be an array of floats.
|
||||
* By default there are three floats per pixel in the order red, green, blue.
|
||||
* (RGBE_DATA_??? values control this.) Only the mimimal header reading and
|
||||
* writing is implemented. Each routine does error checking and will return
|
||||
* a status value as defined below. This code is intended as a skeleton so
|
||||
* feel free to modify it to suit your needs. <P>
|
||||
* <p/>
|
||||
* Ported to Java and restructured by Kenneth Russell. <BR>
|
||||
* posted to http://www.graphics.cornell.edu/~bjw/ <BR>
|
||||
* written by Bruce Walter (bjw@graphics.cornell.edu) 5/26/95 <BR>
|
||||
* based on code written by Greg Ward <BR>
|
||||
* <p/>
|
||||
* Source: https://java.net/projects/jogl-demos/sources/svn/content/trunk/src/demos/hdr/RGBE.java
|
||||
*/
|
||||
final class RGBE {
|
||||
// Flags indicating which fields in a Header are valid
|
||||
private static final int VALID_PROGRAMTYPE = 0x01;
|
||||
private static final int VALID_GAMMA = 0x02;
|
||||
private static final int VALID_EXPOSURE = 0x04;
|
||||
|
||||
private static final String gammaString = "GAMMA=";
|
||||
private static final String exposureString = "EXPOSURE=";
|
||||
|
||||
private static final Pattern widthHeightPattern = Pattern.compile("-Y (\\d+) \\+X (\\d+)");
|
||||
|
||||
public static class Header {
|
||||
// Indicates which fields are valid
|
||||
private int valid;
|
||||
|
||||
// Listed at beginning of file to identify it after "#?".
|
||||
// Defaults to "RGBE"
|
||||
private String programType;
|
||||
|
||||
// Image has already been gamma corrected with given gamma.
|
||||
// Defaults to 1.0 (no correction)
|
||||
private float gamma;
|
||||
|
||||
// A value of 1.0 in an image corresponds to <exposure>
|
||||
// watts/steradian/m^2. Defaults to 1.0.
|
||||
private float exposure;
|
||||
|
||||
// Width and height of image
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
private Header(int valid,
|
||||
String programType,
|
||||
float gamma,
|
||||
float exposure,
|
||||
int width,
|
||||
int height) {
|
||||
this.valid = valid;
|
||||
this.programType = programType;
|
||||
this.gamma = gamma;
|
||||
this.exposure = exposure;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public boolean isProgramTypeValid() {
|
||||
return ((valid & VALID_PROGRAMTYPE) != 0);
|
||||
}
|
||||
|
||||
public boolean isGammaValid() {
|
||||
return ((valid & VALID_GAMMA) != 0);
|
||||
}
|
||||
|
||||
public boolean isExposureValid() {
|
||||
return ((valid & VALID_EXPOSURE) != 0);
|
||||
}
|
||||
|
||||
public String getProgramType() {
|
||||
return programType;
|
||||
}
|
||||
|
||||
public float getGamma() {
|
||||
return gamma;
|
||||
}
|
||||
|
||||
public float getExposure() {
|
||||
return exposure;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
if (isProgramTypeValid()) {
|
||||
buf.append(" Program type: ");
|
||||
buf.append(getProgramType());
|
||||
}
|
||||
buf.append(" Gamma");
|
||||
if (isGammaValid()) {
|
||||
buf.append(" [valid]");
|
||||
}
|
||||
buf.append(": ");
|
||||
buf.append(getGamma());
|
||||
buf.append(" Exposure");
|
||||
if (isExposureValid()) {
|
||||
buf.append(" [valid]");
|
||||
}
|
||||
buf.append(": ");
|
||||
buf.append(getExposure());
|
||||
buf.append(" Width: ");
|
||||
buf.append(getWidth());
|
||||
buf.append(" Height: ");
|
||||
buf.append(getHeight());
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static Header readHeader(final DataInput in) throws IOException {
|
||||
int valid = 0;
|
||||
String programType = null;
|
||||
float gamma = 1.0f;
|
||||
float exposure = 1.0f;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
String buf = in.readLine();
|
||||
if (buf == null) {
|
||||
throw new IOException("Unexpected EOF reading magic token");
|
||||
}
|
||||
if (buf.charAt(0) == '#' && buf.charAt(1) == '?') {
|
||||
valid |= VALID_PROGRAMTYPE;
|
||||
programType = buf.substring(2);
|
||||
buf = in.readLine();
|
||||
if (buf == null) {
|
||||
throw new IOException("Unexpected EOF reading line after magic token");
|
||||
}
|
||||
}
|
||||
|
||||
boolean foundFormat = false;
|
||||
boolean done = false;
|
||||
while (!done) {
|
||||
if (buf.equals("FORMAT=32-bit_rle_rgbe")) {
|
||||
foundFormat = true;
|
||||
}
|
||||
else if (buf.startsWith(gammaString)) {
|
||||
valid |= VALID_GAMMA;
|
||||
gamma = Float.parseFloat(buf.substring(gammaString.length()));
|
||||
}
|
||||
else if (buf.startsWith(exposureString)) {
|
||||
valid |= VALID_EXPOSURE;
|
||||
exposure = Float.parseFloat(buf.substring(exposureString.length()));
|
||||
}
|
||||
else {
|
||||
Matcher m = widthHeightPattern.matcher(buf);
|
||||
if (m.matches()) {
|
||||
width = Integer.parseInt(m.group(2));
|
||||
height = Integer.parseInt(m.group(1));
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
buf = in.readLine();
|
||||
if (buf == null) {
|
||||
throw new IOException("Unexpected EOF reading header");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundFormat) {
|
||||
throw new IOException("No FORMAT specifier found");
|
||||
}
|
||||
|
||||
return new Header(valid, programType, gamma, exposure, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple read routine. Will not correctly handle run length encoding.
|
||||
*/
|
||||
public static void readPixels(DataInput in, float[] data, int numpixels) throws IOException {
|
||||
byte[] rgbe = new byte[4];
|
||||
float[] rgb = new float[3];
|
||||
int offset = 0;
|
||||
|
||||
while (numpixels-- > 0) {
|
||||
in.readFully(rgbe);
|
||||
|
||||
rgbe2float(rgb, rgbe, 0);
|
||||
|
||||
data[offset++] = rgb[0];
|
||||
data[offset++] = rgb[1];
|
||||
data[offset++] = rgb[2];
|
||||
}
|
||||
}
|
||||
|
||||
public static void readPixelsRaw(DataInput in, byte[] data, int offset, int numpixels) throws IOException {
|
||||
int numExpected = 4 * numpixels;
|
||||
in.readFully(data, offset, numExpected);
|
||||
}
|
||||
|
||||
public static void readPixelsRawRLE(DataInput in, byte[] data, int offset,
|
||||
int scanline_width, int num_scanlines) throws IOException {
|
||||
byte[] rgbe = new byte[4];
|
||||
byte[] scanline_buffer = null;
|
||||
int ptr, ptr_end;
|
||||
int count;
|
||||
byte[] buf = new byte[2];
|
||||
|
||||
if ((scanline_width < 8) || (scanline_width > 0x7fff)) {
|
||||
// run length encoding is not allowed so read flat
|
||||
readPixelsRaw(in, data, offset, scanline_width * num_scanlines);
|
||||
}
|
||||
|
||||
// read in each successive scanline
|
||||
while (num_scanlines > 0) {
|
||||
in.readFully(rgbe);
|
||||
|
||||
if ((rgbe[0] != 2) || (rgbe[1] != 2) || ((rgbe[2] & 0x80) != 0)) {
|
||||
// this file is not run length encoded
|
||||
data[offset++] = rgbe[0];
|
||||
data[offset++] = rgbe[1];
|
||||
data[offset++] = rgbe[2];
|
||||
data[offset++] = rgbe[3];
|
||||
readPixelsRaw(in, data, offset, scanline_width * num_scanlines - 1);
|
||||
}
|
||||
|
||||
if ((((rgbe[2] & 0xFF) << 8) | (rgbe[3] & 0xFF)) != scanline_width) {
|
||||
throw new IOException("Wrong scanline width " +
|
||||
(((rgbe[2] & 0xFF) << 8) | (rgbe[3] & 0xFF)) +
|
||||
", expected " + scanline_width);
|
||||
}
|
||||
|
||||
if (scanline_buffer == null) {
|
||||
scanline_buffer = new byte[4 * scanline_width];
|
||||
}
|
||||
|
||||
ptr = 0;
|
||||
// read each of the four channels for the scanline into the buffer
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ptr_end = (i + 1) * scanline_width;
|
||||
while (ptr < ptr_end) {
|
||||
in.readFully(buf);
|
||||
|
||||
if ((buf[0] & 0xFF) > 128) {
|
||||
// a run of the same value
|
||||
count = (buf[0] & 0xFF) - 128;
|
||||
if ((count == 0) || (count > ptr_end - ptr)) {
|
||||
throw new IOException("Bad scanline data");
|
||||
}
|
||||
while (count-- > 0) {
|
||||
scanline_buffer[ptr++] = buf[1];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// a non-run
|
||||
count = buf[0] & 0xFF;
|
||||
if ((count == 0) || (count > ptr_end - ptr)) {
|
||||
throw new IOException("Bad scanline data");
|
||||
}
|
||||
scanline_buffer[ptr++] = buf[1];
|
||||
if (--count > 0) {
|
||||
in.readFully(scanline_buffer, ptr, count);
|
||||
ptr += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// copy byte data to output
|
||||
for (int i = 0; i < scanline_width; i++) {
|
||||
data[offset++] = scanline_buffer[i];
|
||||
data[offset++] = scanline_buffer[i + scanline_width];
|
||||
data[offset++] = scanline_buffer[i + 2 * scanline_width];
|
||||
data[offset++] = scanline_buffer[i + 3 * scanline_width];
|
||||
}
|
||||
num_scanlines--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard conversion from float pixels to rgbe pixels.
|
||||
*/
|
||||
public static void float2rgbe(byte[] rgbe, float red, float green, float blue) {
|
||||
float v;
|
||||
int e;
|
||||
|
||||
v = red;
|
||||
if (green > v) {
|
||||
v = green;
|
||||
}
|
||||
if (blue > v) {
|
||||
v = blue;
|
||||
}
|
||||
if (v < 1e-32f) {
|
||||
rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0;
|
||||
}
|
||||
else {
|
||||
FracExp fe = frexp(v);
|
||||
v = (float) (fe.getFraction() * 256.0 / v);
|
||||
rgbe[0] = (byte) (red * v);
|
||||
rgbe[1] = (byte) (green * v);
|
||||
rgbe[2] = (byte) (blue * v);
|
||||
rgbe[3] = (byte) (fe.getExponent() + 128);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard conversion from rgbe to float pixels. Note: Ward uses
|
||||
* ldexp(col+0.5,exp-(128+8)). However we wanted pixels in the
|
||||
* range [0,1] to map back into the range [0,1].
|
||||
*/
|
||||
public static void rgbe2float(float[] rgb, byte[] rgbe, int startRGBEOffset) {
|
||||
float f;
|
||||
|
||||
if (rgbe[startRGBEOffset + 3] != 0) { // nonzero pixel
|
||||
f = (float) ldexp(1.0, (rgbe[startRGBEOffset + 3] & 0xFF) - (128 + 8));
|
||||
rgb[0] = (rgbe[startRGBEOffset + 0] & 0xFF) * f;
|
||||
rgb[1] = (rgbe[startRGBEOffset + 1] & 0xFF) * f;
|
||||
rgb[2] = (rgbe[startRGBEOffset + 2] & 0xFF) * f;
|
||||
}
|
||||
else {
|
||||
rgb[0] = 0;
|
||||
rgb[1] = 0;
|
||||
rgb[2] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static double ldexp(double value, int exp) {
|
||||
if (!finite(value) || value == 0.0) {
|
||||
return value;
|
||||
}
|
||||
value = scalbn(value, exp);
|
||||
// No good way to indicate errno (want to avoid throwing
|
||||
// exceptions because don't know about stability of calculations)
|
||||
// if(!finite(value)||value==0.0) errno = ERANGE;
|
||||
return value;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Internals only below this point
|
||||
//
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Math routines, some fdlibm-derived
|
||||
//
|
||||
|
||||
static class FracExp {
|
||||
private double fraction;
|
||||
private int exponent;
|
||||
|
||||
public FracExp(double fraction, int exponent) {
|
||||
this.fraction = fraction;
|
||||
this.exponent = exponent;
|
||||
}
|
||||
|
||||
public double getFraction() {
|
||||
return fraction;
|
||||
}
|
||||
|
||||
public int getExponent() {
|
||||
return exponent;
|
||||
}
|
||||
}
|
||||
|
||||
private static final double two54 = 1.80143985094819840000e+16; // 43500000 00000000
|
||||
private static final double twom54 = 5.55111512312578270212e-17; // 0x3C900000 0x00000000
|
||||
private static final double huge = 1.0e+300;
|
||||
private static final double tiny = 1.0e-300;
|
||||
|
||||
private static int hi(double x) {
|
||||
long bits = Double.doubleToRawLongBits(x);
|
||||
return (int) (bits >>> 32);
|
||||
}
|
||||
|
||||
private static int lo(double x) {
|
||||
long bits = Double.doubleToRawLongBits(x);
|
||||
return (int) bits;
|
||||
}
|
||||
|
||||
private static double fromhilo(int hi, int lo) {
|
||||
return Double.longBitsToDouble((((long) hi) << 32) |
|
||||
(((long) lo) & 0xFFFFFFFFL));
|
||||
}
|
||||
|
||||
private static FracExp frexp(double x) {
|
||||
int hx = hi(x);
|
||||
int ix = 0x7fffffff & hx;
|
||||
int lx = lo(x);
|
||||
int e = 0;
|
||||
if (ix >= 0x7ff00000 || ((ix | lx) == 0)) {
|
||||
return new FracExp(x, e); // 0,inf,nan
|
||||
}
|
||||
if (ix < 0x00100000) { // subnormal
|
||||
x *= two54;
|
||||
hx = hi(x);
|
||||
ix = hx & 0x7fffffff;
|
||||
e = -54;
|
||||
}
|
||||
e += (ix >> 20) - 1022;
|
||||
hx = (hx & 0x800fffff) | 0x3fe00000;
|
||||
lx = lo(x);
|
||||
return new FracExp(fromhilo(hx, lx), e);
|
||||
}
|
||||
|
||||
private static boolean finite(double x) {
|
||||
int hx;
|
||||
hx = hi(x);
|
||||
return (((hx & 0x7fffffff) - 0x7ff00000) >> 31) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* copysign(double x, double y) <BR>
|
||||
* copysign(x,y) returns a value with the magnitude of x and
|
||||
* with the sign bit of y.
|
||||
*/
|
||||
private static double copysign(double x, double y) {
|
||||
return fromhilo((hi(x) & 0x7fffffff) | (hi(y) & 0x80000000), lo(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* scalbn (double x, int n) <BR>
|
||||
* scalbn(x,n) returns x* 2**n computed by exponent
|
||||
* manipulation rather than by actually performing an
|
||||
* exponentiation or a multiplication.
|
||||
*/
|
||||
private static double scalbn(double x, int n) {
|
||||
int hx = hi(x);
|
||||
int lx = lo(x);
|
||||
int k = (hx & 0x7ff00000) >> 20; // extract exponent
|
||||
if (k == 0) { // 0 or subnormal x
|
||||
if ((lx | (hx & 0x7fffffff)) == 0) {
|
||||
return x; // +-0
|
||||
}
|
||||
x *= two54;
|
||||
hx = hi(x);
|
||||
k = ((hx & 0x7ff00000) >> 20) - 54;
|
||||
if (n < -50000) {
|
||||
return tiny * x; // underflow
|
||||
}
|
||||
}
|
||||
if (k == 0x7ff) {
|
||||
return x + x; // NaN or Inf
|
||||
}
|
||||
k = k + n;
|
||||
if (k > 0x7fe) {
|
||||
return huge * copysign(huge, x); // overflow
|
||||
}
|
||||
if (k > 0) {
|
||||
// normal result
|
||||
return fromhilo((hx & 0x800fffff) | (k << 20), lo(x));
|
||||
}
|
||||
if (k <= -54) {
|
||||
if (n > 50000) {
|
||||
// in case integer overflow in n+k
|
||||
return huge * copysign(huge, x); // overflow
|
||||
}
|
||||
else {
|
||||
return tiny * copysign(tiny, x); // underflow
|
||||
}
|
||||
}
|
||||
k += 54; // subnormal result
|
||||
x = fromhilo((hx & 0x800fffff) | (k << 20), lo(x));
|
||||
return x * twom54;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Test harness
|
||||
//
|
||||
|
||||
public static void main(String[] args) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
try {
|
||||
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(args[i])));
|
||||
Header header = RGBE.readHeader(in);
|
||||
System.err.println("Header for file \"" + args[i] + "\":");
|
||||
System.err.println(" " + header);
|
||||
byte[] data = new byte[header.getWidth() * header.getHeight() * 4];
|
||||
readPixelsRawRLE(in, data, 0, header.getWidth(), header.getHeight());
|
||||
in.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr.tonemap;
|
||||
|
||||
/**
|
||||
* DefaultToneMapper.
|
||||
* <p/>
|
||||
* Normalizes values to range [0...1] using:
|
||||
*
|
||||
* <p><em>V<sub>out</sub> = V<sub>in</sub> / (V<sub>in</sub> + C)</em></p>
|
||||
*
|
||||
* Where <em>C</em> is constant.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: DefaultToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public final class DefaultToneMapper implements ToneMapper {
|
||||
|
||||
private final float constant;
|
||||
|
||||
public DefaultToneMapper() {
|
||||
this(1);
|
||||
}
|
||||
|
||||
public DefaultToneMapper(final float constant) {
|
||||
this.constant = constant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void map(final float[] rgb) {
|
||||
// Default Vo = Vi / (Vi + 1)
|
||||
for (int i = 0; i < rgb.length; i++) {
|
||||
rgb[i] = rgb[i] / (rgb[i] + constant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr.tonemap;
|
||||
|
||||
/**
|
||||
* GammaToneMapper.
|
||||
* <p/>
|
||||
* Normalizes values to range [0...1] using:
|
||||
*
|
||||
* <p><em>V<sub>out</sub> = A V<sub>in</sub><sup>\u03b3</sup></em></p>
|
||||
*
|
||||
* Where <em>A</em> is constant and <em>\u03b3</em> is the gamma.
|
||||
* Values > 1 are clamped.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: GammaToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public final class GammaToneMapper implements ToneMapper {
|
||||
|
||||
private final float constant;
|
||||
private final float gamma;
|
||||
|
||||
public GammaToneMapper() {
|
||||
this(0.5f, .25f);
|
||||
}
|
||||
|
||||
public GammaToneMapper(final float constant, final float gamma) {
|
||||
this.constant = constant;
|
||||
this.gamma = gamma;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void map(final float[] rgb) {
|
||||
// Gamma Vo = A * Vi^y
|
||||
for (int i = 0; i < rgb.length; i++) {
|
||||
rgb[i] = Math.min(1f, (float) (constant * Math.pow(rgb[i], gamma)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr.tonemap;
|
||||
|
||||
/**
|
||||
* NullToneMapper.
|
||||
* <p/>
|
||||
* This {@code ToneMapper} does *not* normalize or clamp values
|
||||
* to range [0...1], but leaves the values as-is.
|
||||
* Useful for applications that implements custom tone mapping.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: NullToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public final class NullToneMapper implements ToneMapper {
|
||||
@Override
|
||||
public void map(float[] rgb) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr.tonemap;
|
||||
|
||||
/**
|
||||
* ToneMapper.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: ToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public interface ToneMapper {
|
||||
void map(float[] rgb);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.twelvemonkeys.imageio.plugins.hdr.HDRImageReaderSpi
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.hdr;
|
||||
|
||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
|
||||
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* TGAImageReaderTest
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: TGAImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$
|
||||
*/
|
||||
public class HDRImageReaderTest extends ImageReaderAbstractTest<HDRImageReader> {
|
||||
@Override
|
||||
protected List<TestData> getTestData() {
|
||||
return Arrays.asList(
|
||||
new TestData(getClassLoaderResource("/hdr/memorial_o876.hdr"), new Dimension(512, 768))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImageReaderSpi createProvider() {
|
||||
return new HDRImageReaderSpi();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<HDRImageReader> getReaderClass() {
|
||||
return HDRImageReader.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HDRImageReader createReader() {
|
||||
return new HDRImageReader(createProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getFormatNames() {
|
||||
return Arrays.asList("HDR", "hdr", "RGBE", "rgbe");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getSuffixes() {
|
||||
return Arrays.asList("hdr", "rgbe", "xyze");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getMIMETypes() {
|
||||
return Collections.singletonList(
|
||||
"image/vnd.radiance"
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr
Normal file
BIN
imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr
Normal file
Binary file not shown.
@@ -18,7 +18,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
|
||||
@@ -267,10 +267,15 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
@Override
|
||||
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
|
||||
// If delegate can determine the spec, we'll just go with that
|
||||
ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex);
|
||||
try {
|
||||
ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex);
|
||||
|
||||
if (rawType != null) {
|
||||
return rawType;
|
||||
if (rawType != null) {
|
||||
return rawType;
|
||||
}
|
||||
}
|
||||
catch (NullPointerException ignore) {
|
||||
// Fall through
|
||||
}
|
||||
|
||||
// Otherwise, consult the image metadata
|
||||
@@ -312,22 +317,10 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
assertInput();
|
||||
checkBounds(imageIndex);
|
||||
|
||||
// CompoundDirectory exif = getExif();
|
||||
// if (exif != null) {
|
||||
// System.err.println("exif: " + exif);
|
||||
// System.err.println("Orientation: " + exif.getEntryById(TIFF.TAG_ORIENTATION));
|
||||
// Entry exifIFDEntry = exif.getEntryById(TIFF.TAG_EXIF_IFD);
|
||||
//
|
||||
// if (exifIFDEntry != null) {
|
||||
// Directory exifIFD = (Directory) exifIFDEntry.getValue();
|
||||
// System.err.println("PixelXDimension: " + exifIFD.getEntryById(EXIF.TAG_PIXEL_X_DIMENSION));
|
||||
// System.err.println("PixelYDimension: " + exifIFD.getEntryById(EXIF.TAG_PIXEL_Y_DIMENSION));
|
||||
// }
|
||||
// }
|
||||
|
||||
SOFSegment sof = getSOF();
|
||||
ICC_Profile profile = getEmbeddedICCProfile(false);
|
||||
AdobeDCTSegment adobeDCT = getAdobeDCT();
|
||||
boolean bogusAdobeDCT = false;
|
||||
|
||||
if (adobeDCT != null && (adobeDCT.getTransform() == AdobeDCTSegment.YCC && sof.componentsInFrame() != 3 ||
|
||||
adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() != 4)) {
|
||||
@@ -338,6 +331,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
sof.marker & 0xf, sof.componentsInFrame()
|
||||
));
|
||||
|
||||
bogusAdobeDCT = true;
|
||||
adobeDCT = null;
|
||||
}
|
||||
|
||||
@@ -346,11 +340,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
|
||||
// - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream.
|
||||
if (delegate.canReadRaster() && (
|
||||
bogusAdobeDCT ||
|
||||
sourceCSType == JPEGColorSpace.CMYK ||
|
||||
sourceCSType == JPEGColorSpace.YCCK ||
|
||||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK ||
|
||||
profile != null && !ColorSpaces.isCS_sRGB(profile)) ||
|
||||
sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null) { // TODO: Issue warning?
|
||||
profile != null && !ColorSpaces.isCS_sRGB(profile) ||
|
||||
sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null)) { // TODO: Issue warning?
|
||||
if (DEBUG) {
|
||||
System.out.println("Reading using raster and extra conversion");
|
||||
System.out.println("ICC color profile: " + profile);
|
||||
@@ -471,12 +465,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
YCbCrConverter.convertYCbCr2RGB(raster);
|
||||
}
|
||||
else if (csType == JPEGColorSpace.YCCK) {
|
||||
YCbCrConverter.convertYCCK2CMYK(raster);
|
||||
// TODO: Need to rethink this (non-) inversion, see #147
|
||||
// TODO: Allow param to specify inversion, or possibly the PDF decode array
|
||||
// flag0 bit 15, blend = 1 see http://graphicdesign.stackexchange.com/questions/12894/cmyk-jpegs-extracted-from-pdf-appear-inverted
|
||||
if ((getAdobeDCT().flags0 & 0x8000) != 0) {
|
||||
/// TODO: Better yet would be to not inverting in the first place, add flag to convertYCCK2CMYK
|
||||
invertCMYK(raster);
|
||||
}
|
||||
boolean invert = true;// || (adobeDCT.flags0 & 0x8000) == 0;
|
||||
YCbCrConverter.convertYCCK2CMYK(raster, invert);
|
||||
}
|
||||
else if (csType == JPEGColorSpace.CMYK) {
|
||||
invertCMYK(raster);
|
||||
@@ -948,6 +941,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
delegate.abort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageReadParam getDefaultReadParam() {
|
||||
return delegate.getDefaultReadParam();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readerSupportsThumbnails() {
|
||||
return true; // We support EXIF, JFIF and JFXX style thumbnails
|
||||
@@ -1176,19 +1174,28 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
|
||||
}
|
||||
|
||||
static void convertYCCK2CMYK(final Raster raster) {
|
||||
static void convertYCCK2CMYK(final Raster raster, final boolean invert) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCCK2CMYK(data, data, (x + y * width) * 4);
|
||||
if (invert) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCCK2CMYKInverted(data, data, (x + y * width) * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCCK2CMYK(data, data, (x + y * width) * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
|
||||
private static void convertYCCK2CMYKInverted(byte[] ycck, byte[] cmyk, int offset) {
|
||||
// Inverted
|
||||
int y = 255 - ycck[offset ] & 0xff;
|
||||
int cb = 255 - ycck[offset + 1] & 0xff;
|
||||
@@ -1205,6 +1212,22 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
cmyk[offset + 3] = (byte) k; // K passes through unchanged
|
||||
}
|
||||
|
||||
private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
|
||||
int y = ycck[offset ] & 0xff;
|
||||
int cb = ycck[offset + 1] & 0xff;
|
||||
int cr = ycck[offset + 2] & 0xff;
|
||||
int k = ycck[offset + 3] & 0xff;
|
||||
|
||||
int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
|
||||
int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
|
||||
|
||||
cmyk[offset ] = clamp(cmykC);
|
||||
cmyk[offset + 1] = clamp(cmykM);
|
||||
cmyk[offset + 2] = clamp(cmykY);
|
||||
cmyk[offset + 3] = (byte) k; // K passes through unchanged
|
||||
}
|
||||
|
||||
private static byte clamp(int val) {
|
||||
return (byte) Math.max(0, Math.min(255, val));
|
||||
}
|
||||
|
||||
@@ -1491,4 +1491,40 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRawImageTypeAdobeAPP14CMYKAnd3channelData() throws IOException {
|
||||
JPEGImageReader reader = createReader();
|
||||
|
||||
try {
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jfif-app13-app14ycck-3channel.jpg")));
|
||||
|
||||
ImageTypeSpecifier rawType = reader.getRawImageType(0);
|
||||
assertNull(rawType); // But no exception, please...
|
||||
}
|
||||
finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadAdobeAPP14CMYKAnd3channelData() throws IOException {
|
||||
JPEGImageReader reader = createReader();
|
||||
|
||||
try {
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jfif-app13-app14ycck-3channel.jpg")));
|
||||
|
||||
assertEquals(310, reader.getWidth(0));
|
||||
assertEquals(384, reader.getHeight(0));
|
||||
|
||||
BufferedImage image = reader.read(0, null);
|
||||
assertNotNull(image);
|
||||
assertEquals(310, image.getWidth());
|
||||
assertEquals(384, image.getHeight());
|
||||
assertEquals(ColorSpace.TYPE_RGB, image.getColorModel().getColorSpace().getType());
|
||||
}
|
||||
finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -38,13 +38,14 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry;
|
||||
* @version $Id: EXIFEntry.java,v 1.0 Nov 13, 2009 5:47:35 PM haraldk Exp$
|
||||
*/
|
||||
final class EXIFEntry extends AbstractEntry {
|
||||
// TODO: Expose as TIFFEntry
|
||||
final private short type;
|
||||
|
||||
EXIFEntry(final int identifier, final Object value, final short type) {
|
||||
super(identifier, value);
|
||||
|
||||
if (type < 1 || type >= TIFF.TYPE_NAMES.length) {
|
||||
throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", type));
|
||||
throw new IllegalArgumentException(String.format("Illegal TIFF type: %s", type));
|
||||
}
|
||||
|
||||
// TODO: Validate that type is applicable to value?
|
||||
@@ -114,6 +115,8 @@ final class EXIFEntry extends AbstractEntry {
|
||||
return "PlanarConfiguration";
|
||||
case TIFF.TAG_RESOLUTION_UNIT:
|
||||
return "ResolutionUnit";
|
||||
case TIFF.TAG_PAGE_NAME:
|
||||
return "PageName";
|
||||
case TIFF.TAG_PAGE_NUMBER:
|
||||
return "PageNumber";
|
||||
case TIFF.TAG_SOFTWARE:
|
||||
@@ -228,6 +231,8 @@ final class EXIFEntry extends AbstractEntry {
|
||||
return "DateTimeDigitized";
|
||||
case EXIF.TAG_IMAGE_NUMBER:
|
||||
return "ImageNumber";
|
||||
case EXIF.TAG_MAKER_NOTE:
|
||||
return "MakerNote";
|
||||
case EXIF.TAG_USER_COMMENT:
|
||||
return "UserComment";
|
||||
|
||||
|
||||
@@ -89,8 +89,8 @@ public final class EXIFReader extends MetadataReader {
|
||||
|
||||
// TODO: Consider re-writing so that the linked IFD parsing is done externally to the method
|
||||
protected Directory readDirectory(final ImageInputStream pInput, final long pOffset, final boolean readLinked) throws IOException {
|
||||
List<IFD> ifds = new ArrayList<IFD>();
|
||||
List<Entry> entries = new ArrayList<Entry>();
|
||||
List<IFD> ifds = new ArrayList<>();
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
|
||||
pInput.seek(pOffset);
|
||||
long nextOffset = -1;
|
||||
@@ -156,7 +156,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
try {
|
||||
if (KNOWN_IFDS.contains(tagId)) {
|
||||
long[] pointerOffsets = getPointerOffsets(entry);
|
||||
List<IFD> subIFDs = new ArrayList<IFD>(pointerOffsets.length);
|
||||
List<IFD> subIFDs = new ArrayList<>(pointerOffsets.length);
|
||||
|
||||
for (long pointerOffset : pointerOffsets) {
|
||||
CompoundDirectory subDirectory = (CompoundDirectory) readDirectory(input, pointerOffset, false);
|
||||
@@ -177,8 +177,11 @@ public final class EXIFReader extends MetadataReader {
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
// TODO: Issue warning without crashing...?
|
||||
e.printStackTrace();
|
||||
if (DEBUG) {
|
||||
// TODO: Issue warning without crashing...?
|
||||
System.err.println("Error parsing sub-IFD: " + tagId);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import java.io.IOException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -248,9 +249,12 @@ public final class EXIFWriter extends MetadataWriter {
|
||||
switch (type) {
|
||||
case TIFF.TYPE_UNDEFINED:
|
||||
case TIFF.TYPE_BYTE:
|
||||
case TIFF.TYPE_SBYTE:
|
||||
stream.write((byte[]) value);
|
||||
break;
|
||||
|
||||
case TIFF.TYPE_SHORT:
|
||||
case TIFF.TYPE_SSHORT:
|
||||
short[] shorts;
|
||||
|
||||
if (value instanceof short[]) {
|
||||
@@ -279,7 +283,9 @@ public final class EXIFWriter extends MetadataWriter {
|
||||
|
||||
stream.writeShorts(shorts, 0, shorts.length);
|
||||
break;
|
||||
|
||||
case TIFF.TYPE_LONG:
|
||||
case TIFF.TYPE_SLONG:
|
||||
int[] ints;
|
||||
|
||||
if (value instanceof int[]) {
|
||||
@@ -298,17 +304,45 @@ public final class EXIFWriter extends MetadataWriter {
|
||||
}
|
||||
|
||||
stream.writeInts(ints, 0, ints.length);
|
||||
|
||||
break;
|
||||
|
||||
case TIFF.TYPE_RATIONAL:
|
||||
case TIFF.TYPE_SRATIONAL:
|
||||
Rational[] rationals = (Rational[]) value;
|
||||
for (Rational rational : rationals) {
|
||||
stream.writeInt((int) rational.numerator());
|
||||
stream.writeInt((int) rational.denominator());
|
||||
}
|
||||
|
||||
// TODO: More types
|
||||
break;
|
||||
|
||||
case TIFF.TYPE_FLOAT:
|
||||
float[] floats;
|
||||
|
||||
if (value instanceof float[]) {
|
||||
floats = (float[]) value;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass());
|
||||
}
|
||||
|
||||
stream.writeFloats(floats, 0, floats.length);
|
||||
|
||||
break;
|
||||
|
||||
case TIFF.TYPE_DOUBLE:
|
||||
double[] doubles;
|
||||
|
||||
if (value instanceof double[]) {
|
||||
doubles = (double[]) value;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass());
|
||||
}
|
||||
|
||||
stream.writeDoubles(doubles, 0, doubles.length);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported TIFF type: " + type);
|
||||
@@ -319,27 +353,36 @@ public final class EXIFWriter extends MetadataWriter {
|
||||
// }
|
||||
else {
|
||||
switch (type) {
|
||||
case TIFF.TYPE_UNDEFINED:
|
||||
case TIFF.TYPE_BYTE:
|
||||
stream.writeByte((Integer) value);
|
||||
case TIFF.TYPE_SBYTE:
|
||||
case TIFF.TYPE_UNDEFINED:
|
||||
stream.writeByte(((Number) value).intValue());
|
||||
break;
|
||||
case TIFF.TYPE_ASCII:
|
||||
byte[] bytes = ((String) value).getBytes(Charset.forName("UTF-8"));
|
||||
byte[] bytes = ((String) value).getBytes(StandardCharsets.UTF_8);
|
||||
stream.write(bytes);
|
||||
stream.write(0);
|
||||
break;
|
||||
case TIFF.TYPE_SHORT:
|
||||
stream.writeShort((Integer) value);
|
||||
case TIFF.TYPE_SSHORT:
|
||||
stream.writeShort(((Number) value).intValue());
|
||||
break;
|
||||
case TIFF.TYPE_LONG:
|
||||
case TIFF.TYPE_SLONG:
|
||||
stream.writeInt(((Number) value).intValue());
|
||||
break;
|
||||
case TIFF.TYPE_RATIONAL:
|
||||
case TIFF.TYPE_SRATIONAL:
|
||||
Rational rational = (Rational) value;
|
||||
stream.writeInt((int) rational.numerator());
|
||||
stream.writeInt((int) rational.denominator());
|
||||
break;
|
||||
// TODO: More types
|
||||
case TIFF.TYPE_FLOAT:
|
||||
stream.writeFloat(((Number) value).floatValue());
|
||||
break;
|
||||
case TIFF.TYPE_DOUBLE:
|
||||
stream.writeDouble(((Number) value).doubleValue());
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported TIFF type: " + type);
|
||||
@@ -356,11 +399,26 @@ public final class EXIFWriter extends MetadataWriter {
|
||||
}
|
||||
|
||||
private short getType(final Entry entry) {
|
||||
// TODO: What a MESS! Rewrite and expose EXIFEntry as TIFFEntry or so...
|
||||
|
||||
// For internal entries use type directly
|
||||
if (entry instanceof EXIFEntry) {
|
||||
EXIFEntry exifEntry = (EXIFEntry) entry;
|
||||
return exifEntry.getType();
|
||||
}
|
||||
|
||||
// For other entries, use name if it matches
|
||||
String typeName = entry.getTypeName();
|
||||
|
||||
if (typeName != null) {
|
||||
for (int i = 1; i < TIFF.TYPE_NAMES.length; i++) {
|
||||
if (typeName.equals(TIFF.TYPE_NAMES[i])) {
|
||||
return (short) i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, fall back to the native Java type
|
||||
Object value = Validate.notNull(entry.getValue());
|
||||
|
||||
boolean array = value.getClass().isArray();
|
||||
|
||||
@@ -95,6 +95,7 @@ public interface TIFF {
|
||||
null, null,
|
||||
"LONG8", "SLONG8", "IFD8"
|
||||
};
|
||||
/** Length of the corresponding type, in bytes. */
|
||||
int[] TYPE_LENGTHS = {
|
||||
-1,
|
||||
1, 1, 2, 4, 8,
|
||||
@@ -165,6 +166,7 @@ public interface TIFF {
|
||||
int TAG_IMAGE_DESCRIPTION = 270;
|
||||
int TAG_MAKE = 271;
|
||||
int TAG_MODEL = 272;
|
||||
int TAG_PAGE_NAME = 285;
|
||||
int TAG_PAGE_NUMBER = 297;
|
||||
int TAG_SOFTWARE = 305;
|
||||
int TAG_ARTIST = 315;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
package com.twelvemonkeys.imageio.plugins.tga;
|
||||
|
||||
interface TGA {
|
||||
byte[] MAGIC = {'T', 'R', 'U', 'E', 'V', 'I', 'S', 'I', 'O', 'N', '-', 'X', 'F', 'I', 'L', 'E', '.', 0};
|
||||
|
||||
/** Fixed header size: 18.*/
|
||||
int HEADER_SIZE = 18;
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.twelvemonkeys.imageio.plugins.tga;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* TGAExtensions.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: TGAExtensions.java,v 1.0 27/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
final class TGAExtensions {
|
||||
public static final int EXT_AREA_SIZE = 495;
|
||||
|
||||
private String authorName;
|
||||
private String authorComments;
|
||||
|
||||
private Calendar creationDate;
|
||||
private String jobId;
|
||||
|
||||
private String softwareId;
|
||||
private String softwareVersion;
|
||||
|
||||
private int backgroundColor;
|
||||
private double pixelAspectRatio;
|
||||
private double gamma;
|
||||
|
||||
private long colorCorrectionOffset;
|
||||
private long postageStampOffset;
|
||||
private long scanLineOffset;
|
||||
|
||||
private int attributeType;
|
||||
|
||||
private TGAExtensions() {
|
||||
}
|
||||
|
||||
static TGAExtensions read(final ImageInputStream stream) throws IOException {
|
||||
int extSize = stream.readUnsignedShort();
|
||||
|
||||
// Should always be 495 for version 2.0, no newer version exists...
|
||||
if (extSize < EXT_AREA_SIZE) {
|
||||
throw new IIOException(String.format("TGA Extension Area size less than %d: %d", EXT_AREA_SIZE, extSize));
|
||||
}
|
||||
|
||||
TGAExtensions extensions = new TGAExtensions();
|
||||
extensions.authorName = readString(stream, 41);;
|
||||
extensions.authorComments = readString(stream, 324);
|
||||
extensions.creationDate = readDate(stream);
|
||||
extensions.jobId = readString(stream, 41);
|
||||
|
||||
stream.skipBytes(6); // Job time, 3 shorts, hours/minutes/seconds elapsed
|
||||
|
||||
extensions.softwareId = readString(stream, 41);
|
||||
|
||||
// Software version (* 100) short + single byte ASCII (ie. 101 'b' for 1.01b)
|
||||
int softwareVersion = stream.readUnsignedShort();
|
||||
int softwareLetter = stream.readByte();
|
||||
|
||||
extensions.softwareVersion = softwareVersion != 0 && softwareLetter != ' '
|
||||
? String.format("%d.%d%d", softwareVersion / 100, softwareVersion % 100, softwareLetter).trim()
|
||||
: null;
|
||||
|
||||
extensions.backgroundColor = stream.readInt(); // ARGB
|
||||
|
||||
extensions.pixelAspectRatio = readRational(stream);
|
||||
extensions.gamma = readRational(stream);
|
||||
|
||||
extensions.colorCorrectionOffset = stream.readUnsignedInt();
|
||||
extensions.postageStampOffset = stream.readUnsignedInt();
|
||||
extensions.scanLineOffset = stream.readUnsignedInt();
|
||||
|
||||
// Offset 494 specifies Attribute type:
|
||||
// 0: no Alpha data included (bits 3-0 of field 5.6 should also be set to zero)
|
||||
// 1: undefined data in the Alpha field, can be ignored
|
||||
// 2: undefined data in the Alpha field, but should be retained
|
||||
// 3: useful Alpha channel data is present
|
||||
// 4: pre-multiplied Alpha (see description below)
|
||||
// 5 -127: RESERVED
|
||||
// 128-255: Un-assigned
|
||||
extensions.attributeType = stream.readUnsignedByte();
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
private static double readRational(final ImageInputStream stream) throws IOException {
|
||||
int numerator = stream.readUnsignedShort();
|
||||
int denominator = stream.readUnsignedShort();
|
||||
|
||||
return denominator != 0 ? numerator / (double) denominator : 1;
|
||||
}
|
||||
|
||||
private static Calendar readDate(final ImageInputStream stream) throws IOException {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.clear();
|
||||
|
||||
int month = stream.readUnsignedShort();
|
||||
int date = stream.readUnsignedShort();
|
||||
int year = stream.readUnsignedShort();
|
||||
|
||||
int hourOfDay = stream.readUnsignedShort();
|
||||
int minute = stream.readUnsignedShort();
|
||||
int second = stream.readUnsignedShort();
|
||||
|
||||
// Unused
|
||||
if (month == 0 && year == 0 && date == 0 && hourOfDay == 0 && minute == 0 && second == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
calendar.set(year, month - 1, date, hourOfDay, minute, second);
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private static String readString(final ImageInputStream stream, final int maxLength) throws IOException {
|
||||
byte[] data = new byte[maxLength];
|
||||
stream.readFully(data);
|
||||
|
||||
return asZeroTerminatedASCIIString(data);
|
||||
}
|
||||
|
||||
private static String asZeroTerminatedASCIIString(final byte[] data) {
|
||||
int len = data.length;
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
if (data[i] == 0) {
|
||||
len = i;
|
||||
}
|
||||
}
|
||||
|
||||
return new String(data, 0, len, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public boolean hasAlpha() {
|
||||
switch (attributeType) {
|
||||
case 3:
|
||||
case 4:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAlphaPremultiplied() {
|
||||
switch (attributeType) {
|
||||
case 4:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public long getThumbnailOffset() {
|
||||
return postageStampOffset;
|
||||
}
|
||||
|
||||
public String getAuthorName() {
|
||||
return authorName;
|
||||
}
|
||||
|
||||
public String getAuthorComments() {
|
||||
return authorComments;
|
||||
}
|
||||
|
||||
public Calendar getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public String getSoftware() {
|
||||
return softwareId;
|
||||
}
|
||||
|
||||
public String getSoftwareVersion() {
|
||||
return softwareVersion;
|
||||
}
|
||||
|
||||
public double getPixelAspectRatio() {
|
||||
return pixelAspectRatio;
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
|
||||
import com.twelvemonkeys.io.LittleEndianDataInputStream;
|
||||
import com.twelvemonkeys.io.enc.DecoderStream;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
import com.twelvemonkeys.xml.XMLSerializer;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
@@ -51,6 +52,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@@ -59,6 +61,7 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
// http://www.gamers.org/dEngine/quake3/TGA.txt
|
||||
|
||||
private TGAHeader header;
|
||||
private TGAExtensions extensions;
|
||||
|
||||
protected TGAImageReader(final ImageReaderSpi provider) {
|
||||
super(provider);
|
||||
@@ -67,6 +70,7 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
@Override
|
||||
protected void resetMembers() {
|
||||
header = null;
|
||||
extensions = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,7 +93,7 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
|
||||
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
||||
|
||||
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
|
||||
List<ImageTypeSpecifier> specifiers = new ArrayList<>();
|
||||
|
||||
// TODO: Implement
|
||||
specifiers.add(rawType);
|
||||
@@ -110,19 +114,29 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
return ImageTypeSpecifiers.createFromIndexColorModel(header.getColorMap());
|
||||
case TGA.IMAGETYPE_MONOCHROME:
|
||||
case TGA.IMAGETYPE_MONOCHROME_RLE:
|
||||
return ImageTypeSpecifiers.createGrayscale(1, DataBuffer.TYPE_BYTE);
|
||||
return ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE);
|
||||
case TGA.IMAGETYPE_TRUECOLOR:
|
||||
case TGA.IMAGETYPE_TRUECOLOR_RLE:
|
||||
ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
|
||||
|
||||
boolean hasAlpha = header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha();
|
||||
boolean isAlphaPremultiplied = extensions != null && extensions.isAlphaPremultiplied();
|
||||
|
||||
switch (header.getPixelDepth()) {
|
||||
case 16:
|
||||
if (hasAlpha) {
|
||||
// USHORT_1555_ARGB...
|
||||
return ImageTypeSpecifiers.createPacked(sRGB, 0x7C00, 0x03E0, 0x001F, 0x8000, DataBuffer.TYPE_USHORT, isAlphaPremultiplied);
|
||||
}
|
||||
// Default mask out alpha
|
||||
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB);
|
||||
case 24:
|
||||
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
|
||||
case 32:
|
||||
// 4BYTE_BGRA...
|
||||
return ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, false);
|
||||
// 4BYTE_BGRX...
|
||||
// Can't mask out alpha (efficiently) for 4BYTE, so we'll ignore it while reading instead,
|
||||
// if hasAlpha is false
|
||||
return ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, isAlphaPremultiplied);
|
||||
default:
|
||||
throw new IIOException("Unknown pixel depth for truecolor: " + header.getPixelDepth());
|
||||
}
|
||||
@@ -166,31 +180,32 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
DataInput input;
|
||||
if (imageType == TGA.IMAGETYPE_COLORMAPPED_RLE || imageType == TGA.IMAGETYPE_TRUECOLOR_RLE || imageType == TGA.IMAGETYPE_MONOCHROME_RLE) {
|
||||
input = new LittleEndianDataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new RLEDecoder(header.getPixelDepth())));
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
input = imageInput;
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
switch (header.getPixelDepth()) {
|
||||
case 8:
|
||||
case 24:
|
||||
case 32:
|
||||
byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
|
||||
readRowByte(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataByte, destRaster, clippedRow, y);
|
||||
break;
|
||||
case 16:
|
||||
short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
|
||||
readRowUShort(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataUShort, destRaster, clippedRow, y);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth());
|
||||
}
|
||||
|
||||
processImageProgress(100f * y / height);
|
||||
|
||||
if (height - 1 - y < srcRegion.y) {
|
||||
case 8:
|
||||
case 24:
|
||||
case 32:
|
||||
byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
|
||||
readRowByte(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataByte, destRaster, clippedRow, y);
|
||||
break;
|
||||
}
|
||||
case 16:
|
||||
short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
|
||||
readRowUShort(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataUShort, destRaster, clippedRow, y);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth());
|
||||
}
|
||||
|
||||
processImageProgress(100f * y / height);
|
||||
|
||||
if (height - 1 - y < srcRegion.y) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
@@ -212,11 +227,11 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
input.readFully(rowDataByte, 0, rowDataByte.length);
|
||||
|
||||
if (srcChannel.getNumBands() == 4) {
|
||||
invertAlpha(rowDataByte);
|
||||
if (srcChannel.getNumBands() == 4 && (header.getAttributeBits() == 0 || extensions != null && !extensions.hasAlpha())) {
|
||||
// Remove the alpha channel (make pixels opaque) if there are no "attribute bits" (alpha bits)
|
||||
removeAlpha32(rowDataByte);
|
||||
}
|
||||
|
||||
// Subsample horizontal
|
||||
@@ -240,9 +255,9 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
private void invertAlpha(final byte[] rowDataByte) {
|
||||
for (int i = 3; i < rowDataByte.length; i += 4) {
|
||||
rowDataByte[i] = (byte) (0xFF - rowDataByte[i]);
|
||||
private void removeAlpha32(final byte[] rowData) {
|
||||
for (int i = 3; i < rowData.length; i += 4) {
|
||||
rowData[i] = (byte) 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,21 +328,154 @@ public final class TGAImageReader extends ImageReaderBase {
|
||||
private void readHeader() throws IOException {
|
||||
if (header == null) {
|
||||
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Read header
|
||||
header = TGAHeader.read(imageInput);
|
||||
|
||||
// System.err.println("header: " + header);
|
||||
|
||||
imageInput.flushBefore(imageInput.getStreamPosition());
|
||||
|
||||
// Read footer, if 2.0 format (ends with TRUEVISION-XFILE\0)
|
||||
skipToEnd(imageInput);
|
||||
imageInput.seek(imageInput.getStreamPosition() - 26);
|
||||
|
||||
long extOffset = imageInput.readInt();
|
||||
/*long devOffset = */imageInput.readInt(); // Ignored for now
|
||||
|
||||
byte[] magic = new byte[18];
|
||||
imageInput.readFully(magic);
|
||||
|
||||
if (Arrays.equals(magic, TGA.MAGIC)) {
|
||||
if (extOffset > 0) {
|
||||
imageInput.seek(extOffset);
|
||||
extensions = TGAExtensions.read(imageInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imageInput.seek(imageInput.getFlushedPosition());
|
||||
}
|
||||
|
||||
@Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
|
||||
// TODO: Candidate util method
|
||||
private static void skipToEnd(final ImageInputStream stream) throws IOException {
|
||||
if (stream.length() > 0) {
|
||||
// Seek to end of file
|
||||
stream.seek(stream.length());
|
||||
}
|
||||
else {
|
||||
// Skip to end
|
||||
long lastGood = stream.getStreamPosition();
|
||||
|
||||
while (stream.read() != -1) {
|
||||
lastGood = stream.getStreamPosition();
|
||||
stream.skipBytes(1024);
|
||||
}
|
||||
|
||||
stream.seek(lastGood);
|
||||
|
||||
while (true) {
|
||||
if (stream.read() == -1) {
|
||||
break;
|
||||
}
|
||||
// Just continue reading to EOF...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbnail support
|
||||
|
||||
@Override
|
||||
public boolean readerSupportsThumbnails() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasThumbnails(final int imageIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
return new TGAMetadata(header);
|
||||
return extensions != null && extensions.getThumbnailOffset() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumThumbnails(final int imageIndex) throws IOException {
|
||||
return hasThumbnails(imageIndex) ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getThumbnailWidth(final int imageIndex, final int thumbnailIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
Validate.isTrue(thumbnailIndex >= 0 && thumbnailIndex < getNumThumbnails(imageIndex), "thumbnailIndex >= numThumbnails");
|
||||
|
||||
imageInput.seek(extensions.getThumbnailOffset());
|
||||
|
||||
return imageInput.readUnsignedByte();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getThumbnailHeight(final int imageIndex, final int thumbnailIndex) throws IOException {
|
||||
getThumbnailWidth(imageIndex, thumbnailIndex); // Laziness...
|
||||
|
||||
return imageInput.readUnsignedByte();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage readThumbnail(final int imageIndex, final int thumbnailIndex) throws IOException {
|
||||
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
|
||||
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
||||
|
||||
int width = getThumbnailWidth(imageIndex, thumbnailIndex);
|
||||
int height = getThumbnailHeight(imageIndex, thumbnailIndex);
|
||||
|
||||
// For thumbnail, always read entire image
|
||||
Rectangle srcRegion = new Rectangle(width, height);
|
||||
|
||||
BufferedImage destination = getDestination(null, imageTypes, width, height);
|
||||
WritableRaster destRaster = destination.getRaster();
|
||||
WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
|
||||
|
||||
processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
|
||||
// Thumbnail is always stored non-compressed, no need for RLE support
|
||||
imageInput.seek(extensions.getThumbnailOffset() + 2);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
switch (header.getPixelDepth()) {
|
||||
case 8:
|
||||
case 24:
|
||||
case 32:
|
||||
byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
|
||||
readRowByte(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataByte, destRaster, rowRaster, y);
|
||||
break;
|
||||
case 16:
|
||||
short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
|
||||
readRowUShort(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataUShort, destRaster, rowRaster, y);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth());
|
||||
}
|
||||
|
||||
processThumbnailProgress(100f * y / height);
|
||||
|
||||
if (height - 1 - y < srcRegion.y) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
processThumbnailComplete();
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
// Metadata support
|
||||
|
||||
@Override
|
||||
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
readHeader();
|
||||
|
||||
return new TGAMetadata(header, extensions);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
@@ -45,7 +45,8 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase {
|
||||
super(new TGAProviderInfo());
|
||||
}
|
||||
|
||||
@Override public boolean canDecodeInput(final Object source) throws IOException {
|
||||
@Override
|
||||
public boolean canDecodeInput(final Object source) throws IOException {
|
||||
if (!(source instanceof ImageInputStream)) {
|
||||
return false;
|
||||
}
|
||||
@@ -58,7 +59,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase {
|
||||
try {
|
||||
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// NOTE: The TGA format does not have a magic identifier, so this is guesswork...
|
||||
// NOTE: The original TGA format does not have a magic identifier, so this is guesswork...
|
||||
// We'll try to match sane values, and hope no other files contains the same sequence.
|
||||
|
||||
stream.readUnsignedByte();
|
||||
@@ -88,11 +89,11 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase {
|
||||
|
||||
int colorMapStart = stream.readUnsignedShort();
|
||||
int colorMapSize = stream.readUnsignedShort();
|
||||
int colorMapDetph = stream.readUnsignedByte();
|
||||
int colorMapDepth = stream.readUnsignedByte();
|
||||
|
||||
if (colorMapSize == 0) {
|
||||
// No color map, all 3 fields should be 0
|
||||
if (colorMapStart!= 0 || colorMapDetph != 0) {
|
||||
if (colorMapStart != 0 || colorMapDepth != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +107,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase {
|
||||
if (colorMapStart >= colorMapSize) {
|
||||
return false;
|
||||
}
|
||||
if (colorMapDetph != 15 && colorMapDetph != 16 && colorMapDetph != 24 && colorMapDetph != 32) {
|
||||
if (colorMapDepth != 15 && colorMapDepth != 16 && colorMapDepth != 24 && colorMapDepth != 32) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -134,6 +135,7 @@ public final class TGAImageReaderSpi extends ImageReaderSpiBase {
|
||||
|
||||
// We're pretty sure by now, but there can still be false positives...
|
||||
// For 2.0 format, we could skip to end, and read "TRUEVISION-XFILE.\0" but it would be too slow
|
||||
// unless we are working with a local file (and the file may still be a valid original TGA without it).
|
||||
return true;
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -31,13 +31,17 @@ package com.twelvemonkeys.imageio.plugins.tga;
|
||||
import com.twelvemonkeys.imageio.AbstractMetadata;
|
||||
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import java.awt.*;
|
||||
import java.awt.image.IndexColorModel;
|
||||
import java.util.Calendar;
|
||||
|
||||
final class TGAMetadata extends AbstractMetadata {
|
||||
private final TGAHeader header;
|
||||
private final TGAExtensions extensions;
|
||||
|
||||
TGAMetadata(final TGAHeader header) {
|
||||
TGAMetadata(final TGAHeader header, final TGAExtensions extensions) {
|
||||
this.header = header;
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,6 +49,8 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
|
||||
|
||||
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
|
||||
chroma.appendChild(csType);
|
||||
|
||||
switch (header.getImageType()) {
|
||||
case TGA.IMAGETYPE_MONOCHROME:
|
||||
case TGA.IMAGETYPE_MONOCHROME_RLE:
|
||||
@@ -62,15 +68,22 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
default:
|
||||
csType.setAttribute("name", "Unknown");
|
||||
}
|
||||
chroma.appendChild(csType);
|
||||
|
||||
// TODO: Channels in chroma node reflects channels in color model (see data node, for channels in data)
|
||||
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
|
||||
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
|
||||
chroma.appendChild(numChannels);
|
||||
switch (header.getPixelDepth()) {
|
||||
case 8:
|
||||
case 16:
|
||||
numChannels.setAttribute("value", Integer.toString(1));
|
||||
break;
|
||||
case 16:
|
||||
if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) {
|
||||
numChannels.setAttribute("value", Integer.toString(4));
|
||||
}
|
||||
else {
|
||||
numChannels.setAttribute("value", Integer.toString(3));
|
||||
}
|
||||
break;
|
||||
case 24:
|
||||
numChannels.setAttribute("value", Integer.toString(3));
|
||||
break;
|
||||
@@ -78,11 +91,10 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
numChannels.setAttribute("value", Integer.toString(4));
|
||||
break;
|
||||
}
|
||||
chroma.appendChild(numChannels);
|
||||
|
||||
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
|
||||
blackIsZero.setAttribute("value", "TRUE");
|
||||
chroma.appendChild(blackIsZero);
|
||||
blackIsZero.setAttribute("value", "TRUE");
|
||||
|
||||
// NOTE: TGA files may contain a color map, even if true color...
|
||||
// Not sure if this is a good idea to expose to the meta data,
|
||||
@@ -94,16 +106,26 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
|
||||
for (int i = 0; i < colorMap.getMapSize(); i++) {
|
||||
IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry");
|
||||
palette.appendChild(paletteEntry);
|
||||
paletteEntry.setAttribute("index", Integer.toString(i));
|
||||
|
||||
paletteEntry.setAttribute("red", Integer.toString(colorMap.getRed(i)));
|
||||
paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(i)));
|
||||
paletteEntry.setAttribute("blue", Integer.toString(colorMap.getBlue(i)));
|
||||
|
||||
palette.appendChild(paletteEntry);
|
||||
}
|
||||
}
|
||||
|
||||
if (extensions != null && extensions.getBackgroundColor() != 0) {
|
||||
Color background = new Color(extensions.getBackgroundColor(), true);
|
||||
|
||||
IIOMetadataNode backgroundColor = new IIOMetadataNode("BackgroundColor");
|
||||
chroma.appendChild(backgroundColor);
|
||||
|
||||
backgroundColor.setAttribute("red", Integer.toString(background.getRed()));
|
||||
backgroundColor.setAttribute("green", Integer.toString(background.getGreen()));
|
||||
backgroundColor.setAttribute("blue", Integer.toString(background.getBlue()));
|
||||
}
|
||||
|
||||
return chroma;
|
||||
}
|
||||
|
||||
@@ -116,15 +138,16 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN:
|
||||
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE:
|
||||
IIOMetadataNode node = new IIOMetadataNode("Compression");
|
||||
|
||||
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
|
||||
node.appendChild(compressionTypeName);
|
||||
String value = header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN || header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE
|
||||
? "Uknown" : "RLE";
|
||||
compressionTypeName.setAttribute("value", value);
|
||||
node.appendChild(compressionTypeName);
|
||||
|
||||
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
|
||||
lossless.setAttribute("value", "TRUE");
|
||||
node.appendChild(lossless);
|
||||
lossless.setAttribute("value", "TRUE");
|
||||
|
||||
return node;
|
||||
default:
|
||||
@@ -138,10 +161,12 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
IIOMetadataNode node = new IIOMetadataNode("Data");
|
||||
|
||||
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
|
||||
planarConfiguration.setAttribute("value", "PixelInterleaved");
|
||||
node.appendChild(planarConfiguration);
|
||||
planarConfiguration.setAttribute("value", "PixelInterleaved");
|
||||
|
||||
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
|
||||
node.appendChild(sampleFormat);
|
||||
|
||||
switch (header.getImageType()) {
|
||||
case TGA.IMAGETYPE_COLORMAPPED:
|
||||
case TGA.IMAGETYPE_COLORMAPPED_RLE:
|
||||
@@ -154,13 +179,19 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
break;
|
||||
}
|
||||
|
||||
node.appendChild(sampleFormat);
|
||||
|
||||
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
|
||||
node.appendChild(bitsPerSample);
|
||||
|
||||
switch (header.getPixelDepth()) {
|
||||
case 8:
|
||||
case 16:
|
||||
bitsPerSample.setAttribute("value", createListValue(1, Integer.toString(header.getPixelDepth())));
|
||||
case 16:
|
||||
if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) {
|
||||
bitsPerSample.setAttribute("value", "5, 5, 5, 1");
|
||||
}
|
||||
else {
|
||||
bitsPerSample.setAttribute("value", createListValue(3, "5"));
|
||||
}
|
||||
break;
|
||||
case 24:
|
||||
bitsPerSample.setAttribute("value", createListValue(3, Integer.toString(8)));
|
||||
@@ -170,12 +201,6 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
break;
|
||||
}
|
||||
|
||||
node.appendChild(bitsPerSample);
|
||||
|
||||
// TODO: Do we need MSB?
|
||||
// IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
|
||||
// sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0"));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -198,6 +223,7 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
|
||||
|
||||
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
|
||||
dimension.appendChild(imageOrientation);
|
||||
|
||||
switch (header.getOrigin()) {
|
||||
case TGA.ORIGIN_LOWER_LEFT:
|
||||
@@ -214,28 +240,64 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
break;
|
||||
}
|
||||
|
||||
dimension.appendChild(imageOrientation);
|
||||
IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio");
|
||||
dimension.appendChild(pixelAspectRatio);
|
||||
pixelAspectRatio.setAttribute("value", extensions != null ? String.valueOf(extensions.getPixelAspectRatio()) : "1.0");
|
||||
|
||||
return dimension;
|
||||
}
|
||||
|
||||
// No document node
|
||||
@Override
|
||||
protected IIOMetadataNode getStandardDocumentNode() {
|
||||
IIOMetadataNode document = new IIOMetadataNode("Document");
|
||||
|
||||
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
|
||||
document.appendChild(formatVersion);
|
||||
formatVersion.setAttribute("value", extensions == null ? "1.0" : "2.0");
|
||||
|
||||
// ImageCreationTime from extensions date
|
||||
if (extensions != null && extensions.getCreationDate() != null) {
|
||||
IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime");
|
||||
document.appendChild(imageCreationTime);
|
||||
|
||||
Calendar date = extensions.getCreationDate();
|
||||
|
||||
imageCreationTime.setAttribute("year", String.valueOf(date.get(Calendar.YEAR)));
|
||||
imageCreationTime.setAttribute("month", String.valueOf(date.get(Calendar.MONTH) + 1));
|
||||
imageCreationTime.setAttribute("day", String.valueOf(date.get(Calendar.DAY_OF_MONTH)));
|
||||
imageCreationTime.setAttribute("hour", String.valueOf(date.get(Calendar.HOUR_OF_DAY)));
|
||||
imageCreationTime.setAttribute("minute", String.valueOf(date.get(Calendar.MINUTE)));
|
||||
imageCreationTime.setAttribute("second", String.valueOf(date.get(Calendar.SECOND)));
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IIOMetadataNode getStandardTextNode() {
|
||||
// TODO: Extra "developer area" and other stuff might go here...
|
||||
IIOMetadataNode text = new IIOMetadataNode("Text");
|
||||
|
||||
// NOTE: Names corresponds to equivalent fields in TIFF
|
||||
if (header.getIdentification() != null && !header.getIdentification().isEmpty()) {
|
||||
IIOMetadataNode text = new IIOMetadataNode("Text");
|
||||
|
||||
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
|
||||
textEntry.setAttribute("keyword", "identification");
|
||||
textEntry.setAttribute("value", header.getIdentification());
|
||||
text.appendChild(textEntry);
|
||||
|
||||
return text;
|
||||
appendTextEntry(text, "DocumentName", header.getIdentification());
|
||||
}
|
||||
|
||||
return null;
|
||||
if (extensions != null) {
|
||||
appendTextEntry(text, "Software", extensions.getSoftwareVersion() == null ? extensions.getSoftware() : extensions.getSoftware() + " " + extensions.getSoftwareVersion());
|
||||
appendTextEntry(text, "Artist", extensions.getAuthorName());
|
||||
appendTextEntry(text, "UserComment", extensions.getAuthorComments());
|
||||
}
|
||||
|
||||
return text.hasChildNodes() ? text : null;
|
||||
}
|
||||
|
||||
private void appendTextEntry(final IIOMetadataNode parent, final String keyword, final String value) {
|
||||
if (value != null) {
|
||||
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
|
||||
parent.appendChild(textEntry);
|
||||
textEntry.setAttribute("keyword", keyword);
|
||||
textEntry.setAttribute("value", value);
|
||||
}
|
||||
}
|
||||
|
||||
// No tiling
|
||||
@@ -245,9 +307,23 @@ final class TGAMetadata extends AbstractMetadata {
|
||||
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
|
||||
|
||||
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
|
||||
alpha.setAttribute("value", header.getPixelDepth() == 32 ? "nonpremultiplied" : "none");
|
||||
transparency.appendChild(alpha);
|
||||
|
||||
if (extensions != null) {
|
||||
if (extensions.hasAlpha()) {
|
||||
alpha.setAttribute("value", extensions.isAlphaPremultiplied() ? "premultiplied" : "nonpremultiplied");
|
||||
}
|
||||
else {
|
||||
alpha.setAttribute("value", "none");
|
||||
}
|
||||
}
|
||||
else if (header.getAttributeBits() == 8) {
|
||||
alpha.setAttribute("value", "nonpremultiplied");
|
||||
}
|
||||
else {
|
||||
alpha.setAttribute("value", "none");
|
||||
}
|
||||
|
||||
return transparency;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -28,24 +28,23 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
/**
|
||||
* CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression.
|
||||
*
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author <a href="https://github.com/Schmidor">Oliver Schmidtmer</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: CCITTFaxDecoderStream.java,v 1.0 23.05.12 15:55 haraldk Exp$
|
||||
*/
|
||||
final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
// See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression",
|
||||
// page 43.
|
||||
// See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression", page 43.
|
||||
|
||||
private final int columns;
|
||||
private final byte[] decodedRow;
|
||||
@@ -62,8 +61,6 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
private int changesReferenceRowCount;
|
||||
private int changesCurrentRowCount;
|
||||
|
||||
private static final int EOL_CODE = 0x01; // 12 bit
|
||||
|
||||
private boolean optionG32D = false;
|
||||
|
||||
@SuppressWarnings("unused") // Leading zeros for aligning EOL
|
||||
@@ -72,29 +69,34 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
private boolean optionUncompressed = false;
|
||||
|
||||
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder,
|
||||
final long options) {
|
||||
final long options) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
|
||||
// We know this is only used for b/w (1 bit)
|
||||
this.decodedRow = new byte[(columns + 7) / 8];
|
||||
this.type = type;
|
||||
this.fillOrder = fillOrder;// Validate.isTrue(fillOrder == 1, fillOrder,
|
||||
// "Only fill order 1 supported: %s"); //
|
||||
// TODO: Implement fillOrder == 2
|
||||
this.type = Validate.isTrue(
|
||||
type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE ||
|
||||
type == TIFFExtension.COMPRESSION_CCITT_T4 || type == TIFFExtension.COMPRESSION_CCITT_T6,
|
||||
type, "Only CCITT Modified Huffman RLE compression (2), CCITT T4 (3) or CCITT T6 (4) supported: %s"
|
||||
);
|
||||
this.fillOrder = Validate.isTrue(
|
||||
fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT || fillOrder == TIFFExtension.FILL_RIGHT_TO_LEFT,
|
||||
fillOrder, "Expected fill order 1 or 2: %s"
|
||||
);
|
||||
|
||||
this.changesReferenceRow = new int[columns];
|
||||
this.changesCurrentRow = new int[columns];
|
||||
|
||||
switch (type) {
|
||||
case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||
optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0;
|
||||
optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0;
|
||||
optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0;
|
||||
break;
|
||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0;
|
||||
break;
|
||||
case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||
optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0;
|
||||
optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0;
|
||||
optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0;
|
||||
break;
|
||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0;
|
||||
break;
|
||||
}
|
||||
|
||||
Validate.isTrue(!optionUncompressed, optionUncompressed,
|
||||
@@ -107,7 +109,8 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
|
||||
try {
|
||||
decodeRow();
|
||||
} catch (EOFException e) {
|
||||
}
|
||||
catch (EOFException e) {
|
||||
// TODO: Rewrite to avoid throw/catch for normal flow...
|
||||
if (decodedLength != 0) {
|
||||
throw e;
|
||||
@@ -126,16 +129,20 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
int index = 0;
|
||||
boolean white = true;
|
||||
changesCurrentRowCount = 0;
|
||||
|
||||
do {
|
||||
int completeRun = 0;
|
||||
int completeRun;
|
||||
|
||||
if (white) {
|
||||
completeRun = decodeRun(whiteRunTree);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
completeRun = decodeRun(blackRunTree);
|
||||
}
|
||||
|
||||
index += completeRun;
|
||||
changesCurrentRow[changesCurrentRowCount++] = index;
|
||||
|
||||
// Flip color for next run
|
||||
white = !white;
|
||||
} while (index < columns);
|
||||
@@ -147,62 +154,79 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
changesCurrentRow = changesReferenceRow;
|
||||
changesReferenceRow = tmp;
|
||||
|
||||
if (changesReferenceRowCount == 0) {
|
||||
changesReferenceRowCount = 3;
|
||||
changesReferenceRow[0] = columns;
|
||||
changesReferenceRow[1] = columns;
|
||||
changesReferenceRow[2] = columns;
|
||||
}
|
||||
|
||||
boolean white = true;
|
||||
int index = 0;
|
||||
changesCurrentRowCount = 0;
|
||||
|
||||
mode: while (index < columns) {
|
||||
// read mode
|
||||
Node n = codeTree.root;
|
||||
|
||||
while (true) {
|
||||
n = n.walk(readBit());
|
||||
|
||||
if (n == null) {
|
||||
continue mode;
|
||||
} else if (n.isLeaf) {
|
||||
|
||||
}
|
||||
else if (n.isLeaf) {
|
||||
switch (n.value) {
|
||||
case VALUE_HMODE:
|
||||
int runLength = 0;
|
||||
runLength = decodeRun(white ? whiteRunTree : blackRunTree);
|
||||
index += runLength;
|
||||
changesCurrentRow[changesCurrentRowCount++] = index;
|
||||
case VALUE_HMODE:
|
||||
int runLength;
|
||||
runLength = decodeRun(white ? whiteRunTree : blackRunTree);
|
||||
index += runLength;
|
||||
changesCurrentRow[changesCurrentRowCount++] = index;
|
||||
|
||||
runLength = decodeRun(white ? blackRunTree : whiteRunTree);
|
||||
index += runLength;
|
||||
changesCurrentRow[changesCurrentRowCount++] = index;
|
||||
break;
|
||||
case VALUE_PASSMODE:
|
||||
index = changesReferenceRow[getNextChangingElement(index, white) + 1];
|
||||
break;
|
||||
default:
|
||||
// Vertical mode (-3 to 3)
|
||||
index = changesReferenceRow[getNextChangingElement(index, white)] + n.value;
|
||||
changesCurrentRow[changesCurrentRowCount] = index;
|
||||
changesCurrentRowCount++;
|
||||
white = !white;
|
||||
break;
|
||||
runLength = decodeRun(white ? blackRunTree : whiteRunTree);
|
||||
index += runLength;
|
||||
changesCurrentRow[changesCurrentRowCount++] = index;
|
||||
break;
|
||||
|
||||
case VALUE_PASSMODE:
|
||||
int pChangingElement = getNextChangingElement(index, white) + 1;
|
||||
|
||||
if (pChangingElement >= changesReferenceRowCount || pChangingElement == -1) {
|
||||
index = columns;
|
||||
}
|
||||
else {
|
||||
index = changesReferenceRow[pChangingElement];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Vertical mode (-3 to 3)
|
||||
int vChangingElement = getNextChangingElement(index, white);
|
||||
|
||||
if (vChangingElement >= changesReferenceRowCount || vChangingElement == -1) {
|
||||
index = columns + n.value;
|
||||
}
|
||||
else {
|
||||
index = changesReferenceRow[vChangingElement] + n.value;
|
||||
}
|
||||
|
||||
changesCurrentRow[changesCurrentRowCount] = index;
|
||||
changesCurrentRowCount++;
|
||||
white = !white;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
continue mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getNextChangingElement(int a0, boolean white) {
|
||||
private int getNextChangingElement(final int a0, final boolean white) {
|
||||
int start = white ? 0 : 1;
|
||||
|
||||
for (int i = start; i < changesReferenceRowCount; i += 2) {
|
||||
if (a0 < changesReferenceRow[i]) {
|
||||
if (a0 < changesReferenceRow[i] || (a0 == 0 && changesReferenceRow[i] == 0)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void decodeRowType2() throws IOException {
|
||||
@@ -214,20 +238,24 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
eof: while (true) {
|
||||
// read till next EOL code
|
||||
Node n = eolOnlyTree.root;
|
||||
|
||||
while (true) {
|
||||
Node tmp = n;
|
||||
n = n.walk(readBit());
|
||||
if (n == null)
|
||||
|
||||
if (n == null) {
|
||||
continue eof;
|
||||
}
|
||||
|
||||
if (n.isLeaf) {
|
||||
break eof;
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean k = optionG32D ? readBit() : true;
|
||||
if (k) {
|
||||
|
||||
if (!optionG32D || readBit()) {
|
||||
decode1D();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
decode2D();
|
||||
}
|
||||
}
|
||||
@@ -238,23 +266,28 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
|
||||
private void decodeRow() throws IOException {
|
||||
switch (type) {
|
||||
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
|
||||
decodeRowType2();
|
||||
break;
|
||||
case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||
decodeRowType4();
|
||||
break;
|
||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
decodeRowType6();
|
||||
break;
|
||||
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
|
||||
decodeRowType2();
|
||||
break;
|
||||
case TIFFExtension.COMPRESSION_CCITT_T4:
|
||||
decodeRowType4();
|
||||
break;
|
||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
decodeRowType6();
|
||||
break;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
boolean white = true;
|
||||
|
||||
|
||||
for (int i = 0; i <= changesCurrentRowCount; i++) {
|
||||
int nextChange = columns;
|
||||
|
||||
if (i != changesCurrentRowCount) {
|
||||
nextChange = changesCurrentRow[i];
|
||||
}
|
||||
|
||||
if (nextChange > columns) {
|
||||
nextChange = columns;
|
||||
}
|
||||
@@ -281,13 +314,14 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
if (index % 8 == 0) {
|
||||
decodedRow[byteIndex] = 0;
|
||||
}
|
||||
|
||||
decodedRow[byteIndex] |= (white ? 0 : 1 << (7 - ((index) % 8)));
|
||||
index++;
|
||||
}
|
||||
|
||||
white = !white;
|
||||
}
|
||||
|
||||
|
||||
if (index != columns) {
|
||||
throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns);
|
||||
}
|
||||
@@ -295,43 +329,42 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
decodedLength = (index + 7) / 8;
|
||||
}
|
||||
|
||||
private int decodeRun(Tree tree) throws IOException {
|
||||
private int decodeRun(final Tree tree) throws IOException {
|
||||
int total = 0;
|
||||
|
||||
Node n = tree.root;
|
||||
|
||||
while (true) {
|
||||
boolean bit = readBit();
|
||||
n = n.walk(bit);
|
||||
if (n == null)
|
||||
|
||||
if (n == null) {
|
||||
throw new IOException("Unknown code in Huffman RLE stream");
|
||||
}
|
||||
|
||||
if (n.isLeaf) {
|
||||
total += n.value;
|
||||
if (n.value < 64) {
|
||||
return total;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
n = tree.root;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetBuffer() {
|
||||
private void resetBuffer() throws IOException {
|
||||
for (int i = 0; i < decodedRow.length; i++) {
|
||||
decodedRow[i] = 0;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (bufferPos == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean skip = readBit();
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
readBit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,22 +374,29 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
private boolean readBit() throws IOException {
|
||||
if (bufferPos < 0 || bufferPos > 7) {
|
||||
buffer = in.read();
|
||||
|
||||
if (buffer == -1) {
|
||||
throw new EOFException("Unexpected end of Huffman RLE stream");
|
||||
}
|
||||
|
||||
bufferPos = 0;
|
||||
}
|
||||
|
||||
boolean isSet;
|
||||
|
||||
if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) {
|
||||
isSet = ((buffer >> (7 - bufferPos)) & 1) == 1;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
isSet = ((buffer >> (bufferPos)) & 1) == 1;
|
||||
}
|
||||
|
||||
bufferPos++;
|
||||
if (bufferPos > 7)
|
||||
|
||||
if (bufferPos > 7) {
|
||||
bufferPos = -1;
|
||||
}
|
||||
|
||||
return isSet;
|
||||
}
|
||||
|
||||
@@ -428,23 +468,25 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
throw new IOException("mark/reset not supported");
|
||||
}
|
||||
|
||||
static class Node {
|
||||
private static final class Node {
|
||||
Node left;
|
||||
Node right;
|
||||
|
||||
int value; // > 63 non term.
|
||||
|
||||
boolean canBeFill = false;
|
||||
boolean isLeaf = false;
|
||||
|
||||
void set(boolean next, Node node) {
|
||||
void set(final boolean next, final Node node) {
|
||||
if (!next) {
|
||||
left = node;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
right = node;
|
||||
}
|
||||
}
|
||||
|
||||
Node walk(boolean next) {
|
||||
Node walk(final boolean next) {
|
||||
return next ? right : left;
|
||||
}
|
||||
|
||||
@@ -454,51 +496,69 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
static class Tree {
|
||||
Node root = new Node();
|
||||
private static final class Tree {
|
||||
final Node root = new Node();
|
||||
|
||||
void fill(int depth, int path, int value) throws IOException {
|
||||
void fill(final int depth, final int path, final int value) throws IOException {
|
||||
Node current = root;
|
||||
|
||||
for (int i = 0; i < depth; i++) {
|
||||
int bitPos = depth - 1 - i;
|
||||
boolean isSet = ((path >> bitPos) & 1) == 1;
|
||||
Node next = current.walk(isSet);
|
||||
|
||||
if (next == null) {
|
||||
next = new Node();
|
||||
|
||||
if (i == depth - 1) {
|
||||
next.value = value;
|
||||
next.isLeaf = true;
|
||||
}
|
||||
if (path == 0)
|
||||
|
||||
if (path == 0) {
|
||||
next.canBeFill = true;
|
||||
}
|
||||
|
||||
current.set(isSet, next);
|
||||
} else {
|
||||
if (next.isLeaf)
|
||||
throw new IOException("node is leaf, no other following");
|
||||
}
|
||||
else {
|
||||
if (next.isLeaf) {
|
||||
throw new IOException("node is leaf, no other following");
|
||||
}
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
void fill(int depth, int path, Node node) throws IOException {
|
||||
void fill(final int depth, final int path, final Node node) throws IOException {
|
||||
Node current = root;
|
||||
|
||||
for (int i = 0; i < depth; i++) {
|
||||
int bitPos = depth - 1 - i;
|
||||
boolean isSet = ((path >> bitPos) & 1) == 1;
|
||||
Node next = current.walk(isSet);
|
||||
|
||||
if (next == null) {
|
||||
if (i == depth - 1) {
|
||||
next = node;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
next = new Node();
|
||||
}
|
||||
if (path == 0)
|
||||
|
||||
if (path == 0) {
|
||||
next.canBeFill = true;
|
||||
}
|
||||
|
||||
current.set(isSet, next);
|
||||
} else {
|
||||
if (next.isLeaf)
|
||||
throw new IOException("node is leaf, no other following");
|
||||
}
|
||||
else {
|
||||
if (next.isLeaf) {
|
||||
throw new IOException("node is leaf, no other following");
|
||||
}
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
@@ -506,105 +566,148 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
|
||||
static final short[][] BLACK_CODES = {
|
||||
{ // 2 bits
|
||||
0x2, 0x3, },
|
||||
0x2, 0x3,
|
||||
},
|
||||
{ // 3 bits
|
||||
0x2, 0x3, },
|
||||
0x2, 0x3,
|
||||
},
|
||||
{ // 4 bits
|
||||
0x2, 0x3, },
|
||||
0x2, 0x3,
|
||||
},
|
||||
{ // 5 bits
|
||||
0x3, },
|
||||
0x3,
|
||||
},
|
||||
{ // 6 bits
|
||||
0x4, 0x5, },
|
||||
0x4, 0x5,
|
||||
},
|
||||
{ // 7 bits
|
||||
0x4, 0x5, 0x7, },
|
||||
0x4, 0x5, 0x7,
|
||||
},
|
||||
{ // 8 bits
|
||||
0x4, 0x7, },
|
||||
0x4, 0x7,
|
||||
},
|
||||
{ // 9 bits
|
||||
0x18, },
|
||||
0x18,
|
||||
},
|
||||
{ // 10 bits
|
||||
0x17, 0x18, 0x37, 0x8, 0xf, },
|
||||
0x17, 0x18, 0x37, 0x8, 0xf,
|
||||
},
|
||||
{ // 11 bits
|
||||
0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd, },
|
||||
0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd,
|
||||
},
|
||||
{ // 12 bits
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33,
|
||||
0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65,
|
||||
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3,
|
||||
0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb, },
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33,
|
||||
0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65,
|
||||
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3,
|
||||
0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb,
|
||||
},
|
||||
{ // 13 bits
|
||||
0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73,
|
||||
0x74, 0x75, 0x76, 0x77, } };
|
||||
0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73,
|
||||
0x74, 0x75, 0x76, 0x77,
|
||||
}
|
||||
};
|
||||
static final short[][] BLACK_RUN_LENGTHS = {
|
||||
{ // 2 bits
|
||||
3, 2, },
|
||||
3, 2,
|
||||
},
|
||||
{ // 3 bits
|
||||
1, 4, },
|
||||
1, 4,
|
||||
},
|
||||
{ // 4 bits
|
||||
6, 5, },
|
||||
6, 5,
|
||||
},
|
||||
{ // 5 bits
|
||||
7, },
|
||||
7,
|
||||
},
|
||||
{ // 6 bits
|
||||
9, 8, },
|
||||
9, 8,
|
||||
},
|
||||
{ // 7 bits
|
||||
10, 11, 12, },
|
||||
10, 11, 12,
|
||||
},
|
||||
{ // 8 bits
|
||||
13, 14, },
|
||||
13, 14,
|
||||
},
|
||||
{ // 9 bits
|
||||
15, },
|
||||
15,
|
||||
},
|
||||
{ // 10 bits
|
||||
16, 17, 0, 18, 64, },
|
||||
16, 17, 0, 18, 64,
|
||||
},
|
||||
{ // 11 bits
|
||||
24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920, },
|
||||
24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920,
|
||||
},
|
||||
{ // 12 bits
|
||||
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320, 384, 448, 53,
|
||||
54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49, 62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26,
|
||||
27, 28, 29, 34, 35, 36, 37, 38, 39, 42, 43, },
|
||||
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320, 384, 448, 53,
|
||||
54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49, 62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26,
|
||||
27, 28, 29, 34, 35, 36, 37, 38, 39, 42, 43,
|
||||
},
|
||||
{ // 13 bits
|
||||
640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, 1024, 1088,
|
||||
1152, 1216, } };
|
||||
640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, 1024, 1088,
|
||||
1152, 1216,
|
||||
}
|
||||
};
|
||||
|
||||
public static final short[][] WHITE_CODES = {
|
||||
{ // 4 bits
|
||||
0x7, 0x8, 0xb, 0xc, 0xe, 0xf, },
|
||||
0x7, 0x8, 0xb, 0xc, 0xe, 0xf,
|
||||
},
|
||||
{ // 5 bits
|
||||
0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, },
|
||||
0x12, 0x13, 0x14, 0x1b, 0x7, 0x8,
|
||||
},
|
||||
{ // 6 bits
|
||||
0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8, },
|
||||
0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8,
|
||||
},
|
||||
{ // 7 bits
|
||||
0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc, },
|
||||
0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc,
|
||||
},
|
||||
{ // 8 bits
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d,
|
||||
0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, 0x58, 0x59,
|
||||
0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb, },
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d,
|
||||
0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, 0x58, 0x59,
|
||||
0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb,
|
||||
},
|
||||
{ // 9 bits
|
||||
0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, },
|
||||
0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb,
|
||||
},
|
||||
{ // 10 bits
|
||||
},
|
||||
{ // 11 bits
|
||||
0x8, 0xc, 0xd, },
|
||||
0x8, 0xc, 0xd,
|
||||
},
|
||||
{ // 12 bits
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, } };
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
}
|
||||
};
|
||||
|
||||
public static final short[][] WHITE_RUN_LENGTHS = {
|
||||
{ // 4 bits
|
||||
2, 3, 4, 5, 6, 7, },
|
||||
2, 3, 4, 5, 6, 7,
|
||||
},
|
||||
{ // 5 bits
|
||||
128, 8, 9, 64, 10, 11, },
|
||||
128, 8, 9, 64, 10, 11,
|
||||
},
|
||||
{ // 6 bits
|
||||
192, 1664, 16, 17, 13, 14, 15, 1, 12, },
|
||||
192, 1664, 16, 17, 13, 14, 15, 1, 12,
|
||||
},
|
||||
{ // 7 bits
|
||||
26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19, },
|
||||
26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19,
|
||||
},
|
||||
{ // 8 bits
|
||||
33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, 44, 30, 61, 62, 63, 0, 320, 384, 45,
|
||||
59, 60, 46, 49, 50, 51, 52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48, },
|
||||
{ // 9
|
||||
// bits
|
||||
1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, },
|
||||
33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, 44, 30, 61, 62, 63, 0, 320, 384, 45,
|
||||
59, 60, 46, 49, 50, 51, 52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48,
|
||||
},
|
||||
{ // 9 bits
|
||||
1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408,
|
||||
},
|
||||
{ // 10 bits
|
||||
},
|
||||
{ // 11 bits
|
||||
1792, 1856, 1920, },
|
||||
1792, 1856, 1920,
|
||||
},
|
||||
{ // 12 bits
|
||||
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, } };
|
||||
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560,
|
||||
}
|
||||
};
|
||||
|
||||
final static Node EOL;
|
||||
final static Node FILL;
|
||||
@@ -631,8 +734,9 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
try {
|
||||
eolOnlyTree.fill(12, 0, FILL);
|
||||
eolOnlyTree.fill(12, 1, EOL);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
blackRunTree = new Tree();
|
||||
@@ -644,9 +748,11 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
}
|
||||
blackRunTree.fill(12, 0, FILL);
|
||||
blackRunTree.fill(12, 1, EOL);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
whiteRunTree = new Tree();
|
||||
try {
|
||||
for (int i = 0; i < WHITE_CODES.length; i++) {
|
||||
@@ -654,10 +760,12 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
whiteRunTree.fill(i + 4, WHITE_CODES[i][j], WHITE_RUN_LENGTHS[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
whiteRunTree.fill(12, 0, FILL);
|
||||
whiteRunTree.fill(12, 1, EOL);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
codeTree = new Tree();
|
||||
@@ -671,8 +779,9 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
|
||||
codeTree.fill(3, 2, -1); // V_L(1)
|
||||
codeTree.fill(6, 2, -2); // V_L(2)
|
||||
codeTree.fill(7, 2, -3); // V_L(3)
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,6 @@ abstract class LZWDecoder implements Decoder {
|
||||
|
||||
public static Decoder create(boolean oldBitReversedStream) {
|
||||
return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder();
|
||||
// return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWTreeDecoder();
|
||||
}
|
||||
|
||||
static final class LZWSpecDecoder extends LZWDecoder {
|
||||
|
||||
@@ -62,6 +62,8 @@ interface TIFFExtension {
|
||||
int PREDICTOR_HORIZONTAL_DIFFERENCING = 2;
|
||||
int PREDICTOR_HORIZONTAL_FLOATINGPOINT = 3;
|
||||
|
||||
int FILL_RIGHT_TO_LEFT = 2;
|
||||
|
||||
int SAMPLEFORMAT_INT = 2;
|
||||
int SAMPLEFORMAT_FP = 3;
|
||||
int SAMPLEFORMAT_UNDEFINED = 4;
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.imageio.AbstractMetadata;
|
||||
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.Rational;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import java.lang.reflect.Array;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* TIFFImageMetadata.
|
||||
@@ -21,18 +27,46 @@ import java.util.Calendar;
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: TIFFImageMetadata.java,v 1.0 17/04/15 harald.kuhr Exp$
|
||||
*/
|
||||
final class TIFFImageMetadata extends AbstractMetadata {
|
||||
public final class TIFFImageMetadata extends AbstractMetadata {
|
||||
|
||||
private final Directory ifd;
|
||||
static final int RATIONAL_SCALE_FACTOR = 100000;
|
||||
|
||||
TIFFImageMetadata(final Directory ifd) {
|
||||
super(true, TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, TIFFMedataFormat.class.getName(), null, null);
|
||||
this.ifd = Validate.notNull(ifd, "IFD");
|
||||
private final Directory original;
|
||||
private Directory ifd;
|
||||
|
||||
/**
|
||||
* Creates an empty TIFF metadata object.
|
||||
*
|
||||
* Client code can update or change the metadata using the
|
||||
* {@link #setFromTree(String, Node)}
|
||||
* or {@link #mergeTree(String, Node)} methods.
|
||||
*/
|
||||
public TIFFImageMetadata() {
|
||||
this(new TIFFIFD(Collections.<Entry>emptyList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return false;
|
||||
/**
|
||||
* Creates a TIFF metadata object, using the values from the given IFD.
|
||||
*
|
||||
* Client code can update or change the metadata using the
|
||||
* {@link #setFromTree(String, Node)}
|
||||
* or {@link #mergeTree(String, Node)} methods.
|
||||
*/
|
||||
public TIFFImageMetadata(final Directory ifd) {
|
||||
super(true, TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, TIFFMedataFormat.class.getName(), null, null);
|
||||
this.ifd = Validate.notNull(ifd, "IFD");
|
||||
this.original = ifd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TIFF metadata object, using the values from the given entries.
|
||||
*
|
||||
* Client code can update or change the metadata using the
|
||||
* {@link #setFromTree(String, Node)}
|
||||
* or {@link #mergeTree(String, Node)} methods.
|
||||
*/
|
||||
public TIFFImageMetadata(final Collection<Entry> entries) {
|
||||
this(new TIFFIFD(entries));
|
||||
}
|
||||
|
||||
protected IIOMetadataNode getNativeTree() {
|
||||
@@ -99,7 +133,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
IIOMetadataNode elementNode = new IIOMetadataNode(typeName);
|
||||
valueNode.appendChild(elementNode);
|
||||
|
||||
setValue(value, unsigned, elementNode);
|
||||
setTIFFNativeValue(value, unsigned, elementNode);
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
@@ -107,7 +141,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
IIOMetadataNode elementNode = new IIOMetadataNode(typeName);
|
||||
valueNode.appendChild(elementNode);
|
||||
|
||||
setValue(val, unsigned, elementNode);
|
||||
setTIFFNativeValue(val, unsigned, elementNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +153,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
return ifdNode;
|
||||
}
|
||||
|
||||
private void setValue(final Object value, final boolean unsigned, final IIOMetadataNode elementNode) {
|
||||
private void setTIFFNativeValue(final Object value, final boolean unsigned, final IIOMetadataNode elementNode) {
|
||||
if (unsigned && value instanceof Byte) {
|
||||
elementNode.setAttribute("value", String.valueOf((Byte) value & 0xFF));
|
||||
}
|
||||
@@ -289,12 +323,12 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
|
||||
// Handle ColorSpaceType (RGB/CMYK/YCbCr etc)...
|
||||
Entry photometricTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
|
||||
int photometricValue = ((Number) photometricTag.getValue()).intValue(); // No default for this tag!
|
||||
int photometricValue = getValueAsInt(photometricTag); // No default for this tag!
|
||||
|
||||
Entry samplesPerPixelTag = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
|
||||
Entry bitsPerSampleTag = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
|
||||
int numChannelsValue = samplesPerPixelTag != null
|
||||
? ((Number) samplesPerPixelTag.getValue()).intValue()
|
||||
? getValueAsInt(samplesPerPixelTag)
|
||||
: bitsPerSampleTag.valueCount();
|
||||
|
||||
IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType");
|
||||
@@ -393,7 +427,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
Entry compressionTag = ifd.getEntryById(TIFF.TAG_COMPRESSION);
|
||||
int compressionValue = compressionTag == null
|
||||
? TIFFBaseline.COMPRESSION_NONE
|
||||
: ((Number) compressionTag.getValue()).intValue();
|
||||
: getValueAsInt(compressionTag);
|
||||
|
||||
// Naming is identical to JAI ImageIO metadata as far as possible
|
||||
switch (compressionValue) {
|
||||
@@ -502,7 +536,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
Entry planarConfigurationTag = ifd.getEntryById(TIFF.TAG_PLANAR_CONFIGURATION);
|
||||
int planarConfigurationValue = planarConfigurationTag == null
|
||||
? TIFFBaseline.PLANARCONFIG_CHUNKY
|
||||
: ((Number) planarConfigurationTag.getValue()).intValue();
|
||||
: getValueAsInt(planarConfigurationTag);
|
||||
|
||||
switch (planarConfigurationValue) {
|
||||
case TIFFBaseline.PLANARCONFIG_CHUNKY:
|
||||
@@ -519,14 +553,16 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
Entry photometricInterpretationTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
|
||||
int photometricInterpretationValue = photometricInterpretationTag == null
|
||||
? TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO
|
||||
: ((Number) photometricInterpretationTag.getValue()).intValue();
|
||||
: getValueAsInt(photometricInterpretationTag);
|
||||
|
||||
Entry samleFormatTag = ifd.getEntryById(TIFF.TAG_SAMPLE_FORMAT);
|
||||
// TODO: Fix for sampleformat 1 1 1 (as int[]) ??!?!?
|
||||
int sampleFormatValue = samleFormatTag == null
|
||||
? TIFFBaseline.SAMPLEFORMAT_UINT
|
||||
: ((Number) samleFormatTag.getValue()).intValue();
|
||||
: getValueAsInt(samleFormatTag);
|
||||
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
|
||||
node.appendChild(sampleFormat);
|
||||
|
||||
switch (sampleFormatValue) {
|
||||
case TIFFBaseline.SAMPLEFORMAT_UINT:
|
||||
if (photometricInterpretationValue == TIFFBaseline.PHOTOMETRIC_PALETTE) {
|
||||
@@ -562,13 +598,13 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
|
||||
Entry samplesPerPixelTag = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
|
||||
int numChannelsValue = samplesPerPixelTag != null
|
||||
? ((Number) samplesPerPixelTag.getValue()).intValue()
|
||||
? getValueAsInt(samplesPerPixelTag)
|
||||
: bitsPerSampleTag.valueCount();
|
||||
|
||||
// SampleMSB
|
||||
Entry fillOrderTag = ifd.getEntryById(TIFF.TAG_FILL_ORDER);
|
||||
int fillOrder = fillOrderTag != null
|
||||
? ((Number) fillOrderTag.getValue()).intValue()
|
||||
? getValueAsInt(fillOrderTag)
|
||||
: TIFFBaseline.FILL_LEFT_TO_RIGHT;
|
||||
IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
|
||||
node.appendChild(sampleMSB);
|
||||
@@ -588,6 +624,22 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
return node;
|
||||
}
|
||||
|
||||
private static int getValueAsInt(final Entry entry) {
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
else if (value instanceof short[]) {
|
||||
return ((short[]) value)[0];
|
||||
}
|
||||
else if (value instanceof int[]) {
|
||||
return ((int[]) value)[0];
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported type: " + entry);
|
||||
}
|
||||
|
||||
// TODO: Candidate superclass method!
|
||||
private String createListValue(final int itemCount, final String... values) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
@@ -620,7 +672,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
// ImageOrientation
|
||||
Entry orientationTag = ifd.getEntryById(TIFF.TAG_ORIENTATION);
|
||||
if (orientationTag != null) {
|
||||
int orientationValue = ((Number) orientationTag.getValue()).intValue();
|
||||
int orientationValue = getValueAsInt(orientationTag);
|
||||
|
||||
String value = null;
|
||||
switch (orientationValue) {
|
||||
@@ -659,7 +711,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
}
|
||||
|
||||
Entry resUnitTag = ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT);
|
||||
int resUnitValue = resUnitTag == null ? TIFFBaseline.RESOLUTION_UNIT_DPI : ((Number) resUnitTag.getValue()).intValue();
|
||||
int resUnitValue = resUnitTag == null ? TIFFBaseline.RESOLUTION_UNIT_DPI : getValueAsInt(resUnitTag);
|
||||
if (resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER || resUnitValue == TIFFBaseline.RESOLUTION_UNIT_DPI) {
|
||||
// 10 mm in 1 cm or 25.4 mm in 1 inch
|
||||
double scale = resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER ? 10 : 25.4;
|
||||
@@ -703,7 +755,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
|
||||
if (extraSamplesTag != null) {
|
||||
int extraSamplesValue = (extraSamplesTag.getValue() instanceof Number)
|
||||
? ((Number) extraSamplesTag.getValue()).intValue()
|
||||
? getValueAsInt(extraSamplesTag)
|
||||
: ((Number) Array.get(extraSamplesTag.getValue(), 0)).intValue();
|
||||
|
||||
// Other values exists, these are not alpha
|
||||
@@ -739,7 +791,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
if (subFileTypeTag != null) {
|
||||
// NOTE: The JAI metadata is somewhat broken here, as these are bit flags, not values...
|
||||
String value = null;
|
||||
int subFileTypeValue = ((Number) subFileTypeTag.getValue()).intValue();
|
||||
int subFileTypeValue = getValueAsInt(subFileTypeTag);
|
||||
if ((subFileTypeValue & TIFFBaseline.FILETYPE_MASK) != 0) {
|
||||
value = "TransparencyMask";
|
||||
}
|
||||
@@ -795,6 +847,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
addTextEntryIfPresent(text, TIFF.TAG_IMAGE_DESCRIPTION);
|
||||
addTextEntryIfPresent(text, TIFF.TAG_MAKE);
|
||||
addTextEntryIfPresent(text, TIFF.TAG_MODEL);
|
||||
addTextEntryIfPresent(text, TIFF.TAG_PAGE_NAME);
|
||||
addTextEntryIfPresent(text, TIFF.TAG_SOFTWARE);
|
||||
addTextEntryIfPresent(text, TIFF.TAG_ARTIST);
|
||||
addTextEntryIfPresent(text, TIFF.TAG_HOST_COMPUTER);
|
||||
@@ -821,4 +874,435 @@ final class TIFFImageMetadata extends AbstractMetadata {
|
||||
// See http://stackoverflow.com/questions/30910719/javax-imageio-1-0-standard-plug-in-neutral-metadata-format-tiling-information
|
||||
return super.getStandardTileNode();
|
||||
}
|
||||
|
||||
/// Mutation
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setFromTree(final String formatName, final Node root) throws IIOInvalidTreeException {
|
||||
// Standard validation
|
||||
super.mergeTree(formatName, root);
|
||||
|
||||
// Set by "merging" with empty map
|
||||
LinkedHashMap<Integer, Entry> entries = new LinkedHashMap<>();
|
||||
mergeEntries(formatName, root, entries);
|
||||
|
||||
// TODO: Consistency validation?
|
||||
|
||||
// Finally create a new IFD from merged values
|
||||
ifd = new TIFFIFD(entries.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mergeTree(final String formatName, final Node root) throws IIOInvalidTreeException {
|
||||
// Standard validation
|
||||
super.mergeTree(formatName, root);
|
||||
|
||||
// Clone entries (shallow clone, as entries themselves are immutable)
|
||||
LinkedHashMap<Integer, Entry> entries = new LinkedHashMap<>(ifd.size() + 10);
|
||||
|
||||
for (Entry entry : ifd) {
|
||||
entries.put((Integer) entry.getIdentifier(), entry);
|
||||
}
|
||||
|
||||
mergeEntries(formatName, root, entries);
|
||||
|
||||
// TODO: Consistency validation?
|
||||
|
||||
// Finally create a new IFD from merged values
|
||||
ifd = new TIFFIFD(entries.values());
|
||||
}
|
||||
|
||||
private void mergeEntries(final String formatName, final Node root, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
|
||||
// Merge from both native and standard trees
|
||||
if (getNativeMetadataFormatName().equals(formatName)) {
|
||||
mergeNativeTree(root, entries);
|
||||
}
|
||||
else if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) {
|
||||
mergeStandardTree(root, entries);
|
||||
}
|
||||
else {
|
||||
// Should already be checked for
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeStandardTree(final Node root, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
|
||||
NodeList nodes = root.getChildNodes();
|
||||
|
||||
// Merge selected values from standard tree
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Node node = nodes.item(i);
|
||||
|
||||
if ("Dimension".equals(node.getNodeName())) {
|
||||
mergeFromStandardDimensionNode(node, entries);
|
||||
}
|
||||
else if ("Document".equals(node.getNodeName())) {
|
||||
mergeFromStandardDocumentNode(node, entries);
|
||||
}
|
||||
else if ("Text".equals(node.getNodeName())) {
|
||||
mergeFromStandardTextNode(node, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeFromStandardDimensionNode(final Node dimensionNode, final Map<Integer, Entry> entries) {
|
||||
// Dimension: xRes/yRes
|
||||
// - If set, set res unit to pixels per cm as this better reflects values?
|
||||
// - Or, convert to DPI, if we already had values in DPI??
|
||||
// Also, if we have only aspect, set these values, and use unknown as unit?
|
||||
// TODO: ImageOrientation => Orientation
|
||||
NodeList children = dimensionNode.getChildNodes();
|
||||
|
||||
Float aspect = null;
|
||||
Float xRes = null;
|
||||
Float yRes = null;
|
||||
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
String nodeName = child.getNodeName();
|
||||
|
||||
if ("PixelAspectRatio".equals(nodeName)) {
|
||||
aspect = Float.parseFloat(getAttribute(child, "value"));
|
||||
}
|
||||
else if ("HorizontalPixelSize".equals(nodeName)) {
|
||||
xRes = Float.parseFloat(getAttribute(child, "value"));
|
||||
}
|
||||
else if ("VerticalPixelSize".equals(nodeName)) {
|
||||
yRes = Float.parseFloat(getAttribute(child, "value"));
|
||||
}
|
||||
}
|
||||
|
||||
// If we have one size compute the other
|
||||
if (xRes == null && yRes != null) {
|
||||
xRes = yRes * (aspect != null ? aspect : 1f);
|
||||
}
|
||||
else if (yRes == null && xRes != null) {
|
||||
yRes = xRes / (aspect != null ? aspect : 1f);
|
||||
}
|
||||
|
||||
// If we have resolution
|
||||
if (xRes != null && yRes != null) {
|
||||
// If old unit was DPI, convert values and keep DPI, otherwise use PPCM
|
||||
Entry resUnitEntry = entries.get(TIFF.TAG_RESOLUTION_UNIT);
|
||||
int resUnitValue = resUnitEntry != null && resUnitEntry.getValue() != null
|
||||
&& ((Number) resUnitEntry.getValue()).intValue() == TIFFBaseline.RESOLUTION_UNIT_DPI
|
||||
? TIFFBaseline.RESOLUTION_UNIT_DPI
|
||||
: TIFFBaseline.RESOLUTION_UNIT_CENTIMETER;
|
||||
|
||||
// Units from standard format are pixels per mm, convert to cm or inches
|
||||
float scale = resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER ? 10 : 25.4f;
|
||||
|
||||
int x = Math.round(xRes * scale * RATIONAL_SCALE_FACTOR);
|
||||
int y = Math.round(yRes * scale * RATIONAL_SCALE_FACTOR);
|
||||
|
||||
entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(x, RATIONAL_SCALE_FACTOR)));
|
||||
entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(y, RATIONAL_SCALE_FACTOR)));
|
||||
entries.put(TIFF.TAG_RESOLUTION_UNIT,
|
||||
new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resUnitValue));
|
||||
}
|
||||
else if (aspect != null) {
|
||||
if (aspect >= 1) {
|
||||
int v = Math.round(aspect * RATIONAL_SCALE_FACTOR);
|
||||
entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR)));
|
||||
entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(1)));
|
||||
}
|
||||
else {
|
||||
int v = Math.round(RATIONAL_SCALE_FACTOR / aspect);
|
||||
entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(1)));
|
||||
entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR)));
|
||||
}
|
||||
|
||||
entries.put(TIFF.TAG_RESOLUTION_UNIT,
|
||||
new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_NONE));
|
||||
}
|
||||
// Else give up...
|
||||
}
|
||||
|
||||
private void mergeFromStandardDocumentNode(final Node documentNode, final Map<Integer, Entry> entries) {
|
||||
// Document: SubfileType, CreationDate
|
||||
NodeList children = documentNode.getChildNodes();
|
||||
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
String nodeName = child.getNodeName();
|
||||
|
||||
if ("SubimageInterpretation".equals(nodeName)) {
|
||||
// TODO: SubFileType
|
||||
}
|
||||
else if ("ImageCreationTime".equals(nodeName)) {
|
||||
// TODO: CreationDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeFromStandardTextNode(final Node textNode, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
|
||||
NodeList textEntries = textNode.getChildNodes();
|
||||
|
||||
for (int i = 0; i < textEntries.getLength(); i++) {
|
||||
Node textEntry = textEntries.item(i);
|
||||
|
||||
if (!"TextEntry".equals(textEntry.getNodeName())) {
|
||||
throw new IIOInvalidTreeException("Text node should only contain TextEntry nodes", textNode);
|
||||
}
|
||||
|
||||
String keyword = getAttribute(textEntry, "keyword");
|
||||
String value = getAttribute(textEntry, "value");
|
||||
|
||||
// DocumentName, ImageDescription, Make, Model, PageName,
|
||||
// Software, Artist, HostComputer, InkNames, Copyright
|
||||
if (value != null && !value.isEmpty() && keyword != null) {
|
||||
// We do all comparisons in lower case, for compatibility
|
||||
keyword = keyword.toLowerCase();
|
||||
|
||||
TIFFImageWriter.TIFFEntry entry;
|
||||
|
||||
if ("documentname".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_DOCUMENT_NAME, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("imagedescription".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_IMAGE_DESCRIPTION, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("make".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MAKE, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("model".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MODEL, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("pagename".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_PAGE_NAME, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("software".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("artist".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("hostcomputer".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_HOST_COMPUTER, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("inknames".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_INK_NAMES, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else if ("copyright".equals(keyword)) {
|
||||
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_COPYRIGHT, TIFF.TYPE_ASCII, value);
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.put((Integer) entry.getIdentifier(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeNativeTree(final Node root, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
|
||||
Directory ifd = toIFD(root.getFirstChild());
|
||||
|
||||
// Merge (overwrite) entries with entries from IFD
|
||||
for (Entry entry : ifd) {
|
||||
entries.put((Integer) entry.getIdentifier(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
private Directory toIFD(final Node ifdNode) throws IIOInvalidTreeException {
|
||||
if (ifdNode == null || !ifdNode.getNodeName().equals("TIFFIFD")) {
|
||||
throw new IIOInvalidTreeException("Expected \"TIFFIFD\" node", ifdNode);
|
||||
}
|
||||
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
NodeList nodes = ifdNode.getChildNodes();
|
||||
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
entries.add(toEntry(nodes.item(i)));
|
||||
}
|
||||
|
||||
return new TIFFIFD(entries);
|
||||
}
|
||||
|
||||
private Entry toEntry(final Node node) throws IIOInvalidTreeException {
|
||||
String name = node.getNodeName();
|
||||
|
||||
if (name.equals("TIFFIFD")) {
|
||||
int tag = Integer.parseInt(getAttribute(node, "parentTagNumber"));
|
||||
Directory subIFD = toIFD(node);
|
||||
|
||||
return new TIFFImageWriter.TIFFEntry(tag, TIFF.TYPE_IFD, subIFD);
|
||||
}
|
||||
else if (name.equals("TIFFField")) {
|
||||
int tag = Integer.parseInt(getAttribute(node, "number"));
|
||||
short type = getTIFFType(node);
|
||||
Object value = getValue(node, type);
|
||||
|
||||
return value != null ? new TIFFImageWriter.TIFFEntry(tag, type, value) : null;
|
||||
}
|
||||
else {
|
||||
throw new IIOInvalidTreeException("Expected \"TIFFIFD\" or \"TIFFField\" node: " + name, node);
|
||||
}
|
||||
}
|
||||
|
||||
private short getTIFFType(final Node node) throws IIOInvalidTreeException {
|
||||
Node containerNode = node.getFirstChild();
|
||||
if (containerNode == null) {
|
||||
throw new IIOInvalidTreeException("Missing value wrapper node", node);
|
||||
}
|
||||
|
||||
String nodeName = containerNode.getNodeName();
|
||||
if (!nodeName.startsWith("TIFF")) {
|
||||
throw new IIOInvalidTreeException("Unexpected value wrapper node, expected type", containerNode);
|
||||
}
|
||||
|
||||
String typeName = nodeName.substring(4);
|
||||
|
||||
if (typeName.equals("Undefined")) {
|
||||
return TIFF.TYPE_UNDEFINED;
|
||||
}
|
||||
|
||||
typeName = typeName.substring(0, typeName.length() - 1).toUpperCase();
|
||||
|
||||
for (int i = 1; i < TIFF.TYPE_NAMES.length; i++) {
|
||||
if (typeName.equals(TIFF.TYPE_NAMES[i])) {
|
||||
return (short) i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IIOInvalidTreeException("Unknown TIFF type: " + typeName, containerNode);
|
||||
}
|
||||
|
||||
private Object getValue(final Node node, final short type) throws IIOInvalidTreeException {
|
||||
Node child = node.getFirstChild();
|
||||
|
||||
if (child != null) {
|
||||
String typeName = child.getNodeName();
|
||||
|
||||
if (type == TIFF.TYPE_UNDEFINED) {
|
||||
String values = getAttribute(child, "value");
|
||||
String[] vals = values.split(",\\s?");
|
||||
|
||||
byte[] bytes = new byte[vals.length];
|
||||
for (int i = 0; i < vals.length; i++) {
|
||||
bytes[i] = Byte.parseByte(vals[i]);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
else {
|
||||
NodeList valueNodes = child.getChildNodes();
|
||||
|
||||
// Create array for each type
|
||||
int count = valueNodes.getLength();
|
||||
Object value = createArrayForType(type, count);
|
||||
|
||||
// Parse each value
|
||||
for (int i = 0; i < count; i++) {
|
||||
Node valueNode = valueNodes.item(i);
|
||||
|
||||
if (!typeName.startsWith(valueNode.getNodeName())) {
|
||||
throw new IIOInvalidTreeException("Value node does not match container node", child);
|
||||
}
|
||||
|
||||
String stringValue = getAttribute(valueNode, "value");
|
||||
|
||||
// NOTE: The reason for parsing "wider" type, is to allow for unsigned values
|
||||
switch (type) {
|
||||
case TIFF.TYPE_BYTE:
|
||||
case TIFF.TYPE_SBYTE:
|
||||
((byte[]) value)[i] = (byte) Short.parseShort(stringValue);
|
||||
break;
|
||||
case TIFF.TYPE_ASCII:
|
||||
((String[]) value)[i] = stringValue;
|
||||
break;
|
||||
case TIFF.TYPE_SHORT:
|
||||
case TIFF.TYPE_SSHORT:
|
||||
((short[]) value)[i] = (short) Integer.parseInt(stringValue);
|
||||
break;
|
||||
case TIFF.TYPE_LONG:
|
||||
case TIFF.TYPE_SLONG:
|
||||
((int[]) value)[i] = (int) Long.parseLong(stringValue);
|
||||
break;
|
||||
case TIFF.TYPE_RATIONAL:
|
||||
case TIFF.TYPE_SRATIONAL:
|
||||
String[] numDenom = stringValue.split("/");
|
||||
((Rational[]) value)[i] = numDenom.length > 1
|
||||
? new Rational(Long.parseLong(numDenom[0]), Long.parseLong(numDenom[1]))
|
||||
: new Rational(Long.parseLong(numDenom[0]));
|
||||
break;
|
||||
case TIFF.TYPE_FLOAT:
|
||||
((float[]) value)[i] = Float.parseFloat(stringValue);
|
||||
break;
|
||||
case TIFF.TYPE_DOUBLE:
|
||||
((double[]) value)[i] = Double.parseDouble(stringValue);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unsupported TIFF type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize value
|
||||
if (count == 0) {
|
||||
return null;
|
||||
}
|
||||
if (count == 1) {
|
||||
return Array.get(value, 0);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IIOInvalidTreeException("Empty TIFField node", node);
|
||||
}
|
||||
|
||||
private Object createArrayForType(final short type, final int length) {
|
||||
switch (type) {
|
||||
case TIFF.TYPE_ASCII:
|
||||
return new String[length];
|
||||
case TIFF.TYPE_BYTE:
|
||||
case TIFF.TYPE_SBYTE:
|
||||
case TIFF.TYPE_UNDEFINED: // Not used here, but for completeness
|
||||
return new byte[length];
|
||||
case TIFF.TYPE_SHORT:
|
||||
case TIFF.TYPE_SSHORT:
|
||||
return new short[length];
|
||||
case TIFF.TYPE_LONG:
|
||||
case TIFF.TYPE_SLONG:
|
||||
return new int[length];
|
||||
case TIFF.TYPE_IFD:
|
||||
return new long[length];
|
||||
case TIFF.TYPE_RATIONAL:
|
||||
case TIFF.TYPE_SRATIONAL:
|
||||
return new Rational[length];
|
||||
case TIFF.TYPE_FLOAT:
|
||||
return new float[length];
|
||||
case TIFF.TYPE_DOUBLE:
|
||||
return new double[length];
|
||||
default:
|
||||
throw new AssertionError("Unsupported TIFF type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private String getAttribute(final Node node, final String attribute) {
|
||||
return node instanceof Element ? ((Element) node).getAttribute(attribute) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
|
||||
ifd = original;
|
||||
}
|
||||
|
||||
Directory getIFD() {
|
||||
return ifd;
|
||||
}
|
||||
|
||||
// TODO: Replace with IFD class when moved to new package and made public!
|
||||
private final static class TIFFIFD extends AbstractDirectory {
|
||||
public TIFFIFD(final Collection<Entry> entries) {
|
||||
super(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.Rational;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
|
||||
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
@@ -58,13 +61,16 @@ import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.CMMException;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
@@ -82,15 +88,16 @@ import java.util.zip.InflaterInputStream;
|
||||
* In addition, it supports many common TIFF extensions such as:
|
||||
* <ul>
|
||||
* <li>Tiling</li>
|
||||
* <li>Class F (Facsimile), CCITT T.4 and T.6 compression (types 3 and 4), 1 bit per sample</li>
|
||||
* <li>LZW Compression (type 5)</li>
|
||||
* <li>"Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined</li>
|
||||
* <li>JPEG Compression (type 7)</li>
|
||||
* <li>ZLib (aka Adobe-style Deflate) Compression (type 8)</li>
|
||||
* <li>Deflate Compression (type 32946)</li>
|
||||
* <li>Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression</li>
|
||||
* <li>Alpha channel (ExtraSamples type 1/Associated Alpha)</li>
|
||||
* <li>CMYK data (PhotometricInterpretation type 5/Separated)</li>
|
||||
* <li>YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG</li>
|
||||
* <li>Alpha channel (ExtraSamples types 1/Associated Alpha and 2/Unassociated Alpha)</li>
|
||||
* <li>Class S, CMYK data (PhotometricInterpretation type 5/Separated)</li>
|
||||
* <li>Class Y, YCbCr data (PhotometricInterpretation type 6/YCbCr for both JPEG and other compressions</li>
|
||||
* <li>Planar data (PlanarConfiguration type 2/Planar)</li>
|
||||
* <li>ICC profiles (ICCProfile)</li>
|
||||
* <li>BitsPerSample values up to 16 for most PhotometricInterpretations</li>
|
||||
@@ -119,7 +126,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// TODO: Implement readAsRenderedImage to allow tiled RenderedImage?
|
||||
// For some layouts, we could do reads super-fast with a memory mapped buffer.
|
||||
// TODO: Implement readAsRaster directly
|
||||
// TODO: IIOMetadata (stay close to Sun's TIFF metadata)
|
||||
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
|
||||
|
||||
// TODOs Extension support
|
||||
@@ -136,6 +142,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// Support Compression 2 (CCITT Modified Huffman RLE) for bi-level images
|
||||
// Source region
|
||||
// Subsampling
|
||||
// IIOMetadata (stay close to Sun's TIFF metadata)
|
||||
|
||||
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
|
||||
|
||||
@@ -167,6 +174,73 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
for (int i = 0; i < IFDs.directoryCount(); i++) {
|
||||
System.err.printf("IFD %d: %s\n", i, IFDs.getDirectory(i));
|
||||
}
|
||||
|
||||
Entry tiffXMP = IFDs.getEntryById(TIFF.TAG_XMP);
|
||||
if (tiffXMP != null) {
|
||||
byte[] value = (byte[]) tiffXMP.getValue();
|
||||
|
||||
// The XMPReader doesn't like null-termination...
|
||||
int len = value.length;
|
||||
for (int i = len - 1; i > 0; i--) {
|
||||
if (value[i] == 0) {
|
||||
len--;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Directory xmp = new XMPReader().read(new ByteArrayImageInputStream(value, 0, len));
|
||||
System.err.println("-----------------------------------------------------------------------------");
|
||||
System.err.println("xmp: " + xmp);
|
||||
}
|
||||
|
||||
Entry tiffIPTC = IFDs.getEntryById(TIFF.TAG_IPTC);
|
||||
if (tiffIPTC != null) {
|
||||
Object value = tiffIPTC.getValue();
|
||||
if (value instanceof short[]) {
|
||||
System.err.println("short[]: " + value);
|
||||
}
|
||||
if (value instanceof long[]) {
|
||||
// As seen in a Magick produced image...
|
||||
System.err.println("long[]: " + value);
|
||||
long[] longs = (long[]) value;
|
||||
value = new byte[longs.length * 8];
|
||||
ByteBuffer.wrap((byte[]) value).asLongBuffer().put(longs);
|
||||
}
|
||||
if (value instanceof float[]) {
|
||||
System.err.println("float[]: " + value);
|
||||
}
|
||||
if (value instanceof double[]) {
|
||||
System.err.println("double[]: " + value);
|
||||
}
|
||||
|
||||
Directory iptc = new IPTCReader().read(new ByteArrayImageInputStream((byte[]) value));
|
||||
System.err.println("-----------------------------------------------------------------------------");
|
||||
System.err.println("iptc: " + iptc);
|
||||
}
|
||||
|
||||
Entry tiffPSD = IFDs.getEntryById(TIFF.TAG_PHOTOSHOP);
|
||||
if (tiffPSD != null) {
|
||||
Directory psd = new PSDReader().read(new ByteArrayImageInputStream((byte[]) tiffPSD.getValue()));
|
||||
System.err.println("-----------------------------------------------------------------------------");
|
||||
System.err.println("psd: " + psd);
|
||||
}
|
||||
Entry tiffPSD2 = IFDs.getEntryById(TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA);
|
||||
if (tiffPSD2 != null) {
|
||||
byte[] value = (byte[]) tiffPSD2.getValue();
|
||||
String foo = "Adobe Photoshop Document Data Block";
|
||||
|
||||
if (Arrays.equals(foo.getBytes(StandardCharsets.US_ASCII), Arrays.copyOf(value, foo.length()))) {
|
||||
System.err.println("foo: " + foo);
|
||||
// int offset = foo.length() + 1;
|
||||
// ImageInputStream input = new ByteArrayImageInputStream(value, offset, value.length - offset);
|
||||
// input.setByteOrder(ByteOrder.LITTLE_ENDIAN); // TODO: WHY???!
|
||||
// Directory psd2 = new PSDReader().read(input);
|
||||
// System.err.println("-----------------------------------------------------------------------------");
|
||||
// System.err.println("psd2: " + psd2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +334,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
switch (samplesPerPixel) {
|
||||
case 1:
|
||||
// TIFF 6.0 Spec says: 1, 4 or 8 for baseline (1 for bi-level, 4/8 for gray)
|
||||
// ImageTypeSpecifier supports 1, 2, 4, 8 or 16 bits, we'll go with that for now
|
||||
// ImageTypeSpecifier supports 1, 2, 4, 8 or 16 bits per sample, we'll support 32 bits as well.
|
||||
// (Chunky or planar makes no difference for a single channel).
|
||||
if (profile != null && profile.getColorSpaceType() != ColorSpace.TYPE_GRAY) {
|
||||
processWarningOccurred(String.format("Embedded ICC color profile (type %s), is incompatible with image data (GRAY/type 6). Ignoring profile.", profile.getColorSpaceType()));
|
||||
profile = null;
|
||||
@@ -274,10 +349,46 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
else if (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 || bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) {
|
||||
return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0}, dataType, false, false);
|
||||
}
|
||||
default:
|
||||
// TODO: If ExtraSamples is used, PlanarConfiguration must be taken into account also for gray data
|
||||
|
||||
throw new IIOException(String.format("Unsupported SamplesPerPixel/BitsPerSample combination for Bi-level/Gray TIFF (expected 1/1, 1/2, 1/4, 1/8 or 1/16): %d/%d", samplesPerPixel, bitsPerSample));
|
||||
throw new IIOException(String.format("Unsupported BitsPerSample for Bi-level/Gray TIFF (expected 1, 2, 4, 8, 16 or 32): %d", bitsPerSample));
|
||||
|
||||
case 2:
|
||||
// Gray + alpha. We'll support:
|
||||
// * 8, 16 or 32 bits per sample
|
||||
// * Associated (pre-multiplied) or unassociated (non-pre-multiplied) alpha
|
||||
// * Chunky (interleaved) or planar (banded) data
|
||||
if (profile != null && profile.getColorSpaceType() != ColorSpace.TYPE_GRAY) {
|
||||
processWarningOccurred(String.format("Embedded ICC color profile (type %s), is incompatible with image data (GRAY/type 6). Ignoring profile.", profile.getColorSpaceType()));
|
||||
profile = null;
|
||||
}
|
||||
|
||||
cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_GRAY) : ColorSpaces.createColorSpace(profile);
|
||||
|
||||
// ExtraSamples 0=unspecified, 1=associated (pre-multiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
|
||||
|
||||
if (cs == ColorSpace.getInstance(ColorSpace.CS_GRAY) && (bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32)) {
|
||||
switch (planarConfiguration) {
|
||||
case TIFFBaseline.PLANARCONFIG_CHUNKY:
|
||||
return ImageTypeSpecifiers.createGrayscale(bitsPerSample, dataType, extraSamples[0] == 1);
|
||||
case TIFFExtension.PLANARCONFIG_PLANAR:
|
||||
return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, dataType, true, extraSamples[0] == 1);
|
||||
}
|
||||
}
|
||||
else if (bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) {
|
||||
switch (planarConfiguration) {
|
||||
case TIFFBaseline.PLANARCONFIG_CHUNKY:
|
||||
return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1}, dataType, true, extraSamples[0] == 1);
|
||||
case TIFFExtension.PLANARCONFIG_PLANAR:
|
||||
return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, dataType, true, extraSamples[0] == 1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IIOException(String.format("Unsupported BitsPerSample for Gray + Alpha TIFF (expected 8, 16 or 32): %d", bitsPerSample));
|
||||
// TODO: More samples might be ok, if multiple alpha or unknown samples
|
||||
|
||||
default:
|
||||
throw new IIOException(String.format("Unsupported SamplesPerPixel/BitsPerSample combination for Bi-level/Gray TIFF (expected 1/1, 1/2, 1/4, 1/8, 1/16 or 1/32, or 2/8, 2/16 or 2/32): %d/%d", samplesPerPixel, bitsPerSample));
|
||||
}
|
||||
|
||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||
@@ -324,6 +435,11 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, true, extraSamples[0] == 1);
|
||||
}
|
||||
}
|
||||
else if (bitsPerSample == 4) {
|
||||
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
|
||||
|
||||
return ImageTypeSpecifier.createPacked(cs, 0xF000, 0xF00, 0xF0, 0xF, DataBuffer.TYPE_USHORT, extraSamples[0] == 1);
|
||||
}
|
||||
// TODO: More samples might be ok, if multiple alpha or unknown samples
|
||||
default:
|
||||
throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIFF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample));
|
||||
@@ -411,14 +527,32 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
case TIFFBaseline.SAMPLEFORMAT_UINT:
|
||||
return bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT;
|
||||
case TIFFExtension.SAMPLEFORMAT_INT:
|
||||
if (bitsPerSample == 16) {
|
||||
return DataBuffer.TYPE_SHORT;
|
||||
switch (bitsPerSample) {
|
||||
case 8:
|
||||
return DataBuffer.TYPE_BYTE;
|
||||
case 16:
|
||||
return DataBuffer.TYPE_SHORT;
|
||||
case 32:
|
||||
return DataBuffer.TYPE_INT;
|
||||
}
|
||||
throw new IIOException("Unsupported BitPerSample for SampleFormat 2/Signed Integer (expected 16): " + bitsPerSample);
|
||||
|
||||
throw new IIOException("Unsupported BitsPerSample for SampleFormat 2/Signed Integer (expected 8/16/32): " + bitsPerSample);
|
||||
|
||||
case TIFFExtension.SAMPLEFORMAT_FP:
|
||||
throw new IIOException("Unsupported TIFF SampleFormat: (3/Floating point)");
|
||||
if (bitsPerSample == 32) {
|
||||
return DataBuffer.TYPE_FLOAT;
|
||||
}
|
||||
|
||||
throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 32): " + bitsPerSample);
|
||||
|
||||
case TIFFExtension.SAMPLEFORMAT_UNDEFINED:
|
||||
throw new IIOException("Unsupported TIFF SampleFormat (4/Undefined)");
|
||||
// Spec says:
|
||||
// A field value of “undefined” is a statement by the writer that it did not know how
|
||||
// to interpret the data samples; for example, if it were copying an existing image. A
|
||||
// reader would typically treat an image with “undefined” data as if the field were
|
||||
// not present (i.e. as unsigned integer data).
|
||||
// TODO: We should probably issue a warning instead, and assume SAMPLEFORMAT_UINT
|
||||
throw new IIOException("Unsupported TIFF SampleFormat: 4 (Undefined)");
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat);
|
||||
}
|
||||
@@ -585,7 +719,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
int tilesAcross = (width + stripTileWidth - 1) / stripTileWidth;
|
||||
int tilesDown = (height + stripTileHeight - 1) / stripTileHeight;
|
||||
WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1);
|
||||
// WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1);
|
||||
WritableRaster rowRaster = rawType.createBufferedImage(stripTileWidth, 1).getRaster();
|
||||
Rectangle clip = new Rectangle(srcRegion);
|
||||
int row = 0;
|
||||
|
||||
@@ -888,7 +1023,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
imageInput.seek(realJPEGOffset);
|
||||
|
||||
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Short.MAX_VALUE);
|
||||
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Integer.MAX_VALUE);
|
||||
jpegReader.setInput(stream);
|
||||
|
||||
// Read data
|
||||
@@ -1259,6 +1394,46 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case DataBuffer.TYPE_FLOAT:
|
||||
float[] rowDataFloat = ((DataBufferFloat) tileRowRaster.getDataBuffer()).getData();
|
||||
|
||||
for (int row = startRow; row < startRow + rowsInTile; row++) {
|
||||
if (row >= srcRegion.y + srcRegion.height) {
|
||||
break; // We're done with this tile
|
||||
}
|
||||
|
||||
readFully(input, rowDataFloat);
|
||||
|
||||
if (row >= srcRegion.y) {
|
||||
// normalizeBlack(interpretation, rowDataFloat);
|
||||
|
||||
// Subsample horizontal
|
||||
if (xSub != 1) {
|
||||
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) {
|
||||
System.arraycopy(rowDataFloat, x * xSub, rowDataFloat, x, numBands);
|
||||
}
|
||||
}
|
||||
|
||||
raster.setDataElements(startCol, row - srcRegion.y, tileRowRaster);
|
||||
}
|
||||
// Else skip data
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Candidate util method (with off/len + possibly byte order)
|
||||
private void readFully(final DataInput input, final float[] rowDataFloat) throws IOException {
|
||||
if (input instanceof ImageInputStream) {
|
||||
ImageInputStream imageInputStream = (ImageInputStream) input;
|
||||
imageInputStream.readFully(rowDataFloat, 0, rowDataFloat.length);
|
||||
}
|
||||
else {
|
||||
for (int k = 0; k < rowDataFloat.length; k++) {
|
||||
rowDataFloat[k] = input.readFloat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1322,7 +1497,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
case TIFFBaseline.COMPRESSION_PACKBITS:
|
||||
return new DecoderStream(stream, new PackBitsDecoder(), 1024);
|
||||
case TIFFExtension.COMPRESSION_LZW:
|
||||
return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), width * bands);
|
||||
return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), Math.max(width * bands, 1024));
|
||||
case TIFFExtension.COMPRESSION_ZLIB:
|
||||
// TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical
|
||||
case TIFFExtension.COMPRESSION_DEFLATE:
|
||||
@@ -1394,14 +1569,24 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return value;
|
||||
}
|
||||
|
||||
public ICC_Profile getICCProfile() {
|
||||
private ICC_Profile getICCProfile() throws IOException {
|
||||
Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
|
||||
if (entry != null) {
|
||||
byte[] value = (byte[]) entry.getValue();
|
||||
|
||||
try {
|
||||
// WEIRDNESS: Reading profile from InputStream is somehow more compatible
|
||||
// than reading from byte array (chops off extra bytes + validates profile).
|
||||
ICC_Profile profile = ICC_Profile.getInstance(new ByteArrayInputStream(value));
|
||||
return ColorSpaces.validateProfile(profile);
|
||||
}
|
||||
catch (CMMException | IllegalArgumentException ignore) {
|
||||
processWarningOccurred("Ignoring broken/incompatible ICC profile: " + ignore.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
byte[] value = (byte[]) entry.getValue();
|
||||
return ICC_Profile.getInstance(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Tiling support
|
||||
@@ -1500,27 +1685,30 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// param.setSourceSubsampling(sub, sub, 0, 0);
|
||||
// }
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
long start = System.currentTimeMillis();
|
||||
// int width = reader.getWidth(imageNo);
|
||||
// int height = reader.getHeight(imageNo);
|
||||
// param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
|
||||
// param.setSourceRegion(new Rectangle(100, 300, 400, 400));
|
||||
// param.setSourceRegion(new Rectangle(3, 3, 9, 9));
|
||||
// param.setDestinationOffset(new Point(50, 150));
|
||||
// param.setSourceSubsampling(2, 2, 0, 0);
|
||||
BufferedImage image = reader.read(imageNo, param);
|
||||
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
BufferedImage image = reader.read(imageNo, param);
|
||||
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
IIOMetadata metadata = reader.getImageMetadata(imageNo);
|
||||
if (metadata != null) {
|
||||
if (metadata.getNativeMetadataFormatName() != null) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
|
||||
IIOMetadata metadata = reader.getImageMetadata(imageNo);
|
||||
if (metadata != null) {
|
||||
if (metadata.getNativeMetadataFormatName() != null) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
|
||||
}
|
||||
/*else*/
|
||||
if (metadata.isStandardMetadataFormatSupported()) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
|
||||
}
|
||||
}
|
||||
/*else*/ if (metadata.isStandardMetadataFormatSupported()) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
|
||||
}
|
||||
}
|
||||
|
||||
System.err.println("image: " + image);
|
||||
System.err.println("image: " + image);
|
||||
|
||||
// File tempFile = File.createTempFile("lzw-", ".bin");
|
||||
// byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
|
||||
@@ -1536,7 +1724,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
//
|
||||
// System.err.println("tempFile: " + tempFile.getAbsolutePath());
|
||||
|
||||
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
|
||||
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
|
||||
//
|
||||
// int maxW = 800;
|
||||
// int maxH = 800;
|
||||
@@ -1553,30 +1741,35 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// // System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
// }
|
||||
|
||||
if (image.getType() == BufferedImage.TYPE_CUSTOM) {
|
||||
start = System.currentTimeMillis();
|
||||
image = new ColorConvertOp(null).filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB));
|
||||
System.err.println("Conversion time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
if (image.getType() == BufferedImage.TYPE_CUSTOM) {
|
||||
start = System.currentTimeMillis();
|
||||
image = new ColorConvertOp(null).filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB));
|
||||
System.err.println("Conversion time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
|
||||
showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(imageNo), reader.getHeight(imageNo)));
|
||||
showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(imageNo), reader.getHeight(imageNo)));
|
||||
|
||||
try {
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
for (int thumbnailNo = 0; thumbnailNo < numThumbnails; thumbnailNo++) {
|
||||
BufferedImage thumbnail = reader.readThumbnail(imageNo, thumbnailNo);
|
||||
// System.err.println("thumbnail: " + thumbnail);
|
||||
showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight()));
|
||||
try {
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
for (int thumbnailNo = 0; thumbnailNo < numThumbnails; thumbnailNo++) {
|
||||
BufferedImage thumbnail = reader.readThumbnail(imageNo, thumbnailNo);
|
||||
// System.err.println("thumbnail: " + thumbnail);
|
||||
showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight()));
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
System.err.println("Could not read thumbnails: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
System.err.println("Could not read thumbnails: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
catch (Throwable t) {
|
||||
System.err.println(file + " image " + imageNo + " can't be read:");
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
System.err.println(file);
|
||||
System.err.println(file + " can't be read:");
|
||||
t.printStackTrace();
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -31,6 +31,7 @@ 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.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.Rational;
|
||||
@@ -39,9 +40,12 @@ import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
import com.twelvemonkeys.io.enc.EncoderStream;
|
||||
import com.twelvemonkeys.io.enc.PackBitsEncoder;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.spi.ImageWriterSpi;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
@@ -50,10 +54,9 @@ import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_ColorSpace;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Array;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
@@ -65,13 +68,16 @@ import java.util.zip.DeflaterOutputStream;
|
||||
* @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
|
||||
*/
|
||||
public final class TIFFImageWriter extends ImageWriterBase {
|
||||
// Short term
|
||||
// TODO: Support more of the ImageIO metadata (ie. compression from metadata, etc)
|
||||
|
||||
// Long term
|
||||
// TODO: Support tiling
|
||||
// TODO: Support thumbnails
|
||||
// TODO: Support ImageIO metadata
|
||||
// TODO: Support CCITT Modified Huffman compression (2)
|
||||
// TODO: Full "Baseline TIFF" support (pending CCITT compression 2)
|
||||
// TODO: CCITT compressions T.4 and T.6
|
||||
// TODO: Support JPEG compression of CMYK data (pending JPEGImageWriter CMYK write support)
|
||||
// ----
|
||||
// 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
|
||||
@@ -91,6 +97,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
// Support LZW compression (5)
|
||||
// Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
|
||||
// Use sensible defaults for compression based on input? None is sensible... :-)
|
||||
// Support resolution, resolution unit and software tags from ImageIO metadata
|
||||
|
||||
public static final Rational STANDARD_DPI = new Rational(72);
|
||||
|
||||
@@ -98,14 +105,81 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutput(final Object output) {
|
||||
super.setOutput(output);
|
||||
|
||||
// TODO: Allow appending/partly overwrite of existing file...
|
||||
}
|
||||
|
||||
static final class TIFFEntry extends AbstractEntry {
|
||||
TIFFEntry(Object identifier, Object value) {
|
||||
// TODO: Expose a merge of this and the EXIFEntry class...
|
||||
private final short type;
|
||||
|
||||
private static short guessType(final Object val) {
|
||||
// TODO: This code is duplicated in EXIFWriter.getType, needs refactor!
|
||||
Object value = Validate.notNull(val);
|
||||
|
||||
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 guessType not implemented for value of type %s", value.getClass()));
|
||||
}
|
||||
|
||||
TIFFEntry(final int identifier, final Object value) {
|
||||
this(identifier, guessType(value), value);
|
||||
}
|
||||
|
||||
TIFFEntry(int identifier, short type, Object value) {
|
||||
super(identifier, value);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return TIFF.TYPE_NAMES[type];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
|
||||
public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException {
|
||||
// TODO: Validate input
|
||||
|
||||
assertOutput();
|
||||
@@ -120,6 +194,14 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
ColorModel colorModel = renderedImage.getColorModel();
|
||||
int numComponents = colorModel.getNumComponents();
|
||||
|
||||
TIFFImageMetadata metadata;
|
||||
if (image.getMetadata() != null) {
|
||||
metadata = convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param);
|
||||
}
|
||||
else {
|
||||
metadata = initMeta(null, ImageTypeSpecifier.createFromRenderedImage(renderedImage), param);
|
||||
}
|
||||
|
||||
SampleModel sampleModel = renderedImage.getSampleModel();
|
||||
|
||||
int[] bandOffsets;
|
||||
@@ -140,7 +222,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
|
||||
}
|
||||
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
Set<Entry> entries = new LinkedHashSet<>();
|
||||
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)
|
||||
@@ -157,10 +239,12 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
}
|
||||
|
||||
// Write compression field from param or metadata
|
||||
// TODO: Support COPY_FROM_METADATA
|
||||
int compression = TIFFImageWriteParam.getCompressionType(param);
|
||||
entries.add(new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
|
||||
|
||||
// TODO: Let param/metadata control predictor
|
||||
// TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT
|
||||
switch (compression) {
|
||||
case TIFFExtension.COMPRESSION_ZLIB:
|
||||
case TIFFExtension.COMPRESSION_DEFLATE:
|
||||
@@ -169,7 +253,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
default:
|
||||
}
|
||||
|
||||
// TODO: We might want to support CMYK in JPEG as well...
|
||||
// TODO: We might want to support CMYK in JPEG as well... Pending JPEG CMYK write support.
|
||||
int photometric = compression == TIFFExtension.COMPRESSION_JPEG ?
|
||||
TIFFExtension.PHOTOMETRIC_YCBCR :
|
||||
getPhotometricInterpretation(colorModel);
|
||||
@@ -189,15 +273,24 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
}
|
||||
}
|
||||
|
||||
// Default sample format SAMPLEFORMAT_UINT need not be written
|
||||
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));
|
||||
}
|
||||
// TODO: Float values!
|
||||
|
||||
entries.add(new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer")); // TODO: Get from metadata (optional) + fill in version number
|
||||
// Get Software from metadata, or use default
|
||||
Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE);
|
||||
entries.add(software != null ? software : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
|
||||
|
||||
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));
|
||||
// Get X/YResolution and ResolutionUnit from metadata if set, otherwise use defaults
|
||||
// TODO: Add logic here OR in metadata merging, to make sure these 3 values are consistent.
|
||||
Entry xRes = metadata.getIFD().getEntryById(TIFF.TAG_X_RESOLUTION);
|
||||
entries.add(xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
|
||||
Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION);
|
||||
entries.add(yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
|
||||
Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT);
|
||||
entries.add(resUnit != null ? resUnit : 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
|
||||
@@ -208,7 +301,8 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
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?
|
||||
// TODO: If tiled, write tile indexes etc
|
||||
// Depending on param.getTilingMode
|
||||
|
||||
EXIFWriter exifWriter = new EXIFWriter();
|
||||
|
||||
@@ -233,6 +327,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
// TODO: Create compressor stream per Tile/Strip
|
||||
if (compression == TIFFExtension.COMPRESSION_JPEG) {
|
||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("JPEG");
|
||||
|
||||
if (!writers.hasNext()) {
|
||||
// This can only happen if someone deliberately uninstalled it
|
||||
throw new IIOException("No JPEG ImageWriter found!");
|
||||
@@ -607,13 +702,75 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
// Metadata
|
||||
|
||||
@Override
|
||||
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
|
||||
return null;
|
||||
public TIFFImageMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) {
|
||||
return initMeta(null, imageType, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
|
||||
return null;
|
||||
public TIFFImageMetadata convertImageMetadata(final IIOMetadata inData,
|
||||
final ImageTypeSpecifier imageType,
|
||||
final ImageWriteParam param) {
|
||||
Validate.notNull(inData, "inData");
|
||||
Validate.notNull(imageType, "imageType");
|
||||
|
||||
Directory ifd;
|
||||
|
||||
if (inData instanceof TIFFImageMetadata) {
|
||||
ifd = ((TIFFImageMetadata) inData).getIFD();
|
||||
}
|
||||
else {
|
||||
TIFFImageMetadata outData = new TIFFImageMetadata(Collections.<Entry>emptySet());
|
||||
|
||||
try {
|
||||
if (Arrays.asList(inData.getMetadataFormatNames()).contains(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)) {
|
||||
outData.setFromTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, inData.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME));
|
||||
}
|
||||
else if (inData.isStandardMetadataFormatSupported()) {
|
||||
outData.setFromTree(IIOMetadataFormatImpl.standardMetadataFormatName, inData.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName));
|
||||
}
|
||||
else {
|
||||
// Unknown format, we can't convert it
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (IIOInvalidTreeException e) {
|
||||
// TODO: How to issue warning when warning requires imageIndex??? Use -1?
|
||||
}
|
||||
|
||||
ifd = outData.getIFD();
|
||||
}
|
||||
|
||||
// Overwrite in values with values from imageType and param as needed
|
||||
return initMeta(ifd, imageType, param);
|
||||
}
|
||||
|
||||
private TIFFImageMetadata initMeta(final Directory ifd, final ImageTypeSpecifier imageType, final ImageWriteParam param) {
|
||||
Validate.notNull(imageType, "imageType");
|
||||
|
||||
Map<Integer, Entry> entries = new LinkedHashMap<>(ifd != null ? ifd.size() + 10 : 20);
|
||||
|
||||
if (ifd != null) {
|
||||
for (Entry entry : ifd) {
|
||||
entries.put((Integer) entry.getIdentifier(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Set values from imageType
|
||||
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, getPhotometricInterpretation(imageType.getColorModel())));
|
||||
|
||||
// TODO: Set values from param if != null + combined values...
|
||||
|
||||
return new TIFFImageMetadata(entries.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam param) {
|
||||
return super.getDefaultStreamMetadata(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIOMetadata convertStreamMetadata(final IIOMetadata inData, final ImageWriteParam param) {
|
||||
return super.convertStreamMetadata(inData, param);
|
||||
}
|
||||
|
||||
// Param
|
||||
@@ -762,5 +919,4 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
||||
|
||||
TIFFImageReader.showIt(read, output.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TIFFProviderInfo extends ReaderWriterProviderInfo {
|
||||
protected TIFFProviderInfo() {
|
||||
super(
|
||||
TIFFProviderInfo.class,
|
||||
new String[] {"tiff", "TIFF"},
|
||||
new String[] {"tiff", "TIFF", "tif", "TIF"},
|
||||
new String[] {"tif", "tiff"},
|
||||
new String[] {
|
||||
"image/tiff", "image/x-tiff"
|
||||
|
||||
@@ -0,0 +1,599 @@
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.Rational;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
|
||||
import com.twelvemonkeys.lang.StringUtil;
|
||||
import org.junit.Test;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* TIFFImageMetadataTest.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: TIFFImageMetadataTest.java,v 1.0 30/07/15 harald.kuhr Exp$
|
||||
*/
|
||||
public class TIFFImageMetadataTest {
|
||||
|
||||
static {
|
||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||
ImageIO.setUseCache(false);
|
||||
}
|
||||
|
||||
// TODO: Candidate super method
|
||||
private URL getClassLoaderResource(final String resource) {
|
||||
return getClass().getResource(resource);
|
||||
}
|
||||
|
||||
// TODO: Candidate abstract super method
|
||||
private IIOMetadata createMetadata(final String resource) throws IOException {
|
||||
try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource(resource))) {
|
||||
Directory ifd = new EXIFReader().read(input);
|
||||
// System.err.println("ifd: " + ifd);
|
||||
return new TIFFImageMetadata(ifd);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMetadataStandardFormat() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/smallliz.tif");
|
||||
Node root = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
|
||||
|
||||
// Root: "javax_imageio_1.0"
|
||||
assertNotNull(root);
|
||||
assertEquals(IIOMetadataFormatImpl.standardMetadataFormatName, root.getNodeName());
|
||||
assertEquals(6, root.getChildNodes().getLength());
|
||||
|
||||
// "Chroma"
|
||||
Node chroma = root.getFirstChild();
|
||||
assertEquals("Chroma", chroma.getNodeName());
|
||||
|
||||
assertEquals(3, chroma.getChildNodes().getLength());
|
||||
|
||||
Node colorSpaceType = chroma.getFirstChild();
|
||||
assertEquals("ColorSpaceType", colorSpaceType.getNodeName());
|
||||
assertEquals("YCbCr", ((Element) colorSpaceType).getAttribute("value"));
|
||||
|
||||
Node numChannels = colorSpaceType.getNextSibling();
|
||||
assertEquals("NumChannels", numChannels.getNodeName());
|
||||
assertEquals("3", ((Element) numChannels).getAttribute("value"));
|
||||
|
||||
Node blackIsZero = numChannels.getNextSibling();
|
||||
assertEquals("BlackIsZero", blackIsZero.getNodeName());
|
||||
assertEquals(0, blackIsZero.getAttributes().getLength());
|
||||
|
||||
// "Compression"
|
||||
Node compression = chroma.getNextSibling();
|
||||
assertEquals("Compression", compression.getNodeName());
|
||||
assertEquals(2, compression.getChildNodes().getLength());
|
||||
|
||||
Node compressionTypeName = compression.getFirstChild();
|
||||
assertEquals("CompressionTypeName", compressionTypeName.getNodeName());
|
||||
assertEquals("Old JPEG", ((Element) compressionTypeName).getAttribute("value"));
|
||||
|
||||
Node lossless = compressionTypeName.getNextSibling();
|
||||
assertEquals("Lossless", lossless.getNodeName());
|
||||
assertEquals("FALSE", ((Element) lossless).getAttribute("value"));
|
||||
|
||||
// "Data"
|
||||
Node data = compression.getNextSibling();
|
||||
assertEquals("Data", data.getNodeName());
|
||||
assertEquals(4, data.getChildNodes().getLength());
|
||||
|
||||
Node planarConfiguration = data.getFirstChild();
|
||||
assertEquals("PlanarConfiguration", planarConfiguration.getNodeName());
|
||||
assertEquals("PixelInterleaved", ((Element) planarConfiguration).getAttribute("value"));
|
||||
|
||||
Node sampleFormat = planarConfiguration.getNextSibling();
|
||||
assertEquals("SampleFormat", sampleFormat.getNodeName());
|
||||
assertEquals("UnsignedIntegral", ((Element) sampleFormat).getAttribute("value"));
|
||||
|
||||
Node bitsPerSample = sampleFormat.getNextSibling();
|
||||
assertEquals("BitsPerSample", bitsPerSample.getNodeName());
|
||||
assertEquals("8 8 8", ((Element) bitsPerSample).getAttribute("value"));
|
||||
|
||||
Node sampleMSB = bitsPerSample.getNextSibling();
|
||||
assertEquals("SampleMSB", sampleMSB.getNodeName());
|
||||
assertEquals("0 0 0", ((Element) sampleMSB).getAttribute("value"));
|
||||
|
||||
// "Dimension"
|
||||
Node dimension = data.getNextSibling();
|
||||
assertEquals("Dimension", dimension.getNodeName());
|
||||
assertEquals(3, dimension.getChildNodes().getLength());
|
||||
|
||||
Node pixelAspectRatio = dimension.getFirstChild();
|
||||
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
|
||||
assertEquals("1.0", ((Element) pixelAspectRatio).getAttribute("value"));
|
||||
|
||||
Node horizontalPixelSize = pixelAspectRatio.getNextSibling();
|
||||
assertEquals("HorizontalPixelSize", horizontalPixelSize.getNodeName());
|
||||
assertEquals("0.254", ((Element) horizontalPixelSize).getAttribute("value"));
|
||||
|
||||
Node verticalPixelSize = horizontalPixelSize.getNextSibling();
|
||||
assertEquals("VerticalPixelSize", verticalPixelSize.getNodeName());
|
||||
assertEquals("0.254", ((Element) verticalPixelSize).getAttribute("value"));
|
||||
|
||||
// "Document"
|
||||
Node document = dimension.getNextSibling();
|
||||
assertEquals("Document", document.getNodeName());
|
||||
assertEquals(1, document.getChildNodes().getLength());
|
||||
|
||||
Node formatVersion = document.getFirstChild();
|
||||
assertEquals("FormatVersion", formatVersion.getNodeName());
|
||||
assertEquals("6.0", ((Element) formatVersion).getAttribute("value"));
|
||||
|
||||
// "Text"
|
||||
Node text = document.getNextSibling();
|
||||
assertEquals("Text", text.getNodeName());
|
||||
assertEquals(1, text.getChildNodes().getLength());
|
||||
|
||||
// NOTE: Could be multiple "TextEntry" elements, with different "keyword" attributes
|
||||
Node textEntry = text.getFirstChild();
|
||||
assertEquals("TextEntry", textEntry.getNodeName());
|
||||
assertEquals("Software", ((Element) textEntry).getAttribute("keyword"));
|
||||
assertEquals("HP IL v1.1", ((Element) textEntry).getAttribute("value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMetadataNativeFormat() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/quad-lzw.tif");
|
||||
Node root = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
|
||||
|
||||
// Root: "com_sun_media_imageio_plugins_tiff_image_1.0"
|
||||
assertNotNull(root);
|
||||
assertEquals(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, root.getNodeName());
|
||||
assertEquals(1, root.getChildNodes().getLength());
|
||||
|
||||
// IFD: "TIFFIFD"
|
||||
Node ifd = root.getFirstChild();
|
||||
assertEquals("TIFFIFD", ifd.getNodeName());
|
||||
|
||||
NodeList entries = ifd.getChildNodes();
|
||||
assertEquals(13, entries.getLength());
|
||||
|
||||
String[] stripOffsets = {
|
||||
"8", "150", "292", "434", "576", "718", "860", "1002", "1144", "1286",
|
||||
"1793", "3823", "7580", "12225", "17737", "23978", "30534", "36863", "42975", "49180",
|
||||
"55361", "61470", "67022", "71646", "74255", "75241", "75411", "75553", "75695", "75837",
|
||||
"75979", "76316", "77899", "80466", "84068", "88471", "93623", "99105", "104483", "109663",
|
||||
"114969", "120472", "126083", "131289", "135545", "138810", "140808", "141840", "141982", "142124",
|
||||
"142266", "142408", "142615", "144074", "146327", "149721", "154066", "158927", "164022", "169217",
|
||||
"174409", "179657", "185166", "190684", "196236", "201560", "206064", "209497", "211612", "212419",
|
||||
"212561", "212703", "212845", "212987", "213129", "213271", "213413"
|
||||
};
|
||||
|
||||
String[] stripByteCounts = {
|
||||
"142", "142", "142", "142", "142", "142", "142", "142", "142", "507",
|
||||
"2030", "3757", "4645", "5512", "6241", "6556", "6329", "6112", "6205", "6181",
|
||||
"6109", "5552", "4624", "2609", "986", "170", "142", "142", "142", "142",
|
||||
"337", "1583", "2567", "3602", "4403", "5152", "5482", "5378", "5180", "5306",
|
||||
"5503", "5611", "5206", "4256", "3265", "1998", "1032", "142", "142", "142",
|
||||
"142", "207", "1459", "2253", "3394", "4345", "4861", "5095", "5195", "5192",
|
||||
"5248", "5509", "5518", "5552", "5324", "4504", "3433", "2115", "807", "142",
|
||||
"142", "142", "142", "142", "142", "142", "128"
|
||||
};
|
||||
|
||||
// The 13 entries
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, "512");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_IMAGE_HEIGHT, TIFF.TYPE_SHORT, "384");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_BITS_PER_SAMPLE, TIFF.TYPE_SHORT, "8", "8", "8");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_COMPRESSION, TIFF.TYPE_SHORT, "5");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, "2");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_STRIP_OFFSETS, TIFF.TYPE_LONG, stripOffsets);
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_SAMPLES_PER_PIXEL, TIFF.TYPE_SHORT, "3");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_ROWS_PER_STRIP, TIFF.TYPE_LONG, "5");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_STRIP_BYTE_COUNTS, TIFF.TYPE_LONG, stripByteCounts);
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_PLANAR_CONFIGURATION, TIFF.TYPE_SHORT, "1");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_X_POSITION, TIFF.TYPE_RATIONAL, "0");
|
||||
assertSingleNodeWithValue(entries, TIFF.TAG_Y_POSITION, TIFF.TYPE_RATIONAL, "0");
|
||||
assertSingleNodeWithValue(entries, 32995, TIFF.TYPE_SHORT, "0"); // Matteing tag, obsoleted by ExtraSamples tag in TIFF 6.0
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTreeDetached() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
Node nativeTree = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
|
||||
assertNotNull(nativeTree);
|
||||
|
||||
Node nativeTree2 = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
|
||||
assertNotNull(nativeTree2);
|
||||
|
||||
assertNotSame(nativeTree, nativeTree2);
|
||||
assertNodeEquals("Unmodified trees differs", nativeTree, nativeTree2); // Both not modified
|
||||
|
||||
// Modify one of the trees
|
||||
Node ifdNode = nativeTree2.getFirstChild();
|
||||
ifdNode.removeChild(ifdNode.getFirstChild());
|
||||
IIOMetadataNode tiffField = new IIOMetadataNode("TIFFField");
|
||||
ifdNode.appendChild(tiffField);
|
||||
|
||||
assertNodeNotEquals("Modified tree does not differ", nativeTree, nativeTree2);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeTree() throws IOException {
|
||||
TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
|
||||
Node nativeTree = metadata.getAsTree(nativeFormat);
|
||||
assertNotNull(nativeTree);
|
||||
|
||||
IIOMetadataNode newTree = new IIOMetadataNode("com_sun_media_imageio_plugins_tiff_image_1.0");
|
||||
IIOMetadataNode ifdNode = new IIOMetadataNode("TIFFIFD");
|
||||
newTree.appendChild(ifdNode);
|
||||
|
||||
createTIFFFieldNode(ifdNode, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_DPI);
|
||||
createTIFFFieldNode(ifdNode, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, new Rational(300));
|
||||
createTIFFFieldNode(ifdNode, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, new Rational(30001, 100));
|
||||
|
||||
metadata.mergeTree(nativeFormat, newTree);
|
||||
|
||||
Directory ifd = metadata.getIFD();
|
||||
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION));
|
||||
assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue());
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION));
|
||||
assertEquals(new Rational(30001, 100), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue());
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT));
|
||||
assertEquals(TIFFBaseline.RESOLUTION_UNIT_DPI, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue());
|
||||
|
||||
Node mergedTree = metadata.getAsTree(nativeFormat);
|
||||
NodeList fields = mergedTree.getFirstChild().getChildNodes();
|
||||
|
||||
// Validate there's one and only one resolution unit, x res and y res
|
||||
// Validate resolution unit == 1, x res & y res
|
||||
assertSingleNodeWithValue(fields, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, String.valueOf(TIFFBaseline.RESOLUTION_UNIT_DPI));
|
||||
assertSingleNodeWithValue(fields, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, "300");
|
||||
assertSingleNodeWithValue(fields, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, "30001/100");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeTreeStandardFormat() throws IOException {
|
||||
TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/zackthecat.tif");
|
||||
|
||||
String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
|
||||
|
||||
Node standardTree = metadata.getAsTree(standardFormat);
|
||||
assertNotNull(standardTree);
|
||||
|
||||
IIOMetadataNode newTree = new IIOMetadataNode(standardFormat);
|
||||
IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension");
|
||||
newTree.appendChild(dimensionNode);
|
||||
|
||||
IIOMetadataNode horizontalPixelSize = new IIOMetadataNode("HorizontalPixelSize");
|
||||
dimensionNode.appendChild(horizontalPixelSize);
|
||||
horizontalPixelSize.setAttribute("value", String.valueOf(300 / 25.4));
|
||||
|
||||
IIOMetadataNode verticalPixelSize = new IIOMetadataNode("VerticalPixelSize");
|
||||
dimensionNode.appendChild(verticalPixelSize);
|
||||
verticalPixelSize.setAttribute("value", String.valueOf(300 / 25.4));
|
||||
|
||||
metadata.mergeTree(standardFormat, newTree);
|
||||
|
||||
Directory ifd = metadata.getIFD();
|
||||
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION));
|
||||
assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue());
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION));
|
||||
assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue());
|
||||
|
||||
// Should keep DPI as unit
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT));
|
||||
assertEquals(TIFFBaseline.RESOLUTION_UNIT_DPI, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeTreeStandardFormatAspectOnly() throws IOException {
|
||||
TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
|
||||
|
||||
Node standardTree = metadata.getAsTree(standardFormat);
|
||||
assertNotNull(standardTree);
|
||||
|
||||
IIOMetadataNode newTree = new IIOMetadataNode(standardFormat);
|
||||
IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension");
|
||||
newTree.appendChild(dimensionNode);
|
||||
|
||||
IIOMetadataNode aspectRatio = new IIOMetadataNode("PixelAspectRatio");
|
||||
dimensionNode.appendChild(aspectRatio);
|
||||
aspectRatio.setAttribute("value", String.valueOf(3f / 2f));
|
||||
|
||||
metadata.mergeTree(standardFormat, newTree);
|
||||
|
||||
Directory ifd = metadata.getIFD();
|
||||
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION));
|
||||
assertEquals(new Rational(3, 2), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue());
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION));
|
||||
assertEquals(new Rational(1), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue());
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT));
|
||||
assertEquals(TIFFBaseline.RESOLUTION_UNIT_NONE, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testMergeTreeUnsupportedFormat() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = "com_foo_bar_tiff_42";
|
||||
metadata.mergeTree(nativeFormat, new IIOMetadataNode(nativeFormat));
|
||||
}
|
||||
|
||||
@Test(expected = IIOInvalidTreeException.class)
|
||||
public void testMergeTreeFormatMisMatch() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
metadata.mergeTree(nativeFormat, new IIOMetadataNode("com_foo_bar_tiff_42"));
|
||||
}
|
||||
|
||||
@Test(expected = IIOInvalidTreeException.class)
|
||||
public void testMergeTreeInvalid() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
metadata.mergeTree(nativeFormat, new IIOMetadataNode(nativeFormat)); // Requires at least one child node
|
||||
}
|
||||
|
||||
// TODO: Test that failed merge leaves metadata unchanged
|
||||
|
||||
@Test
|
||||
public void testSetFromTreeEmpty() throws IOException {
|
||||
// Read from file, set empty to see that all is cleared
|
||||
TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
IIOMetadataNode root = new IIOMetadataNode(nativeFormat);
|
||||
root.appendChild(new IIOMetadataNode("TIFFIFD"));
|
||||
|
||||
metadata.setFromTree(nativeFormat, root);
|
||||
|
||||
Directory ifd = metadata.getIFD();
|
||||
assertNotNull(ifd);
|
||||
assertEquals(0, ifd.size());
|
||||
|
||||
Node tree = metadata.getAsTree(nativeFormat);
|
||||
|
||||
assertNotNull(tree);
|
||||
assertNotNull(tree.getFirstChild());
|
||||
assertEquals(1, tree.getChildNodes().getLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetFromTree() throws IOException {
|
||||
String softwareString = "12M UberTIFF 1.0";
|
||||
|
||||
TIFFImageMetadata metadata = new TIFFImageMetadata(Collections.<Entry>emptySet());
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
IIOMetadataNode root = new IIOMetadataNode(nativeFormat);
|
||||
|
||||
IIOMetadataNode ifdNode = new IIOMetadataNode("TIFFIFD");
|
||||
root.appendChild(ifdNode);
|
||||
|
||||
createTIFFFieldNode(ifdNode, TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, softwareString);
|
||||
|
||||
metadata.setFromTree(nativeFormat, root);
|
||||
|
||||
Directory ifd = metadata.getIFD();
|
||||
assertNotNull(ifd);
|
||||
assertEquals(1, ifd.size());
|
||||
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_SOFTWARE));
|
||||
assertEquals(softwareString, ifd.getEntryById(TIFF.TAG_SOFTWARE).getValue());
|
||||
|
||||
Node tree = metadata.getAsTree(nativeFormat);
|
||||
|
||||
assertNotNull(tree);
|
||||
assertNotNull(tree.getFirstChild());
|
||||
assertEquals(1, tree.getChildNodes().getLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetFromTreeStandardFormat() throws IOException {
|
||||
String softwareString = "12M UberTIFF 1.0";
|
||||
String copyrightString = "Copyright (C) TwelveMonkeys, 2015";
|
||||
|
||||
TIFFImageMetadata metadata = new TIFFImageMetadata(Collections.<Entry>emptySet());
|
||||
|
||||
String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
|
||||
IIOMetadataNode root = new IIOMetadataNode(standardFormat);
|
||||
|
||||
IIOMetadataNode textNode = new IIOMetadataNode("Text");
|
||||
root.appendChild(textNode);
|
||||
|
||||
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
|
||||
textNode.appendChild(textEntry);
|
||||
|
||||
textEntry.setAttribute("keyword", "SOFTWARE"); // Spelling should not matter
|
||||
textEntry.setAttribute("value", softwareString);
|
||||
|
||||
textEntry = new IIOMetadataNode("TextEntry");
|
||||
textNode.appendChild(textEntry);
|
||||
|
||||
textEntry.setAttribute("keyword", "copyright"); // Spelling should not matter
|
||||
textEntry.setAttribute("value", copyrightString);
|
||||
|
||||
metadata.setFromTree(standardFormat, root);
|
||||
|
||||
Directory ifd = metadata.getIFD();
|
||||
assertNotNull(ifd);
|
||||
assertEquals(2, ifd.size());
|
||||
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_SOFTWARE));
|
||||
assertEquals(softwareString, ifd.getEntryById(TIFF.TAG_SOFTWARE).getValue());
|
||||
|
||||
assertNotNull(ifd.getEntryById(TIFF.TAG_COPYRIGHT));
|
||||
assertEquals(copyrightString, ifd.getEntryById(TIFF.TAG_COPYRIGHT).getValue());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testSetFromTreeUnsupportedFormat() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = "com_foo_bar_tiff_42";
|
||||
metadata.setFromTree(nativeFormat, new IIOMetadataNode(nativeFormat));
|
||||
}
|
||||
|
||||
@Test(expected = IIOInvalidTreeException.class)
|
||||
public void testSetFromTreeFormatMisMatch() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
metadata.setFromTree(nativeFormat, new IIOMetadataNode("com_foo_bar_tiff_42"));
|
||||
}
|
||||
|
||||
@Test(expected = IIOInvalidTreeException.class)
|
||||
public void testSetFromTreeInvalid() throws IOException {
|
||||
IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif");
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
metadata.setFromTree(nativeFormat, new IIOMetadataNode(nativeFormat)); // Requires at least one child node
|
||||
}
|
||||
|
||||
private void assertSingleNodeWithValue(final NodeList fields, final int tag, int type, final String... expectedValue) {
|
||||
String tagNumber = String.valueOf(tag);
|
||||
String typeName = StringUtil.capitalize(TIFF.TYPE_NAMES[type].toLowerCase());
|
||||
|
||||
boolean foundTag = false;
|
||||
|
||||
for (int i = 0; i < fields.getLength(); i++) {
|
||||
Element field = (Element) fields.item(i);
|
||||
|
||||
if (tagNumber.equals(field.getAttribute("number"))) {
|
||||
assertFalse("Duplicate tag " + tagNumber + " found", foundTag);
|
||||
|
||||
assertEquals(1, field.getChildNodes().getLength());
|
||||
Node containerNode = field.getFirstChild();
|
||||
assertEquals("TIFF" + typeName + "s", containerNode.getNodeName());
|
||||
|
||||
NodeList valueNodes = containerNode.getChildNodes();
|
||||
assertEquals("Unexpected number of values for tag " + tagNumber, expectedValue.length, valueNodes.getLength());
|
||||
|
||||
for (int j = 0; j < expectedValue.length; j++) {
|
||||
Element valueNode = (Element) valueNodes.item(j);
|
||||
assertEquals("TIFF" + typeName, valueNode.getNodeName());
|
||||
assertEquals("Unexpected tag " + tagNumber + " value", expectedValue[j], valueNode.getAttribute("value"));
|
||||
}
|
||||
|
||||
foundTag = true;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue("No tag " + tagNumber + " found", foundTag);
|
||||
}
|
||||
|
||||
// TODO: Test that failed set leaves metadata unchanged
|
||||
|
||||
static void createTIFFFieldNode(final IIOMetadataNode parentIFDNode, int tag, short type, Object value) {
|
||||
IIOMetadataNode fieldNode = new IIOMetadataNode("TIFFField");
|
||||
parentIFDNode.appendChild(fieldNode);
|
||||
|
||||
fieldNode.setAttribute("number", String.valueOf(tag));
|
||||
|
||||
switch (type) {
|
||||
case TIFF.TYPE_ASCII:
|
||||
createTIFFFieldContainerNode(fieldNode, "Ascii", value);
|
||||
break;
|
||||
case TIFF.TYPE_BYTE:
|
||||
createTIFFFieldContainerNode(fieldNode, "Byte", value);
|
||||
break;
|
||||
case TIFF.TYPE_SHORT:
|
||||
createTIFFFieldContainerNode(fieldNode, "Short", value);
|
||||
break;
|
||||
case TIFF.TYPE_RATIONAL:
|
||||
createTIFFFieldContainerNode(fieldNode, "Rational", value);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
static void createTIFFFieldContainerNode(final IIOMetadataNode fieldNode, final String type, final Object value) {
|
||||
IIOMetadataNode containerNode = new IIOMetadataNode("TIFF" + type + "s");
|
||||
fieldNode.appendChild(containerNode);
|
||||
|
||||
IIOMetadataNode valueNode = new IIOMetadataNode("TIFF" + type);
|
||||
valueNode.setAttribute("value", String.valueOf(value));
|
||||
containerNode.appendChild(valueNode);
|
||||
}
|
||||
|
||||
private void assertNodeNotEquals(final String message, final Node expected, final Node actual) {
|
||||
// Lame, lazy implementation...
|
||||
try {
|
||||
assertNodeEquals(message, expected, actual);
|
||||
}
|
||||
catch (AssertionError ignore) {
|
||||
return;
|
||||
}
|
||||
|
||||
fail(message);
|
||||
}
|
||||
|
||||
private void assertNodeEquals(final String message, final Node expected, final Node actual) {
|
||||
assertEquals(message + " class differs", expected.getClass(), actual.getClass());
|
||||
assertEquals(message, expected.getNodeValue(), actual.getNodeValue());
|
||||
|
||||
if (expected instanceof IIOMetadataNode) {
|
||||
IIOMetadataNode expectedIIO = (IIOMetadataNode) expected;
|
||||
IIOMetadataNode actualIIO = (IIOMetadataNode) actual;
|
||||
|
||||
assertEquals(message, expectedIIO.getUserObject(), actualIIO.getUserObject());
|
||||
}
|
||||
|
||||
NodeList expectedChildNodes = expected.getChildNodes();
|
||||
NodeList actualChildNodes = actual.getChildNodes();
|
||||
|
||||
assertEquals(message + " child length differs: " + toString(expectedChildNodes) + " != " + toString(actualChildNodes),
|
||||
expectedChildNodes.getLength(), actualChildNodes.getLength());
|
||||
|
||||
for (int i = 0; i < expectedChildNodes.getLength(); i++) {
|
||||
Node expectedChild = expectedChildNodes.item(i);
|
||||
Node actualChild = actualChildNodes.item(i);
|
||||
|
||||
assertEquals(message + " node name differs", expectedChild.getLocalName(), actualChild.getLocalName());
|
||||
assertNodeEquals(message + "/" + expectedChild.getLocalName(), expectedChild, actualChild);
|
||||
}
|
||||
}
|
||||
|
||||
private String toString(final NodeList list) {
|
||||
if (list.getLength() == 0) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int i = 0; i < list.getLength(); i++) {
|
||||
if (i > 0) {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
Node node = list.item(i);
|
||||
builder.append(node.getLocalName());
|
||||
}
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,22 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
||||
new TestData(getClassLoaderResource("/tiff/lzw-long-strings-sample.tif"), new Dimension(316, 173)), // RGBA, LZW compressed w/predictor
|
||||
new TestData(getClassLoaderResource("/tiff/part.tif"), new Dimension(50, 50)), // Gray/BlackIsZero, uncompressed, striped signed int (SampleFormat 2)
|
||||
new TestData(getClassLoaderResource("/tiff/cmyk_jpeg_no_profile.tif"), new Dimension(150, 63)), // CMYK, JPEG compressed, no ICC profile
|
||||
new TestData(getClassLoaderResource("/tiff/cmyk_jpeg.tif"), new Dimension(100, 100)) // CMYK, JPEG compressed, with ICC profile
|
||||
new TestData(getClassLoaderResource("/tiff/cmyk_jpeg.tif"), new Dimension(100, 100)), // CMYK, JPEG compressed, with ICC profile
|
||||
new TestData(getClassLoaderResource("/tiff/grayscale-alpha.tiff"), new Dimension(248, 351)), // Gray + unassociated alpha
|
||||
new TestData(getClassLoaderResource("/tiff/signed-integral-8bit.tif"), new Dimension(439, 167)), // Gray, 8 bit *signed* integral
|
||||
new TestData(getClassLoaderResource("/tiff/floatingpoint-32bit.tif"), new Dimension(300, 100)), // RGB, 32 bit floating point
|
||||
new TestData(getClassLoaderResource("/tiff/general-cmm-error.tif"), new Dimension(1181, 860)), // RGB, LZW compression with broken/incompatible ICC profile
|
||||
new TestData(getClassLoaderResource("/tiff/lzw-rgba-padded-icc.tif"), new Dimension(19, 11)), // RGBA, LZW compression with padded ICC profile
|
||||
new TestData(getClassLoaderResource("/tiff/lzw-rgba-4444.tif"), new Dimension(64, 64)), // RGBA, LZW compression with UINT 4/4/4/4 + gray 2/2
|
||||
new TestData(getClassLoaderResource("/tiff/lzw-buffer-overflow.tif"), new Dimension(5, 49)), // RGBA, LZW compression, will throw IOOBE if small buffer
|
||||
// CCITT
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group3_1d.tif"), new Dimension(6, 4)), // B/W, CCITT T4 1D
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group3_1d_fill.tif"), new Dimension(6, 4)), // B/W, CCITT T4 1D
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group3_2d.tif"), new Dimension(6, 4)), // B/W, CCITT T4 2D
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group3_2d_fill.tif"), new Dimension(6, 4)), // B/W, CCITT T4 2D
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group3_2d_lsb2msb.tif"), new Dimension(6, 4)), // B/W, CCITT T4 2D, LSB
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group4.tif"), new Dimension(6, 4)), // B/W, CCITT T6 1D
|
||||
new TestData(getClassLoaderResource("/tiff/fivepages-scan-causingerrors.tif"), new Dimension(2480, 3518)) // B/W, CCITT T4
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,9 +156,8 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
||||
@Test
|
||||
public void testReadOldStyleJPEGGrayscale() throws IOException {
|
||||
TestData testData = new TestData(getClassLoaderResource("/tiff/grayscale-old-style-jpeg.tiff"), new Dimension(600, 600));
|
||||
ImageInputStream stream = testData.getInputStream();
|
||||
|
||||
try {
|
||||
try (ImageInputStream stream = testData.getInputStream()) {
|
||||
TIFFImageReader reader = createReader();
|
||||
reader.setInput(stream);
|
||||
BufferedImage image = reader.read(0);
|
||||
@@ -151,18 +165,13 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
||||
assertNotNull(image);
|
||||
assertEquals(testData.getDimension(0), new Dimension(image.getWidth(), image.getHeight()));
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadIncompatibleICCProfileIgnoredWithWarning() throws IOException {
|
||||
TestData testData = new TestData(getClassLoaderResource("/tiff/rgb-with-embedded-cmyk-icc.tif"), new Dimension(1500, 1500));
|
||||
|
||||
ImageInputStream stream = testData.getInputStream();
|
||||
|
||||
try {
|
||||
try (ImageInputStream stream = testData.getInputStream()) {
|
||||
TIFFImageReader reader = createReader();
|
||||
reader.setInput(stream);
|
||||
|
||||
@@ -175,18 +184,13 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
||||
assertEquals(testData.getDimension(0), new Dimension(image.getWidth(), image.getHeight()));
|
||||
verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), contains("ICC"));
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColorMap8Bit() throws IOException {
|
||||
TestData testData = new TestData(getClassLoaderResource("/tiff/scan-lzw-8bit-colormap.tiff"), new Dimension(2550, 3300));
|
||||
|
||||
ImageInputStream stream = testData.getInputStream();
|
||||
|
||||
try {
|
||||
try (ImageInputStream stream = testData.getInputStream()) {
|
||||
TIFFImageReader reader = createReader();
|
||||
reader.setInput(stream);
|
||||
|
||||
@@ -202,8 +206,26 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
||||
assertEquals(0xffffffff, image.getRGB(0, 0)); // The pixel at 0, 0 should be white, not black
|
||||
verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), contains("ColorMap"));
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadICCProfile() throws IOException {
|
||||
TestData testData = new TestData(getClassLoaderResource("/tiff/general-cmm-error.tif"), new Dimension(1181, 864));
|
||||
|
||||
try (ImageInputStream stream = testData.getInputStream()) {
|
||||
TIFFImageReader reader = createReader();
|
||||
reader.setInput(stream);
|
||||
|
||||
IIOReadWarningListener warningListener = mock(IIOReadWarningListener.class);
|
||||
reader.addIIOReadWarningListener(warningListener);
|
||||
|
||||
ImageReadParam param = reader.getDefaultReadParam();
|
||||
param.setSourceRegion(new Rectangle(8, 8));
|
||||
BufferedImage image = reader.read(0, param);
|
||||
|
||||
assertNotNull(image);
|
||||
assertEquals(new Dimension(8, 8), new Dimension(image.getWidth(), image.getHeight()));
|
||||
verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), contains("ICC profile"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,15 +28,33 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.Rational;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.imageio.IIOImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import javax.imageio.ImageWriter;
|
||||
import java.awt.*;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataTest.createTIFFFieldNode;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* TIFFImageWriterTest
|
||||
*
|
||||
@@ -55,19 +73,208 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
|
||||
|
||||
@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);
|
||||
return Arrays.asList(
|
||||
new BufferedImage(300, 200, BufferedImage.TYPE_INT_RGB),
|
||||
new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB),
|
||||
new BufferedImage(300, 200, BufferedImage.TYPE_3BYTE_BGR),
|
||||
new BufferedImage(300, 200, BufferedImage.TYPE_4BYTE_ABGR),
|
||||
new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_GRAY),
|
||||
new BufferedImage(300, 200, BufferedImage.TYPE_USHORT_GRAY),
|
||||
// new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_BINARY), // TODO!
|
||||
new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_INDEXED)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Test write bilevel stays bilevel
|
||||
// TODO: Test write indexed stays indexed
|
||||
|
||||
@Test
|
||||
public void testWriteWithCustomResolutionNative() throws IOException {
|
||||
// Issue 139 Writing TIFF files with custom resolution value
|
||||
Rational resolutionValue = new Rational(1200);
|
||||
int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER;
|
||||
|
||||
RenderedImage image = getTestData(0);
|
||||
|
||||
ImageWriter writer = createImageWriter();
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
|
||||
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
|
||||
writer.setOutput(stream);
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);
|
||||
|
||||
IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat);
|
||||
|
||||
IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD");
|
||||
customMeta.appendChild(ifd);
|
||||
|
||||
createTIFFFieldNode(ifd, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resolutionUnitValue);
|
||||
createTIFFFieldNode(ifd, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue);
|
||||
createTIFFFieldNode(ifd, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue);
|
||||
|
||||
metadata.mergeTree(nativeFormat, customMeta);
|
||||
|
||||
writer.write(null, new IIOImage(image, null, metadata), null);
|
||||
}
|
||||
finally {
|
||||
graphics.dispose();
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
return Arrays.asList(image);
|
||||
assertTrue("No image data written", buffer.size() > 0);
|
||||
|
||||
Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));
|
||||
|
||||
Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT);
|
||||
assertNotNull(resolutionUnit);
|
||||
assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue());
|
||||
|
||||
Entry xResolution = ifds.getEntryById(TIFF.TAG_X_RESOLUTION);
|
||||
assertNotNull(xResolution);
|
||||
assertEquals(resolutionValue, xResolution.getValue());
|
||||
|
||||
Entry yResolution = ifds.getEntryById(TIFF.TAG_Y_RESOLUTION);
|
||||
assertNotNull(yResolution);
|
||||
assertEquals(resolutionValue, yResolution.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteWithCustomSoftwareNative() throws IOException {
|
||||
String softwareString = "12M TIFF Test 1.0 (build $foo$)";
|
||||
|
||||
RenderedImage image = getTestData(0);
|
||||
|
||||
ImageWriter writer = createImageWriter();
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
|
||||
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
|
||||
writer.setOutput(stream);
|
||||
|
||||
String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
|
||||
IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);
|
||||
|
||||
IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat);
|
||||
|
||||
IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD");
|
||||
customMeta.appendChild(ifd);
|
||||
|
||||
createTIFFFieldNode(ifd, TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, softwareString);
|
||||
|
||||
metadata.mergeTree(nativeFormat, customMeta);
|
||||
|
||||
writer.write(null, new IIOImage(image, null, metadata), null);
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
assertTrue("No image data written", buffer.size() > 0);
|
||||
|
||||
Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));
|
||||
Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE);
|
||||
assertNotNull(software);
|
||||
assertEquals(softwareString, software.getValueAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteWithCustomResolutionStandard() throws IOException {
|
||||
// Issue 139 Writing TIFF files with custom resolution value
|
||||
double resolutionValue = 300 / 25.4; // 300 dpi, 1 inch = 2.54 cm or 25.4 mm
|
||||
int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER;
|
||||
Rational expectedResolutionValue = new Rational(Math.round(resolutionValue * 10 * TIFFImageMetadata.RATIONAL_SCALE_FACTOR), TIFFImageMetadata.RATIONAL_SCALE_FACTOR);
|
||||
|
||||
RenderedImage image = getTestData(0);
|
||||
|
||||
ImageWriter writer = createImageWriter();
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
|
||||
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
|
||||
writer.setOutput(stream);
|
||||
|
||||
String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
|
||||
IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);
|
||||
|
||||
IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat);
|
||||
|
||||
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
|
||||
customMeta.appendChild(dimension);
|
||||
|
||||
IIOMetadataNode xSize = new IIOMetadataNode("HorizontalPixelSize");
|
||||
dimension.appendChild(xSize);
|
||||
xSize.setAttribute("value", String.valueOf(resolutionValue));
|
||||
|
||||
IIOMetadataNode ySize = new IIOMetadataNode("VerticalPixelSize");
|
||||
dimension.appendChild(ySize);
|
||||
ySize.setAttribute("value", String.valueOf(resolutionValue));
|
||||
|
||||
metadata.mergeTree(standardFormat, customMeta);
|
||||
|
||||
writer.write(null, new IIOImage(image, null, metadata), null);
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
assertTrue("No image data written", buffer.size() > 0);
|
||||
|
||||
Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));
|
||||
|
||||
Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT);
|
||||
assertNotNull(resolutionUnit);
|
||||
assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue());
|
||||
|
||||
Entry xResolution = ifds.getEntryById(TIFF.TAG_X_RESOLUTION);
|
||||
assertNotNull(xResolution);
|
||||
assertEquals(expectedResolutionValue, xResolution.getValue());
|
||||
|
||||
Entry yResolution = ifds.getEntryById(TIFF.TAG_Y_RESOLUTION);
|
||||
assertNotNull(yResolution);
|
||||
assertEquals(expectedResolutionValue, yResolution.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteWithCustomSoftwareStandard() throws IOException {
|
||||
String softwareString = "12M TIFF Test 1.0 (build $foo$)";
|
||||
|
||||
RenderedImage image = getTestData(0);
|
||||
|
||||
ImageWriter writer = createImageWriter();
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
|
||||
try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
|
||||
writer.setOutput(stream);
|
||||
|
||||
String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
|
||||
IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);
|
||||
|
||||
IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat);
|
||||
|
||||
IIOMetadataNode dimension = new IIOMetadataNode("Text");
|
||||
customMeta.appendChild(dimension);
|
||||
|
||||
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
|
||||
dimension.appendChild(textEntry);
|
||||
textEntry.setAttribute("keyword", "Software");
|
||||
textEntry.setAttribute("value", softwareString);
|
||||
|
||||
metadata.mergeTree(standardFormat, customMeta);
|
||||
|
||||
writer.write(null, new IIOImage(image, null, metadata), null);
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
assertTrue("No image data written", buffer.size() > 0);
|
||||
|
||||
Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));
|
||||
Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE);
|
||||
assertNotNull(software);
|
||||
assertEquals(softwareString, software.getValueAsString());
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-4444.tif
Normal file
BIN
imageio/imageio-tiff/src/test/resources/tiff/lzw-rgba-4444.tif
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,10 +26,12 @@
|
||||
<!-- Support -->
|
||||
<module>imageio-core</module>
|
||||
<module>imageio-metadata</module>
|
||||
<!--<module>imageio-stream</module>-->
|
||||
<module>imageio-clippath</module>
|
||||
|
||||
<!-- Stand-alone readers/writers -->
|
||||
<module>imageio-bmp</module>
|
||||
<module>imageio-hdr</module>
|
||||
<module>imageio-icns</module>
|
||||
<module>imageio-iff</module>
|
||||
<module>imageio-jpeg</module>
|
||||
@@ -75,14 +77,14 @@
|
||||
<groupId>com.twelvemonkeys.common</groupId>
|
||||
<artifactId>common-lang</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.common</groupId>
|
||||
<artifactId>common-io</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -137,7 +139,7 @@
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
Reference in New Issue
Block a user