1 /** 2 * Copyright 2012 JogAmp Community. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without modification, are 5 * permitted provided that the following conditions are met: 6 * 7 * 1. Redistributions of source code must retain the above copyright notice, this list of 8 * conditions and the following disclaimer. 9 * 10 * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 * of conditions and the following disclaimer in the documentation and/or other materials 12 * provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED 15 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 * 24 * The views and conclusions contained in the software and documentation are those of the 25 * authors and should not be interpreted as representing official policies, either expressed 26 * or implied, of JogAmp Community. 27 */ 28 package com.jogamp.opengl.util; 29 30 import java.io.BufferedInputStream; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.OutputStream; 34 import java.nio.ByteBuffer; 35 import java.nio.IntBuffer; 36 37 import com.jogamp.nativewindow.util.Dimension; 38 import com.jogamp.nativewindow.util.DimensionImmutable; 39 import com.jogamp.nativewindow.util.PixelFormat; 40 import com.jogamp.nativewindow.util.PixelRectangle; 41 import com.jogamp.nativewindow.util.PixelFormatUtil; 42 43 import jogamp.opengl.Debug; 44 import jogamp.opengl.util.pngj.ImageInfo; 45 import jogamp.opengl.util.pngj.ImageLine; 46 import jogamp.opengl.util.pngj.ImageLineHelper; 47 import jogamp.opengl.util.pngj.PngReader; 48 import jogamp.opengl.util.pngj.PngWriter; 49 import jogamp.opengl.util.pngj.chunks.PngChunkPLTE; 50 import jogamp.opengl.util.pngj.chunks.PngChunkTRNS; 51 import jogamp.opengl.util.pngj.chunks.PngChunkTextVar; 52 53 import com.jogamp.common.nio.Buffers; 54 import com.jogamp.common.util.IOUtil; 55 56 public class PNGPixelRect extends PixelRectangle.GenericPixelRect { 57 private static final boolean DEBUG = Debug.debug("PNG"); 58 59 /** 60 * Reads a PNG image from the specified InputStream. 61 * <p> 62 * Implicitly converts the image to match the desired: 63 * <ul> 64 * <li>{@link PixelFormat}, see {@link #getPixelformat()}</li> 65 * <li><code>destStrideInBytes</code>, see {@link #getStride()}</li> 66 * <li><code>destIsGLOriented</code>, see {@link #isGLOriented()}</li> 67 * </ul> 68 * </p> 69 * 70 * @param in input stream 71 * @param destFmt desired destination {@link PixelFormat} incl. conversion, maybe <code>null</code> to use source {@link PixelFormat} 72 * @param destDirectBuffer if true, using a direct NIO buffer, otherwise an array backed buffer 73 * @param destMinStrideInBytes used if greater than PNG's stride, otherwise using PNG's stride. Stride is width * bytes-per-pixel. 74 * @param destIsGLOriented 75 * @return the newly created PNGPixelRect instance 76 * @throws IOException 77 */ read(final InputStream in, final PixelFormat ddestFmt, final boolean destDirectBuffer, final int destMinStrideInBytes, final boolean destIsGLOriented)78 public static PNGPixelRect read(final InputStream in, 79 final PixelFormat ddestFmt, final boolean destDirectBuffer, final int destMinStrideInBytes, 80 final boolean destIsGLOriented) throws IOException { 81 final BufferedInputStream bin = (in instanceof BufferedInputStream) ? (BufferedInputStream)in : new BufferedInputStream(in); 82 final PngReader pngr = new PngReader(bin, null); 83 final ImageInfo imgInfo = pngr.imgInfo; 84 final PngChunkPLTE plte = pngr.getMetadata().getPLTE(); 85 final PngChunkTRNS trns = pngr.getMetadata().getTRNS(); 86 final boolean indexed = imgInfo.indexed; 87 final boolean hasAlpha = indexed ? ( trns != null ) : imgInfo.alpha ; 88 89 if(DEBUG) { 90 System.err.println("PNGPixelRect: "+imgInfo); 91 } 92 final int channels = indexed ? ( hasAlpha ? 4 : 3 ) : imgInfo.channels ; 93 final boolean isGrayAlpha = 2 == channels && imgInfo.greyscale && imgInfo.alpha; 94 if ( ! ( 1 == channels || 3 == channels || 4 == channels || isGrayAlpha ) ) { 95 throw new RuntimeException("PNGPixelRect can only handle Lum/RGB/RGBA [1/3/4 channels] or Lum+A (GA) images for now. Channels "+channels + " Paletted: " + indexed); 96 } 97 final int bytesPerPixel = indexed ? channels : imgInfo.bytesPixel ; 98 if ( ! ( 1 == bytesPerPixel || 3 == bytesPerPixel || 4 == bytesPerPixel || isGrayAlpha ) ) { 99 throw new RuntimeException("PNGPixelRect can only handle Lum/RGB/RGBA [1/3/4 bpp] images for now. BytesPerPixel "+bytesPerPixel); 100 } 101 if( channels != bytesPerPixel ) { 102 throw new RuntimeException("PNGPixelRect currently only handles Channels [1/3/4] == BytePerPixel [1/3/4], channels: "+channels+", bytesPerPixel "+bytesPerPixel); 103 } 104 final int width = imgInfo.cols; 105 final int height = imgInfo.rows; 106 final double dpiX, dpiY; 107 { 108 final double[] dpi = pngr.getMetadata().getDpi(); 109 dpiX = dpi[0]; 110 dpiY = dpi[1]; 111 } 112 final PixelFormat srcFmt; 113 if ( indexed ) { 114 if ( hasAlpha ) { 115 srcFmt = PixelFormat.RGBA8888; 116 } else { 117 srcFmt = PixelFormat.RGB888; 118 } 119 } else { 120 switch( channels ) { 121 case 1: srcFmt = PixelFormat.LUMINANCE; break; 122 case 2: srcFmt = isGrayAlpha ? PixelFormat.LUMINANCE : null; break; 123 case 3: srcFmt = PixelFormat.RGB888; break; 124 case 4: srcFmt = PixelFormat.RGBA8888; break; 125 default: srcFmt = null; 126 } 127 if( null == srcFmt ) { 128 throw new InternalError("XXX: channels: "+channels+", bytesPerPixel "+bytesPerPixel); 129 } 130 } 131 final PixelFormat destFmt; 132 if( null == ddestFmt ) { 133 if( isGrayAlpha ) { 134 destFmt = PixelFormat.BGRA8888; // save alpha value on gray-alpha 135 } else { 136 destFmt = srcFmt; // 1:1 137 } 138 } else { 139 destFmt = ddestFmt; // user choice 140 } 141 final int destStrideInBytes = Math.max(destMinStrideInBytes, destFmt.comp.bytesPerPixel() * width); 142 final ByteBuffer destPixels = destDirectBuffer ? Buffers.newDirectByteBuffer(destStrideInBytes * height) : 143 ByteBuffer.allocate(destStrideInBytes * height); 144 { 145 final int reqBytes = destStrideInBytes * height; 146 if( destPixels.limit() < reqBytes ) { 147 throw new IndexOutOfBoundsException("Dest buffer has insufficient bytes left, needs "+reqBytes+": "+destPixels); 148 } 149 } 150 final boolean vert_flip = destIsGLOriented; 151 152 int[] rgbaScanline = indexed ? new int[width * channels] : null; 153 if(DEBUG) { 154 System.err.println("PNGPixelRect: indexed "+indexed+", alpha "+hasAlpha+", grayscale "+imgInfo.greyscale+", channels "+channels+"/"+imgInfo.channels+ 155 ", bytesPerPixel "+bytesPerPixel+"/"+imgInfo.bytesPixel+ 156 ", grayAlpha "+isGrayAlpha+", pixels "+width+"x"+height+", dpi "+dpiX+"x"+dpiY+", format "+srcFmt); 157 System.err.println("PNGPixelRect: destFormat "+destFmt+" ("+ddestFmt+", fast-path "+(destFmt==srcFmt)+"), destDirectBuffer "+destDirectBuffer+", destIsGLOriented (flip) "+destIsGLOriented); 158 System.err.println("PNGPixelRect: destStrideInBytes "+destStrideInBytes+" (destMinStrideInBytes "+destMinStrideInBytes+")"); 159 } 160 161 for (int row = 0; row < height; row++) { 162 final ImageLine l1 = pngr.readRow(row); 163 int lineOff = 0; 164 int dataOff = vert_flip ? ( height - 1 - row ) * destStrideInBytes : row * destStrideInBytes; 165 if( indexed ) { 166 for (int j = width - 1; j >= 0; j--) { 167 rgbaScanline = ImageLineHelper.palette2rgb(l1, plte, trns, rgbaScanline); // reuse rgbaScanline and update if resized 168 dataOff = getPixelRGBA8ToAny(destFmt, destPixels, dataOff, rgbaScanline, lineOff, hasAlpha); 169 lineOff += bytesPerPixel; 170 } 171 } else if( 1 == channels ) { 172 for (int j = width - 1; j >= 0; j--) { 173 dataOff = getPixelLUMToAny(destFmt, destPixels, dataOff, (byte)l1.scanline[lineOff++], (byte)0xff); // Luminance, 1 bytesPerPixel 174 } 175 } else if( isGrayAlpha ) { 176 for (int j = width - 1; j >= 0; j--) { 177 dataOff = getPixelLUMToAny(destFmt, destPixels, dataOff, (byte)l1.scanline[lineOff++], (byte)l1.scanline[lineOff++]); // Luminance+Alpha, 2 bytesPerPixel 178 } 179 } else if( srcFmt == destFmt ) { // fast-path 180 for (int j = width - 1; j >= 0; j--) { 181 dataOff = getPixelRGBSame(destPixels, dataOff, l1.scanline, lineOff, bytesPerPixel); 182 lineOff += bytesPerPixel; 183 } 184 } else { 185 for (int j = width - 1; j >= 0; j--) { 186 dataOff = getPixelRGBA8ToAny(destFmt, destPixels, dataOff, l1.scanline, lineOff, hasAlpha); 187 lineOff += bytesPerPixel; 188 } 189 } 190 } 191 pngr.end(); 192 193 return new PNGPixelRect(destFmt, new Dimension(width, height), destStrideInBytes, destIsGLOriented, destPixels, dpiX, dpiY); 194 } 195 getPixelLUMToAny(final PixelFormat dest_fmt, final ByteBuffer d, int dOff, final byte lum, final byte alpha)196 private static final int getPixelLUMToAny(final PixelFormat dest_fmt, final ByteBuffer d, int dOff, final byte lum, final byte alpha) { 197 switch(dest_fmt) { 198 case LUMINANCE: 199 d.put(dOff++, lum); 200 break; 201 case BGR888: 202 case RGB888: 203 d.put(dOff++, lum); 204 d.put(dOff++, lum); 205 d.put(dOff++, lum); 206 break; 207 case ABGR8888: 208 case ARGB8888: 209 d.put(dOff++, alpha); // A 210 d.put(dOff++, lum); 211 d.put(dOff++, lum); 212 d.put(dOff++, lum); 213 break; 214 case BGRA8888: 215 case RGBA8888: 216 d.put(dOff++, lum); 217 d.put(dOff++, lum); 218 d.put(dOff++, lum); 219 d.put(dOff++, alpha); // A 220 break; 221 default: 222 throw new InternalError("Unhandled format "+dest_fmt); 223 } 224 return dOff; 225 } getPixelRGBA8ToAny(final PixelFormat dest_fmt, final ByteBuffer d, int dOff, final int[] scanline, final int lineOff, final boolean srcHasAlpha)226 private static final int getPixelRGBA8ToAny(final PixelFormat dest_fmt, final ByteBuffer d, int dOff, final int[] scanline, final int lineOff, final boolean srcHasAlpha) { 227 final int p = PixelFormatUtil.convertToInt32(dest_fmt, (byte)scanline[lineOff], // R 228 (byte)scanline[lineOff+1], // G 229 (byte)scanline[lineOff+2], // B 230 srcHasAlpha ? (byte)scanline[lineOff+3] : (byte)0xff); // A 231 final int dbpp = dest_fmt.comp.bytesPerPixel(); 232 d.put(dOff++, (byte) ( p )); // 1 233 if( 1 < dbpp ) { 234 d.put(dOff++, (byte) ( p >>> 8 )); // 2 235 d.put(dOff++, (byte) ( p >>> 16 )); // 3 236 if( 4 == dbpp ) { 237 d.put(dOff++, (byte) ( p >>> 24 )); // 4 238 } 239 } 240 return dOff; 241 } getPixelRGBSame(final ByteBuffer d, int dOff, final int[] scanline, final int lineOff, final int bpp)242 private static final int getPixelRGBSame(final ByteBuffer d, int dOff, final int[] scanline, final int lineOff, final int bpp) { 243 d.put(dOff++, (byte)scanline[lineOff]); // R 244 if( 1 < bpp ) { 245 d.put(dOff++, (byte)scanline[lineOff + 1]); // G 246 d.put(dOff++, (byte)scanline[lineOff + 2]); // B 247 if( 4 == bpp ) { 248 d.put(dOff++, (byte)scanline[lineOff + 3]); // A 249 } 250 } 251 return dOff; 252 } setPixelRGBA8(final ImageLine line, final int lineOff, final ByteBuffer src, final int srcOff, final int bytesPerPixel, final boolean hasAlpha)253 private int setPixelRGBA8(final ImageLine line, final int lineOff, final ByteBuffer src, final int srcOff, final int bytesPerPixel, final boolean hasAlpha) { 254 final int b = hasAlpha ? 4-1 : 3-1; 255 if( src.limit() <= srcOff + b ) { 256 throw new IndexOutOfBoundsException("Buffer has unsufficient bytes left, needs ["+srcOff+".."+(srcOff+b)+"]: "+src); 257 } 258 final int p = PixelFormatUtil.convertToInt32(hasAlpha ? PixelFormat.RGBA8888 : PixelFormat.RGB888, pixelformat, src, srcOff); 259 line.scanline[lineOff ] = 0xff & p; // R 260 line.scanline[lineOff + 1] = 0xff & ( p >>> 8 ); // G 261 line.scanline[lineOff + 2] = 0xff & ( p >>> 16 ); // B 262 if(hasAlpha) { 263 line.scanline[lineOff + 3] = 0xff & ( p >>> 24 ); // A 264 } 265 return srcOff + pixelformat.comp.bytesPerPixel(); 266 } 267 setPixelRGBA8(final PixelFormat pixelformat, final ImageLine line, final int lineOff, final int srcPix, final int bytesPerPixel, final boolean hasAlpha)268 private static void setPixelRGBA8(final PixelFormat pixelformat, final ImageLine line, final int lineOff, final int srcPix, final int bytesPerPixel, final boolean hasAlpha) { 269 final int p = PixelFormatUtil.convertToInt32(hasAlpha ? PixelFormat.RGBA8888 : PixelFormat.RGB888, pixelformat, srcPix); 270 line.scanline[lineOff ] = 0xff & p; // R 271 line.scanline[lineOff + 1] = 0xff & ( p >>> 8 ); // G 272 line.scanline[lineOff + 2] = 0xff & ( p >>> 16 ); // B 273 if(hasAlpha) { 274 line.scanline[lineOff + 3] = 0xff & ( p >>> 24 ); // A 275 } 276 } 277 278 /** 279 * Creates a PNGPixelRect from data supplied by the end user. Shares 280 * data with the passed ByteBuffer. 281 * 282 * @param pixelformat 283 * @param size 284 * @param strideInBytes 285 * @param isGLOriented see {@link #isGLOriented()}. 286 * @param pixels 287 * @param dpiX 288 * @param dpiY 289 */ PNGPixelRect(final PixelFormat pixelformat, final DimensionImmutable size, final int strideInBytes, final boolean isGLOriented, final ByteBuffer pixels, final double dpiX, final double dpiY)290 public PNGPixelRect(final PixelFormat pixelformat, final DimensionImmutable size, 291 final int strideInBytes, final boolean isGLOriented, final ByteBuffer pixels, 292 final double dpiX, final double dpiY) { 293 super(pixelformat, size, strideInBytes, isGLOriented, pixels); 294 this.dpi = new double[] { dpiX, dpiY }; 295 } PNGPixelRect(final PixelRectangle src, final double dpiX, final double dpiY)296 public PNGPixelRect(final PixelRectangle src, final double dpiX, final double dpiY) { 297 super(src); 298 this.dpi = new double[] { dpiX, dpiY }; 299 } 300 private final double[] dpi; 301 302 /** Returns the dpi of the image. */ getDpi()303 public double[] getDpi() { return dpi; } 304 write(final OutputStream outstream, final boolean closeOutstream)305 public void write(final OutputStream outstream, final boolean closeOutstream) throws IOException { 306 final int width = size.getWidth(); 307 final int height = size.getHeight(); 308 final int bytesPerPixel = pixelformat.comp.bytesPerPixel(); 309 final ImageInfo imi = new ImageInfo(width, height, 8 /* bitdepth */, 310 (4 == bytesPerPixel) ? true : false /* alpha */, 311 (1 == bytesPerPixel) ? true : false /* grayscale */, 312 false /* indexed */); 313 314 // open image for writing to a output stream 315 try { 316 final PngWriter png = new PngWriter(outstream, imi); 317 // add some optional metadata (chunks) 318 png.getMetadata().setDpi(dpi[0], dpi[1]); 319 png.getMetadata().setTimeNow(0); // 0 seconds from now = now 320 png.getMetadata().setText(PngChunkTextVar.KEY_Title, "JogAmp PNGPixelRect"); 321 final boolean hasAlpha = 4 == bytesPerPixel; 322 323 final ImageLine l1 = new ImageLine(imi); 324 for (int row = 0; row < height; row++) { 325 int dataOff = isGLOriented ? ( height - 1 - row ) * strideInBytes : row * strideInBytes; 326 int lineOff = 0; 327 if(1 == bytesPerPixel) { 328 for (int j = width - 1; j >= 0; j--) { 329 l1.scanline[lineOff++] = pixels.get(dataOff++); // // Luminance, 1 bytesPerPixel 330 } 331 } else { 332 for (int j = width - 1; j >= 0; j--) { 333 dataOff = setPixelRGBA8(l1, lineOff, pixels, dataOff, bytesPerPixel, hasAlpha); 334 lineOff += bytesPerPixel; 335 } 336 } 337 png.writeRow(l1, row); 338 } 339 png.end(); 340 } finally { 341 if( closeOutstream ) { 342 IOUtil.close(outstream, false); 343 } 344 } 345 } 346 write(final PixelFormat pixelformat, final DimensionImmutable size, int strideInPixels, final boolean isGLOriented, final IntBuffer pixels, final double dpiX, final double dpiY, final OutputStream outstream, final boolean closeOutstream)347 public static void write(final PixelFormat pixelformat, final DimensionImmutable size, 348 int strideInPixels, final boolean isGLOriented, final IntBuffer pixels, 349 final double dpiX, final double dpiY, 350 final OutputStream outstream, final boolean closeOutstream) throws IOException { 351 final int width = size.getWidth(); 352 final int height = size.getHeight(); 353 final int bytesPerPixel = pixelformat.comp.bytesPerPixel(); 354 final ImageInfo imi = new ImageInfo(width, height, 8 /* bitdepth */, 355 (4 == bytesPerPixel) ? true : false /* alpha */, 356 (1 == bytesPerPixel) ? true : false /* grayscale */, 357 false /* indexed */); 358 if( 0 != strideInPixels ) { 359 if( strideInPixels < size.getWidth()) { 360 throw new IllegalArgumentException("Invalid stride "+bytesPerPixel+", must be greater than width "+size.getWidth()); 361 } 362 } else { 363 strideInPixels = size.getWidth(); 364 } 365 final int reqPixels = strideInPixels * size.getHeight(); 366 if( pixels.limit() < reqPixels ) { 367 throw new IndexOutOfBoundsException("Dest buffer has insufficient pixels left, needs "+reqPixels+": "+pixels); 368 } 369 370 // open image for writing to a output stream 371 try { 372 final PngWriter png = new PngWriter(outstream, imi); 373 // add some optional metadata (chunks) 374 png.getMetadata().setDpi(dpiX, dpiY); 375 png.getMetadata().setTimeNow(0); // 0 seconds from now = now 376 png.getMetadata().setText(PngChunkTextVar.KEY_Title, "JogAmp PNGPixelRect"); 377 final boolean hasAlpha = 4 == bytesPerPixel; 378 379 final ImageLine l1 = new ImageLine(imi); 380 for (int row = 0; row < height; row++) { 381 int dataOff = isGLOriented ? ( height - 1 - row ) * strideInPixels : row * strideInPixels; 382 int lineOff = 0; 383 if(1 == bytesPerPixel) { 384 for (int j = width - 1; j >= 0; j--) { 385 l1.scanline[lineOff++] = pixels.get(dataOff++); // // Luminance, 1 bytesPerPixel 386 } 387 } else { 388 for (int j = width - 1; j >= 0; j--) { 389 setPixelRGBA8(pixelformat, l1, lineOff, pixels.get(dataOff++), bytesPerPixel, hasAlpha); 390 lineOff += bytesPerPixel; 391 } 392 } 393 png.writeRow(l1, row); 394 } 395 png.end(); 396 } finally { 397 if( closeOutstream ) { 398 IOUtil.close(outstream, false); 399 } 400 } 401 } 402 } 403