diff options
author | Sven Gothel <[email protected]> | 2013-04-01 05:16:35 +0200 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2013-04-01 05:16:35 +0200 |
commit | a54bd3e963a7be320dee0c9692d237607fcd0f96 (patch) | |
tree | e365b6baeda020429355f9a7a98a7c3e65da713b | |
parent | 00c65b4a34445d140ca9e6ee531dfa4278b8e770 (diff) |
Fix Bug 671: Add JPEG Decoder w/o AWT Dependencies
Original JavaScript code from <https://github.com/notmasteryet/jpgjs/blob/master/jpg.js>,
author 'notmasteryet' <async.processingjs at yahoo.com>.
Ported to Java.
Enhancements:
* InputStream instead of memory buffer
* User provided memory handler
* Fixed JPEG Component ID/Index mapping
* Color space conversion (YCCK, CMYK -> RGB)
* More error tolerant
+++
Features:
JOGL AWT
RGB ok ok
YCCK ok Exception
CMYK ok Exception
YUV Store ok n/a
Need Y-Flip no yes
+++
Benchmark: TestJPEGJoglAWTBenchmarkNewtAWT
JOGL.RGB Loops 100, dt 1199 ms, 11.99 ms/l
JOGL.YUV Loops 100, dt 351 ms, 3.51 ms/l
AWT..... Loops 100, dt 2144 ms, 21.44 ms/l
File: jogl/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j1-baseline.jpg
Machine: GNU/Linux PC (AMD 8 core), JavaSE 6 (1.6.0_38)
.++++ UITestCase.setUp: com.jogamp.opengl.test.junit.jogl.util.texture.TestJPEGJoglAWTBenchmarkNewtAWT - benchmark
libEGL warning: DRI2: failed to authenticate
0: JPEGImage[261x202, bytesPerPixel 3, reversedChannels false, JPEGPixels[261x202, sourceComp 3, sourceCS YCbCr, storageCS RGB, storageComp 3], java.nio.DirectByteBuffer[pos=0 lim=158166 cap=158166]]
0: TextureData[261x202, y-flip false, internFormat 0x1907, pixelFormat 0x1907, pixelType 0x1401, border 0, estSize 158166, alignment 1, rowlen 0, buffer java.nio.DirectByteBuffer[pos=0 lim=158166 cap=158166]
JOGL.RGB Loops 100, dt 1199 ms, 11.99 ms/l
0: JPEGImage[261x202, bytesPerPixel 3, reversedChannels false, JPEGPixels[261x202, sourceComp 3, sourceCS YCbCr, storageCS YCbCr, storageComp 3], java.nio.DirectByteBuffer[pos=0 lim=158166 cap=158166]]
0: TextureData[261x202, y-flip false, internFormat 0x1907, pixelFormat 0x1907, pixelType 0x1401, border 0, estSize 158166, alignment 1, rowlen 0, buffer java.nio.DirectByteBuffer[pos=0 lim=158166 cap=158166]
JOGL.YUV Loops 100, dt 351 ms, 3.51 ms/l
0: TextureData[261x202, y-flip true, internFormat 0x1907, pixelFormat 0x80e0, pixelType 0x1401, border 0, estSize 158166, alignment 1, rowlen 261, buffer java.nio.HeapByteBuffer[pos=0 lim=158166 cap=158166]
AWT..... Loops 100, dt 2144 ms, 21.44 ms/l
++++ UITestCase.tearDown: com.jogamp.opengl.test.junit.jogl.util.texture.TestJPEGJoglAWTBenchmarkNewtAWT - benchmark
19 files changed, 2595 insertions, 21 deletions
diff --git a/make/scripts/tests.sh b/make/scripts/tests.sh index 41a06ea90..48fa02a6c 100755 --- a/make/scripts/tests.sh +++ b/make/scripts/tests.sh @@ -199,6 +199,7 @@ function jrun() { #D_ARGS="-Dnewt.debug=all" #D_ARGS="-Djogl.debug.GLCanvas -Djogl.debug.GLJPanel" #D_ARGS="-Djogl.debug.PNGImage" + #D_ARGS="-Djogl.debug.JPEGImage" #X_ARGS="-Dsun.java2d.noddraw=True -Dsun.java2d.opengl=True -Dsun.java2d.xrender=false" #X_ARGS="-Dsun.java2d.noddraw=True -Dsun.java2d.opengl=false -Dsun.java2d.xrender=false" #X_ARGS="-verbose:jni" @@ -294,7 +295,7 @@ function testawtswt() { # #testnoawt com.jogamp.nativewindow.NativeWindowVersion $* #testnoawt com.jogamp.opengl.JoglVersion $* -testnoawt com.jogamp.newt.NewtVersion $* +#testnoawt com.jogamp.newt.NewtVersion $* #testnoawt com.jogamp.newt.opengl.GLWindow $* #testnoawt com.jogamp.opengl.test.junit.jogl.acore.TestFloatUtil01MatrixMatrixMultNOUI $* #testnoawt com.jogamp.opengl.test.junit.jogl.acore.TestPMVMatrix01NEWT $* @@ -495,7 +496,11 @@ testnoawt com.jogamp.newt.NewtVersion $* # Texture / TextureUtils # #testawt com.jogamp.opengl.test.junit.jogl.util.texture.TestTexture01AWT +#testnoawt com.jogamp.opengl.test.junit.jogl.util.texture.TestJPEGImage00NEWT $* #testnoawt com.jogamp.opengl.test.junit.jogl.util.texture.TestJPEGImage01NEWT $* +testnoawt com.jogamp.opengl.test.junit.jogl.util.texture.TestJPEGJoglAWTCompareNewtAWT $* +#testnoawt com.jogamp.opengl.test.junit.jogl.util.texture.TestJPEGJoglAWTBenchmarkNewtAWT $* +#testnoawt com.jogamp.opengl.test.junit.jogl.util.texture.TestJPEGTextureFromFileNEWT $* #testnoawt com.jogamp.opengl.test.junit.jogl.util.texture.TestPNGImage01NEWT $* #testawt com.jogamp.opengl.test.junit.jogl.util.texture.TestPNGTextureFromFileAWT $* #testnoawt com.jogamp.opengl.test.junit.jogl.util.texture.TestPNGTextureFromFileNEWT $* diff --git a/src/jogl/classes/com/jogamp/opengl/util/texture/TextureData.java b/src/jogl/classes/com/jogamp/opengl/util/texture/TextureData.java index 96ee233fd..5b72bea82 100644 --- a/src/jogl/classes/com/jogamp/opengl/util/texture/TextureData.java +++ b/src/jogl/classes/com/jogamp/opengl/util/texture/TextureData.java @@ -54,6 +54,8 @@ import com.jogamp.opengl.util.GLBuffers; */ public class TextureData { + public static enum ColorSpace { RGB, YCbCr, YCCK, CMYK }; + protected int width; protected int height; private int border; @@ -77,6 +79,7 @@ public class TextureData { protected boolean haveEXTABGR; protected boolean haveGL12; protected GLProfile glProfile; + protected ColorSpace pixelCS = ColorSpace.RGB; /** * Constructs a new TextureData object with the specified parameters @@ -217,6 +220,18 @@ public class TextureData { } } + /** + * Returns the color space of the pixel data. + * @see #setColorSpace(ColorSpace) + */ + public ColorSpace getColorSpace() { return pixelCS; } + + /** + * Set the color space of the pixel data, which defaults to {@link ColorSpace#RGB}. + * @see #getColorSpace() + */ + public void setColorSpace(ColorSpace cs) { pixelCS = cs; } + /** Used only by subclasses */ protected TextureData(GLProfile glp) { this.glProfile = glp; } diff --git a/src/jogl/classes/com/jogamp/opengl/util/texture/TextureIO.java b/src/jogl/classes/com/jogamp/opengl/util/texture/TextureIO.java index b878c6002..0b0af5625 100644 --- a/src/jogl/classes/com/jogamp/opengl/util/texture/TextureIO.java +++ b/src/jogl/classes/com/jogamp/opengl/util/texture/TextureIO.java @@ -63,6 +63,7 @@ import jogamp.opengl.Debug; import com.jogamp.common.util.IOUtil; import com.jogamp.opengl.util.texture.spi.DDSImage; +import com.jogamp.opengl.util.texture.spi.JPEGImage; import com.jogamp.opengl.util.texture.spi.NetPbmTextureWriter; import com.jogamp.opengl.util.texture.spi.PNGImage; import com.jogamp.opengl.util.texture.spi.SGIImage; @@ -714,8 +715,12 @@ public class TextureIO { // SPI support // - /** Adds a TextureProvider to support reading of a new file - format. */ + /** + * Adds a TextureProvider to support reading of a new file format. + * <p> + * The last provider added, will be the first provider to be tested. + * </p> + */ public static void addTextureProvider(TextureProvider provider) { // Must always add at the front so the ImageIO provider is last, // so we don't accidentally use it instead of a user's possibly @@ -723,8 +728,12 @@ public class TextureIO { textureProviders.add(0, provider); } - /** Adds a TextureWriter to support writing of a new file - format. */ + /** + * Adds a TextureWriter to support writing of a new file format. + * <p> + * The last provider added, will be the first provider to be tested. + * </p> + */ public static void addTextureWriter(TextureWriter writer) { // Must always add at the front so the ImageIO writer is last, // so we don't accidentally use it instead of a user's possibly @@ -768,7 +777,7 @@ public class TextureIO { private static List<TextureProvider> textureProviders = new ArrayList<TextureProvider>(); private static List<TextureWriter> textureWriters = new ArrayList<TextureWriter>(); - static { + static { // ImageIO provider, the fall-back, must be the first one added if(GLProfile.isAWTAvailable()) { try { @@ -787,6 +796,7 @@ public class TextureIO { addTextureProvider(new DDSTextureProvider()); addTextureProvider(new SGITextureProvider()); addTextureProvider(new TGATextureProvider()); + addTextureProvider(new JPGTextureProvider()); addTextureProvider(new PNGTextureProvider()); // ImageIO writer, the fall-back, must be the first one added @@ -1174,6 +1184,43 @@ public class TextureIO { } //---------------------------------------------------------------------- + // JPEG image provider + static class JPGTextureProvider extends StreamBasedTextureProvider { + public TextureData newTextureData(GLProfile glp, InputStream stream, + int internalFormat, + int pixelFormat, + boolean mipmap, + String fileSuffix) throws IOException { + if (JPG.equals(fileSuffix)) { + JPEGImage image = JPEGImage.read(/*glp, */ stream); + if (pixelFormat == 0) { + pixelFormat = image.getGLFormat(); + } + if (internalFormat == 0) { + if(glp.isGL2GL3()) { + internalFormat = (image.getBytesPerPixel()==4)?GL.GL_RGBA8:GL.GL_RGB8; + } else { + internalFormat = (image.getBytesPerPixel()==4)?GL.GL_RGBA:GL.GL_RGB; + } + } + return new TextureData(glp, internalFormat, + image.getWidth(), + image.getHeight(), + 0, + pixelFormat, + image.getGLType(), + mipmap, + false, + false, + image.getData(), + null); + } + + return null; + } + } + + //---------------------------------------------------------------------- // DDS texture writer // static class DDSTextureWriter implements TextureWriter { diff --git a/src/jogl/classes/com/jogamp/opengl/util/texture/awt/AWTTextureData.java b/src/jogl/classes/com/jogamp/opengl/util/texture/awt/AWTTextureData.java index ad96a9939..3b90fad65 100644 --- a/src/jogl/classes/com/jogamp/opengl/util/texture/awt/AWTTextureData.java +++ b/src/jogl/classes/com/jogamp/opengl/util/texture/awt/AWTTextureData.java @@ -37,15 +37,35 @@ package com.jogamp.opengl.util.texture.awt; import java.awt.AlphaComposite; -import java.awt.Color; import java.awt.Graphics2D; import java.awt.Transparency; -import java.awt.color.*; -import java.awt.image.*; -import java.nio.*; +import java.awt.image.BufferedImage; +import java.awt.image.ComponentColorModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferDouble; +import java.awt.image.DataBufferFloat; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.awt.image.WritableRaster; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; -import javax.media.opengl.*; -import com.jogamp.opengl.util.texture.*; +import javax.media.opengl.GL; +import javax.media.opengl.GL2; +import javax.media.opengl.GL2GL3; +import javax.media.opengl.GLException; +import javax.media.opengl.GLProfile; + +import com.jogamp.opengl.util.texture.TextureData; public class AWTTextureData extends TextureData { // Mechanism for lazily converting input BufferedImages with custom @@ -56,13 +76,13 @@ public class AWTTextureData extends TextureData { private boolean expectingEXTABGR; private boolean expectingGL12; - private static final ColorModel rgbaColorModel = - new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + private static final java.awt.image.ColorModel rgbaColorModel = + new ComponentColorModel(java.awt.color.ColorSpace.getInstance(java.awt.color.ColorSpace.CS_sRGB), new int[] {8, 8, 8, 8}, true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); - private static final ColorModel rgbColorModel = - new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + private static final java.awt.image.ColorModel rgbColorModel = + new ComponentColorModel(java.awt.color.ColorSpace.getInstance(java.awt.color.ColorSpace.CS_sRGB), new int[] {8, 8, 8, 0}, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); @@ -282,7 +302,7 @@ public class AWTTextureData extends TextureData { case BufferedImage.TYPE_BYTE_INDEXED: case BufferedImage.TYPE_CUSTOM: default: - ColorModel cm = image.getColorModel(); + java.awt.image.ColorModel cm = image.getColorModel(); if (cm.equals(rgbColorModel)) { pixelFormat = GL.GL_RGB; pixelType = GL.GL_UNSIGNED_BYTE; @@ -350,7 +370,7 @@ public class AWTTextureData extends TextureData { case BufferedImage.TYPE_BYTE_INDEXED: case BufferedImage.TYPE_CUSTOM: default: - ColorModel cm = image.getColorModel(); + java.awt.image.ColorModel cm = image.getColorModel(); if (cm.equals(rgbColorModel)) { pixelFormat = GL.GL_RGB; pixelType = GL.GL_UNSIGNED_BYTE; @@ -409,7 +429,7 @@ public class AWTTextureData extends TextureData { // create a temporary image that is compatible with OpenGL boolean hasAlpha = image.getColorModel().hasAlpha(); - ColorModel cm = null; + java.awt.image.ColorModel cm = null; int dataBufferType = image.getRaster().getDataBuffer().getDataType(); // Don't use integer components for packed int images if (isPackedInt(image)) { @@ -419,12 +439,12 @@ public class AWTTextureData extends TextureData { cm = hasAlpha ? rgbaColorModel : rgbColorModel; } else { if (hasAlpha) { - cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + cm = new ComponentColorModel(java.awt.color.ColorSpace.getInstance(java.awt.color.ColorSpace.CS_sRGB), null, true, true, Transparency.TRANSLUCENT, dataBufferType); } else { - cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + cm = new ComponentColorModel(java.awt.color.ColorSpace.getInstance(java.awt.color.ColorSpace.CS_sRGB), null, false, false, Transparency.OPAQUE, dataBufferType); diff --git a/src/jogl/classes/com/jogamp/opengl/util/texture/spi/JPEGImage.java b/src/jogl/classes/com/jogamp/opengl/util/texture/spi/JPEGImage.java new file mode 100644 index 000000000..71dd53939 --- /dev/null +++ b/src/jogl/classes/com/jogamp/opengl/util/texture/spi/JPEGImage.java @@ -0,0 +1,175 @@ +/** + * Copyright 2013 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.opengl.util.texture.spi; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import javax.media.opengl.GL; + +import jogamp.opengl.Debug; +import jogamp.opengl.util.jpeg.JPEGDecoder; + +import com.jogamp.common.nio.Buffers; +import com.jogamp.opengl.util.texture.TextureData.ColorSpace; + +public class JPEGImage { + private static final boolean DEBUG = Debug.debug("JPEGImage"); + + + /** + * Reads a JPEG image from the specified InputStream, using the given color space for storage. + * + * @param in + * @param cs Storage color space, either {@link ColorSpace#RGB} or {@link ColorSpace#YCbCr}. {@link ColorSpace#YCCK} and {@link ColorSpace#CMYK} will throw an exception! + * @return + * @throws IOException + */ + public static JPEGImage read(InputStream in, ColorSpace cs) throws IOException { + return new JPEGImage(in, cs); + } + + /** Reads a JPEG image from the specified InputStream, using the {@link ColorSpace#RGB}. */ + public static JPEGImage read(InputStream in) throws IOException { + return new JPEGImage(in, ColorSpace.RGB); + } + + private static class JPEGPixelStorage implements JPEGDecoder.PixelStorage { + int width=0, height=0; + int sourceComponents=0; + ColorSpace sourceCS = ColorSpace.YCbCr; + int storageComponents; + final ColorSpace storageCS; + ByteBuffer data = null; + + JPEGPixelStorage(ColorSpace storageCM) { + this.storageCS = storageCM; + switch(storageCS) { + case RGB: + case YCbCr: + storageComponents = 3; + break; + default: + throw new IllegalArgumentException("Unsupported storage color-space: "+storageCS); + } + } + + @Override + public final ColorSpace allocate(int width, int height, ColorSpace sourceCM, int sourceComponents) throws RuntimeException { + this.width = width; + this.height = height; + this.sourceComponents = sourceComponents; + this.sourceCS = sourceCM; + this.data = Buffers.newDirectByteBuffer(width * height * storageComponents); + return storageCS; + } + + @Override + public final void storeRGB(int x, int y, byte r, byte g, byte b) { + int i = ( ( height - y - 1 ) * width + x ) * storageComponents; + data.put(i++, r); + data.put(i++, g); + data.put(i++, b); + // data.put(i++, (byte)0xff); + } + + @Override + public final void store2(int x, int y, byte c1, byte c2) { + throw new RuntimeException("not supported yet"); + } + + @Override + public final void storeYCbCr(int x, int y, byte Y, byte Cb, byte Cr) { + int i = ( ( height - y - 1 ) * width + x ) * storageComponents; + data.put(i++, Y); + data.put(i++, Cb); + data.put(i++, Cr); + } + + public String toString() { + return "JPEGPixels["+width+"x"+height+", sourceComp "+sourceComponents+", sourceCS "+sourceCS+", storageCS "+storageCS+", storageComp "+storageComponents+"]"; + } + }; + + private JPEGImage(InputStream in, ColorSpace cs) throws IOException { + pixelStorage = new JPEGPixelStorage(cs); + final JPEGDecoder decoder = new JPEGDecoder(); + decoder.parse(in); + pixelWidth = decoder.getWidth(); + pixelHeight = decoder.getHeight(); + decoder.getPixel(pixelStorage, pixelWidth, pixelHeight); + data = pixelStorage.data; + final boolean hasAlpha = false; + + bytesPerPixel = 3; + glFormat = GL.GL_RGB; + reversedChannels = false; // RGB[A] + if(DEBUG) { + System.err.println("JPEGImage: alpha "+hasAlpha+", bytesPerPixel "+bytesPerPixel+ + ", pixels "+pixelWidth+"x"+pixelHeight+", glFormat 0x"+Integer.toHexString(glFormat)); + System.err.println("JPEGImage: "+decoder); + System.err.println("JPEGImage: "+pixelStorage); + } + decoder.clear(null); + } + private JPEGPixelStorage pixelStorage; + private final int pixelWidth, pixelHeight, glFormat, bytesPerPixel; + private boolean reversedChannels; + private final ByteBuffer data; + + /** Returns the color space of the pixel data */ + public ColorSpace getColorSpace() { return pixelStorage.storageCS; } + + /** Returns the number of components of the pixel data */ + public int getComponentCount() { return pixelStorage.storageComponents; } + + /** Returns the width of the image. */ + public int getWidth() { return pixelWidth; } + + /** Returns the height of the image. */ + public int getHeight() { return pixelHeight; } + + /** Returns true if data has the channels reversed to BGR or BGRA, otherwise RGB or RGBA is expected. */ + public boolean getHasReversedChannels() { return reversedChannels; } + + /** Returns the OpenGL format for this texture; e.g. GL.GL_LUMINANCE, GL.GL_RGB or GL.GL_RGBA. */ + public int getGLFormat() { return glFormat; } + + /** Returns the OpenGL data type: GL.GL_UNSIGNED_BYTE. */ + public int getGLType() { return GL.GL_UNSIGNED_BYTE; } + + /** Returns the bytes per pixel */ + public int getBytesPerPixel() { return bytesPerPixel; } + + /** Returns the raw data for this texture in the correct + (bottom-to-top) order for calls to glTexImage2D. */ + public ByteBuffer getData() { return data; } + + public String toString() { return "JPEGImage["+pixelWidth+"x"+pixelHeight+", bytesPerPixel "+bytesPerPixel+", reversedChannels "+reversedChannels+", "+pixelStorage+", "+data+"]"; } +} diff --git a/src/jogl/classes/jogamp/opengl/util/jpeg/JPEGDecoder.java b/src/jogl/classes/jogamp/opengl/util/jpeg/JPEGDecoder.java new file mode 100644 index 000000000..748e70d5b --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/jpeg/JPEGDecoder.java @@ -0,0 +1,1505 @@ +/** + * Original JavaScript code from <https://github.com/notmasteryet/jpgjs/blob/master/jpg.js>, + * ported to Java for JogAmp Community. + * + * Enhancements: + * * InputStream instead of memory buffer + * * User provided memory handler + * * Fixed JPEG Component ID/Index mapping + * * Color space conversion (YCCK, CMYK -> RGB) + * * More error tolerant + * + * ***************** + * + * Copyright 2011 notmasteryet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ***************** + * + * Copyright 2013 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package jogamp.opengl.util.jpeg; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; + +import jogamp.opengl.Debug; + +import com.jogamp.common.util.ArrayHashSet; +import com.jogamp.common.util.VersionNumber; +import com.jogamp.opengl.util.texture.TextureData.ColorSpace; + +/** + * + * <ul> + * <li> The JPEG specification can be found in the ITU CCITT Recommendation T.81 + * (www.w3.org/Graphics/JPEG/itu-t81.pdf) </li> + * <li> The JFIF specification can be found in the JPEG File Interchange Format + * (www.w3.org/Graphics/JPEG/jfif3.pdf)</li> + * <li> The Adobe Application-Specific JPEG markers in the Supporting the DCT Filters + * in PostScript Level 2, Technical Note #5116 + * (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf)</li> + * <li> http://halicery.com/jpeg/huffman.html </li> + * <li> https://en.wikipedia.org/wiki/Jpg#Syntax_and_structure </li> + * <li> http://www.cs.sfu.ca/CourseCentral/365/mark/material/notes/Chap4/Chap4.2/Chap4.2.html </li> + * <li> https://github.com/notmasteryet/jpgjs/blob/master/jpg.js </li> + * </ul> + */ +public class JPEGDecoder { + private static final boolean DEBUG = Debug.debug("JPEGImage"); + private static final boolean DEBUG_IN = false; + + public static interface PixelStorage { + /** + * @param width + * @param height + * @param sourceCS the color-space of the decoded JPEG + * @param sourceComponents number of components used for the given source color-space + * @return Either {@link ColorSpace#RGB} or {@link ColorSpace#YCbCr}. {@link ColorSpace#YCCK} and {@link ColorSpace#CMYK} will throw an exception! + * @throws RuntimeException + */ + public ColorSpace allocate(int width, int height, ColorSpace sourceCS, int sourceComponents) throws RuntimeException; + public void store2(int x, int y, byte c1, byte c2); + public void storeRGB(int x, int y, byte r, byte g, byte b); + public void storeYCbCr(int x, int y, byte Y, byte Cb, byte Cr); + } + + public static class JFIF { + final VersionNumber version; + final int densityUnits; + final int xDensity; + final int yDensity; + final int thumbWidth; + final int thumbHeight; + final byte[] thumbData; + + private JFIF(final byte data[]) { + version = new VersionNumber(data[5], data[6], 0); + densityUnits = data[7]; + xDensity = (data[8] << 8) | data[9]; + yDensity = (data[10] << 8) | data[11]; + thumbWidth = data[12]; + thumbHeight = data[13]; + if( 0 < thumbWidth && 0 < thumbHeight ) { + final int len = 14 + 3 * thumbWidth * thumbHeight; + thumbData = new byte[len]; + System.arraycopy(data, 14, thumbData, 0, len); + } else { + thumbData = null; + } + } + + public static final JFIF get(final byte[] data) throws RuntimeException { + if ( data[0] == (byte)0x4A && data[1] == (byte)0x46 && data[2] == (byte)0x49 && + data[3] == (byte)0x46 && data[4] == (byte)0x0) { // 'JFIF\x00' + final JFIF r = new JFIF(data); + return r; + } else { + return null; + } + } + + public final String toString() { + return "JFIF[ver "+version+", density[units "+densityUnits+", "+xDensity+"x"+yDensity+"], thumb "+thumbWidth+"x"+thumbHeight+"]"; + } + } + + public static class Adobe { + final short version; + final short flags0; + final short flags1; + final short colorCode; + final ColorSpace colorSpace; + + private Adobe(final byte[] data) { + version = data[6]; + flags0 = (short) ( (data[7] << 8) | data[8] ) ; + flags1 = (short) ( (data[9] << 8) | data[10] ) ; + colorCode = data[11]; + switch( colorCode ) { + case 2: colorSpace = ColorSpace.YCCK; break; + case 1: colorSpace = ColorSpace.YCbCr; break; + default: colorSpace = ColorSpace.CMYK; break; + } + } + public static final Adobe get(final byte[] data) throws RuntimeException { + if (data[0] == (byte)0x41 && data[1] == (byte)0x64 && data[2] == (byte)0x6F && + data[3] == (byte)0x62 && data[4] == (byte)0x65 && data[5] == (byte)0) { // 'Adobe\x00' + final Adobe r = new Adobe(data); + return r; + } else { + return null; + } + } + public final String toString() { + return "Adobe[ver "+version+", flags["+toHexString(flags0)+", "+toHexString(flags1)+"], colorSpace/Code "+colorSpace+"/"+toHexString(colorCode)+"]"; + } + } + /** TODO */ + public static class EXIF { + private EXIF(final byte data[]) { + } + + public static final EXIF get(final byte[] data) throws RuntimeException { + if ( data[0] == (byte)0x45 && data[1] == (byte)0x78 && data[2] == (byte)0x69 && + data[3] == (byte)0x66 && data[4] == (byte)0x0) { // 'Exif\x00' + final EXIF r = new EXIF(data); + return r; + } else { + return null; + } + } + public final String toString() { + return "EXIF[]"; + } + } + + @SuppressWarnings("serial") + public static class CodecException extends RuntimeException { + CodecException(String message) { + super(message); + } + } + @SuppressWarnings("serial") + public static class MarkerException extends CodecException { + final int marker; + MarkerException(int marker, String message) { + super(message+" - Marker "+toHexString(marker)); + this.marker = marker; + } + public int getMarker() { return marker; } + } + + /** Start of Image */ + private static final int M_SOI = 0xFFD8; + /** End of Image */ + private static final int M_EOI = 0xFFD9; + /** Start of Frame - Baseline DCT */ + private static final int M_SOF0 = 0xFFC0; + /** Start of Frame - Extended sequential DCT */ + // private static final int M_SOF1 = 0xFFC1; + /** Start of Frame - Progressive DCT */ + private static final int M_SOF2 = 0xFFC2; + /** DHT (Define Huffman Tables) */ + private static final int M_DHT = 0xFFC4; + // private static final int M_DAC = 0xFFCC; + /** SOS (Start of Scan) */ + private static final int M_SOS = 0xFFDA; + /** DQT (Define Quantization Tables) */ + private static final int M_QTT = 0xFFDB; + /** DRI (Define Restart Interval) */ + private static final int M_DRI = 0xFFDD; + /** APP0 (Application Specific) - JFIF Header */ + private static final int M_APP00 = 0xFFE0; + /** APP1 (Application Specific) - Exif Header */ + private static final int M_APP01 = 0xFFE1; + /** APP2 (Application Specific) */ + private static final int M_APP02 = 0xFFE2; + /** APP3 (Application Specific) */ + private static final int M_APP03 = 0xFFE3; + /** APP4 (Application Specific) */ + private static final int M_APP04 = 0xFFE4; + /** APP5 (Application Specific) */ + private static final int M_APP05 = 0xFFE5; + /** APP6 (Application Specific) */ + private static final int M_APP06 = 0xFFE6; + /** APP7 (Application Specific) */ + private static final int M_APP07 = 0xFFE7; + /** APP8 (Application Specific) */ + private static final int M_APP08 = 0xFFE8; + /** APP9 (Application Specific) */ + private static final int M_APP09 = 0xFFE9; + /** APP10 (Application Specific) */ + private static final int M_APP10 = 0xFFEA; + /** APP11 (Application Specific) */ + private static final int M_APP11 = 0xFFEB; + /** APP12 (Application Specific) */ + private static final int M_APP12 = 0xFFEC; + /** APP13 (Application Specific) */ + private static final int M_APP13 = 0xFFED; + /** APP14 (Application Specific) - ADOBE Header */ + private static final int M_APP14 = 0xFFEE; + /** APP15 (Application Specific) */ + private static final int M_APP15 = 0xFFEF; + + /** Annotation / Comment */ + private static final int M_ANO = 0xFFFE; + + static final int[] dctZigZag = new int[] { + 0, + 1, 8, + 16, 9, 2, + 3, 10, 17, 24, + 32, 25, 18, 11, 4, + 5, 12, 19, 26, 33, 40, + 48, 41, 34, 27, 20, 13, 6, + 7, 14, 21, 28, 35, 42, 49, 56, + 57, 50, 43, 36, 29, 22, 15, + 23, 30, 37, 44, 51, 58, + 59, 52, 45, 38, 31, + 39, 46, 53, 60, + 61, 54, 47, + 55, 62, + 63 + }; + + static final int dctCos1 = 4017; // cos(pi/16) + static final int dctSin1 = 799; // sin(pi/16) + static final int dctCos3 = 3406; // cos(3*pi/16) + static final int dctSin3 = 2276; // sin(3*pi/16) + static final int dctCos6 = 1567; // cos(6*pi/16) + static final int dctSin6 = 3784; // sin(6*pi/16) + static final int dctSqrt2 = 5793; // sqrt(2) + static final int dctSqrt1d2 = 2896; // sqrt(2) / 2 + + static class Frame { + final boolean progressive; + final int precision; + final int scanLines; + final int samplesPerLine; + private final ArrayHashSet<Integer> compIDs; + private final ComponentIn[] comps; + private final int compCount; + int maxCompID; + int maxH; + int maxV; + int mcusPerLine; + int mcusPerColumn; + + Frame(boolean progressive, int precision, int scanLines, int samplesPerLine, int componentsCount) { + this.progressive = progressive; + this.precision = precision; + this.scanLines = scanLines; + this.samplesPerLine = samplesPerLine; + compIDs = new ArrayHashSet<Integer>(componentsCount); + comps = new ComponentIn[componentsCount]; + this.compCount = componentsCount; + } + + private final void checkBounds(int idx) { + if( 0 > idx || idx >= compCount ) { + throw new CodecException("Idx out of bounds "+idx+", "+this); + } + } + + public final int getCompCount() { return compCount; } + public final int getMaxCompID() { return maxCompID; } + + public final void putOrdered(int compID, ComponentIn component) { + if( maxCompID < compID ) { + maxCompID = compID; + } + final int idx = compIDs.size(); + checkBounds(idx); + compIDs.add(compID); + comps[idx] = component; + } + public final ComponentIn getCompByIndex(int i) { + checkBounds(i); + return comps[i]; + } + public final ComponentIn getCompByID(int componentID) { + return getCompByIndex( compIDs.indexOf(componentID) ); + } + public final int getCompID(int idx) { + return compIDs.get(idx); + } + public final boolean hasCompID(int componentID) { + return compIDs.contains(componentID); + } + public final String toString() { + return "Frame[progressive "+progressive+", precision "+precision+", scanLines "+scanLines+", samplesPerLine "+samplesPerLine+ + ", components[count "+compCount+", maxID "+maxCompID+", componentIDs "+compIDs+", comps "+Arrays.asList(comps)+"]]"; + } + } + + /** The JPEG encoded components */ + class ComponentIn { + final int h, v; + final int[] quantizationTable; + int blocksPerColumn; + int blocksPerColumnForMcu; + int blocksPerLine; + int blocksPerLineForMcu; + /** [blocksPerColumnForMcu][blocksPerLineForMcu][64]; */ + int[][][] blocks; + int pred; + BinObj huffmanTableAC; + BinObj huffmanTableDC; + + ComponentIn(int h, int v, int[] quantizationTable) { + this.h = h; + this.v = v; + this.quantizationTable = quantizationTable; + } + + public final void allocateBlocks(int blocksPerColumn, int blocksPerColumnForMcu, int blocksPerLine, int blocksPerLineForMcu) { + this.blocksPerColumn = blocksPerColumn; + this.blocksPerColumnForMcu = blocksPerColumnForMcu; + this.blocksPerLine = blocksPerLine; + this.blocksPerLineForMcu = blocksPerLineForMcu; + this.blocks = new int[blocksPerColumnForMcu][blocksPerLineForMcu][64]; + } + public final int[] getBlock(int row, int col) { + if( row >= blocksPerColumnForMcu || col >= blocksPerLineForMcu ) { + throw new CodecException("Out of bounds given ["+row+"]["+col+"] - "+this); + } + return blocks[row][col]; + } + + public final String toString() { + return "CompIn[h "+h+", v "+v+", blocks["+blocksPerColumn+", mcu "+blocksPerColumnForMcu+"]["+blocksPerLine+", mcu "+blocksPerLineForMcu+"][64]]"; + } + } + + /** The decoded components */ + class ComponentOut { + private final ArrayList<byte[]> lines; + final float scaleX; + final float scaleY; + + ComponentOut(ArrayList<byte[]> lines, float scaleX, float scaleY) { + this.lines = lines; + this.scaleX = scaleX; + this.scaleY = scaleY; + } + + /** Safely returning a line, if index exceeds number of lines, last line is returned. */ + public final byte[] getLine(int i) { + final int sz = lines.size(); + return lines.get( i < sz ? i : sz - 1); + } + + public final String toString() { + return "CompOut[lines "+lines.size()+", scale "+scaleX+"x"+scaleY+"]"; + } + } + + public String toString() { + final String jfifS = null != jfif ? jfif.toString() : "JFIF nil"; + final String exifS = null != exif ? exif.toString() : "Exif nil"; + final String adobeS = null != adobe ? adobe.toString() : "Adobe nil"; + final String compOuts = null != components ? Arrays.asList(components).toString() : "nil"; + return "JPEG[size "+width+"x"+height+", compOut "+compOuts+", "+jfifS+", "+exifS+", "+adobeS+"]"; + } + + private BufferedInputStream istream; + private int _ipos = 0; + private int _iposSave = 0; + + private int width = 0; + private int height = 0; + private JFIF jfif = null; + private EXIF exif = null; + private Adobe adobe = null; + private ComponentOut[] components = null; + + public final JFIF getJFIFHeader() { return jfif; } + public final EXIF getEXIFHeader() { return exif; } + public final Adobe getAdobeHeader() { return adobe; } + public final int getWidth() { return width; } + public final int getHeight() { return height; } + + private final void resetInput(InputStream is) { + if( is instanceof BufferedInputStream ) { + istream = (BufferedInputStream) is; + } else { + istream = new BufferedInputStream(is); + } + _ipos = 0; + } + + private final void markStream(int readLimit) { + istream.mark(readLimit); + _iposSave = _ipos; + } + private final void rewindStream() throws IOException { + if(DEBUG_IN) { System.err.println("JPG.rewindStream: "+_ipos+" -> "+_iposSave); } + istream.reset(); + _ipos = _iposSave; + _iposSave = 0; + } + private final int readUint8() throws IOException { + final int r = istream.read(); + if( -1 < r ) { + if(DEBUG_IN) { System.err.println("u8["+_ipos+"]: "+toHexString(r)); } + _ipos++; + } else if(DEBUG_IN) { + System.err.println("u8["+_ipos+"]: EOS"); + } + return r; + } + + private final int readUint16() throws IOException { + final int hi = istream.read(); + if( -1 < hi ) { + _ipos++; + final int lo = istream.read(); + if( -1 < lo ) { + _ipos++; + final int r = hi << 8 | lo ; + if(DEBUG_IN) { System.err.println("u16["+(_ipos-2)+"]: "+toHexString(r)); } + return r; + } + } + if(DEBUG_IN) { System.err.println("u16["+_ipos+"]: EOS"); } + return -1; + } + + private final int readNumber() throws IOException { + final int len=readUint16(); + if(len!=4){ + throw new CodecException("ERROR: Define number format error [Len!=4, but "+len+"]"); + } + return readUint16(); + } + + private final byte[] readDataBlock() throws IOException { + int count=0, i=0; + final int len=readUint16(); count+=2; + byte[] data = new byte[len-2]; + while(count<len){ + data[i++] = (byte)readUint8(); count++; + } + if(DEBUG_IN) { System.err.println("JPEG.readDataBlock: net-len "+(len-2)+", "+this); dumpData(data, 0, len-2); } + return data; + } + static final void dumpData(byte[] data, int offset, int len) { + for(int i=0; i<len; ) { + System.err.print(i%8+": "); + for(int j=0; j<8 && i<len; j++, i++) { + System.err.println(toHexString(0x000000FF & data[offset+i])+", "); + } + System.err.println(""); + } + } + + public synchronized void clear(InputStream inputStream) { + resetInput(inputStream); + width = 0; + height = 0; + jfif = null; + exif = null; + adobe = null; + components = null; + } + public synchronized JPEGDecoder parse(final InputStream inputStream) throws IOException { + clear(inputStream); + Frame frame = null; + int resetInterval = 0; + int[][] quantizationTables = new int[0x0F][]; // 4 bits + ArrayList<Frame> frames = new ArrayList<Frame>(); + final BinObj[] huffmanTablesAC = new BinObj[0x0F]; // Huffman table spec - 4 bits + final BinObj[] huffmanTablesDC = new BinObj[0x0F]; // Huffman table spec - 4 bits + int fileMarker = readUint16(); + if ( fileMarker != M_SOI ) { + throw new CodecException("SOI not found, but has marker "+toHexString(fileMarker)); + } + + fileMarker = readUint16(); + while (fileMarker != M_EOI) { + if(DEBUG) { System.err.println("JPG.parse got marker "+toHexString(fileMarker)); } + switch(fileMarker) { + case M_APP00: + case M_APP01: + case M_APP02: + case M_APP03: + case M_APP04: + case M_APP05: + case M_APP06: + case M_APP07: + case M_APP08: + case M_APP09: + case M_APP10: + case M_APP11: + case M_APP12: + case M_APP13: + case M_APP14: + case M_APP15: + case M_ANO: { + final byte[] appData = readDataBlock(); + + if ( fileMarker == M_APP00 ) { + jfif = JFIF.get( appData ); + } + if ( fileMarker == M_APP01 ) { + exif = EXIF.get(appData); + } + if (fileMarker == M_APP14) { + adobe = Adobe.get(appData); + } + fileMarker = 0; // consumed and get-next + } + break; + + case M_QTT: { + int count = 0; + final int quantizationTablesLength = readUint16(); count+=2; + while( count < quantizationTablesLength ) { + final int quantizationTableSpec = readUint8(); count++; + final int precisionID = quantizationTableSpec >> 4; + final int[] tableData = new int[64]; + if ( precisionID == 0 ) { // 8 bit values + for (int j = 0; j < 64; j++) { + final int z = dctZigZag[j]; + tableData[z] = readUint8(); count++; + } + } else if ( precisionID == 1) { //16 bit + for (int j = 0; j < 64; j++) { + final int z = dctZigZag[j]; + tableData[z] = readUint16(); count+=2; + } + } else { + throw new CodecException("DQT: invalid table precision "+precisionID+", quantizationTableSpec "+quantizationTableSpec); + } + quantizationTables[quantizationTableSpec & 0x0F] = tableData; + } + if(count!=quantizationTablesLength){ + throw new CodecException("ERROR: QTT format error [count!=Length]"); + } + fileMarker = 0; // consumed and get-next + } + break; + + case M_SOF0: + case M_SOF2: { + int count = 0; + final int sofLen = readUint16(); count+=2; // header length; + final int componentsCount; + { + final boolean progressive = (fileMarker == M_SOF2); + final int precision = readUint8(); count++; + final int scanLines = readUint16(); count+=2; + final int samplesPerLine = readUint16(); count+=2; + componentsCount = readUint8(); count++; + frame = new Frame(progressive, precision, scanLines, samplesPerLine, componentsCount); + width = frame.samplesPerLine; + height = frame.scanLines; + } + for (int i = 0; i < componentsCount; i++) { + final int componentId = readUint8(); count++; + final int temp = readUint8(); count++; + final int h = temp >> 4; + final int v = temp & 0x0F; + final int qId = readUint8(); count++; + frame.putOrdered(componentId, new ComponentIn(h, v, quantizationTables[qId])); + } + if(count!=sofLen){ + throw new CodecException("ERROR: SOF format error [count!=Length]"); + } + prepareComponents(frame); + frames.add(frame); + if(DEBUG) { System.err.println("JPG.parse.SOF[02]: Got frame "+frame); } + fileMarker = 0; // consumed and get-next + } + break; + + case M_DHT: { + int count = 0; + final int huffmanLength = readUint16(); count+=2; + int i=count, codeLengthTotal = 0; + while( i < huffmanLength ) { + final int huffmanTableSpec = readUint8(); count++; + final int[] codeLengths = new int[16]; + int codeLengthSum = 0; + for (int j = 0; j < 16; j++) { + codeLengthSum += (codeLengths[j] = readUint8()); count++; + } + final byte[] huffmanValues = new byte[codeLengthSum]; + for (int j = 0; j < codeLengthSum; j++) { + huffmanValues[j] = (byte)readUint8(); count++; + } + codeLengthTotal += codeLengthSum; + i += 17 + codeLengthSum; + final BinObj[] table = ( huffmanTableSpec >> 4 ) == 0 ? huffmanTablesDC : huffmanTablesAC; + table[huffmanTableSpec & 0x0F] = buildHuffmanTable(codeLengths, huffmanValues); + } + if(count!=huffmanLength || i!=count){ + throw new CodecException("ERROR: Huffman table format error [count!=Length]"); + } + if(DEBUG) { System.err.println("JPG.parse.DHT: Got Huffman CodeLengthTotal "+codeLengthTotal); } + fileMarker = 0; // consumed and get-next + } + break; + + case M_DRI: + resetInterval = readNumber(); + if(DEBUG) { System.err.println("JPG.parse.DRI: Got Reset Interval "+resetInterval); } + fileMarker = 0; // consumed and get-next + break; + + case M_SOS: { + int count = 0; + final int sosLen = readUint16(); count+=2; + final int selectorsCount = readUint8(); count++; + ArrayList<ComponentIn> components = new ArrayList<ComponentIn>(); + if(DEBUG) { System.err.println("JPG.parse.SOS: selectorCount [0.."+(selectorsCount-1)+"]: "+frame); } + for (int i = 0; i < selectorsCount; i++) { + final int compID = readUint8(); count++; + final ComponentIn component = frame.getCompByID(compID); + final int tableSpec = readUint8(); count++; + component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4]; + component.huffmanTableAC = huffmanTablesAC[tableSpec & 15]; + components.add(component); + } + final int spectralStart = readUint8(); count++; + final int spectralEnd = readUint8(); count++; + final int successiveApproximation = readUint8(); count++; + if(count!=sosLen){ + throw new CodecException("ERROR: scan header format error [count!=Length]"); + } + fileMarker = decoder.decodeScan(frame, components, resetInterval, + spectralStart, spectralEnd, + successiveApproximation >> 4, successiveApproximation & 15); + if(DEBUG) { System.err.println("JPG.parse.SOS.decode result "+toHexString(fileMarker)); } + } + break; + default: + /** + if (data[offset - 3] == 0xFF && + data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) { + // could be incorrect encoding -- last 0xFF byte of the previous + // block was eaten by the encoder + offset -= 3; + break; + } */ + throw new CodecException("unknown JPEG marker " + toHexString(fileMarker)); + } + if( 0 == fileMarker ) { + fileMarker = readUint16(); + } + } + if(DEBUG) { System.err.println("JPG.parse.2: End of parsing input "+this); } + if ( frames.size() != 1 ) { + throw new CodecException("only single frame JPEGs supported"); + } + + final int compCount = frame.getCompCount(); + this.components = new ComponentOut[compCount]; + for (int i = 0; i < compCount; i++) { + final ComponentIn component = frame.getCompByIndex(i); + this.components[i] = new ComponentOut( output.buildComponentData(frame, component), + (float)component.h / (float)frame.maxH, + (float)component.v / (float)frame.maxV ); + } + if(DEBUG) { System.err.println("JPG.parse.X: End of processing input "+this); } + return this; + } + + private void prepareComponents(Frame frame) { + int maxH = 0, maxV = 0; + // for (componentId in frame.components) { + final int compCount = frame.getCompCount(); + for (int i=0; i<compCount; i++) { + final ComponentIn component = frame.getCompByIndex(i); + if (maxH < component.h) maxH = component.h; + if (maxV < component.v) maxV = component.v; + } + int mcusPerLine = (int) Math.ceil((float)frame.samplesPerLine / 8f / (float)maxH); + int mcusPerColumn = (int) Math.ceil((float)frame.scanLines / 8f / (float)maxV); + // for (componentId in frame.components) { + for (int i=0; i<compCount; i++) { + final ComponentIn component = frame.getCompByIndex(i); + final int blocksPerLine = (int) Math.ceil(Math.ceil((float)frame.samplesPerLine / 8f) * component.h / maxH); + final int blocksPerColumn = (int) Math.ceil(Math.ceil((float)frame.scanLines / 8f) * component.v / maxV); + final int blocksPerLineForMcu = mcusPerLine * component.h; + final int blocksPerColumnForMcu = mcusPerColumn * component.v; + component.allocateBlocks(blocksPerColumn, blocksPerColumnForMcu, blocksPerLine, blocksPerLineForMcu); + } + frame.maxH = maxH; + frame.maxV = maxV; + frame.mcusPerLine = mcusPerLine; + frame.mcusPerColumn = mcusPerColumn; + } + + private static class BinObjIdxed { + final BinObj children; + byte index; + BinObjIdxed() { + this.children = new BinObj(); + this.index = 0; + } + } + private static class BinObj { + final boolean isValue; + final BinObj[] tree; + final byte b; + + BinObj(byte b) { + this.isValue= true; + this.b = b; + this.tree = null; + } + BinObj() { + this.isValue= false; + this.b = (byte)0; + this.tree = new BinObj[2]; + } + final byte getValue() { return b; } + final BinObj get(int i) { return tree[i]; } + final void set(byte i, byte v) { tree[i] = new BinObj(v); } + final void set(byte i, BinObj v) { tree[i] = v; } + } + + private BinObj buildHuffmanTable(int[] codeLengths, byte[] values) { + int k = 0; + int length = 16; + final ArrayList<BinObjIdxed> code = new ArrayList<BinObjIdxed>(); + while (length > 0 && 0==codeLengths[length - 1]) { + length--; + } + code.add(new BinObjIdxed()); + BinObjIdxed p = code.get(0), q; + for (int i = 0; i < length; i++) { + for (int j = 0; j < codeLengths[i]; j++) { + p = code.remove(code.size()-1); + p.children.set(p.index, values[k]); + while (p.index > 0) { + p = code.remove(code.size()-1); + } + p.index++; + code.add(p); + while (code.size() <= i) { + q = new BinObjIdxed(); + code.add(q); + p.children.set(p.index, q.children); + p = q; + } + k++; + } + if (i + 1 < length) { + // p here points to last code + q = new BinObjIdxed(); + code.add(q); + p.children.set(p.index, q.children); + p = q; + } + } + return code.get(0).children; + } + + private final Output output = new Output(); + private static class Output { + private int blocksPerLine; + private int blocksPerColumn; + private int samplesPerLine; + + private ArrayList<byte[]> buildComponentData(Frame frame, ComponentIn component) { + ArrayList<byte[]> lines = new ArrayList<byte[]>(); + blocksPerLine = component.blocksPerLine; + blocksPerColumn = component.blocksPerColumn; + samplesPerLine = blocksPerLine << 3; + final int[] R = new int[64]; + final byte[] r = new byte[64]; + + for (int blockRow = 0; blockRow < blocksPerColumn; blockRow++) { + int scanLine = blockRow << 3; + for (int i = 0; i < 8; i++) { + lines.add(new byte[samplesPerLine]); + } + for (int blockCol = 0; blockCol < blocksPerLine; blockCol++) { + quantizeAndInverse(component.getBlock(blockRow, blockCol), r, R, component.quantizationTable); + + final int sample = blockCol << 3; + int offset = 0; + for (int j = 0; j < 8; j++) { + final byte[] line = lines.get(scanLine + j); + for (int i = 0; i < 8; i++) + line[sample + i] = r[offset++]; + } + } + } + return lines; + } + + // A port of poppler's IDCT method which in turn is taken from: + // Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, + // "Practical Fast 1-D DCT Algorithms with 11 Multiplications", + // IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, + // 988-991. + private void quantizeAndInverse(int[] zz, byte[] dataOut, int[] dataIn, int[] qt) { + int v0, v1, v2, v3, v4, v5, v6, v7, t; + int[] p = dataIn; + int i; + + // dequant + for (i = 0; i < 64; i++) { + p[i] = zz[i] * qt[i]; + } + + // inverse DCT on rows + for (i = 0; i < 8; ++i) { + int row = 8 * i; + + // check for all-zero AC coefficients + if (p[1 + row] == 0 && p[2 + row] == 0 && p[3 + row] == 0 && + p[4 + row] == 0 && p[5 + row] == 0 && p[6 + row] == 0 && + p[7 + row] == 0) { + t = (dctSqrt2 * p[0 + row] + 512) >> 10; + p[0 + row] = t; + p[1 + row] = t; + p[2 + row] = t; + p[3 + row] = t; + p[4 + row] = t; + p[5 + row] = t; + p[6 + row] = t; + p[7 + row] = t; + continue; + } + + // stage 4 + v0 = (dctSqrt2 * p[0 + row] + 128) >> 8; + v1 = (dctSqrt2 * p[4 + row] + 128) >> 8; + v2 = p[2 + row]; + v3 = p[6 + row]; + v4 = (dctSqrt1d2 * (p[1 + row] - p[7 + row]) + 128) >> 8; + v7 = (dctSqrt1d2 * (p[1 + row] + p[7 + row]) + 128) >> 8; + v5 = p[3 + row] << 4; + v6 = p[5 + row] << 4; + + // stage 3 + t = (v0 - v1+ 1) >> 1; + v0 = (v0 + v1 + 1) >> 1; + v1 = t; + t = (v2 * dctSin6 + v3 * dctCos6 + 128) >> 8; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 128) >> 8; + v3 = t; + t = (v4 - v6 + 1) >> 1; + v4 = (v4 + v6 + 1) >> 1; + v6 = t; + t = (v7 + v5 + 1) >> 1; + v5 = (v7 - v5 + 1) >> 1; + v7 = t; + + // stage 2 + t = (v0 - v3 + 1) >> 1; + v0 = (v0 + v3 + 1) >> 1; + v3 = t; + t = (v1 - v2 + 1) >> 1; + v1 = (v1 + v2 + 1) >> 1; + v2 = t; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p[0 + row] = v0 + v7; + p[7 + row] = v0 - v7; + p[1 + row] = v1 + v6; + p[6 + row] = v1 - v6; + p[2 + row] = v2 + v5; + p[5 + row] = v2 - v5; + p[3 + row] = v3 + v4; + p[4 + row] = v3 - v4; + } + + // inverse DCT on columns + for (i = 0; i < 8; ++i) { + int col = i; + + // check for all-zero AC coefficients + if (p[1*8 + col] == 0 && p[2*8 + col] == 0 && p[3*8 + col] == 0 && + p[4*8 + col] == 0 && p[5*8 + col] == 0 && p[6*8 + col] == 0 && + p[7*8 + col] == 0) { + t = (dctSqrt2 * dataIn[i+0] + 8192) >> 14; + p[0*8 + col] = t; + p[1*8 + col] = t; + p[2*8 + col] = t; + p[3*8 + col] = t; + p[4*8 + col] = t; + p[5*8 + col] = t; + p[6*8 + col] = t; + p[7*8 + col] = t; + continue; + } + + // stage 4 + v0 = (dctSqrt2 * p[0*8 + col] + 2048) >> 12; + v1 = (dctSqrt2 * p[4*8 + col] + 2048) >> 12; + v2 = p[2*8 + col]; + v3 = p[6*8 + col]; + v4 = (dctSqrt1d2 * (p[1*8 + col] - p[7*8 + col]) + 2048) >> 12; + v7 = (dctSqrt1d2 * (p[1*8 + col] + p[7*8 + col]) + 2048) >> 12; + v5 = p[3*8 + col]; + v6 = p[5*8 + col]; + + // stage 3 + t = (v0 - v1 + 1) >> 1; + v0 = (v0 + v1 + 1) >> 1; + v1 = t; + t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12; + v3 = t; + t = (v4 - v6 + 1) >> 1; + v4 = (v4 + v6 + 1) >> 1; + v6 = t; + t = (v7 + v5 + 1) >> 1; + v5 = (v7 - v5 + 1) >> 1; + v7 = t; + + // stage 2 + t = (v0 - v3 + 1) >> 1; + v0 = (v0 + v3 + 1) >> 1; + v3 = t; + t = (v1 - v2 + 1) >> 1; + v1 = (v1 + v2 + 1) >> 1; + v2 = t; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p[0*8 + col] = v0 + v7; + p[7*8 + col] = v0 - v7; + p[1*8 + col] = v1 + v6; + p[6*8 + col] = v1 - v6; + p[2*8 + col] = v2 + v5; + p[5*8 + col] = v2 - v5; + p[3*8 + col] = v3 + v4; + p[4*8 + col] = v3 - v4; + } + + // convert to 8-bit integers + for (i = 0; i < 64; ++i) { + int sample = 128 + ((p[i] + 8) >> 4); + dataOut[i] = (byte) ( sample < 0 ? 0 : sample > 0xFF ? 0xFF : sample ); + } + } + } + + private static interface DecoderFunction { + void decode(ComponentIn component, int[] zz) throws IOException; + } + + private class Decoder { + // private int precision; + // private int samplesPerLine; + // private int scanLines; + private int mcusPerLine; + private boolean progressive; + // private int maxH, maxV; + private int bitsData, bitsCount; + private int spectralStart, spectralEnd; + private int successive; + private int eobrun; + private int successiveACState, successiveACNextValue; + + private int decodeScan(Frame frame, ArrayList<ComponentIn> components, int resetInterval, + int spectralStart, int spectralEnd, int successivePrev, int successive) throws IOException { + // this.precision = frame.precision; + // this.samplesPerLine = frame.samplesPerLine; + // this.scanLines = frame.scanLines; + this.mcusPerLine = frame.mcusPerLine; + this.progressive = frame.progressive; + // this.maxH = frame.maxH; + // this.maxV = frame.maxV; + this.bitsData = 0; + this.bitsCount = 0; + this.spectralStart = spectralStart; + this.spectralEnd = spectralEnd; + this.successive = successive; + + final int componentsLength = components.size(); + + final DecoderFunction decodeFn; + if (progressive) { + if (spectralStart == 0) { + decodeFn = successivePrev == 0 ? decodeDCFirst : decodeDCSuccessive; + } else { + decodeFn = successivePrev == 0 ? decodeACFirst : decodeACSuccessive; + } + } else { + decodeFn = decodeBaseline; + } + + int mcu = 0; + int mcuExpected; + if (componentsLength == 1) { + final ComponentIn c = components.get(0); + mcuExpected = c.blocksPerLine * c.blocksPerColumn; + } else { + mcuExpected = mcusPerLine * frame.mcusPerColumn; + } + if (0 == resetInterval) { + resetInterval = mcuExpected; + } + if(DEBUG) { + System.err.println("JPEG.decodeScan.1 resetInterval "+resetInterval+", mcuExpected "+mcuExpected+", sA "+spectralStart+", sP "+successivePrev+", sE "+spectralEnd+", suc "+successive+", decodeFn "+decodeFn.getClass().getSimpleName()); + } + int marker = 0; + while ( /* untilMarker || */ mcu < mcuExpected) { + // reset interval stuff + for (int i = 0; i < componentsLength; i++) { + components.get(i).pred = 0; + } + eobrun = 0; + + try { + if (componentsLength == 1) { + final ComponentIn component = components.get(0); + for (int n = 0; n < resetInterval; n++) { + decodeBlock(component, decodeFn, mcu); + mcu++; + } + } else { + for (int n = 0; n < resetInterval; n++) { + for (int i = 0; i < componentsLength; i++) { + final ComponentIn component = components.get(i); + final int h = component.h; + final int v = component.v; + for (int j = 0; j < v; j++) { + for (int k = 0; k < h; k++) { + decodeMcu(component, decodeFn, mcu, j, k); + } + } + } + mcu++; + } + } + } catch (MarkerException markerException) { + if(DEBUG) { System.err.println("JPEG.decodeScan: Marker exception: "+markerException.getMessage()); markerException.printStackTrace(); } + return markerException.getMarker(); + } catch (CodecException codecException) { + if(DEBUG) { System.err.println("JPEG.decodeScan: Codec exception: "+codecException.getMessage()); codecException.printStackTrace(); } + bitsCount = 0; + return M_EOI; // force end ! + } + + // find marker + bitsCount = 0; + markStream(2); + marker = readUint16(); + if( marker < 0xFF00 ) { + rewindStream(); + throw new CodecException("marker not found @ mcu "+mcu+"/"+mcuExpected+", u16: "+toHexString(marker)); + } + final boolean isRSTx = 0xFFD0 <= marker && marker <= 0xFFD7; // !RSTx + if(DEBUG) { + System.err.println("JPEG.decodeScan: MCUs "+mcu+"/"+mcuExpected+", u16 "+toHexString(marker)+", RSTx "+isRSTx+", "+frame); + } + if ( !isRSTx ) { + break; // handle !RSTx marker in caller + } + } + return marker; + } + + private int readBit() throws MarkerException, IOException { + if (bitsCount > 0) { + bitsCount--; + return (bitsData >> bitsCount) & 1; + } + bitsData = readUint8(); + if( -1 == bitsData ) { + return -1; + } + if (bitsData == 0xFF) { // marker prefix + final int nextByte = readUint8(); // marker signature + if( -1 == nextByte ) { + throw new CodecException("marked prefix 0xFF, then EOF"); + } + if (0 != nextByte) { + final int marker = (bitsData << 8) | nextByte; + throw new MarkerException(marker, "Marker at readBit file pos " + _ipos); + } + // unstuff 0 + } + bitsCount = 7; + return bitsData >>> 7; + } + + private int decodeHuffman(BinObj tree) throws IOException { + BinObj node = tree; + int bit; + while ( ( bit = readBit() ) != -1 ) { + node = node.get(bit); + if ( node.isValue ) { + return 0x000000FF & node.getValue(); + } + } + throw new CodecException("EOF reached at "+_ipos); + } + private int receive(int length) throws IOException { + int n = 0; + while (length > 0) { + final int bit = readBit(); + if (bit == -1) { + return -1; + } + n = (n << 1) | bit; + length--; + } + return n; + } + private int receiveAndExtend(int length) throws IOException { + final int n = receive(length); + if (n >= 1 << (length - 1)) { + return n; + } + return n + (-1 << length) + 1; + } + + final DecoderFunction decodeBaseline = new BaselineDecoder(); + final DecoderFunction decodeDCFirst = new DCFirstDecoder(); + final DecoderFunction decodeDCSuccessive = new DCSuccessiveDecoder(); + final DecoderFunction decodeACFirst = new ACFirstDecoder(); + final DecoderFunction decodeACSuccessive = new ACSuccessiveDecoder(); + + class BaselineDecoder implements DecoderFunction { + public void decode(ComponentIn component, int[] zz) throws IOException { + final int t = decodeHuffman(component.huffmanTableDC); + final int diff = ( t == 0 ) ? 0 : receiveAndExtend(t); + zz[0] = ( component.pred += diff ); + int k = 1; + while (k < 64) { + final int rs = decodeHuffman(component.huffmanTableAC); + final int s = rs & 15, r = rs >> 4; + if (s == 0) { + if (r < 15) { + break; + } + k += 16; + continue; + } + k += r; + final int z = dctZigZag[k]; + zz[z] = receiveAndExtend(s); + k++; + } + } + } + class DCFirstDecoder implements DecoderFunction { + public void decode(ComponentIn component, int[] zz) throws IOException { + final int t = decodeHuffman(component.huffmanTableDC); + final int diff = ( t == 0 ) ? 0 : (receiveAndExtend(t) << successive); + zz[0] = ( component.pred += diff ); + } + } + class DCSuccessiveDecoder implements DecoderFunction { + public void decode(ComponentIn component, int[] zz) throws IOException { + zz[0] |= readBit() << successive; + } + } + + class ACFirstDecoder implements DecoderFunction { + public void decode(ComponentIn component, int[] zz) throws IOException { + if (eobrun > 0) { + eobrun--; + return; + } + int k = spectralStart, e = spectralEnd; + while (k <= e) { + final int rs = decodeHuffman(component.huffmanTableAC); + final int s = rs & 15, r = rs >> 4; + if (s == 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r) - 1; + break; + } + k += 16; + continue; + } + k += r; + final int z = dctZigZag[k]; + zz[z] = receiveAndExtend(s) * (1 << successive); + k++; + } + } + } + class ACSuccessiveDecoder implements DecoderFunction { + public void decode(ComponentIn component, int[] zz) throws IOException { + int k = spectralStart, e = spectralEnd, r = 0; + while (k <= e) { + final int z = dctZigZag[k]; + switch (successiveACState) { + case 0: // initial state + final int rs = decodeHuffman(component.huffmanTableAC); + final int s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r); + successiveACState = 4; + } else { + r = 16; + successiveACState = 1; + } + } else { + // if (s !== 1) { + if (s != 1) { + throw new CodecException("invalid ACn encoding"); + } + successiveACNextValue = receiveAndExtend(s); + successiveACState = r != 0 ? 2 : 3; + } + continue; + case 1: // skipping r zero items + case 2: + if ( zz[z] != 0 ) { + zz[z] += (readBit() << successive); + } else { + r--; + if (r == 0) { + successiveACState = successiveACState == 2 ? 3 : 0; + } + } + break; + case 3: // set value for a zero item + if ( zz[z] != 0 ) { + zz[z] += (readBit() << successive); + } else { + zz[z] = successiveACNextValue << successive; + successiveACState = 0; + } + break; + case 4: // eob + if ( zz[z] != 0 ) { + zz[z] += (readBit() << successive); + } + break; + } + k++; + } + if (successiveACState == 4) { + eobrun--; + if (eobrun == 0) { + successiveACState = 0; + } + } + } + } + void decodeMcu(ComponentIn component, DecoderFunction decoder, int mcu, int row, int col) throws IOException { + final int mcuRow = (mcu / mcusPerLine) | 0; + final int mcuCol = mcu % mcusPerLine; + final int blockRow = mcuRow * component.v + row; + final int blockCol = mcuCol * component.h + col; + decoder.decode(component, component.getBlock(blockRow, blockCol)); + } + void decodeBlock(ComponentIn component, DecoderFunction decoder, int mcu) throws IOException { + final int blockRow = (mcu / component.blocksPerLine) | 0; + final int blockCol = mcu % component.blocksPerLine; + decoder.decode(component, component.getBlock(blockRow, blockCol)); + } + } + private final Decoder decoder = new Decoder(); + + /** wrong color space .. + private final void storeYCbCr2BGR(final PixelStorage pixelStorage, int x, int y, int Y, final int Cb, final int Cr) + { + if(Y<0) Y=0; + int B = Y + ( ( 116130 * Cb ) >> 16 ) ; + if(B<0) B=0; + else if(B>255) B=255; + + int G = Y - ( ( 22554 * Cb + 46802 * Cr ) >> 16 ) ; + if(G<0) G=0; + else if(G>255) G=255; + + int R = Y + ( ( 91881 * Cr ) >> 16 ); + if(R<0) R=0; + else if(R>255) R=255; + + pixelStorage.storeRGB(x, y, (byte)R, (byte)G, (byte)B); + } */ + + public synchronized void getPixel(PixelStorage pixelStorage, int width, int height) { + final int scaleX = this.width / width, scaleY = this.height / height; + + final int componentCount = this.components.length; + final ColorSpace sourceCS = ( null != adobe ) ? adobe.colorSpace : ColorSpace.YCbCr; + final ColorSpace storageCS = pixelStorage.allocate(width, height, sourceCS, componentCount); + if( ColorSpace.RGB != storageCS && ColorSpace.YCbCr != storageCS ) { + throw new IllegalArgumentException("Unsupported storage color space: "+storageCS); + } + + switch (componentCount) { + case 1: { + // Grayscale + final ComponentOut component1 = this.components[0]; + for (int y = 0; y < height; y++) { + final byte[] component1Line = component1.getLine((int)(y * component1.scaleY * scaleY)); + for (int x = 0; x < width; x++) { + final byte Y = component1Line[(int)(x * component1.scaleX * scaleX)]; + if( ColorSpace.YCbCr == storageCS ) { + pixelStorage.storeYCbCr(x, y, Y, (byte)0, (byte)0); + } else { + pixelStorage.storeRGB(x, y, Y, Y, Y); + } + } + } + } + break; + case 2: { + // PDF might compress two component data in custom colorspace + final ComponentOut component1 = this.components[0]; + final ComponentOut component2 = this.components[1]; + for (int y = 0; y < height; y++) { + final int ys = y * scaleY; + final byte[] component1Line = component1.getLine((int)(ys * component1.scaleY)); + final byte[] component2Line = component1.getLine((int)(ys * component2.scaleY)); + for (int x = 0; x < width; x++) { + final int xs = x * scaleX; + final byte Y1 = component1Line[(int)(xs * component1.scaleX)]; + final byte Y2 = component2Line[(int)(xs * component2.scaleX)]; + pixelStorage.store2(x, y, Y1, Y2); + } + } + } + break; + case 3: { + if (ColorSpace.YCbCr != sourceCS) { + throw new CodecException("Unsupported source color space w 3 components: "+sourceCS); + } + final ComponentOut component1 = this.components[0]; + final ComponentOut component2 = this.components[1]; + final ComponentOut component3 = this.components[2]; + for (int y = 0; y < height; y++) { + final int ys = y * scaleY; + final byte[] component1Line = component1.getLine((int)(ys * component1.scaleY)); + final byte[] component2Line = component2.getLine((int)(ys * component2.scaleY)); + final byte[] component3Line = component3.getLine((int)(ys * component3.scaleY)); + if( ColorSpace.YCbCr == storageCS ) { + for (int x = 0; x < width; x++) { + final int xs = x * scaleX; + final byte Y = component1Line[(int)(xs * component1.scaleX)]; + final byte Cb = component2Line[(int)(xs * component2.scaleX)]; + final byte Cr = component3Line[(int)(xs * component3.scaleX)]; + pixelStorage.storeYCbCr(x, y, Y, Cb, Cr); + } + } else { + for (int x = 0; x < width; x++) { + final int xs = x * scaleX; + final int Y = 0x000000FF & component1Line[(int)(xs * component1.scaleX)]; + final int Cb = 0x000000FF & component2Line[(int)(xs * component2.scaleX)]; + final int Cr = 0x000000FF & component3Line[(int)(xs * component3.scaleX)]; + // storeYCbCr2BGR(pixelStorage, x, y, Y, Cb, Cr); + final byte R = clampTo8bit(Y + 1.402f * (Cr - 128f)); + final byte G = clampTo8bit(Y - 0.3441363f * (Cb - 128f) - 0.71413636f * (Cr - 128f)); + final byte B = clampTo8bit(Y + 1.772f * (Cb - 128f)); + pixelStorage.storeRGB(x, y, R, G, B); + } + } + } + } + break; + case 4: { + if (ColorSpace.YCCK != sourceCS && ColorSpace.CMYK != sourceCS) { + throw new CodecException("Unsupported source color space w 4 components: "+sourceCS); + } + final ComponentOut component1 = this.components[0]; + final ComponentOut component2 = this.components[1]; + final ComponentOut component3 = this.components[2]; + final ComponentOut component4 = this.components[3]; + for (int y = 0; y < height; y++) { + final int ys = y * scaleY; + final byte[] component1Line = component1.getLine((int)(ys * component1.scaleY)); + final byte[] component2Line = component2.getLine((int)(ys * component2.scaleY)); + final byte[] component3Line = component3.getLine((int)(ys * component3.scaleY)); + final byte[] component4Line = component4.getLine((int)(ys * component4.scaleY)); + if( ColorSpace.YCbCr == storageCS ) { + if (ColorSpace.YCCK != sourceCS) { + throw new CodecException("Unsupported storage color space "+storageCS+" with source color space "+sourceCS); + } + for (int x = 0; x < width; x++) { + final int xs = x * scaleX; + final byte Y1 = component1Line[(int)(xs * component1.scaleX)]; + final byte C1 = component2Line[(int)(xs * component2.scaleX)]; + final byte C2 = component3Line[(int)(xs * component3.scaleX)]; + // final byte K = component4Line[(int)(xs * component4.scaleX)]; + // FIXME: YCCK is not really YCbCr, since K (black) is missing! + pixelStorage.storeYCbCr(x, y, Y1, C1, C2); + } + } else { + if (ColorSpace.CMYK == sourceCS) { + for (int x = 0; x < width; x++) { + final int xs = x * scaleX; + final int cC = 0x000000FF & component1Line[(int)(xs * component1.scaleX)]; + final int cM = 0x000000FF & component2Line[(int)(xs * component2.scaleX)]; + final int cY = 0x000000FF & component3Line[(int)(xs * component3.scaleX)]; + final int cK = 0x000000FF & component4Line[(int)(xs * component4.scaleX)]; + // CMYK -> RGB + final byte R = clampTo8bit( ( cC * cK ) / 255f ); + final byte G = clampTo8bit( ( cM * cK ) / 255f ); + final byte B = clampTo8bit( ( cY * cK ) / 255f ); + pixelStorage.storeRGB(x, y, R, G, B); + } + } else { // ColorModel.YCCK == sourceCM + for (int x = 0; x < width; x++) { + final int xs = x * scaleX; + final int Y = 0x000000FF & component1Line[(int)(xs * component1.scaleX)]; + final int Cb = 0x000000FF & component2Line[(int)(xs * component2.scaleX)]; + final int Cr = 0x000000FF & component3Line[(int)(xs * component3.scaleX)]; + final int cK = 0x000000FF & component4Line[(int)(xs * component4.scaleX)]; + // YCCK -> 255f - [ R'G'B' ] -> CMYK + final float cC = 255f - ( Y + 1.402f * (Cr - 128f) ); + final float cM = 255f - ( Y - 0.3441363f * (Cb - 128f) - 0.71413636f * (Cr - 128f) ); + final float cY = 255f - ( Y + 1.772f * (Cb - 128f) ); + // CMYK -> RGB + final byte R = clampTo8bit( ( cC * cK ) / 255f ); + final byte G = clampTo8bit( ( cM * cK ) / 255f ); + final byte B = clampTo8bit( ( cY * cK ) / 255f ); + pixelStorage.storeRGB(x, y, R, G, B); + } + } + } + } + } + break; + default: + throw new CodecException("Unsupported color model: Space "+sourceCS+", components "+componentCount); + } + } + + private static byte clampTo8bit(float a) { + return (byte) ( a < 0f ? 0 : a > 255f ? 255 : a ); + } + + private static String toHexString(int v) { + return "0x"+Integer.toHexString(v); + } +}
\ No newline at end of file diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGImage01NEWT.java b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGImage01NEWT.java new file mode 100644 index 000000000..37054afe6 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGImage01NEWT.java @@ -0,0 +1,124 @@ +package com.jogamp.opengl.test.junit.jogl.util.texture; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URLConnection; + +import javax.media.opengl.GLAutoDrawable; +import javax.media.opengl.GLCapabilities; +import javax.media.opengl.GLEventListener; +import javax.media.opengl.GLProfile; + +import org.junit.Assert; +import org.junit.Test; + +import com.jogamp.common.util.IOUtil; +import com.jogamp.newt.opengl.GLWindow; +import com.jogamp.opengl.test.junit.jogl.demos.TextureDraw01Accessor; +import com.jogamp.opengl.test.junit.jogl.demos.es2.TextureDraw01ES2Listener; +import com.jogamp.opengl.test.junit.util.MiscUtils; +import com.jogamp.opengl.test.junit.util.QuitAdapter; +import com.jogamp.opengl.test.junit.util.UITestCase; +import com.jogamp.opengl.util.Animator; +import com.jogamp.opengl.util.GLReadBufferUtil; +import com.jogamp.opengl.util.texture.TextureData; +import com.jogamp.opengl.util.texture.TextureIO; +import com.jogamp.opengl.util.texture.spi.JPEGImage; +import javax.media.opengl.GL; + +public class TestJPEGImage01NEWT extends UITestCase { + + static boolean showFPS = false; + static long duration = 100; // ms + + public void testImpl(final boolean withAlpha, final InputStream istream) throws InterruptedException, IOException { + final GLReadBufferUtil screenshot = new GLReadBufferUtil(true, false); + final GLProfile glp = GLProfile.getGL2ES2(); + final GLCapabilities caps = new GLCapabilities(glp); + if( withAlpha ) { + caps.setAlphaBits(1); + } + + final JPEGImage image = JPEGImage.read(istream); + Assert.assertNotNull(image); + System.err.println("JPEGImage: "+image); + + final int internalFormat = (image.getBytesPerPixel()==4)?GL.GL_RGBA:GL.GL_RGB; + final TextureData texData = new TextureData(glp, internalFormat, + image.getWidth(), + image.getHeight(), + 0, + image.getGLFormat(), + image.getGLType(), + false /* mipmap */, + false /* compressed */, + false /* must flip-vert */, + image.getData(), + null); + // final TextureData texData = TextureIO.newTextureData(glp, istream, false /* mipmap */, TextureIO.JPG); + System.err.println("TextureData: "+texData); + + final GLWindow glad = GLWindow.create(caps); + glad.setTitle("TestJPEGImage01NEWT"); + // Size OpenGL to Video Surface + glad.setSize(texData.getWidth(), texData.getHeight()); + + // load texture from file inside current GL context to match the way + // the bug submitter was doing it + final GLEventListener gle = new TextureDraw01ES2Listener( texData ) ; + glad.addGLEventListener(gle); + glad.addGLEventListener(new GLEventListener() { + boolean shot = false; + + @Override public void init(GLAutoDrawable drawable) {} + + public void display(GLAutoDrawable drawable) { + // 1 snapshot + if(null!=((TextureDraw01Accessor)gle).getTexture() && !shot) { + shot = true; + snapshot(0, null, drawable.getGL(), screenshot, TextureIO.PNG, null); + } + } + + @Override public void dispose(GLAutoDrawable drawable) { } + @Override public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { } + }); + + Animator animator = new Animator(glad); + animator.setUpdateFPSFrames(60, showFPS ? System.err : null); + QuitAdapter quitAdapter = new QuitAdapter(); + glad.addKeyListener(quitAdapter); + glad.addWindowListener(quitAdapter); + glad.setVisible(true); + animator.start(); + + while(!quitAdapter.shouldQuit() && animator.isAnimating() && animator.getTotalFPSDuration()<duration) { + Thread.sleep(100); + } + + animator.stop(); + glad.destroy(); + } + + @Test + public void testReadES2_RGB() throws InterruptedException, IOException, MalformedURLException { + final String fname = null == _fname ? "test-ntscN_3-01-160x90-90pct-yuv444-base.jpg" : _fname; + final URLConnection urlConn = IOUtil.getResource(this.getClass(), fname); + testImpl(false, urlConn.getInputStream()); + } + + static String _fname = null; + public static void main(String args[]) { + for(int i=0; i<args.length; i++) { + if(args[i].equals("-time")) { + i++; + duration = MiscUtils.atol(args[i], duration); + } else if(args[i].equals("-file")) { + i++; + _fname = args[i]; + } + } + org.junit.runner.JUnitCore.main(TestJPEGImage01NEWT.class.getName()); + } +} diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGJoglAWTBenchmarkNewtAWT.java b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGJoglAWTBenchmarkNewtAWT.java new file mode 100644 index 000000000..de8a4e2b5 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGJoglAWTBenchmarkNewtAWT.java @@ -0,0 +1,152 @@ +/** + * Copyright 2012 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package com.jogamp.opengl.test.junit.jogl.util.texture; + + +import com.jogamp.common.util.IOUtil; +import com.jogamp.opengl.test.junit.util.UITestCase; + +import javax.imageio.ImageIO; +import javax.media.opengl.GL; +import javax.media.opengl.GLProfile; + +import com.jogamp.opengl.util.texture.TextureData; +import com.jogamp.opengl.util.texture.awt.AWTTextureData; +import com.jogamp.opengl.util.texture.spi.JPEGImage; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; +import java.nio.Buffer; + +import org.junit.Test; + +public class TestJPEGJoglAWTBenchmarkNewtAWT extends UITestCase { + static boolean showFPS = false; + static String fname = "j1-baseline.jpg"; + + @Test + public void benchmark() throws IOException { + benchmarkImpl(100, fname); + } + void benchmarkImpl(int loops, String fname) throws IOException { + { + final long t0 = System.currentTimeMillis(); + for(int i = 0; i< loops; i++ ) { + final URLConnection urlConn = IOUtil.getResource(this.getClass(), fname); + final InputStream istream = urlConn.getInputStream(); + final JPEGImage image = JPEGImage.read(istream); // parsing & completion done !!! + final int internalFormat = (image.getBytesPerPixel()==4)?GL.GL_RGBA:GL.GL_RGB; + final TextureData texData = new TextureData(GLProfile.getGL2ES2(), internalFormat, + image.getWidth(), + image.getHeight(), + 0, + image.getGLFormat(), + image.getGLType(), + false /* mipmap */, + false /* compressed */, + false /* must flip-vert */, + image.getData(), + null); + if(0==i || loops-1==i) { + System.err.println(i+": "+image.toString()); + System.err.println(i+": "+texData+", buffer "+texData.getBuffer()); + } + istream.close(); + } + final long t1 = System.currentTimeMillis(); + final long dt = t1 - t0; + final float msPl = (float)dt / (float)loops ; + System.err.println("JOGL.RGB Loops "+loops+", dt "+dt+" ms, "+msPl+" ms/l"); + } + { + final long t0 = System.currentTimeMillis(); + for(int i = 0; i< loops; i++ ) { + final URLConnection urlConn = IOUtil.getResource(this.getClass(), fname); + final InputStream istream = urlConn.getInputStream(); + final JPEGImage image = JPEGImage.read(istream, TextureData.ColorSpace.YCbCr); // parsing & completion done !!! + final int internalFormat = (image.getBytesPerPixel()==4)?GL.GL_RGBA:GL.GL_RGB; + final TextureData texData = new TextureData(GLProfile.getGL2ES2(), internalFormat, + image.getWidth(), + image.getHeight(), + 0, + image.getGLFormat(), + image.getGLType(), + false /* mipmap */, + false /* compressed */, + false /* must flip-vert */, + image.getData(), + null); + if(0==i || loops-1==i) { + System.err.println(i+": "+image.toString()); + System.err.println(i+": "+texData+", buffer "+texData.getBuffer()); + } + istream.close(); + } + final long t1 = System.currentTimeMillis(); + final long dt = t1 - t0; + final float msPl = (float)dt / (float)loops ; + System.err.println("JOGL.YUV Loops "+loops+", dt "+dt+" ms, "+msPl+" ms/l"); + } + { + final long t0 = System.currentTimeMillis(); + for(int i = 0; i< loops; i++ ) { + final URLConnection urlConn = IOUtil.getResource(this.getClass(), fname); + final InputStream istream = urlConn.getInputStream(); + Buffer data = null; + try { + BufferedImage img = ImageIO.read(istream); + AWTTextureData texData = new AWTTextureData(GLProfile.getGL2ES2(), 0, 0, false, img); + data = texData.getBuffer(); // completes data conversion !!! + if(0==i || loops-1==i) { + System.err.println(i+": "+texData+", buffer "+data); + } + } catch (Exception e) { + System.err.println("AWT ImageIO failure w/ file "+fname+": "+e.getMessage()); + } + istream.close(); + } + final long t1 = System.currentTimeMillis(); + final long dt = t1 - t0; + final float msPl = (float)dt / (float)loops ; + System.err.println("AWT..... Loops "+loops+", dt "+dt+" ms, "+msPl+" ms/l"); + } + } + + public static void main(String args[]) throws IOException { + for(int i=0; i<args.length; i++) { + if(args[i].equals("-file")) { + i++; + fname = args[i]; + } + } + org.junit.runner.JUnitCore.main(TestJPEGJoglAWTBenchmarkNewtAWT.class.getName()); + } +} diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGJoglAWTCompareNewtAWT.java b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGJoglAWTCompareNewtAWT.java new file mode 100644 index 000000000..c85aa55b8 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGJoglAWTCompareNewtAWT.java @@ -0,0 +1,267 @@ +/** + * Copyright 2012 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package com.jogamp.opengl.test.junit.jogl.util.texture; + + +import com.jogamp.common.util.IOUtil; +import com.jogamp.newt.opengl.GLWindow; +import com.jogamp.opengl.test.junit.jogl.demos.TextureDraw01Accessor; +import com.jogamp.opengl.test.junit.jogl.demos.es2.TextureDraw01ES2Listener; +import com.jogamp.opengl.test.junit.util.MiscUtils; +import com.jogamp.opengl.test.junit.util.QuitAdapter; +import com.jogamp.opengl.test.junit.util.UITestCase; + +import javax.imageio.ImageIO; +import javax.media.opengl.GL; +import javax.media.opengl.GLAutoDrawable; +import javax.media.opengl.GLEventListener; +import javax.media.opengl.GLProfile; +import javax.media.opengl.GLCapabilities; + +import com.jogamp.opengl.util.texture.TextureData; +import com.jogamp.opengl.util.texture.TextureIO; +import com.jogamp.opengl.util.texture.awt.AWTTextureData; +import com.jogamp.opengl.util.texture.spi.JPEGImage; +import com.jogamp.opengl.util.Animator; +import com.jogamp.opengl.util.GLReadBufferUtil; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; + +import org.junit.Assert; +import org.junit.Test; + +public class TestJPEGJoglAWTCompareNewtAWT extends UITestCase { + static boolean showFPS = false; + static long duration = 100; // ms + + String[] files = { "test-ntscN_3-01-160x90-90pct-yuv444-base.jpg", // 0 + "test-ntscN_3-01-160x90-90pct-yuv444-prog.jpg", // 1 + "test-ntscN_3-01-160x90-60pct-yuv422h-base.jpg", // 2 + "test-ntscN_3-01-160x90-60pct-yuv422h-prog.jpg", // 3 + "j1-baseline.jpg", // 4 + "j2-progressive.jpg", // 5 + "j3-baseline_gray.jpg", // 6 + "test-cmyk-01.jpg", // 7 + "test-ycck-01.jpg" }; // 8 + + void testImpl(final String fname) throws InterruptedException, IOException { + final Animator animator = new Animator(); + + final GLWindow w1 = testJOGLJpeg(fname); + final GLWindow w2 = testAWTJpeg(fname, w1.getWidth() + 50); + + animator.add(w1); + animator.add(w2); + animator.setUpdateFPSFrames(60, showFPS ? System.err : null); + QuitAdapter quitAdapter = new QuitAdapter(); + w1.setVisible(true); + w2.setVisible(true); + animator.start(); + + while(!quitAdapter.shouldQuit() && animator.isAnimating() && animator.getTotalFPSDuration()<duration) { + Thread.sleep(100); + } + + animator.stop(); + w1.destroy(); + w2.destroy(); + } + + GLWindow testJOGLJpeg(final String fname) throws InterruptedException, IOException { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), fname); + Assert.assertNotNull(testTextureUrlConn); + InputStream istream = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(istream); + + final JPEGImage image = JPEGImage.read(istream); + Assert.assertNotNull(image); + System.err.println("JPEGImage: "+image); + + GLProfile glp = GLProfile.getGL2ES2(); + final int internalFormat = (image.getBytesPerPixel()==4)?GL.GL_RGBA:GL.GL_RGB; + final TextureData texData = new TextureData(glp, internalFormat, + image.getWidth(), + image.getHeight(), + 0, + image.getGLFormat(), + image.getGLType(), + false /* mipmap */, + false /* compressed */, + false /* must flip-vert */, + image.getData(), + null); + // final TextureData texData = TextureIO.newTextureData(glp, istream, false /* mipmap */, TextureIO.JPG); + System.err.println("TextureData: "+texData); + + final GLReadBufferUtil screenshot = new GLReadBufferUtil(true, false); + final GLCapabilities caps = new GLCapabilities(glp); + caps.setAlphaBits(1); + + final GLWindow glad1 = GLWindow.create(caps); + glad1.setTitle("JPEG JOGL"); + // Size OpenGL to Video Surface + glad1.setSize(texData.getWidth(), texData.getHeight()); + glad1.setPosition(0, 0); + + // load texture from file inside current GL context to match the way + // the bug submitter was doing it + final GLEventListener gle = new TextureDraw01ES2Listener( texData ) ; + glad1.addGLEventListener(gle); + glad1.addGLEventListener(new GLEventListener() { + boolean shot = false; + + @Override public void init(GLAutoDrawable drawable) {} + + public void display(GLAutoDrawable drawable) { + // 1 snapshot + if(null!=((TextureDraw01Accessor)gle).getTexture() && !shot) { + shot = true; + snapshot(0, "JoglJPEG", drawable.getGL(), screenshot, TextureIO.PNG, null); + } + } + + @Override public void dispose(GLAutoDrawable drawable) { } + @Override public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { } + }); + + return glad1; + } + + GLWindow testAWTJpeg(final String fname, int xpos) throws InterruptedException, IOException { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), fname); + Assert.assertNotNull(testTextureUrlConn); + InputStream istream = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(istream); + + GLProfile glp = GLProfile.getGL2ES2(); + TextureData texData = null; + int w = 300, h = 300; + try { + BufferedImage img = ImageIO.read(istream); + texData = new AWTTextureData(glp, 0, 0, false, img); + System.err.println("TextureData: "+texData); + w = texData.getWidth(); + h = texData.getHeight(); + } catch (Exception e) { + System.err.println("AWT ImageIO failure w/ file "+fname+": "+e.getMessage()); + // e.printStackTrace(); // : CMYK, YCCK -> com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(Unknown Source) + } + + final GLReadBufferUtil screenshot = new GLReadBufferUtil(true, false); + final GLCapabilities caps = new GLCapabilities(glp); + caps.setAlphaBits(1); + + final GLWindow glad1 = GLWindow.create(caps); + glad1.setTitle("JPEG AWT"); + // Size OpenGL to Video Surface + glad1.setSize(w, h); + glad1.setPosition(xpos, 0); + + // load texture from file inside current GL context to match the way + // the bug submitter was doing it + final GLEventListener gle; + if( texData != null ) { + gle = new TextureDraw01ES2Listener( texData ) ; + glad1.addGLEventListener(gle); + } else { + gle = null; + } + glad1.addGLEventListener(new GLEventListener() { + boolean shot = false; + + @Override public void init(GLAutoDrawable drawable) {} + + public void display(GLAutoDrawable drawable) { + // 1 snapshot + if( null!=gle && null!=((TextureDraw01Accessor)gle).getTexture() && !shot) { + shot = true; + snapshot(0, "AWTJPEG", drawable.getGL(), screenshot, TextureIO.PNG, null); + } + } + + @Override public void dispose(GLAutoDrawable drawable) { } + @Override public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { } + }); + + return glad1; + } + + @Test + public void test01YUV444Base__ES2() throws InterruptedException, IOException { + testImpl(files[0]); + } + @Test + public void test01YUV444Prog__ES2() throws InterruptedException, IOException { + testImpl(files[1]); + } + + @Test + public void test01YUV422hBase__ES2() throws InterruptedException, IOException { + testImpl(files[2]); + } + @Test + public void test01YUV422hProg_ES2() throws InterruptedException, IOException { + testImpl(files[3]); + } + + @Test + public void test02YUV420Base__ES2() throws InterruptedException, IOException { + testImpl(files[4]); + } + @Test + public void test02YUV420Prog_ES2() throws InterruptedException, IOException { + testImpl(files[5]); + } + @Test + public void test02YUV420BaseGray_ES2() throws InterruptedException, IOException { + testImpl(files[6]); + } + + @Test + public void test03CMYK_01_ES2() throws InterruptedException, IOException { + testImpl(files[7]); + } + @Test + public void test03YCCK_01_ES2() throws InterruptedException, IOException { + testImpl(files[8]); + } + + public static void main(String args[]) throws IOException { + for(int i=0; i<args.length; i++) { + if(args[i].equals("-time")) { + i++; + duration = MiscUtils.atol(args[i], duration); + } + } + org.junit.runner.JUnitCore.main(TestJPEGJoglAWTCompareNewtAWT.class.getName()); + } +} diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGTextureFromFileNEWT.java b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGTextureFromFileNEWT.java new file mode 100644 index 000000000..55e8152ae --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/TestJPEGTextureFromFileNEWT.java @@ -0,0 +1,264 @@ +/** + * Copyright 2012 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package com.jogamp.opengl.test.junit.jogl.util.texture; + + +import com.jogamp.common.util.IOUtil; +import com.jogamp.newt.opengl.GLWindow; +import com.jogamp.opengl.test.junit.jogl.demos.TextureDraw01Accessor; +import com.jogamp.opengl.test.junit.jogl.demos.es2.TextureDraw01ES2Listener; +import com.jogamp.opengl.test.junit.jogl.demos.gl2.TextureDraw01GL2Listener; +import com.jogamp.opengl.test.junit.util.MiscUtils; +import com.jogamp.opengl.test.junit.util.QuitAdapter; +import com.jogamp.opengl.test.junit.util.UITestCase; + +import javax.media.opengl.GLAutoDrawable; +import javax.media.opengl.GLEventListener; +import javax.media.opengl.GLProfile; +import javax.media.opengl.GLCapabilities; + +import com.jogamp.opengl.util.texture.TextureData; +import com.jogamp.opengl.util.texture.TextureIO; +import com.jogamp.opengl.util.Animator; +import com.jogamp.opengl.util.GLReadBufferUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; + +import org.junit.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TestJPEGTextureFromFileNEWT extends UITestCase { + static boolean showFPS = false; + static long duration = 100; // ms + + InputStream testTextureStream01YUV444_Base; + InputStream testTextureStream01YUV444_Prog; + + InputStream testTextureStream01YUV422h_Base; + InputStream testTextureStream01YUV422h_Prog; + + InputStream testTextureStream02YUV420_Base; + InputStream testTextureStream02YUV420_Prog; + InputStream testTextureStream02YUV420_BaseGray; + + InputStream testTextureStream03CMYK_01; + InputStream testTextureStream03YCCK_01; + + @Before + public void initTest() throws IOException { + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "test-ntscN_3-01-160x90-90pct-yuv444-base.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream01YUV444_Base = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream01YUV444_Base); + } + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "test-ntscN_3-01-160x90-90pct-yuv444-prog.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream01YUV444_Prog = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream01YUV444_Prog); + } + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "test-ntscN_3-01-160x90-60pct-yuv422h-base.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream01YUV422h_Base = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream01YUV422h_Base); + } + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "test-ntscN_3-01-160x90-60pct-yuv422h-prog.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream01YUV422h_Prog = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream01YUV422h_Prog); + } + + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "j1-baseline.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream02YUV420_Base = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream02YUV420_Base); + } + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "j2-progressive.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream02YUV420_Prog = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream02YUV420_Prog); + } + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "j3-baseline_gray.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream02YUV420_BaseGray = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream02YUV420_BaseGray); + } + + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "test-cmyk-01.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream03CMYK_01 = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream03CMYK_01); + } + { + URLConnection testTextureUrlConn = IOUtil.getResource(this.getClass(), "test-ycck-01.jpg"); + Assert.assertNotNull(testTextureUrlConn); + testTextureStream03YCCK_01 = testTextureUrlConn.getInputStream(); + Assert.assertNotNull(testTextureStream03YCCK_01); + } + } + + @After + public void cleanupTest() { + testTextureStream01YUV444_Base = null; + testTextureStream01YUV444_Prog = null; + testTextureStream01YUV422h_Base = null; + testTextureStream01YUV422h_Prog = null; + testTextureStream02YUV420_Base = null; + testTextureStream02YUV420_Prog = null; + testTextureStream02YUV420_BaseGray = null; + } + + public void testImpl(boolean useFFP, final InputStream istream) throws InterruptedException, IOException { + final GLReadBufferUtil screenshot = new GLReadBufferUtil(true, false); + GLProfile glp; + if(useFFP && GLProfile.isAvailable(GLProfile.GL2GL3)) { + glp = GLProfile.getGL2GL3(); + } else if(!useFFP && GLProfile.isAvailable(GLProfile.GL2ES2)) { + glp = GLProfile.getGL2ES2(); + } else { + System.err.println(getSimpleTestName(".")+": GLProfile n/a, useFFP: "+useFFP); + return; + } + final GLCapabilities caps = new GLCapabilities(glp); + caps.setAlphaBits(1); + + final TextureData texData = TextureIO.newTextureData(glp, istream, false /* mipmap */, TextureIO.JPG); + System.err.println("TextureData: "+texData); + + final GLWindow glad = GLWindow.create(caps); + glad.setTitle("TestPNGTextureGL2FromFileNEWT"); + // Size OpenGL to Video Surface + glad.setSize(texData.getWidth(), texData.getHeight()); + + // load texture from file inside current GL context to match the way + // the bug submitter was doing it + final GLEventListener gle = useFFP ? new TextureDraw01GL2Listener( texData ) : new TextureDraw01ES2Listener( texData ) ; + glad.addGLEventListener(gle); + glad.addGLEventListener(new GLEventListener() { + boolean shot = false; + + @Override public void init(GLAutoDrawable drawable) {} + + public void display(GLAutoDrawable drawable) { + // 1 snapshot + if(null!=((TextureDraw01Accessor)gle).getTexture() && !shot) { + shot = true; + snapshot(0, null, drawable.getGL(), screenshot, TextureIO.PNG, null); + } + } + + @Override public void dispose(GLAutoDrawable drawable) { } + @Override public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { } + }); + + Animator animator = new Animator(glad); + animator.setUpdateFPSFrames(60, showFPS ? System.err : null); + QuitAdapter quitAdapter = new QuitAdapter(); + glad.addKeyListener(quitAdapter); + glad.addWindowListener(quitAdapter); + glad.setVisible(true); + animator.start(); + + while(!quitAdapter.shouldQuit() && animator.isAnimating() && animator.getTotalFPSDuration()<duration) { + Thread.sleep(100); + } + + animator.stop(); + glad.destroy(); + } + + @Test + public void test01YUV444Base__GL2() throws InterruptedException, IOException { + testImpl(true, testTextureStream01YUV444_Base); + } + @Test + public void test01YUV444Base__ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream01YUV444_Base); + } + @Test + public void test01YUV444Prog__GL2() throws InterruptedException, IOException { + testImpl(true, testTextureStream01YUV444_Prog); + } + @Test + public void test01YUV444Prog__ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream01YUV444_Prog); + } + + @Test + public void test01YUV422hBase__ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream01YUV422h_Base); + } + @Test + public void test01YUV422hProg_ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream01YUV422h_Prog); + } + + @Test + public void test02YUV420Base__ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream02YUV420_Base); + } + @Test + public void test02YUV420Prog_ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream02YUV420_Prog); + } + @Test + public void test02YUV420BaseGray_ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream02YUV420_BaseGray); + } + + @Test + public void test03CMYK_01_ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream03CMYK_01); + } + @Test + public void test03YCCK_01_ES2() throws InterruptedException, IOException { + testImpl(false, testTextureStream03YCCK_01); + } + + public static void main(String args[]) throws IOException { + for(int i=0; i<args.length; i++) { + if(args[i].equals("-time")) { + i++; + duration = MiscUtils.atol(args[i], duration); + } + } + org.junit.runner.JUnitCore.main(TestJPEGTextureFromFileNEWT.class.getName()); + } +} diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j1-baseline.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j1-baseline.jpg Binary files differnew file mode 100644 index 000000000..8efe6af1f --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j1-baseline.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j2-progressive.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j2-progressive.jpg Binary files differnew file mode 100644 index 000000000..aa5fbd0d9 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j2-progressive.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j3-baseline_gray.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j3-baseline_gray.jpg Binary files differnew file mode 100644 index 000000000..048eb791b --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/j3-baseline_gray.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-cmyk-01.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-cmyk-01.jpg Binary files differnew file mode 100644 index 000000000..40a300f2d --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-cmyk-01.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-60pct-yuv422h-base.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-60pct-yuv422h-base.jpg Binary files differnew file mode 100644 index 000000000..8e4fe9a09 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-60pct-yuv422h-base.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-60pct-yuv422h-prog.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-60pct-yuv422h-prog.jpg Binary files differnew file mode 100644 index 000000000..35556f90b --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-60pct-yuv422h-prog.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-90pct-yuv444-base.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-90pct-yuv444-base.jpg Binary files differnew file mode 100644 index 000000000..051a166cd --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-90pct-yuv444-base.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-90pct-yuv444-prog.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-90pct-yuv444-prog.jpg Binary files differnew file mode 100644 index 000000000..40b0fc8c2 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ntscN_3-01-160x90-90pct-yuv444-prog.jpg diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ycck-01.jpg b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ycck-01.jpg Binary files differnew file mode 100644 index 000000000..84c1984f9 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/util/texture/test-ycck-01.jpg |