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