diff --git a/library/libjpegturbo/LICENSE.libjpegturbo.txt b/library/libjpegturbo/LICENSE.libjpegturbo.txt new file mode 100644 index 000000000..652ddb022 --- /dev/null +++ b/library/libjpegturbo/LICENSE.libjpegturbo.txt @@ -0,0 +1,28 @@ +Most of libjpeg-turbo inherits the non-restrictive, BSD-style license used by +libjpeg (see README.) The TurboJPEG/OSS wrapper (both C and Java versions) and +associated test programs bear a similar license, which is reproduced below: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS 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. + diff --git a/library/libjpegturbo/README-turbo.txt b/library/libjpegturbo/README-turbo.txt new file mode 100644 index 000000000..f4526180f --- /dev/null +++ b/library/libjpegturbo/README-turbo.txt @@ -0,0 +1,362 @@ +******************************************************************************* +** Background +******************************************************************************* + +libjpeg-turbo is a derivative of libjpeg that uses SIMD instructions (MMX, +SSE2, NEON) to accelerate baseline JPEG compression and decompression on x86, +x86-64, and ARM systems. On such systems, libjpeg-turbo is generally 2-4x as +fast as the unmodified version of libjpeg, all else being equal. + +libjpeg-turbo was originally based on libjpeg/SIMD by Miyasaka Masaru, but +the TigerVNC and VirtualGL projects made numerous enhancements to the codec in +2009, including improved support for Mac OS X, 64-bit support, support for +32-bit and big-endian pixel formats (RGBX, XBGR, etc.), accelerated Huffman +encoding/decoding, and various bug fixes. The goal was to produce a fully +open-source codec that could replace the partially closed-source TurboJPEG/IPP +codec used by VirtualGL and TurboVNC. libjpeg-turbo generally achieves 80-120% +of the performance of TurboJPEG/IPP. It is faster in some areas but slower in +others. + +In early 2010, libjpeg-turbo spun off into its own independent project, with +the goal of making high-speed JPEG compression/decompression technology +available to a broader range of users and developers. + + +******************************************************************************* +** License +******************************************************************************* + +Most of libjpeg-turbo inherits the non-restrictive, BSD-style license used by +libjpeg (see README.) The TurboJPEG/OSS wrapper (both C and Java versions) and +associated test programs bear a similar license, which is reproduced below: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS 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. + + +******************************************************************************* +** Using libjpeg-turbo +******************************************************************************* + +libjpeg-turbo includes two APIs that can be used to compress and decompress +JPEG images: + + TurboJPEG API: This API provides an easy-to-use interface for compressing + and decompressing JPEG images in memory. It also provides some functionality + that would not be straightforward to achieve using the underlying libjpeg + API, such as generating planar YUV images and performing multiple + simultaneous lossless transforms on an image. The Java interface for + libjpeg-turbo is written on top of the TurboJPEG API. + + libjpeg API: This is the de facto industry-standard API for compressing and + decompressing JPEG images. It is more difficult to use than the TurboJPEG + API but also more powerful. libjpeg-turbo is both API/ABI-compatible and + mathematically compatible with libjpeg v6b. It can also optionally be + configured to be API/ABI-compatible with libjpeg v7 and v8 (see below.) + + +============================= +Replacing libjpeg at Run Time +============================= + +If a Unix application is dynamically linked with libjpeg, then you can replace +libjpeg with libjpeg-turbo at run time by manipulating LD_LIBRARY_PATH. +For instance: + + [Using libjpeg] + > time cjpeg vgl_5674_0098.jpg + real 0m0.392s + user 0m0.074s + sys 0m0.020s + + [Using libjpeg-turbo] + > export LD_LIBRARY_PATH=/opt/libjpeg-turbo/{lib}:$LD_LIBRARY_PATH + > time cjpeg vgl_5674_0098.jpg + real 0m0.109s + user 0m0.029s + sys 0m0.010s + +NOTE: {lib} can be lib, lib32, lib64, or lib/64, depending on the O/S and +architecture. + +System administrators can also replace the libjpeg sym links in /usr/{lib} with +links to the libjpeg-turbo dynamic library located in /opt/libjpeg-turbo/{lib}. +This will effectively accelerate every application that uses the libjpeg +dynamic library on the system. + +The libjpeg-turbo SDK for Visual C++ installs the libjpeg-turbo DLL +(jpeg62.dll, jpeg7.dll, or jpeg8.dll, depending on whether it was built with +libjpeg v6b, v7, or v8 emulation) into c:\libjpeg-turbo[64]\bin, and the PATH +environment variable can be modified such that this directory is searched +before any others that might contain a libjpeg DLL. However, if a libjpeg +DLL exists in an application's install directory, then Windows will load this +DLL first whenever the application is launched. Thus, if an application ships +with jpeg62.dll, jpeg7.dll, or jpeg8.dll, then back up the application's +version of this DLL and copy c:\libjpeg-turbo[64]\bin\jpeg*.dll into the +application's install directory to accelerate it. + +The version of the libjpeg-turbo DLL distributed in the libjpeg-turbo SDK for +Visual C++ requires the Visual C++ 2008 C run-time DLL (msvcr90.dll). +msvcr90.dll ships with more recent versions of Windows, but users of older +Windows releases can obtain it from the Visual C++ 2008 Redistributable +Package, which is available as a free download from Microsoft's web site. + +NOTE: Features of libjpeg that require passing a C run-time structure, such +as a file handle, from an application to libjpeg will probably not work with +the version of the libjpeg-turbo DLL distributed in the libjpeg-turbo SDK for +Visual C++, unless the application is also built to use the Visual C++ 2008 C +run-time DLL. In particular, this affects jpeg_stdio_dest() and +jpeg_stdio_src(). + +Mac applications typically embed their own copies of the libjpeg dylib inside +the (hidden) application bundle, so it is not possible to globally replace +libjpeg on OS X systems. If an application uses a shared library version of +libjpeg, then it may be possible to replace the application's version of it. +This would generally involve copying libjpeg.*.dylib from libjpeg-turbo into +the appropriate place in the application bundle and using install_name_tool to +repoint the dylib to the new directory. This requires an advanced knowledge of +OS X and would not survive an upgrade or a re-install of the application. +Thus, it is not recommended for most users. + +======================= +Replacing TurboJPEG/IPP +======================= + +libjpeg-turbo is a drop-in replacement for the TurboJPEG/IPP SDK used by +VirtualGL 2.1.x and TurboVNC 0.6 (and prior.) libjpeg-turbo contains a wrapper +library (TurboJPEG/OSS) that emulates the TurboJPEG API using libjpeg-turbo +instead of the closed-source Intel Performance Primitives. You can replace the +TurboJPEG/IPP package on Linux systems with the libjpeg-turbo package in order +to make existing releases of VirtualGL 2.1.x and TurboVNC 0.x use the new codec +at run time. Note that the 64-bit libjpeg-turbo packages contain only 64-bit +binaries, whereas the TurboJPEG/IPP 64-bit packages contained both 64-bit and +32-bit binaries. Thus, to replace a TurboJPEG/IPP 64-bit package, install +both the 64-bit and 32-bit versions of libjpeg-turbo. + +You can also build the VirtualGL 2.1.x and TurboVNC 0.6 source code with +the libjpeg-turbo SDK instead of TurboJPEG/IPP. It should work identically. +libjpeg-turbo also includes static library versions of TurboJPEG/OSS, which +are used to build VirtualGL 2.2 and TurboVNC 1.0 and later. + +======================================== +Using libjpeg-turbo in Your Own Programs +======================================== + +For the most part, libjpeg-turbo should work identically to libjpeg, so in +most cases, an application can be built against libjpeg and then run against +libjpeg-turbo. On Unix systems (including Cygwin), you can build against +libjpeg-turbo instead of libjpeg by setting + + CPATH=/opt/libjpeg-turbo/include + and + LIBRARY_PATH=/opt/libjpeg-turbo/{lib} + +({lib} = lib32 or lib64, depending on whether you are building a 32-bit or a +64-bit application.) + +If using MinGW, then set + + CPATH=/c/libjpeg-turbo-gcc[64]/include + and + LIBRARY_PATH=/c/libjpeg-turbo-gcc[64]/lib + +Building against libjpeg-turbo is useful, for instance, if you want to build an +application that leverages the libjpeg-turbo colorspace extensions (see below.) +On Linux and Solaris systems, you would still need to manipulate +LD_LIBRARY_PATH or create appropriate sym links to use libjpeg-turbo at run +time. On such systems, you can pass -R /opt/libjpeg-turbo/{lib} to the linker +to force the use of libjpeg-turbo at run time rather than libjpeg (also useful +if you want to leverage the colorspace extensions), or you can link against the +libjpeg-turbo static library. + +To force a Linux, Solaris, or MinGW application to link against the static +version of libjpeg-turbo, you can use the following linker options: + + -Wl,-Bstatic -ljpeg -Wl,-Bdynamic + +On OS X, simply add /opt/libjpeg-turbo/lib/libjpeg.a to the linker command +line (this also works on Linux and Solaris.) + +To build Visual C++ applications using libjpeg-turbo, add +c:\libjpeg-turbo[64]\include to the system or user INCLUDE environment +variable and c:\libjpeg-turbo[64]\lib to the system or user LIB environment +variable, and then link against either jpeg.lib (to use the DLL version of +libjpeg-turbo) or jpeg-static.lib (to use the static version of libjpeg-turbo.) + +===================== +Colorspace Extensions +===================== + +libjpeg-turbo includes extensions that allow JPEG images to be compressed +directly from (and decompressed directly to) buffers that use BGR, BGRX, +RGBX, XBGR, and XRGB pixel ordering. This is implemented with ten new +colorspace constants: + + JCS_EXT_RGB /* red/green/blue */ + JCS_EXT_RGBX /* red/green/blue/x */ + JCS_EXT_BGR /* blue/green/red */ + JCS_EXT_BGRX /* blue/green/red/x */ + JCS_EXT_XBGR /* x/blue/green/red */ + JCS_EXT_XRGB /* x/red/green/blue */ + JCS_EXT_RGBA /* red/green/blue/alpha */ + JCS_EXT_BGRA /* blue/green/red/alpha */ + JCS_EXT_ABGR /* alpha/blue/green/red */ + JCS_EXT_ARGB /* alpha/red/green/blue */ + +Setting cinfo.in_color_space (compression) or cinfo.out_color_space +(decompression) to one of these values will cause libjpeg-turbo to read the +red, green, and blue values from (or write them to) the appropriate position in +the pixel when compressing from/decompressing to an RGB buffer. + +Your application can check for the existence of these extensions at compile +time with: + + #ifdef JCS_EXTENSIONS + +At run time, attempting to use these extensions with a version of libjpeg +that doesn't support them will result in a "Bogus input colorspace" error. + +When using the RGBX, BGRX, XBGR, and XRGB colorspaces during decompression, the +X byte is undefined, and in order to ensure the best performance, libjpeg-turbo +can set that byte to whatever value it wishes. If an application expects the X +byte to be used as an alpha channel, then it should specify JCS_EXT_RGBA, +JCS_EXT_BGRA, JCS_EXT_ABGR, or JCS_EXT_ARGB. When these colorspace constants +are used, the X byte is guaranteed to be 0xFF, which is interpreted as opaque. + +Your application can check for the existence of the alpha channel colorspace +extensions at compile time with: + + #ifdef JCS_ALPHA_EXTENSIONS + +jcstest.c, located in the libjpeg-turbo source tree, demonstrates how to check +for the existence of the colorspace extensions at compile time and run time. + +================================= +libjpeg v7 and v8 API/ABI support +================================= + +With libjpeg v7 and v8, new features were added that necessitated extending the +compression and decompression structures. Unfortunately, due to the exposed +nature of those structures, extending them also necessitated breaking backward +ABI compatibility with previous libjpeg releases. Thus, programs that are +built to use libjpeg v7 or v8 did not work with libjpeg-turbo, since it is +based on the libjpeg v6b code base. Although libjpeg v7 and v8 are still not +as widely used as v6b, enough programs (including a few Linux distros) have +made the switch that it was desirable to provide support for the libjpeg v7/v8 +API/ABI in libjpeg-turbo. Although libjpeg-turbo can now be configured as a +drop-in replacement for libjpeg v7 or v8, it should be noted that not all of +the features in libjpeg v7 and v8 are supported (see below.) + +By passing an argument of --with-jpeg7 or --with-jpeg8 to configure, or an +argument of -DWITH_JPEG7=1 or -DWITH_JPEG8=1 to cmake, you can build a version +of libjpeg-turbo that emulates the libjpeg v7 or v8 API/ABI, so that programs +that are built against libjpeg v7 or v8 can be run with libjpeg-turbo. The +following section describes which libjpeg v7+ features are supported and which +aren't. + +libjpeg v7 and v8 Features: +--------------------------- + +Fully supported: + +-- cjpeg: Separate quality settings for luminance and chrominance + Note that the libpjeg v7+ API was extended to accommodate this feature only + for convenience purposes. It has always been possible to implement this + feature with libjpeg v6b (see rdswitch.c for an example.) + +-- libjpeg: IDCT scaling extensions in decompressor + libjpeg-turbo supports IDCT scaling with scaling factors of 1/8, 1/4, 3/8, + 1/2, 5/8, 3/4, 7/8, 9/8, 5/4, 11/8, 3/2, 13/8, 7/4, 15/8, and 2/1 (only 1/4 + and 1/2 are SIMD-accelerated.) + +-- cjpeg: 32-bit BMP support + +-- jpegtran: lossless cropping + +-- jpegtran: -perfect option + +-- rdjpgcom: -raw option + +-- rdjpgcom: locale awareness + + +Fully supported when using libjpeg v7/v8 emulation: + +-- libjpeg: In-memory source and destination managers + + +Not supported: + +-- libjpeg: DCT scaling in compressor + cinfo.scale_num and cinfo.scale_denom are silently ignored. + There is no technical reason why DCT scaling cannot be supported, but + without the SmartScale extension (see below), it would only be able to + down-scale using ratios of 1/2, 8/15, 4/7, 8/13, 2/3, 8/11, 4/5, and 8/9, + which is of limited usefulness. + +-- libjpeg: SmartScale + cinfo.block_size is silently ignored. + SmartScale is an extension to the JPEG format that allows for DCT block + sizes other than 8x8. It would be difficult to support this feature while + retaining backward compatibility with libjpeg v6b. + +-- libjpeg: Fancy downsampling in compressor + cinfo.do_fancy_downsampling is silently ignored. + This requires the DCT scaling feature, which is not supported. + +-- jpegtran: Scaling + This requires both the DCT scaling and SmartScale features, which are not + supported. + +-- Lossless RGB JPEG files + This requires the SmartScale feature, which is not supported. + + +******************************************************************************* +** Performance pitfalls +******************************************************************************* + +=============== +Restart Markers +=============== + +The optimized Huffman decoder in libjpeg-turbo does not handle restart markers +in a way that makes the rest of the libjpeg infrastructure happy, so it is +necessary to use the slow Huffman decoder when decompressing a JPEG image that +has restart markers. This can cause the decompression performance to drop by +as much as 20%, but the performance will still be much greater than that of +libjpeg. Many consumer packages, such as PhotoShop, use restart markers when +generating JPEG images, so images generated by those programs will experience +this issue. + +=============================================== +Fast Integer Forward DCT at High Quality Levels +=============================================== + +The algorithm used by the SIMD-accelerated quantization function cannot produce +correct results whenever the fast integer forward DCT is used along with a JPEG +quality of 98-100. Thus, libjpeg-turbo must use the non-SIMD quantization +function in those cases. This causes performance to drop by as much as 40%. +It is therefore strongly advised that you use the slow integer forward DCT +whenever encoding images with a JPEG quality of 98 or higher. diff --git a/library/libjpegturbo/pom.xml b/library/libjpegturbo/pom.xml new file mode 100644 index 000000000..edd5be28e --- /dev/null +++ b/library/libjpegturbo/pom.xml @@ -0,0 +1,16 @@ + + 4.0.0 + + it.geosolutions.imageio-ext + imageio-ext-library + 1.2-SNAPSHOT + + + org.libjpegturbo + turbojpeg-wrapper + 1.2.1 + jar + Libjpegturbo java wrapper + + diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJ.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJ.java new file mode 100644 index 000000000..78a72f67b --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJ.java @@ -0,0 +1,372 @@ +/* + * Copyright (C)2011-2012 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +/** + * TurboJPEG utility class (cannot be instantiated) + */ +final public class TJ { + + + /** + * The number of chrominance subsampling options + */ + final public static int NUMSAMP = 5; + /** + * 4:4:4 chrominance subsampling (no chrominance subsampling). The JPEG + * or YUV image will contain one chrominance component for every pixel in the + * source image. + */ + final public static int SAMP_444 = 0; + /** + * 4:2:2 chrominance subsampling. The JPEG or YUV image will contain one + * chrominance component for every 2x1 block of pixels in the source image. + */ + final public static int SAMP_422 = 1; + /** + * 4:2:0 chrominance subsampling. The JPEG or YUV image will contain one + * chrominance component for every 2x2 block of pixels in the source image. + */ + final public static int SAMP_420 = 2; + /** + * Grayscale. The JPEG or YUV image will contain no chrominance components. + */ + final public static int SAMP_GRAY = 3; + /** + * 4:4:0 chrominance subsampling. The JPEG or YUV image will contain one + * chrominance component for every 1x2 block of pixels in the source image. + */ + final public static int SAMP_440 = 4; + + + /** + * Returns the MCU block width for the given level of chrominance + * subsampling. + * + * @param subsamp the level of chrominance subsampling (one of + * SAMP_*) + * + * @return the MCU block width for the given level of chrominance subsampling + */ + public static int getMCUWidth(int subsamp) throws Exception { + if(subsamp < 0 || subsamp >= NUMSAMP) + throw new Exception("Invalid subsampling type"); + return mcuWidth[subsamp]; + } + + final private static int mcuWidth[] = { + 8, 16, 16, 8, 8 + }; + + + /** + * Returns the MCU block height for the given level of chrominance + * subsampling. + * + * @param subsamp the level of chrominance subsampling (one of + * SAMP_*) + * + * @return the MCU block height for the given level of chrominance + * subsampling + */ + public static int getMCUHeight(int subsamp) throws Exception { + if(subsamp < 0 || subsamp >= NUMSAMP) + throw new Exception("Invalid subsampling type"); + return mcuHeight[subsamp]; + } + + final private static int mcuHeight[] = { + 8, 8, 16, 8, 16 + }; + + + /** + * The number of pixel formats + */ + final public static int NUMPF = 11; + /** + * RGB pixel format. The red, green, and blue components in the image are + * stored in 3-byte pixels in the order R, G, B from lowest to highest byte + * address within each pixel. + */ + final public static int PF_RGB = 0; + /** + * BGR pixel format. The red, green, and blue components in the image are + * stored in 3-byte pixels in the order B, G, R from lowest to highest byte + * address within each pixel. + */ + final public static int PF_BGR = 1; + /** + * RGBX pixel format. The red, green, and blue components in the image are + * stored in 4-byte pixels in the order R, G, B from lowest to highest byte + * address within each pixel. The X component is ignored when compressing + * and undefined when decompressing. + */ + final public static int PF_RGBX = 2; + /** + * BGRX pixel format. The red, green, and blue components in the image are + * stored in 4-byte pixels in the order B, G, R from lowest to highest byte + * address within each pixel. The X component is ignored when compressing + * and undefined when decompressing. + */ + final public static int PF_BGRX = 3; + /** + * XBGR pixel format. The red, green, and blue components in the image are + * stored in 4-byte pixels in the order R, G, B from highest to lowest byte + * address within each pixel. The X component is ignored when compressing + * and undefined when decompressing. + */ + final public static int PF_XBGR = 4; + /** + * XRGB pixel format. The red, green, and blue components in the image are + * stored in 4-byte pixels in the order B, G, R from highest to lowest byte + * address within each pixel. The X component is ignored when compressing + * and undefined when decompressing. + */ + final public static int PF_XRGB = 5; + /** + * Grayscale pixel format. Each 1-byte pixel represents a luminance + * (brightness) level from 0 to 255. + */ + final public static int PF_GRAY = 6; + /** + * RGBA pixel format. This is the same as {@link #PF_RGBX}, except that when + * decompressing, the X byte is guaranteed to be 0xFF, which can be + * interpreted as an opaque alpha channel. + */ + final public static int PF_RGBA = 7; + /** + * BGRA pixel format. This is the same as {@link #PF_BGRX}, except that when + * decompressing, the X byte is guaranteed to be 0xFF, which can be + * interpreted as an opaque alpha channel. + */ + final public static int PF_BGRA = 8; + /** + * ABGR pixel format. This is the same as {@link #PF_XBGR}, except that when + * decompressing, the X byte is guaranteed to be 0xFF, which can be + * interpreted as an opaque alpha channel. + */ + final public static int PF_ABGR = 9; + /** + * ARGB pixel format. This is the same as {@link #PF_XRGB}, except that when + * decompressing, the X byte is guaranteed to be 0xFF, which can be + * interpreted as an opaque alpha channel. + */ + final public static int PF_ARGB = 10; + + + /** + * Returns the pixel size (in bytes) of the given pixel format. + * + * @param pixelFormat the pixel format (one of PF_*) + * + * @return the pixel size (in bytes) of the given pixel format + */ + public static int getPixelSize(int pixelFormat) throws Exception { + if(pixelFormat < 0 || pixelFormat >= NUMPF) + throw new Exception("Invalid pixel format"); + return pixelSize[pixelFormat]; + } + + final private static int pixelSize[] = { + 3, 3, 4, 4, 4, 4, 1, 4, 4, 4, 4 + }; + + + /** + * For the given pixel format, returns the number of bytes that the red + * component is offset from the start of the pixel. For instance, if a pixel + * of format TJ.PF_BGRX is stored in char pixel[], + * then the red component will be + * pixel[TJ.getRedOffset(TJ.PF_BGRX)]. + * + * @param pixelFormat the pixel format (one of PF_*) + * + * @return the red offset for the given pixel format + */ + public static int getRedOffset(int pixelFormat) throws Exception { + if(pixelFormat < 0 || pixelFormat >= NUMPF) + throw new Exception("Invalid pixel format"); + return redOffset[pixelFormat]; + } + + final private static int redOffset[] = { + 0, 2, 0, 2, 3, 1, 0, 0, 2, 3, 1 + }; + + + /** + * For the given pixel format, returns the number of bytes that the green + * component is offset from the start of the pixel. For instance, if a pixel + * of format TJ.PF_BGRX is stored in char pixel[], + * then the green component will be + * pixel[TJ.getGreenOffset(TJ.PF_BGRX)]. + * + * @param pixelFormat the pixel format (one of PF_*) + * + * @return the green offset for the given pixel format + */ + public static int getGreenOffset(int pixelFormat) throws Exception { + if(pixelFormat < 0 || pixelFormat >= NUMPF) + throw new Exception("Invalid pixel format"); + return greenOffset[pixelFormat]; + } + + final private static int greenOffset[] = { + 1, 1, 1, 1, 2, 2, 0, 1, 1, 2, 2 + }; + + + /** + * For the given pixel format, returns the number of bytes that the blue + * component is offset from the start of the pixel. For instance, if a pixel + * of format TJ.PF_BGRX is stored in char pixel[], + * then the blue component will be + * pixel[TJ.getBlueOffset(TJ.PF_BGRX)]. + * + * @param pixelFormat the pixel format (one of PF_*) + * + * @return the blue offset for the given pixel format + */ + public static int getBlueOffset(int pixelFormat) throws Exception { + if(pixelFormat < 0 || pixelFormat >= NUMPF) + throw new Exception("Invalid pixel format"); + return blueOffset[pixelFormat]; + } + + final private static int blueOffset[] = { + 2, 0, 2, 0, 1, 3, 0, 2, 0, 1, 3 + }; + + + /** + * The uncompressed source/destination image is stored in bottom-up (Windows, + * OpenGL) order, not top-down (X11) order. + */ + final public static int FLAG_BOTTOMUP = 2; + /** + * Turn off CPU auto-detection and force TurboJPEG to use MMX code + * (if the underlying codec supports it.) + */ + final public static int FLAG_FORCEMMX = 8; + /** + * Turn off CPU auto-detection and force TurboJPEG to use SSE code + * (if the underlying codec supports it.) + */ + final public static int FLAG_FORCESSE = 16; + /** + * Turn off CPU auto-detection and force TurboJPEG to use SSE2 code + * (if the underlying codec supports it.) + */ + final public static int FLAG_FORCESSE2 = 32; + /** + * Turn off CPU auto-detection and force TurboJPEG to use SSE3 code + * (if the underlying codec supports it.) + */ + final public static int FLAG_FORCESSE3 = 128; + /** + * When decompressing, use the fastest chrominance upsampling algorithm + * available in the underlying codec. The default is to use smooth + * upsampling, which creates a smooth transition between neighboring + * chrominance components in order to reduce upsampling artifacts in the + * decompressed image. + */ + final public static int FLAG_FASTUPSAMPLE = 256; + /** + * Use the fastest DCT/IDCT algorithm available in the underlying codec. The + * default if this flag is not specified is implementation-specific. The + * libjpeg implementation, for example, uses the fast algorithm by default + * when compressing, because this has been shown to have only a very slight + * effect on accuracy, but it uses the accurate algorithm when decompressing, + * because this has been shown to have a larger effect. + */ + final public static int FLAG_FASTDCT = 2048; + /** + * Use the most accurate DCT/IDCT algorithm available in the underlying + * codec. The default if this flag is not specified is + * implementation-specific. The libjpeg implementation, for example, uses + * the fast algorithm by default when compressing, because this has been + * shown to have only a very slight effect on accuracy, but it uses the + * accurate algorithm when decompressing, because this has been shown to have + * a larger effect. + */ + final public static int FLAG_ACCURATEDCT = 4096; + + + /** + * Returns the maximum size of the buffer (in bytes) required to hold a JPEG + * image with the given width and height, and level of chrominance + * subsampling. + * + * @param width the width (in pixels) of the JPEG image + * + * @param height the height (in pixels) of the JPEG image + * + * @param jpegSubsamp the level of chrominance subsampling to be used when + * generating the JPEG image (one of {@link TJ TJ.SAMP_*}) + * + * @return the maximum size of the buffer (in bytes) required to hold a JPEG + * image with the given width and height, and level of chrominance + * subsampling + */ + public native static int bufSize(int width, int height, int jpegSubsamp) + throws Exception; + + /** + * Returns the size of the buffer (in bytes) required to hold a YUV planar + * image with the given width, height, and level of chrominance subsampling. + * + * @param width the width (in pixels) of the YUV image + * + * @param height the height (in pixels) of the YUV image + * + * @param subsamp the level of chrominance subsampling used in the YUV + * image (one of {@link TJ TJ.SAMP_*}) + * + * @return the size of the buffer (in bytes) required to hold a YUV planar + * image with the given width, height, and level of chrominance subsampling + */ + public native static int bufSizeYUV(int width, int height, + int subsamp) + throws Exception; + + /** + * Returns a list of fractional scaling factors that the JPEG decompressor in + * this implementation of TurboJPEG supports. + * + * @return a list of fractional scaling factors that the JPEG decompressor in + * this implementation of TurboJPEG supports + */ + public native static TJScalingFactor[] getScalingFactors() + throws Exception; + + static { + TJLoader.load(); + } +}; diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJCompressor.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJCompressor.java new file mode 100644 index 000000000..b3c9b9560 --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJCompressor.java @@ -0,0 +1,470 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +import java.awt.image.*; +import java.nio.*; + +/** + * TurboJPEG compressor + */ +public class TJCompressor { + + private final static String NO_ASSOC_ERROR = + "No source image is associated with this instance"; + + /** + * Create a TurboJPEG compressor instance. + */ + public TJCompressor() throws Exception { + init(); + } + + /** + * Create a TurboJPEG compressor instance and associate the uncompressed + * source image stored in srcImage with the newly-created + * instance. + * + * @param srcImage see {@link #setSourceImage} for description + * + * @param width see {@link #setSourceImage} for description + * + * @param pitch see {@link #setSourceImage} for description + * + * @param height see {@link #setSourceImage} for description + * + * @param pixelFormat see {@link #setSourceImage} for description + */ + public TJCompressor(byte[] srcImage, int width, int pitch, int height, + int pixelFormat) throws Exception { + setSourceImage(srcImage, width, pitch, height, pixelFormat); + } + + /** + * Associate an uncompressed source image with this compressor instance. + * + * @param srcImage image buffer containing RGB or grayscale pixels to be + * compressed + * + * @param width width (in pixels) of the source image + * + * @param pitch bytes per line of the source image. Normally, this should be + * width * TJ.pixelSize(pixelFormat) if the source image is + * unpadded, but you can use this parameter to, for instance, specify that + * the scanlines in the source image are padded to 4-byte boundaries, as is + * the case for Windows bitmaps. You can also be clever and use this + * parameter to skip lines, etc. Setting this parameter to 0 is the + * equivalent of setting it to width * + * TJ.pixelSize(pixelFormat). + * + * @param height height (in pixels) of the source image + * + * @param pixelFormat pixel format of the source image (one of + * {@link TJ TJ.PF_*}) + */ + public void setSourceImage(byte[] srcImage, int width, int pitch, + int height, int pixelFormat) throws Exception { + if(handle == 0) init(); + if(srcImage == null || width < 1 || height < 1 || pitch < 0 + || pixelFormat < 0 || pixelFormat >= TJ.NUMPF) + throw new Exception("Invalid argument in setSourceImage()"); + srcBuf = srcImage; + srcWidth = width; + if(pitch == 0) srcPitch = width * TJ.getPixelSize(pixelFormat); + else srcPitch = pitch; + srcHeight = height; + srcPixelFormat = pixelFormat; + } + + /** + * Set the level of chrominance subsampling for subsequent compress/encode + * operations. + * + * @param newSubsamp the new level of chrominance subsampling (one of + * {@link TJ TJ.SAMP_*}) + */ + public void setSubsamp(int newSubsamp) throws Exception { + if(newSubsamp < 0 || newSubsamp >= TJ.NUMSAMP) + throw new Exception("Invalid argument in setSubsamp()"); + subsamp = newSubsamp; + } + + /** + * Set the JPEG image quality level for subsequent compress operations. + * + * @param quality the new JPEG image quality level (1 to 100, 1 = worst, + * 100 = best) + */ + public void setJPEGQuality(int quality) throws Exception { + if(quality < 1 || quality > 100) + throw new Exception("Invalid argument in setJPEGQuality()"); + jpegQuality = quality; + } + + /** + * Compress the uncompressed source image associated with this compressor + * instance and output a JPEG image to the given destination buffer. + * + * @param dstBuf buffer that will receive the JPEG image. Use + * {@link TJ#bufSize} to determine the maximum size for this buffer based on + * the image width and height. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void compress(byte[] dstBuf, int flags) throws Exception { + if(dstBuf == null || flags < 0) + throw new Exception("Invalid argument in compress()"); + if(srcBuf == null) throw new Exception(NO_ASSOC_ERROR); + if(jpegQuality < 0) throw new Exception("JPEG Quality not set"); + if(subsamp < 0) throw new Exception("Subsampling level not set"); + compressedSize = compress(srcBuf, srcWidth, srcPitch, + srcHeight, srcPixelFormat, dstBuf, subsamp, jpegQuality, flags); + } + + /** + * Compress the uncompressed source image associated with this compressor + * instance and return a buffer containing a JPEG image. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + * + * @return a buffer containing a JPEG image. The length of this buffer will + * not be equal to the size of the JPEG image. Use {@link + * #getCompressedSize} to obtain the size of the JPEG image. + */ + public byte[] compress(int flags) throws Exception { + if(srcWidth < 1 || srcHeight < 1) + throw new Exception(NO_ASSOC_ERROR); + byte[] buf = new byte[TJ.bufSize(srcWidth, srcHeight, subsamp)]; + compress(buf, flags); + return buf; + } + + /** + * Compress the uncompressed source image stored in srcImage + * and output a JPEG image to the given destination buffer. + * + * @param srcImage a BufferedImage instance containing RGB or + * grayscale pixels to be compressed + * + * @param dstBuf buffer that will receive the JPEG image. Use + * {@link TJ#bufSize} to determine the maximum size for this buffer based on + * the image width and height. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void compress(BufferedImage srcImage, byte[] dstBuf, int flags) + throws Exception { + if(srcImage == null || dstBuf == null || flags < 0) + throw new Exception("Invalid argument in compress()"); + int width = srcImage.getWidth(); + int height = srcImage.getHeight(); + int pixelFormat; boolean intPixels = false; + if(byteOrder == null) + byteOrder = ByteOrder.nativeOrder(); + switch(srcImage.getType()) { + case BufferedImage.TYPE_3BYTE_BGR: + pixelFormat = TJ.PF_BGR; break; + case BufferedImage.TYPE_4BYTE_ABGR: + case BufferedImage.TYPE_4BYTE_ABGR_PRE: + pixelFormat = TJ.PF_XBGR; break; + case BufferedImage.TYPE_BYTE_GRAY: + pixelFormat = TJ.PF_GRAY; break; + case BufferedImage.TYPE_INT_BGR: + if(byteOrder == ByteOrder.BIG_ENDIAN) + pixelFormat = TJ.PF_XBGR; + else + pixelFormat = TJ.PF_RGBX; + intPixels = true; break; + case BufferedImage.TYPE_INT_RGB: + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + if(byteOrder == ByteOrder.BIG_ENDIAN) + pixelFormat = TJ.PF_XRGB; + else + pixelFormat = TJ.PF_BGRX; + intPixels = true; break; + default: + throw new Exception("Unsupported BufferedImage format"); + } + WritableRaster wr = srcImage.getRaster(); + if(jpegQuality < 0) throw new Exception("JPEG Quality not set"); + if(subsamp < 0) throw new Exception("Subsampling level not set"); + if(intPixels) { + SinglePixelPackedSampleModel sm = + (SinglePixelPackedSampleModel)srcImage.getSampleModel(); + int pitch = sm.getScanlineStride(); + DataBufferInt db = (DataBufferInt)wr.getDataBuffer(); + int[] buf = db.getData(); + compressedSize = compress(buf, width, pitch, height, pixelFormat, dstBuf, + subsamp, jpegQuality, flags); + } + else { + ComponentSampleModel sm = + (ComponentSampleModel)srcImage.getSampleModel(); + int pixelSize = sm.getPixelStride(); + if(pixelSize != TJ.getPixelSize(pixelFormat)) + throw new Exception("Inconsistency between pixel format and pixel size in BufferedImage"); + int pitch = sm.getScanlineStride(); + DataBufferByte db = (DataBufferByte)wr.getDataBuffer(); + byte[] buf = db.getData(); + compressedSize = compress(buf, width, pitch, height, pixelFormat, dstBuf, + subsamp, jpegQuality, flags); + } + } + + /** + * Compress the uncompressed source image stored in srcImage + * and return a buffer containing a JPEG image. + * + * @param srcImage a BufferedImage instance containing RGB or + * grayscale pixels to be compressed + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + * + * @return a buffer containing a JPEG image. The length of this buffer will + * not be equal to the size of the JPEG image. Use {@link + * #getCompressedSize} to obtain the size of the JPEG image. + */ + public byte[] compress(BufferedImage srcImage, int flags) throws Exception { + int width = srcImage.getWidth(); + int height = srcImage.getHeight(); + byte[] buf = new byte[TJ.bufSize(width, height, subsamp)]; + compress(srcImage, buf, flags); + return buf; + } + + /** + * Encode the uncompressed source image associated with this compressor + * instance and output a YUV planar image to the given destination buffer. + * This method uses the accelerated color conversion routines in + * TurboJPEG's underlying codec to produce a planar YUV image that is + * suitable for direct video display. Specifically, if the chrominance + * components are subsampled along the horizontal dimension, then the width + * of the luminance plane is padded to 2 in the output image (same goes for + * the height of the luminance plane, if the chrominance components are + * subsampled along the vertical dimension.) Also, each line of each plane + * in the output image is padded to 4 bytes. Although this will work with + * any subsampling option, it is really only useful in combination with + * {@link TJ#SAMP_420}, which produces an image compatible with the I420 (AKA + * "YUV420P") format. + * + * @param dstBuf buffer that will receive the YUV planar image. Use + * {@link TJ#bufSizeYUV} to determine the appropriate size for this buffer + * based on the image width, height, and level of chrominance subsampling. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void encodeYUV(byte[] dstBuf, int flags) throws Exception { + if(dstBuf == null || flags < 0) + throw new Exception("Invalid argument in compress()"); + if(srcBuf == null) throw new Exception(NO_ASSOC_ERROR); + if(subsamp < 0) throw new Exception("Subsampling level not set"); + encodeYUV(srcBuf, srcWidth, srcPitch, srcHeight, + srcPixelFormat, dstBuf, subsamp, flags); + compressedSize = TJ.bufSizeYUV(srcWidth, srcHeight, subsamp); + } + + /** + * Encode the uncompressed source image associated with this compressor + * instance and return a buffer containing a YUV planar image. See + * {@link #encodeYUV(byte[], int)} for more detail. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + * + * @return a buffer containing a YUV planar image + */ + public byte[] encodeYUV(int flags) throws Exception { + if(srcWidth < 1 || srcHeight < 1) + throw new Exception(NO_ASSOC_ERROR); + if(subsamp < 0) throw new Exception("Subsampling level not set"); + byte[] buf = new byte[TJ.bufSizeYUV(srcWidth, srcHeight, subsamp)]; + encodeYUV(buf, flags); + return buf; + } + + /** + * Encode the uncompressed source image stored in srcImage + * and output a YUV planar image to the given destination buffer. See + * {@link #encodeYUV(byte[], int)} for more detail. + * + * @param srcImage a BufferedImage instance containing RGB or + * grayscale pixels to be encoded + * + * @param dstBuf buffer that will receive the YUV planar image. Use + * {@link TJ#bufSizeYUV} to determine the appropriate size for this buffer + * based on the image width, height, and level of chrominance subsampling. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void encodeYUV(BufferedImage srcImage, byte[] dstBuf, int flags) + throws Exception { + if(srcImage == null || dstBuf == null || flags < 0) + throw new Exception("Invalid argument in encodeYUV()"); + int width = srcImage.getWidth(); + int height = srcImage.getHeight(); + int pixelFormat; boolean intPixels = false; + if(byteOrder == null) + byteOrder = ByteOrder.nativeOrder(); + switch(srcImage.getType()) { + case BufferedImage.TYPE_3BYTE_BGR: + pixelFormat = TJ.PF_BGR; break; + case BufferedImage.TYPE_4BYTE_ABGR: + case BufferedImage.TYPE_4BYTE_ABGR_PRE: + pixelFormat = TJ.PF_XBGR; break; + case BufferedImage.TYPE_BYTE_GRAY: + pixelFormat = TJ.PF_GRAY; break; + case BufferedImage.TYPE_INT_BGR: + if(byteOrder == ByteOrder.BIG_ENDIAN) + pixelFormat = TJ.PF_XBGR; + else + pixelFormat = TJ.PF_RGBX; + intPixels = true; break; + case BufferedImage.TYPE_INT_RGB: + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + if(byteOrder == ByteOrder.BIG_ENDIAN) + pixelFormat = TJ.PF_XRGB; + else + pixelFormat = TJ.PF_BGRX; + intPixels = true; break; + default: + throw new Exception("Unsupported BufferedImage format"); + } + WritableRaster wr = srcImage.getRaster(); + if(subsamp < 0) throw new Exception("Subsampling level not set"); + if(intPixels) { + SinglePixelPackedSampleModel sm = + (SinglePixelPackedSampleModel)srcImage.getSampleModel(); + int pitch = sm.getScanlineStride(); + DataBufferInt db = (DataBufferInt)wr.getDataBuffer(); + int[] buf = db.getData(); + encodeYUV(buf, width, pitch, height, pixelFormat, dstBuf, subsamp, + flags); + } + else { + ComponentSampleModel sm = + (ComponentSampleModel)srcImage.getSampleModel(); + int pixelSize = sm.getPixelStride(); + if(pixelSize != TJ.getPixelSize(pixelFormat)) + throw new Exception("Inconsistency between pixel format and pixel size in BufferedImage"); + int pitch = sm.getScanlineStride(); + DataBufferByte db = (DataBufferByte)wr.getDataBuffer(); + byte[] buf = db.getData(); + encodeYUV(buf, width, pitch, height, pixelFormat, dstBuf, subsamp, + flags); + } + compressedSize = TJ.bufSizeYUV(width, height, subsamp); + } + + /** + * Encode the uncompressed source image stored in srcImage + * and return a buffer containing a YUV planar image. See + * {@link #encodeYUV(byte[], int)} for more detail. + * + * @param srcImage a BufferedImage instance containing RGB or + * grayscale pixels to be encoded + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + * + * @return a buffer containing a YUV planar image + */ + public byte[] encodeYUV(BufferedImage srcImage, int flags) + throws Exception { + if(subsamp < 0) throw new Exception("Subsampling level not set"); + int width = srcImage.getWidth(); + int height = srcImage.getHeight(); + byte[] buf = new byte[TJ.bufSizeYUV(width, height, subsamp)]; + encodeYUV(srcImage, buf, flags); + return buf; + } + + /** + * Returns the size of the image (in bytes) generated by the most recent + * compress/encode operation. + * + * @return the size of the image (in bytes) generated by the most recent + * compress/encode operation + */ + public int getCompressedSize() { + return compressedSize; + } + + /** + * Free the native structures associated with this compressor instance. + */ + public void close() throws Exception { + destroy(); + } + + protected void finalize() throws Throwable { + try { + close(); + } + catch(Exception e) {} + finally { + super.finalize(); + } + }; + + private native void init() throws Exception; + + private native void destroy() throws Exception; + + // JPEG size in bytes is returned + private native int compress(byte[] srcBuf, int width, int pitch, + int height, int pixelFormat, byte[] dstbuf, int jpegSubsamp, int jpegQual, + int flags) throws Exception; + + private native int compress(int[] srcBuf, int width, int pitch, + int height, int pixelFormat, byte[] dstbuf, int jpegSubsamp, int jpegQual, + int flags) throws Exception; + + private native void encodeYUV(byte[] srcBuf, int width, int pitch, + int height, int pixelFormat, byte[] dstbuf, int subsamp, int flags) + throws Exception; + + private native void encodeYUV(int[] srcBuf, int width, int pitch, + int height, int pixelFormat, byte[] dstbuf, int subsamp, int flags) + throws Exception; + + static { + TJLoader.load(); + } + + private long handle = 0; + private byte[] srcBuf = null; + private int srcWidth = 0; + private int srcHeight = 0; + private int srcPitch = 0; + private int srcPixelFormat = -1; + private int subsamp = -1; + private int jpegQuality = -1; + private int compressedSize = 0; + private ByteOrder byteOrder = null; +}; diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJCustomFilter.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJCustomFilter.java new file mode 100644 index 000000000..711225b77 --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJCustomFilter.java @@ -0,0 +1,76 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +import java.awt.*; +import java.nio.*; + +/** + * Custom filter callback interface + */ +public interface TJCustomFilter { + + /** + * A callback function that can be used to modify the DCT coefficients after + * they are losslessly transformed but before they are transcoded to a new + * JPEG file. This allows for custom filters or other transformations to be + * applied in the frequency domain. + * + * @param coeffBuffer a buffer containing transformed DCT coefficients. + * (NOTE: this buffer is not guaranteed to be valid once the callback + * returns, so applications wishing to hand off the DCT coefficients to + * another function or library should make a copy of them within the body of + * the callback.) + * + * @param bufferRegion rectangle containing the width and height of + * coeffBuffer as well as its offset relative to the component + * plane. TurboJPEG implementations may choose to split each component plane + * into multiple DCT coefficient buffers and call the callback function once + * for each buffer. + * + * @param planeRegion rectangle containing the width and height of the + * component plane to which coeffBuffer belongs + * + * @param componentID ID number of the component plane to which + * coeffBufferbelongs (Y, Cb, and Cr have, respectively, ID's of + * 0, 1, and 2 in typical JPEG images.) + * + * @param transformID ID number of the transformed image to which + * coeffBuffer belongs. This is the same as the index of the + * transform in the transforms array that was passed to {@link + * TJTransformer#transform TJTransformer.transform()}. + * + * @param transform a {@link TJTransform} instance that specifies the + * parameters and/or cropping region for this transform + */ + public void customFilter(ShortBuffer coeffBuffer, Rectangle bufferRegion, + Rectangle planeRegion, int componentID, int transformID, + TJTransform transform) + throws Exception; +} diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJDecompressor.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJDecompressor.java new file mode 100644 index 000000000..de6cacc42 --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJDecompressor.java @@ -0,0 +1,520 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +import java.awt.image.*; +import java.nio.*; + +/** + * TurboJPEG decompressor + */ +public class TJDecompressor { + + private final static String NO_ASSOC_ERROR = + "No JPEG image is associated with this instance"; + + /** + * Create a TurboJPEG decompresssor instance. + */ + public TJDecompressor() throws Exception { + init(); + } + + /** + * Create a TurboJPEG decompressor instance and associate the JPEG image + * stored in jpegImage with the newly-created instance. + * + * @param jpegImage JPEG image buffer (size of the JPEG image is assumed to + * be the length of the array) + */ + public TJDecompressor(byte[] jpegImage) throws Exception { + init(); + setJPEGImage(jpegImage, jpegImage.length); + } + + /** + * Create a TurboJPEG decompressor instance and associate the JPEG image + * of length imageSize bytes stored in jpegImage + * with the newly-created instance. + * + * @param jpegImage JPEG image buffer + * + * @param imageSize size of the JPEG image (in bytes) + */ + public TJDecompressor(byte[] jpegImage, int imageSize) throws Exception { + init(); + setJPEGImage(jpegImage, imageSize); + } + + /** + * Associate the JPEG image of length imageSize bytes stored in + * jpegImage with this decompressor instance. This image will + * be used as the source image for subsequent decompress operations. + * + * @param jpegImage JPEG image buffer + * + * @param imageSize size of the JPEG image (in bytes) + */ + public void setJPEGImage(byte[] jpegImage, int imageSize) throws Exception { + if(jpegImage == null || imageSize < 1) + throw new Exception("Invalid argument in setJPEGImage()"); + jpegBuf = jpegImage; + jpegBufSize = imageSize; + decompressHeader(jpegBuf, jpegBufSize); + } + + /** + * Returns the width of the JPEG image associated with this decompressor + * instance. + * + * @return the width of the JPEG image associated with this decompressor + * instance + */ + public int getWidth() throws Exception { + if(jpegWidth < 1) throw new Exception(NO_ASSOC_ERROR); + return jpegWidth; + } + + /** + * Returns the height of the JPEG image associated with this decompressor + * instance. + * + * @return the height of the JPEG image associated with this decompressor + * instance + */ + public int getHeight() throws Exception { + if(jpegHeight < 1) throw new Exception(NO_ASSOC_ERROR); + return jpegHeight; + } + + /** + * Returns the level of chrominance subsampling used in the JPEG image + * associated with this decompressor instance. + * + * @return the level of chrominance subsampling used in the JPEG image + * associated with this decompressor instance + */ + public int getSubsamp() throws Exception { + if(jpegSubsamp < 0) throw new Exception(NO_ASSOC_ERROR); + if(jpegSubsamp >= TJ.NUMSAMP) + throw new Exception("JPEG header information is invalid"); + return jpegSubsamp; + } + + /** + * Returns the JPEG image buffer associated with this decompressor instance. + * + * @return the JPEG image buffer associated with this decompressor instance + */ + public byte[] getJPEGBuf() throws Exception { + if(jpegBuf == null) throw new Exception(NO_ASSOC_ERROR); + return jpegBuf; + } + + /** + * Returns the size of the JPEG image (in bytes) associated with this + * decompressor instance. + * + * @return the size of the JPEG image (in bytes) associated with this + * decompressor instance + */ + public int getJPEGSize() throws Exception { + if(jpegBufSize < 1) throw new Exception(NO_ASSOC_ERROR); + return jpegBufSize; + } + + + /** + * Returns the width of the largest scaled down image that the TurboJPEG + * decompressor can generate without exceeding the desired image width and + * height. + * + * @param desiredWidth desired width (in pixels) of the decompressed image. + * Setting this to 0 is the same as setting it to the width of the JPEG image + * (in other words, the width will not be considered when determining the + * scaled image size.) + * + * @param desiredHeight desired height (in pixels) of the decompressed image. + * Setting this to 0 is the same as setting it to the height of the JPEG + * image (in other words, the height will not be considered when determining + * the scaled image size.) + * + * @return the width of the largest scaled down image that the TurboJPEG + * decompressor can generate without exceeding the desired image width and + * height + */ + public int getScaledWidth(int desiredWidth, int desiredHeight) + throws Exception { + if(jpegWidth < 1 || jpegHeight < 1) + throw new Exception(NO_ASSOC_ERROR); + if(desiredWidth < 0 || desiredHeight < 0) + throw new Exception("Invalid argument in getScaledWidth()"); + TJScalingFactor sf[] = TJ.getScalingFactors(); + if(desiredWidth == 0) desiredWidth = jpegWidth; + if(desiredHeight == 0) desiredHeight = jpegHeight; + int scaledWidth = jpegWidth, scaledHeight = jpegHeight; + for(int i = 0; i < sf.length; i++) { + scaledWidth = sf[i].getScaled(jpegWidth); + scaledHeight = sf[i].getScaled(jpegHeight); + if(scaledWidth <= desiredWidth && scaledHeight <= desiredHeight) + break; + } + if(scaledWidth > desiredWidth || scaledHeight > desiredHeight) + throw new Exception("Could not scale down to desired image dimensions"); + return scaledWidth; + } + + /** + * Returns the height of the largest scaled down image that the TurboJPEG + * decompressor can generate without exceeding the desired image width and + * height. + * + * @param desiredWidth desired width (in pixels) of the decompressed image. + * Setting this to 0 is the same as setting it to the width of the JPEG image + * (in other words, the width will not be considered when determining the + * scaled image size.) + * + * @param desiredHeight desired height (in pixels) of the decompressed image. + * Setting this to 0 is the same as setting it to the height of the JPEG + * image (in other words, the height will not be considered when determining + * the scaled image size.) + * + * @return the height of the largest scaled down image that the TurboJPEG + * decompressor can generate without exceeding the desired image width and + * height + */ + public int getScaledHeight(int desiredWidth, int desiredHeight) + throws Exception { + if(jpegWidth < 1 || jpegHeight < 1) + throw new Exception(NO_ASSOC_ERROR); + if(desiredWidth < 0 || desiredHeight < 0) + throw new Exception("Invalid argument in getScaledHeight()"); + TJScalingFactor sf[] = TJ.getScalingFactors(); + if(desiredWidth == 0) desiredWidth = jpegWidth; + if(desiredHeight == 0) desiredHeight = jpegHeight; + int scaledWidth = jpegWidth, scaledHeight = jpegHeight; + for(int i = 0; i < sf.length; i++) { + scaledWidth = sf[i].getScaled(jpegWidth); + scaledHeight = sf[i].getScaled(jpegHeight); + if(scaledWidth <= desiredWidth && scaledHeight <= desiredHeight) + break; + } + if(scaledWidth > desiredWidth || scaledHeight > desiredHeight) + throw new Exception("Could not scale down to desired image dimensions"); + return scaledHeight; + } + + /** + * Decompress the JPEG source image associated with this decompressor + * instance and output a decompressed image to the given destination buffer. + * + * @param dstBuf buffer that will receive the decompressed image. This + * buffer should normally be pitch * scaledHeight bytes in size, + * where scaledHeight can be determined by calling + * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegHeight) + * with one of the scaling factors returned from {@link + * TJ#getScalingFactors} or by calling {@link #getScaledHeight}. + * + * @param desiredWidth desired width (in pixels) of the decompressed image. + * If the desired image dimensions are smaller than the dimensions of the + * JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG + * decompressor to generate the largest possible image that will fit within + * the desired dimensions. Setting this to 0 is the same as setting it to + * the width of the JPEG image (in other words, the width will not be + * considered when determining the scaled image size.) + * + * @param pitch bytes per line of the destination image. Normally, this + * should be set to scaledWidth * TJ.pixelSize(pixelFormat) if + * the decompressed image is unpadded, but you can use this to, for instance, + * pad each line of the decompressed image to a 4-byte boundary. NOTE: + * scaledWidth can be determined by calling + * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegWidth) + * or by calling {@link #getScaledWidth}. Setting this parameter to + * 0 is the equivalent of setting it to scaledWidth * + * TJ.pixelSize(pixelFormat). + * + * @param desiredHeight desired height (in pixels) of the decompressed image. + * If the desired image dimensions are smaller than the dimensions of the + * JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG + * decompressor to generate the largest possible image that will fit within + * the desired dimensions. Setting this to 0 is the same as setting it to + * the height of the JPEG image (in other words, the height will not be + * considered when determining the scaled image size.) + * + * @param pixelFormat pixel format of the decompressed image (one of + * {@link TJ TJ.PF_*}) + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void decompress(byte[] dstBuf, int desiredWidth, int pitch, + int desiredHeight, int pixelFormat, int flags) throws Exception { + if(jpegBuf == null) throw new Exception(NO_ASSOC_ERROR); + if(dstBuf == null || desiredWidth < 0 || pitch < 0 || desiredHeight < 0 + || pixelFormat < 0 || pixelFormat >= TJ.NUMPF || flags < 0) + throw new Exception("Invalid argument in decompress()"); + decompress(jpegBuf, jpegBufSize, dstBuf, desiredWidth, pitch, + desiredHeight, pixelFormat, flags); + } + + /** + * Decompress the JPEG source image associated with this decompressor + * instance and return a buffer containing the decompressed image. + * + * @param desiredWidth see + * {@link #decompress(byte[], int, int, int, int, int)} for description + * + * @param pitch see + * {@link #decompress(byte[], int, int, int, int, int)} for description + * + * @param desiredHeight see + * {@link #decompress(byte[], int, int, int, int, int)} for description + * + * @param pixelFormat pixel format of the decompressed image (one of + * {@link TJ TJ.PF_*}) + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + * + * @return a buffer containing the decompressed image + */ + public byte[] decompress(int desiredWidth, int pitch, int desiredHeight, + int pixelFormat, int flags) throws Exception { + if(desiredWidth < 0 || pitch < 0 || desiredHeight < 0 + || pixelFormat < 0 || pixelFormat >= TJ.NUMPF || flags < 0) + throw new Exception("Invalid argument in decompress()"); + int pixelSize = TJ.getPixelSize(pixelFormat); + int scaledWidth = getScaledWidth(desiredWidth, desiredHeight); + int scaledHeight = getScaledHeight(desiredWidth, desiredHeight); + if(pitch == 0) pitch = scaledWidth * pixelSize; + byte[] buf = new byte[pitch * scaledHeight]; + decompress(buf, desiredWidth, pitch, desiredHeight, pixelFormat, flags); + return buf; + } + + /** + * Decompress the JPEG source image associated with this decompressor + * instance and output a YUV planar image to the given destination buffer. + * This method performs JPEG decompression but leaves out the color + * conversion step, so a planar YUV image is generated instead of an RGB + * image. The padding of the planes in this image is the same as the images + * generated by {@link TJCompressor#encodeYUV(byte[], int)}. Note that, if + * the width or height of the image is not an even multiple of the MCU block + * size (see {@link TJ#getMCUWidth} and {@link TJ#getMCUHeight}), then an + * intermediate buffer copy will be performed within TurboJPEG. + * + * @param dstBuf buffer that will receive the YUV planar image. Use + * {@link TJ#bufSizeYUV} to determine the appropriate size for this buffer + * based on the image width, height, and level of chrominance subsampling. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void decompressToYUV(byte[] dstBuf, int flags) throws Exception { + if(jpegBuf == null) throw new Exception(NO_ASSOC_ERROR); + if(dstBuf == null || flags < 0) + throw new Exception("Invalid argument in decompressToYUV()"); + decompressToYUV(jpegBuf, jpegBufSize, dstBuf, flags); + } + + + /** + * Decompress the JPEG source image associated with this decompressor + * instance and return a buffer containing a YUV planar image. See {@link + * #decompressToYUV(byte[], int)} for more detail. + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + * + * @return a buffer containing a YUV planar image + */ + public byte[] decompressToYUV(int flags) throws Exception { + if(flags < 0) + throw new Exception("Invalid argument in decompressToYUV()"); + if(jpegWidth < 1 || jpegHeight < 1 || jpegSubsamp < 0) + throw new Exception(NO_ASSOC_ERROR); + if(jpegSubsamp >= TJ.NUMSAMP) + throw new Exception("JPEG header information is invalid"); + byte[] buf = new byte[TJ.bufSizeYUV(jpegWidth, jpegHeight, jpegSubsamp)]; + decompressToYUV(buf, flags); + return buf; + } + + /** + * Decompress the JPEG source image associated with this decompressor + * instance and output a decompressed image to the given + * BufferedImage instance. + * + * @param dstImage a BufferedImage instance that will receive + * the decompressed image + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void decompress(BufferedImage dstImage, int flags) throws Exception { + if(dstImage == null || flags < 0) + throw new Exception("Invalid argument in decompress()"); + int desiredWidth = dstImage.getWidth(); + int desiredHeight = dstImage.getHeight(); + int scaledWidth = getScaledWidth(desiredWidth, desiredHeight); + int scaledHeight = getScaledHeight(desiredWidth, desiredHeight); + if(scaledWidth != desiredWidth || scaledHeight != desiredHeight) + throw new Exception("BufferedImage dimensions do not match a scaled image size that TurboJPEG is capable of generating."); + int pixelFormat; boolean intPixels = false; + if(byteOrder == null) + byteOrder = ByteOrder.nativeOrder(); + switch(dstImage.getType()) { + case BufferedImage.TYPE_3BYTE_BGR: + pixelFormat = TJ.PF_BGR; break; + case BufferedImage.TYPE_4BYTE_ABGR: + case BufferedImage.TYPE_4BYTE_ABGR_PRE: + pixelFormat = TJ.PF_XBGR; break; + case BufferedImage.TYPE_BYTE_GRAY: + pixelFormat = TJ.PF_GRAY; break; + case BufferedImage.TYPE_INT_BGR: + if(byteOrder == ByteOrder.BIG_ENDIAN) + pixelFormat = TJ.PF_XBGR; + else + pixelFormat = TJ.PF_RGBX; + intPixels = true; break; + case BufferedImage.TYPE_INT_RGB: + if(byteOrder == ByteOrder.BIG_ENDIAN) + pixelFormat = TJ.PF_XRGB; + else + pixelFormat = TJ.PF_BGRX; + intPixels = true; break; + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + if(byteOrder == ByteOrder.BIG_ENDIAN) + pixelFormat = TJ.PF_ARGB; + else + pixelFormat = TJ.PF_BGRA; + intPixels = true; break; + default: + throw new Exception("Unsupported BufferedImage format"); + } + WritableRaster wr = dstImage.getRaster(); + if(intPixels) { + SinglePixelPackedSampleModel sm = + (SinglePixelPackedSampleModel)dstImage.getSampleModel(); + int pitch = sm.getScanlineStride(); + DataBufferInt db = (DataBufferInt)wr.getDataBuffer(); + int[] buf = db.getData(); + if(jpegBuf == null) throw new Exception(NO_ASSOC_ERROR); + decompress(jpegBuf, jpegBufSize, buf, scaledWidth, pitch, scaledHeight, + pixelFormat, flags); + } + else { + ComponentSampleModel sm = + (ComponentSampleModel)dstImage.getSampleModel(); + int pixelSize = sm.getPixelStride(); + if(pixelSize != TJ.getPixelSize(pixelFormat)) + throw new Exception("Inconsistency between pixel format and pixel size in BufferedImage"); + int pitch = sm.getScanlineStride(); + DataBufferByte db = (DataBufferByte)wr.getDataBuffer(); + byte[] buf = db.getData(); + decompress(buf, scaledWidth, pitch, scaledHeight, pixelFormat, flags); + } + } + + /** + * Decompress the JPEG source image associated with this decompressor + * instance and return a BufferedImage instance containing the + * decompressed image. + * + * @param desiredWidth see + * {@link #decompress(byte[], int, int, int, int, int)} for description + * + * @param desiredHeight see + * {@link #decompress(byte[], int, int, int, int, int)} for description + * + * @param bufferedImageType the image type of the newly-created + * BufferedImage instance (for instance, + * BufferedImage.TYPE_INT_RGB) + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + * + * @return a BufferedImage instance containing the + * decompressed image + */ + public BufferedImage decompress(int desiredWidth, int desiredHeight, + int bufferedImageType, int flags) throws Exception { + if(desiredWidth < 0 || desiredHeight < 0 || flags < 0) + throw new Exception("Invalid argument in decompress()"); + int scaledWidth = getScaledWidth(desiredWidth, desiredHeight); + int scaledHeight = getScaledHeight(desiredWidth, desiredHeight); + BufferedImage img = new BufferedImage(scaledWidth, scaledHeight, + bufferedImageType); + decompress(img, flags); + return img; + } + + /** + * Free the native structures associated with this decompressor instance. + */ + public void close() throws Exception { + destroy(); + } + + protected void finalize() throws Throwable { + try { + close(); + } + catch(Exception e) {} + finally { + super.finalize(); + } + }; + + private native void init() throws Exception; + + private native void destroy() throws Exception; + + private native void decompressHeader(byte[] srcBuf, int size) + throws Exception; + + private native void decompress(byte[] srcBuf, int size, byte[] dstBuf, + int desiredWidth, int pitch, int desiredHeight, int pixelFormat, int flags) + throws Exception; + + private native void decompress(byte[] srcBuf, int size, int[] dstBuf, + int desiredWidth, int pitch, int desiredHeight, int pixelFormat, int flags) + throws Exception; + + private native void decompressToYUV(byte[] srcBuf, int size, byte[] dstBuf, + int flags) + throws Exception; + + static { + TJLoader.load(); + } + + protected long handle = 0; + protected byte[] jpegBuf = null; + protected int jpegBufSize = 0; + protected int jpegWidth = 0; + protected int jpegHeight = 0; + protected int jpegSubsamp = -1; + private ByteOrder byteOrder = null; +}; diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJLoader.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJLoader.java new file mode 100644 index 000000000..db77bbaec --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJLoader.java @@ -0,0 +1,35 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +final class TJLoader { + static void load() { + System.loadLibrary("turbojpeg"); + } +}; diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJLoader.java.in b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJLoader.java.in new file mode 100644 index 000000000..22353a5df --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJLoader.java.in @@ -0,0 +1,35 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +final class TJLoader { + static void load() { + System.loadLibrary("@TURBOJPEG_DLL_NAME@"); + } +}; diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJScalingFactor.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJScalingFactor.java new file mode 100644 index 000000000..d71ceee0b --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJScalingFactor.java @@ -0,0 +1,98 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +/** + * Fractional scaling factor + */ +public class TJScalingFactor { + + public TJScalingFactor(int num, int denom) throws Exception { + if(num < 1 || denom < 1) + throw new Exception("Numerator and denominator must be >= 1"); + this.num = num; + this.denom = denom; + } + + /** + * Returns numerator + * @return numerator + */ + public int getNum() { + return num; + } + + /** + * Returns denominator + * @return denominator + */ + public int getDenom() { + return denom; + } + + /** + * Returns the scaled value of dimension. This function + * performs the integer equivalent of + * ceil(dimension * scalingFactor). + * @return the scaled value of dimension + */ + public int getScaled(int dimension) { + return (dimension * num + denom - 1) / denom; + } + + /** + * Returns true or false, depending on whether this instance and + * other have the same numerator and denominator. + * @return true or false, depending on whether this instance and + * other have the same numerator and denominator + */ + public boolean equals(TJScalingFactor other) { + return (this.num == other.num && this.denom == other.denom); + } + + /** + * Returns true or false, depending on whether this instance is equal to + * 1/1. + * @return true or false, depending on whether this instance is equal to + * 1/1 + */ + public boolean isOne() { + return (num == 1 && denom == 1); + } + + /** + * Numerator + */ + private int num = 1; + + /** + * Denominator + */ + private int denom = 1; +}; diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJTransform.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJTransform.java new file mode 100644 index 000000000..399cf3a60 --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJTransform.java @@ -0,0 +1,202 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +import java.awt.*; + +/** + * Lossless transform parameters + */ +public class TJTransform extends Rectangle { + + private static final long serialVersionUID = -127367705761430371L; + + /** + * The number of lossless transform operations + */ + final public static int NUMOP = 8; + /** + * Do not transform the position of the image pixels. + */ + final public static int OP_NONE = 0; + /** + * Flip (mirror) image horizontally. This transform is imperfect if there + * are any partial MCU blocks on the right edge. + * @see #OPT_PERFECT + */ + final public static int OP_HFLIP = 1; + /** + * Flip (mirror) image vertically. This transform is imperfect if there are + * any partial MCU blocks on the bottom edge. + * @see #OPT_PERFECT + */ + final public static int OP_VFLIP = 2; + /** + * Transpose image (flip/mirror along upper left to lower right axis). This + * transform is always perfect. + * @see #OPT_PERFECT + */ + final public static int OP_TRANSPOSE = 3; + /** + * Transverse transpose image (flip/mirror along upper right to lower left + * axis). This transform is imperfect if there are any partial MCU blocks in + * the image. + * @see #OPT_PERFECT + */ + final public static int OP_TRANSVERSE = 4; + /** + * Rotate image clockwise by 90 degrees. This transform is imperfect if + * there are any partial MCU blocks on the bottom edge. + * @see #OPT_PERFECT + */ + final public static int OP_ROT90 = 5; + /** + * Rotate image 180 degrees. This transform is imperfect if there are any + * partial MCU blocks in the image. + * @see #OPT_PERFECT + */ + final public static int OP_ROT180 = 6; + /** + * Rotate image counter-clockwise by 90 degrees. This transform is imperfect + * if there are any partial MCU blocks on the right edge. + * @see #OPT_PERFECT + */ + final public static int OP_ROT270 = 7; + + + /** + * This option will cause {@link TJTransformer#transform + * TJTransformer.transform()} to throw an exception if the transform is not + * perfect. Lossless transforms operate on MCU blocks, whose size depends on + * the level of chrominance subsampling used. If the image's width or height + * is not evenly divisible by the MCU block size (see {@link TJ#getMCUWidth} + * and {@link TJ#getMCUHeight}), then there will be partial MCU blocks on the + * right and/or bottom edges. It is not possible to move these partial MCU + * blocks to the top or left of the image, so any transform that would + * require that is "imperfect." If this option is not specified, then any + * partial MCU blocks that cannot be transformed will be left in place, which + * will create odd-looking strips on the right or bottom edge of the image. + */ + final public static int OPT_PERFECT = 1; + /** + * This option will discard any partial MCU blocks that cannot be + * transformed. + */ + final public static int OPT_TRIM = 2; + /** + * This option will enable lossless cropping. + */ + final public static int OPT_CROP = 4; + /** + * This option will discard the color data in the input image and produce + * a grayscale output image. + */ + final public static int OPT_GRAY = 8; + /** + * This option will prevent {@link TJTransformer#transform + * TJTransformer.transform()} from outputting a JPEG image for this + * particular transform. This can be used in conjunction with a custom + * filter to capture the transformed DCT coefficients without transcoding + * them. + */ + final public static int OPT_NOOUTPUT = 16; + + + /** + * Create a new lossless transform instance. + */ + public TJTransform() { + } + + /** + * Create a new lossless transform instance with the given parameters. + * + * @param x the left boundary of the cropping region. This must be evenly + * divisible by the MCU block width (see {@link TJ#getMCUWidth}) + * + * @param y the upper boundary of the cropping region. This must be evenly + * divisible by the MCU block height (see {@link TJ#getMCUHeight}) + * + * @param w the width of the cropping region. Setting this to 0 is the + * equivalent of setting it to the width of the source JPEG image - x. + * + * @param h the height of the cropping region. Setting this to 0 is the + * equivalent of setting it to the height of the source JPEG image - y. + * + * @param op one of the transform operations (OP_*) + * + * @param options the bitwise OR of one or more of the transform options + * (OPT_*) + * + * @param cf an instance of an object that implements the {@link + * TJCustomFilter} interface, or null if no custom filter is needed + */ + public TJTransform(int x, int y, int w, int h, int op, int options, + TJCustomFilter cf) throws Exception { + super(x, y, w, h); + this.op = op; this.options = options; this.cf = cf; + } + + /** + * Create a new lossless transform instance with the given parameters. + * + * @param r a Rectangle instance that specifies the cropping + * region. See {@link + * #TJTransform(int, int, int, int, int, int, TJCustomFilter)} for more + * detail. + * + * @param op one of the transform operations (OP_*) + * + * @param options the bitwise OR of one or more of the transform options + * (OPT_*) + * + * @param cf an instance of an object that implements the {@link + * TJCustomFilter} interface, or null if no custom filter is needed + */ + public TJTransform(Rectangle r, int op, int options, + TJCustomFilter cf) throws Exception { + super(r); + this.op = op; this.options = options; this.cf = cf; + } + + /** + * Transform operation (one of OP_*) + */ + public int op = 0; + + /** + * Transform options (bitwise OR of one or more of OPT_*) + */ + public int options = 0; + + /** + * Custom filter instance + */ + public TJCustomFilter cf = null; +} diff --git a/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJTransformer.java b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJTransformer.java new file mode 100644 index 000000000..6c0748394 --- /dev/null +++ b/library/libjpegturbo/src/main/java/org/libjpegturbo/turbojpeg/TJTransformer.java @@ -0,0 +1,158 @@ +/* + * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.libjpegturbo.turbojpeg; + +/** + * TurboJPEG lossless transformer + */ +public class TJTransformer extends TJDecompressor { + + /** + * Create a TurboJPEG lossless transformer instance. + */ + public TJTransformer() throws Exception { + init(); + } + + /** + * Create a TurboJPEG lossless transformer instance and associate the JPEG + * image stored in jpegImage with the newly-created instance. + * + * @param jpegImage JPEG image buffer (size of the JPEG image is assumed to + * be the length of the array) + */ + public TJTransformer(byte[] jpegImage) throws Exception { + init(); + setJPEGImage(jpegImage, jpegImage.length); + } + + /** + * Create a TurboJPEG lossless transformer instance and associate the JPEG + * image of length imageSize bytes stored in + * jpegImage with the newly-created instance. + * + * @param jpegImage JPEG image buffer + * + * @param imageSize size of the JPEG image (in bytes) + */ + public TJTransformer(byte[] jpegImage, int imageSize) throws Exception { + init(); + setJPEGImage(jpegImage, imageSize); + } + + /** + * Losslessly transform the JPEG image associated with this transformer + * instance into one or more JPEG images stored in the given destination + * buffers. Lossless transforms work by moving the raw coefficients from one + * JPEG image structure to another without altering the values of the + * coefficients. While this is typically faster than decompressing the + * image, transforming it, and re-compressing it, lossless transforms are not + * free. Each lossless transform requires reading and Huffman decoding all + * of the coefficients in the source image, regardless of the size of the + * destination image. Thus, this method provides a means of generating + * multiple transformed images from the same source or of applying multiple + * transformations simultaneously, in order to eliminate the need to read the + * source coefficients multiple times. + * + * @param dstBufs an array of image buffers. dstbufs[i] will + * receive a JPEG image that has been transformed using the parameters in + * transforms[i]. Use {@link TJ#bufSize} to determine the + * maximum size for each buffer based on the cropped width and height. + * + * @param transforms an array of {@link TJTransform} instances, each of + * which specifies the transform parameters and/or cropping region for the + * corresponding transformed output image + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public void transform(byte[][] dstBufs, TJTransform[] transforms, + int flags) throws Exception { + if(jpegBuf == null) throw new Exception("JPEG buffer not initialized"); + transformedSizes = transform(jpegBuf, jpegBufSize, dstBufs, transforms, + flags); + } + + /** + * Losslessly transform the JPEG image associated with this transformer + * instance and return an array of {@link TJDecompressor} instances, each of + * which has a transformed JPEG image associated with it. + * + * @param transforms an array of {@link TJTransform} instances, each of + * which specifies the transform parameters and/or cropping region for the + * corresponding transformed output image + * + * @return an array of {@link TJDecompressor} instances, each of + * which has a transformed JPEG image associated with it + * + * @param flags the bitwise OR of one or more of {@link TJ TJ.FLAG_*} + */ + public TJDecompressor[] transform(TJTransform[] transforms, int flags) + throws Exception { + byte[][] dstBufs = new byte[transforms.length][]; + if(jpegWidth < 1 || jpegHeight < 1) + throw new Exception("JPEG buffer not initialized"); + for(int i = 0; i < transforms.length; i++) { + int w = jpegWidth, h = jpegHeight; + if((transforms[i].options & TJTransform.OPT_CROP) != 0) { + if(transforms[i].width != 0) w = transforms[i].width; + if(transforms[i].height != 0) h = transforms[i].height; + } + dstBufs[i] = new byte[TJ.bufSize(w, h, jpegSubsamp)]; + } + TJDecompressor[] tjd = new TJDecompressor[transforms.length]; + transform(dstBufs, transforms, flags); + for(int i = 0; i < transforms.length; i++) + tjd[i] = new TJDecompressor(dstBufs[i], transformedSizes[i]); + return tjd; + } + + /** + * Returns an array containing the sizes of the transformed JPEG images from + * the most recent call to {@link #transform transform()}. + * + * @return an array containing the sizes of the transformed JPEG images from + * the most recent call to {@link #transform transform()} + */ + public int[] getTransformedSizes() throws Exception { + if(transformedSizes == null) + throw new Exception("No image has been transformed yet"); + return transformedSizes; + } + + private native void init() throws Exception; + + private native int[] transform(byte[] srcBuf, int srcSize, byte[][] dstBufs, + TJTransform[] transforms, int flags) throws Exception; + + static { + TJLoader.load(); + } + + private int[] transformedSizes = null; +}; diff --git a/library/pom.xml b/library/pom.xml index ee79e963d..0378dfaa7 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -41,5 +41,6 @@ imagereadmt jmatio netcdf-core + libjpegturbo diff --git a/plugin/pom.xml b/plugin/pom.xml index 060547f00..85b350db7 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -84,6 +84,7 @@ arcgrid tiff gdal + turbojpeg unidata-netcdf diff --git a/plugin/turbojpeg/pom.xml b/plugin/turbojpeg/pom.xml new file mode 100644 index 000000000..48df5213a --- /dev/null +++ b/plugin/turbojpeg/pom.xml @@ -0,0 +1,83 @@ + + 4.0.0 + + + it.geosolutions.imageio-ext + imageio-ext-plugin + 1.2-SNAPSHOT + + + it.geosolutions.imageio-ext + imageio-ext-turbojpeg + 1.2-SNAPSHOT + + jar + + Turbo Jpeg Plugin + A plugin using the libjpeg-turbo binary lib. + + + + it.geosolutions.imageio-ext + imageio-ext-streams + + + it.geosolutions.imageio-ext + imageio-ext-imagereadmt + + + it.geosolutions.imageio-ext + imageio-ext-test-data + test + + + it.geosolutions.imageio-ext + imageio-ext-utilities + + + + + org.libjpegturbo + turbojpeg-wrapper + 1.2.1.1 + + + + commons-io + commons-io + 1.4 + + + + junit + junit + 4.10 + test + + + + + + + + + diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFMetadata.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFMetadata.java new file mode 100644 index 000000000..664e1c6f8 --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFMetadata.java @@ -0,0 +1,113 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.exif; + +import it.geosolutions.imageio.plugins.exif.EXIFTags.Type; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Daniele Romagnoli, GeoSolutions SAS + * + * Class representing EXIF entity in terms of list of baseline TIFF Tags and specific EXIF tags. + */ +public class EXIFMetadata { + + Map> tags; + +// /** Package private list of tags */ +// List baselineExifTags; +// List exifTags; +// + /** + * In order to minimize inner checks, make sure that the elements in each list + * are provided in ascending order, as requested by the EXIF specification. + * + * @param baselineExifTags a {@link List} containing baseline TIFF tags elements, + * already sorted in ascending order. + * @param exifTags a {@link List} containing EXIF TIFF tags elements, + * already sorted in ascending order. + */ + public EXIFMetadata( + List baselineExifTags, + List exifTags) { + tags = new HashMap>(); + tags.put(Type.BASELINE, baselineExifTags); + tags.put(Type.EXIF, exifTags); + } + + /** + * In order to minimize inner checks, make sure that the elements in each list of the map + * are provided in ascending order, as requested by the EXIF specification. + * + * @param tagsMap the map containing EXIF tags. The map won't be cloned + */ + public EXIFMetadata(Map> tagsMap) { + this.tags = tagsMap; + } + + /** + * Set the specified TAG of the specified list, with the specified content. + * The TAG needs to be already present within the list. No Tags will be added to the list + * if missing. The content set will also update the count value. + * + * @param tagNumber the number of the tag to be updated. + * @param content the content to be set for that tag. + * @param tagType the type of TAGs list to be scanned. + */ + public void setTag( + final int tagNumber, + final Object content, + final Type tagType){ + List list = getList(tagType); + for (TIFFTagWrapper wrapper: list) { + if (wrapper.getNumber() == tagNumber){ + wrapper.setContent(content); + + // Update the count field on top of the content/prefix/suffix + if (content instanceof byte[]){ + int count = ((byte[]) content).length; + if (wrapper.getSuffix() != null){ + count += wrapper.getSuffix().length; + } + if (wrapper.getPrefix() != null){ + count += wrapper.getPrefix().length; + } + wrapper.setCount(count); + } + break; + } + } + } + + /** + * Return the proper tag list, depending on the specified {@link EXIFTagType}. + * @param tagType an {@link EXIFTagType} specified the type of tag. + * @return the specific tag list. + */ + List getList(final Type tagType) { + switch (tagType){ + case EXIF: + return tags.get(Type.EXIF); + case BASELINE: + return tags.get(Type.BASELINE); + } + return null; + } +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFTags.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFTags.java new file mode 100644 index 000000000..9c69e8bbd --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFTags.java @@ -0,0 +1,39 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.exif; + +import com.sun.media.imageio.plugins.tiff.BaselineTIFFTagSet; +import com.sun.media.imageio.plugins.tiff.EXIFParentTIFFTagSet; +import com.sun.media.imageio.plugins.tiff.EXIFTIFFTagSet; + +public class EXIFTags { + + public static final int COPYRIGHT = BaselineTIFFTagSet.TAG_COPYRIGHT; + + public static final int EXIF_IFD_POINTER = EXIFParentTIFFTagSet.TAG_EXIF_IFD_POINTER; + + public static final int USER_COMMENT = EXIFTIFFTagSet.TAG_USER_COMMENT; + + private EXIFTags() { + + } + + public enum Type{ + BASELINE, EXIF + //TODO more may be added in the future, like GPS, ... + } +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFUtilities.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFUtilities.java new file mode 100644 index 000000000..2af6e1dc6 --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/EXIFUtilities.java @@ -0,0 +1,899 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.exif; + +import it.geosolutions.imageio.plugins.exif.EXIFTags.Type; +import it.geosolutions.imageio.stream.input.FileImageInputStreamExt; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.io.FileUtils; + +import com.sun.media.imageio.plugins.tiff.BaselineTIFFTagSet; +import com.sun.media.imageio.plugins.tiff.EXIFParentTIFFTagSet; +import com.sun.media.imageio.plugins.tiff.EXIFTIFFTagSet; +import com.sun.media.imageio.plugins.tiff.TIFFTag; + +/** + * @author Daniele Romagnoli, GeoSolutions SAS + * + * Utility class providing methods to setup/parse EXIF, write it to stream, retrieve from stream. + */ +public class EXIFUtilities { + + /** @deprecated use {@link EXIFTags#COPYRIGHT} */ + public static final int TAG_COPYRIGHT = BaselineTIFFTagSet.TAG_COPYRIGHT; + + /** @deprecated use {@link EXIFTags#EXIF_IFD_POINTER} */ + public static final int TAG_EXIF_IFD_POINTER = EXIFParentTIFFTagSet.TAG_EXIF_IFD_POINTER; + + /** @deprecated use {@link EXIFTags#USER_COMMENT} */ + public static final int TAG_USER_COMMENT = EXIFTIFFTagSet.TAG_USER_COMMENT; + + /** @deprecated use {@link EXIFTags#Type} */ + public enum EXIFTagType{ + BASELINE, EXIF + //TODO more may be added in the future, like GPS, ... + } + /** + * Simple dummy class to wrap an EXIFMetadata instance as well as the length + * of the APP1 marker. + */ + static class EXIFMetadataWrapper { + public EXIFMetadata getExif() { + return exif; + } + + public void setExif(EXIFMetadata exif) { + this.exif = exif; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + /** + * @param exif + * @param length + */ + public EXIFMetadataWrapper(EXIFMetadata exif, int length) { + super(); + this.exif = exif; + this.length = length; + } + + EXIFMetadata exif; + + int length; + } + + /** Utility buffer size */ + final static int DEFAULT_BUFFER_SIZE = 4096; + + final static int EXIF_SCAN_BUFFER_SIZE = 32768; + + final static byte _0 = 0x00; + + final static byte FF = (byte) 0xFF; + + /** EXIF Marker identifier */ + final static byte[] EXIF_MARKER = new byte[] { 'E', 'x', 'i', 'f', _0, _0 }; + + /** Offset to be appended before starting IFD1 content */ + final static byte[] NEXT_IFD = new byte[] { _0, _0, _0, _0 }; + + /** BIG Endian TIFF HEADER */ + final static byte[] TIFF_HEADER = new byte[] { 'M', 'M', _0, 0x2A, _0, _0, _0, 8 }; + + final static int TIFF_HEADER_LENGTH = TIFF_HEADER.length; + + /** + * UserComment ASCII character code prefix to be inserted before any UserComment String when writing it + * into the EXIF marker + */ + final static byte[] USER_COMMENT_ASCII_CHAR_CODE = new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, _0, _0, _0 }; + + /** The APP1 Marker bytes, (APP1 is the one containing EXIF) */ + final static byte[] APP1_MARKER = new byte[] { FF, (byte) 0xE1 }; + + /** + * The Data Quantization Table Marker. + * It is contained within a JPEG encoded data stream before the data image. + * EXIF marker should be put before this marker + */ + final static byte[] DQT_MARKER = new byte[] { FF, (byte) 0xDB }; + + /** The specification requires 2 bytes to identify the number of tags */ + final static int BYTES_FOR_TAGS_NUMBER = 2; + + /** A byte array representing NULL String terminator, to be appended after any ASCII byte array */ + final static byte[] NULL_STRING = new byte[] { _0 }; + + /** + * The fixed length of each IFD, + * made of Number (2 bytes) + Type (2 bytes) + Count (4 bytes) + Value/Offset (4 Bytes) + */ + final static int IFD_LENGTH = 12; + + /** + * This method will update the image referred by the specified inputStream, by replacing + * the underlying EXIF with the one represented by the specified {@link EXIFMetadata} instance. + * Write the result to the specified {@link OutputStream}. + * + * @param outputStream the stream where to write the result + * @param inputStream the input stream referring to the original image to be copied back to the + * output + * @param exif the {@link EXIFMetadata} instance containing updated exif to replace the one + * contained into the input image. + * @param previousEXIFLength + * the length of the previous EXIF marker. + * It is needed in order to understand the portion of the input image to be copied back to + * the output + * @throws IOException + */ + private static void updateStream( + final OutputStream outputStream, + final FileImageInputStreamExt inputStream, + final EXIFMetadata exif, + final int previousEXIFLength) throws IOException { + ByteArrayOutputStream baos = null; + BufferedOutputStream bos = null; + try { + + // Setup a new byteArrayOutputStream on top of the Exif object + baos = initializeExifStream(exif, null); + + // Update this outputStream by copying bytes from the original image + // referred by the inputStream, but inserting updated EXIF + // instead of copying it from the original image + bos = new BufferedOutputStream(outputStream, DEFAULT_BUFFER_SIZE); + updateFromStream(bos, baos, inputStream, previousEXIFLength); + } finally { + if (bos != null) { + try { + bos.close(); + } catch (Throwable t) { + // Eat exception on close + } + } + if (baos != null) { + try { + baos.close(); + } catch (Throwable t) { + // Eat exception on close + } + } + } + + } + + /** + * This method allows to parse the provided {@link EXIFMetadata} object and put it into + * the specified outputStream while copying back the JPEG encoded image referred by + * the imageData argument. + * + * @param outputStream the stream where to write + * @param imageData the bytes containing JPEG encoded image data + * @param imageDataSize the number of bytes to be used from the data array + * @param exif the {@link EXIFMetadata} object holding EXIF. + * @throws IOException + */ + public static void insertEXIFintoStream( + final OutputStream outputStream, + final byte[] imageData, + final int imageDataSize, + final EXIFMetadata exif) throws IOException { + ByteArrayOutputStream baos = null; + if (outputStream instanceof ByteArrayOutputStream){ + baos = (ByteArrayOutputStream) outputStream; + writeToByteStream(baos, imageData, imageDataSize, exif); + } else { + writeBuffered(outputStream, imageData, imageDataSize, exif); + } + + } + + /** + * This method write the provided {@link EXIFMetadata} into the specified outputStream + * while copying back the JPEG encoded image referred by the imageData argument. + * + * @param outputStream the stream where to write + * @param imageData the bytes containing JPEG encoded image data + * @param imageDataSize the number of bytes to be used from the data array + * @param exif the {@link EXIFMetadata} object holding EXIF. + * @throws IOException + */ + private static void writeBuffered( + final OutputStream outputStream, + final byte[] imageData, + final int imageDataSize, + final EXIFMetadata exif) throws IOException { + ByteArrayOutputStream baos = null; + try { + baos = initializeExifStream(exif, null); + updateFromBytes(outputStream, baos, imageData, imageDataSize); + } finally { + if (baos != null) { + try { + baos.close(); + } catch (Throwable t) { + // Eat exception on close + } + } + } + } + + /** + * Write the specified {@link EXIFMetadata} object as well as the specified image data bytes to + * the specified outputStream. It will take care of inserting the EXIF marker in the proper + * location within the outputStream when writing image data bytes. + * + * @param outputStream the outputStream where to write + * @param imageData the bytes containing JPEG encoded image data + * @param imageDataSize the number of bytes to be used from the data array + * @param exif the {@link EXIFMetadata} object holding EXIF. + * @throws IOException + */ + private static void writeToByteStream( + ByteArrayOutputStream outputStream, + final byte[] imageData, + final int imageDataSize, + final EXIFMetadata exif) throws IOException { + + // locate the DQT marker in the input imageData bytes + final int dqtMarkerPos = locateFirst(imageData, DQT_MARKER); + if (dqtMarkerPos != -1) { + + // write to stream the initial part of imageData before appending EXIF marker + // at the proper position + outputStream.write(imageData, 0, dqtMarkerPos); + outputStream.flush(); + + // Append the EXIF content + outputStream = initializeExifStream(exif, outputStream); + outputStream.write(_0); + + // Proceed with writing the remaining part of image data bytes. + outputStream.write(imageData, dqtMarkerPos, imageDataSize - dqtMarkerPos); + } + } + + /** + * Initialize a ByteArrayOutputStream on top of an {@link EXIFMetadata} entity. + * + * @param exif an {@link EXIFMetadata} instance representing EXIF tags to be put to the stream + * @param outputStream an optional {@link ByteArrayOutputStream} where to write the exif marker. + * If null, a new {@link ByteArrayOutputStream} will be created and returned + * + * @return the {@link ByteArrayOutputStream} containing the written EXIF bytes. + * @throws IOException + */ + private static ByteArrayOutputStream initializeExifStream( + final EXIFMetadata exif, + final ByteArrayOutputStream outputStream) throws IOException { + + // Preliminar check. Write on: the provided ByteArrayOutputStream VS a newly created one + final ByteArrayOutputStream baos = outputStream == null ? new ByteArrayOutputStream() : outputStream; + + // Get exif tags from the specified EXIF object. + List baselineTags = exif.getList(Type.BASELINE); + List exifTags = exif.getList(Type.EXIF); + final int baseLength = TIFF_HEADER_LENGTH + BYTES_FOR_TAGS_NUMBER + NEXT_IFD.length; + + // Initialize number of fields and tags + final int numBaselineTags = baselineTags.size(); + final int numExifTags = exifTags.size(); + final byte[] numFieldsB = intToBytes(numBaselineTags); + final byte[] numSpecificFieldsB = intToBytes(numExifTags); + + // Initialize tags sizes and offsets + final int baseLineTagsOffsets[] = new int[numBaselineTags]; + final int baseLineTagsContentSizes[] = new int[numBaselineTags]; + computeOffsetsAndSizes(baselineTags, baseLength, baseLineTagsOffsets, baseLineTagsContentSizes); + final int baselineContentLength = sum(baseLineTagsContentSizes); + + final int exifTagsOffsets[] = new int[numExifTags]; + final int exifTagsContentSizes[] = new int[numExifTags]; + computeOffsetsAndSizes(exifTags, baseLineTagsOffsets[numBaselineTags - 1] + + BYTES_FOR_TAGS_NUMBER, exifTagsOffsets, exifTagsContentSizes); + final int exifTagsContentLength = sum(exifTagsContentSizes); + + // Compute total marker length (which is the first entry to be written out after the marker) + int app1Lenght = APP1_MARKER.length + - 1 // -1 due to the 0xFF part + + EXIF_MARKER.length + baseLength + + BYTES_FOR_TAGS_NUMBER // Num Fields (2 bytes) + + BYTES_FOR_TAGS_NUMBER // Num EXIF Fields (2 bytes) + + numBaselineTags * IFD_LENGTH + numExifTags * IFD_LENGTH // Bytes needed to represent all IFD + + baselineContentLength + exifTagsContentLength; // Bytes used for tags contents + + // Write headers + baos.write(APP1_MARKER); + baos.write(intToBytes(app1Lenght)); + baos.write(EXIF_MARKER); + baos.write(TIFF_HEADER); + baos.write(numFieldsB); + + // Write BaseLine IFDs and their content + writeIFDs(baos, baselineTags); + baos.write(NEXT_IFD); + writeTagsContent(baos, baselineTags); + baos.write(numSpecificFieldsB); + + // Write EXIF Specific IFDs and their content + writeIFDs(baos, exifTags); + writeTagsContent(baos, exifTags); + baos.flush(); + return baos; + } + + /** + * Update the EXIF content referred by a {@link FileImageInputStreamExt} using the + * content available in the {@link ByteArrayOutputStream}. Store the result + * to the specified {@link OutputStream}. + * + * @param outputStream a {@link OutputStream} where to write + * @param byteStream a {@link ByteArrayOutputStream} previously populated with the content + * of an EXIF marker. + * @param inputStream a {@link FileImageInputStreamExt} referring to a file containing + * EXIF metadata to be updated + * @param originalAPP1MarkerLength is the length of the original APP1 marker + * (the one containing EXIF content), before the update + * @throws IOException + */ + private static void updateFromStream( + final OutputStream outputStream, + final ByteArrayOutputStream byteStream, + final FileImageInputStreamExt inputStream, + final int originalAPP1MarkerLength) throws IOException { + final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int readlength = 0; + boolean replacedExif = false; + + // Read from the input stream + while ((readlength = inputStream.read(buffer)) != -1) { + + //Look for the EXIF marker + int app1MarkerPos = replacedExif ? -1 : locateFirst(buffer, APP1_MARKER); + + if (app1MarkerPos != -1) { + replacedExif = true; + + // Copy back all the previous part before putting the updated EXIF + outputStream.write(buffer, 0, app1MarkerPos); + outputStream.write(byteStream.toByteArray()); + outputStream.write(_0); + + // Copy back the remaining part of the image by skipping the original EXIF marker + // TODO: make sure we still work within the buffer boundaries + outputStream.write(buffer, app1MarkerPos + originalAPP1MarkerLength + 2, readlength + - (app1MarkerPos + originalAPP1MarkerLength + 2)); + } else { + outputStream.write(buffer, 0, readlength); + } + } + } + + /** + * Update the EXIF content of the image stored in the specified byte array, using the + * content available in the input {@link ByteArrayOutputStream}. Store the result + * to the specified {@link OutputStream} + * + * @param outputStream The stream where to write + * @param byteStream the byte stream containing exif metadata to be written + * @param imageData the original image bytes + * @param imageDataSize the portion of the image bytes array to be used + * @throws IOException + */ + private static void updateFromBytes( + final OutputStream outputStream, + final ByteArrayOutputStream byteStream, + final byte[] imageData, + final int imageDataSize) throws IOException { + int dqtMarkerPos = locateFirst(imageData, DQT_MARKER); + + // Look for the DQT marker + if (dqtMarkerPos != -1) { + + // copy back the first part of the image bytes + outputStream.write(imageData, 0, dqtMarkerPos); + + // insert the EXIF content + outputStream.write(byteStream.toByteArray()); + outputStream.write(_0); + + // continue copying back the remaining part of data + outputStream.write(imageData, dqtMarkerPos, imageDataSize - dqtMarkerPos); + outputStream.flush(); + } + } + + /** + * Write the IFDs referred by the tags list argument, to the specified stream. + * No content referred by a offset, is written. + * + * @param stream the {@link ByteArrayOutputStream} where to append the IFDs. + * @param tags the IFDs to be written. In order to respect the TIFF specification, make + * sure the element of this list are sorted in ascending order. + * @throws IOException + */ + private static void writeIFDs( + final ByteArrayOutputStream stream, + final List tags) + throws IOException { + if (stream == null) { + throw new IllegalArgumentException("Null stream has been provided"); + } + for (TIFFTagWrapper tag : tags) { + stream.write(tagAsBytes(tag, true)); + } + + } + + /** + * Write the content of the IFDs referred by the tags list argument, to the specified stream. + * Note that only the content of the IFDs having an offset will be written. + * + * @param stream the {@link ByteArrayOutputStream} where to append the IFDs content. + * @param tags the Tags list to be written. In order to respect the TIFF specification, make + * sure the element of this list are sorted in ascending order. + * @throws IOException + */ + private static void writeTagsContent( + final ByteArrayOutputStream stream, + final List tags) + throws IOException { + + // Scan tags looking for entries which has a not null content to be written + for (TIFFTagWrapper tag : tags) { + if (tag.getContent() != null) { + + //Make sure to write prefix and suffix bytes if present + if (tag.getPrefix() != null) { + stream.write(tag.getPrefix()); + } + stream.write((byte[]) tag.getContent()); + if (tag.getSuffix() != null) { + stream.write(tag.getSuffix()); + } + } + } + } + + /** + * Simply computes the sum of the value provided in the array. + * @param values + * @return + */ + final static int sum(final int values[]) { + int sum = 0; + for (int s : values) { + sum += s; + } + return sum; + } + + /** + * Scan the IFDs contained in the input tags collection. + * Store the offsetValue and size of each entry in the proper array. + * + * @param tags the tags to be scan + * @param baseOffset the baseOffset to be taken into account when computing + * the tags offsets + * @param valueOffsets the array where to put found valueOffsets + * @param sizes the array where to put found sizes + */ + private static void computeOffsetsAndSizes( + final List tags, + final int baseOffset, + final int[] valueOffsets, + final int[] sizes) { + final int elementsSize = tags.size(); + int i = 0; + int previousOffset = 0; + + // Scan the tags list + for (TIFFTagWrapper tag : tags) { + + // Offsets refer to position in the stream after the IFD, where the content will be written + valueOffsets[i] = baseOffset + elementsSize * IFD_LENGTH + previousOffset; + if (tag.getNumber() == EXIFParentTIFFTagSet.TAG_EXIF_IFD_POINTER) { + // Special case, being a pointer, make sure to set the offset as value, + // without byte counts increment + tag.setValue(valueOffsets[i]); + } else if (tag.getContent() != null) { + tag.setValue(valueOffsets[i]); + previousOffset += tag.getCount(); + sizes[i] = tag.getCount(); + } else { + sizes[i] = 0; + } + i++; + } + } + + /** + * Simple utility method returning a 2 bytes representation of a value, in compliance with the + * specified endianness + * + * @param value the value to be represented through 2 bytes + * @param isBigEndian {@code true} in case of bigEndian + * @return + */ + public final static byte[] intToBytes(final int value, final boolean isBigEndian) { + if (isBigEndian) { + return new byte[] { (byte) ((value >> 8) & 0xFF), (byte) (value & 0xFF) }; + } else { + return new byte[] { (byte) (value & 0xFF), (byte) ((value >> 8) & 0xFF) }; + } + } + + /** + * Simple utility method returning a 2 bytes representation of a value as bigEndian + * + * @param value the value to be represented through 2 bytes + * @return + */ + public final static byte[] intToBytes(final int value) { + return intToBytes(value, true); + } + + /** + * Simple utility methods returning an int built on top of 2 bytes, in compliance + * with the specified endianness + * @param buff the buffer containing 2 bytes to be transformed + * @param start the position within the buffer of the first byte to be transformed + * @param isBigEndian {@code true} in case we need to encode it as bigEndian + * @return + */ + public final static int bytes2ToInt(byte[] buff, int start, final boolean isBigEndian) { + int intValue = 0; + intValue |= buff[start + (isBigEndian ? 0 : 1)] & 0xFF; + intValue <<= 8; + intValue |= buff[start + (isBigEndian ? 1 : 0)] & 0xFF; + return intValue; + } + + /** + * Simple utility methods returning an int built on top of 4 bytes, in compliance + * with the specified endianness + * @param buff the buffer containing 4 bytes to be transformed + * @param start the position within the buffer of the first byte to be transformed + * @param isBigEndian {@code true} in case we need to encode it as bigEndian + * @return + */ + public final static int bytes4ToInt(byte[] buff, int start, final boolean isBigEndian) { + int intValue = 0; + intValue |= buff[start + (isBigEndian ? 0 : 3)] & 0xFF; + intValue <<= 8; + intValue |= buff[start + (isBigEndian ? 1 : 2)] & 0xFF; + intValue <<= 8; + intValue |= buff[start + (isBigEndian ? 2 : 1)] & 0xFF; + intValue <<= 8; + intValue |= buff[start + (isBigEndian ? 3 : 0)] & 0xFF; + return intValue; + } + + /** + * Return the specified {@link TIFFTagWrapper} as a byte array, by taking endianness into + * account + * + * @param tag + * @param isBigEndian + * @return + */ + private static byte[] tagAsBytes(final TIFFTagWrapper tag, final boolean isBigEndian) { + final byte[] output = new byte[IFD_LENGTH]; + final int number = tag.getNumber(); + final int type = tag.getType(); + final int count = tag.getCount(); + final int offset = tag.getValue(); + output[isBigEndian ? 1 : 0] = (byte) (number & 0xFF); + output[isBigEndian ? 0 : 1] = (byte) ((number >> 8) & 0xFF); + output[isBigEndian ? 3 : 2] = (byte) (type & 0xFF); + output[isBigEndian ? 2 : 3] = (byte) ((type >> 8) & 0xFF); + output[isBigEndian ? 7 : 4] = (byte) (count & 0xFF); + output[isBigEndian ? 6 : 5] = (byte) ((count >> 8) & 0xFF); + output[isBigEndian ? 5 : 6] = (byte) ((count >> 16) & 0xFF); + output[isBigEndian ? 4 : 7] = (byte) ((count >> 24) & 0xFF); + output[isBigEndian ? 11 : 8] = (byte) (offset & 0xFF); + output[isBigEndian ? 10 : 9] = (byte) ((offset >> 8) & 0xFF); + output[isBigEndian ? 9 : 10] = (byte) ((offset >> 16) & 0xFF); + output[isBigEndian ? 8 : 11] = (byte) ((offset >> 24) & 0xFF); + return output; + + } + + /** + * Scan the input byte buffer, looking for a candidate bytes sequence and return + * the index of the first occurrence within the input buffer, or -1 in case nothing + * is found. + */ + public static int locateFirst(final byte[] buffer, final byte[] candidate) { + if (IsEmptyLocate(buffer, candidate)) { + return -1; + } + for (int i = 0; i < buffer.length; i++) { + if (!IsMatch(buffer, i, candidate)) { + continue; + } + return i; + } + return -1; + } + + /** + * Scan the input buffer, starting from the specified position, and check whether it matches the + * candidate byte sequence. + * + * @param buffer the byte array to be scanned + * @param start the starting index + * @param candidate the byte array to be matched + * @return {@code true} in case the buffer match the candidate sequence + */ + final static boolean IsMatch(final byte[] buffer, final int start, final byte[] candidate) { + if (candidate.length > (buffer.length - start)) { + return false; + } + + for (int i = 0; i < candidate.length; i++) { + if (buffer[start + i] != candidate[i]) { + return false; + } + } + + return true; + } + + /** + * Simple utility method which check for conditions which won't allow to get a match + * from the scan. As an instance, a null candidate byte array or a candidate array longer + * than the array to be scan won't allow to get a match. + * @param toBeScan + * @param candidate + * @return {@code false} in case the needed conditions for matching aren't satisfied. + */ + static boolean IsEmptyLocate(byte[] toBeScan, byte[] candidate) { + return (toBeScan == null) || (candidate == null) || (toBeScan.length == 0) + || (candidate.length == 0) || (candidate.length > toBeScan.length); + } + + /** + * Replace the EXIF contained within a file referred by a {@link FileImageInputStreamExt} instance + * with the EXIF represented by the specified {@link EXIFMetadata} instance. The original file + * will be overwritten by the new one containing updated EXIF. + * + * It is worth to point out that this replacing method won't currently perform any fields delete, + * but simply content update. Therefore, tags in the original EXIF which are missing in the + * updated EXIF parameter, won't be modified. + * + * @param inputStream a {@link FileImageInputStreamExt} referring to a JPEG containing EXIF + * @param exif the {@link EXIFMetadata} instance containing tags to be updated + */ + public static void replaceEXIFs( + final FileImageInputStreamExt inputStream, + final EXIFMetadata exif) + throws IOException { + + EXIFMetadataWrapper exifMarker = parseExifMetadata(inputStream, exif); + EXIFMetadata updatedExif = exifMarker.getExif(); + + final int app1Length = exifMarker.getLength(); + if (updatedExif != null){ + // Create a temp file where to store the updated EXIF + final File file = File.createTempFile("replacingExif", ".exif"); + final OutputStream fos = new FileOutputStream(file); + updateStream(fos, inputStream, updatedExif, app1Length); + final File previousFile = inputStream.getFile(); + FileUtils.deleteQuietly(previousFile); + FileUtils.moveFile(file, previousFile); + } + } + + /** + * Return an {@link EXIFMetadataWrapper} made of an {@link EXIFMetadata} setup on top of the + * EXIF found in the inputStream, merged with the EXIF found on the specified + * exif parameter (if any). The EXIF tags contained in the specified parameter will override + * the ones found within the inputStream. + * The returned wrapper will also contain the length of the APP1 marker of the original EXIF. + * + * @param inputStream a {@link FileImageInputStreamExt} referring to a JPEG containing EXIF + * @param exif the optional {@link EXIFMetadata} instance containing tags to be updated + * @return a {@link EXIFMetadataWrapper} containing the merged/updated EXIF as well as the + * length of the original APP1 marker + * @throws IOException + */ + private static EXIFMetadataWrapper parseExifMetadata( + final FileImageInputStreamExt inputStream, + final EXIFMetadata exif) throws IOException { + + List baselineTags = null; + List exifTags = null; + int app1Length = -1; + if (exif != null){ + // Get the updated Tags + baselineTags = exif.getList(Type.BASELINE); + exifTags = exif.getList(Type.EXIF); + } + + inputStream.mark(); + final Map foundBaseLineTags = new TreeMap(); + final Map foundExifTags = new TreeMap(); + + EXIFMetadata updatedExif = null; + final byte[] buff = new byte[EXIF_SCAN_BUFFER_SIZE]; + boolean contains_EXIF_IFD = false; + boolean found = false; + + // Scan the stream looking for exif tags + while ((inputStream.read(buff)) != -1) { + + // Look for the EXIF section (referred by the APP1 marker) + int exifTagPos = found ? -1 : locateFirst(buff, APP1_MARKER); + int pos = 0; + if (exifTagPos != -1) { + found = true; + pos = exifTagPos; + + // Get the original EXIF length + app1Length = bytes2ToInt(buff, pos + APP1_MARKER.length, true); + final boolean isBigEndian = buff[pos + APP1_MARKER.length + 2 + EXIF_MARKER.length] == 'M' + && buff[pos + APP1_MARKER.length + 2 + 1 + EXIF_MARKER.length] == 'M'; + + // Initialize number of tags + final int numBaselineTags = bytes2ToInt(buff, pos + APP1_MARKER.length + 2 + EXIF_MARKER.length + + TIFF_HEADER.length, isBigEndian); + int skip = 0; + int start = pos + APP1_MARKER.length + 2 + EXIF_MARKER.length; + int globalContentLength = 0; + + for (int i = 0; i < numBaselineTags; i++) { + skip = start + TIFF_HEADER.length + 2 + i * IFD_LENGTH; + + // Retrieve TIFF Tag information by scanning the buffer (Tag Number, type, count, offsetValue) + int number = bytes2ToInt(buff, skip, isBigEndian); + int type = bytes2ToInt(buff, skip + 2, isBigEndian); + int count = bytes4ToInt(buff, skip + 4, isBigEndian); + int offsetValue = bytes4ToInt(buff, skip + 8, isBigEndian); + Object content = null; + + // In case we find an IFDPointer during the scan, we need to take note of this + if (number == EXIFParentTIFFTagSet.TAG_EXIF_IFD_POINTER) { + contains_EXIF_IFD = true; + } + // Increase the globalContentLength when finding specific fields + if (type == TIFFTag.TIFF_ASCII || type == TIFFTag.TIFF_UNDEFINED) { + content = new byte[count]; + System.arraycopy(buff, offsetValue + start, content, 0, count); + globalContentLength += count; + } + + // Check whether the TAG found in the original EXIF is also contained + // in the EXIF to be updated + TIFFTagWrapper updatedTag = null; + for (TIFFTagWrapper tag : baselineTags) { + if (tag.getNumber() == number) { + updatedTag = tag; + break; + } + } + + // Update the Map of merged EXIFs + if (updatedTag == null) { + + // In case the list of EXIF to be merged doesn't contain this tag id, + // take the one from the original EXIF to preserve its value + updatedTag = new TIFFTagWrapper(number, type, content, offsetValue, + count, null, null); + } + foundBaseLineTags.put(number, updatedTag); + } + + // Handle the EXIF Specific tags + if (contains_EXIF_IFD) { + start = skip + globalContentLength + IFD_LENGTH + 4; // 4 due to 4 0x00 to start + // the IFD values + int numExifFields = bytes2ToInt(buff, start, isBigEndian); + + for (int i = 0; i < numExifFields; i++) { + skip = start + 2 + i * IFD_LENGTH; + + // Retrieve TIFF Tag information by scanning the buffer (Tag Number, type, count, offsetValue) + int number = bytes2ToInt(buff, skip, isBigEndian); + int type = bytes2ToInt(buff, skip + 2, isBigEndian); + int count = bytes4ToInt(buff, skip + 4, isBigEndian); + int offsetValue = bytes4ToInt(buff, skip + 8, isBigEndian); + Object content = null; + + // Increase the globalContentLength when finding specific fields + if (type == TIFFTag.TIFF_ASCII || type == TIFFTag.TIFF_UNDEFINED) { + content = new byte[count]; + System.arraycopy(buff, offsetValue + start, content, 0, count); + globalContentLength += count; + } + + // Check whether the TAG found in the original EXIF is also contained + // in the EXIF to be updated + TIFFTagWrapper updatedTag = null; + for (TIFFTagWrapper tag : exifTags) { + if (tag.getNumber() == number) { + updatedTag = tag; + break; + } + } + + // Update the Map of merged EXIFs + if (updatedTag == null) { + + // In case the list of EXIF to be merged doesn't contain this tag id, + // take the one from the original EXIF to preserve its value + updatedTag = new TIFFTagWrapper(number, type, content, offsetValue, + count, null, null); + } + foundExifTags.put(number, updatedTag); + } + } + + List mergedBaselineTags = null; + List mergedExifTags = null; + if (!foundBaseLineTags.isEmpty()) { + mergedBaselineTags = new ArrayList(foundBaseLineTags.values()); + + } + if (!foundExifTags.isEmpty()) { + mergedExifTags = new ArrayList(foundExifTags.values()); + } + + // Setup a new EXIF Metadata object containing all the EXIFs to be put + updatedExif = new EXIFMetadata(mergedBaselineTags, mergedExifTags); + } + } + inputStream.reset(); + return new EXIFMetadataWrapper(updatedExif, app1Length); + } + + /** + * @param tagNumber + * @return + */ + public static TIFFTagWrapper createTag(int tagNumber) { + switch (tagNumber){ + case EXIFTags.USER_COMMENT: + return new TIFFTagWrapper(EXIFTags.USER_COMMENT, TIFFTag.TIFF_UNDEFINED, null, -1, 0, + EXIFUtilities.USER_COMMENT_ASCII_CHAR_CODE, null); + case EXIFTags.COPYRIGHT: + return new TIFFTagWrapper(EXIFTags.COPYRIGHT, TIFFTag.TIFF_ASCII, null, 0, 0, + null, EXIFUtilities.NULL_STRING); + case EXIFTags.EXIF_IFD_POINTER: + return new TIFFTagWrapper(EXIFTags.EXIF_IFD_POINTER, TIFFTag.TIFF_LONG, null, -1, 1); + } + return null; + } +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/TIFFTagWrapper.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/TIFFTagWrapper.java new file mode 100644 index 000000000..5760b4ce6 --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/exif/TIFFTagWrapper.java @@ -0,0 +1,178 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.exif; + +import java.util.Arrays; + +/** + * @author Daniele Romagnoli, GeoSolutions SaS + * + * A class holding TIFF Tag properties like TAG ID (number), count, type, value/offset, content + * + * @see TIFF specification, page 15 + */ +public class TIFFTagWrapper{ + + /** the TAG ID number */ + private int number; + + /** the number of values */ + private int count; + + /** the TIFF field type */ + private int type; + + /** the value/offset */ + private int value; + + /** + * specific byte[] suffix and prefix for specific tag to be used to fully represent a field. + * + * As an instance, each ascii value should be terminated by a null byte. + * In that case, the user will set a content made of bytes representing the text and + * the suffix will be a byte[]{0}. + * + * Another example: the UserComment allows to put a comment into exif. The user should + * simply specify the bytes representing the comment content. The prefix will be specified + * as requested by the specification. + * See {@link EXIFUtilities#USER_COMMENT_ASCII_CHAR_CODE} + * @see + * UserComment character code prefix + */ + private byte[] suffix; + private byte[] prefix; + + /** + * An object storing the Field value (currently as a byte[]), setup by the user representing the + * raw content to be set without any prefix/suffix additional bytes requested by the specifications. + */ + private Object content; + + + @Override + public String toString() { + return "TIFFTagWrapper [content=" + content + ", value=" + value + ", count=" + count + + ", prefix=" + Arrays.toString(prefix) + ", suffix=" + Arrays.toString(suffix) + + ", type=" + type + ", number=" + number + "]"; + } + + public TIFFTagWrapper(final int tagNumber, final int type, final String content, final int value, final int count) { + this(tagNumber, type, content, value, count, null, null); + } + + /** + * A fully specified {@link TIFFTagWrapper} constructor. + * + * @param tagNumber the ID of the underlying TIFF Tag + * @param type the type of the TIFF Field (BYTE, ASCII, SHORT, LONG, ...) + * @param content the content (currently, a byte[]) to be set for that field + * (without any prefix/suffix). + * @param valueOffset the value of this + * @param count the number of values. (this value is ignored in case of not null content/prefix/suffix since + * it will be computed on top of these byte arrays) + * @param prefix an optional byte[] to be inserted before the specified content to fully represent the field + * (as an instance, the UserComment content need to be prefixed by a 8 byte array representing the character code) + * @see + * UserComment character code prefix + * @param suffix an optional byte[] to be appended after the specified content to fully represent the field + * (as an instance, a byte[]{0} null char to be appended to an ASCII content) + */ + public TIFFTagWrapper( + final int tagNumber, + final int type, + final Object content, + final int valueOffset, + final int count, + final byte[] prefix, + final byte[] suffix) { + this.count = count; + this.number = tagNumber; + this.type = type; + this.prefix = prefix != null ? prefix.clone() : null; + this.suffix = suffix != null ? suffix.clone() : null; + + if (content != null) { + this.content = content; + this.count = (content instanceof byte[]) ? ((byte[]) content).length : 0 ; + if (suffix != null){ + this.count += suffix.length; + } + if (prefix != null){ + this.count += prefix.length; + } + } + this.value = valueOffset; + + } + + public Object getContent() { + return content; + } + + public void setContent(Object content) { + this.content = content; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public byte[] getPrefix() { + return prefix; + } + + public void setPrefix(byte[] prefix) { + this.prefix = prefix; + } + + public byte[] getSuffix() { + return suffix; + } + + public void setSuffix(byte[] suffix) { + this.suffix = suffix; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + +} \ No newline at end of file diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageReader.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageReader.java new file mode 100644 index 000000000..6b349936e --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageReader.java @@ -0,0 +1,277 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2012, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +import org.libjpegturbo.turbojpeg.TJ; +import org.libjpegturbo.turbojpeg.TJDecompressor; + +/** + * @author Emanuele Tajariol, GeoSolutions SaS + * @author Daniele Romagnoli, GeoSolutions SaS An {@link ImageReader} for JPEG decompression using the TurboJPEG library. It can accept (as setInput + * method) both an ImageInputStream as any other {@link ImageReader}, as well as byte[] object. The last one is useful when the reader is + * initialized by a TiffImageReader having internally JPEG compressed tiles. It can pass down the byte[] array instead of doing any copy, + * repeated read of a buffer (Which should be faster) + */ +public class TurboJpegImageReader extends ImageReader { + + private static final Logger LOGGER = Logger + .getLogger("it.geosolutions.imageio.plugins.turbojpeg"); + + private int INITIAL_JPG_BUFFER_SIZE = 1 * 1024 * 1024; + + private static final int DEFAULT_READ_BUFFER_SIZE = 1024 * 4; + + private int width = -1; + + private int height = -1; + + private byte[] data = null; + + private ImageInputStream iis = null; + + private static int EXTERNAL_FLAGS = -1; + + private int flags = TJ.FLAG_FASTUPSAMPLE; + + // the type of subsampling of the compressed data. (GRAY, 4:2:0, ...) + private int subsamp; + + static { + String flagsProp = System.getProperty(TurboJpegUtilities.FLAGS_PROPERTY); + if (flagsProp != null) { + String[] tjFlags = flagsProp.split(","); + EXTERNAL_FLAGS = 0; + for (String tjFlag : tjFlags) { + EXTERNAL_FLAGS |= TurboJpegUtilities.getTurboJpegFlag(tjFlag); + } + } + } + + public TurboJpegImageReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + return 1; + } + + @Override + public int getWidth(int imageIndex) throws IOException { + checkIndex(imageIndex); + return width; + } + + @Override + public int getHeight(int imageIndex) throws IOException { + checkIndex(imageIndex); + return height; + } + + private static final List FIXEDIMGETYPES = Collections + .unmodifiableList(Arrays.asList( + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))); + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + checkIndex(imageIndex); + + // uhm, cannot get the pixelFormat from the decompressor + return FIXEDIMGETYPES.iterator(); + } + + @Override + public IIOMetadata getStreamMetadata() throws IOException { + return null; + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + return null; + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + checkIndex(imageIndex); + if (data == null) { + throw new IllegalArgumentException("Missing data array"); + } + + BufferedImage bi = new BufferedImage(width, height, + subsamp == TJ.SAMP_GRAY ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_3BYTE_BGR); + + // Using local variables to avoid changing the internal state + TJDecompressor decompressor = null; + try { + decompressor = new TJDecompressor(data, data.length); + decompressor.decompress(bi, flags); + } catch (Exception e) { + throw new IOException("Exception while decompressing:", e); + } finally { + if (decompressor != null) { + try { + decompressor.close(); + decompressor = null; + } catch (Exception ex) { + // Eat exception. There is nothing else we can do + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Exception occurred while closing the decompressor: " + + ex.getLocalizedMessage()); + } + } + } + } + return bi; + } + + @Override + public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { + + // Preliminar reset + reset(); + this.ignoreMetadata = ignoreMetadata; + + TJDecompressor decompressor = null; + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("setting input"); + } + + if (input instanceof ImageInputStream) { + iis = (ImageInputStream) input; // Always works + } + + /* + * Read the whole JPG. there's no exposed API to read the header only the decompressor requires the whole data to be in a byte array. + * Therefore we need to load the byte array data in case it's not already available as input. + */ + try { + if (input instanceof byte[]) { + data = (byte[]) input; + } else { + if (iis == null) { + throw new NullPointerException("The provided input is null!"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(INITIAL_JPG_BUFFER_SIZE); + byte[] buffer = new byte[DEFAULT_READ_BUFFER_SIZE]; + + int n = 0; + + while (-1 != (n = iis.read(buffer))) { + baos.write(buffer, 0, n); + } + data = baos.toByteArray(); + } + flags = EXTERNAL_FLAGS > 0 ? EXTERNAL_FLAGS : flags; + decompressor = new TJDecompressor(); + decompressor.setJPEGImage(data, data.length); + width = decompressor.getWidth(); + height = decompressor.getHeight(); + subsamp = decompressor.getSubsamp(); + + } catch (Exception ex) { + throw new RuntimeException("Error creating jpegturbo decompressor: " + ex.getMessage(), + ex); + } finally { + + if (decompressor != null) { + try { + decompressor.close(); + decompressor = null; + } catch (Exception ex) { + // Eat exception. There is nothing else we can do + } + } + } + super.setInput(input, seekForwardOnly, ignoreMetadata); + // CHECKME: should we mark the position? + } + + public void reset() { + super.setInput(null, false, false); + dispose(); + } + + private void checkIndex(int imageIndex) { + if (imageIndex != 0) { + throw new IndexOutOfBoundsException("ImageIndex out of bound: " + imageIndex); + } + } + + @Override + public void dispose() { + if (data != null) { + data = null; + } + } + + protected static String toString(ImageReadParam param) { + StringBuilder sb = new StringBuilder(); + if (param == null) { + sb.append("ImageReadParam is null"); + } else { + sb.append(param.getClass().getSimpleName()).append('['); + sb.append(" srcXsub:").append(param.getSourceXSubsampling()); + sb.append(" srcYsub:").append(param.getSourceYSubsampling()); + sb.append(" subXoff:").append(param.getSubsamplingXOffset()); + sb.append(" subYoff:").append(param.getSubsamplingYOffset()); + sb.append(" srcRegion:").append(param.getSourceRegion()); + sb.append(" destOff:").append(param.getDestinationOffset()); + + ImageTypeSpecifier its = param.getDestinationType(); + if(its != null) { + sb.append(" its:").append(its.getSampleModel()); + if(its.getSampleModel() != null ) { + sb.append(" its.sm.datatype:").append(its.getSampleModel().getDataType()); + sb.append(" its.sm.numbands:").append(its.getSampleModel().getNumBands()); + } + } + + BufferedImage bi = param.getDestination(); + if(bi != null) { + sb.append(" bi.sm:").append(bi.getSampleModel()); + if(bi.getSampleModel() != null) { + sb.append(" bi.sm.datatype:").append(bi.getSampleModel().getDataType()); + sb.append(" bi.sm.numbands:").append(bi.getSampleModel().getNumBands()); + } + } + + sb.append(']'); + } + return sb.toString(); + } +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageReaderSpi.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageReaderSpi.java new file mode 100644 index 000000000..424ed4452 --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageReaderSpi.java @@ -0,0 +1,249 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2012, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ServiceRegistry; +import javax.imageio.stream.ImageInputStream; + +import com.sun.media.imageioimpl.common.PackageUtil; + +/** + * + * @author Emanuele Tajariol, GeoSolutions SaS + * @author Daniele Romagnoli, GeoSolutions SaS + */ +public class TurboJpegImageReaderSpi extends ImageReaderSpi { + + static { + // Initialization to make sure the native library is available + // before instantiating any reader + TurboJpegUtilities.loadTurboJpeg(); + } + + /** The LOGGER for this class. */ + private static final Logger LOGGER = Logger + .getLogger("it.geosolutions.imageio.plugins.turbojpeg"); + + // Adding a byte[] supported class. This allows the reader to receive bytes from a tiff reader + // which may have internally JPEG compressed tiles + public static final Class[] SUPPORTED_CLASSES = { ImageInputStream.class, byte[].class }; + + public static final String[] names = { "jpeg", "JPEG", "jpg", "JPG", "jfif", "JFIF", + "jpeg-lossless", "JPEG-LOSSLESS", "jpeg-ls", "JPEG-LS" }; + + private static final String[] suffixes = {"jpeg", "jpg", "jfif", "jls"}; + + private static final String[] MIMETypes = {"image/jpeg"}; + + private static final String readerClassName = + "it.geosolutions.imageio.plugins.turbojpeg.TurboJpegImageReader"; + + private static final String[] writerSpiNames = { + "it.geosolutions.imageio.plugins.turbojpeg.TurboJpegImageWriterSpi" + }; + + static final String nativeImageMetadataFormatName = null; + + static final String nativeImageMetadataFormatClassName = null; + + static final String[] extraImageMetadataFormatNames = { null }; + + static final String[] extraImageMetadataFormatClassNames = { null }; + + private boolean registered = false; + + public TurboJpegImageReaderSpi() { + super(PackageUtil.getVendor(), + PackageUtil.getVersion(), + names, + suffixes, + MIMETypes, + readerClassName, + SUPPORTED_CLASSES, + writerSpiNames, + false, // supportsStandardStreamMetadataFormat + null, // nativeStreamMetadataFormatName + null, // nativeStreamMetadataFormatClassName + null, // extraStreamMetadataFormatNames + null, // extraStreamMetadataFormatClassNames + true, // supportsStandardImageMetadataFormat + nativeImageMetadataFormatName, + nativeImageMetadataFormatClassName, + extraImageMetadataFormatNames, + extraImageMetadataFormatClassNames); + } + + public void onRegistration(ServiceRegistry registry, Class category) { + super.onRegistration(registry, category); + if (registered) { + return; + } + registered = true; + + boolean turboJpegavailable = TurboJpegUtilities.isTurboJpegAvailable(); + + if (turboJpegavailable) { + deregisterOtherSPIs(registry, category); + } else { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Deregistering " + this.getClass().getName()); + } + registry.deregisterServiceProvider(this); + } + } + + private static Method readerFormatNamesMethod; + static { + try { + readerFormatNamesMethod = ImageReaderSpi.class.getMethod("getFormatNames"); + } catch (NoSuchMethodException e) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(e.getLocalizedMessage()); + } + } + } + + private void deregisterOtherSPIs(ServiceRegistry registry, Class category) { + + if (registry == null) { + return; + } + + try { + Iterator iter = registry.getServiceProviders(ImageReaderSpi.class, + new ContainsFilter(readerFormatNamesMethod, "JPEG"), true); + + for (; iter.hasNext();) { + ImageReaderSpi spi = iter.next(); + if (!spi.getClass().equals(this.getClass())) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Deprioritizing " + spi); + } + + // ETj: we used to deregister the other ones, but they may be + // explicitely requested +// registry.deregisterServiceProvider(spi); + registry.setOrdering(category, this, spi); + } + } + } catch (IllegalArgumentException e) { + } + } + + static class ContainsFilter implements ServiceRegistry.Filter { + + Method method; + + String name; + + // method returns an array of Strings + public ContainsFilter(Method method, String name) { + this.method = method; + this.name = name; + } + + public boolean filter(Object elt) { + try { + return contains((String[]) method.invoke(elt), name); + } catch (Exception e) { + return false; + } + } + + private static boolean contains(String[] names, String name) { + for (int i = 0; i < names.length; i++) { + if (name.equalsIgnoreCase(names[i])) { + return true; + } + } + return false; + } + } + + @Override + public void onDeregistration(ServiceRegistry registry, Class category) { + super.onDeregistration(registry, category); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(getClass().getSimpleName() + " being deregistered"); + } + } + + public String getDescription(Locale locale) { + return "SPI for JPEG ImageReader based on TurboJPEG"; + } + + public boolean canDecodeInput(Object source) throws IOException { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("canDecodeInput"); + } + + if (!(source instanceof ImageInputStream)) { + return false; + } + ImageInputStream iis = (ImageInputStream) source; + iis.mark(); + // If the first two bytes are a JPEG SOI marker, it's probably + // a JPEG file. If they aren't, it definitely isn't a JPEG file. + int byte1 = iis.read(); + int byte2 = iis.read(); + if ((byte1 != 0xFF) || (byte2 != 0xD8)) { + iis.reset(); + return false; + } + do { + byte1 = iis.read(); + byte2 = iis.read(); + if (byte1 != 0xFF) + break; // something wrong, but probably readable + if (byte2 == 0xDA) + break; // Start of scan + if (byte2 == 0xC2) { // progressive mode, can't decode + iis.reset(); + return false; + } + if ((byte2 >= 0xC0) && (byte2 <= 0xC3)) // not progressive, can decode + break; + int length = iis.read() << 8; + length += iis.read(); + length -= 2; + while (length > 0) + length -= iis.skipBytes(length); + } while (true); + iis.reset(); + + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("canDecodeInput -> True"); + } + + return true; + } + + public ImageReader createReaderInstance(Object extension) throws IIOException { + return new TurboJpegImageReader(this); + } +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriteParam.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriteParam.java new file mode 100644 index 000000000..cdedd8d68 --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriteParam.java @@ -0,0 +1,100 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import java.util.Locale; + +import it.geosolutions.imageio.plugins.exif.EXIFMetadata; + +import javax.imageio.ImageWriteParam; +import org.libjpegturbo.turbojpeg.TJ; + +/** + * Class holding Write parameters to customize the write operations + * + * @author Daniele Romagnoli, GeoSolutions SaS + * @author Emanuele Tajariol, GeoSolutions SaS + */ +public class TurboJpegImageWriteParam extends ImageWriteParam { + + public TurboJpegImageWriteParam() { + this(Locale.getDefault()); + } + + public TurboJpegImageWriteParam(Locale locale) { + super(locale); + // fix compression type + this.compressionTypes = new String[] { DEFAULT_COMPRESSION_SCHEME }; + + } + + public final static String DEFAULT_COMPRESSION_SCHEME = "JPEG"; + + public final static float DEFAULT_COMPRESSION_QUALITY = 0.75f; + + public final static int DEFAULT_RGB_COMPONENT_SUBSAMPLING = TJ.SAMP_420; + + private int componentSubsampling = -1; + + private EXIFMetadata exif; + + @Override + public boolean canWriteCompressed() { + return true; + } + + @Override + public boolean canWriteTiles() { + return false; + } + + public EXIFMetadata getExif() { + return exif; + } + + public void setExif(EXIFMetadata exif) { + this.exif = exif; + } + + /** + * @param componentSubsampling the componentSubsampling to set. + * It represents the Chrominance subsampling factor applied by the turbojpeg library. Supported values are: + * + *
    + *
  • {@linkplain TurboJpegLibrary#TJ_444} : 4:4:4 chrominance subsampling (no chrominance subsampling).
    + * The JPEG or YUV image will contain one chrominance component for every pixel in the source image.
  • + *
  • {@linkplain TurboJpegLibrary#TJ_422} : 4:2:2 chrominance subsampling.
    + * The JPEG or YUV image will contain one chrominance component for every 2x1 block of pixels in the source image.
  • + *
  • {@linkplain TurboJpegLibrary#TJ_420} : 4:2:0 chrominance subsampling.
    + * The JPEG or YUV image will contain one chrominance component for every 2x2 block of pixels in the source image..
  • + *
  • {@linkplain TurboJpegLibrary#TJ_GRAYSCALE} : Grayscale.
    + * The JPEG or YUV image will contain no chrominance components
  • + *
+ * + */ + public void setComponentSubsampling(int componentSubsampling) { + this.componentSubsampling = componentSubsampling; + } + + /** + * @return the componentSubsampling + */ + public int getComponentSubsampling() { + return componentSubsampling; + } + +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriter.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriter.java new file mode 100644 index 000000000..db03c29dd --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriter.java @@ -0,0 +1,287 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2012, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import it.geosolutions.imageio.plugins.exif.EXIFMetadata; +import it.geosolutions.imageio.plugins.exif.EXIFUtilities; +import it.geosolutions.imageio.utilities.ImageOutputStreamAdapter2; + +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBufferByte; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.IIOImage; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageOutputStream; +import javax.media.jai.ImageLayout; +import javax.media.jai.JAI; +import javax.media.jai.RenderedOp; + +import com.sun.media.jai.opimage.CopyOpImage; +import org.libjpegturbo.turbojpeg.TJ; +import org.libjpegturbo.turbojpeg.TJCompressor; + + +/** + * @author Daniele Romagnoli, GeoSolutions SaS + * @author Simone Giannecchini, GeoSolutions SaS + * @author Emanuele Tajariol, GeoSolutions SaS + */ +public class TurboJpegImageWriter extends ImageWriter +{ + + /** The LOGGER for this class. */ + private static final Logger LOGGER = Logger.getLogger("it.geosolutions.imageio.plugins.turbojpeg"); + + private ImageOutputStream outputStream = null; + + public TurboJpegImageWriter(ImageWriterSpi originatingProvider) + { + super(originatingProvider); + } + + /** + * Get a default {@link ImageWriteParam} instance. + */ + @Override + public ImageWriteParam getDefaultWriteParam() + { + TurboJpegImageWriteParam wparam = new TurboJpegImageWriteParam(); + wparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + wparam.setCompressionType(TurboJpegImageWriteParam.DEFAULT_COMPRESSION_SCHEME); + wparam.setCompressionQuality(TurboJpegImageWriteParam.DEFAULT_COMPRESSION_QUALITY); + return wparam; + } + + @Override + public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, + ImageWriteParam param) + { + throw new UnsupportedOperationException(); + } + + @Override + public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) + { + throw new UnsupportedOperationException(); + } + + @Override + public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) + { + return null; + } + + @Override + public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) + { + return null; + } + + /** + * Sets the destination to the given Object. + * For this TurboJPEG specific implementation, it needs to be + * an instance of {@link ImageOutputStreamAdapter2}. + * + * @param output + * the Object to use for future writing. + */ + public void setOutput(Object output) + { + if (output instanceof OutputStream) { + outputStream = new ImageOutputStreamAdapter2((OutputStream) output); + } else if (output instanceof ImageOutputStreamAdapter2) { + outputStream = (ImageOutputStreamAdapter2) output; + } else if (output instanceof File){ + try { + outputStream = new ImageOutputStreamAdapter2(new FileOutputStream((File) output)); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + super.setOutput(output); + } + + @Override + public void write(IIOMetadata metadata, IIOImage image, ImageWriteParam writeParam) throws IOException + { + + // Getting image properties + RenderedImage srcImage = image.getRenderedImage(); + srcImage = refineImage(srcImage); + + final ComponentSampleModel sm = (ComponentSampleModel) srcImage.getSampleModel(); + int[] bandOffsets = sm.getBandOffsets(); + + // Getting image Write params + TurboJpegImageWriteParam param = (TurboJpegImageWriteParam) writeParam; + // use default as needed + if (param == null) { + param = (TurboJpegImageWriteParam) getDefaultWriteParam(); + } + final EXIFMetadata exif = param.getExif(); + int componentSampling = param.getComponentSubsampling(); + final int quality = (int) (param.getCompressionQuality() * 100); + + int pf = TJ.PF_RGB; + if (bandOffsets.length == 3) + { + if (componentSampling == -1) { + componentSampling = TurboJpegImageWriteParam.DEFAULT_RGB_COMPONENT_SUBSAMPLING; + } + if ((bandOffsets[0] == 2) && (bandOffsets[2] == 0)) + { + pf = TJ.PF_BGR; + } + } + else if (bandOffsets.length == 1 && componentSampling == -1) + { + pf = TJ.PF_GRAY; + componentSampling = TJ.SAMP_GRAY; + } + else + { + throw new IllegalArgumentException("TurboJPEG won't work with this type of sampleModel"); + } + + if (componentSampling < 0) + { + throw new IOException("Subsampling level not set"); + } + + final int pixelsize = sm.getPixelStride(); + final int width = srcImage.getWidth(); + final int height = srcImage.getHeight(); + final int pitch = pixelsize * width; + + TJCompressor compressor = null; + try + { +// final long jsize = TurboJpegUtilities.bufSize(width, height); + + Rectangle rect = new Rectangle(srcImage.getMinX(), srcImage.getMinY(), srcImage.getWidth(), srcImage.getHeight()); + Raster data = srcImage.getData(rect); + final byte[] inputImageData = ((DataBufferByte) data.getDataBuffer()).getData(); + + final byte[] outputImageData; + try { + compressor = new TJCompressor(); + compressor.setSourceImage(inputImageData, width, pitch, height, pf); + compressor.setJPEGQuality(quality); + compressor.setSubsamp(componentSampling); + + outputImageData = compressor.compress(TJ.FLAG_FASTDCT); + } catch (Exception ex) { + throw new IOException("Error in turbojpeg comressor: " + ex.getMessage(), ex); + } + + final int imageDataSize = compressor.getCompressedSize(); + + if (exif != null) + { + EXIFUtilities.insertEXIFintoStream( + ((ImageOutputStreamAdapter2) outputStream).getOs(), outputImageData, imageDataSize, exif); + } + else + { + outputStream.write(outputImageData, 0, imageDataSize); + } + } + + finally + { + if(compressor != null) { + try + { + compressor.close(); + } + catch (Exception t) + { + LOGGER.log(Level.SEVERE, t.getLocalizedMessage(), t); + } + } + } + } + + /** + * Performs a few check in order to make sure to provide the proper data bytes to the + * incoming encoding phase. When calling getData(Rectangle).getDataBuffer() on an image having size + * smaller than the tile size, the underlying data buffer will be made of the data contained in the + * full tile (Even having used the getData(Rectangle) call. As an instance, a Rectangle(0,0,64,64) + * extracted from a 64x64 image with tiling 128x128 will result into a ByteBuffer filled with + * 128x128xBands bytes. (Therefore a lot of zeros). The encoded image will have a lot of scattered + * black stripes. + * This method do a copy of the only needed part of data when such a condition is met, or return the + * original image otherwise. + * + * @param srcImage The source image to be refined. + * @return + */ + private RenderedImage refineImage(RenderedImage srcImage) + { + final int w = srcImage.getWidth(); + final int h = srcImage.getHeight(); + final int minX = srcImage.getMinX(); + final int minY = srcImage.getMinY(); + final int tw = srcImage.getTileWidth(); + final int th = srcImage.getTileHeight(); + if ((tw > w) || (th > h)) + { + RenderingHints hints = null; + ImageLayout layout = null; + if (srcImage instanceof RenderedOp) + { + hints = ((RenderedOp) srcImage).getRenderingHints(); + if ((hints != null) && hints.containsKey(JAI.KEY_IMAGE_LAYOUT)) + { + layout = (ImageLayout) hints.get(JAI.KEY_IMAGE_LAYOUT); + } + else + { + layout = new ImageLayout(srcImage); + } + } + else + { + layout = new ImageLayout(srcImage); + hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout); + } + layout.setTileHeight(h); + layout.setTileWidth(w); + layout.setTileGridXOffset(minX); + layout.setTileGridYOffset(minY); + srcImage = new CopyOpImage(srcImage, hints, layout); + } + + return srcImage; + } + +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriterSpi.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriterSpi.java new file mode 100644 index 000000000..1f76c0eee --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegImageWriterSpi.java @@ -0,0 +1,137 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import it.geosolutions.imageio.utilities.ImageOutputStreamAdapter2; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.Locale; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriter; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.spi.ServiceRegistry; + +/** + * @author Daniele Romagnoli, GeoSolutions SaS + */ +public class TurboJpegImageWriterSpi extends ImageWriterSpi { + + static { + // Initialization to make sure the native library is available + // before instantiating any writer + TurboJpegUtilities.loadTurboJpeg(); + + } + + static final String[] suffixes = { "JPEG", "JPG", "jpeg", "jpg" }; + + static final String[] formatNames = { "jpeg", "jpg" }; + + static final String[] MIMETypes = { "image/jpeg" }; + + static final String version = "1.0"; + + static final String writerCN = "it.geosolutions.imageio.plugins.turbojpeg.TurboJpegImageWriter"; + + static final String vendorName = "GeoSolutions"; + + // ReaderSpiNames + static final String[] readerSpiName = { "it.geosolutions.imageio.plugins.turbojpeg.TurboJpegImageReaderSpi" }; + + // StreamMetadataFormatNames and StreamMetadataFormatClassNames + static final boolean supportsStandardStreamMetadataFormat = false; + + static final String nativeStreamMetadataFormatName = null; + + static final String nativeStreamMetadataFormatClassName = null; + + static final String[] extraStreamMetadataFormatNames = null; + + static final String[] extraStreamMetadataFormatClassNames = null; + + // ImageMetadataFormatNames and ImageMetadataFormatClassNames + static final boolean supportsStandardImageMetadataFormat = false; + + static final String nativeImageMetadataFormatName = null; + + static final String nativeImageMetadataFormatClassName = null; + + static final String[] extraImageMetadataFormatNames = { null }; + + static final String[] extraImageMetadataFormatClassNames = { null }; + + /** + * Default {@link ImageWriterSpi} constructor for JP2K writers. + */ + public TurboJpegImageWriterSpi() { + super(vendorName, version, formatNames, suffixes, MIMETypes, writerCN, + new Class[]{ImageOutputStreamAdapter2.class, OutputStream.class, File.class}, readerSpiName, + supportsStandardStreamMetadataFormat, + nativeStreamMetadataFormatName, + nativeStreamMetadataFormatClassName, + extraStreamMetadataFormatNames, + extraStreamMetadataFormatClassNames, + supportsStandardImageMetadataFormat, + nativeImageMetadataFormatName, + nativeImageMetadataFormatClassName, + extraImageMetadataFormatNames, + extraImageMetadataFormatClassNames); + } + + /** + * @see javax.imageio.spi.ImageWriterSpi#createWriterInstance(java.lang.Object) + */ + public ImageWriter createWriterInstance(Object extension) throws IOException { + return new TurboJpegImageWriter(this); + } + + /** + * @see javax.imageio.spi.IIOServiceProvider#getDescription(java.util.Locale) + */ + public String getDescription(Locale locale) { + return "SPI for JPEG ImageWriter based on TurboJPEG"; + } + + /** + * TODO: Refine the check before releasing. + */ + public boolean canEncodeImage(ImageTypeSpecifier type) { + return true; + } + + @Override + public void onRegistration(ServiceRegistry registry, Class category) { + super.onRegistration(registry, category); + if (!TurboJpegUtilities.isTurboJpegAvailable()) { + IIORegistry iioRegistry = (IIORegistry) registry; + final Class spiClass = ImageWriterSpi.class; + final Iterator iter = iioRegistry.getServiceProviders(spiClass,true); + while (iter.hasNext()) { + final ImageWriterSpi provider = (ImageWriterSpi) iter.next(); + if (provider instanceof TurboJpegImageWriterSpi) { + registry.deregisterServiceProvider(provider); + } + } + } + } + +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegUtilities.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegUtilities.java new file mode 100644 index 000000000..189f3e172 --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/plugins/turbojpeg/TurboJpegUtilities.java @@ -0,0 +1,133 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2012, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.libjpegturbo.turbojpeg.TJ; + +/** + * @author Daniele Romagnoli, GeoSolutions SaS + * @author Emanuele Tajariol, GeoSolutions SaS + * + * Class containing some methods ported from the TurboJPEG C code as well as lib availability check. + * + */ +public class TurboJpegUtilities { + + private static final Logger LOGGER = Logger.getLogger(TurboJpegUtilities.class.getName()); + + public static final String FLAGS_PROPERTY = "it.geosolutions.imageio.plugins.turbojpeg.flags"; + + private static boolean isAvailable; + + private static boolean isInitialized = false; + + public static boolean isTurboJpegAvailable() { + + loadTurboJpeg(); + return isAvailable; + } + + public static void loadTurboJpeg() { + if (isInitialized) { + return; + } + synchronized (LOGGER) { + if (isInitialized) { + return; + } + try { + load(); + isAvailable = true; + + } catch (Throwable t) { + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.warning("Failed to load the TurboJpeg native libs." + + " This is not a problem, but the TurboJpeg encoder won't be available: " + t.toString()); + } + } finally { + isInitialized = true; + } + } + + } + + public static final String LIBNAME = "turbojpeg"; + + static void load() { + try { + System.loadLibrary(LIBNAME); // If this method is called more than once with the same library name, the second and subsequent calls are ignored. + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.info("TurboJPEG library loaded ("+LIBNAME+")"); + } + } catch (java.lang.UnsatisfiedLinkError e) { + String os = System.getProperty("os.name").toLowerCase(); + if (os.indexOf("mac") >= 0) { + System.load("/usr/lib/libturbojpeg.jnilib"); + } else { + throw e; + } + } + } + + public static int getTurboJpegFlag(final String key) { + if (key != null) { + if (key.equalsIgnoreCase("FLAG_ACCURATEDCT")) { + return TJ.FLAG_ACCURATEDCT; + } else if (key.equalsIgnoreCase("FLAG_BOTTOMUP")) { + return TJ.FLAG_BOTTOMUP; + } else if (key.equalsIgnoreCase("FLAG_FASTDCT")) { + return TJ.FLAG_FASTDCT; + } else if (key.equalsIgnoreCase("FLAG_FASTUPSAMPLE")) { + return TJ.FLAG_FASTUPSAMPLE; + } else if (key.equalsIgnoreCase("FLAG_FORCEMMX")) { + return TJ.FLAG_FORCEMMX; + } else if (key.equalsIgnoreCase("FLAG_FORCESSE")) { + return TJ.FLAG_FORCESSE; + } else if (key.equalsIgnoreCase("FLAG_FORCESSE2")) { + return TJ.FLAG_FORCESSE2; + } else if (key.equalsIgnoreCase("FLAG_FORCESSE3")) { + return TJ.FLAG_FORCESSE3; + } + } + throw new IllegalArgumentException("Unsupported flag"); + } + + public static String getTurboJpegFlagAsString(final int key) { + switch (key) { + case TJ.FLAG_ACCURATEDCT: + return "FLAG_ACCURATEDCT"; + case TJ.FLAG_BOTTOMUP: + return "FLAG_BOTTOMUP"; + case TJ.FLAG_FASTDCT: + return "FLAG_FASTDCT"; + case TJ.FLAG_FASTUPSAMPLE: + return "FLAG_FASTUPSAMPLE"; + case TJ.FLAG_FORCEMMX: + return "FLAG_FORCEMMX"; + case TJ.FLAG_FORCESSE: + return "FLAG_FORCESSE"; + case TJ.FLAG_FORCESSE2: + return "FLAG_FORCESSE2"; + case TJ.FLAG_FORCESSE3: + return "FLAG_FORCESSE3"; + } + throw new IllegalArgumentException("Unsupported flag"); + } +} diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/utilities/ImageOutputStreamAdapter2.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/utilities/ImageOutputStreamAdapter2.java new file mode 100644 index 000000000..403abb8ee --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/utilities/ImageOutputStreamAdapter2.java @@ -0,0 +1,86 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.utilities; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.imageio.stream.ImageOutputStreamImpl; + +/** + * @author Simone Giannecchini, GeoSolutions + */ +public class ImageOutputStreamAdapter2 extends ImageOutputStreamImpl { + + // Supporting marking is a big issue. I should overline this somehow + + private OutputStream os; + + public OutputStream getOs() { + return os; + } + + public ImageOutputStreamAdapter2(OutputStream os) { + this.os = os; + } + + /** + * @see javax.imageio.stream.ImageOutputStreamImpl#write(int) + */ + public void write(int b) throws IOException { + os.write(b); + } + + /** + * @see javax.imageio.stream.ImageOutputStreamImpl#write(byte[], int, int) + */ + public void write(byte[] b, int off, int len) throws IOException { + os.write(b, off, len); + } + + /** + * @see javax.imageio.stream.ImageInputStreamImpl#read() + */ + public int read() throws IOException { + throw new UnsupportedOperationException("Operation not supported."); + } + + /** + * @see javax.imageio.stream.ImageInputStreamImpl#read(byte[], int, int) + */ + public int read(byte[] b, int off, int len) throws IOException { + throw new UnsupportedOperationException("Operation not supported."); + } + + /** + * @see javax.imageio.stream.ImageInputStreamImpl#flush() + */ + public void flush() throws IOException { + os.flush(); + } + + /** + * @see javax.imageio.stream.ImageInputStreamImpl#close() + */ + public void close() throws IOException { + try { + super.close(); + } finally { + os.close(); + } + } +} \ No newline at end of file diff --git a/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/utilities/TilesByteGetter.java b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/utilities/TilesByteGetter.java new file mode 100644 index 000000000..1b9899bed --- /dev/null +++ b/plugin/turbojpeg/src/main/java/it/geosolutions/imageio/utilities/TilesByteGetter.java @@ -0,0 +1,197 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.utilities; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.media.jai.JAI; +import javax.media.jai.PlanarImage; +import javax.media.jai.TileScheduler; + +/** + * @author Daniele Romagnoli, GeoSolutions SaS + */ +public class TilesByteGetter { + + byte[] bb; + + RenderedImage ri; + + int tileWidth; + + int tileHeight; + + int width; + + int height; + + final static boolean usePrefetching; + + static int multithreadingLevel; + + static { + String mt = System.getProperty("it.geosolutions.tilesgetter.multithreading"); + if (mt != null) { + try { + multithreadingLevel = Integer.parseInt(mt); + } catch (NumberFormatException nfe){ + System.out.println("Error parsing " + mt + " as integer; using default 1"); + multithreadingLevel = 1; + } + } else { + multithreadingLevel = 1; + } + usePrefetching = Boolean.getBoolean("it.geosolutions.tilesgetter.usePrefetching"); + System.out.println("Multithreading level: " + multithreadingLevel + " prefetching: " + usePrefetching); + } + + + public TilesByteGetter(RenderedImage ri){ + this.ri = ri; + tileWidth = ri.getTileWidth(); + tileHeight = ri.getTileHeight(); + width = ri.getWidth(); + height = ri.getHeight(); + } + + private class ByteGetter implements Callable { + + RenderedImage ri; + + int tileX; + + int tileY; + + public ByteGetter(RenderedImage ri, final int tileX, final int tileY) { + this.ri = ri; + this.tileX = tileX; + this.tileY = tileY; + } + + public Integer call() { + try { + Raster tile = ri.getTile(tileX, tileY); + final int nBands = ri.getSampleModel().getNumBands(); + DataBufferByte dbb = (DataBufferByte) tile.getDataBuffer(); + + byte[] bytes = dbb.getData(); + final int h; + final int w; + if ((tileWidth * (tileX + 1) > width)) { + w = width - (tileX * tileWidth); + } else { + w = tileWidth; + } + if ((tileHeight * (tileY + 1) > height)) { + h = height - (tileY * tileHeight); + } else { + h = tileHeight; + } + + final int localStripeLength = w * nBands; + final int stripeLength = tileWidth * nBands; + int tileSkipX = stripeLength * tileX; + + int offset; + for (int j=0; j < h; j++) { + offset = (((j + tileHeight * tileY) * width * nBands) + tileSkipX); + System.arraycopy(bytes, stripeLength * j, bb, offset, localStripeLength); + } + + + + } catch (Exception e){ + + } + return 1; + + } +} + + public byte[] getBytes() throws InterruptedException{ + if (ri instanceof BufferedImage){ + Raster wr = ri.getTile(0, 0); + return ((DataBufferByte) wr.getDataBuffer()).getData(); + } else { + final int nX = ri.getNumXTiles(); + final int nY = ri.getNumYTiles(); + if (nX == 1 && nY == 1) { + Raster wr = ri.getTile(0, 0); + return ((DataBufferByte) wr.getDataBuffer()).getData(); + } else { + final int size = ri.getHeight() * ri.getWidth() * ri.getSampleModel().getNumBands(); + bb = new byte[size]; + if (multithreadingLevel != 1) { + final int minTx = ri.getMinTileX(); + final int minTy = ri.getMinTileY(); + int TH = multithreadingLevel; + final TileScheduler ts = JAI.getDefaultInstance().getTileScheduler(); + final List tiles = new ArrayList(); + final LinkedBlockingQueue queue = new LinkedBlockingQueue(); + final List> queueX = new ArrayList>(); + final ThreadPoolExecutor ex = new ThreadPoolExecutor(TH, TH, 10000L, TimeUnit.SECONDS, queue); + ex.prestartAllCoreThreads(); + int tx = 0; + int ty = 0; + for (int j = 0; j < nY; j++) { + for (int i = 0; i < nX; i++) { + tx = minTx + i; + ty = minTy + j; + tiles.add(new Point( tx, ty)); + queueX.add(new ByteGetter(ri, tx, ty)); + } + } + Point[] tilesArray = (Point[]) tiles.toArray(new Point[tiles.size()]); + ts.prefetchTiles(PlanarImage.wrapRenderedImage(ri), tilesArray); + ex.invokeAll(queueX); + ex.shutdown(); + } else { + if (usePrefetching){ + final int minTx = ri.getMinTileX(); + final int minTy = ri.getMinTileY(); + TileScheduler ts = JAI.getDefaultInstance().getTileScheduler(); + List tiles = new ArrayList(); + int tx = 0; + int ty = 0; + for (int j = 0; j < nY; j++) { + for (int i = 0; i < nX; i++) { + tx = minTx + i; + ty = minTy + j; + tiles.add(new Point( tx, ty)); + } + } + Point[] tilesArray = (Point[]) tiles.toArray(new Point[tiles.size()]); + ts.prefetchTiles(PlanarImage.wrapRenderedImage(ri), tilesArray); + } + bb = ((DataBufferByte)ri.getData().getDataBuffer()).getData(); + + } + return bb; + } + } + } +} diff --git a/plugin/turbojpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/plugin/turbojpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100644 index 000000000..bddf07ea4 --- /dev/null +++ b/plugin/turbojpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1 @@ +it.geosolutions.imageio.plugins.turbojpeg.TurboJpegImageReaderSpi \ No newline at end of file diff --git a/plugin/turbojpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi b/plugin/turbojpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi new file mode 100644 index 000000000..1d06a82a6 --- /dev/null +++ b/plugin/turbojpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi @@ -0,0 +1 @@ +it.geosolutions.imageio.plugins.turbojpeg.TurboJpegImageWriterSpi \ No newline at end of file diff --git a/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/BaseTest.java b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/BaseTest.java new file mode 100644 index 000000000..5eb3cf942 --- /dev/null +++ b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/BaseTest.java @@ -0,0 +1,42 @@ +package it.geosolutions.imageio.plugins.turbojpeg; + +import com.sun.imageio.plugins.jpeg.JPEGImageWriterSpi; +import com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi; +import java.awt.image.RenderedImage; +import java.io.File; +import java.util.logging.Logger; +import org.junit.Before; + +/** + * + * @author etajario + */ +public abstract class BaseTest { + + static final String INPUT_FILE_PATH = "/tmp/test.tif"; + + static final File INPUT_FILE = new File(INPUT_FILE_PATH); + + static RenderedImage SAMPLE_IMAGE = null; + + static boolean SKIP_TESTS = false; + + static final String ERROR_LIB_MESSAGE = "The TurboJpeg native library hasn't been loaded: Skipping test"; + + static final String ERROR_FILE_MESSAGE = "The specified input file can't be read: Skipping test"; + + static final String OUTPUT_FOLDER = "/tmp" // /media/bigdisk/data/turbojpeg"// System.getProperty("java.io.tmpdir") + + File.separatorChar; + + static final CLibJPEGImageWriterSpi clibSPI = new CLibJPEGImageWriterSpi(); + + static final JPEGImageWriterSpi standardSPI = new JPEGImageWriterSpi(); + + static final TurboJpegImageWriterSpi turboSPI = new TurboJpegImageWriterSpi(); + + + static { + SKIP_TESTS = !TurboJpegUtilities.isTurboJpegAvailable(); + } + +} diff --git a/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGMultiThTest.java b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGMultiThTest.java new file mode 100644 index 000000000..1df7788e6 --- /dev/null +++ b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGMultiThTest.java @@ -0,0 +1,142 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import it.geosolutions.imageio.utilities.ImageOutputStreamAdapter2; + +import java.awt.image.RenderedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.imageio.IIOImage; +import javax.imageio.ImageWriteParam; + +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assume.*; + +/** + * @author Daniele Romagnoli, GeoSolutions SAS + * + */ +public class JPEGMultiThTest extends BaseTest { + + private static final Logger LOGGER = Logger.getLogger(JPEGMultiThTest.class.toString()); + + private static ImageWriteParam param = new TurboJpegImageWriteParam(); + + /** + * TODO JUNIT tests + * + * @param args + * @throws IOException + */ + @Test + @Ignore + public void multithreadedTest() throws IOException { + + assumeTrue(!SKIP_TESTS); + + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.75f); + + ThreadPoolExecutor ex = null; + try { + final int TH = 20; + LinkedBlockingQueue queueX = new LinkedBlockingQueue(); + ex = new ThreadPoolExecutor(TH, TH, 10000L, TimeUnit.SECONDS, queueX); + ex.prestartAllCoreThreads(); + + final int LOOP = 1000; + List> queue = new ArrayList>(LOOP); + for (int i = 0; i < LOOP; i++) { + Callable a = new WritingTest(SAMPLE_IMAGE, OUTPUT_FOLDER + "___" + i + + "___" + Math.random() + ".jpeg"); + queue.add(a); + } + ex.invokeAll(queue); + ex.shutdown(); + + } catch (InterruptedException e) { + e.printStackTrace(); + // } catch (ExecutionException e) { + // e.printStackTrace(); + } finally { + if (ex != null) { + ex.shutdown(); + } + } + + return; + } + + private class WritingTest implements Callable { + + RenderedImage bi; + + String file; + + public WritingTest(RenderedImage bi, final String outputFile) { + this.bi = bi; + file = outputFile; + + } + + public String call() { + ImageOutputStreamAdapter2 out1 = null; + TurboJpegImageWriter writer1 = null; + try { + FileOutputStream fos = new FileOutputStream(new File(file)); + System.out.println("writing on " + file); + + out1 = new ImageOutputStreamAdapter2(fos); + writer1 = (TurboJpegImageWriter) turboSPI.createWriterInstance(); + writer1.setOutput(out1); + writer1.write(null, new IIOImage(bi, null, null), param); + return file; + } catch (Exception e) { + LOGGER.severe(e.getLocalizedMessage()); + + return "ERROR"; + } finally { + if (writer1 != null){ + try { + writer1.dispose(); + } catch (Throwable t){ + + } + } + if (out1 != null){ + try { + out1.close(); + } catch (Throwable t){ + + } + } + } + } + } + +} diff --git a/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGReaderTest.java b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGReaderTest.java new file mode 100644 index 000000000..a6f44aa6d --- /dev/null +++ b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGReaderTest.java @@ -0,0 +1,119 @@ +package it.geosolutions.imageio.plugins.turbojpeg; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; +import it.geosolutions.imageio.utilities.ImageIOUtilities; +import it.geosolutions.resources.TestData; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.logging.Logger; + +import javax.imageio.ImageReader; +import javax.imageio.stream.FileImageInputStream; + +import org.junit.Test; + +public class JPEGReaderTest { + + private static final Logger LOGGER = Logger.getLogger(JPEGReaderTest.class.toString()); + + private static final String FILENAME = "test.jpg"; + + private static final String FILENAMEGRAY = "testgray.jpg"; + + @Test + public void readManual() throws Exception { + final File file = TestData.file(this, FILENAME); + final File fileGray = TestData.file(this, FILENAMEGRAY); + if (!TurboJpegUtilities.isTurboJpegAvailable()) { + LOGGER.warning("Unable to find native libs. Tests are skipped"); + assumeTrue(false); + return; + } + + final ImageReader reader = new TurboJpegImageReaderSpi().createReaderInstance(); + + FileImageInputStream fis = null; + BufferedImage image = null; + + // // + // Read RGB image + // // + try { + fis = new FileImageInputStream(file); + reader.setInput(fis); + image = reader.read(0, null); + assertEquals(227, image.getWidth()); + assertEquals(103, image.getHeight()); + assertEquals(3, image.getSampleModel().getNumBands()); + + if (TestData.isInteractiveTest()) { + ImageIOUtilities.visualize(image, "testManualRead"); + Thread.sleep(1000); + } else { + assertNotNull(image.getData()); + } + + image.flush(); + image = null; + } finally { + if (reader != null) { + try { + reader.dispose(); + } catch (Throwable t) { + // Does nothing + } + } + + if (fis != null) { + try { + fis.close(); + } catch (Throwable t) { + // Does nothing + } + } + } + + // // + // Read GRAY image + // // + try { + fis = new FileImageInputStream(fileGray); + + reader.setInput(fis); + image = reader.read(0, null); + assertEquals(227, image.getWidth()); + assertEquals(103, image.getHeight()); + assertEquals(1, image.getSampleModel().getNumBands()); + + if (TestData.isInteractiveTest()) { + ImageIOUtilities.visualize(image, "testManualRead"); + Thread.sleep(1000); + } else { + assertNotNull(image.getData()); + } + + image.flush(); + image = null; + + } finally { + if (reader != null) { + try { + reader.dispose(); + } catch (Throwable t) { + // Does nothing + } + } + + if (fis != null) { + try { + fis.close(); + } catch (Throwable t) { + // Does nothing + } + } + } + } +} diff --git a/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGTurboSPITest.java b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGTurboSPITest.java new file mode 100644 index 000000000..072fdc524 --- /dev/null +++ b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGTurboSPITest.java @@ -0,0 +1,94 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2012, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; + +import org.junit.Test; + +/** + * @author Simone Giannecchini, GeoSolutions SAS + * @author Emanuele Tajariol, GeoSolutions SAS + */ +public class JPEGTurboSPITest { + + + @Test + public void testWriterSPI() { + final Iterator it = ImageIO.getImageWritersByFormatName(TurboJpegImageWriterSpi.formatNames[0]); + + // at least one writer should exist + assertTrue(it.hasNext()); + + // if turbojpeg has not been loaded, ignore further test clause + assumeTrue(TurboJpegUtilities.isTurboJpegAvailable()); + + boolean existTurbo = false; + boolean existAnother = false; + + while (it.hasNext()) { + if (it.next().getOriginatingProvider() instanceof TurboJpegImageWriterSpi) { + existTurbo = true; + } else { + existAnother = true; + } + } + + assertTrue("Unable to find TurboJpegImageWriterSpi", existTurbo); + assertTrue("Unable to find another jpeg ImageWriter", existAnother); + } + + @Test + public void testReaderSPI() { + final Iterator it = ImageIO.getImageReadersByFormatName(TurboJpegImageReaderSpi.names[0]); + + // at least one reader should exist + assertTrue("Unable to find any jpeg ImageReader", it.hasNext()); + + // if turbojpeg has not been loaded, ignore further test clause + assumeTrue(TurboJpegUtilities.isTurboJpegAvailable()); + + boolean existTurbo = false; + boolean existAnother = false; + + while (it.hasNext()) { + if (it.next().getOriginatingProvider() instanceof TurboJpegImageReaderSpi) { + existTurbo = true; + } else { + existAnother = true; + } + } + + assertTrue("Unable to find TurboJpegImageReaderSpi", existTurbo); + assertTrue("Unable to find another jpeg ImageReader", existAnother); + + + // Need to investigate on the CLIB Jpeg reader SPIs which seem are registered as first +// // first one should be the turbojpeg +// assertTrue("First reader SPI is not the turbo one", it.next().getOriginatingProvider() instanceof TurboJpegImageReaderSpi); +// +// // at least another reader should exist +// assertTrue("Unable to find another jpeg ImageReader", it.hasNext()); + } +} diff --git a/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterCompareTest.java b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterCompareTest.java new file mode 100644 index 000000000..fc463967c --- /dev/null +++ b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterCompareTest.java @@ -0,0 +1,310 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import it.geosolutions.imageio.utilities.ImageOutputStreamAdapter2; +import it.geosolutions.resources.TestData; + +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import javax.imageio.IIOImage; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.plugins.jpeg.JPEGImageWriteParam; +import javax.imageio.spi.ImageOutputStreamSpi; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.FileImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; +import javax.media.jai.operator.ExtremaDescriptor; +import javax.media.jai.operator.SubtractDescriptor; + +import org.junit.Test; +import static org.junit.Assume.*; + +import com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi; +import com.sun.imageio.plugins.png.PNGImageReaderSpi; +import com.sun.media.imageioimpl.common.PackageUtil; +import java.util.logging.Logger; + +public class JPEGWriterCompareTest extends BaseTest { + + private static final Logger LOGGER = Logger.getLogger(JPEGWriterCompareTest.class.toString()); + + static final int LOOP = 30; + + static final long DELAY_MS = 15000; + + static final boolean CODEC_LIB_AVAILABLE = PackageUtil.isCodecLibAvailable(); + + static void dispose(ByteArrayOutputStream out, FileOutputStream fos) { + + if (out != null) { + try { + out.close(); + } catch (Throwable t) { + + } + } + if (fos != null) { + try { + fos.flush(); + fos.close(); + fos = null; + } catch (Throwable t) { + + } + } + } + + public static java.io.ByteArrayOutputStream encodeImageAsJpeg(RenderedImage image, + final float quality, final boolean useNative) throws Exception { + // sanity check, the two writers will emit very odd messages in this case, let's + // have a human readable one instead + if (image.getColorModel().hasAlpha()) { + throw new Exception("Can't write images with alpha band in JPEG " + + "format, please use alpha=false with jpeg output"); + } + + java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream(); + // use efficient native jai writer + writeJPEG(image, output, "JPEG", quality, useNative); + + return output; + + } + + private static int write(final int loop, final long delayMs, final BufferedImage buffered, + final boolean useNative) throws Exception { + int differences = 0; + for (int i = 0; i < loop; i++) { + ByteArrayOutputStream out1 = encodeImageAsJpeg(buffered, 0.75f, useNative); + Thread.sleep(delayMs); + + ByteArrayOutputStream out2 = encodeImageAsJpeg(buffered, 0.75f, useNative); + final File file1 = new File(OUTPUT_FOLDER + "outA" + i + ".jpg"); + file1.delete(); + + final File file2 = new File(OUTPUT_FOLDER + "outB" + i + ".jpg"); + file2.delete(); + + FileOutputStream fos1 = new FileOutputStream(file1); + FileOutputStream fos2 = new FileOutputStream(file2); + out1.writeTo(fos1); + out2.writeTo(fos2); + dispose(out2, fos2); + dispose(out1, fos1); + + ImageReaderSpi spi = new JPEGImageReaderSpi(); + ImageReader reader1 = spi.createReaderInstance(); + ImageReader reader2 = spi.createReaderInstance(); + FileImageInputStream fis1 = new FileImageInputStream(file1); + reader1.setInput(fis1); + + FileImageInputStream fis2 = new FileImageInputStream(file2); + reader2.setInput(fis2); + + BufferedImage bi1 = reader1.read(0); + BufferedImage bi2 = reader2.read(0); + if (!imagesAreEquals(bi1, bi2)) { + differences++; + } + fis1.close(); + fis2.close(); + file1.delete(); + file2.delete(); + } + + return differences; + } + + private static boolean imagesAreEquals(BufferedImage bi1, BufferedImage bi2) { + RenderedImage subtractA = SubtractDescriptor.create(bi1, bi2, null); + double[][] extremaA = (double[][]) ExtremaDescriptor.create(subtractA, null, 1, 1, false, + 1, null).getProperty("Extrema"); + System.out.println("A - B"); + + return extremaIsZero(extremaA); + } + + private static boolean extremaIsZero(double[][] extrema) { + System.out.println("extrema values: MIN[R,G,B]; MAX[R,G,B] = [" + extrema[0][0] + "," + + extrema[0][1] + "," + extrema[0][2] + "] ; [" + extrema[1][0] + "," + + extrema[1][1] + "," + extrema[1][2] + "]"); + + if (isZero(extrema[0][0]) && isZero(extrema[0][1]) && isZero(extrema[0][2]) + && isZero(extrema[1][0]) && isZero(extrema[1][1]) && isZero(extrema[1][2])) { + return true; + } + + return false; + } + + private static boolean isZero(double d) { + return (Math.abs(d - 0) < 1E-9d); + } + + /** + * Writes outs the image contained into this {@link ImageWorker} as a JPEG using the provided + * destination , compression and compression rate. + *

+ * The destination object can be anything providing that we have an {@link ImageOutputStreamSpi} + * that recognizes it. + * + * @param destination + * where to write the internal {@link #image} as a JPEG. + * @param compression + * algorithm. + * @param compressionRate + * percentage of compression. + * @param nativeAcc + * should we use native acceleration. + * @return this {@link ImageWorker}. + * @throws IOException + * In case an error occurs during the search for an {@link ImageOutputStream} or + * during the eoncding process. + */ + public static final void writeJPEG(final RenderedImage image, final Object destination, + final String compression, final float compressionRate, final boolean nativeAcc) + throws IOException { + ImageWriterSpi spi = nativeAcc ? clibSPI : turboSPI; + ImageWriter writer = spi.createWriterInstance(); + + // Compression is available on both lib + final ImageWriteParam iwp = writer.getDefaultWriteParam(); + + final ImageOutputStream outStream = nativeAcc ? new MemoryCacheImageOutputStream( + (OutputStream) destination) : new ImageOutputStreamAdapter2((OutputStream) destination); + + iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + iwp.setCompressionType("JPEG"); + iwp.setCompressionQuality(compressionRate); // We can control quality here. + if (nativeAcc) { + iwp.setCompressionType(compression); // Lossy compression. + + } + if (iwp instanceof JPEGImageWriteParam) { + final JPEGImageWriteParam param = (JPEGImageWriteParam) iwp; + param.setOptimizeHuffmanTables(true); + try { + param.setProgressiveMode(JPEGImageWriteParam.MODE_DEFAULT); + } catch (UnsupportedOperationException e) { + throw (IOException) new IOException().initCause(e); + // TODO: inline cause when we will be allowed to target Java 6. + } + } + + try { + + writer.setOutput(outStream); + writer.write(null, new IIOImage(image, null, null), iwp); + + } finally { + if (writer != null) { + try { + writer.dispose(); + } catch (Throwable e) { + System.out.println(e.getLocalizedMessage()); + + } + } + if (outStream != null) { + try { + ((ImageOutputStream) outStream).close(); + } catch (Throwable e) { + System.out.println(e.getLocalizedMessage()); + } + } + + } + } + + @Test + public void writeAsJpeg() throws Exception { + if (!TestData.isExtensiveTest()){ + LOGGER.info("Skipping compare tests. Use Extensive tests to enable it"); + return; + } + + if (SKIP_TESTS){ + LOGGER.warning(ERROR_LIB_MESSAGE); + assumeTrue(!SKIP_TESTS); + return; + } + + ImageReaderSpi spiReader = new PNGImageReaderSpi(); + ImageReader reader = null; + File inputFile = TestData.file(this, "testmergb.png"); + FileImageInputStream fis = null; + + try { + reader = spiReader.createReaderInstance(); + fis = new FileImageInputStream(inputFile); + reader.setInput(fis); + + BufferedImage buffered = reader.read(0); + writeAsJpeg(buffered); + } catch (Throwable th) { + if (fis != null) { + try { + fis.close(); + } catch (Throwable t) { + + } + } + + if (reader != null) { + try { + reader.dispose(); + } catch (Throwable t) { + + } + } + th.printStackTrace(); + } + + // System.in.read(); + } + + private void writeAsJpeg(final BufferedImage buffered) throws Exception { + + int differencesNative = 0; + if (CODEC_LIB_AVAILABLE) { + System.out.println("----------------------------\nTESTING NATIVE WRITER\n----------------------------\n"); + differencesNative = write(LOOP, DELAY_MS, buffered, true); + } else { + System.out.println("----------------------------\nSKIPPING NATIVE WRITER\n----------------------------\n"); + } + + System.out.println("----------------------------\nTESTING Not-Native WRITER\n----------------------------\n"); + + int differencesNoNative = write(LOOP, DELAY_MS, buffered, false); + + System.out.println(" Doing "+ LOOP + " couples of writes resulted in \n" + + (CODEC_LIB_AVAILABLE ? (differencesNative + " difference on outputImage between 2 consecutive writes using the CLIB writer and \n") : "") + + differencesNoNative + " difference on outputImage between 2 consecutive writes using the Turbo writer"); + + } +} diff --git a/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterSpeedTest.java b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterSpeedTest.java new file mode 100644 index 000000000..1c21e5438 --- /dev/null +++ b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterSpeedTest.java @@ -0,0 +1,251 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import it.geosolutions.imageio.utilities.ImageOutputStreamAdapter2; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.IIOImage; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; + +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assume.*; + +public class JPEGWriterSpeedTest extends BaseTest { + + private static final Logger LOGGER = Logger.getLogger(JPEGWriterSpeedTest.class.toString()); + + private static final int LOOP = 20; + + static { + if (SAMPLE_IMAGE != null){ + LOGGER.info("If enabled, tests are made of " + LOOP + " iterations on top of a " + + SAMPLE_IMAGE.getWidth() + "*" + SAMPLE_IMAGE.getHeight() + " (" + + SAMPLE_IMAGE.getSampleModel().getNumBands() + " bands) image"); + } + } + + @Test + @Ignore + public void testJPEGCLIB() throws FileNotFoundException, IOException, SecurityException, + NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + + assumeTrue(!SKIP_TESTS); + + String fileName = null; + ImageOutputStream out1 = null; + + try { + + ImageWriterSpi spi = clibSPI; + fileName = OUTPUT_FOLDER + + ((SAMPLE_IMAGE.getSampleModel().getNumBands() == 1) ? "GRAY" : "RGB") + + "CLIBoutput.jpeg"; + final File file = new File(fileName); + out1 = new FileImageOutputStream(file); + + ImageWriter writer1 = spi.createWriterInstance(); + ImageWriteParam param = writer1.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.75f); + + writer1.setOutput(out1); + writer1.write(null, new IIOImage(SAMPLE_IMAGE, null, null), param); + out1.close(); + writer1.dispose(); + + // Writing loops + long start = System.nanoTime(); + for (int i = 0; i < LOOP; i++) { + // Startup write + out1 = new FileImageOutputStream(file); + writer1 = spi.createWriterInstance(); + writer1.setOutput(out1); + writer1.write(null, new IIOImage(SAMPLE_IMAGE, null, null), param); + out1.close(); + writer1.dispose(); + } + + long end = System.nanoTime(); + long total = end - start; + reportTime("Clib", total, LOOP); + + } catch (Throwable t) { + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.warning(t.getLocalizedMessage()); + } + } finally { + if (out1 != null) { + try { + out1.close(); + } catch (Throwable t) { + // + } + } + } + } + + @Test + @Ignore + public void testJPEGJDK() throws FileNotFoundException, IOException, SecurityException, + NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + + assumeTrue(!SKIP_TESTS); + + String fileName = null; + ImageOutputStream out1 = null; + try { + + ImageWriterSpi spi = standardSPI; + fileName = OUTPUT_FOLDER + + ((SAMPLE_IMAGE.getSampleModel().getNumBands() == 1) ? "GRAY" : "jdkRGB") + + "METAoutput.jpeg"; + final File file = new File(fileName); + out1 = new FileImageOutputStream(file); + + ImageWriter writer1 = spi.createWriterInstance(); + ImageWriteParam param = writer1.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.75f); + writer1.setOutput(out1); + writer1.write(null, new IIOImage(SAMPLE_IMAGE, null, null), param); + out1.close(); + writer1.dispose(); + + // Writing loops + long start = System.nanoTime(); + for (int i = 0; i < LOOP; i++) { + // Startup write + out1 = new FileImageOutputStream(file); + writer1 = spi.createWriterInstance(); + writer1.setOutput(out1); + writer1.write(null, new IIOImage(SAMPLE_IMAGE, null, null), param); + out1.close(); + writer1.dispose(); + } + + long end = System.nanoTime(); + long total = end - start; + reportTime("JDK", total, LOOP); + } catch (Throwable t) { + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.warning(t.getLocalizedMessage()); + } + } finally { + if (out1 != null) { + try { + out1.close(); + } catch (Throwable t) { + // + } + } + } + } + + @Test + @Ignore + public void testJPEGTurbo() throws FileNotFoundException, IOException, SecurityException, + NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + + assumeTrue(!SKIP_TESTS); + + if (!TurboJpegUtilities.isTurboJpegAvailable()) { + LOGGER.warning(ERROR_LIB_MESSAGE); + return; + } + + ImageWriterSpi spi = turboSPI; + String fileName = null; + OutputStream os = null; + ImageOutputStream out1 = null; + + TurboJpegImageWriteParam param = new TurboJpegImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.75f); +// EXIFMetadata exif = initExif(); +// param.setExif(exif); + + try { + + fileName = OUTPUT_FOLDER + + ((SAMPLE_IMAGE.getSampleModel().getNumBands() == 1) ? "GRAY" + : "RGBTurbo") + INPUT_FILE.getName() + ".jpeg"; + final File file = new File(fileName); + os = new FileOutputStream(file); + out1 = new ImageOutputStreamAdapter2(os); + + + TurboJpegImageWriter writer1 = (TurboJpegImageWriter) spi.createWriterInstance(); + writer1.setOutput(out1); + writer1.write(null, new IIOImage(SAMPLE_IMAGE, null, null), param); + out1.close(); + writer1.dispose(); + + // Writing loops + long start = System.nanoTime(); + for (int i = 0; i < LOOP; i++) { + // Startup write + os = new FileOutputStream(file); + out1 = new ImageOutputStreamAdapter2(os); + writer1 = (TurboJpegImageWriter) spi.createWriterInstance(); + writer1.setOutput(out1); + writer1.write(null, new IIOImage(SAMPLE_IMAGE, null, null), param); + out1.close(); + writer1.dispose(); + } + + long end = System.nanoTime(); + long total = end - start; + reportTime("Turbo", total, LOOP); + + } catch (Throwable t) { + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.warning(t.getLocalizedMessage()); + } + } finally { + if (out1 != null) { + try { + out1.close(); + } catch (Throwable t) { + // + } + } + } + } + + /** + * @param total + */ + protected static void reportTime(String encoder, long total, final int LOOP) { + LOGGER.info("JPEG " + encoder + " TOTAL TIME = " + ((total) / 1000000) + + "(ms) ; AVERAGE TIME = " + (total / (1000 * LOOP)) + "(micros)"); + + } + +} diff --git a/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterTest.java b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterTest.java new file mode 100644 index 000000000..f98398f31 --- /dev/null +++ b/plugin/turbojpeg/src/test/java/it/geosolutions/imageio/plugins/turbojpeg/JPEGWriterTest.java @@ -0,0 +1,270 @@ +/* + * ImageI/O-Ext - OpenSource Java Image translation Library + * http://www.geo-solutions.it/ + * http://java.net/projects/imageio-ext/ + * (C) 2007 - 2011, GeoSolutions + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * either version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.imageio.plugins.turbojpeg; + +import it.geosolutions.imageio.plugins.exif.EXIFMetadata; +import it.geosolutions.imageio.plugins.exif.EXIFTags; +import it.geosolutions.imageio.plugins.exif.EXIFTags.Type; +import it.geosolutions.imageio.plugins.exif.EXIFUtilities; +import it.geosolutions.imageio.plugins.exif.TIFFTagWrapper; +import it.geosolutions.imageio.stream.input.FileImageInputStreamExt; +import it.geosolutions.imageio.stream.input.FileImageInputStreamExtImpl; +import it.geosolutions.resources.TestData; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Logger; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.spi.ImageWriterSpi; + + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assume.*; +import static org.junit.Assert.*; + +import org.libjpegturbo.turbojpeg.TJ; + +public class JPEGWriterTest extends BaseTest { + + private static final Logger LOGGER = Logger.getLogger(JPEGWriterTest.class.toString()); + +// @Before +// public void setup() { +// SKIP_TESTS = !TurboJpegUtilities.isTurboJpegAvailable(); +// } + +// static { +// try { +// JAI.getDefaultInstance().getTileCache().setMemoryCapacity(128 * 1024 * 1024); +// if (!(INPUT_FILE.exists() && INPUT_FILE.canRead())) { +// SKIP_TESTS = true; +// LOGGER.warning(ERROR_FILE_MESSAGE); +// } else { +// FileImageInputStream fis = new FileImageInputStream(INPUT_FILE); +// ImageReader reader = ImageIO.getImageReaders(fis).next(); +// reader.setInput(fis); +// SAMPLE_IMAGE = ImageReadDescriptor.create(fis, 0, false, false, false, null, null, +// null, reader, null); +// } +// +// } catch (IOException e) { +// if (LOGGER.isLoggable(Level.SEVERE)) { +// LOGGER.severe(e.getLocalizedMessage()); +// } +// } +// } + + + public static EXIFMetadata getDefaultInstance() { + List baselineTiffTags = new ArrayList(2); + List exifTags = new ArrayList(1); + + // Make sure to set them in proper order + TIFFTagWrapper copyrightTag = EXIFUtilities.createTag(EXIFTags.COPYRIGHT); + TIFFTagWrapper exifPointerTag = EXIFUtilities.createTag(EXIFTags.EXIF_IFD_POINTER); + + baselineTiffTags.add(copyrightTag); + baselineTiffTags.add(exifPointerTag); + + TIFFTagWrapper userCommentTag = EXIFUtilities.createTag(EXIFTags.USER_COMMENT); + exifTags.add(userCommentTag); + + EXIFMetadata exif = new EXIFMetadata(baselineTiffTags, exifTags); + + return exif; + } + + /** + * @return + */ + protected EXIFMetadata initExif() { + EXIFMetadata exif = getDefaultInstance(); + exif.setTag(EXIFTags.USER_COMMENT, "Sample User Comment 2".getBytes(), Type.EXIF); + exif.setTag(EXIFTags.COPYRIGHT, "Copyright 2011 DigitalGlobe".getBytes(), Type.BASELINE); + return exif; + } + + + + @Test + @Ignore + public void testExifReplace() throws IOException { + EXIFMetadata exif = initExif(); + FileImageInputStreamExt inStream = new FileImageInputStreamExtImpl(new File( + "/media/bigdisk/data/turbojpeg/lastExif.jpeg")); + EXIFUtilities.replaceEXIFs(inStream, exif); + } + + @Test + public void basicWriterTest() throws IOException{ + if (SKIP_TESTS){ + LOGGER.warning(ERROR_LIB_MESSAGE); + assumeTrue(!SKIP_TESTS); + return; + } + + //test-data + final File input = TestData.file(this, "testmergb.png"); + assertTrue("Unable to find test data", input.exists() && input.isFile() && input.canRead()); + + // get the SPI for writer\ + final Iterator it = ImageIO + .getImageWritersByFormatName(TurboJpegImageWriterSpi.formatNames[0]); + assertTrue(it.hasNext()); + TurboJpegImageWriter writer = null; + while (it.hasNext()) { + ImageWriterSpi writer_ = it.next().getOriginatingProvider(); + if (writer_ instanceof TurboJpegImageWriterSpi) { + writer = (TurboJpegImageWriter) writer_.createWriterInstance(); + break; + } + } + assertNotNull("Unable to find TurboJpegImageWriter", writer); + + // create output file + final File output = TestData.temp(this, "output.jpeg", false); + writer.setOutput(output); + writer.write(ImageIO.read(input)); + LOGGER.warning("Writing output to " + output); + assertTrue("Unable to create output file", output.exists() && output.isFile()); + + } + + @Test + public void writerTest() throws IOException { + if (SKIP_TESTS){ + LOGGER.warning(ERROR_LIB_MESSAGE); + assumeTrue(!SKIP_TESTS); + return; + } + + // test-data + final File input = TestData.file(this, "testmergb.png"); + assertTrue("Unable to find test data", input.exists() && input.isFile() && input.canRead()); + + // get the SPI for writer\ + final Iterator it = ImageIO + .getImageWritersByFormatName(TurboJpegImageWriterSpi.formatNames[0]); + assertTrue(it.hasNext()); + TurboJpegImageWriter writer = null; + while (it.hasNext()) { + ImageWriterSpi writer_ = it.next().getOriginatingProvider(); + if (writer_ instanceof TurboJpegImageWriterSpi) { + writer = (TurboJpegImageWriter) writer_.createWriterInstance(); + break; + } + } + assertNotNull("Unable to find TurboJpegImageWriter", writer); + + // create write param + ImageWriteParam wParam_ = writer.getDefaultWriteParam(); + assertTrue(wParam_ instanceof TurboJpegImageWriteParam); + + TurboJpegImageWriteParam wParam = (TurboJpegImageWriteParam) wParam_; + wParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + try { + wParam.setCompressionType(""); + assertTrue("We should not be allowed to set an empty compression type", false); + } catch (Exception e) { + // TODO: handle exception + } + try { + wParam.setCompressionType("aaa"); + assertTrue("We should not be allowed to set a generic compression type", false); + } catch (Exception e) { + // TODO: handle exception + } + wParam.setCompressionType("JPEG"); + wParam.setCompressionQuality(.75f); + + // create output file + final File output = TestData.temp(this, "output.jpeg", true); + writer.setOutput(output); + writer.write(null, new IIOImage(ImageIO.read(input), null, null), wParam); + assertTrue("Unable to create output file", output.exists() && output.isFile()); + + } + + @Test + public void writerTestComponentsSubsampling() throws IOException { + if (SKIP_TESTS){ + LOGGER.warning(ERROR_LIB_MESSAGE); + assumeTrue(!SKIP_TESTS); + return; + } + + final long[] lengths = new long[3]; + final int[] componentSubsampling = new int[]{TJ.SAMP_444,TJ.SAMP_422, TJ.SAMP_420}; + // test-data + final File input = TestData.file(this, "testmergb.png"); + assertTrue("Unable to find test data", input.exists() && input.isFile() && input.canRead()); + + + // get the SPI for writer\ + final Iterator it = ImageIO + .getImageWritersByFormatName(TurboJpegImageWriterSpi.formatNames[0]); + assertTrue(it.hasNext()); + TurboJpegImageWriter writer = null; + while (it.hasNext()) { + ImageWriterSpi writer_ = it.next().getOriginatingProvider(); + if (writer_ instanceof TurboJpegImageWriterSpi) { + writer = (TurboJpegImageWriter) writer_.createWriterInstance(); + break; + } + } + assertNotNull("Unable to find TurboJpegImageWriter", writer); + + IIOImage image = new IIOImage(ImageIO.read(input), null, null); + + for (int i = 0; i < 3; i++) { + // create write param + ImageWriteParam wParam_ = writer.getDefaultWriteParam(); + assertTrue(wParam_ instanceof TurboJpegImageWriteParam); + + TurboJpegImageWriteParam wParam = (TurboJpegImageWriteParam) wParam_; + wParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + wParam.setCompressionType("JPEG"); + wParam.setCompressionQuality(.75f); + wParam.setComponentSubsampling(componentSubsampling[i]); + + // create output file + final File output = TestData.temp(this, "output.jpeg", false); + LOGGER.warning("output file is " + output); + writer.setOutput(output); + writer.write(null, image, wParam); + writer.dispose(); + + assertTrue("Unable to create output file", output.exists() && output.isFile()); + lengths[i] = output.length(); +// output.delete(); + } + + assertEquals(lengths[0], 11604); + assertEquals(lengths[1], 9376); + assertEquals(lengths[2], 8209); + } + +} diff --git a/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/test.jpg b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/test.jpg new file mode 100644 index 000000000..0ac9a0592 Binary files /dev/null and b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/test.jpg differ diff --git a/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testgray.jpg b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testgray.jpg new file mode 100644 index 000000000..a700cdaaa Binary files /dev/null and b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testgray.jpg differ diff --git a/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testme.jpg b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testme.jpg new file mode 100644 index 000000000..0ac9a0592 Binary files /dev/null and b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testme.jpg differ diff --git a/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testmergb.png b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testmergb.png new file mode 100644 index 000000000..c05d7d961 Binary files /dev/null and b/plugin/turbojpeg/src/test/resources/it/geosolutions/imageio/plugins/turbojpeg/test-data/testmergb.png differ diff --git a/pom.xml b/pom.xml index e0a64eb38..c342405f3 100644 --- a/pom.xml +++ b/pom.xml @@ -440,6 +440,17 @@ imageio-ext-streams ${project.version} + + it.geosolutions.imageio-ext + imageio-ext-imagereadmt + ${project.version} + + + it.geosolutions.imageio-ext + imageio-ext-utilities + ${project.version} + +