diff --git a/.gitignore b/.gitignore index 43e23f6d5..6724a8300 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ hs_err_pid*.log /version.properties /tools.properties /nbproject/private/ +/libsrc/cmykjpeg/build/ +/libsrc/cmykjpeg/nbproject/private/ \ No newline at end of file diff --git a/lib/cmykjpeg.jar b/lib/cmykjpeg.jar new file mode 100644 index 000000000..179d50c90 Binary files /dev/null and b/lib/cmykjpeg.jar differ diff --git a/libsrc/cmykjpeg/build.xml b/libsrc/cmykjpeg/build.xml new file mode 100644 index 000000000..81f02213d --- /dev/null +++ b/libsrc/cmykjpeg/build.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + Builds, tests, and runs the project cmykjpeg. + + + diff --git a/libsrc/cmykjpeg/nbproject/build-impl.xml b/libsrc/cmykjpeg/nbproject/build-impl.xml new file mode 100644 index 000000000..4044e3e5a --- /dev/null +++ b/libsrc/cmykjpeg/nbproject/build-impl.xml @@ -0,0 +1,1413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libsrc/cmykjpeg/nbproject/genfiles.properties b/libsrc/cmykjpeg/nbproject/genfiles.properties new file mode 100644 index 000000000..605d6401d --- /dev/null +++ b/libsrc/cmykjpeg/nbproject/genfiles.properties @@ -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 diff --git a/libsrc/cmykjpeg/nbproject/project.properties b/libsrc/cmykjpeg/nbproject/project.properties new file mode 100644 index 000000000..e39fb97c5 --- /dev/null +++ b/libsrc/cmykjpeg/nbproject/project.properties @@ -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 diff --git a/libsrc/cmykjpeg/nbproject/project.xml b/libsrc/cmykjpeg/nbproject/project.xml new file mode 100644 index 000000000..f2591e5f5 --- /dev/null +++ b/libsrc/cmykjpeg/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + cmykjpeg + + + + + + + + + diff --git a/libsrc/cmykjpeg/src/org/monte/media/io/ByteArrayImageInputStream.java b/libsrc/cmykjpeg/src/org/monte/media/io/ByteArrayImageInputStream.java new file mode 100644 index 000000000..4baa50420 --- /dev/null +++ b/libsrc/cmykjpeg/src/org/monte/media/io/ByteArrayImageInputStream.java @@ -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. + *

+ * 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 buf[0] + * through buf[count-1] are the + * only bytes that can ever be read from the + * stream; element buf[streamPos] 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 buf. + * It is one greater than the position of + * the last byte within buf 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 int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. + *

+ * This read method + * cannot block. + * + * @return the next byte of data, or -1 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 len bytes of data into an array of bytes + * from this input stream. + * If streamPos equals count, + * then -1 is returned to indicate + * end of file. Otherwise, the number k + * of bytes read is equal to the smaller of + * len and count-streamPos. + * If k is positive, then bytes + * buf[streamPos] through buf[streamPos+k-1] + * are copied into b[off] through + * b[off+k-1] in the manner performed + * by System.arraycopy. The + * value k is added into streamPos + * and k is returned. + *

+ * This read method cannot block. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + */ + @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 n bytes of input from this input stream. Fewer + * bytes might be skipped if the end of the input stream is reached. + * The actual number k + * of bytes to be skipped is equal to the smaller + * of n and count-streamPos. + * The value k is added into streamPos + * and k 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. + *

+ * The value returned is count - streamPos, + * 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 ByteArrayInputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + *

+ */ + @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; + } +} diff --git a/libsrc/cmykjpeg/src/org/monte/media/io/ImageInputStreamAdapter.java b/libsrc/cmykjpeg/src/org/monte/media/io/ImageInputStreamAdapter.java new file mode 100644 index 000000000..30fd47813 --- /dev/null +++ b/libsrc/cmykjpeg/src/org/monte/media/io/ImageInputStreamAdapter.java @@ -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 int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. This method blocks until input data + * is available, the end of the stream is detected, or an exception + * is thrown. + *

+ * This method + * simply performs in.read() and returns the result. + * + * @return the next byte of data, or -1 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 len bytes of data from this input stream + * into an array of bytes. If len is not zero, the method + * blocks until some input is available; otherwise, no + * bytes are read and 0 is returned. + *

+ * This method simply performs in.read(b, off, len) + * and returns the result. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + * @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} + *

+ * This method simply performs in.skip(n). + */ + @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. + *

+ * 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 in.close(). + * + * @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 reset method repositions this stream at + * the last marked position so that subsequent reads re-read the same bytes. + *

+ * The readlimit argument tells this input stream to + * allow that many bytes to be read before the mark position gets + * invalidated. + *

+ * This method simply performs in.mark(readlimit). + * + * @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 + * mark method was last called on this input stream. + *

+ * This method + * simply performs in.reset(). + *

+ * 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 mark + * and reset methods. + * This method + * simply performs in.markSupported(). + * + * @return true if this stream type supports the + * mark and reset method; + * false otherwise. + * @see java.io.FilterInputStream#in + * @see java.io.InputStream#mark(int) + * @see java.io.InputStream#reset() + */ + @Override + public boolean markSupported() { + return true; + } + +} diff --git a/libsrc/cmykjpeg/src/org/monte/media/io/ImageInputStreamImpl2.java b/libsrc/cmykjpeg/src/org/monte/media/io/ImageInputStreamImpl2.java new file mode 100644 index 000000000..501224398 --- /dev/null +++ b/libsrc/cmykjpeg/src/org/monte/media/io/ImageInputStreamImpl2.java @@ -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. + *

+ * 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)); + } + } + +} diff --git a/libsrc/cmykjpeg/src/org/monte/media/jpeg/CMYKJPEGImageReader.java b/libsrc/cmykjpeg/src/org/monte/media/jpeg/CMYKJPEGImageReader.java new file mode 100644 index 000000000..206472bc4 --- /dev/null +++ b/libsrc/cmykjpeg/src/org/monte/media/jpeg/CMYKJPEGImageReader.java @@ -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 getImageTypes(int imageIndex) throws IOException { + readHeader(); + LinkedList l = new LinkedList(); + 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 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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; + } +} diff --git a/libsrc/cmykjpeg/src/org/monte/media/jpeg/CMYKJPEGImageReaderSpi.java b/libsrc/cmykjpeg/src/org/monte/media/jpeg/CMYKJPEGImageReaderSpi.java new file mode 100644 index 000000000..e15280fdb --- /dev/null +++ b/libsrc/cmykjpeg/src/org/monte/media/jpeg/CMYKJPEGImageReaderSpi.java @@ -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. + *
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"; + } +} diff --git a/libsrc/cmykjpeg/src/org/monte/media/jpeg/Generic CMYK Profile.icc b/libsrc/cmykjpeg/src/org/monte/media/jpeg/Generic CMYK Profile.icc new file mode 100644 index 000000000..458eb098c Binary files /dev/null and b/libsrc/cmykjpeg/src/org/monte/media/jpeg/Generic CMYK Profile.icc differ diff --git a/libsrc/cmykjpeg/src/org/monte/media/jpeg/JFIFInputStream.java b/libsrc/cmykjpeg/src/org/monte/media/jpeg/JFIFInputStream.java new file mode 100644 index 000000000..ebb0519c4 --- /dev/null +++ b/libsrc/cmykjpeg/src/org/monte/media/jpeg/JFIFInputStream.java @@ -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. + *

+ * This InputStream uses two special marker values which do not exist + * in the JFIF stream: + *

+ *

+ * 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. + *

+ * 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. + * + *

+ * References:
+ * JPEG File Interchange Format Version 1.02
+ * http://www.jpeg.org/public/jfif.pdf + *

+ * Pennebaker, W., Mitchell, J. (1993).
+ * JPEG Still Image Data Compression Standard.
+ * Chapmann & Hall, New York.
+ * ISBN 0-442-01272-1
+ * + * + * @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 standaloneMarkers = new HashSet(); + /** + * This hash set holds the Id's of markers which have a data + * segment followed by a entropy-coded data segment. + */ + private final HashSet doubleSegMarkers = new HashSet(); + + /** 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 int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. This method blocks until input data + * is available, the end of the stream is detected, or an exception + * is thrown. + *

+ * This method + * simply performs in.read() and returns the result. + * + * @return the next byte of data, or -1 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 len b of data from this input stream + * into an array of b. This method blocks until some input is + * available. + *

+ * This method simply performs in.read(b, off, len) + * 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 + * -1 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 n b of data from the + * input stream. The skip method may, for a variety of + * reasons, end up skipping over some smaller number of b, + * possibly 0. The actual number of b skipped is + * returned. + *

+ * This method + * simply performs in.skip(n). + * + * @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 reset method repositions this stream at + * the last marked position so that subsequent reads re-read the same b. + *

+ * The readlimit argument tells this input stream to + * allow that many b to be read before the mark position gets + * invalidated. + *

+ * This method simply performs in.mark(readlimit). + * + * @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 + * mark method was last called on this input stream. + *

+ * This method + * simply performs in.reset(). + *

+ * 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 mark + * and reset methods. + * This method + * simply performs in.markSupported(). + * + * @return true if this stream type supports the + * mark and reset method; + * false otherwise. + * @see java.io.FilterInputStream#in + * @see java.io.InputStream#mark(int) + * @see java.io.InputStream#reset() + */ + @Override + public boolean markSupported() { + return false; + } +} diff --git a/libsrc/ffdec_lib/nbproject/project.xml b/libsrc/ffdec_lib/nbproject/project.xml index 634fd0434..029f090fb 100644 --- a/libsrc/ffdec_lib/nbproject/project.xml +++ b/libsrc/ffdec_lib/nbproject/project.xml @@ -236,7 +236,7 @@ auxiliary.show.customizer.message= src - ../../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 + ../../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 build reports dist diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java index 45ef6916a..96453d508 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java @@ -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 exportImages(AbortRetryIgnoreHandler handler, String outdir, List tags, ImageExportSettings settings, EventListener evl) throws IOException { - List 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 exportImages(AbortRetryIgnoreHandler handler, String outdir, List tags, ImageExportSettings settings, EventListener evl) throws IOException { + List 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; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/helpers/ImageHelper.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/helpers/ImageHelper.java index 0442c9a2b..dee0b8fce 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/helpers/ImageHelper.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/helpers/ImageHelper.java @@ -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(); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG2Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG2Tag.java index 409ac02fc..b7ef635b5 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG2Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG2Tag.java @@ -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); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG3Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG3Tag.java index 7ce6edc6d..87391402a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG3Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG3Tag.java @@ -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; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG4Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG4Tag.java index b163dbd7a..d60790d11 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG4Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineBitsJPEG4Tag.java @@ -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;