Add blank projects for CDC APIs, these are reserved for future work with them when...
[SquirrelJME.git] / modules / midp-lcdui / src / main / java / cc / squirreljme / runtime / lcdui / image / PNGReader.java
blob9547924a0801a254dce1d3daf4d38796b93881e2
1 // -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
2 // ---------------------------------------------------------------------------
3 // SquirrelJME
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;
26 /**
27 * This class parses PNG images.
29 * PNG specifications:
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
35 * @since 2017/02/28
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;
49 /** Image width. */
50 private int _width;
52 /** Scanline length. */
53 private int _scanlen;
55 /** Image height. */
56 private int _height;
58 /** The bit depth. */
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. */
68 private int[] _argb;
70 /** Palette 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. */
77 private int _initvy;
79 /** Initial value for read X position, for multiple IDAT chunks. */
80 private int _initvx;
82 /** The number of colors used. */
83 private int _numcolors;
85 /** The maximum number of permitted colors. */
86 private int _maxcolors;
88 /**
89 * Initializes the PNG parser.
91 * @param __in The input stream.
92 * @param __loader The loader to use.
93 * @throws NullPointerException On null arguments.
94 * @since 2017/02/28
96 public PNGReader(InputStream __in, NativeImageLoadCallback __loader)
97 throws NullPointerException
99 // Check
100 if (__in == null || __loader == null)
101 throw new NullPointerException("NARG");
103 // Set
104 this.in = new DataInputStream(__in);
105 this.loader = __loader;
109 * Parses the PNG image data.
111 * @throws IOException On read errors.
112 * @since 2017/02/28
114 public void parse()
115 throws IOException
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
138 for (;;)
140 // {@squirreljme.erorr EB1l Length of chunk is negative.}
141 int len = in.readInt();
142 if (len < 0)
143 throw new IOException("EB1l");
145 // Setup data stream for reading packet data, do not propogate
146 // close
147 CRC32Calculator crc = new CRC32Calculator(true, true,
148 0x04C11DB7, 0xFFFFFFFF, 0xFFFFFFFF);
149 int lasttype = 0;
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();
156 lasttype = type;
158 // End of PNG, stop processing
159 if (type == 0x49454E44)
160 break;
162 // Depends on the type
163 switch (type)
165 // Header
166 case 0x49484452:
167 this.__parseHeader(data);
168 break;
170 // Palette
171 case 0x504c5445:
172 this.__parsePalette(data, len);
173 break;
175 // Image data
176 case 0x49444154:
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
185 // all...
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,
192 nl = gn + xn;
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,
199 imageChunk, gn, xn);
202 // The first chunk
203 else
204 imageChunk = PNGReader.__chunkLater(data);
205 break;
207 // Transparency information
208 case 0x74524E53:
209 this.__parseAlpha(data, len);
210 break;
212 // Unknown, ignore
213 default:
214 break;
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();
222 if (want != real)
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;
229 if (argb == null)
230 throw new IOException("EB0v");
232 // Is an alpha channel being used?
233 if (!this._hasalpha)
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;
240 if (palette != null)
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.} */
251 if (this._adamseven)
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));
269 // RGB(A)
270 else if (colorType == 2 || colorType == 6)
271 this.__pixelsRGB(data, (colorType == 6));
273 // YA (Grayscale + Alpha)
274 else
275 this.__pixelsYA(data);
278 // Create image
279 loader.initialize(this._width, this._height,
280 false, false);
281 loader.addImage(argb, 0, argb.length,
282 0, this._hasalpha);
286 * Determines the total number of bytes that represent a single pixel,
287 * rounded up.
289 * @return The bytes per pixel.
290 * @since 2022/06/14
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
300 // in the algorithm
301 switch (colorType)
303 // Grayscale or Indexed
304 case 0:
305 case 3:
306 return PNGReader.__roundNumBitsToByte(bitDepth);
308 // RGB
309 case 2:
310 return PNGReader.__roundNumBitsToByte(bitDepth * 3);
312 // RGBA
313 case 6:
314 return PNGReader.__roundNumBitsToByte(bitDepth * 4);
316 // YA (Grayscale + Alpha), aka 4
317 default:
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.
329 * @since 2019/04/14
331 private void __parseAlpha(DataInputStream __in, int __dlen)
332 throws IOException, NullPointerException
334 // Check
335 if (__in == null)
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
347 if (colortype == 0)
349 // Read double-byte values
350 for (int at = 0;;)
352 // Read in color
353 int col = __in.read(),
354 ign = __in.read();
356 // EOF?
357 if ((col | ign) < 0)
358 break;
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
371 int i = 0;
372 for (; i < numcolors; i++)
374 int val = __in.read();
376 // Reached end of data, the rest are implied opaque
377 if (val < 0)
378 break;
380 // Fill in color
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.
397 * @since 2017/02/28
399 private void __parseHeader(DataInputStream __in)
400 throws IOException, NullPointerException
402 // Check
403 if (__in == null)
404 throw new NullPointerException("NARG");
406 /* {@squirreljme.error EB0z Image has zero or negative width.
407 (The width)} */
408 int width = __in.readInt();
409 if (width <= 0)
410 throw new IOException(String.format("EB0z %d", width));
411 this._width = width;
413 /* {@squirreljme.error EB10 Image has zero or negative height. (The
414 height)} */
415 int height = __in.readInt();
416 if (height <= 0)
417 throw new IOException(String.format("EB10 %d", height));
418 this._height = height;
420 // Debug
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.
428 (The 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,
437 bitdepth));
439 // Set
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
445 boolean hasalpha;
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
455 // is any
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();
467 if (filter != 0)
468 throw new IOException(String.format("EB14 %d", filter));
470 /* {@squirreljme.error EB15 Unsupported PNG interlace method. (The
471 interlace type)} */
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
483 if (colortype == 0)
485 // 2^d colors available
486 int numcolors = (1 << bitdepth);
488 // Build palette, force everything to opaque, it will be cleared
489 // later
490 int[] palette = new int[numcolors];
491 for (int i = 0; i < numcolors; i++)
492 palette[i] = ((int)(((double)i / (double)numcolors) * 255.0)) |
493 0xFF_000000;
495 // Set
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.
507 * @since 2017/02/28
509 private void __parsePalette(DataInputStream __in, int __len)
510 throws IOException, NullPointerException
512 // Check
513 if (__in == null)
514 throw new NullPointerException("NARG");
516 // Ignore the palette if this is not an indexed image
517 if (this._colorType != 3)
518 return;
520 // Determine the number of colors
521 int numColors = __len / 3;
522 int maxColors = 1 << this._bitDepth;
523 if (numColors > maxColors)
524 numColors = maxColors;
526 // Set
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();
540 // Fill in color
541 palette[i] = (r << 16) | (g << 8) | b;
544 // Notify that a palette was set
545 this._wantIndexed =
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.
556 * @since 2019/04/15
558 private void __pixelIndexed(InputStream __dis, boolean __idx)
559 throws IOException, NullPointerException
561 if (__dis == null)
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
579 for (int o = 0;;)
581 // Read and check EOF
582 int v = __dis.read();
583 if (v < 0)
584 break;
586 // Handle each bit
587 for (int b = 0; b < 8 && o < limit; b += bitdepth, v <<= bitdepth)
589 int index = ((v & himask) >>> hishift) % numpals;
591 if (wantIndexed)
592 argb[o++] = index;
593 else
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.
606 * @since 2019/04/15
608 private void __pixelsRGB(InputStream __dis, boolean __alpha)
609 throws IOException, NullPointerException
611 if (__dis == null)
612 throw new NullPointerException("NARG");
614 // Get output
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)
632 break;
634 // Write pixel
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.
645 * @since 2019/04/15
647 private void __pixelsYA(InputStream __dis)
648 throws IOException, NullPointerException
650 if (__dis == null)
651 throw new NullPointerException("NARG");
653 // Get output
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
668 if ((a | y) < 0)
669 break;
671 // Write pixel
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.
684 * @since 2019/04/15
686 private byte[] __unfilter(InputStream __in, int __bpp)
687 throws IOException, NullPointerException
689 if (__in == null)
690 throw new NullPointerException("NARG");
692 // Need these
693 int scanLen = this._scanlen;
694 int height = this._height;
696 // Allocate buffer that will be returned, containing the unfiltered
697 // data
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
720 int vX = dx / __bpp;
722 // The current position in the buffer
723 int di = ibase + dx;
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
729 // treated as zero.
731 // The current byte being filtered
732 int x = __in.read() & 0xFF;
734 // The byte to the left of (x, y) [-1, 0]
735 int a = (vX <= 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
746 int res = 0;
747 switch (type)
749 // None
750 case 0:
751 res = x;
752 break;
754 // Sub
755 case 1:
756 res = x + a;
757 break;
759 // Up
760 case 2:
761 res = x + b;
762 break;
764 // Average
765 case 3:
766 res = x + ((a + b) >>> 1);
767 break;
769 // Paeth
770 case 4:
772 // Calculate these
773 int p = a + b - c;
774 int pa = p - a;
775 int pb = p - b;
776 int pc = p - c;
778 // Absolute values
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)
785 res = x + a;
786 else if (pb <= pc)
787 res = x + b;
788 else
789 res = x + c;
791 break;
794 // Set result
795 rv[di] = (byte)res;
799 return rv;
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.
810 * @since 2019/04/14
812 private static byte[] __chunkLater(InputStream __in)
813 throws IOException, NullPointerException
815 if (__in == null)
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);
823 for (;;)
825 // Read in the chunk data
826 int rc = __in.read(buf, 0, buf.length);
828 // EOF?
829 if (rc < 0)
830 break;
832 // Add to the buffer
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.
844 * @since 2022/06/14
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);