Issue #895 Correct handling CMYK JPEG images (Monte Video library)

Correct JPEG to Image conversion when it has alpha
This commit is contained in:
Jindra Petřík
2015-05-26 20:01:22 +02:00
parent a54392e3f5
commit 8074aa5fd8
20 changed files with 3641 additions and 120 deletions

2
.gitignore vendored
View File

@@ -58,3 +58,5 @@ hs_err_pid*.log
/version.properties
/tools.properties
/nbproject/private/
/libsrc/cmykjpeg/build/
/libsrc/cmykjpeg/nbproject/private/

BIN
lib/cmykjpeg.jar Normal file

Binary file not shown.

73
libsrc/cmykjpeg/build.xml Normal file
View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- You may freely edit this file. See commented blocks below for -->
<!-- some examples of how to customize the build. -->
<!-- (If you delete it and reopen the project it will be recreated.) -->
<!-- By default, only the Clean and Build commands use this build script. -->
<!-- Commands such as Run, Debug, and Test only use this build script if -->
<!-- the Compile on Save feature is turned off for the project. -->
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
<!-- in the project's Project Properties dialog box.-->
<project name="cmykjpeg" default="default" basedir=".">
<description>Builds, tests, and runs the project cmykjpeg.</description>
<import file="nbproject/build-impl.xml"/>
<!--
There exist several targets which are by default empty and which can be
used for execution of your tasks. These targets are usually executed
before and after some main targets. They are:
-pre-init: called before initialization of project properties
-post-init: called after initialization of project properties
-pre-compile: called before javac compilation
-post-compile: called after javac compilation
-pre-compile-single: called before javac compilation of single file
-post-compile-single: called after javac compilation of single file
-pre-compile-test: called before javac compilation of JUnit tests
-post-compile-test: called after javac compilation of JUnit tests
-pre-compile-test-single: called before javac compilation of single JUnit test
-post-compile-test-single: called after javac compilation of single JUunit test
-pre-jar: called before JAR building
-post-jar: called after JAR building
-post-clean: called after cleaning build products
(Targets beginning with '-' are not intended to be called on their own.)
Example of inserting an obfuscator after compilation could look like this:
<target name="-post-compile">
<obfuscate>
<fileset dir="${build.classes.dir}"/>
</obfuscate>
</target>
For list of available properties check the imported
nbproject/build-impl.xml file.
Another way to customize the build is by overriding existing main targets.
The targets of interest are:
-init-macrodef-javac: defines macro for javac compilation
-init-macrodef-junit: defines macro for junit execution
-init-macrodef-debug: defines macro for class debugging
-init-macrodef-java: defines macro for class execution
-do-jar: JAR building
run: execution of project
-javadoc-build: Javadoc generation
test-report: JUnit report generation
An example of overriding the target for project execution could look like this:
<target name="run" depends="cmykjpeg-impl.jar">
<exec dir="bin" executable="launcher.exe">
<arg file="${dist.jar}"/>
</exec>
</target>
Notice that the overridden target depends on the jar target and not only on
the compile target as the regular run target does. Again, for a list of available
properties which you can use, check the target you are overriding in the
nbproject/build-impl.xml file.
-->
</project>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
build.xml.data.CRC32=73f9688e
build.xml.script.CRC32=22af611f
build.xml.stylesheet.CRC32=8064a381@1.75.2.48
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
nbproject/build-impl.xml.data.CRC32=73f9688e
nbproject/build-impl.xml.script.CRC32=de1179e8
nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48

View File

@@ -0,0 +1,73 @@
annotation.processing.enabled=true
annotation.processing.enabled.in.editor=false
annotation.processing.processors.list=
annotation.processing.run.all.processors=true
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
application.title=cmykjpeg
application.vendor=JPEXS
build.classes.dir=${build.dir}/classes
build.classes.excludes=**/*.java,**/*.form
# This directory is removed when the project is cleaned:
build.dir=build
build.generated.dir=${build.dir}/generated
build.generated.sources.dir=${build.dir}/generated-sources
# Only compile against the classpath explicitly listed here:
build.sysclasspath=ignore
build.test.classes.dir=${build.dir}/test/classes
build.test.results.dir=${build.dir}/test/results
# Uncomment to specify the preferred debugger connection transport:
#debug.transport=dt_socket
debug.classpath=\
${run.classpath}
debug.test.classpath=\
${run.test.classpath}
# Files in build.classes.dir which should be excluded from distribution jar
dist.archive.excludes=
# This directory is removed when the project is cleaned:
dist.dir=dist
dist.jar=../../lib/cmykjpeg.jar
dist.javadoc.dir=${dist.dir}/javadoc
endorsed.classpath=
excludes=
includes=**
jar.compress=false
javac.classpath=
# Space-separated list of extra javac options
javac.compilerargs=
javac.deprecation=false
javac.processorpath=\
${javac.classpath}
javac.source=1.8
javac.target=1.8
javac.test.classpath=\
${javac.classpath}:\
${build.classes.dir}
javac.test.processorpath=\
${javac.test.classpath}
javadoc.additionalparam=
javadoc.author=false
javadoc.encoding=${source.encoding}
javadoc.noindex=false
javadoc.nonavbar=false
javadoc.notree=false
javadoc.private=false
javadoc.splitindex=true
javadoc.use=true
javadoc.version=false
javadoc.windowtitle=
meta.inf.dir=${src.dir}/META-INF
mkdist.disabled=true
platform.active=default_platform
run.classpath=\
${javac.classpath}:\
${build.classes.dir}
# Space-separated list of JVM arguments used when running the project.
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
# To set system properties for unit tests define test-sys-prop.name=value:
run.jvmargs=
run.test.classpath=\
${javac.test.classpath}:\
${build.test.classes.dir}
source.encoding=UTF-8
src.dir=src
test.src.dir=test

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.java.j2seproject</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
<name>cmykjpeg</name>
<source-roots>
<root id="src.dir"/>
</source-roots>
<test-roots>
<root id="test.src.dir"/>
</test-roots>
</data>
</configuration>
</project>

View File

@@ -0,0 +1,216 @@
/*
* @(#)ByteArrayImageInputStream.java
*
* Copyright (c) 2008-2011 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.io;
import java.io.IOException;
import java.nio.ByteOrder;
/**
* A {@code ByteArrayImageInputStream} contains
* an internal buffer that contains bytes that
* may be read from the stream. An internal
* counter keeps track of the next byte to
* be supplied by the {@code read} method.
* <p>
* Closing a {@code ByteArrayImageInputStream} has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an {@code IOException}.
*
* @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau
* @version $Id: ByteArrayImageInputStream.java 299 2013-01-03 07:40:18Z werner $
*/
public class ByteArrayImageInputStream extends ImageInputStreamImpl2 {
/**
* An array of bytes that was provided
* by the creator of the stream. Elements <code>buf[0]</code>
* through <code>buf[count-1]</code> are the
* only bytes that can ever be read from the
* stream; element <code>buf[streamPos]</code> is
* the next byte to be read.
*/
protected byte buf[];
/**
* The index one greater than the last valid character in the input
* stream buffer.
* This value should always be nonnegative
* and not larger than the length of <code>buf</code>.
* It is one greater than the position of
* the last byte within <code>buf</code> that
* can ever be read from the input stream buffer.
*/
protected int count;
/** The offset to the start of the array. */
private final int arrayOffset;
public ByteArrayImageInputStream(byte[] buf) {
this(buf, ByteOrder.BIG_ENDIAN);
}
public ByteArrayImageInputStream(byte[] buf, ByteOrder byteOrder) {
this(buf, 0, buf.length, byteOrder);
}
public ByteArrayImageInputStream(byte[] buf, int offset, int length, ByteOrder byteOrder) {
this.buf = buf;
this.streamPos = offset;
this.count = Math.min(offset + length, buf.length);
this.arrayOffset = offset;
this.byteOrder = byteOrder;
}
/**
* Reads the next byte of data from this input stream. The value
* byte is returned as an <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned.
* <p>
* This <code>read</code> method
* cannot block.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream has been reached.
*/
@Override
public synchronized int read() {
flushBits();
return (streamPos < count) ? (buf[(int)(streamPos++)] & 0xff) : -1;
}
/**
* Reads up to <code>len</code> bytes of data into an array of bytes
* from this input stream.
* If <code>streamPos</code> equals <code>count</code>,
* then <code>-1</code> is returned to indicate
* end of file. Otherwise, the number <code>k</code>
* of bytes read is equal to the smaller of
* <code>len</code> and <code>count-streamPos</code>.
* If <code>k</code> is positive, then bytes
* <code>buf[streamPos]</code> through <code>buf[streamPos+k-1]</code>
* are copied into <code>b[off]</code> through
* <code>b[off+k-1]</code> in the manner performed
* by <code>System.arraycopy</code>. The
* value <code>k</code> is added into <code>streamPos</code>
* and <code>k</code> is returned.
* <p>
* This <code>read</code> method cannot block.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
*/
@Override
public synchronized int read(byte b[], int off, int len) {
flushBits();
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
}
if (streamPos >= count) {
return -1;
}
if (streamPos + len > count) {
len = (int)(count - streamPos);
}
if (len <= 0) {
return 0;
}
System.arraycopy(buf, (int)streamPos, b, off, len);
streamPos += len;
return len;
}
/**
* Skips <code>n</code> bytes of input from this input stream. Fewer
* bytes might be skipped if the end of the input stream is reached.
* The actual number <code>k</code>
* of bytes to be skipped is equal to the smaller
* of <code>n</code> and <code>count-streamPos</code>.
* The value <code>k</code> is added into <code>streamPos</code>
* and <code>k</code> is returned.
*
* @param n the number of bytes to be skipped.
* @return the actual number of bytes skipped.
*/
public synchronized long skip(long n) {
if (streamPos + n > count) {
n = count - streamPos;
}
if (n < 0) {
return 0;
}
streamPos += n;
return n;
}
/**
* Returns the number of remaining bytes that can be read (or skipped over)
* from this input stream.
* <p>
* The value returned is <code>count&nbsp;- streamPos</code>,
* which is the number of bytes remaining to be read from the input buffer.
*
* @return the number of remaining bytes that can be read (or skipped
* over) from this input stream without blocking.
*/
public synchronized int available() {
return (int)(count - streamPos);
}
/**
* Closing a <tt>ByteArrayInputStream</tt> has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an <tt>IOException</tt>.
* <p>
*/
@Override
public void close() {
// does nothing!!
}
@Override
public long getStreamPosition() throws IOException {
checkClosed();
return streamPos-arrayOffset;
}
@Override
public void seek(long pos) throws IOException {
checkClosed();
flushBits();
// This test also covers pos < 0
if (pos < flushedPos) {
throw new IndexOutOfBoundsException("pos < flushedPos!");
}
this.streamPos = pos+arrayOffset;
}
private void flushBits() {
bitOffset=0;
}
@Override
public long length() {
return count-arrayOffset;
}
}

View File

@@ -0,0 +1,189 @@
/*
* @(#)ImageInputStreamAdapter.java 1.0 2009-12-17
*
* Copyright (c) 2009 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.io;
import java.io.FilterInputStream;
import java.io.IOException;
import javax.imageio.stream.ImageInputStream;
/**
* ImageInputStreamAdapter.
*
* @author Werner Randelshofer
* @version 1.0 2009-12-17 Created.
*/
public class ImageInputStreamAdapter extends FilterInputStream {
private ImageInputStream iis;
public ImageInputStreamAdapter(ImageInputStream iis) {
super(null);
this.iis=iis;
}
/**
* Reads the next byte of data from this input stream. The value
* byte is returned as an <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned. This method blocks until input data
* is available, the end of the stream is detected, or an exception
* is thrown.
* <p>
* This method
* simply performs <code>in.read()</code> and returns the result.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream is reached.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
@Override
public int read() throws IOException {
return iis.read();
}
/**
* Reads up to <code>len</code> bytes of data from this input stream
* into an array of bytes. If <code>len</code> is not zero, the method
* blocks until some input is available; otherwise, no
* bytes are read and <code>0</code> is returned.
* <p>
* This method simply performs <code>in.read(b, off, len)</code>
* and returns the result.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
* @exception IOException if an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
@Override
public int read(byte b[], int off, int len) throws IOException {
return iis.read(b, off, len);
}
/**
* {@inheritDoc}
* <p>
* This method simply performs <code>in.skip(n)</code>.
*/
@Override
public long skip(long n) throws IOException {
return iis.skipBytes(n);
}
/**
* Returns an estimate of the number of bytes that can be read (or
* skipped over) from this input stream without blocking by the next
* caller of a method for this input stream. The next caller might be
* the same thread or another thread. A single read or skip of this
* many bytes will not block, but may read or skip fewer bytes.
* <p>
* This method returns the result of {@link #in in}.available().
*
* @return an estimate of the number of bytes that can be read (or skipped
* over) from this input stream without blocking.
* @exception IOException if an I/O error occurs.
*/
@Override
public int available() throws IOException {
return (iis.isCached()) ? //
(int)Math.min(Integer.MAX_VALUE, iis.length() - iis.getStreamPosition()) :
0;
}
/**
* Closes this input stream and releases any system resources
* associated with the stream.
* This
* method simply performs <code>in.close()</code>.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
@Override
public void close() throws IOException {
iis.close();
}
/**
* Marks the current position in this input stream. A subsequent
* call to the <code>reset</code> method repositions this stream at
* the last marked position so that subsequent reads re-read the same bytes.
* <p>
* The <code>readlimit</code> argument tells this input stream to
* allow that many bytes to be read before the mark position gets
* invalidated.
* <p>
* This method simply performs <code>in.mark(readlimit)</code>.
*
* @param readlimit the maximum limit of bytes that can be read before
* the mark position becomes invalid.
* @see java.io.FilterInputStream#in
* @see java.io.FilterInputStream#reset()
*/
@Override
public synchronized void mark(int readlimit) {
iis.mark();
}
/**
* Repositions this stream to the position at the time the
* <code>mark</code> method was last called on this input stream.
* <p>
* This method
* simply performs <code>in.reset()</code>.
* <p>
* Stream marks are intended to be used in
* situations where you need to read ahead a little to see what's in
* the stream. Often this is most easily done by invoking some
* general parser. If the stream is of the type handled by the
* parse, it just chugs along happily. If the stream is not of
* that type, the parser should toss an exception when it fails.
* If this happens within readlimit bytes, it allows the outer
* code to reset the stream and try another parser.
*
* @exception IOException if the stream has not been marked or if the
* mark has been invalidated.
* @see java.io.FilterInputStream#in
* @see java.io.FilterInputStream#mark(int)
*/
@Override
public synchronized void reset() throws IOException {
iis.reset();
}
/**
* Tests if this input stream supports the <code>mark</code>
* and <code>reset</code> methods.
* This method
* simply performs <code>in.markSupported()</code>.
*
* @return <code>true</code> if this stream type supports the
* <code>mark</code> and <code>reset</code> method;
* <code>false</code> otherwise.
* @see java.io.FilterInputStream#in
* @see java.io.InputStream#mark(int)
* @see java.io.InputStream#reset()
*/
@Override
public boolean markSupported() {
return true;
}
}

View File

@@ -0,0 +1,65 @@
/*
* @(#)ImageInputStreamImpl2.java
*
* Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.io;
import java.io.IOException;
import java.nio.ByteOrder;
import javax.imageio.stream.ImageInputStreamImpl;
/**
* {@code ImageInputStreamImpl2} fixes bugs in ImageInputStreamImpl.
* <p>
* ImageInputStreamImpl uses read(byte[]) instead of readFully(byte[]) inside of
* readShort. This results in corrupt data input if the underlying stream can
* not fulfill the read operation in a single step.
*
* @author Werner Randelshofer
* @version $Id: ImageInputStreamImpl2.java 299 2013-01-03 07:40:18Z werner $
*/
public abstract class ImageInputStreamImpl2 extends ImageInputStreamImpl {
// Length of the buffer used for readFully(type[], int, int)
private static final int BYTE_BUF_LENGTH = 8192;
/**
* Byte buffer used for readFully(type[], int, int). Note that this
* array is also used for bulk reads in readShort(), readInt(), etc, so
* it should be large enough to hold a primitive value (i.e. >= 8 bytes).
* Also note that this array is package protected, so that it can be
* used by ImageOutputStreamImpl in a similar manner.
*/
byte[] byteBuf = new byte[BYTE_BUF_LENGTH];
@Override
public short readShort() throws IOException {
readFully(byteBuf, 0, 2);
if (byteOrder == ByteOrder.BIG_ENDIAN) {
return (short)
(((byteBuf[0] & 0xff) << 8) | ((byteBuf[1] & 0xff) << 0));
} else {
return (short)
(((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0));
}
}
public int readInt() throws IOException {
readFully(byteBuf, 0, 4);
if (byteOrder == ByteOrder.BIG_ENDIAN) {
return
(((byteBuf[0] & 0xff) << 24) | ((byteBuf[1] & 0xff) << 16) |
((byteBuf[2] & 0xff) << 8) | ((byteBuf[3] & 0xff) << 0));
} else {
return
(((byteBuf[3] & 0xff) << 24) | ((byteBuf[2] & 0xff) << 16) |
((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0));
}
}
}

View File

@@ -0,0 +1,823 @@
/*
* @(#)CMYJKJPEGImageReader.java
*
* Copyright (c) 2010-2011 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.jpeg;
import com.sun.imageio.plugins.jpeg.JPEGImageReader;
import java.util.Iterator;
import java.util.LinkedList;
import javax.imageio.metadata.IIOMetadata;
import org.monte.media.io.ByteArrayImageInputStream;
import org.monte.media.io.ImageInputStreamAdapter;
//import com.sun.imageio.plugins.jpeg.JPEGImageReader;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.*;
import static java.lang.Math.*;
/**
* Reads a JPEG image with colors in the CMYK color space.
*
* @author Werner Randelshofer
* @version $Id: CMYKJPEGImageReader.java 308 2013-01-06 11:24:06Z werner $
*/
public class CMYKJPEGImageReader extends ImageReader {
private boolean isIgnoreICCProfile = false;
private boolean isYCCKInversed = true;
private static DirectColorModel RGB = new DirectColorModel(24, 0xff0000, 0xff00, 0xff, 0x0);
/**
* When we read the header, we read the whole image.
*/
private BufferedImage image;
public CMYKJPEGImageReader(ImageReaderSpi originatingProvider) {
super(originatingProvider);
}
@Override
public int getNumImages(boolean allowSearch) throws IOException {
return 1;
}
@Override
public int getWidth(int imageIndex) throws IOException {
readHeader();
return image.getWidth();
}
@Override
public int getHeight(int imageIndex) throws IOException {
readHeader();
return image.getHeight();
}
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
readHeader();
LinkedList<ImageTypeSpecifier> l = new LinkedList<ImageTypeSpecifier>();
l.add(new ImageTypeSpecifier(RGB, RGB.createCompatibleSampleModel(image.getWidth(), image.getHeight())));
return l.iterator();
}
@Override
public IIOMetadata getStreamMetadata() throws IOException {
return null;
}
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
return null;
}
@Override
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
if (imageIndex > 0) {
throw new IndexOutOfBoundsException();
}
readHeader();
return image;
}
/**
* Reads the PGM header. Does nothing if the header has already been loaded.
*/
private void readHeader() throws IOException {
if (image == null) {
ImageInputStream iis = null;
Object in = getInput();
/* No need for JMF support in CMYKJPEGImageReader.
if (in instanceof Buffer) {
in = ((Buffer) in).getData();
}*/
if (in instanceof byte[]) {
iis = new ByteArrayImageInputStream((byte[]) in);
} else if (in instanceof ImageInputStream) {
iis = (ImageInputStream) in;
} else if (in instanceof InputStream) {
iis = new MemoryCacheImageInputStream((InputStream) in);
} else {
throw new IOException("Can't handle input of type " + in);
}
image = read(iis, isYCCKInversed, isIgnoreICCProfile);
}
}
/**
* @return the YCCKInversed property.
*/
public boolean isYCCKInversed() {
return isYCCKInversed;
}
/**
* @param newValue the new value
*/
public void setYCCKInversed(boolean newValue) {
this.isYCCKInversed = newValue;
}
public boolean isIgnoreICCProfile() {
return isIgnoreICCProfile;
}
public void setIgnoreICCProfile(boolean newValue) {
this.isIgnoreICCProfile = newValue;
}
public static BufferedImage read(ImageInputStream in, boolean inverseYCCKColors, boolean isIgnoreColorProfile) throws IOException {
// Seek to start of input stream
in.seek(0);
// Extract metadata from the JFIF stream.
// --------------------------------------
// In particular, we are interested into the following fields:
int samplePrecision = 0;
int numberOfLines = 0;
int numberOfSamplesPerLine = 0;
int numberOfComponentsInFrame = 0;
int app14AdobeColorTransform = 0;
ByteArrayOutputStream app2ICCProfile = new ByteArrayOutputStream();
// Browse for marker segments, and extract data from those
// which are of interest.
JFIFInputStream fifi = new JFIFInputStream(new ImageInputStreamAdapter(in));
for (JFIFInputStream.Segment seg = fifi.getNextSegment(); seg != null; seg = fifi.getNextSegment()) {
if (0xffc0 <= seg.marker && seg.marker <= 0xffc3
|| 0xffc5 <= seg.marker && seg.marker <= 0xffc7
|| 0xffc9 <= seg.marker && seg.marker <= 0xffcb
|| 0xffcd <= seg.marker && seg.marker <= 0xffcf) {
// SOF0 - SOF15: Start of Frame Header marker segment
DataInputStream dis = new DataInputStream(fifi);
samplePrecision = dis.readUnsignedByte();
numberOfLines = dis.readUnsignedShort();
numberOfSamplesPerLine = dis.readUnsignedShort();
numberOfComponentsInFrame = dis.readUnsignedByte();
// ...the rest of SOF header is not important to us.
// In fact, by encounterint a SOF header, we have reached
// the end of the metadata section we are interested in.
// Thus we can abort here.
break;
} else if (seg.marker == 0xffe2) {
// APP2: Application-specific marker segment
if (seg.length >= 26) {
DataInputStream dis = new DataInputStream(fifi);
// Check for 12-bytes containing the null-terminated string: "ICC_PROFILE".
if (dis.readLong() == 0x4943435f50524f46L && dis.readInt() == 0x494c4500) {
// Skip 2 bytes
dis.skipBytes(2);
// Read Adobe ICC_PROFILE int buffer. The profile is split up over
// multiple APP2 marker segments.
byte[] b = new byte[512];
for (int count = dis.read(b); count != -1; count = dis.read(b)) {
app2ICCProfile.write(b, 0, count);
}
}
}
} else if (seg.marker == 0xffee) {
// APP14: Application-specific marker segment
if (seg.length == 12) {
DataInputStream dis = new DataInputStream(fifi);
// Check for 6-bytes containing the null-terminated string: "Adobe".
if (dis.readInt() == 0x41646f62L && dis.readUnsignedShort() == 0x6500) {
int version = dis.readUnsignedByte();
int app14Flags0 = dis.readUnsignedShort();
int app14Flags1 = dis.readUnsignedShort();
app14AdobeColorTransform = dis.readUnsignedByte();
}
}
}
}
//fifi.close();
// Read the image data
BufferedImage img = null;
if (numberOfComponentsInFrame != 4) {
// Read image with YCC color encoding.
in.seek(0);
// img = readImageFromYCCorGray(in);
img = readRGBImageFromYCC(new ImageInputStreamAdapter(in), null);
} else if (numberOfComponentsInFrame == 4) {
// Try to instantiate an ICC_Profile from the app2ICCProfile
ICC_Profile profile = null;
if (!isIgnoreColorProfile && app2ICCProfile.size() > 0) {
try {
profile = ICC_Profile.getInstance(new ByteArrayInputStream(app2ICCProfile.toByteArray()));
} catch (Throwable ex) {
// icc profile is corrupt
ex.printStackTrace();
}
}
switch (app14AdobeColorTransform) {
case 0:
default:
// Read image with RGBA color encoding.
in.seek(0);
img = readRGBAImageFromRGBA(new ImageInputStreamAdapter(in), profile);
break;
case 1:
throw new IOException("YCbCr not supported");
case 2:
// Read image with inverted YCCK color encoding.
// FIXME - How do we determine from the JFIF file whether
// YCCK colors are inverted?
// We must have a color profile in order to perform a
// conersion from CMYK to RGB.
// I case none has been supplied, we create a default one here.
if (profile == null) {
profile = ICC_Profile.getInstance(CMYKJPEGImageReader.class.getResourceAsStream("Generic CMYK Profile.icc"));
}
in.seek(0);
if (inverseYCCKColors) {
img = readRGBImageFromInvertedYCCK(new ImageInputStreamAdapter(in), profile);
} else {
img = readRGBImageFromYCCK(new ImageInputStreamAdapter(in), profile);
}
break;
}
}
return img;
}
private static ImageReader createNativeJPEGReader() {
return new JPEGImageReader(new CMYKJPEGImageReaderSpi());
/*
for (Iterator<ImageReader> i =
ImageIO.getImageReadersByFormatName("jpeg"); i.hasNext();) {
ImageReader r = i.next();
if (!(r instanceof CMYKJPEGImageReader)
&& !r.getClass().getName().contains("CMYKJPEGImageReader")) {
return r;
}
}
return null;
*
*/
}
/**
* Reads a CMYK JPEG image from the provided InputStream, converting the
* colors to RGB using the provided CMYK ICC_Profile. The image data must be
* in the CMYK color space.
* <p>
* Use this method, if you have already determined that the input stream
* contains a CMYK JPEG image.
*
* @param in An InputStream, preferably an ImageInputStream, in the JPEG
* File Interchange Format (JFIF).
* @param cmykProfile An ICC_Profile for conversion from the CMYK color
* space to the RGB color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage containing the decoded image converted into the
* RGB color space.
* @throws java.io.IOException
*/
public static BufferedImage readRGBImageFromCMYK(InputStream in, ICC_Profile cmykProfile) throws IOException {
ImageInputStream inputStream = null;
ImageReader reader = createNativeJPEGReader();
inputStream = (in instanceof ImageInputStream) ? (ImageInputStream) in : ImageIO.createImageInputStream(in);
reader.setInput(inputStream);
Raster raster = reader.readRaster(0, null);
BufferedImage image = createRGBImageFromCMYK(raster, cmykProfile);
return image;
}
/**
* Reads a RGBA JPEG image from the provided InputStream, converting the
* colors to RGBA using the provided RGBA ICC_Profile. The image data must
* be in the RGBA color space.
* <p>
* Use this method, if you have already determined that the input stream
* contains a RGBA JPEG image.
*
* @param in An InputStream, preferably an ImageInputStream, in the JPEG
* File Interchange Format (JFIF).
* @param rgbaProfile An ICC_Profile for conversion from the RGBA color
* space to the RGBA color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage containing the decoded image converted into the
* RGB color space.
* @throws java.io.IOException
*/
public static BufferedImage readRGBAImageFromRGBA(InputStream in, ICC_Profile rgbaProfile) throws IOException {
ImageInputStream inputStream = null;
ImageReader reader = createNativeJPEGReader();
inputStream = (in instanceof ImageInputStream) ? (ImageInputStream) in : ImageIO.createImageInputStream(in);
reader.setInput(inputStream);
Raster raster = reader.readRaster(0, null);
BufferedImage image = createRGBAImageFromRGBA(raster, rgbaProfile);
return image;
}
public static BufferedImage readRGBImageFromRGB(InputStream in, ICC_Profile rgbaProfile) throws IOException {
ImageInputStream inputStream = null;
ImageReader reader = createNativeJPEGReader();
inputStream = (in instanceof ImageInputStream) ? (ImageInputStream) in : ImageIO.createImageInputStream(in);
reader.setInput(inputStream);
Raster raster = reader.readRaster(0, null);
BufferedImage image = createRGBImageFromRGB(raster, rgbaProfile);
return image;
}
public static BufferedImage readRGBImageFromYCC(InputStream in, ICC_Profile rgbaProfile) throws IOException {
ImageInputStream inputStream = null;
ImageReader reader = createNativeJPEGReader();
inputStream = (in instanceof ImageInputStream) ? (ImageInputStream) in : ImageIO.createImageInputStream(in);
reader.setInput(inputStream);
Raster raster = reader.readRaster(0, null);
BufferedImage image = createRGBImageFromYCC(raster, rgbaProfile);
return image;
}
/**
* Reads a YCCK JPEG image from the provided InputStream, converting the
* colors to RGB using the provided CMYK ICC_Profile. The image data must be
* in the YCCK color space.
* <p>
* Use this method, if you have already determined that the input stream
* contains a YCCK JPEG image.
*
* @param in An InputStream, preferably an ImageInputStream, in the JPEG
* File Interchange Format (JFIF).
* @param cmykProfile An ICC_Profile for conversion from the CMYK color
* space to the RGB color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage containing the decoded image converted into the
* RGB color space.
* @throws java.io.IOException
*/
public static BufferedImage readRGBImageFromYCCK(InputStream in, ICC_Profile cmykProfile) throws IOException {
ImageInputStream inputStream = null;
ImageReader reader = createNativeJPEGReader();
inputStream = (in instanceof ImageInputStream) ? (ImageInputStream) in : ImageIO.createImageInputStream(in);
reader.setInput(inputStream);
Raster raster = reader.readRaster(0, null);
BufferedImage image = createRGBImageFromYCCK(raster, cmykProfile);
return image;
}
/**
* Reads an inverted-YCCK JPEG image from the provided InputStream,
* converting the colors to RGB using the provided CMYK ICC_Profile. The
* image data must be in the inverted-YCCK color space.
* <p>
* Use this method, if you have already determined that the input stream
* contains an inverted-YCCK JPEG image.
*
* @param in An InputStream, preferably an ImageInputStream, in the JPEG
* File Interchange Format (JFIF).
* @param cmykProfile An ICC_Profile for conversion from the CMYK color
* space to the RGB color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage containing the decoded image converted into the
* RGB color space.
* @throws java.io.IOException
*/
public static BufferedImage readRGBImageFromInvertedYCCK(InputStream in, ICC_Profile cmykProfile) throws IOException {
ImageInputStream inputStream = null;
ImageReader reader = createNativeJPEGReader();
inputStream = (in instanceof ImageInputStream) ? (ImageInputStream) in : ImageIO.createImageInputStream(in);
reader.setInput(inputStream);
Raster raster = reader.readRaster(0, null);
raster = convertInvertedYCCKToCMYK(raster);
BufferedImage image = createRGBImageFromCMYK(raster, cmykProfile);
return image;
}
/**
* Creates a buffered image from a raster in the YCCK color space,
* converting the colors to RGB using the provided CMYK ICC_Profile.
*
* @param ycckRaster A raster with (at least) 4 bands of samples.
* @param cmykProfile An ICC_Profile for conversion from the CMYK color
* space to the RGB color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage in the RGB color space.
* @throws NullPointerException.
*/
public static BufferedImage createRGBImageFromYCCK(Raster ycckRaster, ICC_Profile cmykProfile) {
BufferedImage image;
if (cmykProfile != null) {
ycckRaster = convertYCCKtoCMYK(ycckRaster);
image = createRGBImageFromCMYK(ycckRaster, cmykProfile);
} else {
int w = ycckRaster.getWidth(), h = ycckRaster.getHeight();
int[] rgb = new int[w * h];
int[] Y = ycckRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] Cb = ycckRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] Cr = ycckRaster.getSamples(0, 0, w, h, 2, (int[]) null);
int[] K = ycckRaster.getSamples(0, 0, w, h, 3, (int[]) null);
float vr, vg, vb;
for (int i = 0, imax = Y.length; i < imax; i++) {
// FIXME - Use integer arithmetic to improve performance
float k = K[i], y = Y[i], cb = Cb[i], cr = Cr[i];
vr = y + 1.402f * (cr - 128) - k;
vg = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128) - k;
vb = y + 1.772f * (cb - 128) - k;
rgb[i] = (0xff & (vr < 0.0f ? 0 : vr > 255.0f ? 0xff : (int) (vr + 0.5f))) << 16
| (0xff & (vg < 0.0f ? 0 : vg > 255.0f ? 0xff : (int) (vg + 0.5f))) << 8
| (0xff & (vb < 0.0f ? 0 : vb > 255.0f ? 0xff : (int) (vb + 0.5f)));
}
Raster rgbRaster = Raster.createPackedRaster(
new DataBufferInt(rgb, rgb.length),
w, h, w, new int[]{0xff0000, 0xff00, 0xff}, null);
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = RGB;//new DirectColorModel(cs, 24, 0xff0000, 0xff00, 0xff, 0x0, false, DataBuffer.TYPE_INT);
image = new BufferedImage(cm, (WritableRaster) rgbRaster, true, null);
}
return image;
}
/**
* Creates a buffered image from a raster in the inverted YCCK color space,
* converting the colors to RGB using the provided CMYK ICC_Profile.
*
* @param ycckRaster A raster with (at least) 4 bands of samples.
* @param cmykProfile An ICC_Profile for conversion from the CMYK color
* space to the RGB color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage in the RGB color space.
*/
public static BufferedImage createRGBImageFromInvertedYCCK(Raster ycckRaster, ICC_Profile cmykProfile) {
BufferedImage image;
if (cmykProfile != null) {
ycckRaster = convertInvertedYCCKToCMYK(ycckRaster);
image = createRGBImageFromCMYK(ycckRaster, cmykProfile);
} else {
int w = ycckRaster.getWidth(), h = ycckRaster.getHeight();
int[] rgb = new int[w * h];
PixelInterleavedSampleModel pix;
// if (Adobe_APP14 and transform==2) then YCCK else CMYK
int[] Y = ycckRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] Cb = ycckRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] Cr = ycckRaster.getSamples(0, 0, w, h, 2, (int[]) null);
int[] K = ycckRaster.getSamples(0, 0, w, h, 3, (int[]) null);
float vr, vg, vb;
for (int i = 0, imax = Y.length; i < imax; i++) {
// FIXME - Use integer arithmetic to improve performance
float k = 255 - K[i], y = 255 - Y[i], cb = 255 - Cb[i], cr = 255 - Cr[i];
vr = y + 1.402f * (cr - 128) - k;
vg = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128) - k;
vb = y + 1.772f * (cb - 128) - k;
rgb[i] = (0xff & (vr < 0.0f ? 0 : vr > 255.0f ? 0xff : (int) (vr + 0.5f))) << 16
| (0xff & (vg < 0.0f ? 0 : vg > 255.0f ? 0xff : (int) (vg + 0.5f))) << 8
| (0xff & (vb < 0.0f ? 0 : vb > 255.0f ? 0xff : (int) (vb + 0.5f)));
}
Raster rgbRaster = Raster.createPackedRaster(
new DataBufferInt(rgb, rgb.length),
w, h, w, new int[]{0xff0000, 0xff00, 0xff}, null);
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = RGB;//new DirectColorModel(cs, 24, 0xff0000, 0xff00, 0xff, 0x0, false, DataBuffer.TYPE_INT);
image = new BufferedImage(cm, (WritableRaster) rgbRaster, true, null);
}
return image;
}
/**
* Creates a buffered image from a raster in the CMYK color space,
* converting the colors to RGB using the provided CMYK ICC_Profile.
*
* As seen from a comment made by 'phelps' at
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4799903
*
* @param cmykRaster A raster with (at least) 4 bands of samples.
* @param cmykProfile An ICC_Profile for conversion from the CMYK color
* space to the RGB color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage in the RGB color space.
*/
public static BufferedImage createRGBImageFromCMYK(Raster cmykRaster, ICC_Profile cmykProfile) {
BufferedImage image;
int w = cmykRaster.getWidth();
int h = cmykRaster.getHeight();
if (cmykProfile != null) {
ColorSpace cmykCS = new ICC_ColorSpace(cmykProfile);
image = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);
WritableRaster rgbRaster = image.getRaster();
ColorSpace rgbCS = image.getColorModel().getColorSpace();
ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null);
cmykToRgb.filter(cmykRaster, rgbRaster);
} else {
int[] rgb = new int[w * h];
int[] C = cmykRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] M = cmykRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] Y = cmykRaster.getSamples(0, 0, w, h, 2, (int[]) null);
int[] K = cmykRaster.getSamples(0, 0, w, h, 3, (int[]) null);
for (int i = 0, imax = C.length; i < imax; i++) {
int k = K[i];
rgb[i] = (255 - min(255, C[i] + k)) << 16
| (255 - min(255, M[i] + k)) << 8
| (255 - min(255, Y[i] + k));
}
Raster rgbRaster = Raster.createPackedRaster(
new DataBufferInt(rgb, rgb.length),
w, h, w, new int[]{0xff0000, 0xff00, 0xff}, null);
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = RGB;//new DirectColorModel(cs, 24, 0xff0000, 0xff00, 0xff, 0x0, false, DataBuffer.TYPE_INT);
image = new BufferedImage(cm, (WritableRaster) rgbRaster, true, null);
}
return image;
}
/**
* Creates a buffered image from a raster in the RGBA color space,
* converting the colors to RGB using the provided CMYK ICC_Profile.
*
* As seen from a comment made by 'phelps' at
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4799903
*
* @param rgbaRaster A raster with (at least) 4 bands of samples.
* @param rgbaProfile An ICC_Profile for conversion from the CMYK color
* space to the RGB color space. If this parameter is null, a default
* profile is used.
* @return a BufferedImage in the RGB color space.
*/
public static BufferedImage createRGBAImageFromRGBA(Raster rgbaRaster, ICC_Profile rgbaProfile) {
BufferedImage image;
int w = rgbaRaster.getWidth();
int h = rgbaRaster.getHeight();
if (rgbaProfile != null) {
ColorSpace rgbaCS = new ICC_ColorSpace(rgbaProfile);
image = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);
WritableRaster rgbRaster = image.getRaster();
ColorSpace rgbCS = image.getColorModel().getColorSpace();
ColorConvertOp cmykToRgb = new ColorConvertOp(rgbaCS, rgbCS, null);
cmykToRgb.filter(rgbaRaster, rgbRaster);
} else {
int[] rgb = new int[w * h];
int[] R = rgbaRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] G = rgbaRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] B = rgbaRaster.getSamples(0, 0, w, h, 2, (int[]) null);
int[] A = rgbaRaster.getSamples(0, 0, w, h, 3, (int[]) null);
for (int i = 0, imax = R.length; i < imax; i++) {
rgb[i] = A[i] << 24 | R[i] << 16 | G[i] << 8 | B[i];
}
Raster rgbRaster = Raster.createPackedRaster(
new DataBufferInt(rgb, rgb.length),
w, h, w, new int[]{0xff0000, 0xff00, 0xff, 0xff000000}, null);
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = ColorModel.getRGBdefault();//new DirectColorModel(cs, 32, 0xff0000, 0xff00, 0xff, 0x0ff000000, false, DataBuffer.TYPE_INT);
image = new BufferedImage(cm, (WritableRaster) rgbRaster, true, null);
}
return image;
}
public static BufferedImage createRGBImageFromRGB(Raster rgbaRaster, ICC_Profile rgbaProfile) {
BufferedImage image;
int w = rgbaRaster.getWidth();
int h = rgbaRaster.getHeight();
// ICC_Profile currently not supported
rgbaProfile = null;
if (rgbaProfile != null) {
ColorSpace rgbaCS = new ICC_ColorSpace(rgbaProfile);
image = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);
WritableRaster rgbRaster = image.getRaster();
ColorSpace rgbCS = image.getColorModel().getColorSpace();
ColorConvertOp cmykToRgb = new ColorConvertOp(rgbaCS, rgbCS, null);
cmykToRgb.filter(rgbaRaster, rgbRaster);
} else {
int[] rgb = new int[w * h];
int[] R = rgbaRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] G = rgbaRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] B = rgbaRaster.getSamples(0, 0, w, h, 2, (int[]) null);
//int[] A = rgbaRaster.getSamples(0, 0, w, h, 3, (int[]) null);
for (int i = 0, imax = R.length; i < imax; i++) {
rgb[i] = 0xff << 24 | R[i] << 16 | G[i] << 8 | B[i];
}
Raster rgbRaster = Raster.createPackedRaster(
new DataBufferInt(rgb, rgb.length),
w, h, w, new int[]{0xff0000, 0xff00, 0xff, 0xff000000}, null);
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = ColorModel.getRGBdefault();//new DirectColorModel(cs, 32, 0xff0000, 0xff00, 0xff, 0x0ff000000, false, DataBuffer.TYPE_INT);
image = new BufferedImage(cm, (WritableRaster) rgbRaster, true, null);
}
return image;
}
public static BufferedImage createRGBImageFromYCC(Raster rgbaRaster, ICC_Profile rgbaProfile) {
BufferedImage image;
int w = rgbaRaster.getWidth();
int h = rgbaRaster.getHeight();
// ICC_Profile currently not supported
rgbaProfile = null;
if (rgbaProfile != null) {
ColorSpace rgbaCS = new ICC_ColorSpace(rgbaProfile);
image = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);
WritableRaster rgbRaster = image.getRaster();
ColorSpace rgbCS = image.getColorModel().getColorSpace();
ColorConvertOp cmykToRgb = new ColorConvertOp(rgbaCS, rgbCS, null);
cmykToRgb.filter(rgbaRaster, rgbRaster);
} else {
int[] rgb = new int[w * h];
int[] Y = rgbaRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] Cb = rgbaRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] Cr = rgbaRaster.getSamples(0, 0, w, h, 2, (int[]) null);
//int[] A = rgbaRaster.getSamples(0, 0, w, h, 3, (int[]) null);
for (int i = 0, imax = Y.length; i < imax; i++) {
int Yi, Cbi, Cri;
int R, G, B;
//RGB can be computed directly from YCbCr (256 levels) as follows:
//R = Y + 1.402 (Cr-128)
//G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128)
//B = Y + 1.772 (Cb-128)
Yi = Y[i];
Cbi = Cb[i];
Cri = Cr[i];
R = (1000 * Yi + 1402 * (Cri - 128)) / 1000;
G = (100000 * Yi - 34414 * (Cbi - 128) - 71414 * (Cri - 128)) / 100000;
B = (1000 * Yi + 1772 * (Cbi - 128)) / 1000;
R = min(255, max(0, R));
G = min(255, max(0, G));
B = min(255, max(0, B));
rgb[i] = 0xff << 24 | R << 16 | G << 8 | B;
}
Raster rgbRaster = Raster.createPackedRaster(
new DataBufferInt(rgb, rgb.length),
w, h, w, new int[]{0xff0000, 0xff00, 0xff, 0xff000000}, null);
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = ColorModel.getRGBdefault();//new DirectColorModel(cs, 32, 0xff0000, 0xff00, 0xff, 0x0ff000000, false, DataBuffer.TYPE_INT);
image = new BufferedImage(cm, (WritableRaster) rgbRaster, true, null);
}
return image;
}
/**
* Define tables for YCC->RGB color space conversion.
*/
private final static int SCALEBITS = 16;
private final static int MAXJSAMPLE = 255;
private final static int CENTERJSAMPLE = 128;
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
private final static int[] Cr_r_tab = new int[MAXJSAMPLE + 1];
private final static int[] Cb_b_tab = new int[MAXJSAMPLE + 1];
private final static int[] Cr_g_tab = new int[MAXJSAMPLE + 1];
private final static int[] Cb_g_tab = new int[MAXJSAMPLE + 1];
/*
* Initialize tables for YCC->RGB colorspace conversion.
*/
private static synchronized void buildYCCtoRGBtable() {
if (Cr_r_tab[0] == 0) {
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_tab[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
/* Cb=>B value is nearest int to 1.77200 * x */
Cb_b_tab[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
/* Cr=>G value is scaled-up -0.71414 * x */
Cr_g_tab[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_tab[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
}
}
}
/*
* Adobe-style YCCK->CMYK conversion.
* We convert YCbCr to R=1-C, G=1-M, and B=1-Y using the same
* conversion as above, while passing K (black) unchanged.
* We assume build_ycc_rgb_table has been called.
*/
private static Raster convertInvertedYCCKToCMYK(Raster ycckRaster) {
buildYCCtoRGBtable();
int w = ycckRaster.getWidth(), h = ycckRaster.getHeight();
int[] ycckY = ycckRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] ycckCb = ycckRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] ycckCr = ycckRaster.getSamples(0, 0, w, h, 2, (int[]) null);
int[] ycckK = ycckRaster.getSamples(0, 0, w, h, 3, (int[]) null);
int[] cmyk = new int[ycckY.length];
for (int i = 0; i < ycckY.length; i++) {
int y = 255 - ycckY[i];
int cb = 255 - ycckCb[i];
int cr = 255 - ycckCr[i];
int cmykC, cmykM, cmykY;
// Range-limiting is essential due to noise introduced by DCT losses.
cmykC = MAXJSAMPLE - (y + Cr_r_tab[cr]); // red
cmykM = MAXJSAMPLE - (y + // green
(Cb_g_tab[cb] + Cr_g_tab[cr]
>> SCALEBITS));
cmykY = MAXJSAMPLE - (y + Cb_b_tab[cb]); // blue
/* K passes through unchanged */
cmyk[i] = (cmykC < 0 ? 0 : (cmykC > 255) ? 255 : cmykC) << 24
| (cmykM < 0 ? 0 : (cmykM > 255) ? 255 : cmykM) << 16
| (cmykY < 0 ? 0 : (cmykY > 255) ? 255 : cmykY) << 8
| 255 - ycckK[i];
}
Raster cmykRaster = Raster.createPackedRaster(
new DataBufferInt(cmyk, cmyk.length),
w, h, w, new int[]{0xff000000, 0xff0000, 0xff00, 0xff}, null);
return cmykRaster;
}
private static Raster convertYCCKtoCMYK(Raster ycckRaster) {
buildYCCtoRGBtable();
int w = ycckRaster.getWidth(), h = ycckRaster.getHeight();
int[] ycckY = ycckRaster.getSamples(0, 0, w, h, 0, (int[]) null);
int[] ycckCb = ycckRaster.getSamples(0, 0, w, h, 1, (int[]) null);
int[] ycckCr = ycckRaster.getSamples(0, 0, w, h, 2, (int[]) null);
int[] ycckK = ycckRaster.getSamples(0, 0, w, h, 3, (int[]) null);
int[] cmyk = new int[ycckY.length];
for (int i = 0; i < ycckY.length; i++) {
int y = ycckY[i];
int cb = ycckCb[i];
int cr = ycckCr[i];
int cmykC, cmykM, cmykY;
// Range-limiting is essential due to noise introduced by DCT losses.
cmykC = MAXJSAMPLE - (y + Cr_r_tab[cr]); // red
cmykM = MAXJSAMPLE - (y + // green
(Cb_g_tab[cb] + Cr_g_tab[cr]
>> SCALEBITS));
cmykY = MAXJSAMPLE - (y + Cb_b_tab[cb]); // blue
/* K passes through unchanged */
cmyk[i] = (cmykC < 0 ? 0 : (cmykC > 255) ? 255 : cmykC) << 24
| (cmykM < 0 ? 0 : (cmykM > 255) ? 255 : cmykM) << 16
| (cmykY < 0 ? 0 : (cmykY > 255) ? 255 : cmykY) << 8
| ycckK[i];
}
return Raster.createPackedRaster(
new DataBufferInt(cmyk, cmyk.length),
w, h, w, new int[]{0xff000000, 0xff0000, 0xff00, 0xff}, null);
}
/**
* Reads a JPEG image from the provided InputStream. The image data must be
* in the YUV or the Gray color space.
* <p>
* Use this method, if you have already determined that the input stream
* contains a YCC or Gray JPEG image.
*
* @param in An InputStream, preferably an ImageInputStream, in the JPEG
* File Interchange Format (JFIF).
* @return a BufferedImage containing the decoded image converted into the
* RGB color space.
* @throws java.io.IOException
*/
public static BufferedImage readImageFromYCCorGray(ImageInputStream in) throws IOException {
ImageReader r = createNativeJPEGReader();
r.setInput(in);
BufferedImage img = r.read(0);
return img;
}
}

View File

@@ -0,0 +1,77 @@
/*
* @(#)CMYKJPEGImageReaderSpi.java 1.2 2011-02-17
*
* Copyright (c) 2010-2011 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.jpeg;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
/**
* A reader for JPEG images in the CMYK color space.
*
* @author Werner Randelshofer
* @version 1.2 2011-02-17 Removes support for JMF.
* <br>1.0 2010-07-23 Created.
*/
public class CMYKJPEGImageReaderSpi extends ImageReaderSpi {
public CMYKJPEGImageReaderSpi() {
super("Werner Randelshofer",//vendor name
"1.0",//version
new String[]{"JPEG","JPG"},//names
new String[]{"jpg"},//suffixes,
new String[]{"image/jpg"},// MIMETypes,
"org.monte.media.jpeg.CMYKJPEGImageReader",// readerClassName,
new Class[]{ImageInputStream.class,InputStream.class,byte[].class},// inputTypes,
null,// writerSpiNames,
false,// supportsStandardStreamMetadataFormat,
null,// nativeStreamMetadataFormatName,
null,// nativeStreamMetadataFormatClassName,
null,// extraStreamMetadataFormatNames,
null,// extraStreamMetadataFormatClassNames,
false,// supportsStandardImageMetadataFormat,
null,// nativeImageMetadataFormatName,
null,// nativeImageMetadataFormatClassName,
null,// extraImageMetadataFormatNames,
null// extraImageMetadataFormatClassNames
);
}
@Override
public boolean canDecodeInput(Object source) throws IOException {
if (source instanceof ImageInputStream) {
ImageInputStream in = (ImageInputStream) source;
in.mark();
// Check if file starts with a JFIF SOI magic (0xffd8=-40)
if (in.readShort() != -40) {
in.reset();
return false;
}
in.reset();
return true;
}
return false;
}
@Override
public ImageReader createReaderInstance(Object extension) throws IOException {
return new CMYKJPEGImageReader(this);
}
@Override
public String getDescription(Locale locale) {
return "CMYK JPEG Image Reader";
}
}

View File

@@ -0,0 +1,521 @@
/*
* @(#)JFIFInputStream.java
*
* Copyright (c) 2008-2012 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.jpeg;
import java.io.*;
import java.util.*;
/**
* JFIFInputStream.
* <p>
* This InputStream uses two special marker values which do not exist
* in the JFIF stream:
* <ul>
* <li><b>-1</b>: marks junk data at the beginning of the file.</li>
* <li><b>0</b>: marks entropy encoded image data.</li>
* </ul>
* <p>
* The junk data at the beginning of the file can be accessed by calling the
* read-methods immediately after opening the stream. Call nextSegment()
* immediately after opening the stream if you are not interested into this
* junk data.
* <p>
* Junk data at the end of the file is delivered as part of the EOI_MARKER segment.
* Finish reading after encountering the EOI_MARKER segment if you are not interested
* in this junk data.
*
* <p>
* References:<br>
* JPEG File Interchange Format Version 1.02<br>
* <a href="http://www.jpeg.org/public/jfif.pdf">http://www.jpeg.org/public/jfif.pdf</a>
* <p>
* Pennebaker, W., Mitchell, J. (1993).<br>
* JPEG Still Image Data Compression Standard.<br>
* Chapmann & Hall, New York.<br>
* ISBN 0-442-01272-1<br>
*
*
* @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau
* @version $Id: JFIFInputStream.java 299 2013-01-03 07:40:18Z werner $
*/
public class JFIFInputStream extends FilterInputStream {
/**
* This hash set holds the Id's of markers which stand alone,
* respectively do no have a data segment.
*/
private final HashSet<Integer> standaloneMarkers = new HashSet<Integer>();
/**
* This hash set holds the Id's of markers which have a data
* segment followed by a entropy-coded data segment.
*/
private final HashSet<Integer> doubleSegMarkers = new HashSet<Integer>();
/** Represents a segment within a JFIF File.
*/
public static class Segment {
/**
* Holds the marker code.
* A marker is an unsigned short between 0xff01 and 0xfffe.
*/
public final int marker;
/**
* Holds the offset of the first data byte to the beginning
* of the stream.
*/
public final long offset;
/**
* If the marker starts a marker segment, holds the length
* of the data in the data segment.
* If the marker starts a entropy-coded data segment, holds
* the value -1.
*/
public final int length;
public Segment(int marker, long offset, int length) {
this.marker = marker;
this.offset = offset;
this.length = length;
}
public boolean isEntropyCoded() {
return length == -1;
}
@Override
public String toString() {
return "Segment marker=0x" + Integer.toHexString(marker) + " offset=" + offset + "=0x" + Long.toHexString(offset);
}
}
private Segment segment;
/**
* This variable is set to true, if a 0xff byte has been found in
* entropy-code data.
*/
private boolean markerFound;
private int marker = JUNK_MARKER;
private long offset = 0;
private boolean isStuffed0xff = false;
/** JUNK_MARKER Marker (for data which is not part of the JFIF stream. */
public final static int JUNK_MARKER = -1;
/** Start of image */
public final static int SOI_MARKER = 0xffd8;
/** End of image */
public final static int EOI_MARKER = 0xffd9;
/** Temporary private use in arithmetic coding */
public final static int TEM_MARKER = 0xff01;
/** Start of scan */
public final static int SOS_MARKER = 0xffda;
/** APP1_MARKER Reserved for application use */
public final static int APP1_MARKER = 0xffe1;
/** APP2_MARKER Reserved for application use */
public final static int APP2_MARKER = 0xffe2;
/** Reserved for JPEG extensions */
public final static int JPG0_MARKER = 0xfff0;
public final static int JPG1_MARKER = 0xfff1;
public final static int JPG2_MARKER = 0xfff2;
public final static int JPG3_MARKER = 0xfff3;
public final static int JPG4_MARKER = 0xfff4;
public final static int JPG5_MARKER = 0xfff5;
public final static int JPG6_MARKER = 0xfff6;
public final static int JPG7_MARKER = 0xfff7;
public final static int JPG8_MARKER = 0xfff8;
public final static int JPG9_MARKER = 0xfff9;
public final static int JPGA_MARKER = 0xfffA;
public final static int JPGB_MARKER = 0xfffB;
public final static int JPGC_MARKER = 0xfffC;
public final static int JPGD_MARKER = 0xfffD;
/** Start of frame markers */
public final static int SOF0_MARKER = 0xffc0;//nondifferential Huffman-coding frames with baseline DCT.
public final static int SOF1_MARKER = 0xffc1;//nondifferential Huffman-coding frames with extended sequential DCT.
public final static int SOF2_MARKER = 0xffc2;//nondifferential Huffman-coding frames with progressive DCT.
public final static int SOF3_MARKER = 0xffc3;//nondifferential Huffman-coding frames with lossless (sequential) data.
//public final static int SOF4_MARKER = 0xffc4;//
public final static int SOF5_MARKER = 0xffc5;//differential Huffman-coding frames with differential sequential DCT.
public final static int SOF6_MARKER = 0xffc6;//differential Huffman-coding frames with differential progressive DCT.
public final static int SOF7_MARKER = 0xffc7;//differential Huffman-coding frames with differential lossless data.
//public final static int SOF8_MARKER = 0xffc8;//
public final static int SOF9_MARKER = 0xffc9;//nondifferential Arithmetic-coding frames with extended sequential DCT.
public final static int SOFA_MARKER = 0xffcA;//nondifferential Arithmetic-coding frames with progressive DCT.
public final static int SOFB_MARKER = 0xffcB;//nondifferential Arithmetic-coding frames with lossless (sequential) data.
//public final static int SOFC_MARKER = 0xffcC;//
public final static int SOFD_MARKER = 0xffcD;//differential Arithmetic-coding frames with differential sequential DCT.
public final static int SOFE_MARKER = 0xffcE;//differential Arithmetic-coding frames with differential progressive DCT.
public final static int SOFF_MARKER = 0xffcF;//differential Arithmetic-coding frames with differential lossless DCT.
// Restart markers
public final static int RST0_MARKER = 0xffd0;
public final static int RST1_MARKER = 0xffd1;
public final static int RST2_MARKER = 0xffd2;
public final static int RST3_MARKER = 0xffd3;
public final static int RST4_MARKER = 0xffd4;
public final static int RST5_MARKER = 0xffd5;
public final static int RST6_MARKER = 0xffd6;
public final static int RST7_MARKER = 0xffd7;
public JFIFInputStream(File f) throws IOException {
this(new BufferedInputStream(new FileInputStream(f)));
}
public JFIFInputStream(InputStream in) {
super(in);
for (int i = RST0_MARKER; i <= RST7_MARKER; i++) {
standaloneMarkers.add(i); // RST(i) Restart interval termination
}
standaloneMarkers.add(SOI_MARKER); // SOI_MARKER Start of image
standaloneMarkers.add(EOI_MARKER); // EOI_MARKER End of image
standaloneMarkers.add(TEM_MARKER); // TEM_MARKER Temporary private use in arithmetic coding
standaloneMarkers.add(JPG0_MARKER); // JPEG Extensions
standaloneMarkers.add(JPG1_MARKER);
standaloneMarkers.add(JPG2_MARKER);
standaloneMarkers.add(JPG3_MARKER);
standaloneMarkers.add(JPG4_MARKER);
standaloneMarkers.add(JPG5_MARKER);
standaloneMarkers.add(JPG6_MARKER);
standaloneMarkers.add(JPG7_MARKER);
standaloneMarkers.add(JPG8_MARKER);
standaloneMarkers.add(JPG9_MARKER);
standaloneMarkers.add(JPGA_MARKER);
standaloneMarkers.add(JPGB_MARKER);
standaloneMarkers.add(JPGC_MARKER);
standaloneMarkers.add(JPGD_MARKER);
standaloneMarkers.add(0xffff); // Illegal marker
doubleSegMarkers.add(SOS_MARKER); // SOS_MARKER Start of Scan
// Start with a dummy entropy-coded data segment.
segment = new Segment(-1, 0, -1);
}
/**
* Gets the current segment from the input stream.
*
* @return The current segment. Returns null, if we encountered
* the end of the stream.
* @throws java.io.IOException
*/
public Segment getSegment() throws IOException {
return segment;
}
/**
* Gets the next segment from the input stream.
*
* @return The next segment. Returns null, if we encountered
* the end of the stream.
* @throws java.io.IOException
*/
public Segment getNextSegment() throws IOException {
// If we are inside of a marker segment, skip the
// marker
if (!segment.isEntropyCoded()) {
markerFound = false;
do {
long skipped = in.skip(segment.length - offset + segment.offset);
if (skipped == -1) {
segment = new Segment(0, offset, -1);
return null;
}
offset += skipped;
} while (offset < segment.length + segment.offset);
if (doubleSegMarkers.contains(segment.marker)) {
segment = new Segment(0, offset, -1);
return segment;
}
}
// Scan the input stream for the next marker.
while (!markerFound) {
while (true) {
int b;
if (isStuffed0xff) {
b = 0xff;
isStuffed0xff = false;
} else {
b = read0();
}
if (b == -1) {
return null;
}
if (b == 0xff) {
markerFound = true;
break;
}
}
int b = read0();
if (b == -1) {
return null;
}
if (b == 0x00) {
markerFound = false;
} else if (b == 0xff) {
isStuffed0xff = true;
markerFound = false;
} else {
marker = 0xff00 | b;
}
}
markerFound = false;
/*
if (marker <= 0xff00 || marker >= 0xffff) {
throw new IOException("JFIFInputStream found illegal marker " + Integer.toHexString(marker) + " at offset " + offset + " 0x"+Long.toHexString(offset)+".");
}*/
// Note: 0xffff is an illegal marker segment, we process it here
// for robustness.
if (standaloneMarkers.contains(marker)) {
segment = new Segment(0xff00 | marker, offset, -1);
} else {
int length = (read0() << 8) | read0();
if (length < 2) {
throw new IOException("JFIFInputStream found illegal segment length " + length + " after marker " + Integer.toHexString(marker) + " at offset " + offset + ".");
}
segment = new Segment(0xff00 | marker, offset, length - 2);
}
return segment;
}
public long getStreamPosition() {
return offset;
}
private int read0() throws IOException {
int b = in.read();
if (b != -1) {
offset++;
}
return b;
}
/**
* Reads the next byte of data from this input stream. The value
* byte is returned as an <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned. This method blocks until input data
* is available, the end of the stream is detected, or an exception
* is thrown.
* <p>
* This method
* simply performs <code>in.read()</code> and returns the result.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream is reached.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
@Override
public int read() throws IOException {
if (markerFound) {
return -1;
}
int b;
if (isStuffed0xff) {
isStuffed0xff = false;
b = 0xff;
} else {
b = read0();
}
if (segment.isEntropyCoded()) {
if (b == 0xff) {
b = read0();
if (b == 0x00) {
// found a stuffed 0xff byte
return 0xff;
} else if (b == 0xff) {
// found an invalid sequence of two 0xff bytes
isStuffed0xff = true;
return 0xff;
}
markerFound = true;
marker = 0xff00 | b;
return -1;
}
}
return b;
}
/**
* Reads up to <code>len</code> b of data from this input stream
* into an array of b. This method blocks until some input is
* available.
* <p>
* This method simply performs <code>in.read(b, off, len)</code>
* and returns the result.
*
* @param b the buffer into which the data is read.
* @param off the start offset of the data.
* @param len the maximum number of b read.
* @return the total number of b read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
@Override
public int read(byte b[], int off, int len) throws IOException {
if (markerFound) {
return -1;
}
int count = 0;
if (segment.isEntropyCoded()) {
for (; count < len; count++) {
int data = read();
if (data == -1) {
if (count==0) return -1;
break;
}
b[off + count] = (byte) data;
}
} else {
long available = segment.length - offset + segment.offset;
if (available <= 0) {
return -1;
}
if (available < len) {
len = (int) available;
}
count = in.read(b, off, len);
if (count != -1) {
offset += count;
}
}
return count;
}
/** Fully skips the specified number of bytes. */
public final void skipFully(long n) throws IOException {
long total = 0;
long cur = 0;
while ((total < n) && ((cur = (int) in.skip(n - total)) > 0)) {
total += cur;
}
offset+=total;
if (total < n) {
throw new EOFException();
}
}
/**
* Skips over and discards <code>n</code> b of data from the
* input stream. The <code>skip</code> method may, for a variety of
* reasons, end up skipping over some smaller number of b,
* possibly <code>0</code>. The actual number of b skipped is
* returned.
* <p>
* This method
* simply performs <code>in.skip(n)</code>.
*
* @param n the number of b to be skipped.
* @return the actual number of b skipped.
* @exception IOException if an I/O error occurs.
*/
@Override
public long skip(long n) throws IOException {
if (markerFound) {
return -1;
}
long count = 0;
if (segment.isEntropyCoded()) {
for (; count < n; count++) {
int data = read();
if (data == -1) {
break;
}
}
} else {
long available = segment.length - offset + segment.offset;
if (available < n) {
n = (int) available;
}
count = in.skip(n);
if (count != -1) {
offset += count;
}
}
return count;
}
/**
* Marks the current position in this input stream. A subsequent
* call to the <code>reset</code> method repositions this stream at
* the last marked position so that subsequent reads re-read the same b.
* <p>
* The <code>readlimit</code> argument tells this input stream to
* allow that many b to be read before the mark position gets
* invalidated.
* <p>
* This method simply performs <code>in.mark(readlimit)</code>.
*
* @param readlimit the maximum limit of b that can be read before
* the mark position becomes invalid.
* @see java.io.FilterInputStream#in
* @see java.io.FilterInputStream#reset()
*/
@Override
public synchronized void mark(int readlimit) {
// do nothing, since we don't support marking
}
/**
* Repositions this stream to the position at the time the
* <code>mark</code> method was last called on this input stream.
* <p>
* This method
* simply performs <code>in.reset()</code>.
* <p>
* Stream marks are intended to be used in
* situations where you need to read ahead a little to see what's in
* the stream. Often this is most easily done by invoking some
* general parser. If the stream is of the type handled by the
* parse, it just chugs along happily. If the stream is not of
* that type, the parser should toss an exception when it fails.
* If this happens within readlimit b, it allows the outer
* code to reset the stream and try another parser.
*
* @exception IOException if the stream has not been marked or if the
* mark has been invalidated.
* @see java.io.FilterInputStream#in
* @see java.io.FilterInputStream#mark(int)
*/
@Override
public synchronized void reset() throws IOException {
throw new IOException("Reset not supported");
}
/**
* Tests if this input stream supports the <code>mark</code>
* and <code>reset</code> methods.
* This method
* simply performs <code>in.markSupported()</code>.
*
* @return <code>true</code> if this stream type supports the
* <code>mark</code> and <code>reset</code> method;
* <code>false</code> otherwise.
* @see java.io.FilterInputStream#in
* @see java.io.InputStream#mark(int)
* @see java.io.InputStream#reset()
*/
@Override
public boolean markSupported() {
return false;
}
}

View File

@@ -236,7 +236,7 @@ auxiliary.show.customizer.message=<message>
<java-data xmlns="http://www.netbeans.org/ns/freeform-project-java/4">
<compilation-unit>
<package-root>src</package-root>
<classpath mode="compile">../../lib/LZMA.jar;../../lib/avi.jar;../../lib/gif.jar;../../lib/gnujpdf.jar;../../lib/jl1.0.1.jar;../../lib/jpacker.jar;../../lib/nellymoser.jar;../../lib/sfntly.jar;../../lib/ttf.jar;../../src</classpath>
<classpath mode="compile">../../lib/LZMA.jar;../../lib/avi.jar;../../lib/gif.jar;../../lib/gnujpdf.jar;../../lib/jl1.0.1.jar;../../lib/jpacker.jar;../../lib/nellymoser.jar;../../lib/sfntly.jar;../../lib/ttf.jar;../../lib/cmykjpeg.jar;../../src</classpath>
<built-to>build</built-to>
<built-to>reports</built-to>
<built-to>dist</built-to>

View File

@@ -1,108 +1,111 @@
/*
* Copyright (C) 2010-2015 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.decompiler.flash.exporters;
import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler;
import com.jpexs.decompiler.flash.EventListener;
import com.jpexs.decompiler.flash.RetryTask;
import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode;
import com.jpexs.decompiler.flash.exporters.settings.ImageExportSettings;
import com.jpexs.decompiler.flash.helpers.BMPFile;
import com.jpexs.decompiler.flash.helpers.ImageHelper;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.base.ImageTag;
import com.jpexs.decompiler.flash.tags.enums.ImageFormat;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.Path;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author JPEXS
*/
public class ImageExporter {
public List<File> exportImages(AbortRetryIgnoreHandler handler, String outdir, List<Tag> tags, ImageExportSettings settings, EventListener evl) throws IOException {
List<File> ret = new ArrayList<>();
if (tags.isEmpty()) {
return ret;
}
File foutdir = new File(outdir);
Path.createDirectorySafe(foutdir);
int count = 0;
for (Tag t : tags) {
if (t instanceof ImageTag) {
count++;
}
}
if (count == 0) {
return ret;
}
int currentIndex = 1;
for (Tag t : tags) {
if (t instanceof ImageTag) {
if (evl != null) {
evl.handleExportingEvent("image", currentIndex, count, t.getName());
}
final ImageTag imageTag = (ImageTag) t;
ImageFormat fileFormat = imageTag.getImageFormat();
if (settings.mode == ImageExportMode.PNG) {
fileFormat = ImageFormat.PNG;
}
if (settings.mode == ImageExportMode.JPEG) {
fileFormat = ImageFormat.JPEG;
}
if (settings.mode == ImageExportMode.BMP) {
fileFormat = ImageFormat.BMP;
}
{
final File file = new File(outdir + File.separator + Helper.makeFileName(imageTag.getCharacterExportFileName() + "." + ImageHelper.getImageFormatString(fileFormat)));
final ImageFormat ffileFormat = fileFormat;
new RetryTask(() -> {
if (ffileFormat == ImageFormat.BMP) {
BMPFile.saveBitmap(imageTag.getImage().getBufferedImage(), file);
} else {
ImageHelper.write(imageTag.getImage().getBufferedImage(), ffileFormat, file);
}
}, handler).run();
ret.add(file);
}
if (evl != null) {
evl.handleExportedEvent("image", currentIndex, count, t.getName());
}
currentIndex++;
}
}
return ret;
}
}
/*
* Copyright (C) 2010-2015 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.decompiler.flash.exporters;
import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler;
import com.jpexs.decompiler.flash.EventListener;
import com.jpexs.decompiler.flash.RetryTask;
import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode;
import com.jpexs.decompiler.flash.exporters.settings.ImageExportSettings;
import com.jpexs.decompiler.flash.helpers.BMPFile;
import com.jpexs.decompiler.flash.helpers.ImageHelper;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.base.ImageTag;
import com.jpexs.decompiler.flash.tags.enums.ImageFormat;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.Path;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author JPEXS
*/
public class ImageExporter {
public List<File> exportImages(AbortRetryIgnoreHandler handler, String outdir, List<Tag> tags, ImageExportSettings settings, EventListener evl) throws IOException {
List<File> ret = new ArrayList<>();
if (tags.isEmpty()) {
return ret;
}
File foutdir = new File(outdir);
Path.createDirectorySafe(foutdir);
int count = 0;
for (Tag t : tags) {
if (t instanceof ImageTag) {
count++;
}
}
if (count == 0) {
return ret;
}
int currentIndex = 1;
for (Tag t : tags) {
if (t instanceof ImageTag) {
if (evl != null) {
evl.handleExportingEvent("image", currentIndex, count, t.getName());
}
final ImageTag imageTag = (ImageTag) t;
ImageFormat fileFormat = imageTag.getImageFormat();
ImageFormat originalFormat = fileFormat;
if (settings.mode == ImageExportMode.PNG) {
fileFormat = ImageFormat.PNG;
}
if (settings.mode == ImageExportMode.JPEG) {
fileFormat = ImageFormat.JPEG;
}
if (settings.mode == ImageExportMode.BMP) {
fileFormat = ImageFormat.BMP;
}
{
final File file = new File(outdir + File.separator + Helper.makeFileName(imageTag.getCharacterExportFileName() + "." + ImageHelper.getImageFormatString(fileFormat)));
final ImageFormat ffileFormat = fileFormat;
new RetryTask(() -> {
if (ffileFormat == originalFormat) {
} else if (ffileFormat == ImageFormat.BMP) {
BMPFile.saveBitmap(imageTag.getImage().getBufferedImage(), file);
} else {
ImageHelper.write(imageTag.getImage().getBufferedImage(), ffileFormat, file);
}
}, handler).run();
ret.add(file);
}
if (evl != null) {
evl.handleExportedEvent("image", currentIndex, count, t.getName());
}
currentIndex++;
}
}
return ret;
}
}

View File

@@ -18,6 +18,7 @@ package com.jpexs.decompiler.flash.helpers;
import com.jpexs.decompiler.flash.tags.base.ImageTag;
import com.jpexs.decompiler.flash.tags.enums.ImageFormat;
import com.jpexs.helpers.Helper;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -25,10 +26,15 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.monte.media.jpeg.CMYKJPEGImageReader;
import org.monte.media.jpeg.CMYKJPEGImageReaderSpi;
/**
*
@@ -41,9 +47,18 @@ public class ImageHelper {
}
public static BufferedImage read(InputStream input) throws IOException {
BufferedImage in = ImageIO.read(input);
if (in == null) {
return null;
BufferedImage in;
byte data[] = Helper.readStream(input);
try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) {
CMYKJPEGImageReader r = new CMYKJPEGImageReader(new CMYKJPEGImageReaderSpi());
r.setInput(iis);
in = r.read(0);
} catch (IOException ex) {
try {
return ImageIO.read(ImageIO.createImageInputStream(new ByteArrayInputStream(data)));
} catch (IOException ex1) {
in = null;
}
}
int type = in.getType();

View File

@@ -58,7 +58,7 @@ public class DefineBitsJPEG2Tag extends ImageTag implements AloneTag {
public DefineBitsJPEG2Tag(SWF swf) {
super(swf, ID, NAME, null);
characterID = swf.getNextCharacterId();
imageData = ByteArrayRange.EMPTY;
imageData = new ByteArrayRange(createEmptyImage());
forceWriteAsLong = true;
}
@@ -68,6 +68,13 @@ public class DefineBitsJPEG2Tag extends ImageTag implements AloneTag {
this.imageData = new ByteArrayRange(imageData);
}
private byte[] createEmptyImage() {
BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
ByteArrayOutputStream bitmapDataOS = new ByteArrayOutputStream();
ImageHelper.write(img, ImageFormat.JPEG, bitmapDataOS);
return bitmapDataOS.toByteArray();
}
public DefineBitsJPEG2Tag(SWFInputStream sis, ByteArrayRange data) throws IOException {
super(sis.getSwf(), ID, NAME, data);
readData(sis, data, 0, false, false, false);

View File

@@ -153,7 +153,7 @@ public class DefineBitsJPEG3Tag extends ImageTag implements AloneTag {
@Override
public ImageFormat getImageFormat() {
ImageFormat fmt = ImageTag.getImageFormat(imageData);
if (fmt == ImageFormat.JPEG) {
if (fmt == ImageFormat.JPEG && bitmapAlphaData.getLength() > 0) {
fmt = ImageFormat.PNG; //transparency
}
return fmt;
@@ -161,8 +161,17 @@ public class DefineBitsJPEG3Tag extends ImageTag implements AloneTag {
@Override
public InputStream getImageData() {
int errorLength = hasErrorHeader(imageData) ? 4 : 0;
return new ByteArrayInputStream(imageData.getArray(), imageData.getPos() + errorLength, imageData.getLength() - errorLength);
if (bitmapAlphaData.getLength() == 0) { //No alpha, then its JPEG
int errorLength = hasErrorHeader(imageData) ? 4 : 0;
return new ByteArrayInputStream(imageData.getArray(), imageData.getPos() + errorLength, imageData.getLength() - errorLength);
}
//Make PNG
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageHelper.write(getImage().getBufferedImage(), ImageFormat.PNG, baos);
return new ByteArrayInputStream(baos.toByteArray());
}
@Override
@@ -171,7 +180,10 @@ public class DefineBitsJPEG3Tag extends ImageTag implements AloneTag {
return cachedImage;
}
try {
BufferedImage image = ImageHelper.read(getImageData());
int errorLength = hasErrorHeader(imageData) ? 4 : 0;
ByteArrayInputStream bis = new ByteArrayInputStream(imageData.getArray(), imageData.getPos() + errorLength, imageData.getLength() - errorLength);
BufferedImage image = ImageHelper.read(bis);
if (image == null) {
Logger.getLogger(DefineBitsJPEG3Tag.class.getName()).log(Level.SEVERE, "Failed to load image");
return null;

View File

@@ -158,7 +158,7 @@ public class DefineBitsJPEG4Tag extends ImageTag implements AloneTag {
@Override
public ImageFormat getImageFormat() {
ImageFormat fmt = ImageTag.getImageFormat(imageData);
if (fmt == ImageFormat.JPEG) {
if (fmt == ImageFormat.JPEG && bitmapAlphaData.getLength() > 0) {
fmt = ImageFormat.PNG; //transparency
}
return fmt;
@@ -166,7 +166,15 @@ public class DefineBitsJPEG4Tag extends ImageTag implements AloneTag {
@Override
public InputStream getImageData() {
return new ByteArrayInputStream(imageData.getArray(), imageData.getPos(), imageData.getLength());
if (bitmapAlphaData.getLength() == 0) { //No alpha, then its JPEG
return new ByteArrayInputStream(imageData.getArray(), imageData.getPos(), imageData.getLength());
}
//Make PNG
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageHelper.write(getImage().getBufferedImage(), ImageFormat.PNG, baos);
return new ByteArrayInputStream(baos.toByteArray());
}
@Override
@@ -175,7 +183,8 @@ public class DefineBitsJPEG4Tag extends ImageTag implements AloneTag {
return cachedImage;
}
try {
BufferedImage image = ImageHelper.read(getImageData());
BufferedImage image = ImageHelper.read(new ByteArrayInputStream(imageData.getArray(), imageData.getPos(), imageData.getLength()));
if (image == null) {
Logger.getLogger(DefineBitsJPEG4Tag.class.getName()).log(Level.SEVERE, "Failed to load image");
return null;