JOGL v2.6.0-rc-20250706
JOGL, High-Performance Graphics Binding for Java™ (public API).
PNGPixelRect.java
Go to the documentation of this file.
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 */
28package com.jogamp.opengl.util;
29
30import java.io.BufferedInputStream;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.OutputStream;
34import java.nio.ByteBuffer;
35import java.nio.IntBuffer;
36
37import com.jogamp.nativewindow.util.Dimension;
38import com.jogamp.nativewindow.util.DimensionImmutable;
39import com.jogamp.nativewindow.util.PixelFormat;
40import com.jogamp.nativewindow.util.PixelRectangle;
41import com.jogamp.nativewindow.util.PixelFormatUtil;
42
43import jogamp.opengl.Debug;
44import jogamp.opengl.util.pngj.ImageInfo;
45import jogamp.opengl.util.pngj.ImageLine;
46import jogamp.opengl.util.pngj.ImageLineHelper;
47import jogamp.opengl.util.pngj.PngReader;
48import jogamp.opengl.util.pngj.PngWriter;
49import jogamp.opengl.util.pngj.chunks.PngChunkPLTE;
50import jogamp.opengl.util.pngj.chunks.PngChunkTRNS;
51import jogamp.opengl.util.pngj.chunks.PngChunkTextVar;
52
53import com.jogamp.common.nio.Buffers;
54import com.jogamp.common.util.IOUtil;
55
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 */
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
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 }
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 }
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 }
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
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 */
291 final int strideInBytes, final boolean isGLOriented, final ByteBuffer pixels,
292 final double dpiX, final double dpiY) {
294 this.dpi = new double[] { dpiX, dpiY };
295 }
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. */
303 public double[] getDpi() { return dpi; }
304
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
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}
void write(final OutputStream outstream, final boolean closeOutstream)
static void 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)
static PNGPixelRect read(final InputStream in, final PixelFormat ddestFmt, final boolean destDirectBuffer, final int destMinStrideInBytes, final boolean destIsGLOriented)
Reads a PNG image from the specified InputStream.
PNGPixelRect(final PixelRectangle src, final double dpiX, final double dpiY)
double[] getDpi()
Returns the dpi of the image.
PNGPixelRect(final PixelFormat pixelformat, final DimensionImmutable size, final int strideInBytes, final boolean isGLOriented, final ByteBuffer pixels, final double dpiX, final double dpiY)
Creates a PNGPixelRect from data supplied by the end user.
LUMINANCE
Stride is 8 bits, 8 bits per pixel, 1 component of 8 bits.
BGRA8888
Stride is 32 bits, 32 bits per pixel, 4 uniform components of 8 bits.
final Composition comp
Unique Pixel Composition, i.e.
RGBA8888
Stride is 32 bits, 32 bits per pixel, 4 uniform components of 8 bits.
RGB888
Stride 24 bits, 24 bits per pixel, 3 uniform components of 8 bits.
Immutable Dimension Interface, consisting of it's read only components:
int bytesPerPixel()
Number of bytes per pixel, i.e.
Pixel Rectangle identified by it's hashCode().