Compare commits

...

49 Commits

Author SHA1 Message Date
Harald Kuhr 7de8231471 [maven-release-plugin] prepare release twelvemonkeys-3.8.0 2021-12-12 13:17:16 +01:00
Harald Kuhr 0de9f79029 [maven-release-plugin] rollback the release of twelvemonkeys-3.8.0 2021-12-12 13:13:54 +01:00
Harald Kuhr eeb56acdde [maven-release-plugin] prepare for next development iteration 2021-12-11 23:26:21 +01:00
Harald Kuhr a6862cfec8 Fixed JavaDoc. 2021-12-11 23:23:05 +01:00
Harald Kuhr f8284700b4 #631 Attempt to fix tests for JDK 15 & 16 2021-12-11 22:38:42 +01:00
Harald Kuhr 38caeb22e0 #631 Introduced ColorProfiles. Profile activation through SPI to force early activation. 2021-12-11 18:58:25 +01:00
Harald Kuhr b2c5915db8 #631 New way of forcing profile activation + guarding all invocations of ICC_Profile.getInstance() 2021-12-11 18:13:07 +01:00
Harald Kuhr 3911191b04 #645 AAIOBE in CCITTFaxDecoderStream now wrapped in IOException 2021-12-11 17:48:57 +01:00
Snyk bot bc328419ac fix: upgrade commons-fileupload:commons-fileupload from 1.3.3 to 1.4 (#642)
Snyk has created this PR to upgrade commons-fileupload:commons-fileupload from 1.3.3 to 1.4.

See this package in Maven Repository:
https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload/

See this project in Snyk:
https://app.snyk.io/org/haraldk/project/3eb50c00-e586-4f4c-a39c-90c80f89cc60?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-12-10 16:13:29 +01:00
Harald Kuhr da4efe98bf Avoid fetching external resources in XMPReader. 2021-12-10 13:41:05 +01:00
Harald Kuhr 6653f4a85d Minor clean-up. 2021-12-10 13:33:28 +01:00
Harald Kuhr 511a29beb9 Minor AffineTransformOp clean-up + removed test output. 2021-10-29 11:05:24 +02:00
Gauthier 5617b4323c Make tests pass on JDK 16 and 17 (#635)
* make tests pass on JDK 16 and 17
replace deprecated mockito-all by mockito-core, and updated to latest 3.x
replace deprecated org.mockito.Matchers

* code cleanup from IDE suggestions

* add oracle jdk 16 and 17 to Travis
2021-10-26 18:38:33 +02:00
Harald Kuhr 16d0af357d #629: Fixed build 2021-10-15 16:04:53 +02:00
Harald Kuhr 74927d5396 #629: Preliminary WebP animation (ANIM/ANMF) support 2021-10-15 15:08:39 +02:00
Harald Kuhr 7e809dd834 Readme improvements 2021-10-15 14:48:39 +02:00
Harald Kuhr 7aa95a08bc Fixed Travis link + bonus project summary updates 2021-10-15 09:41:47 +02:00
Harald Kuhr c28963ae49 Minor servlet clean-up. 2021-09-21 14:23:59 +02:00
Harald Kuhr 0327f5fc1a Servlet deprecation 2021-09-21 14:22:16 +02:00
Harald Kuhr 1c59057c30 #628: Stabilized build + better dependency scopes and module names in interop modules 2021-09-17 20:32:25 +02:00
Harald Kuhr 3e1f85c4dc #626 Clean up + fill order support for all compression types 2021-09-17 19:37:28 +02:00
Harald Kuhr 11227a68a0 #628 TIFF metadata fix, now always outputs denominator for rationals.
+ Bonus: Added JAI TIFF interop module with test and other minor fixes.
2021-09-17 16:34:38 +02:00
Oliver Schmidtmer 62ba73a30e #626: Handle fillOrder in TIFFImageReader, not in CCITTFaxDecoderStream (#627) 2021-09-17 16:16:30 +02:00
Harald Kuhr 1f33afb5a1 Fixed NullPointerException due to missing PhotometricInterpretation, now uses fallback as we do when reading. 2021-09-16 22:56:13 +02:00
Harald Kuhr 9d3f271867 #626 TIFF CCITT detection only once per IFD 2021-09-16 22:25:06 +02:00
Harald Kuhr 812e12acb0 #623: TGAImageReader, PCXImageReader and SGIImageReader now return more standard image types as default, for better AffineTransformOp compatibility. Added tests.
Bonus: Subsampling fix for TGAImageReader and BMPImageReader.
2021-09-07 09:29:13 +02:00
Harald Kuhr 060b6cf852 #624: Added metadata support for 16 bit USHORT gray. 2021-09-07 09:24:27 +02:00
Koen De Groote e68ce7ffd1 Certain pixeldepth-16 TGA files fail to process, classcast exception (#624)
* Added fixed for monochrome tga16 bit. Uncertain if that description is complete.
Test files added. Without the changed code, the tests fail.

* Fix suggested by HaraldK

Co-authored-by: Koen De Groote <koen.degroote@limecraft.com>
2021-09-03 16:19:05 +02:00
Harald Kuhr 778cdef69c Fix typo in TIFFImageMetadataFormat mk II. 2021-08-31 22:34:47 +02:00
Harald Kuhr d46a76fca8 Fix typo in TIFFImageMetadataFormat. 2021-08-31 22:26:32 +02:00
Harald Kuhr 105a1ee466 #621 Don't add ICC profile for default gray images 2021-08-31 22:16:08 +02:00
Harald Kuhr aa030f526c #617 BigTIFF write clean-up. 2021-08-31 20:24:42 +02:00
Harald Kuhr 976e5d6210 #619: Fix WebP Y'CbCr->RGB conversion (now uses rec 601) 2021-08-26 16:47:51 +02:00
Harald Kuhr 6daca00fcd Minor clean-up. 2021-08-09 21:24:32 +02:00
Harald Kuhr ef05872934 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java
2021-08-09 21:22:46 +02:00
Harald Kuhr 1ddab866fd #617 BigTIFF write support. 2021-08-09 21:11:40 +02:00
Harald Kuhr ce997a6951 Some more minor clean-up. 2021-08-06 10:10:34 +02:00
Harald Kuhr 23bf5cb7b2 Minor clean-up. 2021-08-06 09:59:07 +02:00
Harald Kuhr 564778f415 #616: Remove dependency on old xmlgraphics-commons (no longer needed) 2021-08-04 11:25:58 +02:00
Harald Kuhr e28bf8fb44 Fix WebP ICC handling for images with alpha. 2021-07-12 10:01:16 +02:00
Harald Kuhr cf8d630d01 Add WebP to BOM. 2021-07-12 09:38:20 +02:00
Harald Kuhr 0ff7224912 Switch build from travis.ci.org to com 2021-06-29 16:56:58 +02:00
Koen De Groote 196081a317 Documentation cleanup (#612)
* Added the `@Deprecated` tag to instances where is was mentioned in documentation, but not for the actual code itself.

Changed one documentation link pointing at a non-existing item.

* As per PR suggestion.
2021-06-29 13:06:07 +02:00
Harald Kuhr ff50180d86 #609 Fixed ICC Profile handling in WebP. 2021-06-03 21:31:54 +02:00
Harald Kuhr 8f2c482167 Minor code clean-up. 2021-06-03 18:21:00 +02:00
Harald Kuhr eab24890ca Merge pull request #608 from Schmidor/ccitt_eol_search
#579 Deeper EOL search in the CCITT stream
2021-05-31 10:40:20 +02:00
Oliver Schmidtmer cd42d81817 Invert EOF check 2021-05-28 14:38:44 +02:00
Oliver Schmidtmer ba5c667b6c #579 Deeper EOL search in the CCITT stream 2021-05-27 22:11:13 +02:00
Harald Kuhr 4e9fa9442c PR template update. 2021-05-13 15:15:09 +02:00
197 changed files with 3709 additions and 1333 deletions
@@ -1,16 +1,10 @@
**What is fixed**
**What is fixed** Add link to the issue this PR fixes.
Add link to the issue this PR fixes.
Example: Fixes #42.
Fixes #42.
**Why is this change proposed** If this change does *not* fix an open issue, briefly describe the rationale for this PR.
**Why is this change proposed**
If this change does *not* fix an open issue, briefly describe the rationale for this PR.
**What is changed**
Briefly describe the changes proposed in this pull request:
**What is changed** Briefly describe the changes proposed in this pull request:
* Fixed rare exception happening in `x >= 42` case
* Small optimization of `decompress()` method
+5 -3
View File
@@ -1,9 +1,11 @@
dist: trusty
language: java
jdk:
- oraclejdk8 # Legacy
- oraclejdk11 # LTS
- oraclejdk15 # Latest
- oraclejdk8 # LTS until Mar 2022
- oraclejdk11 # LTS until Sep 2023
- oraclejdk15 # out of support
- oraclejdk16 # out of support
- oraclejdk17 # LTS until Sep 2026
jobs:
include:
# Extra job, testing legacy CMM option
+5 -8
View File
@@ -1,19 +1,16 @@
[![Build Status](https://travis-ci.org/haraldk/TwelveMonkeys.svg?branch=master)](https://travis-ci.org/haraldk/TwelveMonkeys)
[![Build Status](https://travis-ci.com/haraldk/TwelveMonkeys.svg?branch=master)](https://travis-ci.com/github/haraldk/TwelveMonkeys)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.twelvemonkeys.imageio/imageio/badge.svg?color=slateblue)](https://maven-badges.herokuapp.com/maven-central/com.twelvemonkeys.imageio/imageio)
[![StackOverflow](https://img.shields.io/badge/stack_overflow-twelvemonkeys-orange.svg)](https://stackoverflow.com/questions/tagged/twelvemonkeys)
[![Donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://paypal.me/haraldk76/100)
## About
TwelveMonkeys ImageIO is a collection of plugins and extensions for Java's ImageIO.
TwelveMonkeys ImageIO provides extended image file format support for the Java platform, through plugins for the `javax.imageio.*` package.
These plugins extend the number of image file formats supported in Java, using the `javax.imageio.*` package.
The main purpose of this project is to provide support for formats not covered by the JRE itself.
Support for formats is important, both to be able to read data found
The main goal of this project is to provide support for formats not covered by the JRE itself.
Support for these formats is important, to be able to read data found
"in the wild", as well as to maintain access to data in legacy formats.
Because there is lots of legacy data out there, we see the need for open implementations of readers for popular formats.
The goal is to create a set of efficient and robust ImageIO plug-ins, that can be distributed independently.
As there is lots of legacy data out there, we see the need for open implementations of readers for popular formats.
----
+6 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<groupId>com.twelvemonkeys.bom</groupId>
@@ -123,6 +123,11 @@
<artifactId>imageio-tiff</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-xwd</artifactId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>common-image</artifactId>
<packaging>jar</packaging>
@@ -34,7 +34,13 @@ import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.ImagingOpException;
import java.awt.image.Raster;
import java.awt.image.RasterOp;
import java.awt.image.WritableRaster;
/**
* This is a drop-in replacement for {@link java.awt.image.AffineTransformOp}.
@@ -70,6 +76,7 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp {
delegate = new java.awt.image.AffineTransformOp(xform, interpolationType);
}
@SuppressWarnings("ConstantConditions")
@Override
public BufferedImage filter(final BufferedImage src, BufferedImage dst) {
try {
@@ -80,10 +87,9 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp {
dst = createCompatibleDestImage(src, src.getColorModel());
}
Graphics2D g2d = null;
Graphics2D g2d = dst.createGraphics();
try {
g2d = dst.createGraphics();
int interpolationType = delegate.getInterpolationType();
if (interpolationType > 0) {
@@ -109,9 +115,7 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp {
return dst;
}
finally {
if (g2d != null) {
g2d.dispose();
}
g2d.dispose();
}
}
}
@@ -45,8 +45,8 @@ import java.awt.image.BufferedImage;
*/
public class BufferedImageIcon implements Icon {
private final BufferedImage image;
private int width;
private int height;
private final int width;
private final int height;
private final boolean fast;
public BufferedImageIcon(BufferedImage pImage) {
@@ -81,11 +81,10 @@ public class BufferedImageIcon implements Icon {
else {
//System.out.println("Scaling using interpolation");
Graphics2D g2 = (Graphics2D) g;
AffineTransform xform = AffineTransform.getTranslateInstance(x, y);
xform.scale(width / (double) image.getWidth(), height / (double) image.getHeight());
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(image, xform, null);
AffineTransform transform = AffineTransform.getTranslateInstance(x, y);
transform.scale(width / (double) image.getWidth(), height / (double) image.getHeight());
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(image, transform, null);
}
}
}
@@ -587,6 +587,7 @@ class IndexImage {
* @deprecated Use {@link #getIndexColorModel(Image,int,int)} instead!
* This version will be removed in a later version of the API.
*/
@Deprecated
public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, boolean pFast) {
return getIndexColorModel(pImage, pNumberOfColors, pFast ? COLOR_SELECTION_FAST : COLOR_SELECTION_QUALITY);
}
@@ -30,17 +30,26 @@
package com.twelvemonkeys.image;
import org.junit.Test;
import static java.lang.Math.min;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import javax.imageio.ImageTypeSpecifier;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.*;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.DataBuffer;
import java.awt.image.ImagingOpException;
import java.awt.image.Raster;
import java.awt.image.RasterOp;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
import javax.imageio.ImageTypeSpecifier;
import org.junit.Test;
/**
* AffineTransformOpTest.
@@ -101,6 +110,7 @@ public class AffineTransformOpTest {
private final int width = 30;
private final int height = 20;
private final double anchor = min(width, height) / 2.0;
@Test
public void testGetPoint2D() {
@@ -128,8 +138,8 @@ public class AffineTransformOpTest {
@Test
public void testFilterRotateBIStandard() {
BufferedImageOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
BufferedImageOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
BufferedImageOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
BufferedImageOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
for (Integer type : TYPES) {
BufferedImage image = new BufferedImage(width, height, type);
@@ -147,8 +157,8 @@ public class AffineTransformOpTest {
@Test
public void testFilterRotateBICustom() {
BufferedImageOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
BufferedImageOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
BufferedImageOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
BufferedImageOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
for (ImageTypeSpecifier spec : SPECS) {
BufferedImage image = spec.createBufferedImage(width, height);
@@ -197,8 +207,8 @@ public class AffineTransformOpTest {
@Test
public void testFilterRotateRasterStandard() {
RasterOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
RasterOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
RasterOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
RasterOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
for (Integer type : TYPES) {
Raster raster = new BufferedImage(width, height, type).getRaster();
@@ -221,8 +231,6 @@ public class AffineTransformOpTest {
fail("No result!");
}
else {
System.err.println("AffineTransformOpTest.testFilterRotateRasterStandard");
System.err.println("type: " + type);
continue;
}
}
@@ -240,8 +248,8 @@ public class AffineTransformOpTest {
@Test
public void testFilterRotateRasterCustom() {
RasterOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
RasterOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null);
RasterOp jreOp = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
RasterOp tmOp = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, anchor, anchor), null);
for (ImageTypeSpecifier spec : SPECS) {
Raster raster = spec.createBufferedImage(width, height).getRaster();
@@ -264,8 +272,6 @@ public class AffineTransformOpTest {
fail("No result!");
}
else {
System.err.println("AffineTransformOpTest.testFilterRotateRasterCustom");
System.err.println("spec: " + spec);
continue;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>common-io</artifactId>
<packaging>jar</packaging>
@@ -65,6 +65,7 @@ import java.io.FilenameFilter;
* @see WildcardStringParser
* @deprecated
*/
@Deprecated
public class FilenameMaskFilter implements FilenameFilter {
// TODO: Rewrite to use regexp, or create new class
@@ -442,6 +442,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
* @see java.io.BufferedReader#readLine()
* @see java.io.DataInputStream#readLine()
*/
@Deprecated
public String readLine() throws IOException {
DataInputStream ds = new DataInputStream(in);
return ds.readLine();
@@ -29,6 +29,9 @@
package com.twelvemonkeys.xml;
import java.io.OutputStream;
import java.io.Writer;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.DOMImplementationList;
import org.w3c.dom.Document;
@@ -38,9 +41,6 @@ import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;
import java.io.OutputStream;
import java.io.Writer;
/**
* {@code DOMImplementationLS} backed implementation.
*
@@ -88,17 +88,6 @@ public final class DOMSerializer {
output.setCharacterStream(pStream);
}
/*
// TODO: Is it useful?
public void setNewLine(final String pNewLine) {
serializer.setNewLine(pNewLine);
}
public String getNewLine() {
return serializer.getNewLine();
}
*/
/**
* Specifies wether the serializer should use indentation and optimize for
* readability.
@@ -169,13 +158,7 @@ public final class DOMSerializer {
try {
return DOMImplementationRegistry.newInstance();
}
catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
catch (InstantiationException e) {
throw new IllegalStateException(e);
}
catch (IllegalAccessException e) {
catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
@@ -30,16 +30,23 @@
package com.twelvemonkeys.xml;
import com.twelvemonkeys.lang.StringUtil;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.Date;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import com.twelvemonkeys.lang.StringUtil;
/**
* XMLSerializer
@@ -290,7 +297,7 @@ public class XMLSerializer {
}
private static int appendAndEscape(final String pString, int pStart, final int pEnd, final StringBuilder pBuilder, final String pEntity) {
pBuilder.append(pString.substring(pStart, pEnd));
pBuilder.append(pString, pStart, pEnd);
pBuilder.append(pEntity);
return pEnd + 1;
}
@@ -527,8 +534,7 @@ public class XMLSerializer {
builder = factory.newDocumentBuilder();
}
catch (ParserConfigurationException e) {
//noinspection ThrowableInstanceNeverThrown BOGUS
throw (IOException) new IOException(e.getMessage()).initCause(e);
throw new IOException(e);
}
DOMImplementation dom = builder.getDOMImplementation();
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>common-lang</artifactId>
<packaging>jar</packaging>
@@ -770,6 +770,7 @@ public final class StringUtil {
*/
/*public*/
@Deprecated
static String formatNumber(long pNum, int pLen) throws IllegalArgumentException {
StringBuilder result = new StringBuilder();
@@ -1464,6 +1465,7 @@ public final class StringUtil {
*/
/*public*/
@Deprecated
static String removeSubstring(final String pSource, final char pBeginBoundaryChar, final char pEndBoundaryChar, final int pOffset) {
StringBuilder filteredString = new StringBuilder();
boolean insideDemarcatedArea = false;
@@ -157,6 +157,7 @@ public class Time {
* @see #parseTime(String)
* @deprecated
*/
@Deprecated
public String toString(String pFormatStr) {
TimeFormat tf = new TimeFormat(pFormatStr);
@@ -174,6 +175,7 @@ public class Time {
* @see #toString(String)
* @deprecated
*/
@Deprecated
public static Time parseTime(String pStr) {
TimeFormat tf = TimeFormat.getInstance();
@@ -111,6 +111,7 @@ import java.io.PrintStream;
* @author <a href="mailto:eirik.torske@iconmedialab.no">Eirik Torske</a>
* @deprecated Will probably be removed in the near future
*/
@Deprecated
public class WildcardStringParser {
// TODO: Get rid of this class
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<groupId>com.twelvemonkeys.contrib</groupId>
<artifactId>contrib</artifactId>
@@ -31,8 +31,9 @@
package com.twelvemonkeys.contrib.tiff;
import com.twelvemonkeys.contrib.tiff.TIFFUtilities.TIFFExtension;
import com.twelvemonkeys.imageio.plugins.tiff.TIFFMedataFormat;
import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataFormat;
import com.twelvemonkeys.io.FileUtil;
import org.junit.Assert;
import org.junit.Test;
import org.w3c.dom.Node;
@@ -154,7 +155,7 @@ public class TIFFUtilitiesTest {
reader.setInput(checkTest1);
for (int i = 0; i < 3; i++) {
Node metaData = reader.getImageMetadata(i)
.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
.getAsTree(TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
short orientation = ((Number) expression.evaluate(metaData, XPathConstants.NUMBER)).shortValue();
Assert.assertEquals(orientation, TIFFExtension.ORIENTATION_RIGHTTOP);
}
@@ -171,7 +172,7 @@ public class TIFFUtilitiesTest {
reader.setInput(checkTest2);
for (int i = 0; i < 3; i++) {
Node metaData = reader.getImageMetadata(i)
.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
.getAsTree(TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
short orientation = ((Number) expression.evaluate(metaData, XPathConstants.NUMBER)).shortValue();
Assert.assertEquals(orientation, i == 1
? TIFFExtension.ORIENTATION_BOTRIGHT
+2 -9
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-batik</artifactId>
<name>TwelveMonkeys :: ImageIO :: Batik Plugin</name>
@@ -68,13 +68,6 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>xmlgraphics-commons</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-anim</artifactId>
@@ -98,7 +91,7 @@
<!--
There seems to be some weirdness in the
Batik/FOP poms (Batik depends on FOP 0.20-5) that screws things up,
making everything end up depending on Batik 1.5, not 1.6
making everything end up depending on Batik 1.5, not the specified version
-->
<exclusions>
<exclusion>
@@ -30,37 +30,8 @@
package com.twelvemonkeys.imageio.plugins.svg;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.StringUtil;
import org.apache.batik.anim.dom.SVGDOMImplementation;
import org.apache.batik.anim.dom.SVGOMDocument;
import org.apache.batik.bridge.*;
import org.apache.batik.css.parser.CSSLexicalUnit;
import org.apache.batik.dom.util.DOMUtilities;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.gvt.CanvasGraphicsNode;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.renderer.ConcreteImageRendererFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
import org.apache.batik.gvt.renderer.ImageRendererFactory;
import org.apache.batik.transcoder.*;
import org.apache.batik.transcoder.image.ImageTranscoder;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.xml.LexicalUnits;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.svg.SVGSVGElement;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
@@ -70,6 +41,38 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import org.apache.batik.anim.dom.SVGDOMImplementation;
import org.apache.batik.anim.dom.SVGOMDocument;
import org.apache.batik.bridge.*;
import org.apache.batik.dom.util.DOMUtilities;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.gvt.CanvasGraphicsNode;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.renderer.ConcreteImageRendererFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
import org.apache.batik.gvt.renderer.ImageRendererFactory;
import org.apache.batik.transcoder.SVGAbstractTranscoder;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.TranscodingHints;
import org.apache.batik.transcoder.image.ImageTranscoder;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.SVGConstants;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.svg.SVGSVGElement;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.StringUtil;
/**
* Image reader for SVG document fragments.
*
@@ -132,6 +135,7 @@ public class SVGImageReader extends ImageReaderBase {
// Set ImageReadParams as hints
// Note: The cast to Map invokes a different method that preserves
// unset defaults, DO NOT REMOVE!
//noinspection rawtypes
rasterizer.setTranscodingHints((Map) paramsToHints(svgParam));
}
@@ -260,7 +264,7 @@ public class SVGImageReader extends ImageReaderBase {
}
}
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) {
return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(rasterizer.createImage(1, 1))).iterator();
}
@@ -289,7 +293,7 @@ public class SVGImageReader extends ImageReaderBase {
}
// This is cheating... We don't fully transcode after all
protected void transcode(Document document, final String uri, final TranscoderOutput output) throws TranscoderException {
protected void transcode(Document document, final String uri, final TranscoderOutput output) {
// Sets up root, curTxf & curAoi
// ----
if (document != null) {
@@ -584,9 +588,7 @@ public class SVGImageReader extends ImageReaderBase {
return dest;
}
catch (Exception ex) {
TranscoderException exception = new TranscoderException(ex.getMessage());
exception.initCause(ex);
throw exception;
throw new TranscoderException(ex.getMessage(), ex);
}
finally {
if (context != null) {
@@ -54,8 +54,6 @@ import java.util.Collections;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
/**
@@ -225,7 +223,7 @@ public class SVGImageReaderTest extends ImageReaderAbstractTest<SVGImageReader>
assertEquals(500, image.getHeight());
// CSS and embedded resources all go!
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
}
finally {
reader.dispose();
@@ -266,7 +264,7 @@ public class SVGImageReaderTest extends ImageReaderAbstractTest<SVGImageReader>
assertEquals(500, image.getHeight());
// No more warnings now that the base URI is set
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
}
finally {
reader.dispose();
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-bmp</artifactId>
<name>TwelveMonkeys :: ImageIO :: BMP plugin</name>
@@ -30,6 +30,22 @@
package com.twelvemonkeys.imageio.plugins.bmp;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
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 java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
@@ -40,27 +56,6 @@ import java.nio.ByteOrder;
import java.util.Collections;
import java.util.Iterator;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
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 com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.xml.XMLSerializer;
/**
* ImageReader for Microsoft Windows Bitmap (BMP) format.
*
@@ -482,8 +477,12 @@ public final class BMPImageReader extends ImageReaderBase {
private void readRowByte(final DataInput input, final int height, final Rectangle srcRegion, final int xSub, final int ySub,
final byte[] rowDataByte, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
// Flip into position?
int srcY = !header.topDown ? height - 1 - y : y;
int dstY = (srcY - srcRegion.y) / ySub;
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
if (srcY % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataByte.length);
return;
@@ -498,19 +497,17 @@ public final class BMPImageReader extends ImageReaderBase {
}
}
if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel);
} else {
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
}
destChannel.setDataElements(0, dstY, srcChannel);
}
private void readRowUShort(final DataInput input, final int height, final Rectangle srcRegion, final int xSub, final int ySub,
final short[] rowDataUShort, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
// Flip into position?
int srcY = !header.topDown ? height - 1 - y : y;
int dstY = (srcY - srcRegion.y) / ySub;
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
if (srcY % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataUShort.length * 2 + (rowDataUShort.length % 2) * 2);
return;
@@ -530,19 +527,17 @@ public final class BMPImageReader extends ImageReaderBase {
}
}
if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel);
} else {
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
}
destChannel.setDataElements(0, dstY, srcChannel);
}
private void readRowInt(final DataInput input, final int height, final Rectangle srcRegion, final int xSub, final int ySub,
final int[] rowDataInt, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
// Flip into position?
int srcY = !header.topDown ? height - 1 - y : y;
int dstY = (srcY - srcRegion.y) / ySub;
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
if (srcY % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataInt.length * 4);
return;
@@ -557,13 +552,7 @@ public final class BMPImageReader extends ImageReaderBase {
}
}
if (header.topDown) {
destChannel.setDataElements(0, y, srcChannel);
} else {
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
}
destChannel.setDataElements(0, dstY, srcChannel);
}
// TODO: Candidate util method
@@ -32,8 +32,8 @@ package com.twelvemonkeys.imageio.plugins.bmp;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNoException;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -295,7 +295,7 @@ public class BMPImageReaderTest extends ImageReaderAbstractTest<BMPImageReader>
// At least imageStarted and imageComplete, plus any number of imageProgress
InOrder ordered = inOrder(listener);
ordered.verify(listener).imageStarted(reader, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyFloat());
ordered.verify(listener).imageComplete(reader);
}
@@ -318,7 +318,7 @@ public class BMPImageReaderTest extends ImageReaderAbstractTest<BMPImageReader>
// At least imageStarted and imageComplete, plus any number of imageProgress
InOrder ordered = inOrder(listener);
ordered.verify(listener).imageStarted(reader, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyFloat());
ordered.verify(listener).imageComplete(reader);
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-clippath</artifactId>
<name>TwelveMonkeys :: ImageIO :: Photoshop Path Support</name>
@@ -39,6 +39,7 @@ import java.io.IOException;
*
* @deprecated Use {@link AdobePathReader} instead. This class will be removed in a future release.
*/
@Deprecated
public final class AdobePathBuilder {
private final AdobePathReader delegate;
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-core</artifactId>
<name>TwelveMonkeys :: ImageIO :: Core</name>
@@ -0,0 +1,563 @@
/*
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.color;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.Platform;
import com.twelvemonkeys.lang.SystemUtil;
import com.twelvemonkeys.lang.Validate;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Properties;
import static com.twelvemonkeys.imageio.color.ColorSpaces.DEBUG;
/**
* A helper class for working with ICC color profiles.
* <p>
* Standard ICC color profiles are read from system-specific locations
* for known operating systems.
* </p>
* <p>
* Color profiles may be configured by placing a property-file
* {@code com/twelvemonkeys/imageio/color/icc_profiles.properties}
* on the classpath, specifying the full path to the profiles.
* ICC color profiles are probably already present on your system, or
* can be downloaded from
* <a href="http://www.color.org/profiles2.xalter">ICC</a>,
* <a href="http://www.adobe.com/downloads/">Adobe</a> or other places.
* * </p>
* <p>
* Example property file:
* </p>
* <pre>
* # icc_profiles.properties
* ADOBE_RGB_1998=/path/to/Adobe RGB 1998.icc
* GENERIC_CMYK=/path/to/Generic CMYK.icc
* </pre>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ColorSpaces.java,v 1.0 24.01.11 17.51 haraldk Exp$
*/
public final class ColorProfiles {
/**
* We need special ICC profile handling for KCMS vs LCMS. Delegate to specific strategy.
*/
private final static ICCProfileSanitizer profileCleaner = ICCProfileSanitizer.Factory.get();
static final int ICC_PROFILE_MAGIC = 'a' << 24 | 'c' << 16 | 's' << 8 | 'p';
static final int ICC_PROFILE_HEADER_SIZE = 128;
static {
// In case we didn't activate through SPI already
ProfileDeferralActivator.activateProfiles();
}
private ColorProfiles() {
}
static byte[] getProfileHeaderWithProfileId(final ICC_Profile profile) {
// Get *entire profile data*... :-/
return getProfileHeaderWithProfileId(profile.getData());
}
static byte[] getProfileHeaderWithProfileId(byte[] data) {
// ICC profile header is the first 128 bytes
byte[] header = Arrays.copyOf(data, ICC_PROFILE_HEADER_SIZE);
// Clear out preferred CMM, platform & creator, as these don't affect the profile in any way
// - LCMS updates CMM + creator to "lcms" and platform to current platform
// - KCMS keeps the values in the file...
Arrays.fill(header, ICC_Profile.icHdrCmmId, ICC_Profile.icHdrCmmId + 4, (byte) 0);
Arrays.fill(header, ICC_Profile.icHdrPlatform, ICC_Profile.icHdrPlatform + 4, (byte) 0);
// + Clear out rendering intent, as this may be updated by application
Arrays.fill(header, ICC_Profile.icHdrRenderingIntent, ICC_Profile.icHdrRenderingIntent + 4, (byte) 0);
Arrays.fill(header, ICC_Profile.icHdrCreator, ICC_Profile.icHdrCreator + 4, (byte) 0);
// Clear out any existing MD5, as it is no longer correct
Arrays.fill(header, ICC_Profile.icHdrProfileID, ICC_Profile.icHdrProfileID + 16, (byte) 0);
// Generate new MD5 and store in header
byte[] md5 = computeMD5(header, data);
System.arraycopy(md5, 0, header, ICC_Profile.icHdrProfileID, md5.length);
return header;
}
private static byte[] computeMD5(byte[] header, byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(header, 0, ICC_PROFILE_HEADER_SIZE);
digest.update(data, ICC_PROFILE_HEADER_SIZE, data.length - ICC_PROFILE_HEADER_SIZE);
return digest.digest();
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Missing MD5 MessageDigest");
}
}
/**
* Tests whether an ICC color profile is equal to the default sRGB profile.
*
* @param profile the ICC profile to test. May not be {@code null}.
* @return {@code true} if {@code profile} is equal to the default sRGB profile.
* @throws IllegalArgumentException if {@code profile} is {@code null}
* @see java.awt.color.ColorSpace#CS_sRGB
* @see java.awt.color.ColorSpace#isCS_sRGB()
*/
public static boolean isCS_sRGB(final ICC_Profile profile) {
Validate.notNull(profile, "profile");
return profile.getColorSpaceType() == ColorSpace.TYPE_RGB && Arrays.equals(getProfileHeaderWithProfileId(profile), sRGB.header);
}
/**
* Tests whether an ICC color profile is equal to the default GRAY profile.
*
* @param profile the ICC profile to test. May not be {@code null}.
* @return {@code true} if {@code profile} is equal to the default GRAY profile.
* @throws IllegalArgumentException if {@code profile} is {@code null}
* @see java.awt.color.ColorSpace#CS_GRAY
*/
public static boolean isCS_GRAY(final ICC_Profile profile) {
Validate.notNull(profile, "profile");
return profile.getColorSpaceType() == ColorSpace.TYPE_GRAY && Arrays.equals(getProfileHeaderWithProfileId(profile), GRAY.header);
}
/**
* Tests whether an ICC color profile is known to cause problems for {@link java.awt.image.ColorConvertOp}.
* <p>
* <em>
* Note that this method only tests if a color conversion using this profile is known to fail.
* There's no guarantee that the color conversion will succeed even if this method returns {@code false}.
* </em>
* </p>
*
* @param profile the ICC color profile. May not be {@code null}.
* @return {@code true} if known to be offending, {@code false} otherwise
* @throws IllegalArgumentException if {@code profile} is {@code null}
*/
static boolean isOffendingColorProfile(final ICC_Profile profile) {
Validate.notNull(profile, "profile");
// NOTE:
// Several embedded ICC color profiles are non-compliant with Java pre JDK7 and throws CMMException
// The problem with these embedded ICC profiles seems to be the rendering intent
// 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
byte[] header = profile.getData(ICC_Profile.icSigHead);
return header[ICC_Profile.icHdrRenderingIntent] != 0 || header[ICC_Profile.icHdrRenderingIntent + 1] != 0
|| header[ICC_Profile.icHdrRenderingIntent + 2] != 0 || header[ICC_Profile.icHdrRenderingIntent + 3] > 3;
}
/**
* Tests whether an ICC color profile is valid.
* Invalid profiles are known to cause problems for {@link java.awt.image.ColorConvertOp}.
* <p>
* <em>
* Note that this method only tests if a color conversion using this profile is known to fail.
* There's no guarantee that the color conversion will succeed even if this method returns {@code false}.
* </em>
* </p>
*
* @param profile the ICC color profile. May not be {@code null}.
* @return {@code profile} if valid.
* @throws IllegalArgumentException if {@code profile} is {@code null}
* @throws java.awt.color.CMMException if {@code profile} is invalid.
*/
public static ICC_Profile validateProfile(final ICC_Profile profile) {
// Fix profile before validation
profileCleaner.fixProfile(profile);
ColorSpaces.validateColorSpace(new ICC_ColorSpace(profile)); // TODO: Should use createColorSpace and cache if good?
return profile;
}
/**
* Reads an ICC Profile from the given input stream, as-is, with no validation.
*
* This method behaves exactly like {@code ICC_Profile.getInstance(input)}.
*
* @param input the input stream to read from, may not be {@code null}
* @return an {@code ICC_Profile} object as read from the input stream.
* @throws IOException If an I/O error occurs while reading the stream.
* @throws IllegalArgumentException If {@code input} is {@code null}
* or the stream does not contain valid ICC Profile data.
* @see ICC_Profile#getInstance(InputStream)
* @see #readProfile(InputStream)
*/
public static ICC_Profile readProfileRaw(final InputStream input) throws IOException {
Validate.notNull(input, "input");
return ICC_Profile.getInstance(input);
}
/**
* Reads an ICC Profile from the given input stream, with extra validation.
*
* If a matching profile already exists in cache, the cached instance is returned.
*
* @param input the input stream to read from, may not be {@code null}
* @return an {@code ICC_Profile} object as read from the input stream.
* @throws IOException If an I/O error occurs while reading the stream.
* @throws IllegalArgumentException If {@code input} is {@code null}
* or the stream does not contain valid ICC Profile data.
* @see ICC_Profile#getInstance(InputStream)
*/
public static ICC_Profile readProfile(final InputStream input) throws IOException {
Validate.notNull(input, "input");
DataInputStream dataInput = new DataInputStream(input);
byte[] header = new byte[ICC_PROFILE_HEADER_SIZE];
try {
dataInput.readFully(header);
int size = validateHeaderAndGetSize(header);
byte[] data = Arrays.copyOf(header, size);
dataInput.readFully(data, header.length, size - header.length);
return createProfile(data);
}
catch (EOFException e) {
throw new IllegalArgumentException("Truncated ICC Profile data", e);
}
}
/**
* Creates an ICC Profile from the given byte array, as-is, with no validation.
*
* This method behaves exactly like {@code ICC_Profile.getInstance(input)},
* except that extraneous bytes at the end of the array is ignored.
*
* @param input the byte array to create a profile from, may not be {@code null}
* @return an {@code ICC_Profile} object created from the byte array
* @throws IllegalArgumentException If {@code input} is {@code null}
* or the byte array does not contain valid ICC Profile data.
* @see ICC_Profile#getInstance(byte[])
* @see #createProfile(byte[])
*/
public static ICC_Profile createProfileRaw(final byte[] input) {
int size = validateHeaderAndGetSize(input);
// Unlike the InputStream version, the byte version of ICC_Profile.getInstance()
// does not discard extra bytes at the end. We'll chop them off here for convenience
return ICC_Profile.getInstance(limit(input, size));
}
/**
* Reads an ICC Profile from the given byte array, with extra validation.
* Extraneous bytes at the end of the array are ignored.
*
* If a matching profile already exists in cache, the cached instance is returned.
*
* @param input the byte array to create a profile from, may not be {@code null}
* @return an {@code ICC_Profile} object created from the byte array
* @throws IllegalArgumentException If {@code input} is {@code null}
* or the byte array does not contain valid ICC Profile data.
* @see ICC_Profile#getInstance(byte[])
*/
public static ICC_Profile createProfile(final byte[] input) {
int size = validateAndGetSize(input);
// Look up in cache before returning, these are already validated
byte[] profileHeader = getProfileHeaderWithProfileId(input);
ICC_Profile internal = getInternalProfile(profileHeader);
if (internal != null) {
return internal;
}
ICC_ColorSpace cached = ColorSpaces.getCachedCS(profileHeader);
if (cached != null) {
return cached.getProfile();
}
ICC_Profile profile = ICC_Profile.getInstance(limit(input, size));
// We'll validate & cache by creating a color space and returning its profile...
// TODO: Rewrite with separate cache for profiles...
return ColorSpaces.createColorSpace(profile).getProfile();
}
private static byte[] limit(byte[] input, int size) {
return input.length == size ? input : Arrays.copyOf(input, size);
}
private static int validateAndGetSize(byte[] input) {
int size = validateHeaderAndGetSize(input);
if (size < 0 || size > input.length) {
throw new IllegalArgumentException("Truncated ICC profile data, length < " + size + ": " + input.length);
}
return size;
}
private static int validateHeaderAndGetSize(byte[] input) {
Validate.notNull(input, "input");
if (input.length < ICC_PROFILE_HEADER_SIZE) { // Can't be less than size of ICC header
throw new IllegalArgumentException("Truncated ICC profile data, length < 128: " + input.length);
}
int size = intBigEndian(input, ICC_Profile.icHdrSize);
if (intBigEndian(input, ICC_Profile.icHdrMagic) != ICC_PROFILE_MAGIC) {
throw new IllegalArgumentException("Not an ICC profile, missing file signature");
}
return size;
}
private static ICC_Profile getInternalProfile(final byte[] profileHeader) {
int profileCSType = getCsType(profileHeader);
if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, ColorProfiles.sRGB.header)) {
return ICC_Profile.getInstance(ColorSpace.CS_sRGB);
}
else if (profileCSType == ColorSpace.TYPE_GRAY && Arrays.equals(profileHeader, ColorProfiles.GRAY.header)) {
return ICC_Profile.getInstance(ColorSpace.CS_GRAY);
}
else if (profileCSType == ColorSpace.TYPE_3CLR && Arrays.equals(profileHeader, ColorProfiles.PYCC.header)) {
return ICC_Profile.getInstance(ColorSpace.CS_PYCC);
}
else if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, ColorProfiles.LINEAR_RGB.header)) {
return ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB);
}
else if (profileCSType == ColorSpace.TYPE_XYZ && Arrays.equals(profileHeader, ColorProfiles.CIEXYZ.header)) {
return ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ);
}
return null;
}
private static int intBigEndian(byte[] data, int index) {
return (data[index] & 0xff) << 24 | (data[index + 1] & 0xff) << 16 | (data[index + 2] & 0xff) << 8 | (data[index + 3] & 0xff);
}
private static int getCsType(byte[] profileHeader) {
int csSig = intBigEndian(profileHeader, ICC_Profile.icHdrColorSpace);
switch (csSig) {
case ICC_Profile.icSigXYZData:
return ColorSpace.TYPE_XYZ;
case ICC_Profile.icSigLabData:
return ColorSpace.TYPE_Lab;
case ICC_Profile.icSigLuvData:
return ColorSpace.TYPE_Luv;
case ICC_Profile.icSigYCbCrData:
return ColorSpace.TYPE_YCbCr;
case ICC_Profile.icSigYxyData:
return ColorSpace.TYPE_Yxy;
case ICC_Profile.icSigRgbData:
return ColorSpace.TYPE_RGB;
case ICC_Profile.icSigGrayData:
return ColorSpace.TYPE_GRAY;
case ICC_Profile.icSigHsvData:
return ColorSpace.TYPE_HSV;
case ICC_Profile.icSigHlsData:
return ColorSpace.TYPE_HLS;
case ICC_Profile.icSigCmykData:
return ColorSpace.TYPE_CMYK;
// Note: There is no TYPE_* 10...
case ICC_Profile.icSigCmyData:
return ColorSpace.TYPE_CMY;
case ICC_Profile.icSigSpace2CLR:
return ColorSpace.TYPE_2CLR;
case ICC_Profile.icSigSpace3CLR:
return ColorSpace.TYPE_3CLR;
case ICC_Profile.icSigSpace4CLR:
return ColorSpace.TYPE_4CLR;
case ICC_Profile.icSigSpace5CLR:
return ColorSpace.TYPE_5CLR;
case ICC_Profile.icSigSpace6CLR:
return ColorSpace.TYPE_6CLR;
case ICC_Profile.icSigSpace7CLR:
return ColorSpace.TYPE_7CLR;
case ICC_Profile.icSigSpace8CLR:
return ColorSpace.TYPE_8CLR;
case ICC_Profile.icSigSpace9CLR:
return ColorSpace.TYPE_9CLR;
case ICC_Profile.icSigSpaceACLR:
return ColorSpace.TYPE_ACLR;
case ICC_Profile.icSigSpaceBCLR:
return ColorSpace.TYPE_BCLR;
case ICC_Profile.icSigSpaceCCLR:
return ColorSpace.TYPE_CCLR;
case ICC_Profile.icSigSpaceDCLR:
return ColorSpace.TYPE_DCLR;
case ICC_Profile.icSigSpaceECLR:
return ColorSpace.TYPE_ECLR;
case ICC_Profile.icSigSpaceFCLR:
return ColorSpace.TYPE_FCLR;
default:
throw new IllegalArgumentException("Invalid ICC color space signature: " + csSig); // TODO: fourCC?
}
}
static ICC_Profile readProfileFromClasspathResource(@SuppressWarnings("SameParameterValue") final String profilePath) {
InputStream stream = ColorSpaces.class.getResourceAsStream(profilePath);
if (stream != null) {
if (DEBUG) {
System.out.println("Loading profile from classpath resource: " + profilePath);
}
try {
return ICC_Profile.getInstance(stream);
}
catch (@SuppressWarnings("CatchMayIgnoreException") IOException ignore) {
if (DEBUG) {
ignore.printStackTrace();
}
}
finally {
FileUtil.close(stream);
}
}
return null;
}
static ICC_Profile readProfileFromPath(final String profilePath) {
if (profilePath != null) {
if (DEBUG) {
System.out.println("Loading profile from: " + profilePath);
}
try {
return ICC_Profile.getInstance(profilePath);
}
catch (@SuppressWarnings("CatchMayIgnoreException") SecurityException | IOException ignore) {
if (DEBUG) {
ignore.printStackTrace();
}
}
}
return null;
}
static void fixProfile(ICC_Profile profile) {
profileCleaner.fixProfile(profile);
}
static boolean validationAltersProfileHeader() {
return profileCleaner.validationAltersProfileHeader();
}
// Cache header profile data to avoid excessive array creation/copying. Use static inner class for on-demand lazy init
static class sRGB {
static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_sRGB));
}
static class CIEXYZ {
static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ));
}
static class PYCC {
static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_PYCC));
}
static class GRAY {
static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_GRAY));
}
static class LINEAR_RGB {
static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB));
}
static class Profiles {
// TODO: Honour java.iccprofile.path property?
private static final Properties PROFILES = loadProfiles();
private static Properties loadProfiles() {
Properties systemDefaults;
try {
systemDefaults = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles_" + Platform.os().id());
}
catch (@SuppressWarnings("CatchMayIgnoreException") SecurityException | IOException ignore) {
System.err.printf(
"Warning: Could not load system default ICC profile locations from %s, will use bundled fallback profiles.\n",
ignore.getMessage()
);
if (DEBUG) {
ignore.printStackTrace();
}
systemDefaults = null;
}
// Create map with defaults and add user overrides if any
Properties profiles = new Properties(systemDefaults);
try {
Properties userOverrides = SystemUtil.loadProperties(
ColorSpaces.class,
"com/twelvemonkeys/imageio/color/icc_profiles"
);
profiles.putAll(userOverrides);
}
catch (SecurityException | IOException ignore) {
// Most likely, this file won't be there
}
if (DEBUG) {
System.out.println("User ICC profiles: " + profiles);
System.out.println("System ICC profiles : " + systemDefaults);
}
return profiles;
}
static String getPath(final String profileName) {
return PROFILES.getProperty(profileName);
}
}
}
@@ -30,23 +30,17 @@
package com.twelvemonkeys.imageio.color;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.Platform;
import com.twelvemonkeys.lang.SystemUtil;
import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.util.LRUHashMap;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import static com.twelvemonkeys.imageio.color.ColorProfiles.*;
/**
* A helper class for working with ICC color profiles and color spaces.
@@ -83,9 +77,6 @@ public final class ColorSpaces {
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.color.debug"));
/** We need special ICC profile handling for KCMS vs LCMS. Delegate to specific strategy. */
private final static ICCProfileSanitizer profileCleaner = ICCProfileSanitizer.Factory.get();
// NOTE: java.awt.color.ColorSpace.CS_* uses 1000-1004, we'll use 5000+ to not interfere with future additions
/** The Adobe RGB 1998 (or compatible) color space. Either read from disk or built-in. */
@@ -96,24 +87,17 @@ public final class ColorSpaces {
@SuppressWarnings("WeakerAccess")
public static final int CS_GENERIC_CMYK = 5001;
// TODO: Move to ColorProfiles OR cache ICC_ColorSpace instead?
// Weak references to hold the color spaces while cached
private static WeakReference<ICC_Profile> adobeRGB1998 = new WeakReference<>(null);
private static WeakReference<ICC_Profile> genericCMYK = new WeakReference<>(null);
// Cache for the latest used color spaces
private static final Map<Key, ICC_ColorSpace> cache = new LRUHashMap<>(10);
private static final Map<Key, ICC_ColorSpace> cache = new LRUHashMap<>(16);
static {
try {
// Force invocation of ProfileDeferralMgr.activateProfiles() to avoid JDK-6986863
ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData();
}
catch (Throwable disasters) {
System.err.println("ICC Color Profile not properly activated due to the exception below.");
System.err.println("Expect to see JDK-6986863 in action, and consider filing a bug report to your JRE provider.");
disasters.printStackTrace();
}
// In case we didn't activate through SPI already
ProfileDeferralActivator.activateProfiles();
}
private ColorSpaces() {}
@@ -134,7 +118,7 @@ public final class ColorSpaces {
Validate.notNull(profile, "profile");
// Fix profile before lookup/create
profileCleaner.fixProfile(profile);
fixProfile(profile);
byte[] profileHeader = getProfileHeaderWithProfileId(profile);
@@ -146,53 +130,20 @@ public final class ColorSpaces {
return getCachedOrCreateCS(profile, profileHeader);
}
private static byte[] getProfileHeaderWithProfileId(final ICC_Profile profile) {
// Get *entire profile data*... :-/
byte[] data = profile.getData();
// Clear out preferred CMM, platform & creator, as these does not affect the profile in any way
// - LCMS updates CMM + creator to "lcms" and platform to current platform
// - KCMS keeps the values in the file...
Arrays.fill(data, ICC_Profile.icHdrCmmId, ICC_Profile.icHdrCmmId + 4, (byte) 0);
Arrays.fill(data, ICC_Profile.icHdrPlatform, ICC_Profile.icHdrPlatform + 4, (byte) 0);
// + Clear out rendering intent, as this may be updated by application
Arrays.fill(data, ICC_Profile.icHdrRenderingIntent, ICC_Profile.icHdrRenderingIntent + 4, (byte) 0);
Arrays.fill(data, ICC_Profile.icHdrCreator, ICC_Profile.icHdrCreator + 4, (byte) 0);
// Clear out any existing MD5, as it is no longer correct
Arrays.fill(data, ICC_Profile.icHdrProfileID, ICC_Profile.icHdrProfileID + 16, (byte) 0);
// Generate new MD5 and store in header
byte[] md5 = computeMD5(data);
System.arraycopy(md5, 0, data, ICC_Profile.icHdrProfileID, md5.length);
// ICC profile header is the first 128 bytes
return Arrays.copyOf(data, 128);
}
private static byte[] computeMD5(byte[] data) {
try {
return MessageDigest.getInstance("MD5").digest(data);
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Missing MD5 MessageDigest");
}
}
private static ICC_ColorSpace getInternalCS(final int profileCSType, final byte[] profileHeader) {
if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, sRGB.header)) {
static ICC_ColorSpace getInternalCS(final int profileCSType, final byte[] profileHeader) {
if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, ColorProfiles.sRGB.header)) {
return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_sRGB);
}
else if (profileCSType == ColorSpace.TYPE_GRAY && Arrays.equals(profileHeader, GRAY.header)) {
else if (profileCSType == ColorSpace.TYPE_GRAY && Arrays.equals(profileHeader, ColorProfiles.GRAY.header)) {
return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_GRAY);
}
else if (profileCSType == ColorSpace.TYPE_3CLR && Arrays.equals(profileHeader, PYCC.header)) {
else if (profileCSType == ColorSpace.TYPE_3CLR && Arrays.equals(profileHeader, ColorProfiles.PYCC.header)) {
return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_PYCC);
}
else if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, LINEAR_RGB.header)) {
else if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, ColorProfiles.LINEAR_RGB.header)) {
return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
}
else if (profileCSType == ColorSpace.TYPE_XYZ && Arrays.equals(profileHeader, CIEXYZ.header)) {
else if (profileCSType == ColorSpace.TYPE_XYZ && Arrays.equals(profileHeader, ColorProfiles.CIEXYZ.header)) {
return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);
}
@@ -203,26 +154,36 @@ public final class ColorSpaces {
Key key = new Key(profileHeader);
synchronized (cache) {
ICC_ColorSpace cs = cache.get(key);
ICC_ColorSpace cs = getCachedCS(key);
if (cs == null) {
cs = new ICC_ColorSpace(profile);
validateColorSpace(cs);
// On LCMS, validation *alters* the profile header, need to re-generate key
key = profileCleaner.validationAltersProfileHeader()
? new Key(getProfileHeaderWithProfileId(cs.getProfile()))
: key;
cache.put(key, cs);
// On LCMS, validation *alters* the profile header, need to re-generate key
if (ColorProfiles.validationAltersProfileHeader()) {
cache.put(new Key(getProfileHeaderWithProfileId(cs.getProfile())), cs);
}
}
return cs;
}
}
private static void validateColorSpace(final ICC_ColorSpace cs) {
private static ICC_ColorSpace getCachedCS(Key profileKey) {
synchronized (cache) {
return cache.get(profileKey);
}
}
static ICC_ColorSpace getCachedCS(final byte[] profileHeader) {
return getCachedCS(new Key(profileHeader));
}
static void validateColorSpace(final ICC_ColorSpace cs) {
// Validate the color space, to avoid caching bad profiles/color spaces
// Will throw IllegalArgumentException or CMMException if the profile is bad
cs.fromRGB(new float[] {0.999f, 0.5f, 0.001f});
@@ -233,73 +194,27 @@ public final class ColorSpaces {
}
/**
* Tests whether an ICC color profile is equal to the default sRGB profile.
*
* @param profile the ICC profile to test. May not be {@code null}.
* @return {@code true} if {@code profile} is equal to the default sRGB profile.
* @throws IllegalArgumentException if {@code profile} is {@code null}
*
* @see java.awt.color.ColorSpace#isCS_sRGB()
* @deprecated Use {@link ColorProfiles#isCS_sRGB(ICC_Profile)} instead.
*/
@Deprecated
public static boolean isCS_sRGB(final ICC_Profile profile) {
Validate.notNull(profile, "profile");
return profile.getColorSpaceType() == ColorSpace.TYPE_RGB && Arrays.equals(getProfileHeaderWithProfileId(profile), sRGB.header);
return ColorProfiles.isCS_sRGB(profile);
}
/**
* Tests whether an ICC color profile is known to cause problems for {@link java.awt.image.ColorConvertOp}.
* <p>
* <em>
* Note that this method only tests if a color conversion using this profile is known to fail.
* There's no guarantee that the color conversion will succeed even if this method returns {@code false}.
* </em>
* </p>
*
* @param profile the ICC color profile. May not be {@code null}.
* @return {@code true} if known to be offending, {@code false} otherwise
* @throws IllegalArgumentException if {@code profile} is {@code null}
* @deprecated Use {@link ColorProfiles#isCS_GRAY(ICC_Profile)} instead.
*/
static boolean isOffendingColorProfile(final ICC_Profile profile) {
Validate.notNull(profile, "profile");
// NOTE:
// Several embedded ICC color profiles are non-compliant with Java pre JDK7 and throws CMMException
// The problem with these embedded ICC profiles seems to be the rendering intent
// 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
byte[] header = profile.getData(ICC_Profile.icSigHead);
return header[ICC_Profile.icHdrRenderingIntent] != 0 || header[ICC_Profile.icHdrRenderingIntent + 1] != 0
|| header[ICC_Profile.icHdrRenderingIntent + 2] != 0 || header[ICC_Profile.icHdrRenderingIntent + 3] > 3;
@Deprecated
public static boolean isCS_GRAY(final ICC_Profile profile) {
return ColorProfiles.isCS_GRAY(profile);
}
/**
* Tests whether an ICC color profile is valid.
* Invalid profiles are known to cause problems for {@link java.awt.image.ColorConvertOp}.
* <p>
* <em>
* Note that this method only tests if a color conversion using this profile is known to fail.
* There's no guarantee that the color conversion will succeed even if this method returns {@code false}.
* </em>
* </p>
*
* @param profile the ICC color profile. May not be {@code null}.
* @return {@code profile} if valid.
* @throws IllegalArgumentException if {@code profile} is {@code null}
* @throws java.awt.color.CMMException if {@code profile} is invalid.
* @deprecated Use {@link ColorProfiles#validateProfile(ICC_Profile)} instead.
*/
@Deprecated
public static ICC_Profile validateProfile(final ICC_Profile profile) {
// Fix profile before validation
profileCleaner.fixProfile(profile);
validateColorSpace(new ICC_ColorSpace(profile));
return profile;
return ColorProfiles.validateProfile(profile);
}
/**
@@ -382,50 +297,6 @@ public final class ColorSpaces {
}
}
@SuppressWarnings("SameParameterValue")
private static ICC_Profile readProfileFromClasspathResource(final String profilePath) {
InputStream stream = ColorSpaces.class.getResourceAsStream(profilePath);
if (stream != null) {
if (DEBUG) {
System.out.println("Loading profile from classpath resource: " + profilePath);
}
try {
return ICC_Profile.getInstance(stream);
}
catch (IOException ignore) {
if (DEBUG) {
ignore.printStackTrace();
}
}
finally {
FileUtil.close(stream);
}
}
return null;
}
private static ICC_Profile readProfileFromPath(final String profilePath) {
if (profilePath != null) {
if (DEBUG) {
System.out.println("Loading profile from: " + profilePath);
}
try {
return ICC_Profile.getInstance(profilePath);
}
catch (SecurityException | IOException ignore) {
if (DEBUG) {
ignore.printStackTrace();
}
}
}
return null;
}
private static final class Key {
private final byte[] data;
@@ -448,78 +319,4 @@ public final class ColorSpaces {
return getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
}
}
// Cache header profile data to avoid excessive array creation/copying in static inner class for on-demand lazy init
private static class sRGB {
private static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_sRGB));
}
private static class CIEXYZ {
private static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ));
}
private static class PYCC {
private static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_PYCC));
}
private static class GRAY {
private static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_GRAY));
}
private static class LINEAR_RGB {
private static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB));
}
private static class Profiles {
// TODO: Honour java.iccprofile.path property?
private static final Properties PROFILES = loadProfiles();
private static Properties loadProfiles() {
Properties systemDefaults;
try {
systemDefaults = SystemUtil.loadProperties(
ColorSpaces.class,
"com/twelvemonkeys/imageio/color/icc_profiles_" + Platform.os().id()
);
}
catch (SecurityException | IOException ignore) {
System.err.printf(
"Warning: Could not load system default ICC profile locations from %s, will use bundled fallback profiles.\n",
ignore.getMessage()
);
if (DEBUG) {
ignore.printStackTrace();
}
systemDefaults = null;
}
// Create map with defaults and add user overrides if any
Properties profiles = new Properties(systemDefaults);
try {
Properties userOverrides = SystemUtil.loadProperties(
ColorSpaces.class,
"com/twelvemonkeys/imageio/color/icc_profiles"
);
profiles.putAll(userOverrides);
}
catch (SecurityException | IOException ignore) {
// Most likely, this file won't be there
}
if (DEBUG) {
System.out.println("User ICC profiles: " + profiles);
System.out.println("System ICC profiles : " + systemDefaults);
}
return profiles;
}
static String getPath(final String profileName) {
return PROFILES.getProperty(profileName);
}
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.color;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.File;
import java.util.Locale;
import static com.twelvemonkeys.imageio.util.IIOUtil.deregisterProvider;
/**
* This class exists to force early invocation of {@code ProfileDeferralMgr.activateProfiles()},
* in an attempt to avoid JDK-6986863 and related bugs in Java < 17.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @see <a href="https://bugs.openjdk.java.net/browse/JDK-6986863">JDK-6986863</a>
*/
final class ProfileDeferralActivator {
static {
activateProfilesInternal();
}
private static void activateProfilesInternal() {
try {
// Force invocation of ProfileDeferralMgr.activateProfiles() to avoid JDK-6986863 and friends.
// Relies on static initializer in ColorConvertOp to actually invoke ProfileDeferralMgr.activateProfiles()
Class.forName("java.awt.image.ColorConvertOp");
}
catch (Throwable disasters) {
System.err.println("ProfileDeferralMgr.activateProfiles() failed. ICC Color Profiles may not work properly, see stack trace below.");
System.err.println("For more information, see https://bugs.openjdk.java.net/browse/JDK-6986863");
System.err.println("Please upgrade to Java 17 or later where this bug is fixed, or ask your JRE provider to backport the fix.");
System.err.println();
System.err.println("If you can't update to Java 17, a possible workaround is to add");
System.err.println("\tClass.forName(\"java.awt.image.ColorConvertOp\");");
System.err.println("*early* in your application startup code, to force profile activation before profiles are accessed.");
System.err.println();
disasters.printStackTrace();
}
}
static void activateProfiles() {
// This method exists for other classes in the package to
// ensure this class' static initializer is run.
}
/**
* This is not a service provider, but exploits the SPI mechanism as a hook to force early profile activation.
*/
public static final class Spi extends ImageInputStreamSpi {
@Override public void onRegistration(ServiceRegistry registry, Class<?> category) {
activateProfiles();
deregisterProvider(registry, this, category);
}
@Override public String getDescription(Locale locale) {
return getClass().getName();
}
@Override public ImageInputStream createInputStreamInstance(Object input, boolean useCache, File cacheDir) {
throw new UnsupportedOperationException();
}
}
}
@@ -45,36 +45,82 @@ public final class YCbCrConverter {
private final static int CENTERJSAMPLE = 128;
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
private final static class JPEG {
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
/**
* Initializes tables for YCC->RGB color space conversion.
*/
private static void buildYCCtoRGBtable() {
if (ColorSpaces.DEBUG) {
System.err.println("Building YCC conversion table");
/**
* Initializes tables for YCC->RGB color space conversion.
*/
private static void buildYCCtoRGBtable() {
if (ColorSpaces.DEBUG) {
System.err.println("Building JPEG YCbCr conversion table");
}
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
// Cr=>R value is nearest int to 1.40200 * x
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cb=>B value is nearest int to 1.77200 * x
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cr=>G value is scaled-up -0.71414 * x
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
// Cb=>G value is scaled-up -0.34414 * x
// We also add in ONE_HALF so that need not do it in inner loop
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
}
}
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
// Cr=>R value is nearest int to 1.40200 * x
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cb=>B value is nearest int to 1.77200 * x
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cr=>G value is scaled-up -0.71414 * x
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
// Cb=>G value is scaled-up -0.34414 * x
// We also add in ONE_HALF so that need not do it in inner loop
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
static {
buildYCCtoRGBtable();
}
}
static {
buildYCCtoRGBtable();
private final static class ITU_R_601 {
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Y_LUT = new int[MAXJSAMPLE + 1];
/**
* Initializes tables for YCC->RGB color space conversion.
*/
private static void buildYCCtoRGBtable() {
if (ColorSpaces.DEBUG) {
System.err.println("Building ITU-R REC.601 YCbCr conversion table");
}
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
// Y'CbCr to RGB conversion, using values from BT.601 specification:
// R = 1.16438 * (Y'-16) + 1.59603 * (Cr-128)
// G = 1.16438 * (Y'-16) - 0.39176 * (Cb-128) - 0.81297 * (Cr-128)
// B = 1.16438 * (Y'-16) + 2.01723 * (Cb-128)
// Cr=>R value is nearest int to 1.59603 * x
Cr_R_LUT[i] = ((int) (1.59603 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cb=>B value is nearest int to 2.01723 * x
Cb_B_LUT[i] = ((int) (2.01723 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cr=>G value is scaled-up -0.81297 * x
Cr_G_LUT[i] = -(int) (0.81297 * (1 << SCALEBITS) + 0.5) * x;
// Cb=>G value is scaled-up -0.39176 * x
// We also add in ONE_HALF so that need not do it in inner loop
Cb_G_LUT[i] = -(int) ((0.39176) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
// Y`=>RGB
Y_LUT[i] = ((int) (1.16438 * (1 << SCALEBITS) + 0.5) * (i - 16) + ONE_HALF) >> SCALEBITS;
}
}
static {
buildYCCtoRGBtable();
}
}
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, double[] referenceBW, final int offset) {
@@ -108,17 +154,27 @@ public final class YCbCrConverter {
rgb[offset + 1] = clamp(green);
}
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
int y = yCbCr[offset] & 0xff;
int cr = yCbCr[offset + 2] & 0xff;
public static void convertJPEGYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
int y = yCbCr[offset ] & 0xff;
int cb = yCbCr[offset + 1] & 0xff;
int cr = yCbCr[offset + 2] & 0xff;
rgb[offset] = clamp(y + Cr_R_LUT[cr]);
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
rgb[offset ] = clamp(y + JPEG.Cr_R_LUT[cr]);
rgb[offset + 1] = clamp(y + (JPEG.Cb_G_LUT[cb] + JPEG.Cr_G_LUT[cr] >> SCALEBITS));
rgb[offset + 2] = clamp(y + JPEG.Cb_B_LUT[cb]);
}
private static byte clamp(int val) {
public static void convertRec601YCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
int y = yCbCr[offset ] & 0xff;
int cb = yCbCr[offset + 1] & 0xff;
int cr = yCbCr[offset + 2] & 0xff;
rgb[offset ] = clamp(ITU_R_601.Y_LUT[y] + ITU_R_601.Cr_R_LUT[cr]);
rgb[offset + 1] = clamp(ITU_R_601.Y_LUT[y] + (ITU_R_601.Cr_G_LUT[cr] + ITU_R_601.Cb_G_LUT[cb] >> SCALEBITS));
rgb[offset + 2] = clamp(ITU_R_601.Y_LUT[y] + ITU_R_601.Cb_B_LUT[cb]);
}
private static byte clamp(final int val) {
return (byte) Math.max(0, Math.min(255, val));
}
}
@@ -158,19 +158,18 @@ public final class IIOUtil {
}
/**
* THIS METHOD WILL ME MOVED/RENAMED, DO NOT USE.
* THIS METHOD WILL BE MOVED/RENAMED, DO NOT USE.
*
* @param registry the registry to unregister from.
* @param provider the provider to unregister.
* @param category the category to unregister from.
*/
public static <T> void deregisterProvider(final ServiceRegistry registry, final IIOServiceProvider provider, final Class<T> category) {
// http://www.ibm.com/developerworks/java/library/j-jtp04298.html
registry.deregisterServiceProvider(category.cast(provider), category);
}
/**
* THIS METHOD WILL ME MOVED/RENAMED, DO NOT USE.
* THIS METHOD WILL BE MOVED/RENAMED, DO NOT USE.
*
* @param registry the registry to lookup from.
* @param providerClassName name of the provider class.
@@ -1,2 +1,4 @@
com.twelvemonkeys.imageio.stream.BufferedFileImageInputStreamSpi
com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi
# Use SPI loading as a hook for early profile activation
com.twelvemonkeys.imageio.color.ProfileDeferralActivator$Spi
@@ -0,0 +1,247 @@
package com.twelvemonkeys.imageio.color;
import org.junit.Test;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import static org.junit.Assert.*;
public class ColorProfilesTest {
@Test
public void testCreateColorSpaceFromBrokenProfileIsFixedCS_sRGB() {
ICC_Profile internal = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
ICC_Profile profile = createBrokenProfile(internal);
assertNotSame(internal, profile); // Sanity check
assertTrue(ColorProfiles.isOffendingColorProfile(profile));
ICC_ColorSpace created = ColorSpaces.createColorSpace(profile);
assertSame(ColorSpace.getInstance(ColorSpace.CS_sRGB), created);
assertTrue(created.isCS_sRGB());
}
private ICC_Profile createBrokenProfile(ICC_Profile internal) {
byte[] data = internal.getData();
data[ICC_Profile.icHdrRenderingIntent] = 1; // Intent: 1 == Relative Colormetric Little Endian
data[ICC_Profile.icHdrRenderingIntent + 1] = 0;
data[ICC_Profile.icHdrRenderingIntent + 2] = 0;
data[ICC_Profile.icHdrRenderingIntent + 3] = 0;
return ICC_Profile.getInstance(data);
}
@Test
public void testIsOffendingColorProfile() {
ICC_Profile broken = createBrokenProfile(ICC_Profile.getInstance(ColorSpace.CS_GRAY));
assertTrue(ColorProfiles.isOffendingColorProfile(broken));
}
@Test
public void testIsCS_sRGBTrue() {
assertTrue(ColorProfiles.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_sRGB)));
}
@Test
public void testIsCS_sRGBFalse() {
assertFalse(ColorProfiles.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB)));
assertFalse(ColorProfiles.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ)));
assertFalse(ColorProfiles.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_GRAY)));
assertFalse(ColorProfiles.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_PYCC)));
}
@Test(expected = IllegalArgumentException.class)
public void testIsCS_sRGBNull() {
ColorProfiles.isCS_sRGB(null);
}
@Test
public void testIsCS_GRAYTrue() {
assertTrue(ColorProfiles.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_GRAY)));
}
@Test
public void testIsCS_GRAYFalse() {
assertFalse(ColorProfiles.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_sRGB)));
assertFalse(ColorProfiles.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB)));
assertFalse(ColorProfiles.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ)));
assertFalse(ColorProfiles.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_PYCC)));
}
@Test(expected = IllegalArgumentException.class)
public void testIsCS_GRAYNull() {
ColorProfiles.isCS_GRAY(null);
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileNull() {
ColorProfiles.createProfile(null);
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileNull() throws IOException {
ColorProfiles.readProfile(null);
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileRawNull() {
ColorProfiles.createProfileRaw(null);
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileRawNull() throws IOException {
ColorProfiles.readProfileRaw(null);
}
@Test
public void testCreateProfileRaw() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ICC_Profile profileRaw = ColorProfiles.createProfileRaw(data);
assertArrayEquals(data, profileRaw.getData());
}
@Test
public void testReadProfileRaw() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ICC_Profile profileRaw = ColorProfiles.readProfileRaw(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
assertArrayEquals(data, profileRaw.getData());
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileRawBadData() {
ColorProfiles.createProfileRaw(new byte[5]);
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileRawBadData() throws IOException {
// NOTE: The array here is larger, as there's a bug in OpenJDK 15 & 16, that throws
// ArrayIndexOutOfBoundsException if the stream is shorter than the profile signature...
ColorProfiles.readProfileRaw(new ByteArrayInputStream(new byte[40]));
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileBadData() {
ColorProfiles.createProfile(new byte[5]);
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileBadData() throws IOException {
ColorProfiles.readProfile(new ByteArrayInputStream(new byte[5]));
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileRawTruncated() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.createProfileRaw(Arrays.copyOf(data, 200));
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileRawTruncated() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.readProfileRaw(new ByteArrayInputStream(data, 0, 200));
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileTruncated() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.createProfile(Arrays.copyOf(data, 200));
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileTruncated() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.readProfile(new ByteArrayInputStream(data, 0, 200));
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileRawTruncatedHeader() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.createProfileRaw(Arrays.copyOf(data, 125));
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileRawTruncatedHeader() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.readProfileRaw(new ByteArrayInputStream(data, 0, 125));
}
@Test(expected = IllegalArgumentException.class)
public void testCreateProfileTruncatedHeader() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.createProfile(Arrays.copyOf(data, 125));
}
@Test(expected = IllegalArgumentException.class)
public void testReadProfileTruncatedHeader() throws IOException {
byte[] data = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")).getData();
ColorProfiles.readProfile(new ByteArrayInputStream(data, 0, 125));
}
@Test
public void testCreateProfileBytesSame() throws IOException {
ICC_Profile profile = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
ICC_Profile profile1 = ColorProfiles.createProfile(profile.getData());
ICC_Profile profile2 = ColorProfiles.createProfile(profile.getData());
assertEquals(profile1, profile2);
assertSame(profile1, profile2);
}
@Test
public void testReadProfileInputStreamSame() throws IOException {
ICC_Profile profile1 = ColorProfiles.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
ICC_Profile profile2 = ColorProfiles.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
assertEquals(profile1, profile2);
assertSame(profile1, profile2);
}
@Test
public void testReadProfileDifferent() throws IOException {
// These profiles are extracted from various JPEGs, and have the exact same profile header (but are different profiles)...
ICC_Profile profile1 = ColorProfiles.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
ICC_Profile profile2 = ColorProfiles.readProfile(getClass().getResourceAsStream("/profiles/color_match_rgb.icc"));
assertNotSame(profile1, profile2);
}
@Test
public void testCreateProfileBytesSameAsCached() throws IOException {
ICC_Profile profile = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
ICC_ColorSpace cs1 = ColorSpaces.createColorSpace(profile);
ICC_Profile profile2 = ColorProfiles.createProfile(profile.getData());
assertEquals(cs1.getProfile(), profile2);
assertSame(cs1.getProfile(), profile2);
}
@Test
public void testReadProfileInputStreamSameAsCached() throws IOException {
ICC_ColorSpace cs1 = ColorSpaces.createColorSpace(ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")));
ICC_Profile profile2 = ColorProfiles.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
assertEquals(cs1.getProfile(), profile2);
assertSame(cs1.getProfile(), profile2);
}
@Test
public void testCreateProfileBytesSameAsInternal() {
ICC_Profile profile1 = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
ICC_Profile profile2 = ColorProfiles.createProfile(ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData());
assertEquals(profile1, profile2);
assertSame(profile1, profile2);
}
@Test
public void testReadProfileInputStreamSameAsInternal() throws IOException {
ICC_Profile profile1 = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
ICC_Profile profile2 = ColorProfiles.readProfile(new ByteArrayInputStream(ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData()));
assertEquals(profile1, profile2);
assertSame(profile1, profile2);
}
}
@@ -90,34 +90,6 @@ public class ColorSpacesTest {
assertTrue(created.isCS_sRGB());
}
@Test
public void testCreateColorSpaceFromBrokenProfileIsFixedCS_sRGB() {
ICC_Profile internal = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
ICC_Profile profile = createBrokenProfile(internal);
assertNotSame(internal, profile); // Sanity check
assertTrue(ColorSpaces.isOffendingColorProfile(profile));
ICC_ColorSpace created = ColorSpaces.createColorSpace(profile);
assertSame(ColorSpace.getInstance(ColorSpace.CS_sRGB), created);
assertTrue(created.isCS_sRGB());
}
private ICC_Profile createBrokenProfile(ICC_Profile internal) {
byte[] data = internal.getData();
data[ICC_Profile.icHdrRenderingIntent] = 1; // Intent: 1 == Relative Colormetric Little Endian
data[ICC_Profile.icHdrRenderingIntent + 1] = 0;
data[ICC_Profile.icHdrRenderingIntent + 2] = 0;
data[ICC_Profile.icHdrRenderingIntent + 3] = 0;
return ICC_Profile.getInstance(data);
}
@Test
public void testIsOffendingColorProfile() {
ICC_Profile broken = createBrokenProfile(ICC_Profile.getInstance(ColorSpace.CS_GRAY));
assertTrue(ColorSpaces.isOffendingColorProfile(broken));
}
@Test
public void testCreateColorSpaceFromKnownProfileReturnsInternalCS_GRAY() {
ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_GRAY);
@@ -166,11 +138,13 @@ public class ColorSpacesTest {
assertEquals(ColorSpace.TYPE_CMYK, ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK).getType());
}
@SuppressWarnings("deprecation")
@Test
public void testIsCS_sRGBTrue() {
assertTrue(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_sRGB)));
}
@SuppressWarnings("deprecation")
@Test
public void testIsCS_sRGBFalse() {
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB)));
@@ -179,14 +153,36 @@ public class ColorSpacesTest {
assertFalse(ColorSpaces.isCS_sRGB(ICC_Profile.getInstance(ColorSpace.CS_PYCC)));
}
@SuppressWarnings("deprecation")
@Test(expected = IllegalArgumentException.class)
public void testIsCS_sRGBNull() {
ColorSpaces.isCS_sRGB(null);
}
@SuppressWarnings("deprecation")
@Test
public void testIsCS_GRAYTrue() {
assertTrue(ColorSpaces.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_GRAY)));
}
@SuppressWarnings("deprecation")
@Test
public void testIsCS_GRAYFalse() {
assertFalse(ColorSpaces.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_sRGB)));
assertFalse(ColorSpaces.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB)));
assertFalse(ColorSpaces.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ)));
assertFalse(ColorSpaces.isCS_GRAY(ICC_Profile.getInstance(ColorSpace.CS_PYCC)));
}
@SuppressWarnings("deprecation")
@Test(expected = IllegalArgumentException.class)
public void testIsCS_GRAYNull() {
ColorSpaces.isCS_GRAY(null);
}
@Test
public void testEqualHeadersDifferentProfile() throws IOException {
// These profiles are extracted from various JPEGs, and have the exact same profile header...
// These profiles are extracted from various JPEGs, and have the exact same profile header (but are different)...
ICC_Profile profile1 = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
ICC_Profile profile2 = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/color_match_rgb.icc"));
@@ -39,8 +39,6 @@ import java.io.IOException;
import java.util.Arrays;
import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
public class KCMSSanitizerStrategyTest {
@@ -0,0 +1,28 @@
package com.twelvemonkeys.imageio.color;
import org.junit.Test;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ServiceRegistry;
import static org.mockito.Mockito.*;
public class ProfileDeferralActivatorTest {
@Test
public void testActivateProfiles() {
// Should just run with no exceptions...
ProfileDeferralActivator.activateProfiles();
}
@Test
public void testSpiRegistration() {
ProfileDeferralActivator.Spi spi = new ProfileDeferralActivator.Spi();
ServiceRegistry registry = mock(ServiceRegistry.class);
Class<ImageInputStreamSpi> category = ImageInputStreamSpi.class;
spi.onRegistration(registry, category);
verify(registry, only()).deregisterServiceProvider(spi, category);
}
}
@@ -45,9 +45,8 @@ import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
@@ -57,6 +56,7 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import static java.lang.Math.min;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@@ -1151,7 +1151,7 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
// At least imageStarted and imageComplete, plus any number of imageProgress
InOrder ordered = inOrder(listener);
ordered.verify(listener).imageStarted(reader, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyFloat());
ordered.verify(listener).imageComplete(reader);
reader.dispose();
}
@@ -1184,9 +1184,9 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
ordered.verify(listenerToo).imageStarted(reader, 0);
ordered.verify(listenerThree).imageStarted(reader, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listenerThree, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyFloat());
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(reader), anyFloat());
ordered.verify(listenerThree, atLeastOnce()).imageProgress(eq(reader), anyFloat());
ordered.verify(listener).imageComplete(reader);
ordered.verify(listenerToo).imageComplete(reader);
@@ -1226,7 +1226,7 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
}
// Should not have called any methods...
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
reader.dispose();
}
@@ -1253,11 +1253,11 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
}
// Should not have called any methods on listener1...
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
InOrder ordered = inOrder(listenerToo);
ordered.verify(listenerToo).imageStarted(reader, 0);
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(reader), anyInt());
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(reader), anyFloat());
ordered.verify(listenerToo).imageComplete(reader);
reader.dispose();
}
@@ -1281,7 +1281,7 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
}
// Should not have called any methods...
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
reader.dispose();
}
@@ -1307,8 +1307,8 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
}
// Should not have called any methods...
verifyZeroInteractions(listener);
verifyZeroInteractions(listenerToo);
verifyNoInteractions(listener);
verifyNoInteractions(listenerToo);
reader.dispose();
}
@@ -1333,7 +1333,7 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
}
};
doAnswer(abort).when(abortingListener).imageStarted(any(ImageReader.class), anyInt());
doAnswer(abort).when(abortingListener).imageProgress(any(ImageReader.class), anyInt());
doAnswer(abort).when(abortingListener).imageProgress(any(ImageReader.class), anyFloat());
reader.addIIOReadProgressListener(abortingListener);
@@ -1738,6 +1738,43 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
reader.dispose();
}
protected List<TestData> getTestDataForAffineTransformOpCompatibility() {
// Allow subclasses to filter out test data that can't be converted to a compatible image without data loss
return getTestData();
}
@Test
public void testAffineTransformOpCompatibility() throws IOException {
// Test that the output of normal images are compatible with AffineTransformOp. Is unlikely to work on all test data
ImageReader reader = createReader();
for (TestData testData : getTestDataForAffineTransformOpCompatibility()) {
try (ImageInputStream input = testData.getInputStream()) {
reader.setInput(input);
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(min(reader.getWidth(0), 64), min(reader.getHeight(0), 64)));
BufferedImage originalImage = reader.read(0, param);
AffineTransform transform = AffineTransform.getTranslateInstance(10, 10);
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
try {
BufferedImage resultImage = op.filter(originalImage, null); // The exception happens here
assertNotNull(resultImage);
}
catch (ImagingOpException e) {
fail(e.getMessage() + ".\n\t"
+ originalImage + "\n\t"
+ testData);
}
}
}
reader.dispose();
}
@Ignore("TODO: Implement")
@Test
public void testSetDestinationBands() {
@@ -52,7 +52,6 @@ import java.net.URL;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.*;
/**
@@ -79,7 +78,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
protected abstract ImageWriterSpi createProvider();
protected final T createWriter() throws IOException {
return writerClass.cast(provider.createWriterInstance(null));
return writerClass.cast(provider.createWriterInstance());
}
protected abstract List<? extends RenderedImage> getTestData();
@@ -104,7 +103,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
return image;
}
protected final RenderedImage getTestData(final int index) {
return getTestData().get(index);
}
@@ -219,7 +218,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
// At least imageStarted and imageComplete, plus any number of imageProgress
InOrder ordered = inOrder(listener);
ordered.verify(listener).imageStarted(writer, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(writer), anyInt());
ordered.verify(listener, atLeastOnce()).imageProgress(eq(writer), anyFloat());
ordered.verify(listener).imageComplete(writer);
}
@@ -251,9 +250,9 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
ordered.verify(listenerToo).imageStarted(writer, 0);
ordered.verify(listenerThree).imageStarted(writer, 0);
ordered.verify(listener, atLeastOnce()).imageProgress(eq(writer), anyInt());
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(writer), anyInt());
ordered.verify(listenerThree, atLeastOnce()).imageProgress(eq(writer), anyInt());
ordered.verify(listener, atLeastOnce()).imageProgress(eq(writer), anyFloat());
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(writer), anyFloat());
ordered.verify(listenerThree, atLeastOnce()).imageProgress(eq(writer), anyFloat());
ordered.verify(listener).imageComplete(writer);
ordered.verify(listenerToo).imageComplete(writer);
@@ -290,7 +289,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
}
// Should not have called any methods...
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
}
@Test
@@ -315,12 +314,12 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
}
// Should not have called any methods...
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
// At least imageStarted and imageComplete, plus any number of imageProgress
InOrder ordered = inOrder(listenerToo);
ordered.verify(listenerToo).imageStarted(writer, 0);
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(writer), anyInt());
ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(writer), anyFloat());
ordered.verify(listenerToo).imageComplete(writer);
}
@@ -345,7 +344,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
}
// Should not have called any methods...
verifyZeroInteractions(listener);
verifyNoInteractions(listener);
}
@Test
@@ -371,7 +370,7 @@ public abstract class ImageWriterAbstractTest<T extends ImageWriter> {
}
// Should not have called any methods...
verifyZeroInteractions(listener);
verifyZeroInteractions(listenerToo);
verifyNoInteractions(listener);
verifyNoInteractions(listenerToo);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-hdr</artifactId>
<name>TwelveMonkeys :: ImageIO :: HDR plugin</name>
@@ -38,12 +38,14 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static java.util.Collections.emptyList;
/**
* TGAImageReaderTest
* HDRImageReaderTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TGAImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$
* @version $Id: HDRImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$
*/
public class HDRImageReaderTest extends ImageReaderAbstractTest<HDRImageReader> {
@Override
@@ -58,6 +60,12 @@ public class HDRImageReaderTest extends ImageReaderAbstractTest<HDRImageReader>
);
}
@Override
protected List<TestData> getTestDataForAffineTransformOpCompatibility() {
// HDR images uses floating point buffers...
return emptyList();
}
@Override
protected List<String> getFormatNames() {
return Arrays.asList("HDR", "hdr", "RGBE", "rgbe");
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-icns</artifactId>
<name>TwelveMonkeys :: ImageIO :: ICNS plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-iff</artifactId>
<name>TwelveMonkeys :: ImageIO :: IFF plugin</name>
+3 -2
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-jpeg-jai-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG/JAI TIFF Interop</name>
@@ -13,7 +13,7 @@
</description>
<properties>
<project.jpms.module.name>com.twelvemonkeys.imageio.jaiinterop</project.jpms.module.name>
<project.jpms.module.name>com.twelvemonkeys.imageio.jpeg.jaiinterop</project.jpms.module.name>
</properties>
<build>
@@ -33,6 +33,7 @@
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-core</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-jpeg-jep262-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG/JEP-262 Interop</name>
@@ -13,7 +13,7 @@
</description>
<properties>
<project.jpms.module.name>com.twelvemonkeys.imageio.jep262interop</project.jpms.module.name>
<project.jpms.module.name>com.twelvemonkeys.imageio.jpeg.jep262interop</project.jpms.module.name>
</properties>
<build>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-jpeg</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG plugin</name>
@@ -119,7 +119,7 @@ final class EXIFThumbnail {
case 6:
// YCbCr
for (int i = 0; i < thumbLength; i += 3) {
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
YCbCrConverter.convertJPEGYCbCr2RGB(thumbData, thumbData, i);
}
break;
default:
@@ -31,6 +31,7 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
@@ -123,7 +124,7 @@ public final class JPEGImageReader extends ImageReaderBase {
private int currentStreamIndex = 0;
private final List<Long> streamOffsets = new ArrayList<>();
protected JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
super(provider);
this.delegate = Validate.notNull(delegate);
@@ -335,7 +336,7 @@ public final class JPEGImageReader extends ImageReaderBase {
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
// - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream.
else if (bogusAdobeDCT
|| profile != null && !ColorSpaces.isCS_sRGB(profile)
|| profile != null && !ColorProfiles.isCS_sRGB(profile)
|| (long) sof.lines * sof.samplesPerLine > Integer.MAX_VALUE
|| delegateCSTypeMismatch(jfif, adobeDCT, sof, sourceCSType)) {
if (DEBUG) {
@@ -614,7 +615,7 @@ public final class JPEGImageReader extends ImageReaderBase {
}
}
private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) {
private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) throws IOException {
// NOTE: This is probably not the right way to do it... :-P
// TODO: Consider moving method to ColorSpaces class or new class in imageio.color package
@@ -630,7 +631,7 @@ public final class JPEGImageReader extends ImageReaderBase {
intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first
return ICC_Profile.getInstance(profileData);
return ColorProfiles.createProfile(profileData);
}
}
@@ -950,11 +951,7 @@ public final class JPEGImageReader extends ImageReaderBase {
return null;
}
int segmentDataStart = segment.identifier.length() + 3; // ICC_PROFILE + null + chunk number + count
int iccChunkDataSize = segment.data.length - segmentDataStart;
int iccSize = segment.data.length < segmentDataStart + 4 ? 0 : intFromBigEndian(segment.data, segmentDataStart);
return readICCProfileSafe(stream, allowBadIndexes, iccSize, iccChunkDataSize);
return readICCProfileSafe(stream, allowBadIndexes);
}
else if (!segments.isEmpty()) {
// NOTE: This is probably over-complicated, as I've never encountered ICC_PROFILE chunks out of order...
@@ -989,9 +986,6 @@ public final class JPEGImageReader extends ImageReaderBase {
InputStream[] streams = new InputStream[count];
streams[badICC ? 0 : chunkNumber - 1] = stream;
int iccChunkDataSize = 0;
int iccSize = 0;
for (int i = 1; i < count; i++) {
Application segment = segments.get(i);
stream = new DataInputStream(segment.data());
@@ -1004,33 +998,20 @@ public final class JPEGImageReader extends ImageReaderBase {
int index = badICC ? i : chunkNumber - 1;
streams[index] = stream;
int segmentDataStart = segment.identifier.length() + 3; // ICC_PROFILE + null + chunk number + count
iccChunkDataSize += segment.data.length - segmentDataStart;
if (index == 0) {
iccSize = intFromBigEndian(segment.data, segmentDataStart);
}
}
return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))), allowBadIndexes, iccSize, iccChunkDataSize);
return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))), allowBadIndexes);
}
return null;
}
private ICC_Profile readICCProfileSafe(final InputStream stream, final boolean allowBadProfile, final int iccSize, final int iccChunkDataSize) throws IOException {
if (iccSize < 0 || iccSize > iccChunkDataSize) {
processWarningOccurred(String.format("Truncated 'ICC_PROFILE' chunk(s), size: %d. Ignoring ICC profile.", iccSize));
return null;
}
private ICC_Profile readICCProfileSafe(final InputStream stream, final boolean allowBadProfile) {
try {
ICC_Profile profile = ICC_Profile.getInstance(stream);
// NOTE: Need to ensure we have a display profile *before* validating, for the caching to work
return allowBadProfile ? profile : ColorSpaces.validateProfile(ensureDisplayProfile(profile));
return allowBadProfile ? ColorProfiles.readProfileRaw(stream) : ensureDisplayProfile(ColorProfiles.readProfile(stream));
}
catch (RuntimeException e) {
catch (IOException | RuntimeException e) {
// NOTE: Throws either IllegalArgumentException or CMMException, depending on platform.
// Usual reason: Broken tools store truncated ICC profiles in a single ICC_PROFILE chunk...
processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk(s): %s. Ignoring ICC profile.", e.getMessage()));
@@ -1169,7 +1150,7 @@ public final class JPEGImageReader extends ImageReaderBase {
processThumbnailStarted(imageIndex, thumbnailIndex);
processThumbnailProgress(0f);
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();;
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();
processThumbnailProgress(100f);
processThumbnailComplete();
@@ -1211,7 +1192,7 @@ public final class JPEGImageReader extends ImageReaderBase {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
YCbCrConverter.convertYCbCr2RGB(data, data, (x + y * width) * numComponents);
YCbCrConverter.convertJPEGYCbCr2RGB(data, data, (x + y * width) * numComponents);
}
}
}
@@ -1225,7 +1206,7 @@ public final class JPEGImageReader extends ImageReaderBase {
for (int x = 0; x < width; x++) {
int offset = (x + y * width) * 4;
// YCC -> CMY
YCbCrConverter.convertYCbCr2RGB(data, data, offset);
YCbCrConverter.convertJPEGYCbCr2RGB(data, data, offset);
// Inverse K
data[offset + 3] = (byte) (0xff - data[offset + 3] & 0xff);
}
@@ -36,7 +36,6 @@ import com.twelvemonkeys.lang.StringUtil;
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;
@@ -66,12 +65,11 @@ import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNoException;
import static org.junit.Assume.assumeNotNull;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
/**
@@ -1456,7 +1454,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
assertTrue(markerSequences.getLength() == 1 || markerSequences.getLength() == 2); // In case of JPEG encoded thumbnail, there will be 2
IIOMetadataNode markerSequence = (IIOMetadataNode) markerSequences.item(markerSequences.getLength() - 1); // The last will be the "main" image
assertNotNull(markerSequence);
assertThat(markerSequence.getChildNodes().getLength(), new GreaterThan<>(0));
assertThat(markerSequence.getChildNodes().getLength(), greaterThan(0));
NodeList unknowns = markerSequence.getElementsByTagName("unknown");
for (int j = 0; j < unknowns.getLength(); j++) {
@@ -36,7 +36,6 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
import org.junit.Test;
import org.mockito.internal.matchers.LessOrEqual;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
@@ -48,6 +47,7 @@ import java.net.URL;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -124,7 +124,7 @@ public class JPEGSegmentImageInputStreamTest {
length++;
}
assertThat(length, new LessOrEqual<>(10203L)); // In no case should length increase
assertThat(length, lessThanOrEqualTo(10203L)); // In no case should length increase
assertEquals(9607L, length); // May change, if more chunks are passed to reader...
}
+1 -1
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>imageio-metadata</artifactId>
@@ -46,6 +46,7 @@ import java.io.IOException;
*
* @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.TIFFReader instead.
*/
@Deprecated
public final class EXIFReader extends MetadataReader {
private final TIFFReader delegate = new TIFFReader();
@@ -48,6 +48,7 @@ import java.util.Collection;
*
* @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter instead.
*/
@Deprecated
public final class EXIFWriter extends MetadataWriter {
private final TIFFWriter delegate = new TIFFWriter();
@@ -41,6 +41,7 @@ package com.twelvemonkeys.imageio.metadata.exif;
*
* @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.Rational instead.
*/
@Deprecated
public final class Rational extends Number implements Comparable<Rational> {
private final com.twelvemonkeys.imageio.metadata.tiff.Rational delegate;
@@ -39,5 +39,6 @@ package com.twelvemonkeys.imageio.metadata.exif;
*
* @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.TIFF instead.
*/
@Deprecated
public interface TIFF extends com.twelvemonkeys.imageio.metadata.tiff.TIFF {
}
@@ -30,6 +30,7 @@
package com.twelvemonkeys.imageio.metadata.jpeg;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.psd.PSD;
@@ -42,7 +43,6 @@ import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
@@ -353,8 +353,8 @@ public final class JPEGSegmentUtil {
Directory psd = new PSDReader().read(stream);
Entry iccEntry = psd.getEntryById(PSD.RES_ICC_PROFILE);
if (iccEntry != null) {
ICC_ColorSpace colorSpace = new ICC_ColorSpace(ICC_Profile.getInstance((byte[]) iccEntry.getValue()));
System.err.println("colorSpace: " + colorSpace);
ICC_Profile profile = ColorProfiles.createProfile((byte[]) iccEntry.getValue());
System.err.println("ICC Profile: " + profile);
}
System.err.println("PSD: " + psd);
System.err.println(TIFFReader.HexDump.dump(segment.data));
@@ -30,8 +30,15 @@
package com.twelvemonkeys.imageio.metadata.tiff;
import static com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry.getValueLength;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReader;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
@@ -39,15 +46,7 @@ import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReader;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.lang.Validate;
import static com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry.getValueLength;
/**
* TIFFReader
@@ -73,16 +72,17 @@ public final class TIFFReader extends MetadataReader {
}
};
map.put(TIFF.TAG_SUB_IFD, Collections.unmodifiableCollection(Collections.singleton(TIFF.TAG_SUB_IFD)));
map.put(TIFF.TAG_EXIF_IFD, Collections.unmodifiableCollection(Collections.singleton(TIFF.TAG_INTEROP_IFD)));
map.put(TIFF.TAG_SUB_IFD, Collections.singleton(TIFF.TAG_SUB_IFD));
map.put(TIFF.TAG_EXIF_IFD, Collections.singleton(TIFF.TAG_INTEROP_IFD));
return Collections.unmodifiableMap(map);
}
private final Set<Long> parsedIFDs = new TreeSet<>();
private long length;
private boolean longOffsets;
private int offsetSize;
private Set<Long> parsedIFDs = new TreeSet<>();
@Override
public Directory read(final ImageInputStream input) throws IOException {
@@ -40,7 +40,6 @@ import javax.imageio.IIOException;
import javax.imageio.stream.ImageOutputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
@@ -58,7 +57,24 @@ public final class TIFFWriter extends MetadataWriter {
private static final int WORD_LENGTH = 2;
private static final int LONGWORD_LENGTH = 4;
private static final int ENTRY_LENGTH = 12;
// TODO: We probably want to gloss over client code writing IFDs in BigTIFF (or vice versa) somehow... Silently convert IFD -> IFD8
private final boolean longOffsets;
private final int offsetSize;
private final long entryLength;
private final int directoryCountLength;
public TIFFWriter() {
this(LONGWORD_LENGTH);
}
public TIFFWriter(int offsetSize) {
this.offsetSize = Validate.isTrue(offsetSize == 4 || offsetSize == 8, offsetSize, "offsetSize must be 4 for TIFF or 8 for BigTIFF");
longOffsets = offsetSize == 8;
directoryCountLength = longOffsets ? 8 : WORD_LENGTH;
entryLength = 2 * WORD_LENGTH + 2 * offsetSize;
}
public boolean write(final Collection<? extends Entry> entries, final ImageOutputStream stream) throws IOException {
return write(new IFD(entries), stream);
@@ -87,7 +103,7 @@ public final class TIFFWriter extends MetadataWriter {
}
// Offset to next IFD (EOF)
stream.writeInt(0);
writeOffset(stream, 0);
return true;
}
@@ -96,7 +112,12 @@ public final class TIFFWriter extends MetadataWriter {
// Header
ByteOrder byteOrder = stream.getByteOrder();
stream.writeShort(byteOrder == ByteOrder.BIG_ENDIAN ? TIFF.BYTE_ORDER_MARK_BIG_ENDIAN : TIFF.BYTE_ORDER_MARK_LITTLE_ENDIAN);
stream.writeShort(42);
stream.writeShort(longOffsets ? TIFF.BIGTIFF_MAGIC : TIFF.TIFF_MAGIC);
if (longOffsets) {
stream.writeShort(offsetSize); // Always 8 in this case
stream.writeShort(0);
}
}
public long writeIFD(final Collection<Entry> entries, final ImageOutputStream stream) throws IOException {
@@ -118,37 +139,42 @@ public final class TIFFWriter extends MetadataWriter {
long dataSize = computeDataSize(ordered);
// Offset to this IFD
final long ifdOffset = stream.getStreamPosition() + dataSize + LONGWORD_LENGTH;
final long ifdOffset = stream.getStreamPosition() + dataSize + offsetSize;
if (!isSubIFD) {
stream.writeInt(assertIntegerOffset(ifdOffset));
dataOffset += LONGWORD_LENGTH;
writeOffset(stream, ifdOffset);
dataOffset += offsetSize;
// Seek to offset
stream.seek(ifdOffset);
}
else {
dataOffset += WORD_LENGTH + ordered.size() * ENTRY_LENGTH;
dataOffset += directoryCountLength + ordered.size() * entryLength;
}
// Write directory
stream.writeShort(ordered.size());
writeDirectoryCount(stream, ordered.size());
for (Entry entry : ordered) {
// Write tag id
// Write tag id, type & value count
stream.writeShort((Integer) entry.getIdentifier());
// Write tag type
stream.writeShort(getType(entry));
// Write value count
stream.writeInt(getCount(entry));
writeValueCount(stream, getCount(entry));
// Write value
if (entry.getValue() instanceof Directory) {
// TODO: This could possibly be a compound directory, in which case the count should be > 1
stream.writeInt(assertIntegerOffset(dataOffset));
long streamPosition = stream.getStreamPosition();
Object value = entry.getValue();
if (value instanceof Directory) {
if (value instanceof CompoundDirectory) {
// Can't have both nested and linked IFDs
throw new AssertionError("SubIFD cannot contain linked IFDs");
}
// We can't write offset here, we need to write value, as both LONG/IFD and LONG8/IFD8 is allowed
// TODO: Or possibly gloss over, by always writing IFD8 for BigTIFF?
long streamPosition = stream.getStreamPosition() + offsetSize;
writeValueInline(dataOffset, getType(entry), stream);
stream.seek(dataOffset);
Directory subIFD = (Directory) entry.getValue();
Directory subIFD = (Directory) value;
writeIFD(subIFD, stream, true);
dataOffset += computeDataSize(subIFD);
stream.seek(streamPosition);
@@ -161,8 +187,26 @@ public final class TIFFWriter extends MetadataWriter {
return ifdOffset;
}
public long computeIFDSize(final Collection<Entry> directory) {
return WORD_LENGTH + computeDataSize(new IFD(directory)) + directory.size() * ENTRY_LENGTH;
private void writeDirectoryCount(ImageOutputStream stream, int count) throws IOException {
if (longOffsets) {
stream.writeLong(count);
}
else {
stream.writeShort(count);
}
}
private void writeValueCount(ImageOutputStream stream, int count) throws IOException {
if (longOffsets) {
stream.writeLong(count);
}
else {
stream.writeInt(count);
}
}
public long computeIFDSize(final Collection<? extends Entry> directory) {
return directoryCountLength + computeDataSize(new IFD(directory)) + directory.size() * entryLength;
}
private long computeDataSize(final Directory directory) {
@@ -175,13 +219,13 @@ public final class TIFFWriter extends MetadataWriter {
throw new IllegalArgumentException(String.format("Unknown size for entry %s", entry));
}
if (length > LONGWORD_LENGTH) {
if (length > offsetSize) {
dataSize += length;
}
if (entry.getValue() instanceof Directory) {
Directory subIFD = (Directory) entry.getValue();
long subIFDSize = WORD_LENGTH + subIFD.size() * ENTRY_LENGTH + computeDataSize(subIFD);
long subIFDSize = directoryCountLength + computeDataSize(subIFD) + subIFD.size() * entryLength;
dataSize += subIFDSize;
}
}
@@ -229,11 +273,11 @@ public final class TIFFWriter extends MetadataWriter {
short type = getType(entry);
long valueLength = getValueLength(type, getCount(entry));
if (valueLength <= LONGWORD_LENGTH) {
if (valueLength <= offsetSize) {
writeValueInline(entry.getValue(), type, stream);
// Pad
for (long i = valueLength; i < LONGWORD_LENGTH; i++) {
for (long i = valueLength; i < offsetSize; i++) {
stream.write(0);
}
@@ -248,7 +292,7 @@ public final class TIFFWriter extends MetadataWriter {
private int getCount(final Entry entry) {
Object value = entry.getValue();
return value instanceof String ? ((String) value).getBytes(Charset.forName("UTF-8")).length + 1 : entry.valueCount();
return value instanceof String ? ((String) value).getBytes(StandardCharsets.UTF_8).length + 1 : entry.valueCount();
}
private void writeValueInline(final Object value, final short type, final ImageOutputStream stream) throws IOException {
@@ -344,12 +388,28 @@ public final class TIFFWriter extends MetadataWriter {
doubles = (double[]) value;
}
else {
throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass());
throw new IllegalArgumentException("Unsupported type for TIFF DOUBLE: " + value.getClass());
}
stream.writeDoubles(doubles, 0, doubles.length);
break;
case TIFF.TYPE_LONG8:
case TIFF.TYPE_SLONG8:
if (longOffsets) {
long[] longs;
if (value instanceof long[]) {
longs = (long[]) value;
}
else {
throw new IllegalArgumentException("Unsupported type for TIFF LONG8: " + value.getClass());
}
stream.writeLongs(longs, 0, longs.length);
break;
}
default:
throw new IllegalArgumentException("Unsupported TIFF type: " + type);
@@ -373,6 +433,7 @@ public final class TIFFWriter extends MetadataWriter {
break;
case TIFF.TYPE_LONG:
case TIFF.TYPE_SLONG:
case TIFF.TYPE_IFD:
stream.writeInt(((Number) value).intValue());
break;
case TIFF.TYPE_RATIONAL:
@@ -387,6 +448,13 @@ public final class TIFFWriter extends MetadataWriter {
case TIFF.TYPE_DOUBLE:
stream.writeDouble(((Number) value).doubleValue());
break;
case TIFF.TYPE_LONG8:
case TIFF.TYPE_SLONG8:
case TIFF.TYPE_IFD8:
if (longOffsets) {
stream.writeLong(((Number) value).longValue());
break;
}
default:
throw new IllegalArgumentException("Unsupported TIFF type: " + type);
@@ -395,18 +463,39 @@ public final class TIFFWriter extends MetadataWriter {
}
private void writeValueAt(final long dataOffset, final Object value, final short type, final ImageOutputStream stream) throws IOException {
stream.writeInt(assertIntegerOffset(dataOffset));
writeOffset(stream, dataOffset);
long position = stream.getStreamPosition();
stream.seek(dataOffset);
writeValueInline(value, type, stream);
stream.seek(position);
}
private int assertIntegerOffset(long offset) throws IIOException {
if (offset > Integer.MAX_VALUE - (long) Integer.MIN_VALUE) {
public void writeOffset(final ImageOutputStream output, long offset) throws IOException {
if (longOffsets) {
output.writeLong(assertLongOffset(offset));
}
else {
output.writeInt(assertIntegerOffset(offset)); // Treated as unsigned
}
}
public int offsetSize() {
return offsetSize;
}
private int assertIntegerOffset(final long offset) throws IIOException {
if (offset < 0 || offset > Integer.MAX_VALUE - (long) Integer.MIN_VALUE) {
throw new IIOException("Integer overflow for TIFF stream");
}
return (int) offset;
}
private long assertLongOffset(final long offset) throws IIOException {
if (offset < 0) {
throw new IIOException("Long overflow for BigTIFF stream");
}
return offset;
}
}
@@ -30,11 +30,21 @@
package com.twelvemonkeys.imageio.metadata.xmp;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReader;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.Validate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
@@ -43,13 +53,11 @@ import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.util.*;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReader;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.Validate;
/**
* XMPReader
@@ -67,10 +75,9 @@ public final class XMPReader extends MetadataReader {
public Directory read(final ImageInputStream input) throws IOException {
Validate.notNull(input, "input");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
try {
DocumentBuilderFactory factory = createDocumentBuilderFactory();
// TODO: Consider parsing using SAX?
// TODO: Determine encoding and parse using a Reader...
// TODO: Refactor scanner to return inputstream?
@@ -79,9 +86,6 @@ public final class XMPReader extends MetadataReader {
builder.setErrorHandler(new DefaultHandler());
Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(input)));
// XMLSerializer serializer = new XMLSerializer(System.err, System.getProperty("file.encoding"));
// serializer.serialize(document);
String toolkit = getToolkit(document);
Node rdfRoot = document.getElementsByTagNameNS(XMP.NS_RDF, "RDF").item(0);
NodeList descriptions = document.getElementsByTagNameNS(XMP.NS_RDF, "Description");
@@ -92,10 +96,33 @@ public final class XMPReader extends MetadataReader {
throw new IIOException(e.getMessage(), e);
}
catch (ParserConfigurationException e) {
throw new RuntimeException(e); // TODO: Or IOException?
throw new RuntimeException(e);
}
}
private DocumentBuilderFactory createDocumentBuilderFactory() throws ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
// Security: Disable XInclude & expanding entity references ("bombs"), not needed for XMP
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
// Security: Enable "secure processing", to prevent DoS attacks
factory.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// Security: Remove possibility to access external DTDs or Schema, not needed for XMP
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
// Security: Disable loading of external DTD and entities, not needed for XMP
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return factory;
}
private String getToolkit(Document document) {
NodeList xmpmeta = document.getElementsByTagNameNS(XMP.NS_X, "xmpmeta");
@@ -109,7 +136,7 @@ public final class XMPReader extends MetadataReader {
}
private XMPDirectory parseDirectories(final Node pParentNode, NodeList pNodes, String toolkit) {
Map<String, List<Entry>> subdirs = new LinkedHashMap<String, List<Entry>>();
Map<String, List<Entry>> subdirs = new LinkedHashMap<>();
for (Node desc : asIterable(pNodes)) {
if (desc.getParentNode() != pParentNode) {
@@ -127,7 +154,7 @@ public final class XMPReader extends MetadataReader {
// Lookup
List<Entry> dir = subdirs.get(node.getNamespaceURI());
if (dir == null) {
dir = new ArrayList<Entry>();
dir = new ArrayList<>();
subdirs.put(node.getNamespaceURI(), dir);
}
@@ -139,7 +166,7 @@ public final class XMPReader extends MetadataReader {
else {
// TODO: This method contains loads of duplication an should be cleaned up...
// Support attribute short-hand syntax
Map<String, List<Entry>> subsubdirs = new LinkedHashMap<String, List<Entry>>();
Map<String, List<Entry>> subsubdirs = new LinkedHashMap<>();
parseAttributesForKnownElements(subsubdirs, node);
@@ -161,7 +188,7 @@ public final class XMPReader extends MetadataReader {
}
}
List<Directory> entries = new ArrayList<Directory>(subdirs.size());
List<Directory> entries = new ArrayList<>(subdirs.size());
// TODO: Should we still allow asking for a subdirectory by item id?
for (Map.Entry<String, List<Entry>> entry : subdirs.entrySet()) {
@@ -179,7 +206,7 @@ public final class XMPReader extends MetadataReader {
private RDFDescription parseAsResource(Node node) {
// See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource
List<Entry> entries = new ArrayList<Entry>();
List<Entry> entries = new ArrayList<>();
for (Node child : asIterable(node.getChildNodes())) {
if (child.getNodeType() != Node.ELEMENT_NODE) {
@@ -204,7 +231,7 @@ public final class XMPReader extends MetadataReader {
List<Entry> dir = subdirs.get(attr.getNamespaceURI());
if (dir == null) {
dir = new ArrayList<Entry>();
dir = new ArrayList<>();
subdirs.put(attr.getNamespaceURI(), dir);
}
@@ -216,7 +243,7 @@ public final class XMPReader extends MetadataReader {
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
Map<String, Object> alternatives = new LinkedHashMap<String, Object>();
Map<String, Object> alternatives = new LinkedHashMap<>();
for (Node alternative : asIterable(child.getChildNodes())) {
if (XMP.NS_RDF.equals(alternative.getNamespaceURI()) && "li".equals(alternative.getLocalName())) {
NamedNodeMap attributes = alternative.getAttributes();
@@ -230,7 +257,7 @@ public final class XMPReader extends MetadataReader {
else if (XMP.NS_RDF.equals(child.getNamespaceURI()) && ("Seq".equals(child.getLocalName()) || "Bag".equals(child.getLocalName()))) {
// Support for <rdf:Seq><rdf:li> -> return array
// Support for <rdf:Bag><rdf:li> -> return array/unordered collection (how can a serialized collection not have order?)
List<Object> seq = new ArrayList<Object>();
List<Object> seq = new ArrayList<>();
for (Node sequence : asIterable(child.getChildNodes())) {
if (XMP.NS_RDF.equals(sequence.getNamespaceURI()) && "li".equals(sequence.getLocalName())) {
@@ -0,0 +1,345 @@
/*
* Copyright (c) 2013, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.metadata.tiff;
import com.twelvemonkeys.imageio.metadata.*;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageOutputStreamImpl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* TIFFWriterTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFWriterTest.java,v 1.0 18.07.13 09:53 haraldk Exp$
*/
public class BigTIFFWriterTest extends MetadataWriterAbstractTest {
@Override
protected InputStream getData() throws IOException {
// TODO: Replace with BigTIFF resource
return getResource("/exif/exif-jpeg-segment.bin").openStream();
}
protected TIFFReader createReader() {
return new TIFFReader();
}
@Override
protected TIFFWriter createWriter() {
return new TIFFWriter(8);
}
@Test
public void testWriteReadSimple() throws IOException {
ArrayList<Entry> entries = new ArrayList<>();
entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, TIFF.TYPE_SHORT, 1));
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, 1600));
entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {});
entries.add(new TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, "Harald K."));
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
Directory directory = new AbstractDirectory(entries) {};
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
byte[] data = output.toByteArray();
assertEquals(164, data.length);
assertEquals('M', data[0]);
assertEquals('M', data[1]);
assertEquals(0, data[2]);
assertEquals(43, data[3]);
Directory read = createReader().read(new ByteArrayImageInputStream(data));
assertNotNull(read);
assertEquals(5, read.size());
// TODO: Assert that the tags are written in ascending order (don't test the read directory, but the file structure)!
assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
assertEquals(1600, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_HEIGHT));
assertEquals(900, read.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_ORIENTATION));
assertEquals(1, read.getEntryById(TIFF.TAG_ORIENTATION).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_ARTIST));
assertEquals("Harald K.", read.getEntryById(TIFF.TAG_ARTIST).getValue());
}
@Test
public void testWriteMotorola() throws IOException {
ArrayList<Entry> entries = new ArrayList<>();
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_LONG, Integer.MAX_VALUE));
Directory directory = new AbstractDirectory(entries) {};
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
imageStream.setByteOrder(ByteOrder.BIG_ENDIAN); // BE = Motorola
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
byte[] data = output.toByteArray();
assertEquals(94, data.length);
assertEquals('M', data[0]);
assertEquals('M', data[1]);
assertEquals(0, data[2]);
assertEquals(43, data[3]);
Directory read = new TIFFReader().read(new ByteArrayImageInputStream(data));
assertNotNull(read);
assertEquals(2, read.size());
assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
}
@Test
public void testWriteIntel() throws IOException {
ArrayList<Entry> entries = new ArrayList<>();
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_LONG, Integer.MAX_VALUE));
Directory directory = new AbstractDirectory(entries) {};
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
imageStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // LE = Intel
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
byte[] data = output.toByteArray();
assertEquals(94, data.length);
assertEquals('I', data[0]);
assertEquals('I', data[1]);
assertEquals(43, data[2]);
assertEquals(0, data[3]);
Directory read = new TIFFReader().read(new ByteArrayImageInputStream(data));
assertNotNull(read);
assertEquals(2, read.size());
assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
}
@Test
public void testNestingIFD8Long8() throws IOException {
TIFFEntry artist = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, "TwelveMonkeys ImageIO");
TIFFEntry subSubSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD8, new IFD(Collections.singletonList(artist)));
TIFFEntry subSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG8, new IFD(Collections.singletonList(subSubSubSubIFD)));
TIFFEntry subSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG8, new IFD(Collections.singletonList(subSubSubIFD)));
TIFFEntry subIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD8, new IFD(Collections.singletonList(subSubIFD)));
Directory directory = new IFD(Collections.<Entry>singletonList(subIFD));
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
Directory read = new TIFFReader().read(new ByteArrayImageInputStream(output.toByteArray()));
assertNotNull(read);
assertEquals(1, read.size());
assertEquals(subIFD, read.getEntryById(TIFF.TAG_SUB_IFD)); // Recursively tests content!
}
@Test
public void testNestingIFDLong() throws IOException {
TIFFEntry artist = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, "TwelveMonkeys ImageIO");
TIFFEntry subSubSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD, new IFD(Collections.singletonList(artist)));
TIFFEntry subSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubSubIFD)));
TIFFEntry subSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubIFD)));
TIFFEntry subIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD, new IFD(Collections.singletonList(subSubIFD)));
Directory directory = new IFD(Collections.<Entry>singletonList(subIFD));
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
Directory read = new TIFFReader().read(new ByteArrayImageInputStream(output.toByteArray()));
assertNotNull(read);
assertEquals(1, read.size());
assertEquals(subIFD, read.getEntryById(TIFF.TAG_SUB_IFD)); // Recursively tests content!
}
@Test
public void testReadWriteRead() throws IOException {
Directory original = createReader().read(getDataAsIIS());
ByteArrayOutputStream output = new FastByteArrayOutputStream(256);
ImageOutputStream imageOutput = ImageIO.createImageOutputStream(output);
try {
createWriter().write(original, imageOutput);
}
finally {
imageOutput.close();
}
Directory read = createReader().read(new ByteArrayImageInputStream(output.toByteArray()));
assertEquals(original, read);
}
@Test
public void testComputeIFDSize() throws IOException {
ArrayList<Entry> entries = new ArrayList<>();
entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, TIFF.TYPE_SHORT, 1));
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, 1600));
entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {});
entries.add(new TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, "Harald K."));
entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
TIFFWriter writer = createWriter();
ImageOutputStream stream = new NullImageOutputStream();
writer.writeIFD(entries, stream);
assertEquals(140, writer.computeIFDSize(entries));
assertEquals(148, stream.getStreamPosition());
}
@Test
public void testComputeIFDSizeNestedIFD8Long8() throws IOException {
TIFFEntry artist = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, "TwelveMonkeys ImageIO");
TIFFEntry subSubSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD8, new IFD(Collections.singletonList(artist)));
TIFFEntry subSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG8, new IFD(Collections.singletonList(subSubSubSubIFD)));
TIFFEntry subSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG8, new IFD(Collections.singletonList(subSubSubIFD)));
TIFFEntry subIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD8, new IFD(Collections.singletonList(subSubIFD)));
List<Entry> entries = Collections.<Entry>singletonList(subIFD);
TIFFWriter writer = createWriter();
ImageOutputStream stream = new NullImageOutputStream();
writer.writeIFD(entries, stream);
assertEquals(162, writer.computeIFDSize(entries));
assertEquals(170, stream.getStreamPosition());
}
@Test
public void testComputeIFDSizeNestedIFDLong() throws IOException {
TIFFEntry artist = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, "TwelveMonkeys ImageIO");
TIFFEntry subSubSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD, new IFD(Collections.singletonList(artist)));
TIFFEntry subSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubSubIFD)));
TIFFEntry subSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubIFD)));
TIFFEntry subIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_IFD, new IFD(Collections.singletonList(subSubIFD)));
List<Entry> entries = Collections.<Entry>singletonList(subIFD);
TIFFWriter writer = createWriter();
ImageOutputStream stream = new NullImageOutputStream();
writer.writeIFD(entries, stream);
assertEquals(162, writer.computeIFDSize(entries)); // 162 = 5 * (8 + 20) + 22
assertEquals(170, stream.getStreamPosition()); // 170 = 8 + 5 * (8 + 20) + 22
}
private static class NullImageOutputStream extends ImageOutputStreamImpl {
@Override
public void write(int b) throws IOException {
streamPos++;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
streamPos += len;
}
@Override
public int read() throws IOException {
throw new UnsupportedOperationException("Method read not implemented");
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
throw new UnsupportedOperationException("Method read not implemented");
}
}
}
@@ -33,6 +33,7 @@ package com.twelvemonkeys.imageio.metadata.tiff;
import com.twelvemonkeys.imageio.metadata.*;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import org.junit.Test;
import javax.imageio.ImageIO;
@@ -84,7 +85,7 @@ public class TIFFWriterTest extends MetadataWriterAbstractTest {
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
new TIFFWriter().write(directory, imageStream);
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
@@ -132,7 +133,7 @@ public class TIFFWriterTest extends MetadataWriterAbstractTest {
imageStream.setByteOrder(ByteOrder.BIG_ENDIAN); // BE = Motorola
new TIFFWriter().write(directory, imageStream);
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
@@ -167,7 +168,7 @@ public class TIFFWriterTest extends MetadataWriterAbstractTest {
imageStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // LE = Intel
new TIFFWriter().write(directory, imageStream);
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
@@ -204,7 +205,7 @@ public class TIFFWriterTest extends MetadataWriterAbstractTest {
ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
new TIFFWriter().write(directory, imageStream);
createWriter().write(directory, imageStream);
imageStream.flush();
assertEquals(output.size(), imageStream.getStreamPosition());
@@ -247,9 +248,10 @@ public class TIFFWriterTest extends MetadataWriterAbstractTest {
TIFFWriter writer = createWriter();
ImageOutputStream stream = new NullImageOutputStream();
writer.write(new IFD(entries), stream);
writer.writeIFD(entries, stream);
assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12);
assertEquals(94, writer.computeIFDSize(entries));
assertEquals(98, stream.getStreamPosition());
}
@Test
@@ -266,9 +268,10 @@ public class TIFFWriterTest extends MetadataWriterAbstractTest {
TIFFWriter writer = createWriter();
ImageOutputStream stream = new NullImageOutputStream();
writer.write(new IFD(entries), stream);
writer.writeIFD(entries, stream);
assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12);
assertEquals(92, writer.computeIFDSize(entries)); // 92 = 5 * (2 + 12) + 22
assertEquals(96, stream.getStreamPosition()); // 96 = 4 + 5 * (2 + 12) + 22
}
private static class NullImageOutputStream extends ImageOutputStreamImpl {
@@ -30,26 +30,32 @@
package com.twelvemonkeys.imageio.metadata.xmp;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReaderAbstractTest;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import org.junit.Test;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.MetadataReaderAbstractTest;
/**
* XMPReaderTest
@@ -483,4 +489,69 @@ public class XMPReaderTest extends MetadataReaderAbstractTest {
assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/PixelYDimension"), hasValue("550"));
assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/NativeDigest"), hasValue("36864,40960,40961,37121,37122,40962,40963,37510,40964,36867,36868,33434,33437,34850,34852,34855,34856,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37396,41483,41484,41486,41487,41488,41492,41493,41495,41728,41729,41730,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,42016,0,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,22,23,24,25,26,27,28,30;A7F21D25E2C562F152B2C4ECC9E534DA"));
}
@Test(timeout = 1500L)
public void testNoExternalRequest() throws Exception {
// TODO: Use dynamic port?
try (HTTPServer server = new HTTPServer(7777)) {
try {
createReader().read(getResourceAsIIS("/xmp/xmp-jpeg-xxe.xml"));
} catch (IOException ioe) {
if (ioe.getMessage().contains("501")) {
throw new AssertionError("Reading should not cause external requests", ioe);
}
// Any other exception is a bug (but might happen if the parser does not support certain features)
throw ioe;
}
}
}
private static class HTTPServer implements AutoCloseable {
private final ServerSocket server;
private final Thread thread;
HTTPServer(int port) throws IOException {
server = new ServerSocket(port, 1);
thread = new Thread(new Runnable() {
@Override public void run() {
serve();
}
});
thread.start();
}
private void serve() {
try {
Socket client = server.accept();
// Get the input stream, don't care about the request
try (InputStream inputStream = client.getInputStream()) {
while (inputStream.available() > 0) {
if (inputStream.read() == -1) {
break;
}
}
// Answer with 501, this will cause the client to throw IOException
try (OutputStream outputStream = client.getOutputStream()) {
outputStream.write("HTTP/1.0 501 Not Implemented\r\n\r\n".getBytes(StandardCharsets.UTF_8));
}
}
}
catch (IOException e) {
if (server.isClosed() && e instanceof SocketException) {
// Socket closed due to server close, all good
return;
}
throw new RuntimeException(e);
}
}
@Override public void close() throws Exception {
server.close();
thread.join(); // It's advised against throwing InterruptedException here, but this is not production code...
}
}
}
@@ -0,0 +1,35 @@
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?><!DOCTYPE root [<!ENTITY % ext SYSTEM 'http://localhost:7777/xxx'> %ext;]>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 10.16'>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
<rdf:Description rdf:about=''
xmlns:xmpMM='http://ns.adobe.com/xap/1.0/mm/'>
<xmpMM:InstanceID>xmp.iid:7EDC21BF-371B-4189-90AF-C83A54A6A190</xmpMM:InstanceID>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end='w'?>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-pcx</artifactId>
<name>TwelveMonkeys :: ImageIO :: PCX plugin</name>
@@ -104,8 +104,15 @@ public final class PCXImageReader extends ImageReaderBase {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<>();
// TODO: Implement
if (rawType.getSampleModel() instanceof BandedSampleModel) {
if (rawType.getNumBands() == 3) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
}
else if (rawType.getNumBands() == 4) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE));
}
}
specifiers.add(rawType);
return specifiers.iterator();
@@ -142,10 +149,10 @@ public final class PCXImageReader extends ImageReaderBase {
// PCX RGB has channels for 24 bit RGB, will be validated by ImageTypeSpecifier
return ImageTypeSpecifiers.createBanded(ColorSpace.getInstance(ColorSpace.CS_sRGB), createIndices(channels, 1), createIndices(channels, 0), DataBuffer.TYPE_BYTE, channels == 4, false);
case 24:
// Some sources says this is possible...
// Some sources say this is possible...
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
case 32:
// Some sources says this is possible...
// Some sources say this is possible...
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
default:
throw new IIOException("Unknown number of bytes per pixel: " + header.getBitsPerPixel());
@@ -78,7 +78,7 @@ public class PCXImageReaderTest extends ImageReaderAbstractTest<PCXImageReader>
new TestData(getClassLoaderResource("/pcx/DARKSTAR.PCX"), new Dimension(88, 52)), // RLE encoded monochrome (1 bps/1 channel)
new TestData(getClassLoaderResource("/pcx/MARBLES.PCX"), new Dimension(1419, 1001)), // RLE encoded RGB
new TestData(getClassLoaderResource("/pcx/no-palette-monochrome.pcx"), new Dimension(128, 152)), // RLE encoded monochrome (1 bps/1 channel)
// See cga-pcx.txt, however, the text seems to be in error, the bits can not not as described
// See cga-pcx.txt, however, the text seems to be in error, I don't see how the bits can be as described
new TestData(getClassLoaderResource("/pcx/CGA_BW.PCX"), new Dimension(640, 200)), // RLE encoded indexed (CGA mode)
new TestData(getClassLoaderResource("/pcx/CGA_FSD.PCX"), new Dimension(320, 200)), // RLE encoded indexed (CGA mode)
new TestData(getClassLoaderResource("/pcx/CGA_RGBI.PCX"), new Dimension(320, 200)), // RLE encoded indexed (CGA mode)
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-pdf</artifactId>
<name>TwelveMonkeys :: ImageIO :: PDF plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-pict</artifactId>
<name>TwelveMonkeys :: ImageIO :: PICT plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-pnm</artifactId>
<name>TwelveMonkeys :: ImageIO :: PNM plugin</name>
@@ -61,6 +61,20 @@ public class PNMImageReaderTest extends ImageReaderAbstractTest<PNMImageReader>
);
}
@Override
protected List<TestData> getTestDataForAffineTransformOpCompatibility() {
return Arrays.asList(
new TestData(getClassLoaderResource("/ppm/lena.ppm"), new Dimension(128, 128)), // P6 (PPM RAW)
new TestData(getClassLoaderResource("/ppm/colors.ppm"), new Dimension(3, 2)), // P3 (PPM PLAIN)
new TestData(getClassLoaderResource("/pbm/j.pbm"), new Dimension(6, 10)), // P1 (PBM PLAIN)
new TestData(getClassLoaderResource("/pgm/feep.pgm"), new Dimension(24, 7)), // P2 (PGM PLAIN)
new TestData(getClassLoaderResource("/pgm/feep16.pgm"), new Dimension(24, 7)), // P2 (PGM PLAIN, 16 bits/sample)
new TestData(getClassLoaderResource("/pgm/house.l.pgm"), new Dimension(367, 241)), // P5 (PGM RAW)
new TestData(getClassLoaderResource("/ppm/lighthouse_rgb48.ppm"), new Dimension(768, 512)) // P6 (PPM RAW, 16 bits/sample)
// "/pfm/memorial.pfm" uses floating point
);
}
@Override
protected List<String> getFormatNames() {
return Arrays.asList(
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-psd</artifactId>
<name>TwelveMonkeys :: ImageIO :: PSD plugin</name>
@@ -30,6 +30,7 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.stream.ImageInputStream;
@@ -55,7 +56,7 @@ final class ICCProfile extends PSDImageResource {
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
try (InputStream stream = IIOUtil.createStreamAdapter(pInput, size)) {
profile = ICC_Profile.getInstance(stream);
profile = ColorProfiles.readProfileRaw(stream);
}
}
@@ -331,7 +331,7 @@ public final class PSDImageReader extends ImageReaderBase {
// Just stick to the raw type
}
// Finally add the raw type
// Finally, add the raw type
types.add(rawType);
return types.iterator();
@@ -59,8 +59,8 @@ import java.util.Collections;
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDImageWriter.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
* @see <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/">Adobe Photoshop File Formats Specification<a>
* @see <a href="http://www.fileformat.info/format/psd/egff.htm">Adobe Photoshop File Format Summary<a>
* @see <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/">Adobe Photoshop File Formats Specification</a>
* @see <a href="http://www.fileformat.info/format/psd/egff.htm">Adobe Photoshop File Format Summary</a>
*/
public final class PSDImageWriter extends ImageWriterBase {
@@ -86,8 +86,6 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest<PSDImageReader>
new TestData(getClassLoaderResource("/psd/test_gray16.psd"), new Dimension(710, 512)),
// 4 channel, CMYK, 16 bit samples
new TestData(getClassLoaderResource("/psd/cmyk_16bits.psd"), new Dimension(1000, 275)),
// 3 channel, RGB, 32 bit samples
new TestData(getClassLoaderResource("/psd/32bit5x5.psd"), new Dimension(5, 5)),
// 3 channel, RGB, 8 bit samples ("Large Document Format" aka PSB)
new TestData(getClassLoaderResource("/psb/test_original.psb"), new Dimension(710, 512)),
// From http://telegraphics.com.au/svn/psdparse/trunk/psd/
@@ -104,11 +102,48 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest<PSDImageReader>
new TestData(getClassLoaderResource("/psd/rgb-multichannel-no-transparency.psd"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/psb/rgb-multichannel-no-transparency.psb"), new Dimension(100, 100)),
// CMYK, uncompressed + contains some uncommon MeSa (instead of 8BIM) resource blocks
new TestData(getClassLoaderResource("/psd/fruit-cmyk-MeSa-resource.psd"), new Dimension(400, 191))
new TestData(getClassLoaderResource("/psd/fruit-cmyk-MeSa-resource.psd"), new Dimension(400, 191)),
// 3 channel, RGB, 32 bit samples
new TestData(getClassLoaderResource("/psd/32bit5x5.psd"), new Dimension(5, 5))
// TODO: Need more recent ZIP compressed PSD files from CS2/CS3+
);
}
@Override
protected List<TestData> getTestDataForAffineTransformOpCompatibility() {
return Arrays.asList(
// 5 channel, RGB
new TestData(getClassLoaderResource("/psd/photoshopping.psd"), new Dimension(300, 225)),
// 1 channel, gray, 8 bit samples
new TestData(getClassLoaderResource("/psd/buttons.psd"), new Dimension(20, 20)),
// 3 channel RGB, "no composite layer"
new TestData(getClassLoaderResource("/psd/jugware-icon.psd"), new Dimension(128, 128)),
// 3 channel RGB, old data, no layer info/mask
new TestData(getClassLoaderResource("/psd/MARBLES.PSD"), new Dimension(1419, 1001)),
// 1 channel, indexed color
new TestData(getClassLoaderResource("/psd/coral_fish.psd"), new Dimension(800, 800)),
// 1 channel, bitmap, 1 bit samples
new TestData(getClassLoaderResource("/psd/test_bitmap.psd"), new Dimension(710, 512)),
// 1 channel, gray, 16 bit samples
new TestData(getClassLoaderResource("/psd/test_gray16.psd"), new Dimension(710, 512)),
// 3 channel, RGB, 8 bit samples ("Large Document Format" aka PSB)
new TestData(getClassLoaderResource("/psb/test_original.psb"), new Dimension(710, 512)),
// From http://telegraphics.com.au/svn/psdparse/trunk/psd/
new TestData(getClassLoaderResource("/psd/adobehq.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq_ind.psd"), new Dimension(341, 512)),
// Contains a shorter than normal PrintFlags chunk
new TestData(getClassLoaderResource("/psd/adobehq-2.5.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-3.0.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-5.5.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-7.0.psd"), new Dimension(341, 512)),
// From https://github.com/kmike/psd-tools/tree/master/tests/psd_files
new TestData(getClassLoaderResource("/psd/masks2.psd"), new Dimension(640, 1136)),
// RGB, multiple alpha channels, no transparency
new TestData(getClassLoaderResource("/psd/rgb-multichannel-no-transparency.psd"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/psb/rgb-multichannel-no-transparency.psb"), new Dimension(100, 100))
);
}
@Override
protected List<String> getFormatNames() {
return Collections.singletonList("psd");
@@ -472,7 +507,7 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest<PSDImageReader>
public void testMultiChannelNoTransparencyPSB() throws IOException {
PSDImageReader imageReader = createReader();
// The following PSB is RGB, has 4 channels (1 alpha/auxillary channel), but should be treated as opaque
// The following PSB is RGB, has 4 channels (1 alpha/auxiliary channel), but should be treated as opaque
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psb/rgb-multichannel-no-transparency.psb"))) {
imageReader.setInput(stream);
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-reference</artifactId>
<name>TwelveMonkeys :: ImageIO :: JDK Reference Tests</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-sgi</artifactId>
<name>TwelveMonkeys :: ImageIO :: SGI plugin</name>
@@ -59,7 +59,7 @@ public final class SGIImageReader extends ImageReaderBase {
private SGIHeader header;
protected SGIImageReader(final ImageReaderSpi provider) {
SGIImageReader(final ImageReaderSpi provider) {
super(provider);
}
@@ -88,9 +88,31 @@ public final class SGIImageReader extends ImageReaderBase {
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
List<ImageTypeSpecifier> specifiers = new ArrayList<>();
int channels = header.getChannels();
switch (header.getBytesPerPixel()) {
case 1:
if (channels == 1) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY));
}
else if (channels == 3) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
}
else if (channels == 4) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
}
break;
case 2:
if (channels == 1) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY));
}
break;
}
// TODO: Implement
specifiers.add(rawType);
return specifiers.iterator();
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-tga</artifactId>
<name>TwelveMonkeys :: ImageIO :: TGA plugin</name>
@@ -96,10 +96,18 @@ final class TGAImageReader extends ImageReaderBase {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<>();
// TODO: Implement
specifiers.add(rawType);
if (rawType.getBufferedImageType() == BufferedImage.TYPE_INT_RGB) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
}
else if (rawType.getBufferedImageType() == BufferedImage.TYPE_INT_ARGB) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE));
}
else if (rawType.getBufferedImageType() == BufferedImage.TYPE_INT_ARGB_PRE) {
specifiers.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
}
return specifiers.iterator();
}
@@ -116,7 +124,14 @@ final class TGAImageReader extends ImageReaderBase {
return ImageTypeSpecifiers.createFromIndexColorModel(header.getColorMap());
case TGA.IMAGETYPE_MONOCHROME:
case TGA.IMAGETYPE_MONOCHROME_RLE:
return ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE);
switch (header.getPixelDepth()) {
case 8:
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
case 16:
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY);
default:
throw new IIOException("Unknown pixel depth for monochrome: " + header.getPixelDepth());
}
case TGA.IMAGETYPE_TRUECOLOR:
case TGA.IMAGETYPE_TRUECOLOR_RLE:
ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
@@ -135,12 +150,14 @@ final class TGAImageReader extends ImageReaderBase {
case 24:
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
case 32:
// 4BYTE_BGRX...
// Can't mask out alpha (efficiently) for 4BYTE, so we'll ignore it while reading instead,
// if hasAlpha is false
return ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, isAlphaPremultiplied);
// NOTE: We'll read using little endian byte order, thus the file layout is BGRA/BGRx
if (hasAlpha) {
return ImageTypeSpecifier.createFromBufferedImageType(isAlphaPremultiplied ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_ARGB);
}
return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
default:
throw new IIOException("Unknown pixel depth for truecolor: " + header.getPixelDepth());
throw new IIOException("Unknown pixel depth for true color: " + header.getPixelDepth());
}
default:
throw new IIOException("Unknown image type: " + header.getImageType());
@@ -187,20 +204,26 @@ final class TGAImageReader extends ImageReaderBase {
input = imageInput;
}
int pixelDepth = header.getPixelDepth();
boolean flipped = isOriginLowerLeft(header.getOrigin());
for (int y = 0; y < height; y++) {
switch (header.getPixelDepth()) {
switch (pixelDepth) {
case 8:
case 24:
case 32:
byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
readRowByte(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataByte, destRaster, clippedRow, y);
readRowByte(input, height, srcRegion, flipped, xSub, ySub, rowDataByte, destRaster, clippedRow, y);
break;
case 16:
short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
readRowUShort(input, height, srcRegion, header.getOrigin(), xSub, ySub, rowDataUShort, destRaster, clippedRow, y);
readRowUShort(input, height, srcRegion, flipped, xSub, ySub, rowDataUShort, destRaster, clippedRow, y);
break;
case 32:
int[] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
readRowInt(input, height, srcRegion, flipped, xSub, ySub, rowDataInt, destRaster, clippedRow, y);
break;
default:
throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth());
throw new AssertionError("Unsupported pixel depth: " + pixelDepth);
}
processImageProgress(100f * y / height);
@@ -220,10 +243,26 @@ final class TGAImageReader extends ImageReaderBase {
return destination;
}
private void readRowByte(final DataInput input, int height, Rectangle srcRegion, int origin, int xSub, int ySub,
private boolean isOriginLowerLeft(final int origin) throws IIOException {
switch (origin) {
case TGA.ORIGIN_LOWER_LEFT:
return true;
case TGA.ORIGIN_UPPER_LEFT:
return false;
default:
// Other orientations are not supported
throw new IIOException("Unsupported origin: " + origin);
}
}
private void readRowByte(final DataInput input, int height, Rectangle srcRegion, boolean flip, int xSub, int ySub,
byte[] rowDataByte, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
// Flip into position?
int srcY = flip ? height - 1 - y : y;
int dstY = (srcY - srcRegion.y) / ySub;
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
if (y % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataByte.length);
return;
@@ -244,19 +283,7 @@ final class TGAImageReader extends ImageReaderBase {
}
}
switch (origin) {
case TGA.ORIGIN_LOWER_LEFT:
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
break;
case TGA.ORIGIN_UPPER_LEFT:
dstY = y / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
break;
default:
throw new IIOException("Unsupported origin: " + origin);
}
destChannel.setDataElements(0, dstY, srcChannel);
}
private void removeAlpha32(final byte[] rowData) {
@@ -265,10 +292,14 @@ final class TGAImageReader extends ImageReaderBase {
}
}
private void readRowUShort(final DataInput input, int height, Rectangle srcRegion, int origin, int xSub, int ySub,
private void readRowUShort(final DataInput input, int height, Rectangle srcRegion, boolean flip, int xSub, int ySub,
short[] rowDataUShort, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
// Flip into position?
int srcY = flip ? height - 1 - y : y;
int dstY = (srcY - srcRegion.y) / ySub;
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
if (y % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataUShort.length * 2);
return;
@@ -283,19 +314,32 @@ final class TGAImageReader extends ImageReaderBase {
}
}
switch (origin) {
case TGA.ORIGIN_LOWER_LEFT:
// Flip into position
int dstY = (height - 1 - y - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
break;
case TGA.ORIGIN_UPPER_LEFT:
dstY = y / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
break;
default:
throw new IIOException("Unsupported origin: " + origin);
destChannel.setDataElements(0, dstY, srcChannel);
}
private void readRowInt(final DataInput input, int height, Rectangle srcRegion, boolean flip, int xSub, int ySub,
int[] rowDataInt, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
// Flip into position?
int srcY = flip ? height - 1 - y : y;
int dstY = (srcY - srcRegion.y) / ySub;
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataInt.length * 4);
return;
}
readFully(input, rowDataInt);
// Subsample horizontal
if (xSub != 1) {
for (int x = srcRegion.x / xSub; x < ((srcRegion.x + srcRegion.width) / xSub); x++) {
rowDataInt[x] = rowDataInt[x * xSub];
}
}
destChannel.setDataElements(0, dstY, srcChannel);
}
// TODO: Candidate util method
@@ -311,6 +355,19 @@ final class TGAImageReader extends ImageReaderBase {
}
}
// TODO: Candidate util method
private static void readFully(final DataInput input, final int[] ints) throws IOException {
if (input instanceof ImageInputStream) {
// Optimization for ImageInputStreams, read all in one go
((ImageInputStream) input).readFully(ints, 0, ints.length);
}
else {
for (int i = 0; i < ints.length; i++) {
ints[i] = input.readInt();
}
}
}
private Raster clipRowToRect(final Raster raster, final Rectangle rect, final int[] bands, final int xSub) {
if (rect.contains(raster.getMinX(), 0, raster.getWidth(), 1)
&& xSub == 1
@@ -446,20 +503,26 @@ final class TGAImageReader extends ImageReaderBase {
// Thumbnail is always stored non-compressed, no need for RLE support
imageInput.seek(extensions.getThumbnailOffset() + 2);
int pixelDepth = header.getPixelDepth();
boolean flipped = isOriginLowerLeft(header.getOrigin());
for (int y = 0; y < height; y++) {
switch (header.getPixelDepth()) {
switch (pixelDepth) {
case 8:
case 24:
case 32:
byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
readRowByte(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataByte, destRaster, rowRaster, y);
readRowByte(imageInput, height, srcRegion, flipped, 1, 1, rowDataByte, destRaster, rowRaster, y);
break;
case 16:
short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
readRowUShort(imageInput, height, srcRegion, header.getOrigin(), 1, 1, rowDataUShort, destRaster, rowRaster, y);
readRowUShort(imageInput, height, srcRegion, flipped, 1, 1, rowDataUShort, destRaster, rowRaster, y);
break;
case 32:
int[] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
readRowInt(imageInput, height, srcRegion, flipped, 1, 1, rowDataInt, destRaster, rowRaster, y);
break;
default:
throw new AssertionError("Unsupported pixel depth: " + header.getPixelDepth());
throw new AssertionError("Unsupported pixel depth: " + pixelDepth);
}
processThumbnailProgress(100f * y / height);
@@ -193,10 +193,13 @@ final class TGAMetadata extends AbstractMetadata {
switch (header.getPixelDepth()) {
case 8:
bitsPerSample.setAttribute("value", createListValue(1, Integer.toString(header.getPixelDepth())));
bitsPerSample.setAttribute("value", createListValue(1, "8"));
break;
case 16:
if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) {
if (header.getImageType() == TGA.IMAGETYPE_MONOCHROME || header.getImageType() == TGA.IMAGETYPE_MONOCHROME_RLE) {
bitsPerSample.setAttribute("value", "16");
}
else if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) {
bitsPerSample.setAttribute("value", "5, 5, 5, 1");
}
else {
@@ -91,7 +91,10 @@ public class TGAImageReaderTest extends ImageReaderAbstractTest<TGAImageReader>
new TestData(getClassLoaderResource("/tga/XING_T24.TGA"), new Dimension(240, 164)), // Uncompressed 24 bit BGR top/down
new TestData(getClassLoaderResource("/tga/XING_T32.TGA"), new Dimension(240, 164)), // Uncompressed 32 bit BGRA top/down
new TestData(getClassLoaderResource("/tga/autodesk-3dsmax-extsize494.tga"), new Dimension(440, 200)) // RLE compressed 32 bit BGRA bottom/up
new TestData(getClassLoaderResource("/tga/autodesk-3dsmax-extsize494.tga"), new Dimension(440, 200)), // RLE compressed 32 bit BGRA bottom/up
new TestData(getClassLoaderResource("/tga/monochrome16_top_left.tga"), new Dimension(64, 64)), // Uncompressed 16 bit monochrome
new TestData(getClassLoaderResource("/tga/monochrome16_top_left_rle.tga"), new Dimension(64, 64)) // RLE compressed 16 bit monochrome
);
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-thumbsdb</artifactId>
<name>TwelveMonkeys :: ImageIO :: Thumbs.db plugin</name>
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0</version>
</parent>
<artifactId>imageio-tiff-jai-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF/JAI Metadata Interop</name>
<description>
Test TIFF plugin and JAI TIFF plugin Metadata interoperability
</description>
<properties>
<project.jpms.module.name>com.twelvemonkeys.imageio.tiff.jaiinterop</project.jpms.module.name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-core</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-metadata</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-tiff</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff.jaiinterop;
import com.twelvemonkeys.imageio.metadata.tiff.Rational;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadata;
import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Iterator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Tests our TIFFImageMetadata works with JAI TIFFImageWriter.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageReaderJDKJPEGInteroperabilityTest.java,v 1.0 08.05.12 15:25 haraldk Exp$
*/
public class TIFFImageMetadataJAInteroperabilityTest {
private static final String JAI_TIFF_PROVIDER_CLASS_NAME = "com.github.jaiimageio.impl.plugins.tiff.TIFFImageWriterSpi";
private ImageWriter createImageWriter() {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("TIFF");
while (writers.hasNext()) {
ImageWriter writer = writers.next();
if (JAI_TIFF_PROVIDER_CLASS_NAME.equals(writer.getOriginatingProvider().getClass().getName())) {
return writer;
}
}
throw new AssertionError("Expected Spi not found (dependency issue?): " + JAI_TIFF_PROVIDER_CLASS_NAME);
}
@Test
public void testRationalNeedsDenominator() {
// Set the resolution to 200 dpi
IIOMetadata ourMetadata = new TIFFImageMetadata(Arrays.asList(new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, 2), // Unit DPI (default)
new TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(200)),
new TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(200))));
ImageTypeSpecifier type = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
ImageWriter writer = createImageWriter();
IIOMetadata converted = writer.convertImageMetadata(ourMetadata, type, null);
assertNotNull(converted);
// Make sure we have x/y resolution in converted metadata
IIOMetadataNode standardTree = (IIOMetadataNode) converted.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
String horizontalPixelSize = ((IIOMetadataNode) standardTree.getElementsByTagName("HorizontalPixelSize").item(0)).getAttribute("value");
String verticalPixelSize = ((IIOMetadataNode) standardTree.getElementsByTagName("VerticalPixelSize").item(0)).getAttribute("value");
// For some reason this is *pixel size* in *mm*...
String expected = String.valueOf(2.54 / 200 * 10);
assertEquals(expected, horizontalPixelSize);
assertEquals(expected, verticalPixelSize);
}
}
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-tiff-jdk-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF/JDK JPEG Interop</name>
@@ -13,7 +13,7 @@
</description>
<properties>
<project.jpms.module.name>com.twelvemonkeys.imageio.jdkinterop</project.jpms.module.name>
<project.jpms.module.name>com.twelvemonkeys.imageio.tiff.jdkinterop</project.jpms.module.name>
</properties>
<build>
@@ -28,25 +28,26 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.jpeg.jdkinterop;
package com.twelvemonkeys.imageio.plugins.tiff.jdkinterop;
import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader;
import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import static org.junit.Assert.fail;
import org.junit.Ignore;
import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static org.junit.Assert.fail;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import org.junit.Ignore;
import org.junit.Test;
import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader;
import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
/**
* Tests our TIFFImageReader delegating to the JDK JPEGImageReader.
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>3.8.0</version>
</parent>
<artifactId>imageio-tiff</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF plugin</name>
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import java.util.Locale;
/**
* BigTIFFImageWriterSpi
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BigTIFFImageWriterSpi.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/
public final class BigTIFFImageWriterSpi extends ImageWriterSpiBase {
// TODO: Implement canEncodeImage better
public BigTIFFImageWriterSpi() {
super(new BigTIFFProviderInfo());
}
@Override
public boolean canEncodeImage(final ImageTypeSpecifier type) {
// TODO: Test bit depths compatibility
return true;
}
@Override
public TIFFImageWriter createWriterInstance(final Object extension) {
return new TIFFImageWriter(this);
}
@Override
public String getDescription(final Locale locale) {
return "BigTIFF image writer";
}
}
@@ -50,10 +50,10 @@ final class BigTIFFProviderInfo extends ReaderWriterProviderInfo {
},
"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.tiff.BigTIFFImageReaderSpi"},
null,
null,
"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriter",
new String[] {"com.twelvemonkeys.imageio.plugins.tiff.BigTIFFImageWriterSpi"},
false, TIFFStreamMetadata.SUN_NATIVE_STREAM_METADATA_FORMAT_NAME, "com.twelvemonkeys.imageio.plugins.tiff.TIFFStreamMetadataFormat", null, null,
true, TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, "com.twelvemonkeys.imageio.plugins.tiff.TIFFMedataFormat", null, null
true, TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, "com.twelvemonkeys.imageio.plugins.tiff.TIFFMedataFormat", null, null
);
}
}
@@ -58,8 +58,6 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
private final boolean optionUncompressed;
private final boolean optionByteAligned;
// Need to take fill order into account (?) (use flip table?)
private final int fillOrder;
private final int type;
private int decodedLength;
@@ -81,12 +79,10 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
* @param columns the number of columns in the stream.
* @param type the type of stream, must be one of {@code COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE},
* {@code COMPRESSION_CCITT_T4} or {@code COMPRESSION_CCITT_T6}.
* @param fillOrder fillOrder, must be {@code FILL_LEFT_TO_RIGHT} or
* {@code FILL_RIGHT_TO_LEFT}.
* @param options CCITT T.4 or T.6 options.
* @param byteAligned enable byte alignment used in PDF files (EncodedByteAlign).
*/
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder,
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type,
final long options, final boolean byteAligned) {
super(Validate.notNull(stream, "stream"));
@@ -95,10 +91,6 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
type == TIFFExtension.COMPRESSION_CCITT_T4 ||
type == TIFFExtension.COMPRESSION_CCITT_T6,
type, "Only CCITT Modified Huffman RLE compression (2), CCITT T4 (3) or CCITT T6 (4) supported: %s");
this.fillOrder = Validate.isTrue(
fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT || fillOrder == TIFFExtension.FILL_RIGHT_TO_LEFT,
fillOrder, "Expected fill order 1 or 2: %s"
);
// We know this is only used for b/w (1 bit)
decodedRow = new byte[(columns + 7) / 8];
@@ -140,54 +132,63 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
* @param columns the number of columns in the stream.
* @param type the type of stream, must be one of {@code COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE},
* {@code COMPRESSION_CCITT_T4} or {@code COMPRESSION_CCITT_T6}.
* @param fillOrder fillOrder, must be {@code FILL_LEFT_TO_RIGHT} or
* {@code FILL_RIGHT_TO_LEFT}.
* @param options CCITT T.4 or T.6 options.
*/
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder,
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type,
final long options) {
this(stream, columns, type, fillOrder, options, type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE);
this(stream, columns, type, options, type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE);
}
static int findCompressionType(final int type, final InputStream in) throws IOException {
// Discover possible incorrect type, revert to RLE
if (type == TIFFExtension.COMPRESSION_CCITT_T4 && in.markSupported()) {
byte[] streamData = new byte[32];
static int findCompressionType(final int encodedType, final InputStream stream) throws IOException {
// Discover possible incorrect compression type, revert to RLE if no EOLs found
if (encodedType == TIFFExtension.COMPRESSION_CCITT_T4 && stream.markSupported()) {
int limit = 512;
try {
in.mark(streamData.length);
stream.mark(limit);
int first = stream.read();
int second = stream.read();
int offset = 0;
while (offset < streamData.length) {
int read = in.read(streamData, offset, streamData.length - offset);
if (read <= 0) {
break;
if (second == -1) {
// stream to short
return encodedType;
}
else if (first == 0 && (((byte) second) >> 4 == 1 || ((byte) second) == 1)) {
// correct, starts with EOL or byte aligned EOL
return encodedType;
}
short b = (short) (((((byte) first) << 8) + ((byte) second)) >> 4);
int limitBits = limit * 8;
int read = second;
byte streamByte = (byte) read;
for (int i = 12; i < limitBits; i++) {
if (i % 8 == 0) {
read = stream.read();
if (read == -1) {
// no EOL before stream end
return TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE;
}
}
offset += read;
}
}
finally {
in.reset();
}
if (streamData[0] != 0 || (streamData[1] >> 4 != 1 && streamData[1] != 1)) {
// Leading EOL (0b000000000001) not found, search further and try RLE if not found
int numBits = streamData.length * 8;
short b = (short) (((streamData[0] << 8) + streamData[1]) >> 4);
for (int i = 12; i < numBits; i++) {
b = (short) ((b << 1) + ((streamData[(i / 8)] >> (7 - (i % 8))) & 0x01));
b = (short) ((b << 1) + ((streamByte >> (7 - (i % 8))) & 0x01));
if ((b & 0xFFF) == 1) {
// found EOL
return TIFFExtension.COMPRESSION_CCITT_T4;
}
}
// no EOL till limit
return TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE;
}
finally {
stream.reset();
}
}
return type;
return encodedType;
}
private void fetch() throws IOException {
@@ -197,13 +198,17 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
try {
decodeRow();
}
catch (ArrayIndexOutOfBoundsException e) {
// Mask the AIOOBE as an IOException
throw new IOException("Malformed CCITT stream", e);
}
catch (EOFException e) {
// TODO: Rewrite to avoid throw/catch for normal flow...
if (decodedLength != 0) {
throw e;
}
// ..otherwise, just let client code try to read past the
// ...otherwise, just let client code try to read past the
// end of stream
decodedLength = -1;
}
@@ -468,7 +473,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
int bufferPos = -1;
private boolean readBit() throws IOException {
if (bufferPos < 0 || bufferPos > 7) {
if (bufferPos > 7 || bufferPos < 0) {
buffer = in.read();
if (buffer == -1) {
@@ -478,21 +483,10 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
bufferPos = 0;
}
boolean isSet;
if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) {
isSet = ((buffer >> (7 - bufferPos)) & 1) == 1;
}
else {
isSet = ((buffer >> (bufferPos)) & 1) == 1;
}
boolean isSet = (buffer & 0x80) != 0;
buffer <<= 1;
bufferPos++;
if (bufferPos > 7) {
bufferPos = -1;
}
return isSet;
}
@@ -30,6 +30,22 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader.guessPhotometricInterpretation;
import java.lang.reflect.Array;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.twelvemonkeys.imageio.AbstractMetadata;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
@@ -38,18 +54,6 @@ import com.twelvemonkeys.imageio.metadata.tiff.Rational;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
import com.twelvemonkeys.lang.Validate;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import java.lang.reflect.Array;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* TIFFImageMetadata.
@@ -84,7 +88,7 @@ public final class TIFFImageMetadata extends AbstractMetadata {
* or {@link #mergeTree(String, Node)} methods.
*/
public TIFFImageMetadata(final Directory ifd) {
super(true, TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, TIFFMedataFormat.class.getName(), null, null);
super(true, TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, TIFFImageMetadataFormat.class.getName(), null, null);
this.ifd = Validate.notNull(ifd, "IFD");
this.original = ifd;
}
@@ -96,7 +100,7 @@ public final class TIFFImageMetadata extends AbstractMetadata {
* {@link #setFromTree(String, Node)}
* or {@link #mergeTree(String, Node)} methods.
*/
public TIFFImageMetadata(final Collection<Entry> entries) {
public TIFFImageMetadata(final Collection<? extends Entry> entries) {
this(new IFD(entries));
}
@@ -192,7 +196,12 @@ public final class TIFFImageMetadata extends AbstractMetadata {
elementNode.setAttribute("value", String.valueOf((Short) value & 0xFFFF));
}
else if (unsigned && value instanceof Integer) {
elementNode.setAttribute("value", String.valueOf((Integer) value & 0xFFFFFFFFl));
elementNode.setAttribute("value", String.valueOf((Integer) value & 0xFFFFFFFFL));
}
else if (value instanceof Rational) {
// For compatibility with JAI format, we need denominator
String rational = String.valueOf(value);
elementNode.setAttribute("value", rational.indexOf('/') < 0 && !"NaN".equals(rational) ? rational + "/1" : rational);
}
else {
elementNode.setAttribute("value", String.valueOf(value));
@@ -353,8 +362,7 @@ public final class TIFFImageMetadata extends AbstractMetadata {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
// Handle ColorSpaceType (RGB/CMYK/YCbCr etc)...
Entry photometricTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
int photometricValue = getValueAsInt(photometricTag); // No default for this tag!
int photometricValue = getPhotometricInterpretationWithFallback(); // No default for this tag!
int numChannelsValue = getSamplesPerPixelWithFallback();
IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType");
@@ -445,6 +453,13 @@ public final class TIFFImageMetadata extends AbstractMetadata {
return chroma;
}
private int getPhotometricInterpretationWithFallback() {
Entry photometricTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
return photometricTag != null ? getValueAsInt(photometricTag)
: guessPhotometricInterpretation(getCompression(), getSamplesPerPixelWithFallback(), ifd.getEntryById(TIFF.TAG_EXTRA_SAMPLES), ifd.getEntryById(TIFF.TAG_COLOR_MAP));
}
private int getSamplesPerPixelWithFallback() {
// SamplePerPixel defaults to 1, but we'll check BitsPerSample to be sure
Entry samplesPerPixelTag = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
@@ -455,15 +470,19 @@ public final class TIFFImageMetadata extends AbstractMetadata {
: bitsPerSampleTag != null ? bitsPerSampleTag.valueCount() : 1;
}
private int getCompression() {
Entry compressionTag = ifd.getEntryById(TIFF.TAG_COMPRESSION);
return compressionTag == null
? TIFFBaseline.COMPRESSION_NONE
: getValueAsInt(compressionTag);
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode compression = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = addChildNode(compression, "CompressionTypeName", null);
Entry compressionTag = ifd.getEntryById(TIFF.TAG_COMPRESSION);
int compressionValue = compressionTag == null
? TIFFBaseline.COMPRESSION_NONE
: getValueAsInt(compressionTag);
int compressionValue = getCompression();
// Naming is identical to JAI ImageIO metadata as far as possible
switch (compressionValue) {
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2016, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormatImpl;
/**
* TIFFImageMetadataFormat.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: TIFFImageMetadataFormat.java,v 1.0 17/04/15 harald.kuhr Exp$
*/
public final class TIFFImageMetadataFormat extends IIOMetadataFormatImpl {
private static final TIFFImageMetadataFormat INSTANCE = new TIFFImageMetadataFormat();
// We'll reuse the metadata formats defined for JAI
public static final String SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME = "com_sun_media_imageio_plugins_tiff_image_1.0";
public TIFFImageMetadataFormat() {
super(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, CHILD_POLICY_SOME);
// TODO: Implement!
}
@Override
public boolean canNodeAppear(String elementName, ImageTypeSpecifier imageType) {
return true;
}
public static TIFFImageMetadataFormat getInstance() {
return INSTANCE;
}
}
@@ -33,6 +33,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.CIELabColorConverter;
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
@@ -164,6 +165,7 @@ public final class TIFFImageReader extends ImageReaderBase {
private CompoundDirectory IFDs;
private Directory currentIFD;
private int overrideCCITTCompression = -1;
TIFFImageReader(final ImageReaderSpi provider) {
super(provider);
@@ -173,6 +175,7 @@ public final class TIFFImageReader extends ImageReaderBase {
protected void resetMembers() {
IFDs = null;
currentIFD = null;
overrideCCITTCompression = -1;
}
private void readMetadata() throws IOException {
@@ -377,6 +380,7 @@ public final class TIFFImageReader extends ImageReaderBase {
readMetadata();
checkBounds(imageIndex);
currentIFD = IFDs.getDirectory(imageIndex);
overrideCCITTCompression = -1; // Reset override for next image
}
@Override
@@ -699,40 +703,49 @@ public final class TIFFImageReader extends ImageReaderBase {
private int getPhotometricInterpretationWithFallback() throws IIOException {
// PhotometricInterpretation is a required tag, but as it can be guessed this does a fallback that is similar to JAI ImageIO.
int interpretation = getValueAsIntWithDefault(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation", -1);
if (interpretation == -1) {
int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE);
int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXEL, 1);
Entry extraSamplesEntry = currentIFD.getEntryById(TIFF.TAG_EXTRA_SAMPLES);
int extraSamples = extraSamplesEntry == null ? 0 : extraSamplesEntry.valueCount();
Entry extraSamples = currentIFD.getEntryById(TIFF.TAG_EXTRA_SAMPLES);
Entry colorMap = currentIFD.getEntryById(TIFF.TAG_COLOR_MAP);
interpretation = guessPhotometricInterpretation(compression, samplesPerPixel, extraSamples, colorMap);
if (compression == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE
|| compression == TIFFExtension.COMPRESSION_CCITT_T4
|| compression == TIFFExtension.COMPRESSION_CCITT_T6) {
interpretation = TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO;
}
else if (currentIFD.getEntryById(TIFF.TAG_COLOR_MAP) != null) {
interpretation = TIFFBaseline.PHOTOMETRIC_PALETTE;
}
else if ((samplesPerPixel - extraSamples) == 3) {
if (compression == TIFFExtension.COMPRESSION_JPEG
|| compression == TIFFExtension.COMPRESSION_OLD_JPEG) {
interpretation = TIFFExtension.PHOTOMETRIC_YCBCR;
}
else {
interpretation = TIFFBaseline.PHOTOMETRIC_RGB;
}
}
else if ((samplesPerPixel - extraSamples) == 4) {
interpretation = TIFFExtension.PHOTOMETRIC_SEPARATED;
}
else {
interpretation = TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
}
processWarningOccurred("Missing PhotometricInterpretation, determining fallback: " + interpretation);
}
return interpretation;
}
static int guessPhotometricInterpretation(int compression, int samplesPerPixel, Entry extraSamples, Entry colorMap) {
int extraSamplesCount = extraSamples == null ? 0 : extraSamples.valueCount();
if (compression == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE
|| compression == TIFFExtension.COMPRESSION_CCITT_T4
|| compression == TIFFExtension.COMPRESSION_CCITT_T6) {
return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO;
}
else if (colorMap != null) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
else if ((samplesPerPixel - extraSamplesCount) == 3) {
if (compression == TIFFExtension.COMPRESSION_JPEG
|| compression == TIFFExtension.COMPRESSION_OLD_JPEG) {
return TIFFExtension.PHOTOMETRIC_YCBCR;
}
else {
return TIFFBaseline.PHOTOMETRIC_RGB;
}
}
else if ((samplesPerPixel - extraSamplesCount) == 4) {
return TIFFExtension.PHOTOMETRIC_SEPARATED;
}
else {
return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
}
}
private int getOpaqueSamplesPerPixel(final int photometricInterpretation) throws IIOException {
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
@@ -1040,9 +1053,10 @@ public final class TIFFImageReader extends ImageReaderBase {
// General uncompressed/compressed reading
int bands = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR ? rawType.getNumBands() : 1;
int fillOrder = getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, TIFFBaseline.FILL_LEFT_TO_RIGHT);
int bitsPerSample = getBitsPerSample();
boolean needsBitPadding = bitsPerSample > 16 && bitsPerSample % 16 != 0 || bitsPerSample > 8 && bitsPerSample % 8 != 0 || bitsPerSample == 6;
boolean needsAdapter = compression != TIFFBaseline.COMPRESSION_NONE
boolean needsAdapter = compression != TIFFBaseline.COMPRESSION_NONE || fillOrder != TIFFBaseline.FILL_LEFT_TO_RIGHT
|| interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || needsBitPadding;
for (int y = 0; y < tilesDown; y++) {
@@ -1059,7 +1073,7 @@ public final class TIFFImageReader extends ImageReaderBase {
DataInput input;
if (!needsAdapter) {
// No need for transformation, fast forward
// No need for transformation, fast-forward
input = imageInput;
}
else {
@@ -1067,6 +1081,7 @@ public final class TIFFImageReader extends ImageReaderBase {
? createStreamAdapter(imageInput, stripTileByteCounts[i])
: createStreamAdapter(imageInput);
adapter = createFillOrderStream(fillOrder, adapter);
adapter = createDecompressorStream(compression, stripTileWidth, numBands, adapter);
adapter = createUnpredictorStream(predictor, stripTileWidth, numBands, bitsPerSample, adapter, imageInput.getByteOrder());
@@ -2136,7 +2151,8 @@ public final class TIFFImageReader extends ImageReaderBase {
&& (referenceBW == null || Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT))) {
// Fast, default conversion
for (int i = 0; i < data.length; i += 3) {
YCbCrConverter.convertYCbCr2RGB(data, data, i);
// TODO: The default is likely neither JPEG or rec 601, as the reference B/W doesn't match...
YCbCrConverter.convertJPEGYCbCr2RGB(data, data, i);
}
}
else {
@@ -2310,26 +2326,28 @@ public final class TIFFImageReader extends ImageReaderBase {
return (short) Math.max(0, Math.min(0xffff, val));
}
private InputStream createDecompressorStream(final int compression, final int width, final int bands, final InputStream stream) throws IOException {
int fillOrder = getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1);
private InputStream createDecompressorStream(final int compression, final int width, final int bands, InputStream stream) throws IOException {
switch (compression) {
case TIFFBaseline.COMPRESSION_NONE:
return stream;
case TIFFBaseline.COMPRESSION_PACKBITS:
return new DecoderStream(createFillOrderStream(fillOrder, stream), new PackBitsDecoder(), 256);
return new DecoderStream(stream, new PackBitsDecoder(), 256);
case TIFFExtension.COMPRESSION_LZW:
// NOTE: Needs large buffer for compatibility with certain encoders
return new DecoderStream(createFillOrderStream(fillOrder, stream), LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), Math.max(width * bands, 4096));
return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), Math.max(width * bands, 4096));
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
// TIFF specification, supplement 2 says ZLIB (8) and DEFLATE (32946) algorithms are identical
case TIFFCustom.COMPRESSION_PIXTIFF_ZIP:
return new InflaterInputStream(createFillOrderStream(fillOrder, stream), new Inflater(), 1024);
return new InflaterInputStream(stream, new Inflater(), 1024);
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
case TIFFExtension.COMPRESSION_CCITT_T4:
case TIFFExtension.COMPRESSION_CCITT_T6:
return new CCITTFaxDecoderStream(stream, width, findCCITTType(compression, stream), fillOrder, getCCITTOptions(compression), compression == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE);
// TODO: Find a better way to test for incorrect CCITT type ONCE per IFD
if (overrideCCITTCompression == -1) {
overrideCCITTCompression = findCCITTType(compression, stream);
}
return new CCITTFaxDecoderStream(stream, width, overrideCCITTCompression, getCCITTOptions(compression), compression == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE);
default:
throw new IllegalArgumentException("Unsupported TIFF compression: " + compression);
}
@@ -2476,24 +2494,12 @@ public final class TIFFImageReader extends ImageReaderBase {
return value;
}
private ICC_Profile getICCProfile() throws IOException {
private ICC_Profile getICCProfile() {
Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE);
if (entry != null) {
byte[] value = (byte[]) entry.getValue();
// Validate ICC profile size vs actual value size
int size = (value[0] & 0xff) << 24 | (value[1] & 0xff) << 16 | (value[2] & 0xff) << 8 | (value[3] & 0xff);
if (size < 0 || size > value.length) {
processWarningOccurred("Ignoring truncated ICC profile: Bad ICC profile size (" + size + ")");
return null;
}
try {
// WEIRDNESS: Reading profile from InputStream is somehow more compatible
// than reading from byte array (chops off extra bytes + validates profile).
ICC_Profile profile = ICC_Profile.getInstance(new ByteArrayInputStream(value));
return ColorSpaces.validateProfile(profile);
return ColorProfiles.createProfile((byte[]) entry.getValue());
}
catch (CMMException | IllegalArgumentException e) {
processWarningOccurred("Ignoring broken/incompatible ICC profile: " + e.getMessage());
@@ -32,6 +32,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.Rational;
@@ -63,6 +64,7 @@ import java.util.*;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFStreamMetadata.configureStreamByteOrder;
/**
@@ -202,26 +204,26 @@ public final class TIFFImageWriter extends ImageWriterBase {
long streamPosition = imageOutput.getStreamPosition();
long ifdSize = tiffWriter.computeIFDSize(entries.values());
long stripOffset = streamPosition + 4 + ifdSize + 4;
long stripOffset = streamPosition + tiffWriter.offsetSize() + ifdSize + tiffWriter.offsetSize();
long stripByteCount = ((long) renderedImage.getWidth() * renderedImage.getHeight() * pixelSize + 7L) / 8L;
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, TIFF.TYPE_LONG, stripOffset));
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, TIFF.TYPE_LONG, stripByteCount));
long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags
long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes care of ordering tags
nextIFDPointerOffset = imageOutput.getStreamPosition();
// If we have a previous IFD, update pointer
if (streamPosition > lastIFDPointerOffset) {
imageOutput.seek(lastIFDPointerOffset);
imageOutput.writeInt((int) ifdPointer);
tiffWriter.writeOffset(imageOutput, ifdPointer);
imageOutput.seek(nextIFDPointerOffset);
}
imageOutput.writeInt(0); // Update next IFD pointer later
tiffWriter.writeOffset(imageOutput, 0); // Update next IFD pointer later
}
else {
imageOutput.writeInt(0); // Update current IFD pointer later
tiffWriter.writeOffset(imageOutput, 0); // Update current IFD pointer later
}
long stripOffset = imageOutput.getStreamPosition();
@@ -260,7 +262,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, TIFF.TYPE_LONG, stripOffset));
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, TIFF.TYPE_LONG, stripByteCount));
long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags
long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes care of ordering tags
nextIFDPointerOffset = imageOutput.getStreamPosition();
@@ -268,10 +270,10 @@ public final class TIFFImageWriter extends ImageWriterBase {
// However, need to update here, because to the writeIFD method writes the pointer, but at the incorrect offset
// TODO: Refactor writeIFD to take an offset
imageOutput.seek(lastIFDPointerOffset);
imageOutput.writeInt((int) ifdPointer);
tiffWriter.writeOffset(imageOutput, ifdPointer);
imageOutput.seek(nextIFDPointerOffset);
imageOutput.writeInt(0); // Next IFD pointer updated later
tiffWriter.writeOffset(imageOutput, 0); // Next IFD pointer updated later
}
return nextIFDPointerOffset;
@@ -737,8 +739,8 @@ public final class TIFFImageWriter extends ImageWriterBase {
TIFFImageMetadata outData = new TIFFImageMetadata(Collections.<Entry>emptySet());
try {
if (Arrays.asList(inData.getMetadataFormatNames()).contains(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)) {
outData.setFromTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, inData.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME));
if (Arrays.asList(inData.getMetadataFormatNames()).contains(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)) {
outData.setFromTree(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, inData.getAsTree(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME));
}
else if (inData.isStandardMetadataFormatSupported()) {
outData.setFromTree(IIOMetadataFormatImpl.standardMetadataFormatName, inData.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName));
@@ -849,9 +851,11 @@ public final class TIFFImageWriter extends ImageWriterBase {
else {
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands));
// Note: Assuming sRGB to be the default RGB interpretation
// Embed ICC profile if we have one that:
// * is not sRGB (assuming sRGB to be the default RGB interpretation), and
// * is not gray scale (assuming photometric either BlackIsZero or WhiteIsZero)
ColorSpace colorSpace = colorModel.getColorSpace();
if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) {
if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB() && !ColorProfiles.isCS_GRAY(((ICC_ColorSpace) colorSpace).getProfile())) {
entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
}
}
@@ -955,11 +959,15 @@ public final class TIFFImageWriter extends ImageWriterBase {
configureStreamByteOrder(streamMetadata, imageOutput);
writingSequence = true;
sequenceTIFFWriter = new TIFFWriter();
sequenceTIFFWriter = new TIFFWriter(isBigTIFF() ? 8 : 4);
sequenceTIFFWriter.writeTIFFHeader(imageOutput);
sequenceLastIFDPos = imageOutput.getStreamPosition();
}
private boolean isBigTIFF() throws IOException {
return "bigtiff".equalsIgnoreCase(getFormatName());
}
@Override
public void writeToSequence(final IIOImage image, final ImageWriteParam param) throws IOException {
if (!writingSequence) {
@@ -1015,8 +1023,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
BufferedImage original;
// BufferedImage original = ImageIO.read(file);
ImageInputStream inputStream = ImageIO.createImageInputStream(file);
try {
try (ImageInputStream inputStream = ImageIO.createImageInputStream(file)) {
Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream);
if (!readers.hasNext()) {
@@ -1046,9 +1053,6 @@ public final class TIFFImageWriter extends ImageWriterBase {
original = reader.read(0, param);
}
finally {
inputStream.close();
}
System.err.println("original: " + original);
@@ -1084,12 +1088,11 @@ public final class TIFFImageWriter extends ImageWriterBase {
// output.deleteOnExit();
System.err.println("output: " + output);
TIFFImageWriter writer = new TIFFImageWriter(null);
TIFFImageWriter writer = new TIFFImageWriter(new TIFFImageWriterSpi());
// ImageWriter writer = ImageIO.getImageWritersByFormatName("PNG").next();
// ImageWriter writer = ImageIO.getImageWritersByFormatName("BMP").next();
ImageOutputStream stream = ImageIO.createImageOutputStream(output);
try {
try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) {
writer.setOutput(stream);
ImageWriteParam param = writer.getDefaultWriteParam();
@@ -1107,9 +1110,6 @@ public final class TIFFImageWriter extends ImageWriterBase {
writer.write(null, new IIOImage(image, null, null), param);
System.err.println("Write time: " + (System.currentTimeMillis() - start) + " ms");
}
finally {
stream.close();
}
System.err.println("output.length: " + output.length());

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