1 // -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
2 // ---------------------------------------------------------------------------
4 // Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
5 // ---------------------------------------------------------------------------
6 // SquirrelJME is under the Mozilla Public License Version 2.0.
7 // See license.mkd for licensing and copyright information.
8 // ---------------------------------------------------------------------------
10 package cc
.squirreljme
.runtime
.lcdui
.image
;
12 import cc
.squirreljme
.jvm
.mle
.callbacks
.NativeImageLoadCallback
;
13 import cc
.squirreljme
.runtime
.cldc
.debug
.Debugging
;
14 import cc
.squirreljme
.runtime
.cldc
.util
.StreamUtils
;
15 import java
.io
.ByteArrayInputStream
;
16 import java
.io
.DataInputStream
;
17 import java
.io
.IOException
;
18 import java
.io
.InputStream
;
19 import java
.util
.Arrays
;
20 import net
.multiphasicapps
.io
.ByteDeque
;
21 import net
.multiphasicapps
.io
.CRC32Calculator
;
22 import net
.multiphasicapps
.io
.ChecksumInputStream
;
23 import net
.multiphasicapps
.io
.SizeLimitedInputStream
;
24 import net
.multiphasicapps
.io
.ZLibDecompressor
;
27 * This class parses PNG images.
30 * * http://www.libpng.org/pub/png/pngdocs.html
31 * * http://www.libpng.org/pub/png/spec/iso/index-object.html
32 * * https://www.w3.org/TR/PNG/
33 * * https://tools.ietf.org/html/rfc2083
37 public class PNGReader
38 implements ImageReader
40 /** The input source. */
41 protected final DataInputStream in
;
43 /** The image loader to use. */
44 protected final NativeImageLoadCallback loader
;
46 /** Are indexed pixels desired? */
47 private boolean _wantIndexed
;
52 /** Scanline length. */
59 private int _bitDepth
;
61 /** The color type. */
62 private int _colorType
;
64 /** Is adam7 interlacing being used? */
65 private boolean _adamseven
;
67 /** RGB image data. */
71 private int[] _palette
;
73 /** Was an alpha channel used? */
74 private boolean _hasalpha
;
76 /** Initial value for read Y position, for multiple IDAT chunks. */
79 /** Initial value for read X position, for multiple IDAT chunks. */
82 /** The number of colors used. */
83 private int _numcolors
;
85 /** The maximum number of permitted colors. */
86 private int _maxcolors
;
89 * Initializes the PNG parser.
91 * @param __in The input stream.
92 * @param __loader The loader to use.
93 * @throws NullPointerException On null arguments.
96 public PNGReader(InputStream __in
, NativeImageLoadCallback __loader
)
97 throws NullPointerException
100 if (__in
== null || __loader
== null)
101 throw new NullPointerException("NARG");
104 this.in
= new DataInputStream(__in
);
105 this.loader
= __loader
;
109 * Parses the PNG image data.
111 * @throws IOException On read errors.
117 DataInputStream in
= this.in
;
118 NativeImageLoadCallback loader
= this.loader
;
120 /* {@squirreljme.error EB0t Illegal PNG magic number.} */
121 if (in
.readUnsignedByte() != 137 ||
122 in
.readUnsignedByte() != 80 ||
123 in
.readUnsignedByte() != 78 ||
124 in
.readUnsignedByte() != 71 ||
125 in
.readUnsignedByte() != 13 ||
126 in
.readUnsignedByte() != 10 ||
127 in
.readUnsignedByte() != 26 ||
128 in
.readUnsignedByte() != 10)
129 throw new IOException("EB0t");
131 // Some J2ME games such as Bobby Carrot have invalid PNG files that
132 // contain a tRNS chunk after the IDAT chunk. This violates the PNG
133 // standard so the image chunk has to cached and process later,
134 // otherwise the images will be corrupt.
135 byte[] imageChunk
= null;
137 // Keep reading chunks in the file
140 // {@squirreljme.erorr EB1l Length of chunk is negative.}
141 int len
= in
.readInt();
143 throw new IOException("EB1l");
145 // Setup data stream for reading packet data, do not propogate
147 CRC32Calculator crc
= new CRC32Calculator(true, true,
148 0x04C11DB7, 0xFFFFFFFF, 0xFFFFFFFF);
150 try (DataInputStream data
= new DataInputStream(
151 new SizeLimitedInputStream(new ChecksumInputStream(crc
, in
),
152 len
+ 4, true, false)))
154 // Read the packet type
155 int type
= data
.readInt();
158 // End of PNG, stop processing
159 if (type
== 0x49454E44)
162 // Depends on the type
167 this.__parseHeader(data
);
172 this.__parsePalette(data
, len
);
177 // There may be multiple consecutive IDAT chunks which
178 // just continue where the previous one left off, so
179 // just smash them together
180 if (imageChunk
!= null)
182 // Read chunk data, decompress the data
183 // additionally so that the decoder does not need
184 // to worry about the data being compressed at
186 byte[] xtrachunk
= PNGReader
.__chunkLater(data
);
188 // Setup new array which contains the original
189 // data but has more space
190 int gn
= imageChunk
.length
,
191 xn
= xtrachunk
.length
,
193 imageChunk
= Arrays
.copyOf(imageChunk
, nl
);
195 // Write in all the data
196 // for (int i = 0, o = gn; i < xn; i++, o++)
197 // imageChunk[o] = xtrachunk[i];
198 System
.arraycopy(xtrachunk
, 0,
204 imageChunk
= PNGReader
.__chunkLater(data
);
207 // Transparency information
209 this.__parseAlpha(data
, len
);
218 /* {@squirreljme.error EB0u CRC mismatch in PNG data chunk.
219 (Desired CRC; Actual CRC; Last chunk type read)} */
220 int want
= in
.readInt(),
221 real
= crc
.checksum();
223 throw new IOException(String
.format("EB0u %08x %08x %08x",
224 want
, real
, lasttype
));
227 /* {@squirreljme.error EB0v No image data has been loaded.} */
228 int[] argb
= this._argb
;
230 throw new IOException("EB0v");
232 // Is an alpha channel being used?
235 // Force all pixels to opaque
236 Arrays
.fill(argb
, 0xFF_
000000);
238 // Make all pixels opaque in the palette
239 int[] palette
= this._palette
;
241 for (int i
= 0, n
= palette
.length
; i
< n
; i
++)
242 palette
[i
] |= 0xFF_
000000;
245 /* {@squirreljme.error EB0w Unsupported bit-depth. (The bitdepth)} */
246 int bitdepth
= this._bitDepth
;
247 if (Integer
.bitCount(bitdepth
) != 1 || bitdepth
> 8)
248 throw new IOException("EB0w " + bitdepth
);
250 /* {@squirreljme.error EB0x Adam7 interlacing not supported.} */
252 throw new IOException("EB0x");
254 /* {@squirreljme.error EB0y Paletted PNG image has no palette.} */
255 if (this._colorType
== 3 && this._palette
== null)
256 throw new IOException("EB0y");
258 // Process the image chunk now that the other information was read
259 // Note that the chunk needs to be unfiltered first
260 int colorType
= this._colorType
;
261 try (InputStream data
= new ByteArrayInputStream(this.__unfilter(
262 new ZLibDecompressor(new ByteArrayInputStream(imageChunk
)),
263 this.__determineUnfilterBpp())))
265 // Grayscale or Indexed
266 if (colorType
== 0 || colorType
== 3)
267 this.__pixelIndexed(data
, (colorType
== 3));
270 else if (colorType
== 2 || colorType
== 6)
271 this.__pixelsRGB(data
, (colorType
== 6));
273 // YA (Grayscale + Alpha)
275 this.__pixelsYA(data
);
279 loader
.initialize(this._width
, this._height
,
281 loader
.addImage(argb
, 0, argb
.length
,
286 * Determines the total number of bytes that represent a single pixel,
289 * @return The bytes per pixel.
292 private int __determineUnfilterBpp()
294 // These are used in the calculations
295 int colorType
= this._colorType
;
296 int bitDepth
= this._bitDepth
;
298 // Determine the number of bytes per pixel, needed for unfiltering
299 // Since these refer to previous pixels rather than previous bytes
303 // Grayscale or Indexed
306 return PNGReader
.__roundNumBitsToByte(bitDepth
);
310 return PNGReader
.__roundNumBitsToByte(bitDepth
* 3);
314 return PNGReader
.__roundNumBitsToByte(bitDepth
* 4);
316 // YA (Grayscale + Alpha), aka 4
318 return PNGReader
.__roundNumBitsToByte(bitDepth
* 2);
323 * Parses the alpha transparency data.
325 * @param __in The stream to read data from.
326 * @param __dlen The data length/
327 * @throws IOException On parse errors.
328 * @throws NullPointerException On null arguments.
331 private void __parseAlpha(DataInputStream __in
, int __dlen
)
332 throws IOException
, NullPointerException
336 throw new NullPointerException("NARG");
338 int[] palette
= this._palette
;
339 int colortype
= this._colorType
,
340 numpals
= (palette
!= null ? palette
.length
: 0),
341 numcolors
= this._numcolors
;
343 // Force alpha channel to be set
344 this._hasalpha
= true;
346 // Alpha values for grayscale or true-color
349 // Read double-byte values
353 int col
= __in
.read(),
360 // Find color to remove the alpha channel from the palette
361 for (int p
= 0; p
< numpals
; p
++)
362 if (palette
[p
] == col
)
363 palette
[p
] &= 0xFFFFFF;
367 // Alpha values for indexed values
368 else if (colortype
== 3)
370 // Read as many entries as possible
372 for (; i
< numcolors
; i
++)
374 int val
= __in
.read();
376 // Reached end of data, the rest are implied opaque
381 palette
[i
] |= ((val
& 0xFF) << 24);
384 // The alpha data can be short, which means that all of
385 // the following colors are fully opaque
386 for (; i
< numcolors
; i
++)
387 palette
[i
] |= 0xFF_
000000;
392 * Parses the PNG header.
394 * @param __in The stream to read data from.
395 * @throws IOException On parse errors.
396 * @throws NullPointerException On null arguments.
399 private void __parseHeader(DataInputStream __in
)
400 throws IOException
, NullPointerException
404 throw new NullPointerException("NARG");
406 /* {@squirreljme.error EB0z Image has zero or negative width.
408 int width
= __in
.readInt();
410 throw new IOException(String
.format("EB0z %d", width
));
413 /* {@squirreljme.error EB10 Image has zero or negative height. (The
415 int height
= __in
.readInt();
417 throw new IOException(String
.format("EB10 %d", height
));
418 this._height
= height
;
421 Debugging
.debugNote("Size: %dx%d%n", width
, height
);
423 // Read the bit depth and the color type
424 int bitdepth
= __in
.readUnsignedByte(),
425 colortype
= __in
.readUnsignedByte();
427 /* {@squirreljme.error EB11 Invalid PNG bit depth.
429 if (Integer
.bitCount(bitdepth
) != 1 || bitdepth
< 0 || bitdepth
> 16)
430 throw new IOException(String
.format("EB11 %d", bitdepth
));
432 /* {@squirreljme.error EB12 Invalid PNG bit depth and color type
433 combination. (The color type; The bit depth)} */
434 if ((bitdepth
< 8 && (colortype
!= 0 && colortype
!= 3)) ||
435 (bitdepth
> 8 && colortype
!= 3))
436 throw new IOException(String
.format("EB12 %d %d", colortype
,
440 this._bitDepth
= bitdepth
;
441 this._colorType
= colortype
;
443 // These two color types have alpha, this field may be set later on
444 // if a transparency chunk was found
446 this._hasalpha
= (hasalpha
= (colortype
== 4 || colortype
== 6));
448 // Determine number of channels
449 int channels
= (colortype
== 0 || colortype
== 3 ?
1 :
450 (colortype
== 2 ?
3 :
451 (colortype
== 4 ?
2 :
452 (colortype
== 6 ?
4 : 1))));
454 // Scan length, 7 extra bits are added for any needed padding if there
456 this._scanlen
= ((width
* channels
* bitdepth
) + 7) / 8;
458 /* {@squirreljme.error EB13 Only deflate compressed PNG images are
459 supported. (The compression method)} */
460 int compressionmethod
= __in
.readUnsignedByte();
461 if (compressionmethod
!= 0)
462 throw new IOException(String
.format("EB13 %d", compressionmethod
));
464 /* {@squirreljme.error EB14 Only adapative filtered PNGs are supported.
465 (The filter type)} */
466 int filter
= __in
.readUnsignedByte();
468 throw new IOException(String
.format("EB14 %d", filter
));
470 /* {@squirreljme.error EB15 Unsupported PNG interlace method. (The
472 int interlace
= __in
.readUnsignedByte();
473 if (interlace
!= 0 && interlace
!= 1)
474 throw new IOException(String
.format("EB15 %d", interlace
));
475 this._adamseven
= (interlace
== 1);
477 // Allocate image buffer
478 this._argb
= new int[width
* height
];
480 // If this is grayscale, then force a palette to be initialized so the
481 // colors are more easily read without needing to process them further
482 // So all values are treated as indexed
485 // 2^d colors available
486 int numcolors
= (1 << bitdepth
);
488 // Build palette, force everything to opaque, it will be cleared
490 int[] palette
= new int[numcolors
];
491 for (int i
= 0; i
< numcolors
; i
++)
492 palette
[i
] = ((int)(((double)i
/ (double)numcolors
) * 255.0)) |
496 this._palette
= palette
;
501 * Parses the PNG palette.
503 * @param __in The stream to read data from.
504 * @param __len The length of the palette data.
505 * @throws IOException On parse errors.
506 * @throws NullPointerException On null arguments.
509 private void __parsePalette(DataInputStream __in
, int __len
)
510 throws IOException
, NullPointerException
514 throw new NullPointerException("NARG");
516 // Ignore the palette if this is not an indexed image
517 if (this._colorType
!= 3)
520 // Determine the number of colors
521 int numColors
= __len
/ 3;
522 int maxColors
= 1 << this._bitDepth
;
523 if (numColors
> maxColors
)
524 numColors
= maxColors
;
527 this._numcolors
= numColors
;
528 this._maxcolors
= maxColors
;
530 // Load palette data, any remaining colors are left uninitialized and
531 // are fully transparent or just black
532 int[] palette
= new int[maxColors
];
533 this._palette
= palette
;
534 for (int i
= 0; i
< numColors
; i
++)
536 int r
= __in
.readUnsignedByte(),
537 g
= __in
.readUnsignedByte(),
538 b
= __in
.readUnsignedByte();
541 palette
[i
] = (r
<< 16) | (g
<< 8) | b
;
544 // Notify that a palette was set
546 this.loader
.setPalette(palette
, 0, maxColors
, true, -1);
550 * Decodes grayscale/indexed image data.
552 * @param __dis Input Stream.
553 * @param __idx Indexed colors instead of just grayscale?
554 * @throws IOException On read errors.
555 * @throws NullPointerException On null arguments.
558 private void __pixelIndexed(InputStream __dis
, boolean __idx
)
559 throws IOException
, NullPointerException
562 throw new NullPointerException("NARG");
564 int[] argb
= this._argb
;
565 int[] palette
= this._palette
;
566 int width
= this._width
;
567 int height
= this._height
;
568 int limit
= width
* height
;
569 int bitdepth
= this._bitDepth
;
570 int bitmask
= (1 << bitdepth
) - 1;
571 int numpals
= (palette
!= null ? palette
.length
: 0);
572 int hishift
= (8 - bitdepth
);
573 int himask
= bitmask
<< hishift
;
575 // Do not translate paletted colors, get their raw index values?
576 boolean wantIndexed
= this._wantIndexed
;
578 // Read of multiple bits
581 // Read and check EOF
582 int v
= __dis
.read();
587 for (int b
= 0; b
< 8 && o
< limit
; b
+= bitdepth
, v
<<= bitdepth
)
589 int index
= ((v
& himask
) >>> hishift
) % numpals
;
594 argb
[o
++] = palette
[index
];
600 * Decodes RGB or RGBA image data.
602 * @param __dis Input Stream.
603 * @param __alpha RGBA is used?
604 * @throws IOException On read errors.
605 * @throws NullPointerException On null arguments.
608 private void __pixelsRGB(InputStream __dis
, boolean __alpha
)
609 throws IOException
, NullPointerException
612 throw new NullPointerException("NARG");
615 int[] argb
= this._argb
;
616 int width
= this._width
,
617 height
= this._height
,
618 limit
= width
* height
;
620 // Keep reading in data
621 for (int o
= 0; o
< limit
; o
++)
623 // Read in all values, the mask is used to keep the sign bit in
624 // place but also cap the value to 255!
625 int r
= __dis
.read() & 0x800000FF,
626 g
= __dis
.read() & 0x800000FF,
627 b
= __dis
.read() & 0x800000FF;
628 int a
= (__alpha ?
(__dis
.read() & 0x800000FF) : 0xFF);
630 // Have any hit EOF? Just need to OR all the bits
631 if ((r
| g
| b
| a
) < 0)
635 argb
[o
] = (a
<< 24) | (r
<< 16) | (g
<< 8) | b
;
640 * Decodes image data.
642 * @param __dis Input Stream.
643 * @throws IOException On read errors.
644 * @throws NullPointerException On null arguments.
647 private void __pixelsYA(InputStream __dis
)
648 throws IOException
, NullPointerException
651 throw new NullPointerException("NARG");
654 int[] argb
= this._argb
;
655 int width
= this._width
,
656 height
= this._height
,
657 limit
= width
* height
;
659 // Keep reading in data
660 for (int o
= 0; o
< limit
;)
662 // Read in all values, the mask is used to keep the sign bit in
663 // place but also cap the value to 255!
664 int a
= __dis
.read() & 0x800000FF,
665 y
= __dis
.read() & 0x800000FF;
667 // Have any hit EOF? Just need to OR all the bits
672 argb
[o
++] = (a
<< 24) | (y
<< 16) | (y
<< 8) | y
;
677 * Unfilters the PNG data.
679 * @param __in The stream to read from.
680 * @param __bpp Rounded bytes per pixel.
681 * @return The unfiltered data.
682 * @throws IOException On read errors.
683 * @throws NullPointerException On null arguments.
686 private byte[] __unfilter(InputStream __in
, int __bpp
)
687 throws IOException
, NullPointerException
690 throw new NullPointerException("NARG");
693 int scanLen
= this._scanlen
;
694 int height
= this._height
;
696 // Allocate buffer that will be returned, containing the unfiltered
698 byte[] rv
= new byte[scanLen
* height
];
700 // Read the image scanline by scanline and process it
701 for (int dy
= 0; dy
< height
; dy
++)
703 // Base output for this scanline
704 int ibase
= scanLen
* dy
;
706 // At the start of every scanline is the filter type, which
707 // describes how the data should be treated
708 /* {@squirreljme.error EB16 Unknown filter type. (The type; The
709 scanline base coordinate; The scan line length; Image size)} */
710 int type
= __in
.read();
711 if (type
< 0 || type
> 4)
712 throw new IOException(String
.format(
713 "EB16 %d (%d, %d) %d [%d, %d]",
714 type
, 0, dy
, scanLen
, this._width
, height
));
716 // Go through each byte in the scanline
717 for (int dx
= 0; dx
< scanLen
; dx
++)
719 // Virtual X position
722 // The current position in the buffer
725 // The filter algorithm is a bit confusing and it uses the
726 // prior and old pixel information, so according to the PNG
727 // spec just to be easier to use the variables will be named
728 // the same. Anywhere that bleeds off the image will always be
731 // The current byte being filtered
732 int x
= __in
.read() & 0xFF;
734 // The byte to the left of (x, y) [-1, 0]
736 0 : rv
[di
- __bpp
]) & 0xFF;
738 // The byte to the top of (x, y) [0, -1]
739 int b
= (dy
<= 0 ?
0 : rv
[di
- scanLen
]) & 0xFF;
741 // The byte to the top and left of (x, y) [-1, -1]
742 int c
= (vX
<= 0 || dy
<= 0 ?
743 0 : rv
[(di
- scanLen
) - __bpp
]) & 0xFF;
745 // Depends on the decoding algorithm
766 res
= x
+ ((a
+ b
) >>> 1);
779 pa
= (pa
< 0 ?
-pa
: pa
);
780 pb
= (pb
< 0 ?
-pb
: pb
);
781 pc
= (pc
< 0 ?
-pc
: pc
);
783 // Perform some checks
784 if (pa
<= pb
&& pa
<= pc
)
803 * Reads all the input data and returns a byte array for the data, so it
804 * may be processed later.
806 * @param __in The stream to read from.
807 * @return The read data.
808 * @throws IOException On read errors.
809 * @throws NullPointerException On null arguments.
812 private static byte[] __chunkLater(InputStream __in
)
813 throws IOException
, NullPointerException
816 throw new NullPointerException("NARG");
818 // The final glue point
819 ByteDeque glue
= new ByteDeque();
821 // Read in all the various chunks as much as possible
822 byte[] buf
= StreamUtils
.buffer(__in
);
825 // Read in the chunk data
826 int rc
= __in
.read(buf
, 0, buf
.length
);
833 glue
.addLast(buf
, 0, rc
);
836 return glue
.toByteArray();
840 * Rounds the number of bits to bytes according to the PNG specification.
842 * @param __numBits The number of bits.
843 * @return The number of bytes that represent the bits, rounded up.
846 private static int __roundNumBitsToByte(int __numBits
)
848 // Divide by 8 for bits, flooring... then round up for any other bits
849 return (__numBits
>>> 3) + (((__numBits
& 0b111
) == 0) ?
0 : 1);