mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-03-20 00:00:03 -04:00
Merge branch 'master' of https://github.com/haraldk/TwelveMonkeys
Conflicts: sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java
This commit is contained in:
@@ -526,10 +526,11 @@ public class SVGImageReader extends ImageReaderBase {
|
||||
processImageProgress(99f);
|
||||
|
||||
return dest;
|
||||
//writeImage(dest, output);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new TranscoderException(ex.getMessage(), ex);
|
||||
TranscoderException exception = new TranscoderException(ex.getMessage());
|
||||
exception.initCause(ex);
|
||||
throw exception;
|
||||
}
|
||||
finally {
|
||||
if (mContext != null) {
|
||||
|
||||
@@ -65,7 +65,7 @@ public class SVGImageReaderSpi extends ImageReaderSpi {
|
||||
SVG_READER_AVAILABLE ? new String[]{"svg"} : null, // Suffixes
|
||||
SVG_READER_AVAILABLE ? new String[]{"image/svg", "image/x-svg", "image/svg+xml", "image/svg-xml"} : null, // Mime-types
|
||||
"com.twelvemonkeys.imageio.plugins.svg.SVGImageReader", // Reader class name
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types
|
||||
new Class[] {ImageInputStream.class}, // Input types
|
||||
null, // Writer SPI names
|
||||
true, // Supports standard stream metadata format
|
||||
null, // Native stream metadata format name
|
||||
|
||||
@@ -65,8 +65,8 @@ public class WMFImageReaderSpi extends ImageReaderSpi {
|
||||
WMF_READER_AVAILABLE ? new String[]{"wmf", "WMF"} : new String[]{""}, // Names
|
||||
WMF_READER_AVAILABLE ? new String[]{"wmf", "emf"} : null, // Suffixes
|
||||
WMF_READER_AVAILABLE ? new String[]{"application/x-msmetafile", "image/x-wmf"} : null, // Mime-types
|
||||
WMFImageReader.class.getName(), // Reader class name..?
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types
|
||||
"com.twelvemonkeys.imageio.plugins.wmf.WMFImageReader", // Reader class name..?
|
||||
new Class[] {ImageInputStream.class}, // Input types
|
||||
null, // Writer SPI names
|
||||
true, // Supports standard stream metadata format
|
||||
null, // Native stream metadata format name
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
|
||||
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import java.awt.*;
|
||||
import java.awt.image.ImagingOpException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -75,4 +76,27 @@ public class SVGImageReaderTestCase extends ImageReaderAbstractTestCase<SVGImage
|
||||
protected List<String> getMIMETypes() {
|
||||
return Arrays.asList("image/svg+xml");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testReadWithSizeParam() {
|
||||
try {
|
||||
super.testReadWithSizeParam();
|
||||
}
|
||||
catch (AssertionError failure) {
|
||||
Throwable cause = failure;
|
||||
|
||||
while (cause.getCause() != null) {
|
||||
cause = cause.getCause();
|
||||
}
|
||||
|
||||
if (cause instanceof ImagingOpException && cause.getMessage().equals("Unable to transform src image")) {
|
||||
// This is a very strange regression introduced by the later JDK/JRE (at least it's in 7u45)
|
||||
// Haven't found a workaround yet
|
||||
System.err.println("WARNING: Oracle JRE 7u45 broke my SVGImageReader (known issue): " + cause.getMessage());
|
||||
}
|
||||
else {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ public abstract class ImageReaderBase extends ImageReader {
|
||||
|
||||
// If param is non-null, use it
|
||||
if (param != null) {
|
||||
// Try to get the explicit destinaton image
|
||||
// Try to get the explicit destination image
|
||||
BufferedImage dest = param.getDestination();
|
||||
|
||||
if (dest != null) {
|
||||
|
||||
@@ -86,6 +86,9 @@ public final class ColorSpaces {
|
||||
/** A best-effort "generic" CMYK color space. Either read from disk or built-in. */
|
||||
public static final int CS_GENERIC_CMYK = 5001;
|
||||
|
||||
/** Value used instead of 'XYZ ' in problematic Corbis RGB Profiles */
|
||||
private static final byte[] CORBIS_RGB_ALTERNATE_XYZ = new byte[] {0x17, (byte) 0xA5, 0x05, (byte) 0xB8};
|
||||
|
||||
// Weak references to hold the color spaces while cached
|
||||
private static WeakReference<ICC_Profile> adobeRGB1998 = new WeakReference<ICC_Profile>(null);
|
||||
private static WeakReference<ICC_Profile> genericCMYK = new WeakReference<ICC_Profile>(null);
|
||||
@@ -135,9 +138,42 @@ public final class ColorSpaces {
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling to detect problematic Corbis RGB ICC Profile.
|
||||
// This makes sure tags that are expected to be of type 'XYZ ' really have this expected type.
|
||||
// Should leave other ICC profiles unchanged.
|
||||
if (fixProfileXYZTag(profile, ICC_Profile.icSigMediaWhitePointTag)) {
|
||||
fixProfileXYZTag(profile, ICC_Profile.icSigRedColorantTag);
|
||||
fixProfileXYZTag(profile, ICC_Profile.icSigGreenColorantTag);
|
||||
fixProfileXYZTag(profile, ICC_Profile.icSigBlueColorantTag);
|
||||
}
|
||||
|
||||
return getCachedOrCreateCS(profile, profileHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes problematic 'XYZ ' tags in Corbis RGB profile.
|
||||
*
|
||||
* @return {@code true} if found and fixed, otherwise {@code false} for short-circuiting
|
||||
* to avoid unnecessary array copying.
|
||||
*/
|
||||
private static boolean fixProfileXYZTag(ICC_Profile profile, final int tagSignature) {
|
||||
byte[] data = profile.getData(tagSignature);
|
||||
|
||||
// The CMM expects 0x64 65 73 63 ('XYZ ') but is 0x17 A5 05 B8..?
|
||||
if (data != null && Arrays.equals(Arrays.copyOfRange(data, 0, 4), CORBIS_RGB_ALTERNATE_XYZ)) {
|
||||
data[0] = 'X';
|
||||
data[1] = 'Y';
|
||||
data[2] = 'Z';
|
||||
data[3] = ' ';
|
||||
|
||||
profile.setData(tagSignature, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ICC_ColorSpace getInternalCS(final int profileCSType, final byte[] profileHeader) {
|
||||
if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, sRGB.header)) {
|
||||
return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_sRGB);
|
||||
@@ -209,6 +245,7 @@ public final class ColorSpaces {
|
||||
// being 1 (01000000) - "Media Relative Colormetric" in the offending profiles,
|
||||
// and 0 (00000000) - "Perceptual" in the good profiles
|
||||
// (that is 1 single bit of difference right there.. ;-)
|
||||
// See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7064516
|
||||
|
||||
// This is particularly annoying, as the byte copying isn't really necessary,
|
||||
// except the getRenderingIntent method is package protected in java.awt.color
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.twelvemonkeys.imageio.stream;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.ImageInputStreamImpl;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
|
||||
/**
|
||||
* A buffered {@code ImageInputStream}.
|
||||
@@ -20,61 +21,67 @@ import java.io.IOException;
|
||||
// TODO: Create a provider for this (wrapping the FileIIS and FileCacheIIS classes), and disable the Sun built-in spis?
|
||||
// TODO: Test on other platforms, might be just an OS X issue
|
||||
public final class BufferedImageInputStream extends ImageInputStreamImpl implements ImageInputStream {
|
||||
|
||||
static final int DEFAULT_BUFFER_SIZE = 8192;
|
||||
|
||||
private ImageInputStream stream;
|
||||
|
||||
private byte[] buffer;
|
||||
private long bufferStart = 0;
|
||||
private int bufferPos = 0;
|
||||
private int bufferLength = 0;
|
||||
private ByteBuffer buffer;
|
||||
|
||||
public BufferedImageInputStream(final ImageInputStream pStream) throws IOException {
|
||||
this(pStream, DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
private BufferedImageInputStream(final ImageInputStream pStream, final int pBufferSize) throws IOException {
|
||||
Validate.notNull(pStream, "stream");
|
||||
|
||||
stream = pStream;
|
||||
stream = notNull(pStream, "stream");
|
||||
streamPos = pStream.getStreamPosition();
|
||||
buffer = new byte[pBufferSize];
|
||||
buffer = ByteBuffer.allocate(pBufferSize);
|
||||
buffer.limit(0);
|
||||
}
|
||||
|
||||
private void fillBuffer() throws IOException {
|
||||
bufferStart = streamPos;
|
||||
bufferLength = stream.read(buffer, 0, buffer.length);
|
||||
bufferPos = 0;
|
||||
buffer.clear();
|
||||
|
||||
int length = stream.read(buffer.array(), 0, buffer.capacity());
|
||||
|
||||
if (length >= 0) {
|
||||
try {
|
||||
buffer.position(length);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
System.err.println("length: " + length);
|
||||
throw e;
|
||||
}
|
||||
buffer.flip();
|
||||
}
|
||||
else {
|
||||
buffer.limit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBufferValid() throws IOException {
|
||||
return bufferPos < bufferLength && bufferStart == stream.getStreamPosition() - bufferLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (!isBufferValid()) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
fillBuffer();
|
||||
}
|
||||
|
||||
if (bufferLength <= 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bitOffset = 0;
|
||||
streamPos++;
|
||||
|
||||
return buffer[bufferPos++] & 0xff;
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException {
|
||||
bitOffset = 0;
|
||||
|
||||
if (!isBufferValid()) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
// Bypass cache if cache is empty for reads longer than buffer
|
||||
if (pLength >= buffer.length) {
|
||||
if (pLength >= buffer.capacity()) {
|
||||
return readDirect(pBuffer, pOffset, pLength);
|
||||
}
|
||||
else {
|
||||
@@ -87,30 +94,29 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
|
||||
|
||||
private int readDirect(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException {
|
||||
// TODO: Figure out why reading more than the buffer length causes alignment issues...
|
||||
int read = stream.read(pBuffer, pOffset, Math.min(buffer.length, pLength));
|
||||
int read = stream.read(pBuffer, pOffset, Math.min(buffer.capacity(), pLength));
|
||||
|
||||
if (read > 0) {
|
||||
streamPos += read;
|
||||
}
|
||||
|
||||
bufferStart = stream.getStreamPosition();
|
||||
bufferLength = 0;
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
|
||||
private int readBuffered(final byte[] pBuffer, final int pOffset, final int pLength) {
|
||||
if (bufferLength <= 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read as much as possible from buffer
|
||||
int length = Math.min(bufferLength - bufferPos, pLength);
|
||||
int length = Math.min(buffer.remaining(), pLength);
|
||||
|
||||
if (length > 0) {
|
||||
System.arraycopy(buffer, bufferPos, pBuffer, pOffset, length);
|
||||
bufferPos += length;
|
||||
int position = buffer.position();
|
||||
System.arraycopy(buffer.array(), position, pBuffer, pOffset, length);
|
||||
buffer.position(position + length);
|
||||
|
||||
}
|
||||
|
||||
streamPos += length;
|
||||
@@ -122,7 +128,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
|
||||
public void seek(long pPosition) throws IOException {
|
||||
// TODO: Could probably be optimized to not invalidate buffer if new position is within current buffer
|
||||
stream.seek(pPosition);
|
||||
bufferLength = 0; // Will invalidate buffer
|
||||
buffer.limit(0); // Will invalidate buffer
|
||||
streamPos = stream.getStreamPosition();
|
||||
}
|
||||
|
||||
@@ -158,6 +164,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
|
||||
stream = null;
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.twelvemonkeys.imageio.stream;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.stream.ImageInputStreamImpl;
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
|
||||
/**
|
||||
* Experimental
|
||||
*
|
||||
@@ -22,13 +23,13 @@ public final class ByteArrayImageInputStream extends ImageInputStreamImpl {
|
||||
}
|
||||
|
||||
public ByteArrayImageInputStream(final byte[] pData, int offset, int length) {
|
||||
Validate.notNull(pData, "data");
|
||||
Validate.isTrue(offset >= 0 && offset <= pData.length, offset, "offset out of range: %d");
|
||||
Validate.isTrue(length >= 0 && length <= pData.length - offset, length, "length out of range: %d");
|
||||
data = notNull(pData, "data");
|
||||
dataOffset = isBetween(0, pData.length, offset, "offset");
|
||||
dataLength = isBetween(0, pData.length - offset, length, "length");
|
||||
}
|
||||
|
||||
data = pData;
|
||||
dataOffset = offset;
|
||||
dataLength = length;
|
||||
private static int isBetween(final int low, final int high, final int value, final String name) {
|
||||
return isTrue(value >= low && value <= high, value, String.format("%s out of range [%d, %d]: %d", name, low, high, value));
|
||||
}
|
||||
|
||||
public int read() throws IOException {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.junit.Test;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@@ -184,4 +185,15 @@ public class ColorSpacesTest {
|
||||
public void testIsCS_sRGBNull() {
|
||||
ColorSpaces.isCS_sRGB(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorbisRGBSpecialHandling() throws IOException {
|
||||
ICC_Profile corbisRGB = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/Corbis RGB.icc"));
|
||||
ICC_Profile corbisRGBFixed = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/Corbis RGB_fixed.icc"));
|
||||
|
||||
ICC_ColorSpace colorSpace = ColorSpaces.createColorSpace(corbisRGB);
|
||||
|
||||
assertNotNull(colorSpace);
|
||||
assertArrayEquals(colorSpace.getProfile().getData(), corbisRGBFixed.getData());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.util;
|
||||
|
||||
import com.twelvemonkeys.image.ImageUtil;
|
||||
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
@@ -45,9 +46,7 @@ import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.awt.image.SampleModel;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -75,29 +74,6 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
|
||||
protected abstract List<TestData> getTestData();
|
||||
|
||||
/**
|
||||
* Convenience method to get a list of test files from the classpath.
|
||||
* Currently only works for resources on the filesystem (not in jars or
|
||||
* archives).
|
||||
*
|
||||
* @param pResourceInFolder a resource in the correct classpath folder.
|
||||
* @return a list of files
|
||||
*/
|
||||
protected final List<File> getInputsFromClasspath(final String pResourceInFolder) {
|
||||
URL resource = getClass().getClassLoader().getResource(pResourceInFolder);
|
||||
assertNotNull(resource);
|
||||
File dir;
|
||||
try {
|
||||
dir = new File(resource.toURI()).getParentFile();
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
List<File> files = Arrays.asList(dir.listFiles());
|
||||
assertFalse(files.isEmpty());
|
||||
return files;
|
||||
}
|
||||
|
||||
protected abstract ImageReaderSpi createProvider();
|
||||
|
||||
protected abstract Class<T> getReaderClass();
|
||||
@@ -476,7 +452,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadWithSubsampleParam() {
|
||||
public void testReadWithSubsampleParamDimensions() {
|
||||
ImageReader reader = createReader();
|
||||
TestData data = getTestData().get(0);
|
||||
reader.setInput(data.getInputStream());
|
||||
@@ -493,8 +469,61 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
}
|
||||
|
||||
assertNotNull("Image was null!", image);
|
||||
assertEquals("Read image has wrong width: ", (double) data.getDimension(0).width / 5.0, image.getWidth(), 1.0);
|
||||
assertEquals("Read image has wrong height: ", (double) data.getDimension(0).height / 5.0, image.getHeight(), 1.0);
|
||||
assertEquals("Read image has wrong width: ", (data.getDimension(0).width + 4) / 5, image.getWidth());
|
||||
assertEquals("Read image has wrong height: ", (data.getDimension(0).height + 4) / 5, image.getHeight());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testReadWithSubsampleParamPixels() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
TestData data = getTestData().get(0);
|
||||
reader.setInput(data.getInputStream());
|
||||
|
||||
ImageReadParam param = reader.getDefaultReadParam();
|
||||
param.setSourceRegion(new Rectangle(Math.min(100, reader.getWidth(0)), Math.min(100, reader.getHeight(0))));
|
||||
|
||||
BufferedImage image = null;
|
||||
BufferedImage subsampled = null;
|
||||
try {
|
||||
image = reader.read(0, param);
|
||||
param.setSourceSubsampling(2, 2, 1, 1); // Hmm.. Seems to be the offset the fake version (ReplicateScaleFilter) uses
|
||||
|
||||
subsampled = reader.read(0, param);
|
||||
}
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
BufferedImage expected = ImageUtil.toBuffered(IIOUtil.fakeSubsampling(image, param));
|
||||
|
||||
// JPanel panel = new JPanel();
|
||||
// panel.add(new JLabel("Expected", new BufferedImageIcon(expected, 300, 300), JLabel.CENTER));
|
||||
// panel.add(new JLabel("Actual", new BufferedImageIcon(subsampled, 300, 300), JLabel.CENTER));
|
||||
// JOptionPane.showConfirmDialog(null, panel);
|
||||
|
||||
assertImageDataEquals("Subsampled image data does not match expected", expected, subsampled);
|
||||
}
|
||||
|
||||
protected final void assertImageDataEquals(String message, BufferedImage expected, BufferedImage actual) {
|
||||
assertNotNull("Expected image was null", expected);
|
||||
assertNotNull("Actual image was null!", actual);
|
||||
|
||||
if (expected == actual) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int y = 0; y < expected.getHeight(); y++) {
|
||||
for (int x = 0; x < expected.getWidth(); x++) {
|
||||
int expectedRGB = expected.getRGB(x, y);
|
||||
int actualRGB = actual.getRGB(x, y);
|
||||
|
||||
assertEquals(String.format("%s alpha at (%d, %d)", message, x, y), (expectedRGB >> 24) & 0xff, (actualRGB >> 24) & 0xff, 5);
|
||||
assertEquals(String.format("%s red at (%d, %d)", message, x, y), (expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, 5);
|
||||
assertEquals(String.format("%s green at (%d, %d)", message, x, y), (expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, 5);
|
||||
assertEquals(String.format("%s blue at (%d, %d)", message, x, y), expectedRGB & 0xff, actualRGB & 0xff, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -513,6 +542,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
assertNotNull("Image was null!", image);
|
||||
assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth());
|
||||
assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight());
|
||||
@@ -540,6 +570,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
assertNotNull("Image was null!", image);
|
||||
assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth());
|
||||
assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight());
|
||||
@@ -1291,13 +1322,14 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
// TODO: This is thrown by ImageReader.getDestination. But are we happy with that?
|
||||
// The problem is that the checkReadParamBandSettings throws IllegalArgumentException, which seems more appropriate...
|
||||
String message = expected.getMessage().toLowerCase();
|
||||
assertTrue(
|
||||
"Wrong message: " + message + " for type " + destination.getType(),
|
||||
message.contains("destination") ||
|
||||
((destination.getType() == BufferedImage.TYPE_BYTE_BINARY ||
|
||||
destination.getType() == BufferedImage.TYPE_BYTE_INDEXED)
|
||||
&& message.contains("indexcolormodel"))
|
||||
);
|
||||
if (!(message.contains("destination") || message.contains("band size") || // For JDK classes
|
||||
((destination.getType() == BufferedImage.TYPE_BYTE_BINARY ||
|
||||
destination.getType() == BufferedImage.TYPE_BYTE_INDEXED) &&
|
||||
message.contains("indexcolormodel")))) {
|
||||
failBecause(
|
||||
"Wrong message: " + message + " for type " + destination.getType(), expected
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException expected) {
|
||||
String message = expected.getMessage().toLowerCase();
|
||||
@@ -1352,7 +1384,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
boolean removed = illegalTypes.remove(valid);
|
||||
|
||||
// TODO: 4BYTE_ABGR (6) and 4BYTE_ABGR_PRE (7) is essentially the same type...
|
||||
// !#$#<23>%$! ImageTypeSpecifier.equals is not well-defined
|
||||
// #$@*%$! ImageTypeSpecifier.equals is not well-defined
|
||||
if (!removed) {
|
||||
for (Iterator<ImageTypeSpecifier> iterator = illegalTypes.iterator(); iterator.hasNext();) {
|
||||
ImageTypeSpecifier illegalType = iterator.next();
|
||||
@@ -1422,6 +1454,7 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> {
|
||||
failBecause("Could not read " + data.getInput() + " with explicit destination type " + type, e);
|
||||
}
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(type.getColorModel(), result.getColorModel());
|
||||
|
||||
// The following logically tests
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.util;
|
||||
|
||||
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
@@ -35,12 +36,14 @@ import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.event.IIOWriteProgressListener;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
@@ -56,6 +59,12 @@ import static org.mockito.Mockito.*;
|
||||
*/
|
||||
public abstract class ImageWriterAbstractTestCase {
|
||||
|
||||
// TODO: Move static block + getClassLoaderResource to common superclass for reader/writer test cases or delegate.
|
||||
|
||||
static {
|
||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||
}
|
||||
|
||||
protected abstract ImageWriter createImageWriter();
|
||||
|
||||
protected abstract List<? extends RenderedImage> getTestData();
|
||||
@@ -85,6 +94,10 @@ public abstract class ImageWriterAbstractTestCase {
|
||||
return getTestData().get(index);
|
||||
}
|
||||
|
||||
protected URL getClassLoaderResource(final String pName) {
|
||||
return getClass().getResource(pName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetOutput() throws IOException {
|
||||
// Should just pass with no exceptions
|
||||
|
||||
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB.icc
Normal file
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB.icc
Normal file
Binary file not shown.
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB_fixed.icc
Executable file
BIN
imageio/imageio-core/src/test/resources/profiles/Corbis RGB_fixed.icc
Executable file
Binary file not shown.
@@ -59,7 +59,7 @@ public final class ICNSImageReaderSpi extends ImageReaderSpi{
|
||||
"image/x-apple-icons", // Common extension MIME
|
||||
},
|
||||
"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true,
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
package com.twelvemonkeys.imageio.plugins.icns;
|
||||
|
||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -61,7 +64,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
||||
new Dimension(32, 32), // 24 bit + 8 bit mask
|
||||
new Dimension(48, 48), // 24 bit + 8 bit mask
|
||||
new Dimension(128, 128), // 24 bit + 8 bit mask
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(512, 512) // JPEG 2000 ic09
|
||||
),
|
||||
new TestData(
|
||||
@@ -69,7 +72,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
||||
new Dimension(16, 16), // 24 bit + 8 bit mask
|
||||
new Dimension(32, 32), // 24 bit + 8 bit mask
|
||||
new Dimension(128, 128), // 24 bit + 8 bit mask
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(256, 256), // JPEG 2000 ic08
|
||||
new Dimension(512, 512) // JPEG 2000 ic09
|
||||
),
|
||||
new TestData(
|
||||
@@ -128,4 +131,11 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase {
|
||||
protected List<String> getMIMETypes() {
|
||||
return Arrays.asList("image/x-apple-icons");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Known issue: Subsampled reading not supported")
|
||||
@Override
|
||||
public void testReadWithSubsampleParamPixels() throws IOException {
|
||||
super.testReadWithSubsampleParamPixels();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public final class CURImageReaderSpi extends ImageReaderSpi {
|
||||
"image/cursor" // Unofficial, but common
|
||||
},
|
||||
"com.twelvemonkeys.imageio.plugins.ico.CURImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true,
|
||||
|
||||
@@ -61,7 +61,7 @@ public final class ICOImageReaderSpi extends ImageReaderSpi {
|
||||
"image/ico" // Unofficial, but common
|
||||
},
|
||||
"com.twelvemonkeys.imageio.plugins.ico.ICOImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true,
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.iff;
|
||||
|
||||
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.IndexColorModel;
|
||||
@@ -159,7 +157,7 @@ final class CMAPChunk extends IFFChunk {
|
||||
// with alpha, where all colors above the original color is all transparent?
|
||||
// This is a waste of time and space, of course...
|
||||
int transparent = header.maskType == BMHDChunk.MASK_TRANSPARENT_COLOR ? header.transparentIndex : -1;
|
||||
model = new InverseColorMapIndexColorModel(header.bitplanes, reds.length, reds, greens, blues, transparent);
|
||||
model = new IndexColorModel(header.bitplanes, reds.length, reds, greens, blues, transparent); // https://github.com/haraldk/TwelveMonkeys/issues/15
|
||||
}
|
||||
|
||||
return model;
|
||||
|
||||
@@ -50,15 +50,15 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Reader for Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM
|
||||
* Reader for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM
|
||||
* format (Packed BitMap).
|
||||
* The IFF format (Interchange File Format) is the standard file format
|
||||
* supported by allmost all image software for the Amiga computer.
|
||||
* <p/>
|
||||
* This reader supports the original palette-based 1-8 bit formats, including
|
||||
* EHB (Extra Halfbright), HAM (Hold and Modify), and the more recent "deep"
|
||||
* EHB (Extra Half-Bright), HAM (Hold and Modify), and the more recent "deep"
|
||||
* formats, 8 bit gray, 24 bit RGB and 32 bit ARGB.
|
||||
* Uncompressed and ByteRun1 compressed (run lenght encoding) files are
|
||||
* Uncompressed and ByteRun1 compressed (run length encoding) files are
|
||||
* supported.
|
||||
* <p/>
|
||||
* Palette based images are read as {@code BufferedImage} of
|
||||
@@ -613,12 +613,12 @@ public class IFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
// Skip rows outside AOI
|
||||
if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) {
|
||||
continue;
|
||||
}
|
||||
else if (srcY >= (aoi.y + aoi.height)) {
|
||||
if (srcY >= (aoi.y + aoi.height)) {
|
||||
return;
|
||||
}
|
||||
else if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (formType == IFF.TYPE_ILBM) {
|
||||
// NOTE: Using (channels - c - 1) instead of just c,
|
||||
@@ -639,19 +639,21 @@ public class IFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
int dstY = (srcY - aoi.y) / sourceYSubsampling;
|
||||
// TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?)
|
||||
// TODO: Avoid createChild if no region?
|
||||
if (sourceXSubsampling == 1) {
|
||||
destination.setRect(0, dstY, sourceRow);
|
||||
if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) {
|
||||
int dstY = (srcY - aoi.y) / sourceYSubsampling;
|
||||
// TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?)
|
||||
// TODO: Avoid createChild if no region?
|
||||
if (sourceXSubsampling == 1) {
|
||||
destination.setRect(0, dstY, sourceRow);
|
||||
// dataElements = raster.getDataElements(aoi.x, 0, aoi.width, 1, dataElements);
|
||||
// destination.setDataElements(offset.x, offset.y + (srcY - aoi.y) / sourceYSubsampling, aoi.width, 1, dataElements);
|
||||
}
|
||||
else {
|
||||
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
|
||||
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
|
||||
int dstX = srcX / sourceXSubsampling;
|
||||
destination.setDataElements(dstX, dstY, dataElements);
|
||||
}
|
||||
else {
|
||||
for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
|
||||
dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
|
||||
int dstX = srcX / sourceXSubsampling;
|
||||
destination.setDataElements(dstX, dstY, dataElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public class IFFImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"iff", "lbm", "ham", "ham8", "ilbm"},
|
||||
new String[]{"image/iff", "image/x-iff"},
|
||||
"com.twelvemonkeys.imageio.plugins.iff.IFFImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
new String[]{"com.twelvemonkeys.imageio.plugins.iff.IFFImageWriterSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
@@ -108,7 +108,7 @@ public class IFFImageReaderSpi extends ImageReaderSpi {
|
||||
}
|
||||
|
||||
public String getDescription(Locale pLocale) {
|
||||
return "Amiga (Electronic Arts) Image Interchange Format (IFF) image reader";
|
||||
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image reader";
|
||||
}
|
||||
|
||||
public static ImageReaderSpi sharedProvider() {
|
||||
|
||||
@@ -45,7 +45,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Writer for Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format.
|
||||
* Writer for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format.
|
||||
* The IFF format (Interchange File Format) is the standard file format
|
||||
* supported by almost all image software for the Amiga computer.
|
||||
* <p/>
|
||||
|
||||
@@ -79,6 +79,6 @@ public class IFFImageWriterSpi extends ImageWriterSpi {
|
||||
}
|
||||
|
||||
public String getDescription(Locale pLocale) {
|
||||
return "Amiga (Electronic Arts) IFF image writer";
|
||||
return "Commodore Amiga/Electronic Arts Image Interchange Format (IFF) image writer";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import java.util.Locale;
|
||||
*/
|
||||
abstract class JMagickImageReaderSpiSupport extends ImageReaderSpi {
|
||||
|
||||
final static boolean AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.jmagick.JMagick");
|
||||
final static boolean AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.jmagick.JMagick", JMagickImageReaderSpiSupport.class);
|
||||
|
||||
/**
|
||||
* Creates a JMagickImageReaderSpiSupport
|
||||
@@ -69,7 +69,7 @@ abstract class JMagickImageReaderSpiSupport extends ImageReaderSpi {
|
||||
AVAILABLE ? pSuffixes : null, // Suffixes
|
||||
AVAILABLE ? pMimeTypes : null, // Mime-types
|
||||
pReaderClassName, // Reader class name
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types
|
||||
new Class[] {ImageInputStream.class}, // Input types
|
||||
pWriterSpiNames, // Writer SPI names
|
||||
true, // Supports standard stream metadata format
|
||||
null, // Native stream metadata format name
|
||||
|
||||
@@ -30,9 +30,9 @@ package com.twelvemonkeys.imageio.plugins.jmagick;
|
||||
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.spi.ImageWriterSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -68,7 +68,7 @@ abstract class JMagickImageWriterSpiSupport extends ImageWriterSpi {
|
||||
AVAILABLE ? pSuffixes : null, // Suffixes
|
||||
AVAILABLE ? pMimeTypes : null, // Mime-types
|
||||
pWriterClassName, // Writer class name
|
||||
ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types
|
||||
new Class[] {ImageOutputStream.class}, // Output types
|
||||
pReaderSpiNames, // Reader SPI names
|
||||
true, // Supports standard stream metadata format
|
||||
null, // Native stream metadata format name
|
||||
|
||||
@@ -93,11 +93,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
private static final ColorModel CM_GRAY_ALPHA =
|
||||
new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
|
||||
|
||||
private final boolean mUseTempFile;
|
||||
private File mTempFile;
|
||||
private final boolean useTempFile;
|
||||
private File tempFile;
|
||||
|
||||
private MagickImage mImage;
|
||||
private Dimension mSize;
|
||||
private MagickImage image;
|
||||
private Dimension size;
|
||||
|
||||
protected JMagickReader(final JMagickImageReaderSpiSupport pProvider) {
|
||||
this(pProvider, pProvider.useTempFile());
|
||||
@@ -105,28 +105,29 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
|
||||
protected JMagickReader(final ImageReaderSpi pProvider, final boolean pUseTemp) {
|
||||
super(pProvider);
|
||||
mUseTempFile = pUseTemp;
|
||||
useTempFile = pUseTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetMembers() {
|
||||
if (mTempFile != null) {
|
||||
mTempFile.delete();
|
||||
if (tempFile != null&& !tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
mTempFile = null;
|
||||
|
||||
if (mImage != null) {
|
||||
mImage.destroyImages();
|
||||
tempFile = null;
|
||||
|
||||
if (image != null) {
|
||||
image.destroyImages();
|
||||
}
|
||||
mImage = null;
|
||||
|
||||
mSize = null;
|
||||
image = null;
|
||||
size = null;
|
||||
}
|
||||
|
||||
// TODO: Handle multi-image formats
|
||||
// if (mImage.hasFrames()) {
|
||||
// int count = mImage.getNumFrames();
|
||||
// MagickImage[] images = mImage.breakFrames();
|
||||
// if (image.hasFrames()) {
|
||||
// int count = image.getNumFrames();
|
||||
// MagickImage[] images = image.breakFrames();
|
||||
// }
|
||||
|
||||
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) throws IOException {
|
||||
@@ -140,7 +141,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
try {
|
||||
ColorModel cm;
|
||||
// NOTE: These are all fall-through by intention
|
||||
switch (mImage.getImageType()) {
|
||||
switch (image.getImageType()) {
|
||||
case ImageType.BilevelType:
|
||||
specs.add(IndexedImageTypeSpecifier.createFromIndexColorModel(MonochromeColorModel.getInstance()));
|
||||
case ImageType.GrayscaleType:
|
||||
@@ -158,11 +159,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
));
|
||||
case ImageType.PaletteType:
|
||||
specs.add(IndexedImageTypeSpecifier.createFromIndexColorModel(
|
||||
MagickUtil.createIndexColorModel(mImage.getColormap(), false)
|
||||
MagickUtil.createIndexColorModel(image.getColormap(), false)
|
||||
));
|
||||
case ImageType.PaletteMatteType:
|
||||
specs.add(IndexedImageTypeSpecifier.createFromIndexColorModel(
|
||||
MagickUtil.createIndexColorModel(mImage.getColormap(), true)
|
||||
MagickUtil.createIndexColorModel(image.getColormap(), true)
|
||||
));
|
||||
case ImageType.TrueColorType:
|
||||
// cm = MagickUtil.CM_COLOR_OPAQUE;
|
||||
@@ -183,7 +184,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
case ImageType.ColorSeparationMatteType:
|
||||
case ImageType.OptimizeType:
|
||||
default:
|
||||
throw new MagickException("Unknown JMagick image type: " + mImage.getImageType());
|
||||
throw new MagickException("Unknown JMagick image type: " + image.getImageType());
|
||||
}
|
||||
}
|
||||
catch (MagickException e) {
|
||||
@@ -196,19 +197,19 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
public int getWidth(int pIndex) throws IOException {
|
||||
checkBounds(pIndex);
|
||||
|
||||
if (mSize == null) {
|
||||
if (size == null) {
|
||||
init(0);
|
||||
}
|
||||
return mSize != null ? mSize.width : -1;
|
||||
return size != null ? size.width : -1;
|
||||
}
|
||||
|
||||
public int getHeight(int pIndex) throws IOException {
|
||||
checkBounds(pIndex);
|
||||
|
||||
if (mSize == null) {
|
||||
if (size == null) {
|
||||
init(0);
|
||||
}
|
||||
return mSize != null ? mSize.height : -1;
|
||||
return size != null ? size.height : -1;
|
||||
}
|
||||
|
||||
public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException {
|
||||
@@ -218,14 +219,14 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
processImageStarted(pIndex);
|
||||
|
||||
// Some more waste of time and space...
|
||||
Dimension size = mSize;
|
||||
Dimension size = this.size;
|
||||
|
||||
if (pParam != null) {
|
||||
// Source region
|
||||
// TODO: Maybe have to do some tests, to check if we are within bounds...
|
||||
Rectangle sourceRegion = pParam.getSourceRegion();
|
||||
if (sourceRegion != null) {
|
||||
mImage = mImage.cropImage(sourceRegion);
|
||||
image = image.cropImage(sourceRegion);
|
||||
size = sourceRegion.getSize();
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
int w = size.width / pParam.getSourceXSubsampling();
|
||||
int h = size.height / pParam.getSourceYSubsampling();
|
||||
|
||||
mImage = mImage.sampleImage(w, h);
|
||||
image = image.sampleImage(w, h);
|
||||
size = new Dimension(w, h);
|
||||
}
|
||||
}
|
||||
@@ -245,7 +246,7 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
processImageProgress(10f);
|
||||
BufferedImage buffered = MagickUtil.toBuffered(mImage);
|
||||
BufferedImage buffered = MagickUtil.toBuffered(image);
|
||||
processImageProgress(100f);
|
||||
|
||||
/**/
|
||||
@@ -260,12 +261,12 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
//*/
|
||||
|
||||
/**
|
||||
System.out.println("Colorspace: " + mImage.getColorspace());
|
||||
System.out.println("Depth: " + mImage.getDepth());
|
||||
System.out.println("Format: " + mImage.getImageFormat());
|
||||
System.out.println("Type: " + mImage.getImageType());
|
||||
System.out.println("IPTCProfile: " + StringUtil.deepToString(mImage.getIptcProfile()));
|
||||
System.out.println("StorageClass: " + mImage.getStorageClass());
|
||||
System.out.println("Colorspace: " + image.getColorspace());
|
||||
System.out.println("Depth: " + image.getDepth());
|
||||
System.out.println("Format: " + image.getImageFormat());
|
||||
System.out.println("Type: " + image.getImageType());
|
||||
System.out.println("IPTCProfile: " + StringUtil.deepToString(image.getIptcProfile()));
|
||||
System.out.println("StorageClass: " + image.getStorageClass());
|
||||
//*/
|
||||
|
||||
processImageComplete();
|
||||
@@ -282,11 +283,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
checkBounds(pIndex);
|
||||
|
||||
try {
|
||||
if (mImage == null) {
|
||||
if (image == null) {
|
||||
// TODO: If ImageInputStream is already file-backed, maybe we can peek into that file?
|
||||
// At the moment, the cache/file is not accessible, but we could create our own
|
||||
// FileImageInputStream provider that gives us this access.
|
||||
if (!mUseTempFile && imageInput.length() >= 0 && imageInput.length() <= Integer.MAX_VALUE) {
|
||||
if (!useTempFile && imageInput.length() >= 0 && imageInput.length() <= Integer.MAX_VALUE) {
|
||||
// This works for most file formats, as long as ImageMagick
|
||||
// uses the file magic to decide file format
|
||||
byte[] bytes = new byte[(int) imageInput.length()];
|
||||
@@ -294,17 +295,18 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
|
||||
// Unfortunately, this is a waste of space & time...
|
||||
ImageInfo info = new ImageInfo();
|
||||
mImage = new MagickImage(info);
|
||||
mImage.blobToImage(info, bytes);
|
||||
image = new MagickImage(info);
|
||||
image.blobToImage(info, bytes);
|
||||
}
|
||||
else {
|
||||
// Quirks mode: Use temp file to get correct file extension
|
||||
// (which is even more waste of space & time, but might save memory)
|
||||
String ext = getFormatName().toLowerCase();
|
||||
|
||||
mTempFile = File.createTempFile("jmagickreader", "." + ext);
|
||||
mTempFile.deleteOnExit();
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(mTempFile));
|
||||
tempFile = File.createTempFile("jmagickreader", "." + ext);
|
||||
tempFile.deleteOnExit();
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile));
|
||||
|
||||
try {
|
||||
byte[] buffer = new byte[FileUtil.BUF_SIZE];
|
||||
int count;
|
||||
@@ -320,11 +322,11 @@ abstract class JMagickReader extends ImageReaderBase {
|
||||
out.close();
|
||||
}
|
||||
|
||||
ImageInfo info = new ImageInfo(mTempFile.getAbsolutePath());
|
||||
mImage = new MagickImage(info);
|
||||
ImageInfo info = new ImageInfo(tempFile.getAbsolutePath());
|
||||
image = new MagickImage(info);
|
||||
}
|
||||
|
||||
mSize = mImage.getDimension();
|
||||
size = image.getDimension();
|
||||
}
|
||||
}
|
||||
catch (MagickException e) {
|
||||
|
||||
@@ -52,12 +52,13 @@ public class JPEGImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
boolean canDecode(ImageInputStream pSource) throws IOException {
|
||||
// new byte[][] {new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0},
|
||||
// new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe1}}, // JPEG
|
||||
// new byte[][] {new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xed}}, // PHOTOSHOP 3 JPEG
|
||||
// new byte[][] {new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xee}}, // JPG
|
||||
byte[] magic = new byte[4];
|
||||
pSource.readFully(magic);
|
||||
|
||||
return magic[0] == (byte) 0xFF && magic[1] == (byte) 0xD8 && magic[2] == (byte) 0xFF &&
|
||||
(magic[3] == (byte) 0xE0 || magic[0] == (byte) 0xE1 || magic[0] == (byte) 0xEE);
|
||||
(magic[3] == (byte) 0xE0 || magic[3] == (byte) 0xE1 || magic[3] == (byte) 0xED || magic[3] == (byte) 0xEE);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public class TargaImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
);
|
||||
}
|
||||
|
||||
boolean canDecode(ImageInputStream pSource) throws IOException {
|
||||
boolean canDecode(final ImageInputStream pSource) throws IOException {
|
||||
// // TODO: Targa 1989 signature look like (bytes 8-23 of 26 LAST bytes):
|
||||
// // 'T', 'R', 'U', 'E', 'V', 'I', 'S', 'I', 'O', 'N', '-', 'X', 'F', 'I', 'L', 'E'
|
||||
// // Targa 1987:
|
||||
@@ -63,6 +63,12 @@ public class TargaImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
// new byte[] {-1, 0x01, 0x20}, // Type 31: Compressed CM
|
||||
// new byte[] {-1, 0x01, 0x21}, // Type 32: Compressed CM, 4 pass
|
||||
// },
|
||||
|
||||
// If we don't know the stream length, just give up, as the Targa format has trailing magic bytes...
|
||||
if (pSource.length() < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pSource.seek(pSource.length() - 18);
|
||||
byte[] magic = new byte[18];
|
||||
pSource.readFully(magic);
|
||||
|
||||
@@ -54,7 +54,7 @@ public class WMFImageReaderSpi extends JMagickImageReaderSpiSupport {
|
||||
// (byte) 0x9a, (byte) 0x00, (byte) 0x00,}},
|
||||
byte[] magic = new byte[6];
|
||||
pSource.readFully(magic);
|
||||
return magic[0] == (byte) 0xD7 && magic[2] == (byte) 0xCD &&
|
||||
return magic[0] == (byte) 0xD7 && magic[1] == (byte) 0xCD &&
|
||||
magic[2] == (byte) 0xC6 && magic[3] == (byte) 0x9A &&
|
||||
magic[4] == (byte) 0x00 && magic[5] == (byte) 0x00;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,12 @@ import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -51,14 +54,16 @@ import java.util.Arrays;
|
||||
* @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
|
||||
*/
|
||||
final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
private final ImageReader reader;
|
||||
private final Directory ifd;
|
||||
private final ImageInputStream stream;
|
||||
private final int compression;
|
||||
|
||||
private transient SoftReference<BufferedImage> cachedThumbnail;
|
||||
|
||||
public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) {
|
||||
public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, ImageReader jpegReader, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) {
|
||||
super(progressListener, imageIndex, thumbnailIndex);
|
||||
this.reader = Validate.notNull(jpegReader);
|
||||
this.ifd = ifd;
|
||||
this.stream = stream;
|
||||
|
||||
@@ -125,8 +130,16 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
};
|
||||
|
||||
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
|
||||
|
||||
try {
|
||||
return readJPEGThumbnail(input);
|
||||
MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input);
|
||||
|
||||
try {
|
||||
return readJPEGThumbnail(reader, stream);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
input.close();
|
||||
@@ -202,7 +215,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
|
||||
|
||||
if (width == null) {
|
||||
throw new IIOException("Missing dimensions for unknown EXIF thumbnail");
|
||||
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
|
||||
}
|
||||
|
||||
return ((Number) width.getValue()).intValue();
|
||||
@@ -221,7 +234,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
|
||||
|
||||
if (height == null) {
|
||||
throw new IIOException("Missing dimensions for unknown EXIF thumbnail");
|
||||
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
|
||||
}
|
||||
|
||||
return ((Number) height.getValue()).intValue();
|
||||
|
||||
@@ -144,35 +144,16 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
|
||||
}
|
||||
|
||||
public WritableRaster createCompatibleDestRaster(final Raster src) {
|
||||
return src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[]{0, 1, 2}).createCompatibleWritableRaster();
|
||||
// WHAT?? This code no longer work for JRE 7u45+... JRE bug?!
|
||||
// Raster child = src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2});
|
||||
// return child.createCompatibleWritableRaster(); // Throws an exception complaining about the scanline stride from the verify() method
|
||||
|
||||
// This is a workaround for the above code that no longer works.
|
||||
// It wil use 25% more memory, but it seems to work...
|
||||
WritableRaster raster = src.createCompatibleWritableRaster();
|
||||
return raster.createWritableChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2});
|
||||
}
|
||||
|
||||
/*
|
||||
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
|
||||
Validate.notNull(src, "src may not be null");
|
||||
// Validate.isTrue(src != dest, "src and dest image may not be same");
|
||||
|
||||
if (dest == null) {
|
||||
dest = createCompatibleDestImage(src, ColorModel.getRGBdefault());
|
||||
}
|
||||
|
||||
filter(src.getRaster(), dest.getRaster());
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
public Rectangle2D getBounds2D(BufferedImage src) {
|
||||
return getBounds2D(src.getRaster());
|
||||
}
|
||||
|
||||
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
|
||||
// TODO: dest color model depends on bands...
|
||||
return destCM == null ?
|
||||
new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_3BYTE_BGR) :
|
||||
new BufferedImage(destCM, destCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), destCM.isAlphaPremultiplied(), null);
|
||||
}
|
||||
*/
|
||||
|
||||
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
|
||||
if (dstPt == null) {
|
||||
dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY());
|
||||
|
||||
@@ -29,10 +29,14 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
|
||||
@@ -45,12 +49,14 @@ import java.lang.ref.SoftReference;
|
||||
*/
|
||||
final class JFXXThumbnailReader extends ThumbnailReader {
|
||||
|
||||
private final ImageReader reader;
|
||||
private final JFXXSegment segment;
|
||||
|
||||
private transient SoftReference<BufferedImage> cachedThumbnail;
|
||||
|
||||
protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) {
|
||||
protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) {
|
||||
super(progressListener, imageIndex, thumbnailIndex);
|
||||
this.reader = Validate.notNull(jpegReader);
|
||||
this.segment = segment;
|
||||
}
|
||||
|
||||
@@ -79,11 +85,30 @@ final class JFXXThumbnailReader extends ThumbnailReader {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
public IIOMetadata readMetadata() throws IOException {
|
||||
ImageInputStream input = new ByteArrayImageInputStream(segment.thumbnail);
|
||||
|
||||
try {
|
||||
reader.setInput(input);
|
||||
|
||||
return reader.getImageMetadata(0);
|
||||
}
|
||||
finally {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
|
||||
private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException {
|
||||
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
|
||||
|
||||
if (thumbnail == null) {
|
||||
thumbnail = readJPEGThumbnail(new ByteArrayInputStream(segment.thumbnail));
|
||||
ImageInputStream stream = new ByteArrayImageInputStream(segment.thumbnail);
|
||||
try {
|
||||
thumbnail = readJPEGThumbnail(reader, stream);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
cachedThumbnail = pixelsExposed ? null : new SoftReference<BufferedImage>(thumbnail);
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.xml.XMLSerializer;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JPEGImage10MetadataCleaner
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: JPEGImage10MetadataCleaner.java,v 1.0 22.10.13 14:41 haraldk Exp$
|
||||
*/
|
||||
final class JPEGImage10MetadataCleaner {
|
||||
|
||||
/**
|
||||
* Native metadata format name
|
||||
*/
|
||||
static final String JAVAX_IMAGEIO_JPEG_IMAGE_1_0 = "javax_imageio_jpeg_image_1.0";
|
||||
|
||||
private final JPEGImageReader reader;
|
||||
|
||||
JPEGImage10MetadataCleaner(final JPEGImageReader reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
IIOMetadata cleanMetadata(final IIOMetadata imageMetadata) throws IOException {
|
||||
// We filter out pretty much everything from the stream..
|
||||
// Meaning we have to read get *all APP segments* and re-insert into metadata.
|
||||
List<JPEGSegment> appSegments = reader.getAppSegments(JPEGImageReader.ALL_APP_MARKERS, null);
|
||||
|
||||
// NOTE: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL...
|
||||
// DONE: 1: Work around
|
||||
// TODO: 2: REPORT BUG!
|
||||
// TODO: Report dht inconsistency bug (reads any amount of tables but only allows setting 4 tables)
|
||||
|
||||
// TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. Need new format, might as well create a completely new format...
|
||||
// As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like:
|
||||
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
|
||||
/*
|
||||
from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
||||
|
||||
In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif)
|
||||
by defining other types of nodes which may appear as a child of the JPEGvariety node.
|
||||
|
||||
(Note that an application wishing to interpret Exif metadata given a metadata tree structure in the
|
||||
javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an
|
||||
APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific
|
||||
code to interpret the data in the marker segment. If such an application were to encounter a metadata tree
|
||||
formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be
|
||||
unknown in that format - it might be structured as a child node of the JPEGvariety node.
|
||||
|
||||
Thus, it is important for an application to specify which version to use by passing the string identifying
|
||||
the version to the method/constructor used to obtain an IIOMetadata object.)
|
||||
*/
|
||||
|
||||
IIOMetadataNode tree = (IIOMetadataNode) imageMetadata.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0);
|
||||
IIOMetadataNode jpegVariety = (IIOMetadataNode) tree.getElementsByTagName("JPEGvariety").item(0);
|
||||
IIOMetadataNode markerSequence = (IIOMetadataNode) tree.getElementsByTagName("markerSequence").item(0);
|
||||
|
||||
JFIFSegment jfifSegment = reader.getJFIF();
|
||||
JFXXSegment jfxxSegment = reader.getJFXX();
|
||||
AdobeDCTSegment adobeDCT = reader.getAdobeDCT();
|
||||
ICC_Profile embeddedICCProfile = reader.getEmbeddedICCProfile(true);
|
||||
SOFSegment sof = reader.getSOF();
|
||||
|
||||
boolean hasRealJFIF = false;
|
||||
boolean hasRealJFXX = false;
|
||||
boolean hasRealICC = false;
|
||||
|
||||
if (jfifSegment != null) {
|
||||
// Normal case, conformant JFIF with 1 or 3 components
|
||||
// TODO: Test if we have CMY or other non-JFIF color space?
|
||||
if (sof.componentsInFrame() == 1 || sof.componentsInFrame() == 3) {
|
||||
IIOMetadataNode jfif = new IIOMetadataNode("app0JFIF");
|
||||
jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion));
|
||||
jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion));
|
||||
jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units));
|
||||
jfif.setAttribute("Xdensity", String.valueOf(Math.max(1, jfifSegment.xDensity))); // Avoid 0 density
|
||||
jfif.setAttribute("Ydensity", String.valueOf(Math.max(1,jfifSegment.yDensity)));
|
||||
jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail));
|
||||
jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail));
|
||||
|
||||
jpegVariety.appendChild(jfif);
|
||||
hasRealJFIF = true;
|
||||
|
||||
// Add app2ICC and JFXX as proper nodes
|
||||
if (embeddedICCProfile != null) {
|
||||
IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC");
|
||||
app2ICC.setUserObject(embeddedICCProfile);
|
||||
jfif.appendChild(app2ICC);
|
||||
hasRealICC = true;
|
||||
}
|
||||
|
||||
if (jfxxSegment != null) {
|
||||
IIOMetadataNode JFXX = new IIOMetadataNode("JFXX");
|
||||
jfif.appendChild(JFXX);
|
||||
IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX");
|
||||
app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode));
|
||||
|
||||
JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), 0, 0, jfxxSegment);
|
||||
IIOMetadataNode jfifThumb;
|
||||
|
||||
switch (jfxxSegment.extensionCode) {
|
||||
case JFXXSegment.JPEG:
|
||||
jfifThumb = new IIOMetadataNode("JFIFthumbJPEG");
|
||||
// Contains it's own "markerSequence" with full DHT, DQT, SOF etc...
|
||||
IIOMetadata thumbMeta = thumbnailReader.readMetadata();
|
||||
Node thumbTree = thumbMeta.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0);
|
||||
jfifThumb.appendChild(thumbTree.getLastChild());
|
||||
app0JFXX.appendChild(jfifThumb);
|
||||
break;
|
||||
|
||||
case JFXXSegment.INDEXED:
|
||||
jfifThumb = new IIOMetadataNode("JFIFthumbPalette");
|
||||
jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth()));
|
||||
jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight()));
|
||||
app0JFXX.appendChild(jfifThumb);
|
||||
break;
|
||||
|
||||
case JFXXSegment.RGB:
|
||||
jfifThumb = new IIOMetadataNode("JFIFthumbRGB");
|
||||
jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth()));
|
||||
jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight()));
|
||||
app0JFXX.appendChild(jfifThumb);
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.processWarningOccurred(String.format("Unknown JFXX extension code: %d", jfxxSegment.extensionCode));
|
||||
}
|
||||
|
||||
JFXX.appendChild(app0JFXX);
|
||||
hasRealJFXX = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Typically CMYK JPEG with JFIF segment (Adobe or similar).
|
||||
reader.processWarningOccurred(String.format(
|
||||
"Incompatible JFIF marker segment in stream. " +
|
||||
"SOF%d has %d color components, JFIF allows only 1 or 3 components. Ignoring JFIF marker.",
|
||||
sof.marker & 0xf, sof.componentsInFrame()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF
|
||||
if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) {
|
||||
reader.processWarningOccurred(String.format(
|
||||
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
|
||||
"Ignoring Adobe App14 marker.",
|
||||
sof.marker & 0xf, sof.componentsInFrame()
|
||||
));
|
||||
|
||||
// Remove bad AdobeDCT
|
||||
NodeList app14Adobe = tree.getElementsByTagName("app14Adobe");
|
||||
for (int i = app14Adobe.getLength() - 1; i >= 0; i--) {
|
||||
Node item = app14Adobe.item(i);
|
||||
item.getParentNode().removeChild(item);
|
||||
}
|
||||
|
||||
// We don't add this as unknown marker, as we are certain it's bogus by now
|
||||
}
|
||||
|
||||
Node next = null;
|
||||
for (JPEGSegment segment : appSegments) {
|
||||
// Except real app0JFIF, app0JFXX, app2ICC and app14Adobe, add all the app segments that we filtered away as "unknown" markers
|
||||
if (segment.marker() == JPEG.APP0 && "JFIF".equals(segment.identifier()) && hasRealJFIF) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP0 && "JFXX".equals(segment.identifier()) && hasRealJFXX) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP1 && "Exif".equals(segment.identifier()) /* always inserted */) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP2 && "ICC_PROFILE".equals(segment.identifier()) && hasRealICC) {
|
||||
continue;
|
||||
}
|
||||
else if (segment.marker() == JPEG.APP14 && "Adobe".equals(segment.identifier()) /* always inserted */) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IIOMetadataNode unknown = new IIOMetadataNode("unknown");
|
||||
unknown.setAttribute("MarkerTag", Integer.toString(segment.marker() & 0xff));
|
||||
|
||||
DataInputStream stream = new DataInputStream(segment.data());
|
||||
|
||||
try {
|
||||
String identifier = segment.identifier();
|
||||
int off = identifier != null ? identifier.length() + 1 : 0;
|
||||
|
||||
byte[] data = new byte[off + segment.length()];
|
||||
|
||||
if (identifier != null) {
|
||||
System.arraycopy(identifier.getBytes(Charset.forName("ASCII")), 0, data, 0, identifier.length());
|
||||
}
|
||||
|
||||
stream.readFully(data, off, segment.length());
|
||||
|
||||
unknown.setUserObject(data);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
|
||||
if (next == null) {
|
||||
// To be semi-compatible with the functionality in mergeTree,
|
||||
// let's insert after the last unknown tag, or before any other tag if no unknown tag exists
|
||||
NodeList unknowns = markerSequence.getElementsByTagName("unknown");
|
||||
|
||||
if (unknowns.getLength() > 0) {
|
||||
next = unknowns.item(unknowns.getLength() - 1).getNextSibling();
|
||||
}
|
||||
else {
|
||||
next = markerSequence.getFirstChild();
|
||||
}
|
||||
}
|
||||
|
||||
markerSequence.insertBefore(unknown, next);
|
||||
}
|
||||
|
||||
// Inconsistency issue in the com.sun classes, it can read metadata with dht containing
|
||||
// more than 4 children, but will not allow setting such a tree...
|
||||
// We'll split AC/DC tables into separate dht nodes.
|
||||
NodeList dhts = markerSequence.getElementsByTagName("dht");
|
||||
for (int j = 0; j < dhts.getLength(); j++) {
|
||||
Node dht = dhts.item(j);
|
||||
NodeList dhtables = dht.getChildNodes();
|
||||
|
||||
if (dhtables.getLength() > 4) {
|
||||
IIOMetadataNode acTables = new IIOMetadataNode("dht");
|
||||
dht.getParentNode().insertBefore(acTables, dht.getNextSibling());
|
||||
|
||||
// Split into 2 dht nodes, one for AC and one for DC
|
||||
for (int i = 0; i < dhtables.getLength(); i++) {
|
||||
Element dhtable = (Element) dhtables.item(i);
|
||||
String tableClass = dhtable.getAttribute("class");
|
||||
if ("1".equals(tableClass)) {
|
||||
dht.removeChild(dhtable);
|
||||
acTables.appendChild(dhtable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
imageMetadata.setFromTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0, tree);
|
||||
}
|
||||
catch (IIOInvalidTreeException e) {
|
||||
if (JPEGImageReader.DEBUG) {
|
||||
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return imageMetadata;
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,16 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
import com.twelvemonkeys.xml.XMLSerializer;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOReadUpdateListener;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_ColorSpace;
|
||||
@@ -63,6 +66,7 @@ import java.util.List;
|
||||
* <p/>
|
||||
* Main features:
|
||||
* <ul>
|
||||
* <li>Support for YCbCr JPEGs without JFIF segment (converted to RGB, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)</li>
|
||||
* <li>Support for JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used)</li>
|
||||
@@ -75,10 +79,17 @@ import java.util.List;
|
||||
* </ul>
|
||||
* Thumbnail support:
|
||||
* <ul>
|
||||
* <li>Support for JFIF thumbnails (even if stream contains "inconsistent metadata")</li>
|
||||
* <li>Support for JFIF thumbnails (even if stream contains inconsistent metadata)</li>
|
||||
* <li>Support for JFXX thumbnails (JPEG, Indexed and RGB)</li>
|
||||
* <li>Support for EXIF thumbnails (JPEG, RGB and YCbCr)</li>
|
||||
* </ul>
|
||||
* Metadata support:
|
||||
* <ul>
|
||||
* <li>Support for JPEG metadata in both standard and native formats (even if stream contains inconsistent metadata)</li>
|
||||
* <li>Support for {@code javax_imageio_jpeg_image_1.0} format (currently as native format, may change in the future)</li>
|
||||
* <li>Support for illegal combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the
|
||||
* "MarkerSequence" tag for the unsupported segments (for {@code javax_imageio_jpeg_image_1.0} format)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author LUT-based YCbCR conversion by Werner Randelshofer
|
||||
@@ -86,10 +97,13 @@ import java.util.List;
|
||||
* @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$
|
||||
*/
|
||||
public class JPEGImageReader extends ImageReaderBase {
|
||||
// TODO: Fix the (stream) metadata inconsistency issues.
|
||||
// - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes.. :-/
|
||||
// TODO: Allow automatic rotation based on EXIF rotation field?
|
||||
// TODO: Create a simplified native metadata format that is closer to the actual JPEG stream AND supports EXIF in a sensible way
|
||||
|
||||
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
|
||||
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
|
||||
|
||||
/** Internal constant for referring all APP segments */
|
||||
static final int ALL_APP_MARKERS = -1;
|
||||
|
||||
/** Segment identifiers for the JPEG segments we care about reading. */
|
||||
private static final Map<Integer, List<String>> SEGMENT_IDENTIFIERS = createSegmentIds();
|
||||
@@ -97,17 +111,10 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
private static Map<Integer, List<String>> createSegmentIds() {
|
||||
Map<Integer, List<String>> map = new LinkedHashMap<Integer, List<String>>();
|
||||
|
||||
// JFIF/JFXX APP0 markers
|
||||
map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS);
|
||||
|
||||
// Exif metadata
|
||||
map.put(JPEG.APP1, Collections.singletonList("Exif"));
|
||||
|
||||
// ICC Color Profile
|
||||
map.put(JPEG.APP2, Collections.singletonList("ICC_PROFILE"));
|
||||
|
||||
// Adobe APP14 marker
|
||||
map.put(JPEG.APP14, Collections.singletonList("Adobe"));
|
||||
// Need all APP markers to be able to re-generate proper metadata later
|
||||
for (int appMarker = JPEG.APP0; appMarker <= JPEG.APP15; appMarker++) {
|
||||
map.put(appMarker, JPEGSegmentUtil.ALL_IDS);
|
||||
}
|
||||
|
||||
// SOFn markers
|
||||
map.put(JPEG.SOF0, null);
|
||||
@@ -133,11 +140,15 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
/** Listens to progress updates in the delegate, and delegates back to this instance */
|
||||
private final ProgressDelegator progressDelegator;
|
||||
|
||||
/** Cached JPEG app segments */
|
||||
private List<JPEGSegment> segments;
|
||||
|
||||
/** Extra delegate for reading JPEG encoded thumbnails */
|
||||
private ImageReader thumbnailReader;
|
||||
private List<ThumbnailReader> thumbnails;
|
||||
|
||||
private JPEGImage10MetadataCleaner metadataCleaner;
|
||||
|
||||
/** Cached list of JPEG segments we filter from the underlying stream */
|
||||
private List<JPEGSegment> segments;
|
||||
|
||||
JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
|
||||
super(provider);
|
||||
this.delegate = Validate.notNull(delegate);
|
||||
@@ -157,6 +168,12 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
segments = null;
|
||||
thumbnails = null;
|
||||
|
||||
if (thumbnailReader != null) {
|
||||
thumbnailReader.reset();
|
||||
}
|
||||
|
||||
metadataCleaner = null;
|
||||
|
||||
installListeners();
|
||||
}
|
||||
|
||||
@@ -164,6 +181,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
|
||||
if (thumbnailReader != null) {
|
||||
thumbnailReader.dispose();
|
||||
thumbnailReader = null;
|
||||
}
|
||||
|
||||
delegate.dispose();
|
||||
}
|
||||
|
||||
@@ -280,45 +302,52 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
assertInput();
|
||||
checkBounds(imageIndex);
|
||||
|
||||
// TODO: This test is not good enough for JDK7, which seems to have fixed some of the issues.
|
||||
// NOTE: We rely on the fact that unsupported images has no valid types. This is kind of hacky.
|
||||
// Might want to look into the metadata, to see if there's a better way to identify these.
|
||||
boolean unsupported = !delegate.getImageTypes(imageIndex).hasNext();
|
||||
// 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));
|
||||
// }
|
||||
// }
|
||||
|
||||
ICC_Profile profile = getEmbeddedICCProfile(false);
|
||||
AdobeDCTSegment adobeDCT = getAdobeDCT();
|
||||
SOFSegment sof = getSOF();
|
||||
JPEGColorSpace sourceCSType = getSourceCSType(adobeDCT, sof);
|
||||
|
||||
// TODO: Probably something bogus here, as ICC profile isn't applied if reading through the delegate any more...
|
||||
// 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() && (
|
||||
unsupported ||
|
||||
sourceCSType == JPEGColorSpace.CMYK ||
|
||||
sourceCSType == JPEGColorSpace.YCCK ||
|
||||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK ||
|
||||
profile != null && !ColorSpaces.isCS_sRGB(profile))) {
|
||||
// profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
|
||||
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);
|
||||
}
|
||||
|
||||
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, ensureDisplayProfile(profile));
|
||||
// TODO: Possible to optimize slightly, to avoid readAsRaster for non-CMyK and other good types?
|
||||
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, adobeDCT, ensureDisplayProfile(profile));
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.println("Reading using delegate");
|
||||
}
|
||||
|
||||
|
||||
return delegate.read(imageIndex, param);
|
||||
}
|
||||
|
||||
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, ICC_Profile profile) throws IOException {
|
||||
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, SOFSegment startOfFrame, JPEGColorSpace csType, AdobeDCTSegment adobeDCT, ICC_Profile profile) throws IOException {
|
||||
int origWidth = getWidth(imageIndex);
|
||||
int origHeight = getHeight(imageIndex);
|
||||
|
||||
AdobeDCTSegment adobeDCT = getAdobeDCT();
|
||||
SOFSegment startOfFrame = getSOF();
|
||||
JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame);
|
||||
|
||||
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
|
||||
BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight);
|
||||
WritableRaster destination = image.getRaster();
|
||||
@@ -442,6 +471,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// Apply further color conversion for explicit color space, or just copy the pixels into place
|
||||
if (convert != null) {
|
||||
convert.filter(src, dest);
|
||||
// WritableRaster filtered = convert.filter(src, null);
|
||||
// new AffineTransformOp(AffineTransform.getRotateInstance(2 * Math.PI, filtered.getWidth() / 2.0, filtered.getHeight() / 2.0), null).filter(filtered, dest);
|
||||
}
|
||||
else {
|
||||
dest.setRect(0, 0, src);
|
||||
@@ -474,16 +505,17 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
When reading, the contents of the stream are interpreted by the usual JPEG conventions, as follows:
|
||||
|
||||
• If a JFIF APP0 marker segment is present, the colorspace is known to be either grayscale, YCbCr or CMYK.
|
||||
• If a JFIF APP0 marker segment is present, the colorspace should be either grayscale or YCbCr.
|
||||
If an APP2 marker segment containing an embedded ICC profile is also present, then YCbCr is converted to RGB according
|
||||
to the formulas given in the JFIF spec, and the ICC profile is assumed to refer to the resulting RGB space.
|
||||
CMYK data is read as is, and the ICC profile is assumed to refer to the resulting CMYK space.
|
||||
But, as software does not follow the spec, we can't really assume anything.
|
||||
|
||||
• If an Adobe APP14 marker segment is present, the colorspace is determined by consulting the transform flag.
|
||||
The transform flag takes one of three values:
|
||||
o 2 - The image is encoded as YCCK (implicitly converted from CMYK on encoding).
|
||||
o 1 - The image is encoded as YCbCr (implicitly converted from RGB on encoding).
|
||||
o 0 - Unknown. 3-channel images are assumed to be RGB, 4-channel images are assumed to be CMYK.
|
||||
o 0 - Unknown. 1-channel images are assumed to be Gray, 3-channel images are assumed to be RGB,
|
||||
4-channel images are assumed to be CMYK.
|
||||
|
||||
• If neither marker segment is present, the following procedure is followed: Single-channel images are assumed
|
||||
to be grayscale, and 2-channel images are assumed to be grayscale with an alpha channel. For 3- and 4-channel
|
||||
@@ -511,8 +543,10 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
if (adobeDCT != null) {
|
||||
switch (adobeDCT.getTransform()) {
|
||||
case AdobeDCTSegment.YCC:
|
||||
// TODO: Verify that startOfFrame has 3 components, otherwise issue warning and ignore adobeDCT
|
||||
return JPEGColorSpace.YCbCr;
|
||||
case AdobeDCTSegment.YCCK:
|
||||
// TODO: Verify that startOfFrame has 4 components, otherwise issue warning and ignore adobeDCT
|
||||
return JPEGColorSpace.YCCK;
|
||||
case AdobeDCTSegment.Unknown:
|
||||
if (startOfFrame.components.length == 1) {
|
||||
@@ -545,7 +579,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return JPEGColorSpace.PhotoYCC;
|
||||
}
|
||||
else {
|
||||
// if subsampled, YCbCr else RGB
|
||||
// If subsampled, YCbCr else RGB
|
||||
for (SOFComponent component : startOfFrame.components) {
|
||||
if (component.hSub != 1 || component.vSub != 1) {
|
||||
return JPEGColorSpace.YCbCr;
|
||||
@@ -571,7 +605,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return JPEGColorSpace.YCCK;
|
||||
}
|
||||
else {
|
||||
// if subsampled, YCCK else CMYK
|
||||
// TODO: JPEGMetadata (standard format) will report YCbCrA for 4 channel subsampled... :-/
|
||||
// If subsampled, YCCK else CMYK
|
||||
for (SOFComponent component : startOfFrame.components) {
|
||||
if (component.hSub != 1 || component.vSub != 1) {
|
||||
return JPEGColorSpace.YCCK;
|
||||
@@ -597,6 +632,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
byte[] profileData = profile.getData(); // Need to clone entire profile, due to a JDK 7 bug
|
||||
|
||||
if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) {
|
||||
processWarningOccurred("ICC profile is Perceptual but Display class, treating as Display class");
|
||||
|
||||
intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first
|
||||
|
||||
return ICC_Profile.getInstance(profileData);
|
||||
@@ -653,13 +690,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
private List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
|
||||
List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
|
||||
initHeader();
|
||||
|
||||
List<JPEGSegment> appSegments = Collections.emptyList();
|
||||
|
||||
for (JPEGSegment segment : segments) {
|
||||
if (segment.marker() == marker && (identifier == null || identifier.equals(segment.identifier()))) {
|
||||
if ((marker == ALL_APP_MARKERS && segment.marker() >= JPEG.APP0 && segment.marker() <= JPEG.APP15 || segment.marker() == marker)
|
||||
&& (identifier == null || identifier.equals(segment.identifier()))) {
|
||||
if (appSegments == Collections.EMPTY_LIST) {
|
||||
appSegments = new ArrayList<JPEGSegment>(segments.size());
|
||||
}
|
||||
@@ -671,7 +709,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return appSegments;
|
||||
}
|
||||
|
||||
private SOFSegment getSOF() throws IOException {
|
||||
SOFSegment getSOF() throws IOException {
|
||||
for (JPEGSegment segment : segments) {
|
||||
if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 ||
|
||||
JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 ||
|
||||
@@ -707,7 +745,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private AdobeDCTSegment getAdobeDCT() throws IOException {
|
||||
AdobeDCTSegment getAdobeDCT() throws IOException {
|
||||
// TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers
|
||||
List<JPEGSegment> adobe = getAppSegments(JPEG.APP14, "Adobe");
|
||||
|
||||
@@ -726,18 +764,18 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private JFIFSegment getJFIF() throws IOException{
|
||||
JFIFSegment getJFIF() throws IOException{
|
||||
List<JPEGSegment> jfif = getAppSegments(JPEG.APP0, "JFIF");
|
||||
|
||||
|
||||
if (!jfif.isEmpty()) {
|
||||
JPEGSegment segment = jfif.get(0);
|
||||
return JFIFSegment.read(segment.data());
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private JFXXSegment getJFXX() throws IOException {
|
||||
JFXXSegment getJFXX() throws IOException {
|
||||
List<JPEGSegment> jfxx = getAppSegments(JPEG.APP0, "JFXX");
|
||||
|
||||
if (!jfxx.isEmpty()) {
|
||||
@@ -748,6 +786,27 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private CompoundDirectory getExif() throws IOException {
|
||||
List<JPEGSegment> exifSegments = getAppSegments(JPEG.APP1, "Exif");
|
||||
|
||||
if (!exifSegments.isEmpty()) {
|
||||
JPEGSegment exif = exifSegments.get(0);
|
||||
InputStream data = exif.data();
|
||||
|
||||
if (data.read() == -1) { // Read pad
|
||||
processWarningOccurred("Exif chunk has no data.");
|
||||
}
|
||||
else {
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(data);
|
||||
return (CompoundDirectory) new EXIFReader().read(stream);
|
||||
|
||||
// TODO: Directory offset of thumbnail is wrong/relative to container stream, causing trouble for the EXIFReader...
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Util method?
|
||||
static byte[] readFully(DataInput stream, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
@@ -759,7 +818,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return data;
|
||||
}
|
||||
|
||||
private ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException {
|
||||
ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException {
|
||||
// ICC v 1.42 (2006) annex B:
|
||||
// APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination)
|
||||
// + 1 byte chunk number + 1 byte chunk count (allows ICC profiles chunked in multiple APP2 segments)
|
||||
@@ -893,7 +952,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
case JFXXSegment.JPEG:
|
||||
case JFXXSegment.INDEXED:
|
||||
case JFXXSegment.RGB:
|
||||
thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfxx));
|
||||
thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), imageIndex, thumbnails.size(), jfxx));
|
||||
break;
|
||||
default:
|
||||
processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode);
|
||||
@@ -911,17 +970,23 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
processWarningOccurred("Exif chunk has no data.");
|
||||
}
|
||||
else {
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(data);
|
||||
ImageInputStream stream = new MemoryCacheImageInputStream(data);
|
||||
CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream);
|
||||
|
||||
if (exifMetadata.directoryCount() == 2) {
|
||||
Directory ifd1 = exifMetadata.getDirectory(1);
|
||||
|
||||
Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
|
||||
|
||||
// 1 = no compression, 6 = JPEG compression (default)
|
||||
if (compression == null || compression.getValue().equals(1) || compression.getValue().equals(6)) {
|
||||
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, 0, thumbnails.size(), ifd1, stream));
|
||||
Entry jpegLength = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
||||
|
||||
if ((jpegLength == null || ((Number) jpegLength.getValue()).longValue() > 0)) {
|
||||
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream));
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with empty (zero-length) thumbnail");
|
||||
}
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression.getValue());
|
||||
@@ -932,6 +997,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
ImageReader getThumbnailReader() throws IOException {
|
||||
if (thumbnailReader == null) {
|
||||
thumbnailReader = delegate.getOriginatingProvider().createReaderInstance();
|
||||
}
|
||||
|
||||
return thumbnailReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumThumbnails(final int imageIndex) throws IOException {
|
||||
readThumbnailMetadata(imageIndex);
|
||||
@@ -965,44 +1038,21 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return thumbnails.get(thumbnailIndex).read();
|
||||
}
|
||||
|
||||
|
||||
// Metadata
|
||||
|
||||
@Override
|
||||
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
|
||||
// TODO: Nice try, but no cigar.. getAsTree does not return a "live" view, so any modifications are thrown away
|
||||
IIOMetadata metadata = delegate.getImageMetadata(imageIndex);
|
||||
IIOMetadata imageMetadata = delegate.getImageMetadata(imageIndex);
|
||||
|
||||
// IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
// Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0);
|
||||
if (imageMetadata != null && Arrays.asList(imageMetadata.getMetadataFormatNames()).contains(JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0)) {
|
||||
if (metadataCleaner == null) {
|
||||
metadataCleaner = new JPEGImage10MetadataCleaner(this);
|
||||
}
|
||||
|
||||
// TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node.
|
||||
// As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like:
|
||||
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
|
||||
/*
|
||||
from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
||||
return metadataCleaner.cleanMetadata(imageMetadata);
|
||||
}
|
||||
|
||||
In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif)
|
||||
by defining other types of nodes which may appear as a child of the JPEGvariety node.
|
||||
|
||||
(Note that an application wishing to interpret Exif metadata given a metadata tree structure in the
|
||||
javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an
|
||||
APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific
|
||||
code to interpret the data in the marker segment. If such an application were to encounter a metadata tree
|
||||
formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be
|
||||
unknown in that format - it might be structured as a child node of the JPEGvariety node.
|
||||
|
||||
Thus, it is important for an application to specify which version to use by passing the string identifying
|
||||
the version to the method/constructor used to obtain an IIOMetadata object.)
|
||||
*/
|
||||
|
||||
// IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC");
|
||||
// app2ICC.setUserObject(getEmbeddedICCProfile());
|
||||
// jpegVariety.getFirstChild().appendChild(app2ICC);
|
||||
|
||||
// new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false);
|
||||
|
||||
return metadata;
|
||||
return imageMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1010,6 +1060,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return delegate.getStreamMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processWarningOccurred(String warning) {
|
||||
super.processWarningOccurred(warning);
|
||||
}
|
||||
|
||||
private static void invertCMYK(final Raster raster) {
|
||||
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
@@ -1321,10 +1376,10 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// start = System.currentTimeMillis();
|
||||
float aspect = reader.getAspectRatio(0);
|
||||
if (aspect >= 1f) {
|
||||
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT);
|
||||
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_SMOOTH);
|
||||
}
|
||||
else {
|
||||
image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_DEFAULT);
|
||||
image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_SMOOTH);
|
||||
}
|
||||
// System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
@@ -1332,6 +1387,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(0), reader.getHeight(0)));
|
||||
|
||||
try {
|
||||
IIOMetadata imageMetadata = reader.getImageMetadata(0);
|
||||
System.out.println("Metadata for File: " + file.getName());
|
||||
System.out.println("Native:");
|
||||
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(imageMetadata.getNativeMetadataFormatName()), false);
|
||||
System.out.println("Standard:");
|
||||
new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
|
||||
System.out.println();
|
||||
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
for (int i = 0; i < numThumbnails; i++) {
|
||||
BufferedImage thumbnail = reader.readThumbnail(0, i);
|
||||
@@ -1340,7 +1403,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
System.err.println("Could not read thumbnails: " + e.getMessage());
|
||||
System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadataFormat;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
@@ -65,7 +67,7 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"jpg", "jpeg"},
|
||||
new String[]{"image/jpeg"},
|
||||
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriterSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
@@ -84,14 +86,14 @@ public class JPEGImageReaderSpi extends ImageReaderSpi {
|
||||
}
|
||||
|
||||
static ImageReaderSpi lookupDelegateProvider(final ServiceRegistry registry) {
|
||||
// Should be safe to lookup now, as the bundled providers are hardcoded usually
|
||||
try {
|
||||
return (ImageReaderSpi) registry.getServiceProviderByClass(Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi"));
|
||||
}
|
||||
catch (ClassNotFoundException ignore) {
|
||||
}
|
||||
catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
Iterator<ImageReaderSpi> providers = registry.getServiceProviders(ImageReaderSpi.class, true);
|
||||
|
||||
while (providers.hasNext()) {
|
||||
ImageReaderSpi provider = providers.next();
|
||||
|
||||
if (provider.getClass().getName().equals("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi")) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -37,8 +37,10 @@ import javax.imageio.ImageWriter;
|
||||
import javax.imageio.metadata.IIOMetadataFormat;
|
||||
import javax.imageio.spi.ImageWriterSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
@@ -66,9 +68,9 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
|
||||
new String[]{"JPEG", "jpeg", "JPG", "jpg"},
|
||||
new String[]{"jpg", "jpeg"},
|
||||
new String[]{"image/jpeg"},
|
||||
"twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter",
|
||||
STANDARD_OUTPUT_TYPE,
|
||||
new String[] {"twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"},
|
||||
"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter",
|
||||
new Class[] { ImageOutputStream.class },
|
||||
new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
);
|
||||
@@ -86,14 +88,14 @@ public class JPEGImageWriterSpi extends ImageWriterSpi {
|
||||
}
|
||||
|
||||
static ImageWriterSpi lookupDelegateProvider(final ServiceRegistry registry) {
|
||||
// Should be safe to lookup now, as the bundled providers are hardcoded usually
|
||||
try {
|
||||
return (ImageWriterSpi) registry.getServiceProviderByClass(Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageWriterSpi"));
|
||||
}
|
||||
catch (ClassNotFoundException ignore) {
|
||||
}
|
||||
catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
Iterator<ImageWriterSpi> providers = registry.getServiceProviders(ImageWriterSpi.class, true);
|
||||
|
||||
while (providers.hasNext()) {
|
||||
ImageWriterSpi provider = providers.next();
|
||||
|
||||
if (provider.getClass().getName().equals("com.sun.imageio.plugins.jpeg.JPEGImageWriterSpi")) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -91,16 +91,29 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
long realPosition = stream.getStreamPosition();
|
||||
int marker = stream.readUnsignedShort();
|
||||
|
||||
// Skip over weird 0x00 padding, but leave in stream, read seems to handle it well with a warning
|
||||
int trash = 0;
|
||||
while (marker == 0) {
|
||||
marker = stream.readUnsignedShort();
|
||||
trash += 2;
|
||||
}
|
||||
|
||||
if (marker == 0x00ff) {
|
||||
trash++;
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
// Skip over 0xff padding between markers
|
||||
while (marker == 0xffff) {
|
||||
realPosition++;
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
// TODO: Optionally skip JFIF only for non-JFIF conformant streams
|
||||
// TODO: Refactor to make various segments optional, we probably only want the "Adobe" APP14 segment, 'Exif' APP1 and very few others
|
||||
if (isAppSegmentMarker(marker) && marker != JPEG.APP0 && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) {
|
||||
if (isAppSegmentMarker(marker) && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) {
|
||||
int length = stream.readUnsignedShort(); // Length including length field itself
|
||||
stream.seek(realPosition + 2 + length); // Skip marker (2) + length
|
||||
stream.seek(realPosition + trash + 2 + length); // Skip marker (2) + length
|
||||
}
|
||||
else {
|
||||
if (marker == JPEG.EOI) {
|
||||
@@ -116,7 +129,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
}
|
||||
else {
|
||||
// Length including length field itself
|
||||
length = stream.readUnsignedShort() + 2;
|
||||
length = trash + stream.readUnsignedShort() + 2;
|
||||
}
|
||||
|
||||
segment = new Segment(marker, realPosition, segment.end(), length);
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.image.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* ThumbnailReader
|
||||
@@ -42,6 +42,7 @@ import java.io.InputStream;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: ThumbnailReader.java,v 1.0 18.04.12 12:22 haraldk Exp$
|
||||
*/
|
||||
// TODO: Get rid of the com.sun import!!
|
||||
abstract class ThumbnailReader {
|
||||
|
||||
private final ThumbnailReadProgressListener progressListener;
|
||||
@@ -49,10 +50,11 @@ abstract class ThumbnailReader {
|
||||
protected final int thumbnailIndex;
|
||||
|
||||
protected ThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex) {
|
||||
this.progressListener = progressListener;
|
||||
this.progressListener = progressListener != null ? progressListener : new NullProgressListener();
|
||||
this.imageIndex = imageIndex;
|
||||
this.thumbnailIndex = thumbnailIndex;
|
||||
}
|
||||
|
||||
protected final void processThumbnailStarted() {
|
||||
progressListener.processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
}
|
||||
@@ -65,8 +67,20 @@ abstract class ThumbnailReader {
|
||||
progressListener.processThumbnailComplete();
|
||||
}
|
||||
|
||||
static protected BufferedImage readJPEGThumbnail(InputStream stream) throws IOException {
|
||||
return ImageIO.read(stream);
|
||||
static protected BufferedImage readJPEGThumbnail(final ImageReader reader, final ImageInputStream stream) throws IOException {
|
||||
// try {
|
||||
// try {
|
||||
reader.setInput(stream);
|
||||
|
||||
return reader.read(0);
|
||||
// }
|
||||
// finally {
|
||||
// input.close();
|
||||
// }
|
||||
// }
|
||||
// finally {
|
||||
// reader.dispose();
|
||||
// }
|
||||
}
|
||||
|
||||
static protected BufferedImage readRawThumbnail(final byte[] thumbnail, final int size, final int offset, int w, int h) {
|
||||
@@ -82,4 +96,15 @@ abstract class ThumbnailReader {
|
||||
public abstract int getWidth() throws IOException;
|
||||
|
||||
public abstract int getHeight() throws IOException;
|
||||
|
||||
private static class NullProgressListener implements ThumbnailReadProgressListener {
|
||||
public void processThumbnailStarted(int imageIndex, int thumbnailIndex) {
|
||||
}
|
||||
|
||||
public void processThumbnailProgress(float percentageDone) {
|
||||
}
|
||||
|
||||
public void processThumbnailComplete() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
|
||||
assertEquals(2, ifds.directoryCount());
|
||||
|
||||
return new EXIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
|
||||
return new EXIFThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("JPEG").next(), imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
@@ -63,7 +64,7 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
assertFalse(segments.isEmpty());
|
||||
|
||||
JPEGSegment jfxx = segments.get(0);
|
||||
return new JFXXThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length()));
|
||||
return new JFXXThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("jpeg").next(), imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -29,23 +29,31 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
|
||||
import org.hamcrest.core.IsInstanceOf;
|
||||
import org.junit.Test;
|
||||
import org.mockito.internal.matchers.GreaterThan;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReadParam;
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;
|
||||
import javax.imageio.plugins.jpeg.JPEGQTable;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
@@ -321,6 +329,25 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
verify(warningListener).warningOccurred(eq(reader), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorbisRGB() throws IOException {
|
||||
// Special case, throws exception below without special treatment
|
||||
// java.awt.color.CMMException: General CMM error517
|
||||
JPEGImageReader reader = createReader();
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmm-exception-corbis-rgb.jpg")));
|
||||
|
||||
assertEquals(512, reader.getWidth(0));
|
||||
assertEquals(384, reader.getHeight(0));
|
||||
|
||||
BufferedImage image = reader.read(0);
|
||||
|
||||
assertNotNull(image);
|
||||
assertEquals(512, image.getWidth());
|
||||
assertEquals(384, image.getHeight());
|
||||
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasThumbnailNoIFD1() throws IOException {
|
||||
JPEGImageReader reader = createReader();
|
||||
@@ -600,10 +627,59 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNoJFIFYCbCr() throws IOException {
|
||||
// Basically the same issue as http://stackoverflow.com/questions/9340569/jpeg-image-with-wrong-colors
|
||||
JPEGImageReader reader = createReader();
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-jfif-ycbcr.jpg")));
|
||||
|
||||
assertEquals(310, reader.getWidth(0));
|
||||
assertEquals(206, reader.getHeight(0));
|
||||
|
||||
ImageReadParam param = reader.getDefaultReadParam();
|
||||
param.setSourceRegion(new Rectangle(0, 0, 310, 8));
|
||||
BufferedImage image = reader.read(0, param);
|
||||
assertNotNull(image);
|
||||
assertEquals(310, image.getWidth());
|
||||
assertEquals(8, image.getHeight());
|
||||
|
||||
int[] expectedRGB = new int[] {
|
||||
0xff3c1b14, 0xff35140b, 0xff4b2920, 0xff3b160e, 0xff49231a, 0xff874e3d, 0xff563d27, 0xff926c61,
|
||||
0xff350005, 0xff84432d, 0xff754f46, 0xff2c2223, 0xff422016, 0xff220f0b, 0xff251812, 0xff1c1209,
|
||||
0xff483429, 0xff1b140c, 0xff231c16, 0xff2f261f, 0xff2e2923, 0xff170c08, 0xff383025, 0xff443b34,
|
||||
0xff574a39, 0xff3b322b, 0xffeee1d0, 0xffebdecd, 0xffe9dccb, 0xffe8dbca, 0xffe7dcca,
|
||||
};
|
||||
|
||||
// Validate strip colors
|
||||
for (int i = 0; i < image.getWidth() / 10; i++) {
|
||||
int actualRGB = image.getRGB(i * 10, 7);
|
||||
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
|
||||
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
|
||||
assertEquals((actualRGB ) & 0xff, (expectedRGB[i] ) & 0xff, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXDensityOutOfRangeIssue() throws IOException {
|
||||
// Image has JFIF with x/y density 0
|
||||
JPEGImageReader reader = createReader();
|
||||
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/xdensity-out-of-range-zero.jpg")));
|
||||
|
||||
IIOMetadata imageMetadata = reader.getImageMetadata(0);
|
||||
assertNotNull(imageMetadata);
|
||||
|
||||
// Assume that the aspect ratio is 1 if both x/y density is 0.
|
||||
IIOMetadataNode tree = (IIOMetadataNode) imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
|
||||
NodeList dimensions = tree.getElementsByTagName("Dimension");
|
||||
assertEquals(1, dimensions.getLength());
|
||||
assertEquals("PixelAspectRatio", dimensions.item(0).getFirstChild().getNodeName());
|
||||
assertEquals("1.0", ((Element) dimensions.item(0).getFirstChild()).getAttribute("value"));
|
||||
}
|
||||
|
||||
// TODO: Test RGBA/YCbCrA handling
|
||||
|
||||
@Test
|
||||
public void testReadMetadataMaybeNull() throws IOException {
|
||||
public void testReadMetadata() throws IOException {
|
||||
// Just test that we can read the metadata without exceptions
|
||||
JPEGImageReader reader = createReader();
|
||||
|
||||
@@ -614,11 +690,276 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
try {
|
||||
IIOMetadata metadata = reader.getImageMetadata(i);
|
||||
assertNotNull(String.format("Image metadata null for %s image %s", testData, i), metadata);
|
||||
|
||||
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
assertNotNull(tree);
|
||||
assertThat(tree, new IsInstanceOf(IIOMetadataNode.class));
|
||||
|
||||
IIOMetadataNode iioTree = (IIOMetadataNode) tree;
|
||||
assertEquals(1, iioTree.getElementsByTagName("JPEGvariety").getLength());
|
||||
Node jpegVariety = iioTree.getElementsByTagName("JPEGvariety").item(0);
|
||||
assertNotNull(jpegVariety);
|
||||
|
||||
Node app0JFIF = jpegVariety.getFirstChild();
|
||||
if (app0JFIF != null) {
|
||||
assertEquals("app0JFIF", app0JFIF.getLocalName());
|
||||
}
|
||||
|
||||
NodeList markerSequences = iioTree.getElementsByTagName("markerSequence");
|
||||
assertTrue(markerSequences.getLength() == 1 || markerSequences.getLength() == 2); // In case of JPEG encoded thumbnail, there will be 2
|
||||
IIOMetadataNode markerSequence = (IIOMetadataNode) markerSequences.item(0);
|
||||
assertNotNull(markerSequence);
|
||||
assertThat(markerSequence.getChildNodes().getLength(), new GreaterThan<Integer>(0));
|
||||
|
||||
NodeList unknowns = markerSequence.getElementsByTagName("unknown");
|
||||
for (int j = 0; j < unknowns.getLength(); j++) {
|
||||
IIOMetadataNode unknown = (IIOMetadataNode) unknowns.item(j);
|
||||
assertNotNull(unknown.getUserObject()); // All unknowns must have user object (data array)
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
System.err.println(String.format("WARNING: Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
|
||||
fail(String.format("Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadInconsistentMetadata() throws IOException {
|
||||
// A collection of JPEG files that makes the JPEGImageReader throw exception "Inconsistent metadata read from stream"...
|
||||
List<String> resources = Arrays.asList(
|
||||
"/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg", // Ok
|
||||
"/jpeg/gray-sample.jpg", // Ok
|
||||
"/jpeg/cmyk-sample.jpg",
|
||||
"/jpeg/cmyk-sample-multiple-chunk-icc.jpg",
|
||||
"/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg",
|
||||
"/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg"
|
||||
);
|
||||
|
||||
for (String resource : resources) {
|
||||
// Just test that we can read the metadata without exceptions
|
||||
JPEGImageReader reader = createReader();
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource(resource));
|
||||
|
||||
try {
|
||||
reader.setInput(stream);
|
||||
IIOMetadata metadata = reader.getImageMetadata(0);
|
||||
assertNotNull(String.format("%s: null metadata", resource), metadata);
|
||||
|
||||
Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
assertNotNull(tree);
|
||||
// new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false);
|
||||
|
||||
}
|
||||
catch (IIOException e) {
|
||||
AssertionError fail = new AssertionError(String.format("Reading metadata failed for %ss: %s", resource, e.getMessage()));
|
||||
fail.initCause(e);
|
||||
throw fail;
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadMetadataEqualReference() throws IOException {
|
||||
// Compares the metadata for JFIF-conformant files with metadata from com.sun...JPEGImageReader
|
||||
JPEGImageReader reader = createReader();
|
||||
ImageReader referenceReader;
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<ImageReaderSpi> spiClass = (Class<ImageReaderSpi>) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi");
|
||||
ImageReaderSpi provider = spiClass.newInstance();
|
||||
referenceReader = provider.createReaderInstance();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
System.err.println("WARNING: Could not create ImageReader for reference (missing dependency): " + t.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
for (TestData testData : getTestData()) {
|
||||
reader.setInput(testData.getInputStream());
|
||||
referenceReader.setInput(testData.getInputStream());
|
||||
|
||||
for (int i = 0; i < reader.getNumImages(true); i++) {
|
||||
try {
|
||||
IIOMetadata reference = referenceReader.getImageMetadata(i);
|
||||
|
||||
try {
|
||||
IIOMetadata metadata = reader.getImageMetadata(i);
|
||||
|
||||
String[] formatNames = reference.getMetadataFormatNames();
|
||||
for (String formatName : formatNames) {
|
||||
Node referenceTree = reference.getAsTree(formatName);
|
||||
Node actualTree = metadata.getAsTree(formatName);
|
||||
|
||||
// new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(actualTree, false);
|
||||
assertTreesEquals(String.format("Metadata differs for %s image %s ", testData, i), referenceTree, actualTree);
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
AssertionError fail = new AssertionError(String.format("Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
|
||||
fail.initCause(e);
|
||||
throw fail;
|
||||
}
|
||||
}
|
||||
catch (IIOException ignore) {
|
||||
// The reference reader will fail on certain images, we'll just ignore that
|
||||
System.err.println(String.format("WARNING: Reading reference metadata failed for %s image %s: %s", testData, i, ignore.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTreesEquals(String message, Node expectedTree, Node actualTree) {
|
||||
if (expectedTree == actualTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectedTree == null) {
|
||||
assertNull(actualTree);
|
||||
}
|
||||
|
||||
assertEquals(String.format("%s: Node names differ", message), expectedTree.getNodeName(), actualTree.getNodeName());
|
||||
|
||||
NamedNodeMap expectedAttributes = expectedTree.getAttributes();
|
||||
NamedNodeMap actualAttributes = actualTree.getAttributes();
|
||||
assertEquals(String.format("%s: Number of attributes for <%s> differ", message, expectedTree.getNodeName()), expectedAttributes.getLength(), actualAttributes.getLength());
|
||||
for (int i = 0; i < expectedAttributes.getLength(); i++) {
|
||||
Node item = expectedAttributes.item(i);
|
||||
assertEquals(String.format("%s: \"%s\" attribute for <%s> differ", message, item.getNodeName(), expectedTree.getNodeName()), item.getNodeValue(), actualAttributes.getNamedItem(item.getNodeName()).getNodeValue());
|
||||
}
|
||||
|
||||
// Test for equal user objects.
|
||||
// - array equals or reflective equality... Most user objects does not have a decent equals method.. :-P
|
||||
if (expectedTree instanceof IIOMetadataNode) {
|
||||
assertTrue(String.format("%s: %s not an IIOMetadataNode", message, expectedTree.getNodeName()), actualTree instanceof IIOMetadataNode);
|
||||
|
||||
Object expectedUserObject = ((IIOMetadataNode) expectedTree).getUserObject();
|
||||
|
||||
if (expectedUserObject != null) {
|
||||
Object actualUserObject = ((IIOMetadataNode) actualTree).getUserObject();
|
||||
assertNotNull(String.format("%s: User object missing for <%s>", message, expectedTree.getNodeName()), actualUserObject);
|
||||
assertEqualUserObjects(String.format("%s: User objects for <%s MarkerTag\"%s\"> differ", message, expectedTree.getNodeName(), ((IIOMetadataNode) expectedTree).getAttribute("MarkerTag")), expectedUserObject, actualUserObject);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort nodes to make sure that sequence of equally named tags does not matter
|
||||
List<IIOMetadataNode> expectedChildren = sortNodes(expectedTree.getChildNodes());
|
||||
List<IIOMetadataNode> actualChildren = sortNodes(actualTree.getChildNodes());
|
||||
|
||||
assertEquals(String.format("%s: Number of child nodes for %s differ", message, expectedTree.getNodeName()), expectedChildren.size(), actualChildren.size());
|
||||
|
||||
for (int i = 0; i < expectedChildren.size(); i++) {
|
||||
assertTreesEquals(message + "<" + expectedTree.getNodeName() + ">", expectedChildren.get(i), actualChildren.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertEqualUserObjects(String message, Object expectedUserObject, Object actualUserObject) {
|
||||
if (expectedUserObject.equals(actualUserObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectedUserObject instanceof ICC_Profile) {
|
||||
if (actualUserObject instanceof ICC_Profile) {
|
||||
assertArrayEquals(message, ((ICC_Profile) expectedUserObject).getData(), ((ICC_Profile) actualUserObject).getData());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (expectedUserObject instanceof byte[]) {
|
||||
if (actualUserObject instanceof byte[]) {
|
||||
assertArrayEquals(message, (byte[]) expectedUserObject, (byte[]) actualUserObject);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (expectedUserObject instanceof JPEGHuffmanTable) {
|
||||
if (actualUserObject instanceof JPEGHuffmanTable) {
|
||||
assertArrayEquals(message, ((JPEGHuffmanTable) expectedUserObject).getLengths(), ((JPEGHuffmanTable) actualUserObject).getLengths());
|
||||
assertArrayEquals(message, ((JPEGHuffmanTable) expectedUserObject).getValues(), ((JPEGHuffmanTable) actualUserObject).getValues());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (expectedUserObject instanceof JPEGQTable) {
|
||||
if (actualUserObject instanceof JPEGQTable) {
|
||||
assertArrayEquals(message, ((JPEGQTable) expectedUserObject).getTable(), ((JPEGQTable) actualUserObject).getTable());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fail(expectedUserObject.getClass().getName());
|
||||
}
|
||||
|
||||
private List<IIOMetadataNode> sortNodes(final NodeList nodes) {
|
||||
ArrayList<IIOMetadataNode> sortedNodes = new ArrayList<IIOMetadataNode>(new AbstractList<IIOMetadataNode>() {
|
||||
@Override
|
||||
public IIOMetadataNode get(int index) {
|
||||
return (IIOMetadataNode) nodes.item(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return nodes.getLength();
|
||||
}
|
||||
});
|
||||
|
||||
Collections.sort(
|
||||
sortedNodes,
|
||||
new Comparator<IIOMetadataNode>() {
|
||||
public int compare(IIOMetadataNode left, IIOMetadataNode right) {
|
||||
int res = left.getNodeName().compareTo(right.getNodeName());
|
||||
if (res != 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// Compare attribute values
|
||||
NamedNodeMap leftAttributes = left.getAttributes(); // TODO: We should sort left's attributes as well, for stable sorting + handle diffs in attributes
|
||||
NamedNodeMap rightAttributes = right.getAttributes();
|
||||
|
||||
for (int i = 0; i < leftAttributes.getLength(); i++) {
|
||||
Node leftAttribute = leftAttributes.item(i);
|
||||
Node rightAttribute = rightAttributes.getNamedItem(leftAttribute.getNodeName());
|
||||
|
||||
if (rightAttribute == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
res = leftAttribute.getNodeValue().compareTo(rightAttribute.getNodeValue());
|
||||
if (res != 0) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
if (left.getUserObject() instanceof byte[] && right.getUserObject() instanceof byte[]) {
|
||||
byte[] leftBytes = (byte[]) left.getUserObject();
|
||||
byte[] rightBytes = (byte[]) right.getUserObject();
|
||||
|
||||
if (leftBytes.length < rightBytes.length) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (leftBytes.length > rightBytes.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (leftBytes.length > 0) {
|
||||
for (int i = 0; i < leftBytes.length; i++) {
|
||||
if (leftBytes[i] < rightBytes[i]) {
|
||||
return -1;
|
||||
}
|
||||
if (leftBytes[i] > rightBytes[i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return sortedNodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
public void testStreamRealData() throws IOException {
|
||||
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg")));
|
||||
assertEquals(JPEG.SOI, stream.readUnsignedShort());
|
||||
assertEquals(JPEG.APP0, stream.readUnsignedShort());
|
||||
assertEquals(JPEG.DQT, stream.readUnsignedShort());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,7 +88,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
// NOTE: read(byte[], int, int) must always read len bytes (or until EOF), due to known bug in Sun code
|
||||
assertEquals(20, stream.read(bytes, 0, 20));
|
||||
|
||||
assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, 0x0, 0x10, 'J', 'F', 'I', 'F', 0x0, 0x1, 0x1, 0x1, 0x1, (byte) 0xCC, 0x1, (byte) 0xCC, 0, 0}, bytes);
|
||||
assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xDB, 0x0, 0x43, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}, bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,7 +102,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
|
||||
assertThat(length, new LessOrEqual<Long>(10203l)); // In no case should length increase
|
||||
|
||||
assertEquals(9625l, length); // May change, if more chunks are passed to reader...
|
||||
assertEquals(9607L, length); // May change, if more chunks are passed to reader...
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -110,18 +110,15 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg")));
|
||||
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
|
||||
|
||||
assertEquals(3, appSegments.size());
|
||||
assertEquals(2, appSegments.size());
|
||||
|
||||
assertEquals(JPEG.APP0, appSegments.get(0).marker());
|
||||
assertEquals("JFIF", appSegments.get(0).identifier());
|
||||
assertEquals(JPEG.APP1, appSegments.get(0).marker());
|
||||
assertEquals("Exif", appSegments.get(0).identifier());
|
||||
|
||||
assertEquals(JPEG.APP1, appSegments.get(1).marker());
|
||||
assertEquals("Exif", appSegments.get(1).identifier());
|
||||
assertEquals(JPEG.APP14, appSegments.get(1).marker());
|
||||
assertEquals("Adobe", appSegments.get(1).identifier());
|
||||
|
||||
assertEquals(JPEG.APP14, appSegments.get(2).marker());
|
||||
assertEquals("Adobe", appSegments.get(2).identifier());
|
||||
|
||||
// And thus, no XMP, no ICC_PROFILE or other segments
|
||||
// And thus, no JFIF, no XMP, no ICC_PROFILE or other segments
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -133,7 +130,7 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
length++;
|
||||
}
|
||||
|
||||
assertEquals(9299l, length); // Sanity check: same as file size
|
||||
assertEquals(9281L, length); // Sanity check: same as file size
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,13 +138,10 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg")));
|
||||
|
||||
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
|
||||
assertEquals(2, appSegments.size());
|
||||
assertEquals(1, appSegments.size());
|
||||
|
||||
assertEquals(JPEG.APP0, appSegments.get(0).marker());
|
||||
assertEquals("JFIF", appSegments.get(0).identifier());
|
||||
|
||||
assertEquals(JPEG.APP1, appSegments.get(1).marker());
|
||||
assertEquals("Exif", appSegments.get(1).identifier());
|
||||
assertEquals(JPEG.APP1, appSegments.get(0).marker());
|
||||
assertEquals("Exif", appSegments.get(0).identifier());
|
||||
|
||||
stream.seek(0l);
|
||||
|
||||
@@ -156,6 +150,6 @@ public class JPEGSegmentImageInputStreamTest {
|
||||
length++;
|
||||
}
|
||||
|
||||
assertEquals(1079L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
|
||||
assertEquals(1061L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg
Normal file
BIN
imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
@@ -44,7 +44,7 @@ import java.util.Arrays;
|
||||
public abstract class AbstractEntry implements Entry {
|
||||
|
||||
private final Object identifier;
|
||||
private final Object value; // TODO: Might need to be mutable..
|
||||
private final Object value; // Entries are immutable, directories can be mutated
|
||||
|
||||
protected AbstractEntry(final Object identifier, final Object value) {
|
||||
Validate.notNull(identifier, "identifier");
|
||||
@@ -181,10 +181,10 @@ public abstract class AbstractEntry implements Entry {
|
||||
@Override
|
||||
public String toString() {
|
||||
String name = getFieldName();
|
||||
String nameStr = name != null ? "/" + name + "" : "";
|
||||
String nameStr = name != null ? String.format("/%s", name) : "";
|
||||
|
||||
String type = getTypeName();
|
||||
String typeStr = type != null ? " (" + type + ")" : "";
|
||||
String typeStr = type != null ? String.format(" (%s)", type) : "";
|
||||
|
||||
return String.format("%s%s: %s%s", getNativeIdentifier(), nameStr, getValueAsString(), typeStr);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ final class EXIFEntry extends AbstractEntry {
|
||||
if (type < 1 || type > TIFF.TYPE_NAMES.length) {
|
||||
throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", type));
|
||||
}
|
||||
|
||||
// TODO: Validate that type is applicable to value?
|
||||
|
||||
this.type = type;
|
||||
}
|
||||
@@ -92,6 +94,8 @@ final class EXIFEntry extends AbstractEntry {
|
||||
return "SamplesPerPixels";
|
||||
case TIFF.TAG_ROWS_PER_STRIP:
|
||||
return "RowsPerStrip";
|
||||
case TIFF.TAG_STRIP_BYTE_COUNTS:
|
||||
return "StripByteCounts";
|
||||
case TIFF.TAG_X_RESOLUTION:
|
||||
return "XResolution";
|
||||
case TIFF.TAG_Y_RESOLUTION:
|
||||
|
||||
@@ -110,7 +110,6 @@ public final class EXIFReader extends MetadataReader {
|
||||
|
||||
// Read linked IFDs
|
||||
if (nextOffset != 0) {
|
||||
// TODO: This is probably not okay anymore.. Replace recursion with while loop
|
||||
AbstractCompoundDirectory next = (AbstractCompoundDirectory) readDirectory(pInput, nextOffset);
|
||||
for (int i = 0; i < next.directoryCount(); i++) {
|
||||
ifds.add((IFD) next.getDirectory(i));
|
||||
@@ -298,7 +297,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
long pos = pInput.getStreamPosition();
|
||||
|
||||
switch (pType) {
|
||||
case 2: // ASCII
|
||||
case TIFF.TYPE_ASCII:
|
||||
// TODO: This might be UTF-8 or ISO-8859-x, even though spec says NULL-terminated 7 bit ASCII
|
||||
// TODO: Fail if unknown chars, try parsing with ISO-8859-1 or file.encoding
|
||||
if (pCount == 0) {
|
||||
@@ -308,17 +307,17 @@ public final class EXIFReader extends MetadataReader {
|
||||
pInput.readFully(ascii);
|
||||
int len = ascii[ascii.length - 1] == 0 ? ascii.length - 1 : ascii.length;
|
||||
return StringUtil.decode(ascii, 0, len, "UTF-8"); // UTF-8 is ASCII compatible
|
||||
case 1: // BYTE
|
||||
case TIFF.TYPE_BYTE:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedByte();
|
||||
}
|
||||
// else fall through
|
||||
case 6: // SBYTE
|
||||
case TIFF.TYPE_SBYTE:
|
||||
if (pCount == 1) {
|
||||
return pInput.readByte();
|
||||
}
|
||||
// else fall through
|
||||
case 7: // UNDEFINED
|
||||
case TIFF.TYPE_UNDEFINED:
|
||||
byte[] bytes = new byte[pCount];
|
||||
pInput.readFully(bytes);
|
||||
|
||||
@@ -326,11 +325,11 @@ public final class EXIFReader extends MetadataReader {
|
||||
// binary data and we want to keep that as a byte array for clients to parse futher
|
||||
|
||||
return bytes;
|
||||
case 3: // SHORT
|
||||
case TIFF.TYPE_SHORT:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedShort();
|
||||
}
|
||||
case 8: // SSHORT
|
||||
case TIFF.TYPE_SSHORT:
|
||||
if (pCount == 1) {
|
||||
return pInput.readShort();
|
||||
}
|
||||
@@ -338,21 +337,22 @@ public final class EXIFReader extends MetadataReader {
|
||||
short[] shorts = new short[pCount];
|
||||
pInput.readFully(shorts, 0, shorts.length);
|
||||
|
||||
if (pType == 3) {
|
||||
if (pType == TIFF.TYPE_SHORT) {
|
||||
int[] ints = new int[pCount];
|
||||
for (int i = 0; i < pCount; i++) {
|
||||
ints[i] = shorts[i] & 0xffff;
|
||||
}
|
||||
|
||||
return ints;
|
||||
}
|
||||
|
||||
return shorts;
|
||||
case 13: // IFD
|
||||
case 4: // LONG
|
||||
case TIFF.TYPE_IFD:
|
||||
case TIFF.TYPE_LONG:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedInt();
|
||||
}
|
||||
case 9: // SLONG
|
||||
case TIFF.TYPE_SLONG:
|
||||
if (pCount == 1) {
|
||||
return pInput.readInt();
|
||||
}
|
||||
@@ -360,16 +360,17 @@ public final class EXIFReader extends MetadataReader {
|
||||
int[] ints = new int[pCount];
|
||||
pInput.readFully(ints, 0, ints.length);
|
||||
|
||||
if (pType == 4 || pType == 13) {
|
||||
if (pType == TIFF.TYPE_LONG || pType == TIFF.TYPE_IFD) {
|
||||
long[] longs = new long[pCount];
|
||||
for (int i = 0; i < pCount; i++) {
|
||||
longs[i] = ints[i] & 0xffffffffL;
|
||||
}
|
||||
|
||||
return longs;
|
||||
}
|
||||
|
||||
return ints;
|
||||
case 11: // FLOAT
|
||||
case TIFF.TYPE_FLOAT:
|
||||
if (pCount == 1) {
|
||||
return pInput.readFloat();
|
||||
}
|
||||
@@ -377,7 +378,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
float[] floats = new float[pCount];
|
||||
pInput.readFully(floats, 0, floats.length);
|
||||
return floats;
|
||||
case 12: // DOUBLE
|
||||
case TIFF.TYPE_DOUBLE:
|
||||
if (pCount == 1) {
|
||||
return pInput.readDouble();
|
||||
}
|
||||
@@ -386,7 +387,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
pInput.readFully(doubles, 0, doubles.length);
|
||||
return doubles;
|
||||
|
||||
case 5: // RATIONAL
|
||||
case TIFF.TYPE_RATIONAL:
|
||||
if (pCount == 1) {
|
||||
return createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt());
|
||||
}
|
||||
@@ -397,7 +398,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
}
|
||||
|
||||
return rationals;
|
||||
case 10: // SRATIONAL
|
||||
case TIFF.TYPE_SRATIONAL:
|
||||
if (pCount == 1) {
|
||||
return createSafeRational(pInput.readInt(), pInput.readInt());
|
||||
}
|
||||
@@ -445,7 +446,7 @@ public final class EXIFReader extends MetadataReader {
|
||||
return new Rational(numerator, denominator);
|
||||
}
|
||||
|
||||
private int getValueLength(final int pType, final int pCount) {
|
||||
static int getValueLength(final int pType, final int pCount) {
|
||||
if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) {
|
||||
return TIFF.TYPE_LENGTHS[pType - 1] * pCount;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,25 @@ package com.twelvemonkeys.imageio.metadata.exif;
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public interface TIFF {
|
||||
short BYTE_ORDER_MARK_BIG_ENDIAN = ('M' << 8) | 'M';
|
||||
short BYTE_ORDER_MARK_LITTLE_ENDIAN = ('I' << 8) | 'I';
|
||||
|
||||
int TIFF_MAGIC = 42;
|
||||
|
||||
short TYPE_BYTE = 1;
|
||||
short TYPE_ASCII = 2;
|
||||
short TYPE_SHORT = 3;
|
||||
short TYPE_LONG = 4;
|
||||
short TYPE_RATIONAL = 5;
|
||||
|
||||
short TYPE_SBYTE = 6;
|
||||
short TYPE_UNDEFINED = 7;
|
||||
short TYPE_SSHORT = 8;
|
||||
short TYPE_SLONG = 9;
|
||||
short TYPE_SRATIONAL = 10;
|
||||
short TYPE_FLOAT = 11;
|
||||
short TYPE_DOUBLE = 12;
|
||||
short TYPE_IFD = 13;
|
||||
/*
|
||||
1 = BYTE 8-bit unsigned integer.
|
||||
2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte
|
||||
|
||||
@@ -77,6 +77,8 @@ public final class JPEGSegment implements Serializable {
|
||||
return marker >= 0xFFE0 && marker <= 0xFFEF;
|
||||
}
|
||||
|
||||
// TODO: Consider returning an ImageInputStream and use ByteArrayImageInputStream directly, for less wrapping and better performance
|
||||
// TODO: BUT: Must find a way to skip padding in/after segment identifier (eg: Exif has null-term + null-pad, ICC_PROFILE has only null-term). Is data always word-aligned?
|
||||
public InputStream data() {
|
||||
return data != null ? new ByteArrayInputStream(data, offset(), length()) : null;
|
||||
}
|
||||
@@ -85,7 +87,7 @@ public final class JPEGSegment implements Serializable {
|
||||
return data != null ? data.length - offset() : 0;
|
||||
}
|
||||
|
||||
private int offset() {
|
||||
int offset() {
|
||||
String identifier = identifier();
|
||||
|
||||
return identifier == null ? 0 : identifier.length() + 1;
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
||||
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
|
||||
import com.twelvemonkeys.imageio.metadata.xmp.XMP;
|
||||
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -156,6 +157,22 @@ public final class JPEGSegmentUtil {
|
||||
static JPEGSegment readSegment(final ImageInputStream stream, final Map<Integer, List<String>> segmentIdentifiers) throws IOException {
|
||||
int marker = stream.readUnsignedShort();
|
||||
|
||||
// Skip over weird 0x00 padding...?
|
||||
int bad = 0;
|
||||
while (marker == 0) {
|
||||
marker = stream.readUnsignedShort();
|
||||
bad += 2;
|
||||
}
|
||||
|
||||
if (marker == 0x00ff) {
|
||||
bad++;
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
}
|
||||
|
||||
if (bad != 0) {
|
||||
// System.err.println("bad: " + bad);
|
||||
}
|
||||
|
||||
// Skip over 0xff padding between markers
|
||||
while (marker == 0xffff) {
|
||||
marker = 0xff00 | stream.readUnsignedByte();
|
||||
@@ -245,38 +262,48 @@ public final class JPEGSegmentUtil {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
List<JPEGSegment> segments = readSegments(ImageIO.createImageInputStream(new File(args[0])), ALL_SEGMENTS);
|
||||
|
||||
for (JPEGSegment segment : segments) {
|
||||
System.err.println("segment: " + segment);
|
||||
|
||||
if ("Exif".equals(segment.identifier())) {
|
||||
InputStream data = segment.data();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
data.read(); // Pad
|
||||
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(data);
|
||||
|
||||
// Root entry is TIFF, that contains the EXIF sub-IFD
|
||||
Directory tiff = new EXIFReader().read(stream);
|
||||
System.err.println("EXIF: " + tiff);
|
||||
for (String arg : args) {
|
||||
if (args.length > 1) {
|
||||
System.out.println("File: " + arg);
|
||||
System.out.println("------");
|
||||
}
|
||||
else if (XMP.NS_XAP.equals(segment.identifier())) {
|
||||
Directory xmp = new XMPReader().read(ImageIO.createImageInputStream(segment.data()));
|
||||
System.err.println("XMP: " + xmp);
|
||||
|
||||
List<JPEGSegment> segments = readSegments(ImageIO.createImageInputStream(new File(arg)), ALL_SEGMENTS);
|
||||
|
||||
for (JPEGSegment segment : segments) {
|
||||
System.err.println("segment: " + segment);
|
||||
|
||||
if ("Exif".equals(segment.identifier())) {
|
||||
ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset() + 1, segment.length() - 1);
|
||||
|
||||
// Root entry is TIFF, that contains the EXIF sub-IFD
|
||||
Directory tiff = new EXIFReader().read(stream);
|
||||
System.err.println("EXIF: " + tiff);
|
||||
}
|
||||
else if (XMP.NS_XAP.equals(segment.identifier())) {
|
||||
Directory xmp = new XMPReader().read(new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length()));
|
||||
System.err.println("XMP: " + xmp);
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
}
|
||||
else if ("Photoshop 3.0".equals(segment.identifier())) {
|
||||
// TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain
|
||||
// IPTC metadata. Probably duplicated in the XMP though...
|
||||
ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length());
|
||||
Directory psd = new PSDReader().read(stream);
|
||||
System.err.println("PSD: " + psd);
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
}
|
||||
else if ("ICC_PROFILE".equals(segment.identifier())) {
|
||||
// Skip
|
||||
}
|
||||
else {
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
}
|
||||
}
|
||||
else if ("Photoshop 3.0".equals(segment.identifier())) {
|
||||
// TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain
|
||||
// IPTC metadata. Probably duplicated in the XMP though...
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(segment.data());
|
||||
Directory psd = new PSDReader().read(stream);
|
||||
System.err.println("PSD: " + psd);
|
||||
}
|
||||
else if ("ICC_PROFILE".equals(segment.identifier())) {
|
||||
// Skip
|
||||
}
|
||||
else {
|
||||
System.err.println(EXIFReader.HexDump.dump(segment.data));
|
||||
|
||||
if (args.length > 1) {
|
||||
System.out.println("------");
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ package com.twelvemonkeys.imageio.metadata.psd;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: PSD.java,v 1.0 24.01.12 16:51 haraldk Exp$
|
||||
*/
|
||||
interface PSD {
|
||||
public interface PSD {
|
||||
static final int RESOURCE_TYPE = ('8' << 24) + ('B' << 16) + ('I' << 8) + 'M';
|
||||
|
||||
static final int RES_IPTC_NAA = 0x0404;
|
||||
|
||||
@@ -128,20 +128,8 @@ public final class XMPReader extends MetadataReader {
|
||||
|
||||
Object value;
|
||||
|
||||
Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType");
|
||||
if (parseType != null && "Resource".equals(parseType.getNodeValue())) {
|
||||
// See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource
|
||||
List<Entry> entries = new ArrayList<Entry>();
|
||||
|
||||
for (Node child : asIterable(node.getChildNodes())) {
|
||||
if (child.getNodeType() != Node.ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child)));
|
||||
}
|
||||
|
||||
value = new RDFDescription(entries);
|
||||
if (isResourceType(node)) {
|
||||
value = parseAsResource(node);
|
||||
}
|
||||
else {
|
||||
// TODO: This method contains loads of duplication an should be cleaned up...
|
||||
@@ -178,6 +166,27 @@ public final class XMPReader extends MetadataReader {
|
||||
return new XMPDirectory(entries, toolkit);
|
||||
}
|
||||
|
||||
private boolean isResourceType(Node node) {
|
||||
Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType");
|
||||
|
||||
return parseType != null && "Resource".equals(parseType.getNodeValue());
|
||||
}
|
||||
|
||||
private RDFDescription parseAsResource(Node node) {
|
||||
// See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource
|
||||
List<Entry> entries = new ArrayList<Entry>();
|
||||
|
||||
for (Node child : asIterable(node.getChildNodes())) {
|
||||
if (child.getNodeType() != Node.ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child)));
|
||||
}
|
||||
|
||||
return new RDFDescription(entries);
|
||||
}
|
||||
|
||||
private void parseAttributesForKnownElements(Map<String, List<Entry>> subdirs, Node desc) {
|
||||
// NOTE: NamedNodeMap does not have any particular order...
|
||||
NamedNodeMap attributes = desc.getAttributes();
|
||||
@@ -201,15 +210,13 @@ public final class XMPReader extends MetadataReader {
|
||||
private Object getChildTextValue(final Node node) {
|
||||
for (Node child : asIterable(node.getChildNodes())) {
|
||||
if (XMP.NS_RDF.equals(child.getNamespaceURI()) && "Alt".equals(child.getLocalName())) {
|
||||
// Support for <rdf:Alt><rdf:li> -> return a Map<String, Object> (keyed on xml:lang?)
|
||||
// Support for <rdf:Alt><rdf:li> -> return a Map<String, Object> keyed on xml:lang
|
||||
Map<String, Object> alternatives = new LinkedHashMap<String, Object>();
|
||||
for (Node alternative : asIterable(child.getChildNodes())) {
|
||||
if (XMP.NS_RDF.equals(alternative.getNamespaceURI()) && "li".equals(alternative.getLocalName())) {
|
||||
//return getChildTextValue(alternative);
|
||||
NamedNodeMap attributes = alternative.getAttributes();
|
||||
Node key = attributes.getNamedItem("xml:lang");
|
||||
|
||||
alternatives.put(key.getTextContent(), getChildTextValue(alternative));
|
||||
alternatives.put(key == null ? null : key.getTextContent(), getChildTextValue(alternative));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,9 +242,13 @@ public final class XMPReader extends MetadataReader {
|
||||
}
|
||||
}
|
||||
|
||||
// Need to support rdf:parseType="Resource" here as well...
|
||||
if (isResourceType(node)) {
|
||||
return parseAsResource(node);
|
||||
}
|
||||
|
||||
Node child = node.getFirstChild();
|
||||
String strVal = child != null ? child.getNodeValue() : null;
|
||||
|
||||
return strVal != null ? strVal.trim() : "";
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public class PICTImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"pct", "pict"},
|
||||
new String[]{"image/pict", "image/x-pict"},
|
||||
"com.twelvemkonkeys.imageio.plugins.pict.PICTImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
new String[]{"com.twelvemkonkeys.imageio.plugins.pict.PICTImageWriterSpi"},
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
|
||||
@@ -64,7 +64,7 @@ public class PSDImageReaderSpi extends ImageReaderSpi {
|
||||
"image/x-psd", "application/x-photoshop", "image/x-photoshop"
|
||||
},
|
||||
"com.twelvemkonkeys.imageio.plugins.psd.PSDImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
// new String[]{"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
|
||||
null,
|
||||
true, // supports standard stream metadata
|
||||
|
||||
@@ -27,6 +27,7 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
// TODO: Decide on image/stream metadata...
|
||||
static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_image_1.0";
|
||||
static final String NATIVE_METADATA_FORMAT_CLASS_NAME = "com.twelvemonkeys.imageio.plugins.psd.PSDMetadataFormat";
|
||||
// TODO: Support TIFF metadata, based on EXIF/XMP + merge in PSD specifics
|
||||
|
||||
PSDHeader header;
|
||||
PSDColorData colorData;
|
||||
@@ -626,7 +627,6 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
}
|
||||
|
||||
IIOMetadataNode text = new IIOMetadataNode("Text");
|
||||
IIOMetadataNode node;
|
||||
|
||||
// TODO: Alpha channel names? (PSDAlphaChannelInfo/PSDUnicodeAlphaNames)
|
||||
// TODO: Reader/writer (PSDVersionInfo)
|
||||
@@ -691,7 +691,7 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
String fieldName = entry.getFieldName();
|
||||
|
||||
if (fieldName != null) {
|
||||
tag.setAttribute("keyword", String.format("%s", fieldName));
|
||||
tag.setAttribute("keyword", fieldName);
|
||||
}
|
||||
else {
|
||||
// TODO: This should never happen, as we filter out only specific nodes
|
||||
@@ -722,8 +722,8 @@ public final class PSDMetadata extends AbstractMetadata {
|
||||
}
|
||||
|
||||
private boolean hasAlpha() {
|
||||
return header.mode == PSD.COLOR_MODE_RGB && header.channels >= 4 ||
|
||||
header.mode == PSD.COLOR_MODE_CMYK & header.channels >= 5;
|
||||
return header.mode == PSD.COLOR_MODE_RGB && header.channels > 3 ||
|
||||
header.mode == PSD.COLOR_MODE_CMYK & header.channels > 4;
|
||||
}
|
||||
|
||||
<T extends PSDImageResource> Iterator<T> getResources(final Class<T> resourceType) {
|
||||
|
||||
@@ -65,8 +65,8 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpi {
|
||||
new String[]{"thumbs", "THUMBS", "Thumbs DB"},
|
||||
new String[]{"db"},
|
||||
new String[]{"image/x-thumbs-db", "application/octet-stream"}, // TODO: Check IANA et al...
|
||||
ThumbsDBImageReader.class.getName(),
|
||||
STANDARD_INPUT_TYPE,
|
||||
"com.twelvemonkeys.imageio.plugins.thumbsdb.ThumbsDBImageReader",
|
||||
new Class[] {ImageInputStream.class},
|
||||
null,
|
||||
true, null, null, null, null,
|
||||
true, null, null, null, null
|
||||
|
||||
@@ -31,10 +31,12 @@ 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 java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
/**
|
||||
* A decoder for data converted using "horizontal differencing predictor".
|
||||
@@ -43,29 +45,26 @@ import java.nio.ByteOrder;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$
|
||||
*/
|
||||
final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
final class HorizontalDeDifferencingStream extends InputStream {
|
||||
// See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
|
||||
|
||||
private final int columns;
|
||||
// NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1
|
||||
private final int samplesPerPixel;
|
||||
private final int bitsPerSample;
|
||||
private final ByteOrder byteOrder;
|
||||
|
||||
int decodedLength;
|
||||
int decodedPos;
|
||||
|
||||
private final byte[] buffer;
|
||||
private final ReadableByteChannel channel;
|
||||
private final ByteBuffer buffer;
|
||||
|
||||
public HorizontalDeDifferencingStream(final InputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
channel = Channels.newChannel(Validate.notNull(stream, "stream"));
|
||||
|
||||
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
|
||||
this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s");
|
||||
this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s");
|
||||
this.byteOrder = byteOrder;
|
||||
|
||||
buffer = new byte[(columns * samplesPerPixel * bitsPerSample + 7) / 8];
|
||||
buffer = ByteBuffer.allocate((columns * samplesPerPixel * bitsPerSample + 7) / 8).order(byteOrder);
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
private boolean isValidBPS(final int bitsPerSample) {
|
||||
@@ -83,75 +82,81 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
private void fetch() throws IOException {
|
||||
int pos = 0;
|
||||
int read;
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private boolean fetch() throws IOException {
|
||||
buffer.clear();
|
||||
|
||||
// This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer, otherwise we will throw EOFException below
|
||||
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
|
||||
pos += read;
|
||||
}
|
||||
// This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer,
|
||||
// otherwise we will throw EOFException below
|
||||
while (channel.read(buffer) > 0);
|
||||
|
||||
if (pos > 0) {
|
||||
if (buffer.length > pos) {
|
||||
if (buffer.position() > 0) {
|
||||
if (buffer.hasRemaining()) {
|
||||
throw new EOFException("Unexpected end of stream");
|
||||
}
|
||||
|
||||
decodeRow();
|
||||
buffer.flip();
|
||||
|
||||
decodedLength = buffer.length;
|
||||
decodedPos = 0;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
decodedLength = -1;
|
||||
buffer.position(buffer.capacity());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeRow() throws EOFException {
|
||||
// Un-apply horizontal predictor
|
||||
byte original;
|
||||
int sample = 0;
|
||||
byte temp;
|
||||
|
||||
switch (bitsPerSample) {
|
||||
case 1:
|
||||
for (int b = 0; b < (columns + 7) / 8; b++) {
|
||||
sample += (buffer[b] >> 7) & 0x1;
|
||||
original = buffer.get(b);
|
||||
sample += (original >> 7) & 0x1;
|
||||
temp = (byte) ((sample << 7) & 0x80);
|
||||
sample += (buffer[b] >> 6) & 0x1;
|
||||
sample += (original >> 6) & 0x1;
|
||||
temp |= (byte) ((sample << 6) & 0x40);
|
||||
sample += (buffer[b] >> 5) & 0x1;
|
||||
sample += (original >> 5) & 0x1;
|
||||
temp |= (byte) ((sample << 5) & 0x20);
|
||||
sample += (buffer[b] >> 4) & 0x1;
|
||||
sample += (original >> 4) & 0x1;
|
||||
temp |= (byte) ((sample << 4) & 0x10);
|
||||
sample += (buffer[b] >> 3) & 0x1;
|
||||
sample += (original >> 3) & 0x1;
|
||||
temp |= (byte) ((sample << 3) & 0x08);
|
||||
sample += (buffer[b] >> 2) & 0x1;
|
||||
sample += (original >> 2) & 0x1;
|
||||
temp |= (byte) ((sample << 2) & 0x04);
|
||||
sample += (buffer[b] >> 1) & 0x1;
|
||||
sample += (original >> 1) & 0x1;
|
||||
temp |= (byte) ((sample << 1) & 0x02);
|
||||
sample += buffer[b] & 0x1;
|
||||
buffer[b] = (byte) (temp | sample & 0x1);
|
||||
sample += original & 0x1;
|
||||
buffer.put(b, (byte) (temp | sample & 0x1));
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
for (int b = 0; b < (columns + 3) / 4; b++) {
|
||||
sample += (buffer[b] >> 6) & 0x3;
|
||||
original = buffer.get(b);
|
||||
sample += (original >> 6) & 0x3;
|
||||
temp = (byte) ((sample << 6) & 0xc0);
|
||||
sample += (buffer[b] >> 4) & 0x3;
|
||||
sample += (original >> 4) & 0x3;
|
||||
temp |= (byte) ((sample << 4) & 0x30);
|
||||
sample += (buffer[b] >> 2) & 0x3;
|
||||
sample += (original >> 2) & 0x3;
|
||||
temp |= (byte) ((sample << 2) & 0x0c);
|
||||
sample += buffer[b] & 0x3;
|
||||
buffer[b] = (byte) (temp | sample & 0x3);
|
||||
sample += original & 0x3;
|
||||
buffer.put(b, (byte) (temp | sample & 0x3));
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
for (int b = 0; b < (columns + 1) / 2; b++) {
|
||||
sample += (buffer[b] >> 4) & 0xf;
|
||||
original = buffer.get(b);
|
||||
sample += (original >> 4) & 0xf;
|
||||
temp = (byte) ((sample << 4) & 0xf0);
|
||||
sample += buffer[b] & 0x0f;
|
||||
buffer[b] = (byte) (temp | sample & 0xf);
|
||||
sample += original & 0x0f;
|
||||
buffer.put(b, (byte) (temp | sample & 0xf));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -159,7 +164,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
buffer[off] = (byte) (buffer[off - samplesPerPixel] + buffer[off]);
|
||||
buffer.put(off, (byte) (buffer.get(off - samplesPerPixel) + buffer.get(off)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -168,7 +173,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putShort(off, asShort(off - samplesPerPixel) + asShort(off));
|
||||
buffer.putShort(2 * off, (short) (buffer.getShort(2 * (off - samplesPerPixel)) + buffer.getShort(2 * off)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -177,7 +182,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putInt(off, asInt(off - samplesPerPixel) + asInt(off));
|
||||
buffer.putInt(4 * off, buffer.getInt(4 * (off - samplesPerPixel)) + buffer.getInt(4 * off));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -186,7 +191,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
for (int x = 1; x < columns; x++) {
|
||||
for (int b = 0; b < samplesPerPixel; b++) {
|
||||
int off = x * samplesPerPixel + b;
|
||||
putLong(off, asLong(off - samplesPerPixel) + asLong(off));
|
||||
buffer.putLong(8 * off, buffer.getLong(8 * (off - samplesPerPixel)) + buffer.getLong(8 * off));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -196,145 +201,58 @@ final class HorizontalDeDifferencingStream extends FilterInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
private void putLong(final int index, final long value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 8 ] = (byte) ((value >> 56) & 0xff);
|
||||
buffer[index * 8 + 1] = (byte) ((value >> 48) & 0xff);
|
||||
buffer[index * 8 + 2] = (byte) ((value >> 40) & 0xff);
|
||||
buffer[index * 8 + 3] = (byte) ((value >> 32) & 0xff);
|
||||
buffer[index * 8 + 4] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 8 + 5] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 8 + 6] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 8 + 7] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 8 + 7] = (byte) ((value >> 56) & 0xff);
|
||||
buffer[index * 8 + 6] = (byte) ((value >> 48) & 0xff);
|
||||
buffer[index * 8 + 5] = (byte) ((value >> 40) & 0xff);
|
||||
buffer[index * 8 + 4] = (byte) ((value >> 32) & 0xff);
|
||||
buffer[index * 8 + 3] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 8 + 2] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 8 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 8 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private long asLong(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (buffer[index * 8 ] & 0xffl) << 56l | (buffer[index * 8 + 1] & 0xffl) << 48l |
|
||||
(buffer[index * 8 + 2] & 0xffl) << 40l | (buffer[index * 8 + 3] & 0xffl) << 32l |
|
||||
(buffer[index * 8 + 4] & 0xffl) << 24 | (buffer[index * 8 + 5] & 0xffl) << 16 |
|
||||
(buffer[index * 8 + 6] & 0xffl) << 8 | buffer[index * 8 + 7] & 0xffl;
|
||||
}
|
||||
else {
|
||||
return (buffer[index * 8 + 7] & 0xffl) << 56l | (buffer[index * 8 + 6] & 0xffl) << 48l |
|
||||
(buffer[index * 8 + 5] & 0xffl) << 40l | (buffer[index * 8 + 4] & 0xffl) << 32l |
|
||||
(buffer[index * 8 + 3] & 0xffl) << 24 | (buffer[index * 8 + 2] & 0xffl) << 16 |
|
||||
(buffer[index * 8 + 1] & 0xffl) << 8 | buffer[index * 8] & 0xffl;
|
||||
}
|
||||
}
|
||||
|
||||
private void putInt(final int index, final int value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 4 ] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 4 + 1] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 4 + 2] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 4 + 3] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 4 + 3] = (byte) ((value >> 24) & 0xff);
|
||||
buffer[index * 4 + 2] = (byte) ((value >> 16) & 0xff);
|
||||
buffer[index * 4 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 4 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private int asInt(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (buffer[index * 4] & 0xff) << 24 | (buffer[index * 4 + 1] & 0xff) << 16 |
|
||||
(buffer[index * 4 + 2] & 0xff) << 8 | buffer[index * 4 + 3] & 0xff;
|
||||
}
|
||||
else {
|
||||
return (buffer[index * 4 + 3] & 0xff) << 24 | (buffer[index * 4 + 2] & 0xff) << 16 |
|
||||
(buffer[index * 4 + 1] & 0xff) << 8 | buffer[index * 4] & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
private void putShort(final int index, final int value) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
buffer[index * 2 ] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 2 + 1] = (byte) ((value) & 0xff);
|
||||
}
|
||||
else {
|
||||
buffer[index * 2 + 1] = (byte) ((value >> 8) & 0xff);
|
||||
buffer[index * 2 ] = (byte) ((value) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private short asShort(final int index) {
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
return (short) ((buffer[index * 2] & 0xff) << 8 | buffer[index * 2 + 1] & 0xff);
|
||||
}
|
||||
else {
|
||||
return (short) ((buffer[index * 2 + 1] & 0xff) << 8 | buffer[index * 2] & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (!fetch()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return buffer[decodedPos++] & 0xff;
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (!fetch()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int read = Math.min(decodedLength - decodedPos, len);
|
||||
System.arraycopy(buffer, decodedPos, b, off, read);
|
||||
decodedPos += read;
|
||||
int read = Math.min(buffer.remaining(), len);
|
||||
buffer.get(b, off, read);
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
if (n < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (decodedPos >= decodedLength) {
|
||||
fetch();
|
||||
|
||||
if (decodedLength < 0) {
|
||||
return -1;
|
||||
if (!buffer.hasRemaining()) {
|
||||
if (!fetch()) {
|
||||
return 0; // SIC
|
||||
}
|
||||
}
|
||||
|
||||
int skipped = (int) Math.min(decodedLength - decodedPos, n);
|
||||
decodedPos += skipped;
|
||||
int skipped = (int) Math.min(buffer.remaining(), n);
|
||||
buffer.position(buffer.position() + skipped);
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
}
|
||||
finally {
|
||||
if (channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.twelvemonkeys.io.enc.Decoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Lempel–Ziv–Welch (LZW) decompression. LZW is a universal loss-less data compression algorithm
|
||||
@@ -94,10 +95,9 @@ abstract class LZWDecoder implements Decoder {
|
||||
maxString = 1;
|
||||
}
|
||||
|
||||
public int decode(final InputStream stream, final byte[] buffer) throws IOException {
|
||||
public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
|
||||
// Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992.
|
||||
// See Section 13: "LZW Compression"/"LZW Decoding", page 61+
|
||||
int bufferPos = 0;
|
||||
int code;
|
||||
|
||||
while ((code = getNextCode(stream)) != EOI_CODE) {
|
||||
@@ -109,30 +109,30 @@ abstract class LZWDecoder implements Decoder {
|
||||
break;
|
||||
}
|
||||
|
||||
bufferPos += table[code].writeTo(buffer, bufferPos);
|
||||
table[code].writeTo(buffer);
|
||||
}
|
||||
else {
|
||||
if (isInTable(code)) {
|
||||
bufferPos += table[code].writeTo(buffer, bufferPos);
|
||||
table[code].writeTo(buffer);
|
||||
addStringToTable(table[oldCode].concatenate(table[code].firstChar));
|
||||
}
|
||||
else {
|
||||
String outString = table[oldCode].concatenate(table[oldCode].firstChar);
|
||||
|
||||
bufferPos += outString.writeTo(buffer, bufferPos);
|
||||
outString.writeTo(buffer);
|
||||
addStringToTable(outString);
|
||||
}
|
||||
}
|
||||
|
||||
oldCode = code;
|
||||
|
||||
if (bufferPos >= buffer.length - maxString - 1) {
|
||||
if (buffer.remaining() < maxString + 1) {
|
||||
// Buffer full, stop decoding for now
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bufferPos;
|
||||
return buffer.position();
|
||||
}
|
||||
|
||||
private void addStringToTable(final String string) throws IOException {
|
||||
@@ -301,24 +301,24 @@ abstract class LZWDecoder implements Decoder {
|
||||
return new String(firstChar, this.firstChar, length + 1, this);
|
||||
}
|
||||
|
||||
public final int writeTo(final byte[] buffer, final int offset) {
|
||||
public final void writeTo(final ByteBuffer buffer) {
|
||||
if (length == 0) {
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
else if (length == 1) {
|
||||
buffer[offset] = value;
|
||||
|
||||
return 1;
|
||||
if (length == 1) {
|
||||
buffer.put(value);
|
||||
}
|
||||
else {
|
||||
String e = this;
|
||||
final int offset = buffer.position();
|
||||
|
||||
for (int i = length - 1; i >= 0; i--) {
|
||||
buffer[offset + i] = e.value;
|
||||
buffer.put(offset + i, e.value);
|
||||
e = e.previous;
|
||||
}
|
||||
|
||||
return length;
|
||||
buffer.position(offset + length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,11 @@ import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
||||
import com.twelvemonkeys.io.LittleEndianDataInputStream;
|
||||
import com.twelvemonkeys.io.enc.DecoderStream;
|
||||
import com.twelvemonkeys.io.enc.PackBitsDecoder;
|
||||
import com.twelvemonkeys.xml.XMLSerializer;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
@@ -62,7 +64,6 @@ import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
@@ -80,6 +81,7 @@ import java.util.zip.InflaterInputStream;
|
||||
* <ul>
|
||||
* <li>Tiling</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>
|
||||
@@ -90,6 +92,7 @@ import java.util.zip.InflaterInputStream;
|
||||
* <li>Planar data (PlanarConfiguration type 2/Planar)</li>
|
||||
* <li>ICC profiles (ICCProfile)</li>
|
||||
* <li>BitsPerSample values up to 16 for most PhotometricInterpretations</li>
|
||||
* <li>Multiple images (pages) in one file</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see <a href="http://partners.adobe.com/public/developer/tiff/index.html">Adobe TIFF developer resources</a>
|
||||
@@ -104,6 +107,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// TODOs ImageIO basic functionality:
|
||||
// TODO: Subsampling (*tests should be failing*)
|
||||
// TODO: Source region (*tests should be failing*)
|
||||
// TODO: Thumbnail support
|
||||
// TODO: TIFFImageWriter + Spi
|
||||
|
||||
// TODOs Full BaseLine support:
|
||||
@@ -111,7 +115,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied)
|
||||
|
||||
// TODOs ImageIO advanced functionality:
|
||||
// TODO: Implement readAsRenderedImage to allow tiled renderImage?
|
||||
// TODO: Tiling support (readTile, readTileRaster)
|
||||
// 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)
|
||||
@@ -119,6 +124,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
// TODOs Extension support
|
||||
// TODO: Support PlanarConfiguration 2
|
||||
// TODO: Auto-rotate based on Orientation
|
||||
// TODO: Support ICCProfile (fully)
|
||||
// TODO: Support Compression 3 & 4 (CCITT T.4 & T.6)
|
||||
// TODO: Support Compression 34712 (JPEG2000)? Depends on JPEG2000 ImageReader
|
||||
@@ -290,7 +296,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
case 4:
|
||||
if (bitsPerSample == 8 || bitsPerSample == 16) {
|
||||
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
// ExtraSamples 0=unspecified, 1=associated (pre-multiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
|
||||
|
||||
switch (planarConfiguration) {
|
||||
@@ -356,7 +362,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
case 5:
|
||||
if (bitsPerSample == 8 || bitsPerSample == 16) {
|
||||
// ExtraSamples 0=unspecified, 1=associated (premultiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
// ExtraSamples 0=unspecified, 1=associated (pre-multiplied), 2=unassociated (TODO: Support unspecified, not alpha)
|
||||
long[] extraSamples = getValueAsLongArray(TIFF.TAG_EXTRA_SAMPLES, "ExtraSamples", true);
|
||||
|
||||
switch (planarConfiguration) {
|
||||
@@ -428,19 +434,19 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
readIFD(imageIndex);
|
||||
|
||||
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
|
||||
List<ImageTypeSpecifier> specs = new ArrayList<ImageTypeSpecifier>();
|
||||
Set<ImageTypeSpecifier> specs = new LinkedHashSet<ImageTypeSpecifier>(5);
|
||||
|
||||
// TODO: Based on raw type, we can probably convert to most RGB types at least, maybe gray etc
|
||||
// TODO: Planar to chunky by default
|
||||
if (!rawType.getColorModel().getColorSpace().isCS_sRGB() && rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
|
||||
if (rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
|
||||
if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) {
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
|
||||
// specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
|
||||
// specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
|
||||
}
|
||||
else if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 8) {
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
|
||||
// specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE));
|
||||
}
|
||||
}
|
||||
@@ -666,53 +672,10 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
jpegReader.setInput(new ByteArrayImageInputStream(tablesValue));
|
||||
|
||||
// NOTE: This initializes the tables AND MORE secret internal settings for the reader (as if by magic).
|
||||
// This is probably a bug, as later setInput calls should clear/override the tables.
|
||||
// However, it would be extremely convenient, not having to actually fiddle with the stream meta data (as below)
|
||||
// NOTE: This initializes the tables and other internal settings for the reader (as if by magic).
|
||||
// This is actually a feature of JPEG,
|
||||
// see: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#abbrev
|
||||
/*IIOMetadata streamMetadata = */jpegReader.getStreamMetadata();
|
||||
|
||||
/*
|
||||
IIOMetadataNode root = (IIOMetadataNode) streamMetadata.getAsTree(streamMetadata.getNativeMetadataFormatName());
|
||||
NodeList dqt = root.getElementsByTagName("dqt");
|
||||
NodeList dqtables = ((IIOMetadataNode) dqt.item(0)).getElementsByTagName("dqtable");
|
||||
JPEGQTable[] qTables = new JPEGQTable[dqtables.getLength()];
|
||||
for (int i = 0; i < dqtables.getLength(); i++) {
|
||||
qTables[i] = (JPEGQTable) ((IIOMetadataNode) dqtables.item(i)).getUserObject();
|
||||
System.err.println("qTables: " + qTables[i]);
|
||||
}
|
||||
|
||||
List<JPEGHuffmanTable> acHTables = new ArrayList<JPEGHuffmanTable>();
|
||||
List<JPEGHuffmanTable> dcHTables = new ArrayList<JPEGHuffmanTable>();
|
||||
|
||||
NodeList dht = root.getElementsByTagName("dht");
|
||||
for (int i = 0; i < dht.getLength(); i++) {
|
||||
NodeList dhtables = ((IIOMetadataNode) dht.item(i)).getElementsByTagName("dhtable");
|
||||
for (int j = 0; j < dhtables.getLength(); j++) {
|
||||
System.err.println("dhtables.getLength(): " + dhtables.getLength());
|
||||
IIOMetadataNode dhtable = (IIOMetadataNode) dhtables.item(j);
|
||||
JPEGHuffmanTable userObject = (JPEGHuffmanTable) dhtable.getUserObject();
|
||||
if ("0".equals(dhtable.getAttribute("class"))) {
|
||||
dcHTables.add(userObject);
|
||||
}
|
||||
else {
|
||||
acHTables.add(userObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JPEGHuffmanTable[] dcTables = dcHTables.toArray(new JPEGHuffmanTable[dcHTables.size()]);
|
||||
JPEGHuffmanTable[] acTables = acHTables.toArray(new JPEGHuffmanTable[acHTables.size()]);
|
||||
*/
|
||||
// JPEGTables tables = new JPEGTables(new ByteArrayImageInputStream(tablesValue));
|
||||
// JPEGQTable[] qTables = tables.getQTables();
|
||||
// JPEGHuffmanTable[] dcTables = tables.getDCHuffmanTables();
|
||||
// JPEGHuffmanTable[] acTables = tables.getACHuffmanTables();
|
||||
|
||||
// System.err.println("qTables: " + Arrays.toString(qTables));
|
||||
// System.err.println("dcTables: " + Arrays.toString(dcTables));
|
||||
// System.err.println("acTables: " + Arrays.toString(acTables));
|
||||
|
||||
// jpegParam.setDecodeTables(qTables, dcTables, acTables);
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("Missing JPEGTables for TIFF with compression: 7 (JPEG)");
|
||||
@@ -732,6 +695,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
|
||||
|
||||
try {
|
||||
jpegReader.setInput(subStream);
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
@@ -1236,6 +1200,15 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return ICC_Profile.getInstance(value);
|
||||
}
|
||||
|
||||
// TODO: Tiling support
|
||||
// isImageTiled
|
||||
// getTileWidth
|
||||
// getTileHeight
|
||||
// readTile
|
||||
// readTileRaster
|
||||
|
||||
// TODO: Thumbnail support
|
||||
|
||||
public static void main(final String[] args) throws IOException {
|
||||
for (final String arg : args) {
|
||||
File file = new File(arg);
|
||||
@@ -1312,6 +1285,12 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// param.setSourceSubsampling(2, 2, 0, 0);
|
||||
BufferedImage image = reader.read(imageNo, param);
|
||||
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
IIOMetadata metadata = reader.getImageMetadata(0);
|
||||
if (metadata != null) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
|
||||
}
|
||||
|
||||
// System.err.println("image: " + image);
|
||||
|
||||
// File tempFile = File.createTempFile("lzw-", ".bin");
|
||||
@@ -1371,6 +1350,10 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected static void showIt(BufferedImage image, String title) {
|
||||
ImageReaderBase.showIt(image, title);
|
||||
}
|
||||
|
||||
private static void deregisterOSXTIFFImageReaderSpi() {
|
||||
IIORegistry registry = IIORegistry.getDefaultInstance();
|
||||
Iterator<ImageReaderSpi> providers = registry.getServiceProviders(ImageReaderSpi.class, new ServiceRegistry.Filter() {
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.spi.ProviderInfo;
|
||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
import javax.imageio.spi.ServiceRegistry;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteOrder;
|
||||
@@ -46,7 +47,6 @@ import java.util.Locale;
|
||||
* @version $Id: TIFFImageReaderSpi.java,v 1.0 08.05.12 15:14 haraldk Exp$
|
||||
*/
|
||||
public class TIFFImageReaderSpi extends ImageReaderSpi {
|
||||
// TODO: Should we make sure we register (order) before the com.sun.imageio thing (that isn't what is says) provided by Apple?
|
||||
/**
|
||||
* Creates a {@code TIFFImageReaderSpi}.
|
||||
*/
|
||||
@@ -64,7 +64,7 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
|
||||
"image/tiff", "image/x-tiff"
|
||||
},
|
||||
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
|
||||
STANDARD_INPUT_TYPE,
|
||||
new Class[] {ImageInputStream.class},
|
||||
// new String[]{"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
|
||||
null,
|
||||
true, // supports standard stream metadata
|
||||
@@ -76,6 +76,23 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onRegistration(final ServiceRegistry registry, final Class<?> category) {
|
||||
// Make sure we're ordered before the Apple-provided TIFF reader on OS X
|
||||
try {
|
||||
Class<ImageReaderSpi> providerClass = (Class<ImageReaderSpi>) Class.forName("com.sun.imageio.plugins.tiff.TIFFImageReaderSpi");
|
||||
ImageReaderSpi appleSpi = registry.getServiceProviderByClass(providerClass);
|
||||
|
||||
if (appleSpi != null && appleSpi.getVendorName() != null && appleSpi.getVendorName().startsWith("Apple")) {
|
||||
registry.setOrdering((Class<ImageReaderSpi>) category, this, appleSpi);
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException ignore) {
|
||||
// This is actually OK, now we don't have to do anything
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canDecodeInput(final Object pSource) throws IOException {
|
||||
if (!(pSource instanceof ImageInputStream)) {
|
||||
return false;
|
||||
|
||||
@@ -69,6 +69,9 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
Validate.notNull(chromaSub, "chromaSub");
|
||||
Validate.isTrue(chromaSub.length == 2, "chromaSub.length != 2");
|
||||
|
||||
this.horizChromaSub = chromaSub[0];
|
||||
this.vertChromaSub = chromaSub[1];
|
||||
this.yCbCrPos = yCbCrPos;
|
||||
|
||||
@@ -28,11 +28,13 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.io.InputStreamAbstractTestCase;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* YCbCrUpsamplerStreamTest
|
||||
@@ -41,11 +43,77 @@ import java.io.InputStream;
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: YCbCrUpsamplerStreamTest.java,v 1.0 31.01.13 14:35 haraldk Exp$
|
||||
*/
|
||||
@Ignore
|
||||
public class YCbCrUpsamplerStreamTest extends InputStreamAbstractTestCase {
|
||||
// TODO: Implement + add @Ignore for all tests that makes no sense for this class.
|
||||
@Override
|
||||
protected InputStream makeInputStream(byte[] pBytes) {
|
||||
return new YCbCrUpsamplerStream(new ByteArrayInputStream(pBytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, pBytes.length / 4, null);
|
||||
public class YCbCrUpsamplerStreamTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullStream() {
|
||||
new YCbCrUpsamplerStream(null, new int[2], 7, 5, null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullChroma() {
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateShortChroma() {
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpsample22() throws IOException {
|
||||
byte[] bytes = new byte[] {
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 42, 96,
|
||||
108, 109, 110, 111, 112, 113, 114, 115, 43, 97
|
||||
};
|
||||
YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null);
|
||||
|
||||
byte[] expected = new byte[] {
|
||||
0, -126, 0, 0, -125, 0, 0, 27, 0, 0, 28, 0, 92, 124, 85, 93, 125, 86, 0, -78, 0, 0, -24, 0,
|
||||
0, -124, 0, 0, -123, 0, 15, 62, 7, 69, 116, 61, 94, 126, 87, 95, 127, 88, 0, -121, 0, 0, -121, 0
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
new DataInputStream(stream).readFully(upsampled);
|
||||
|
||||
assertArrayEquals(expected, upsampled);
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpsample21() throws IOException {
|
||||
byte[] bytes = new byte[] {
|
||||
1, 2, 3, 4, 42, 96, 77,
|
||||
112, 113, 114, 115, 43, 97, 43
|
||||
};
|
||||
YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null);
|
||||
|
||||
byte[] expected = new byte[] {
|
||||
0, -123, 0, 0, -122, 0, 20, 71, 0, 74, 125, 6, 0, -78, 90, 0, -77, 91, 75, 126, 7, 21, 72, 0
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
new DataInputStream(stream).readFully(upsampled);
|
||||
|
||||
assertArrayEquals(expected, upsampled);
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpsample12() throws IOException {
|
||||
byte[] bytes = new byte[] {
|
||||
1, 2, 3, 4, 42, 96, 77,
|
||||
112, 113, 114, 115, 43, 97, 43
|
||||
};
|
||||
YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null);
|
||||
|
||||
byte[] expected = new byte[] {
|
||||
0, -123, 0, 20, 71, 0, 0, -78, 90, 0, -24, 0, 0, -122, 0, 74, 125, 6, 0, -77, 91, 0, -78, 0
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
new DataInputStream(stream).readFully(upsampled);
|
||||
|
||||
assertArrayEquals(expected, upsampled);
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
- FileChannelImageInputStream/MappedByteBufferImageInputStream
|
||||
- FileChannelCacheImageInputStream
|
||||
- FileChannelImageOutputStream
|
||||
- FileChannelCacheImageOutputStream
|
||||
|
||||
- Consider creating a raw ImageReader (or util class?) that can read raw bitmaps:
|
||||
o Interleaved (A)RGB (as in BMP, PICT, IFF PBM etc) -> A1R1G1B1, A2R2G2B2, ..., AnRnGnNn
|
||||
o Channeled (A)RGB (as in Photoshop) -> A1A2...An, R1R2...Rn, G1G2...Gn, B1B2...Bn
|
||||
|
||||
Reference in New Issue
Block a user