From 921b33825340d27deec2883ded21cb7434decc94 Mon Sep 17 00:00:00 2001 From: Sven Gothel Date: Mon, 31 Dec 2012 16:39:15 +0100 Subject: Update PNGJ 0.85 -> 1.12 (w/ interlace read support) ; Added PNG Interlace read tests (TestPNGTextureFromFileNEWT) --- .../jogamp/opengl/util/pngj/FilterType.java | 49 +- .../opengl/util/pngj/FilterWriteStrategy.java | 4 +- .../classes/jogamp/opengl/util/pngj/ImageInfo.java | 38 +- .../classes/jogamp/opengl/util/pngj/ImageLine.java | 355 +++++--- .../jogamp/opengl/util/pngj/ImageLineHelper.java | 318 +++++++ .../jogamp/opengl/util/pngj/ImageLines.java | 101 +++ .../jogamp/opengl/util/pngj/PngDeinterlacer.java | 277 ++++++ .../classes/jogamp/opengl/util/pngj/PngHelper.java | 213 ----- .../jogamp/opengl/util/pngj/PngHelperInternal.java | 264 ++++++ .../opengl/util/pngj/PngIDatChunkInputStream.java | 63 +- .../opengl/util/pngj/PngIDatChunkOutputStream.java | 10 +- .../classes/jogamp/opengl/util/pngj/PngReader.java | 959 ++++++++++++++++----- .../classes/jogamp/opengl/util/pngj/PngWriter.java | 677 ++++++++++----- .../opengl/util/pngj/PngjExceptionInternal.java | 23 + .../opengl/util/pngj/ProgressiveOutputStream.java | 8 +- .../util/pngj/chunks/ChunkCopyBehaviour.java | 3 +- .../opengl/util/pngj/chunks/ChunkHelper.java | 151 +++- .../jogamp/opengl/util/pngj/chunks/ChunkList.java | 282 ------ .../util/pngj/chunks/ChunkLoadBehaviour.java | 27 +- .../opengl/util/pngj/chunks/ChunkPredicate.java | 14 + .../jogamp/opengl/util/pngj/chunks/ChunkRaw.java | 97 ++- .../jogamp/opengl/util/pngj/chunks/ChunksList.java | 174 ++++ .../util/pngj/chunks/ChunksListForWrite.java | 171 ++++ .../jogamp/opengl/util/pngj/chunks/PngChunk.java | 239 +++-- .../opengl/util/pngj/chunks/PngChunkBKGD.java | 44 +- .../opengl/util/pngj/chunks/PngChunkCHRM.java | 58 +- .../opengl/util/pngj/chunks/PngChunkGAMA.java | 30 +- .../opengl/util/pngj/chunks/PngChunkHIST.java | 32 +- .../opengl/util/pngj/chunks/PngChunkICCP.java | 35 +- .../opengl/util/pngj/chunks/PngChunkIDAT.java | 28 +- .../opengl/util/pngj/chunks/PngChunkIEND.java | 20 +- .../opengl/util/pngj/chunks/PngChunkIHDR.java | 41 +- .../opengl/util/pngj/chunks/PngChunkITXT.java | 36 +- .../opengl/util/pngj/chunks/PngChunkMultiple.java | 27 + .../opengl/util/pngj/chunks/PngChunkOFFS.java | 89 ++ .../opengl/util/pngj/chunks/PngChunkPHYS.java | 30 +- .../opengl/util/pngj/chunks/PngChunkPLTE.java | 23 +- .../opengl/util/pngj/chunks/PngChunkSBIT.java | 40 +- .../opengl/util/pngj/chunks/PngChunkSPLT.java | 55 +- .../opengl/util/pngj/chunks/PngChunkSRGB.java | 28 +- .../opengl/util/pngj/chunks/PngChunkSTER.java | 60 ++ .../opengl/util/pngj/chunks/PngChunkSingle.java | 43 + .../opengl/util/pngj/chunks/PngChunkSkipped.java | 41 + .../opengl/util/pngj/chunks/PngChunkTEXT.java | 32 +- .../opengl/util/pngj/chunks/PngChunkTIME.java | 38 +- .../opengl/util/pngj/chunks/PngChunkTRNS.java | 60 +- .../opengl/util/pngj/chunks/PngChunkTextVar.java | 10 +- .../opengl/util/pngj/chunks/PngChunkUNKNOWN.java | 21 +- .../opengl/util/pngj/chunks/PngChunkZTXT.java | 28 +- .../opengl/util/pngj/chunks/PngMetadata.java | 198 +++-- .../classes/jogamp/opengl/util/pngj/package.html | 5 +- 51 files changed, 4060 insertions(+), 1609 deletions(-) create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/ImageLineHelper.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/ImageLines.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngDeinterlacer.java delete mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngHelperInternal.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngjExceptionInternal.java delete mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkPredicate.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksList.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksListForWrite.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkMultiple.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkOFFS.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSTER.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSingle.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSkipped.java (limited to 'src/jogl/classes/jogamp/opengl') diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java b/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java index a34f73ab2..e88a95a33 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java @@ -30,17 +30,22 @@ public enum FilterType { */ FILTER_DEFAULT(-1), /** - * Aggresive strategy: select one of the above filters trying each of the filters (this is done every 8 rows) + * Aggressive strategy: select one of the above filters trying each of the filters (every 8 rows) */ FILTER_AGGRESSIVE(-2), + /** + * Very aggressive strategy: select one of the above filters trying each of the filters (for every row!) + */ + FILTER_VERYAGGRESSIVE(-3), /** * Uses all fiters, one for lines, cyciclally. Only for tests. */ - FILTER_ALTERNATE(-3), + FILTER_CYCLIC(-50), + /** - * Aggresive strategy: select one of the above filters trying each of the filters (this is done for every row!) + * Not specified, placeholder for unknown or NA filters. */ - FILTER_VERYAGGRESSIVE(-4), ; + FILTER_UNKNOWN(-100), ; public final int val; private FilterType(int val) { @@ -55,40 +60,4 @@ public enum FilterType { return null; } - public static int unfilterRowNone(int r) { - return (int) (r & 0xFF); - } - - public static int unfilterRowSub(int r, int left) { - return ((int) (r + left) & 0xFF); - } - - public static int unfilterRowUp(int r, int up) { - return ((int) (r + up) & 0xFF); - } - - public static int unfilterRowAverage(int r, int left, int up) { - return (r + (left + up) / 2) & 0xFF; - } - - public static int unfilterRowPaeth(int r, int a, int b, int c) { // a = left, b = above, c = upper left - return (r + filterPaethPredictor(a, b, c)) & 0xFF; - } - - public static int filterPaethPredictor(int a, int b, int c) { - // from http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html - // a = left, b = above, c = upper left - final int p = a + b - c;// ; initial estimate - final int pa = p >= a ? p - a : a - p; - final int pb = p >= b ? p - b : b - p; - final int pc = p >= c ? p - c : c - p; - // ; return nearest of a,b,c, - // ; breaking ties in order a,b,c. - if (pa <= pb && pa <= pc) - return a; - else if (pb <= pc) - return b; - else - return c; - } } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/FilterWriteStrategy.java b/src/jogl/classes/jogamp/opengl/util/pngj/FilterWriteStrategy.java index 27586b292..79eed8f85 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/FilterWriteStrategy.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/FilterWriteStrategy.java @@ -1,7 +1,7 @@ package jogamp.opengl.util.pngj; /** - * Manages the writer strategy for selecting the internal png "filter" + * Manages the writer strategy for selecting the internal png predictor filter */ class FilterWriteStrategy { private static final int COMPUTE_STATS_EVERY_N_LINES = 8; @@ -89,7 +89,7 @@ class FilterWriteStrategy { } } } - if (configuredType == FilterType.FILTER_ALTERNATE) { + if (configuredType == FilterType.FILTER_CYCLIC) { currentType = FilterType.getByVal((currentType.val + 1) % 5); } return currentType; diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/ImageInfo.java b/src/jogl/classes/jogamp/opengl/util/pngj/ImageInfo.java index 2f6b89e9c..26562ef3e 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/ImageInfo.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ImageInfo.java @@ -13,12 +13,12 @@ public class ImageInfo { private static final int MAX_COLS_ROWS_VAL = 1000000; /** - * Image width, in pixels. + * Cols= Image width, in pixels. */ public final int cols; /** - * Image height, in pixels + * Rows= Image height, in pixels */ public final int rows; @@ -29,8 +29,8 @@ public class ImageInfo { public final int bitDepth; /** - * Number of channels, as used internally. This is 3 for RGB, 4 for RGBA, 2 for GA (gray with alpha), 1 for - * grayscales or indexed. + * Number of channels, as used internally: 3 for RGB, 4 for RGBA, 2 for GA (gray with alpha), 1 for grayscale or + * indexed. */ public final int channels; @@ -75,10 +75,14 @@ public class ImageInfo { public final int samplesPerRow; /** - * For internal use only. Samples available for our packed scanline. Equals samplesPerRow if not packed. Elsewhere, - * it's lower + * Amount of "packed samples" : when several samples are stored in a single byte (bitdepth 1,2 4) they are counted + * as one "packed sample". This is less that samplesPerRow only when bitdepth is 1-2-4 (flag packed = true) + *

+ * This equals the number of elements in the scanline array if working with packedMode=true + *

+ * For internal use, client code should rarely access this. */ - final int samplesPerRowP; + public final int samplesPerRowPacked; /** * Short constructor: assumes truecolor (RGB/RGBA) @@ -119,7 +123,7 @@ public class ImageInfo { this.bytesPixel = (bitspPixel + 7) / 8; this.bytesPerRow = (bitspPixel * cols + 7) / 8; this.samplesPerRow = channels * this.cols; - this.samplesPerRowP = packed ? bytesPerRow : samplesPerRow; + this.samplesPerRowPacked = packed ? bytesPerRow : samplesPerRow; // several checks switch (this.bitDepth) { case 1: @@ -147,7 +151,7 @@ public class ImageInfo { public String toString() { return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + channels + ", bitspPixel=" + bitspPixel + ", bytesPixel=" + bytesPixel + ", bytesPerRow=" + bytesPerRow - + ", samplesPerRow=" + samplesPerRow + ", samplesPerRowP=" + samplesPerRowP + ", alpha=" + alpha + + ", samplesPerRow=" + samplesPerRow + ", samplesPerRowP=" + samplesPerRowPacked + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + ", packed=" + packed + "]"; } @@ -157,16 +161,11 @@ public class ImageInfo { int result = 1; result = prime * result + (alpha ? 1231 : 1237); result = prime * result + bitDepth; - result = prime * result + bitspPixel; - result = prime * result + bytesPerRow; - result = prime * result + bytesPixel; result = prime * result + channels; result = prime * result + cols; result = prime * result + (greyscale ? 1231 : 1237); result = prime * result + (indexed ? 1231 : 1237); - result = prime * result + (packed ? 1231 : 1237); result = prime * result + rows; - result = prime * result + samplesPerRow; return result; } @@ -183,12 +182,6 @@ public class ImageInfo { return false; if (bitDepth != other.bitDepth) return false; - if (bitspPixel != other.bitspPixel) - return false; - if (bytesPerRow != other.bytesPerRow) - return false; - if (bytesPixel != other.bytesPixel) - return false; if (channels != other.channels) return false; if (cols != other.cols) @@ -197,12 +190,9 @@ public class ImageInfo { return false; if (indexed != other.indexed) return false; - if (packed != other.packed) - return false; if (rows != other.rows) return false; - if (samplesPerRow != other.samplesPerRow) - return false; return true; } + } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/ImageLine.java b/src/jogl/classes/jogamp/opengl/util/pngj/ImageLine.java index bfbb35b7c..9f8a13230 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/ImageLine.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ImageLine.java @@ -1,6 +1,6 @@ package jogamp.opengl.util.pngj; -import java.util.Arrays; +import jogamp.opengl.util.pngj.ImageLineHelper.ImageLineStats; /** * Lightweight wrapper for an image scanline, used for read and write. @@ -20,26 +20,87 @@ public class ImageLine { /** * The 'scanline' is an array of integers, corresponds to an image line (row). *

- * Except for 'packed' formats (gray/indexed with 1-2-4 bitdepth) each int is a "sample" (one for channel), (0-255 - * or 0-65535) in the respective PNG sequence sequence : (R G B R G B...) or (R G B A R G B A...) or (g g g ...) or - * ( i i i) (palette index) + * Except for 'packed' formats (gray/indexed with 1-2-4 bitdepth) each int is a "sample" (one for + * channel), (0-255 or 0-65535) in the corresponding PNG sequence: R G B R G B... or + * R G B A R G B A... + * or g g g ... or i i i (palette index) *

- * For bitdepth 1/2/4 , each element is a PACKED byte! To get an unpacked copy, see tf_pack() and its - * inverse tf_unpack() + * For bitdepth=1/2/4 , and if samplesUnpacked=false, each value is a PACKED byte! *

- * To convert a indexed line to RGB balues, see ImageLineHelper.tf_palIdx2RGB() (can't do the reverse) + * To convert a indexed line to RGB balues, see ImageLineHelper.palIdx2RGB() (you can't do the reverse) */ - public final int[] scanline; // see explanation above!! + public final int[] scanline; + /** + * Same as {@link #scanline}, but with one byte per sample. Only one of scanline and scanlineb is valid - this + * depends on {@link #sampleType} + */ + public final byte[] scanlineb; protected FilterType filterUsed; // informational ; only filled by the reader - public final int channels; // copied from imgInfo, more handy - public final int bitDepth; // copied from imgInfo, more handy + final int channels; // copied from imgInfo, more handy + final int bitDepth; // copied from imgInfo, more handy + final int elementsPerRow; // = imgInfo.samplePerRowPacked, if packed:imgInfo.samplePerRow elswhere + + public enum SampleType { + INT, // 4 bytes per sample + // SHORT, // 2 bytes per sample + BYTE // 1 byte per sample + } + + /** + * tells if we are using BYTE or INT to store the samples. + */ + public final SampleType sampleType; + + /** + * true: each element of the scanline array represents a sample always, even for internally packed PNG formats + * + * false: if the original image was of packed type (bit depth less than 8) we keep samples packed in a single array + * element + */ + public final boolean samplesUnpacked; + /** + * default mode: INT packed + */ public ImageLine(ImageInfo imgInfo) { + this(imgInfo, SampleType.INT, false); + } + + /** + * + * @param imgInfo + * Inmutable ImageInfo, basic parameter of the image we are reading or writing + * @param stype + * INT or BYTE : this determines which scanline is the really used one + * @param unpackedMode + * If true, we use unpacked format, even for packed original images + * + */ + public ImageLine(ImageInfo imgInfo, SampleType stype, boolean unpackedMode) { + this(imgInfo, stype, unpackedMode, null, null); + } + + /** + * If a preallocated array is passed, the copy is shallow + */ + ImageLine(ImageInfo imgInfo, SampleType stype, boolean unpackedMode, int[] sci, byte[] scb) { this.imgInfo = imgInfo; channels = imgInfo.channels; - scanline = new int[imgInfo.samplesPerRowP]; - this.bitDepth = imgInfo.bitDepth; + bitDepth = imgInfo.bitDepth; + filterUsed = FilterType.FILTER_UNKNOWN; + this.sampleType = stype; + this.samplesUnpacked = unpackedMode || !imgInfo.packed; + elementsPerRow = this.samplesUnpacked ? imgInfo.samplesPerRow : imgInfo.samplesPerRowPacked; + if (stype == SampleType.INT) { + scanline = sci != null ? sci : new int[elementsPerRow]; + scanlineb = null; + } else if (stype == SampleType.BYTE) { + scanlineb = scb != null ? scb : new byte[elementsPerRow]; + scanline = null; + } else + throw new PngjExceptionInternal("bad ImageLine initialization"); + this.rown = -1; } /** This row number inside the image (0 is top) */ @@ -47,129 +108,213 @@ public class ImageLine { return rown; } - /** Increments row number */ - public void incRown() { - this.rown++; - } - - /** Sets row number */ + /** Sets row number (0 : Rows-1) */ public void setRown(int n) { this.rown = n; } - /** Sets scanline, making copy from passed array */ - public void setScanLine(int[] b) { - System.arraycopy(b, 0, scanline, 0, scanline.length); + /* + * Unpacks scanline (for bitdepth 1-2-4) + * + * Arrays must be prealocated. src : samplesPerRowPacked dst : samplesPerRow + * + * This usually works in place (with src==dst and length=samplesPerRow)! + * + * If not, you should only call this only when necesary (bitdepth <8) + * + * If scale==true, it scales the value (just a bit shift) towards 0-255. + */ + static void unpackInplaceInt(final ImageInfo iminfo, final int[] src, final int[] dst, final boolean scale) { + final int bitDepth = iminfo.bitDepth; + if (bitDepth >= 8) + return; // nothing to do + final int mask0 = ImageLineHelper.getMaskForPackedFormatsLs(bitDepth); + final int scalefactor = 8 - bitDepth; + final int offset0 = 8 * iminfo.samplesPerRowPacked - bitDepth * iminfo.samplesPerRow; + int mask, offset, v; + if (offset0 != 8) { + mask = mask0 << offset0; + offset = offset0; // how many bits to shift the mask to the right to recover mask0 + } else { + mask = mask0; + offset = 0; + } + for (int j = iminfo.samplesPerRow - 1, i = iminfo.samplesPerRowPacked - 1; j >= 0; j--) { + v = (src[i] & mask) >> offset; + if (scale) + v <<= scalefactor; + dst[j] = v; + mask <<= bitDepth; + offset += bitDepth; + if (offset == 8) { + mask = mask0; + offset = 0; + i--; + } + } } - /** - * Returns a copy from scanline, in byte array. + /* + * Unpacks scanline (for bitdepth 1-2-4) * - * You can (OPTIONALLY) pass an preallocated array to use. - **/ - public int[] getScanLineCopy(int[] b) { - if (b == null || b.length < scanline.length) - b = new int[scanline.length]; - System.arraycopy(scanline, 0, b, 0, scanline.length); - return b; + * Arrays must be prealocated. src : samplesPerRow dst : samplesPerRowPacked + * + * This usually works in place (with src==dst and length=samplesPerRow)! If not, you should only call this only when + * necesary (bitdepth <8) + * + * The trailing elements are trash + * + * + * If scale==true, it scales the value (just a bit shift) towards 0-255. + */ + static void packInplaceInt(final ImageInfo iminfo, final int[] src, final int[] dst, final boolean scaled) { + final int bitDepth = iminfo.bitDepth; + if (bitDepth >= 8) + return; // nothing to do + final int mask0 = ImageLineHelper.getMaskForPackedFormatsLs(bitDepth); + final int scalefactor = 8 - bitDepth; + final int offset0 = 8 - bitDepth; + int v, v0; + int offset = 8 - bitDepth; + v0 = src[0]; // first value is special for in place + dst[0] = 0; + if (scaled) + v0 >>= scalefactor; + v0 = ((v0 & mask0) << offset); + for (int i = 0, j = 0; j < iminfo.samplesPerRow; j++) { + v = src[j]; + if (scaled) + v >>= scalefactor; + dst[i] |= ((v & mask0) << offset); + offset -= bitDepth; + if (offset < 0) { + offset = offset0; + i++; + dst[i] = 0; + } + } + dst[0] |= v0; } - /** - * Unpacks scanline (for bitdepth 1-2-4) into buffer. - *

- * You can (OPTIONALLY) pass an preallocated array to use. - *

- * If scale==TRUE scales the value (just a bit shift). - */ - public int[] tf_unpack(int[] buf, boolean scale) { - int len = scanline.length; - if (bitDepth == 1) - len *= 8; - else if (bitDepth == 2) - len *= 4; - else if (bitDepth == 4) - len *= 2; - if (buf == null) - buf = new int[len]; + static void unpackInplaceByte(final ImageInfo iminfo, final byte[] src, final byte[] dst, final boolean scale) { + final int bitDepth = iminfo.bitDepth; if (bitDepth >= 8) - System.arraycopy(scanline, 0, buf, 0, scanline.length); - else { - int mask, offset, v; - int mask0 = getMaskForPackedFormats(); - int offset0 = 8 - bitDepth; + return; // nothing to do + final int mask0 = ImageLineHelper.getMaskForPackedFormatsLs(bitDepth); + final int scalefactor = 8 - bitDepth; + final int offset0 = 8 * iminfo.samplesPerRowPacked - bitDepth * iminfo.samplesPerRow; + int mask, offset, v; + if (offset0 != 8) { + mask = mask0 << offset0; + offset = offset0; // how many bits to shift the mask to the right to recover mask0 + } else { mask = mask0; - offset = offset0; - for (int i = 0, j = 0; i < len; i++) { - v = (scanline[j] & mask) >> offset; - if (scale) - v <<= offset0; - buf[i] = v; - mask = mask >> bitDepth; - offset -= bitDepth; - if (mask == 0) { // new byte in source - mask = mask0; - offset = offset0; - j++; - } + offset = 0; + } + for (int j = iminfo.samplesPerRow - 1, i = iminfo.samplesPerRowPacked - 1; j >= 0; j--) { + v = (src[i] & mask) >> offset; + if (scale) + v <<= scalefactor; + dst[j] = (byte) v; + mask <<= bitDepth; + offset += bitDepth; + if (offset == 8) { + mask = mask0; + offset = 0; + i--; } } - return buf; } - /** - * Packs scanline (for bitdepth 1-2-4) from buffer. - *

- * If scale==TRUE scales the value (just a bit shift). - */ - public void tf_pack(int[] buf, boolean scale) { // writes scanline - int len = scanline.length; - if (bitDepth == 1) - len *= 8; - else if (bitDepth == 2) - len *= 4; - else if (bitDepth == 4) - len *= 2; + /** size original: samplesPerRow sizeFinal: samplesPerRowPacked (trailing elements are trash!) **/ + static void packInplaceByte(final ImageInfo iminfo, final byte[] src, final byte[] dst, final boolean scaled) { + final int bitDepth = iminfo.bitDepth; if (bitDepth >= 8) - System.arraycopy(buf, 0, scanline, 0, scanline.length); - else { - int offset0 = 8 - bitDepth; - int mask0 = getMaskForPackedFormats() >> offset0; - int offset, v; - offset = offset0; - Arrays.fill(scanline, 0); - for (int i = 0, j = 0; i < len; i++) { - v = buf[i]; - if (scale) - v >>= offset0; - v = (v & mask0) << offset; - scanline[j] |= v; - offset -= bitDepth; - if (offset < 0) { // new byte in scanline - offset = offset0; - j++; - } + return; // nothing to do + final int mask0 = ImageLineHelper.getMaskForPackedFormatsLs(bitDepth); + final int scalefactor = 8 - bitDepth; + final int offset0 = 8 - bitDepth; + int v, v0; + int offset = 8 - bitDepth; + v0 = src[0]; // first value is special + dst[0] = 0; + if (scaled) + v0 >>= scalefactor; + v0 = ((v0 & mask0) << offset); + for (int i = 0, j = 0; j < iminfo.samplesPerRow; j++) { + v = src[j]; + if (scaled) + v >>= scalefactor; + dst[i] |= ((v & mask0) << offset); + offset -= bitDepth; + if (offset < 0) { + offset = offset0; + i++; + dst[i] = 0; } } + dst[0] |= v0; } - private int getMaskForPackedFormats() { // Utility function for pacj/unpack - if (bitDepth == 1) - return 0x80; - if (bitDepth == 2) - return 0xc0; - if (bitDepth == 4) - return 0xf0; - throw new RuntimeException("?"); + /** + * Creates a new ImageLine similar to this, but unpacked + * + * The caller must be sure that the original was really packed + */ + public ImageLine unpackToNewImageLine() { + ImageLine newline = new ImageLine(imgInfo, sampleType, true); + if (sampleType == SampleType.INT) + unpackInplaceInt(imgInfo, scanline, newline.scanline, false); + else + unpackInplaceByte(imgInfo, scanlineb, newline.scanlineb, false); + return newline; + } + + /** + * Creates a new ImageLine similar to this, but packed + * + * The caller must be sure that the original was really unpacked + */ + public ImageLine packToNewImageLine() { + ImageLine newline = new ImageLine(imgInfo, sampleType, false); + if (sampleType == SampleType.INT) + packInplaceInt(imgInfo, scanline, newline.scanline, false); + else + packInplaceByte(imgInfo, scanlineb, newline.scanlineb, false); + return newline; } public FilterType getFilterUsed() { return filterUsed; } + public void setFilterUsed(FilterType ft) { + filterUsed = ft; + } + + public int[] getScanlineInt() { + return scanline; + } + + public byte[] getScanlineByte() { + return scanlineb; + } + /** * Basic info */ public String toString() { return "row=" + rown + " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length; } + + /** + * Prints some statistics - just for debugging + */ + public static void showLineInfo(ImageLine line) { + System.out.println(line); + ImageLineStats stats = new ImageLineHelper.ImageLineStats(line); + System.out.println(stats); + System.out.println(ImageLineHelper.infoFirstLastPixels(line)); + } + } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/ImageLineHelper.java b/src/jogl/classes/jogamp/opengl/util/pngj/ImageLineHelper.java new file mode 100644 index 000000000..98f235662 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ImageLineHelper.java @@ -0,0 +1,318 @@ +package jogamp.opengl.util.pngj; + +import jogamp.opengl.util.pngj.ImageLine.SampleType; +import jogamp.opengl.util.pngj.chunks.PngChunkPLTE; +import jogamp.opengl.util.pngj.chunks.PngChunkTRNS; + +/** + * Bunch of utility static methods to process/analyze an image line at the pixel level. + *

+ * Not essential at all, some methods are probably to be removed if future releases. + *

+ * WARNING: most methods for getting/setting values work currently only for integer base imageLines + */ +public class ImageLineHelper { + + private final static double BIG_VALUE = Double.MAX_VALUE * 0.5; + + private final static double BIG_VALUE_NEG = Double.MAX_VALUE * (-0.5); + + /** + * Given an indexed line with a palette, unpacks as a RGB array, or RGBA if a non nul PngChunkTRNS chunk is passed + * + * @param line + * ImageLine as returned from PngReader + * @param pal + * Palette chunk + * @param buf + * Preallocated array, optional + * @return R G B (A), one sample 0-255 per array element. Ready for pngw.writeRowInt() + */ + public static int[] palette2rgb(ImageLine line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf) { + boolean isalpha = trns != null; + int channels = isalpha ? 4 : 3; + int nsamples = line.imgInfo.cols * channels; + if (buf == null || buf.length < nsamples) + buf = new int[nsamples]; + if (!line.samplesUnpacked) + line = line.unpackToNewImageLine(); + boolean isbyte = line.sampleType == SampleType.BYTE; + int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0; + for (int c = 0; c < line.imgInfo.cols; c++) { + int index = isbyte ? (line.scanlineb[c] & 0xFF) : line.scanline[c]; + pal.getEntryRgb(index, buf, c * channels); + if (isalpha) { + int alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255; + buf[c * channels + 3] = alpha; + } + } + return buf; + } + + public static int[] palette2rgb(ImageLine line, PngChunkPLTE pal, int[] buf) { + return palette2rgb(line, pal, null, buf); + } + + /** what follows is pretty uninteresting/untested/obsolete, subject to change */ + /** + * Just for basic info or debugging. Shows values for first and last pixel. Does not include alpha + */ + public static String infoFirstLastPixels(ImageLine line) { + return line.imgInfo.channels == 1 ? String.format("first=(%d) last=(%d)", line.scanline[0], + line.scanline[line.scanline.length - 1]) : String.format("first=(%d %d %d) last=(%d %d %d)", + line.scanline[0], line.scanline[1], line.scanline[2], line.scanline[line.scanline.length + - line.imgInfo.channels], line.scanline[line.scanline.length - line.imgInfo.channels + 1], + line.scanline[line.scanline.length - line.imgInfo.channels + 2]); + } + + public static String infoFull(ImageLine line) { + ImageLineStats stats = new ImageLineStats(line); + return "row=" + line.getRown() + " " + stats.toString() + "\n " + infoFirstLastPixels(line); + } + + /** + * Computes some statistics for the line. Not very efficient or elegant, mainly for tests. Only for RGB/RGBA Outputs + * values as doubles (0.0 - 1.0) + */ + static class ImageLineStats { + public double[] prom = { 0.0, 0.0, 0.0, 0.0 }; // channel averages + public double[] maxv = { BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE_NEG }; // maximo + public double[] minv = { BIG_VALUE, BIG_VALUE, BIG_VALUE, BIG_VALUE }; + public double promlum = 0.0; // maximum global (luminance) + public double maxlum = BIG_VALUE_NEG; // max luminance + public double minlum = BIG_VALUE; + public double[] maxdif = { BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE }; // maxima + public final int channels; // diferencia + + public String toString() { + return channels == 3 ? String.format( + "prom=%.1f (%.1f %.1f %.1f) max=%.1f (%.1f %.1f %.1f) min=%.1f (%.1f %.1f %.1f)", promlum, prom[0], + prom[1], prom[2], maxlum, maxv[0], maxv[1], maxv[2], minlum, minv[0], minv[1], minv[2]) + + String.format(" maxdif=(%.1f %.1f %.1f)", maxdif[0], maxdif[1], maxdif[2]) : String.format( + "prom=%.1f (%.1f %.1f %.1f %.1f) max=%.1f (%.1f %.1f %.1f %.1f) min=%.1f (%.1f %.1f %.1f %.1f)", + promlum, prom[0], prom[1], prom[2], prom[3], maxlum, maxv[0], maxv[1], maxv[2], maxv[3], minlum, + minv[0], minv[1], minv[2], minv[3]) + + String.format(" maxdif=(%.1f %.1f %.1f %.1f)", maxdif[0], maxdif[1], maxdif[2], maxdif[3]); + } + + public ImageLineStats(ImageLine line) { + this.channels = line.channels; + if (line.channels < 3) + throw new PngjException("ImageLineStats only works for RGB - RGBA"); + int ch = 0; + double lum, x, d; + for (int i = 0; i < line.imgInfo.cols; i++) { + lum = 0; + for (ch = channels - 1; ch >= 0; ch--) { + x = int2double(line, line.scanline[i * channels]); + if (ch < 3) + lum += x; + prom[ch] += x; + if (x > maxv[ch]) + maxv[ch] = x; + if (x < minv[ch]) + minv[ch] = x; + if (i >= channels) { + d = Math.abs(x - int2double(line, line.scanline[i - channels])); + if (d > maxdif[ch]) + maxdif[ch] = d; + } + } + promlum += lum; + if (lum > maxlum) + maxlum = lum; + if (lum < minlum) + minlum = lum; + } + for (ch = 0; ch < channels; ch++) { + prom[ch] /= line.imgInfo.cols; + } + promlum /= (line.imgInfo.cols * 3.0); + maxlum /= 3.0; + minlum /= 3.0; + } + } + + /** + * integer packed R G B only for bitdepth=8! (does not check!) + * + **/ + public static int getPixelRGB8(ImageLine line, int column) { + int offset = column * line.channels; + return (line.scanline[offset] << 16) + (line.scanline[offset + 1] << 8) + (line.scanline[offset + 2]); + } + + public static int getPixelARGB8(ImageLine line, int column) { + int offset = column * line.channels; + return (line.scanline[offset + 3] << 24) + (line.scanline[offset] << 16) + (line.scanline[offset + 1] << 8) + + (line.scanline[offset + 2]); + } + + public static void setPixelsRGB8(ImageLine line, int[] rgb) { + for (int i = 0, j = 0; i < line.imgInfo.cols; i++) { + line.scanline[j++] = ((rgb[i] >> 16) & 0xFF); + line.scanline[j++] = ((rgb[i] >> 8) & 0xFF); + line.scanline[j++] = ((rgb[i] & 0xFF)); + } + } + + public static void setPixelRGB8(ImageLine line, int col, int r, int g, int b) { + col *= line.channels; + line.scanline[col++] = r; + line.scanline[col++] = g; + line.scanline[col] = b; + } + + public static void setPixelRGB8(ImageLine line, int col, int rgb) { + setPixelRGB8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); + } + + public static void setPixelsRGBA8(ImageLine line, int[] rgb) { + for (int i = 0, j = 0; i < line.imgInfo.cols; i++) { + line.scanline[j++] = ((rgb[i] >> 16) & 0xFF); + line.scanline[j++] = ((rgb[i] >> 8) & 0xFF); + line.scanline[j++] = ((rgb[i] & 0xFF)); + line.scanline[j++] = ((rgb[i] >> 24) & 0xFF); + } + } + + public static void setPixelRGBA8(ImageLine line, int col, int r, int g, int b, int a) { + col *= line.channels; + line.scanline[col++] = r; + line.scanline[col++] = g; + line.scanline[col++] = b; + line.scanline[col] = a; + } + + public static void setPixelRGBA8(ImageLine line, int col, int rgb) { + setPixelRGBA8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, (rgb >> 24) & 0xFF); + } + + public static void setValD(ImageLine line, int i, double d) { + line.scanline[i] = double2int(line, d); + } + + public static int interpol(int a, int b, int c, int d, double dx, double dy) { + // a b -> x (0-1) + // c d + // + double e = a * (1.0 - dx) + b * dx; + double f = c * (1.0 - dx) + d * dx; + return (int) (e * (1 - dy) + f * dy + 0.5); + } + + public static double int2double(ImageLine line, int p) { + return line.bitDepth == 16 ? p / 65535.0 : p / 255.0; + // TODO: replace my multiplication? check for other bitdepths + } + + public static double int2doubleClamped(ImageLine line, int p) { + // TODO: replace my multiplication? + double d = line.bitDepth == 16 ? p / 65535.0 : p / 255.0; + return d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + } + + public static int double2int(ImageLine line, double d) { + d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + return line.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); // + } + + public static int double2intClamped(ImageLine line, double d) { + d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + return line.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); // + } + + public static int clampTo_0_255(int i) { + return i > 255 ? 255 : (i < 0 ? 0 : i); + } + + public static int clampTo_0_65535(int i) { + return i > 65535 ? 65535 : (i < 0 ? 0 : i); + } + + public static int clampTo_128_127(int x) { + return x > 127 ? 127 : (x < -128 ? -128 : x); + } + + /** + * Unpacks scanline (for bitdepth 1-2-4) into a array int[] + *

+ * You can (OPTIONALLY) pass an preallocated array, that will be filled and returned. If null, it will be allocated + *

+ * If scale==true, it scales the value (just a bit shift) towards 0-255. + *

+ * You probably should use {@link ImageLine#unpackToNewImageLine()} + * + */ + public static int[] unpack(ImageInfo imgInfo, int[] src, int[] dst, boolean scale) { + int len1 = imgInfo.samplesPerRow; + int len0 = imgInfo.samplesPerRowPacked; + if (dst == null || dst.length < len1) + dst = new int[len1]; + if (imgInfo.packed) + ImageLine.unpackInplaceInt(imgInfo, src, dst, scale); + else + System.arraycopy(src, 0, dst, 0, len0); + return dst; + } + + public static byte[] unpack(ImageInfo imgInfo, byte[] src, byte[] dst, boolean scale) { + int len1 = imgInfo.samplesPerRow; + int len0 = imgInfo.samplesPerRowPacked; + if (dst == null || dst.length < len1) + dst = new byte[len1]; + if (imgInfo.packed) + ImageLine.unpackInplaceByte(imgInfo, src, dst, scale); + else + System.arraycopy(src, 0, dst, 0, len0); + return dst; + } + + /** + * Packs scanline (for bitdepth 1-2-4) from array into the scanline + *

+ * If scale==true, it scales the value (just a bit shift). + * + * You probably should use {@link ImageLine#packToNewImageLine()} + */ + public static int[] pack(ImageInfo imgInfo, int[] src, int[] dst, boolean scale) { + int len0 = imgInfo.samplesPerRowPacked; + if (dst == null || dst.length < len0) + dst = new int[len0]; + if (imgInfo.packed) + ImageLine.packInplaceInt(imgInfo, src, dst, scale); + else + System.arraycopy(src, 0, dst, 0, len0); + return dst; + } + + public static byte[] pack(ImageInfo imgInfo, byte[] src, byte[] dst, boolean scale) { + int len0 = imgInfo.samplesPerRowPacked; + if (dst == null || dst.length < len0) + dst = new byte[len0]; + if (imgInfo.packed) + ImageLine.packInplaceByte(imgInfo, src, dst, scale); + else + System.arraycopy(src, 0, dst, 0, len0); + return dst; + } + + static int getMaskForPackedFormats(int bitDepth) { // Utility function for pack/unpack + if (bitDepth == 4) + return 0xf0; + else if (bitDepth == 2) + return 0xc0; + else + return 0x80; // bitDepth == 1 + } + + static int getMaskForPackedFormatsLs(int bitDepth) { // Utility function for pack/unpack + if (bitDepth == 4) + return 0x0f; + else if (bitDepth == 2) + return 0x03; + else + return 0x01; // bitDepth == 1 + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/ImageLines.java b/src/jogl/classes/jogamp/opengl/util/pngj/ImageLines.java new file mode 100644 index 000000000..1e0ab746a --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ImageLines.java @@ -0,0 +1,101 @@ +package jogamp.opengl.util.pngj; + +import jogamp.opengl.util.pngj.ImageLine.SampleType; + +/** + * Wraps in a matrix a set of image rows, not necessarily contiguous - but equispaced. + * + * The fields mirrors those of {@link ImageLine}, and you can access each row as a ImageLine backed by the matrix row, + * see {@link #getImageLineAtMatrixRow(int)} + */ +public class ImageLines { + + public final ImageInfo imgInfo; + public final int channels; + public final int bitDepth; + public final SampleType sampleType; + public final boolean samplesUnpacked; + public final int elementsPerRow; + public final int rowOffset; + public final int nRows; + public final int rowStep; + public final int[][] scanlines; + public final byte[][] scanlinesb; + + /** + * Allocates a matrix to store {@code nRows} image rows. See {@link ImageLine} and {@link PngReader#readRowsInt()} + * {@link PngReader#readRowsByte()} + * + * @param imgInfo + * @param stype + * @param unpackedMode + * @param rowOffset + * @param nRows + * @param rowStep + */ + public ImageLines(ImageInfo imgInfo, SampleType stype, boolean unpackedMode, int rowOffset, int nRows, int rowStep) { + this.imgInfo = imgInfo; + channels = imgInfo.channels; + bitDepth = imgInfo.bitDepth; + this.sampleType = stype; + this.samplesUnpacked = unpackedMode || !imgInfo.packed; + elementsPerRow = unpackedMode ? imgInfo.samplesPerRow : imgInfo.samplesPerRowPacked; + this.rowOffset = rowOffset; + this.nRows = nRows; + this.rowStep = rowStep; + if (stype == SampleType.INT) { + scanlines = new int[nRows][elementsPerRow]; + scanlinesb = null; + } else if (stype == SampleType.BYTE) { + scanlinesb = new byte[nRows][elementsPerRow]; + scanlines = null; + } else + throw new PngjExceptionInternal("bad ImageLine initialization"); + } + + /** + * Warning: this always returns a valid matrix row (clamping on 0 : nrows-1, and rounding down) Eg: + * rowOffset=4,rowStep=2 imageRowToMatrixRow(17) returns 6 , imageRowToMatrixRow(1) returns 0 + */ + public int imageRowToMatrixRow(int imrow) { + int r = (imrow - rowOffset) / rowStep; + return r < 0 ? 0 : (r < nRows ? r : nRows - 1); + } + + /** + * Same as imageRowToMatrixRow, but returns negative if invalid + */ + public int imageRowToMatrixRowStrict(int imrow) { + imrow -= rowOffset; + int mrow = imrow >= 0 && imrow % rowStep == 0 ? imrow / rowStep : -1; + return mrow < nRows ? mrow : -1; + } + + /** + * Converts from matrix row number (0 : nRows-1) to image row number + * + * @param mrow + * Matrix row number + * @return Image row number. Invalid only if mrow is invalid + */ + public int matrixRowToImageRow(int mrow) { + return mrow * rowStep + rowOffset; + } + + /** + * Returns a ImageLine is backed by the matrix, no allocation done + * + * @param mrow + * Matrix row, from 0 to nRows This is not necessarily the image row, see + * {@link #imageRowToMatrixRow(int)} and {@link #matrixRowToImageRow(int)} + * @return A new ImageLine, backed by the matrix, with the correct ('real') rownumber + */ + public ImageLine getImageLineAtMatrixRow(int mrow) { + if (mrow < 0 || mrow > nRows) + throw new PngjException("Bad row " + mrow + ". Should be positive and less than " + nRows); + ImageLine imline = sampleType == SampleType.INT ? new ImageLine(imgInfo, sampleType, samplesUnpacked, + scanlines[mrow], null) : new ImageLine(imgInfo, sampleType, samplesUnpacked, null, scanlinesb[mrow]); + imline.setRown(matrixRowToImageRow(mrow)); + return imline; + } +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngDeinterlacer.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngDeinterlacer.java new file mode 100644 index 000000000..e099c4f6a --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngDeinterlacer.java @@ -0,0 +1,277 @@ +package jogamp.opengl.util.pngj; + +import java.util.Random; + +// you really dont' want to peek inside this +class PngDeinterlacer { + private final ImageInfo imi; + private int pass; // 1-7 + private int rows, cols, dY, dX, oY, oX, oXsamples, dXsamples; // at current pass + // current row in the virtual subsampled image; this incrementes from 0 to cols/dy 7 times + private int currRowSubimg = -1; + // in the real image, this will cycle from 0 to im.rows in different steps, 7 times + private int currRowReal = -1; + + private final int packedValsPerPixel; + private final int packedMask; + private final int packedShift; + + private int[][] imageInt; // FULL image -only used for PngWriter as temporary storage + private short[][] imageShort; + private byte[][] imageByte; + + PngDeinterlacer(ImageInfo iminfo) { + this.imi = iminfo; + pass = 0; + if (imi.packed) { + packedValsPerPixel = 8 / imi.bitDepth; + packedShift = imi.bitDepth; + if (imi.bitDepth == 1) + packedMask = 0x80; + else if (imi.bitDepth == 2) + packedMask = 0xc0; + else + packedMask = 0xf0; + } else { + packedMask = packedShift = packedValsPerPixel = 1;// dont care + } + setPass(1); + setRow(0); + } + + /** this refers to the row currRowSubimg */ + void setRow(int n) { + currRowSubimg = n; + currRowReal = n * dY + oY; + if (currRowReal < 0 || currRowReal >= imi.rows) + throw new PngjExceptionInternal("bad row - this should not happen"); + } + + void setPass(int p) { + if (this.pass == p) + return; + pass = p; + switch (pass) { + case 1: + dY = dX = 8; + oX = oY = 0; + break; + case 2: + dY = dX = 8; + oX = 4; + oY = 0; + break; + case 3: + dX = 4; + dY = 8; + oX = 0; + oY = 4; + break; + case 4: + dX = dY = 4; + oX = 2; + oY = 0; + break; + case 5: + dX = 2; + dY = 4; + oX = 0; + oY = 2; + break; + case 6: + dX = dY = 2; + oX = 1; + oY = 0; + break; + case 7: + dX = 1; + dY = 2; + oX = 0; + oY = 1; + break; + default: + throw new PngjExceptionInternal("bad interlace pass" + pass); + } + rows = (imi.rows - oY) / dY + 1; + if ((rows - 1) * dY + oY >= imi.rows) + rows--; // can be 0 + cols = (imi.cols - oX) / dX + 1; + if ((cols - 1) * dX + oX >= imi.cols) + cols--; // can be 0 + if (cols == 0) + rows = 0; // really... + dXsamples = dX * imi.channels; + oXsamples = oX * imi.channels; + } + + // notice that this is a "partial" deinterlace, it will be called several times for the same row! + void deinterlaceInt(int[] src, int[] dst, boolean readInPackedFormat) { + if (!(imi.packed && readInPackedFormat)) + for (int i = 0, j = oXsamples; i < cols * imi.channels; i += imi.channels, j += dXsamples) + for (int k = 0; k < imi.channels; k++) + dst[j + k] = src[i + k]; + else + deinterlaceIntPacked(src, dst); + } + + // interlaced+packed = monster; this is very clumsy! + private void deinterlaceIntPacked(int[] src, int[] dst) { + int spos, smod, smask; // source byte position, bits to shift to left (01,2,3,4 + int tpos, tmod, p, d; + spos = 0; + smask = packedMask; + smod = -1; + // can this really work? + for (int i = 0, j = oX; i < cols; i++, j += dX) { + spos = i / packedValsPerPixel; + smod += 1; + if (smod >= packedValsPerPixel) + smod = 0; + smask >>= packedShift; // the source mask cycles + if (smod == 0) + smask = packedMask; + tpos = j / packedValsPerPixel; + tmod = j % packedValsPerPixel; + p = src[spos] & smask; + d = tmod - smod; + if (d > 0) + p >>= (d * packedShift); + else if (d < 0) + p <<= ((-d) * packedShift); + dst[tpos] |= p; + } + } + + // yes, duplication of code is evil, normally + void deinterlaceByte(byte[] src, byte[] dst, boolean readInPackedFormat) { + if (!(imi.packed && readInPackedFormat)) + for (int i = 0, j = oXsamples; i < cols * imi.channels; i += imi.channels, j += dXsamples) + for (int k = 0; k < imi.channels; k++) + dst[j + k] = src[i + k]; + else + deinterlacePackedByte(src, dst); + } + + private void deinterlacePackedByte(byte[] src, byte[] dst) { + int spos, smod, smask; // source byte position, bits to shift to left (01,2,3,4 + int tpos, tmod, p, d; + // what the heck are you reading here? I told you would not enjoy this. Try Dostoyevsky or Simone Weil instead + spos = 0; + smask = packedMask; + smod = -1; + // Arrays.fill(dst, 0); + for (int i = 0, j = oX; i < cols; i++, j += dX) { + spos = i / packedValsPerPixel; + smod += 1; + if (smod >= packedValsPerPixel) + smod = 0; + smask >>= packedShift; // the source mask cycles + if (smod == 0) + smask = packedMask; + tpos = j / packedValsPerPixel; + tmod = j % packedValsPerPixel; + p = src[spos] & smask; + d = tmod - smod; + if (d > 0) + p >>= (d * packedShift); + else if (d < 0) + p <<= ((-d) * packedShift); + dst[tpos] |= p; + } + } + + /** + * Is current row the last row for the lass pass?? + */ + boolean isAtLastRow() { + return pass == 7 && currRowSubimg == rows - 1; + } + + /** + * current row number inside the "sub image" + */ + int getCurrRowSubimg() { + return currRowSubimg; + } + + /** + * current row number inside the "real image" + */ + int getCurrRowReal() { + return currRowReal; + } + + /** + * current pass number (1-7) + */ + int getPass() { + return pass; + } + + /** + * How many rows has the current pass? + **/ + int getRows() { + return rows; + } + + /** + * How many columns (pixels) are there in the current row + */ + int getCols() { + return cols; + } + + public int getPixelsToRead() { + return getCols(); + } + + int[][] getImageInt() { + return imageInt; + } + + void setImageInt(int[][] imageInt) { + this.imageInt = imageInt; + } + + short[][] getImageShort() { + return imageShort; + } + + void setImageShort(short[][] imageShort) { + this.imageShort = imageShort; + } + + byte[][] getImageByte() { + return imageByte; + } + + void setImageByte(byte[][] imageByte) { + this.imageByte = imageByte; + } + + static void test() { + Random rand = new Random(); + PngDeinterlacer ih = new PngDeinterlacer(new ImageInfo(rand.nextInt(35) + 1, rand.nextInt(52) + 1, 8, true)); + int np = ih.imi.cols * ih.imi.rows; + System.out.println(ih.imi); + for (int p = 1; p <= 7; p++) { + ih.setPass(p); + for (int row = 0; row < ih.getRows(); row++) { + ih.setRow(row); + int b = ih.getCols(); + np -= b; + System.out.printf("Read %d pixels. Pass:%d Realline:%d cols=%d dX=%d oX=%d last:%b\n", b, ih.pass, + ih.currRowReal, ih.cols, ih.dX, ih.oX, ih.isAtLastRow()); + + } + } + if (np != 0) + throw new PngjExceptionInternal("wtf??" + ih.imi); + } + + public static void main(String[] args) { + test(); + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java deleted file mode 100644 index 1016b1b64..000000000 --- a/src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java +++ /dev/null @@ -1,213 +0,0 @@ -package jogamp.opengl.util.pngj; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.util.HashSet; -import java.util.Set; -import java.util.zip.CRC32; - -/** - * Some utility static methods. - *

- * See also FileHelper (if not sandboxed). - *

- * Client code should rarely need these methods. - */ -public class PngHelper { - /** - * Default charset, used internally by PNG for several things - */ - public static Charset charsetLatin1 = Charset.forName("ISO-8859-1"); - public static Charset charsetUTF8 = Charset.forName("UTF-8"); // only for some chunks - - static boolean DEBUG = false; - - public static int readByte(InputStream is) { - try { - return is.read(); - } catch (IOException e) { - throw new PngjOutputException(e); - } - } - - /** - * -1 if eof - * - * PNG uses "network byte order" - */ - public static int readInt2(InputStream is) { - try { - int b1 = is.read(); - int b2 = is.read(); - if (b1 == -1 || b2 == -1) - return -1; - return (b1 << 8) + b2; - } catch (IOException e) { - throw new PngjInputException("error reading readInt2", e); - } - } - - /** - * -1 if eof - */ - public static int readInt4(InputStream is) { - try { - int b1 = is.read(); - int b2 = is.read(); - int b3 = is.read(); - int b4 = is.read(); - if (b1 == -1 || b2 == -1 || b3 == -1 || b4 == -1) - return -1; - return (b1 << 24) + (b2 << 16) + (b3 << 8) + b4; - } catch (IOException e) { - throw new PngjInputException("error reading readInt4", e); - } - } - - public static int readInt1fromByte(byte[] b, int offset) { - return (b[offset] & 0xff); - } - - public static int readInt2fromBytes(byte[] b, int offset) { - return ((b[offset] & 0xff) << 16) | ((b[offset + 1] & 0xff)); - } - - public static int readInt4fromBytes(byte[] b, int offset) { - return ((b[offset] & 0xff) << 24) | ((b[offset + 1] & 0xff) << 16) | ((b[offset + 2] & 0xff) << 8) - | (b[offset + 3] & 0xff); - } - - public static void writeByte(OutputStream os, byte b) { - try { - os.write(b); - } catch (IOException e) { - throw new PngjOutputException(e); - } - } - - public static void writeInt2(OutputStream os, int n) { - byte[] temp = { (byte) ((n >> 8) & 0xff), (byte) (n & 0xff) }; - writeBytes(os, temp); - } - - public static void writeInt4(OutputStream os, int n) { - byte[] temp = new byte[4]; - writeInt4tobytes(n, temp, 0); - writeBytes(os, temp); - } - - public static void writeInt2tobytes(int n, byte[] b, int offset) { - b[offset] = (byte) ((n >> 8) & 0xff); - b[offset + 1] = (byte) (n & 0xff); - } - - public static void writeInt4tobytes(int n, byte[] b, int offset) { - b[offset] = (byte) ((n >> 24) & 0xff); - b[offset + 1] = (byte) ((n >> 16) & 0xff); - b[offset + 2] = (byte) ((n >> 8) & 0xff); - b[offset + 3] = (byte) (n & 0xff); - } - - /** - * guaranteed to read exactly len bytes. throws error if it cant - */ - public static void readBytes(InputStream is, byte[] b, int offset, int len) { - if (len == 0) - return; - try { - int read = 0; - while (read < len) { - int n = is.read(b, offset + read, len - read); - if (n < 1) - throw new RuntimeException("error reading bytes, " + n + " !=" + len); - read += n; - } - } catch (IOException e) { - throw new PngjInputException("error reading", e); - } - } - - public static void writeBytes(OutputStream os, byte[] b) { - try { - os.write(b); - } catch (IOException e) { - throw new PngjOutputException(e); - } - } - - public static void writeBytes(OutputStream os, byte[] b, int offset, int n) { - try { - os.write(b, offset, n); - } catch (IOException e) { - throw new PngjOutputException(e); - } - } - - public static void logdebug(String msg) { - if (DEBUG) - System.out.println(msg); - } - - public static Set asSet(String... values) { - return new HashSet(java.util.Arrays.asList(values)); - } - - public static Set unionSets(Set set1, Set set2) { - Set s = new HashSet(); - s.addAll(set1); - s.addAll(set2); - return s; - } - - public static Set unionSets(Set set1, Set set2, Set set3) { - Set s = new HashSet(); - s.addAll(set1); - s.addAll(set2); - s.addAll(set3); - return s; - } - - private static final ThreadLocal crcProvider = new ThreadLocal() { - protected CRC32 initialValue() { - return new CRC32(); - } - }; - - /** thread-singleton crc engine */ - public static CRC32 getCRC() { - return crcProvider.get(); - } - - static final byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 }; // png magic - - public static double resMetersToDpi(long res) { - return (double) res * 0.0254; - } - - public static long resDpiToMeters(double dpi) { - return (long) (dpi / 0.0254 + 0.5); - } - - public static int doubleToInt100000(double d) { - return (int) (d * 100000.0 + 0.5); - } - - public static double intToDouble100000(int i) { - return i / 100000.0; - } - - public static int clampTo_0_255(int i) { - return i > 255 ? 255 : (i < 0 ? 0 : i); - } - - public static int clampTo_0_65535(int i) { - return i > 65535 ? 65535 : (i < 0 ? 0 : i); - } - - public static int clampTo_128_127(int x) { - return x > 127 ? 127 : (x < -128 ? -128 : x); - } - -} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngHelperInternal.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngHelperInternal.java new file mode 100644 index 000000000..63edf8d17 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngHelperInternal.java @@ -0,0 +1,264 @@ +package jogamp.opengl.util.pngj; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.zip.CRC32; + +/** + * Some utility static methods for internal use. + *

+ * Client code should not normally use this class + *

+ */ +public class PngHelperInternal { + /** + * Default charset, used internally by PNG for several things + */ + public static Charset charsetLatin1 = Charset.forName("ISO-8859-1"); + /** + * UTF-8 is only used for some chunks + */ + public static Charset charsetUTF8 = Charset.forName("UTF-8"); + + static boolean DEBUG = false; + + /** + * PNG magic bytes + */ + public static byte[] getPngIdSignature() { + return new byte[] { -119, 80, 78, 71, 13, 10, 26, 10 }; + } + + public static int doubleToInt100000(double d) { + return (int) (d * 100000.0 + 0.5); + } + + public static double intToDouble100000(int i) { + return i / 100000.0; + } + + public static int readByte(InputStream is) { + try { + return is.read(); + } catch (IOException e) { + throw new PngjInputException("error reading byte", e); + } + } + + /** + * -1 if eof + * + * PNG uses "network byte order" + */ + public static int readInt2(InputStream is) { + try { + int b1 = is.read(); + int b2 = is.read(); + if (b1 == -1 || b2 == -1) + return -1; + return (b1 << 8) + b2; + } catch (IOException e) { + throw new PngjInputException("error reading readInt2", e); + } + } + + /** + * -1 if eof + */ + public static int readInt4(InputStream is) { + try { + int b1 = is.read(); + int b2 = is.read(); + int b3 = is.read(); + int b4 = is.read(); + if (b1 == -1 || b2 == -1 || b3 == -1 || b4 == -1) + return -1; + return (b1 << 24) + (b2 << 16) + (b3 << 8) + b4; + } catch (IOException e) { + throw new PngjInputException("error reading readInt4", e); + } + } + + public static int readInt1fromByte(byte[] b, int offset) { + return (b[offset] & 0xff); + } + + public static int readInt2fromBytes(byte[] b, int offset) { + return ((b[offset] & 0xff) << 16) | ((b[offset + 1] & 0xff)); + } + + public static int readInt4fromBytes(byte[] b, int offset) { + return ((b[offset] & 0xff) << 24) | ((b[offset + 1] & 0xff) << 16) | ((b[offset + 2] & 0xff) << 8) + | (b[offset + 3] & 0xff); + } + + public static void writeByte(OutputStream os, byte b) { + try { + os.write(b); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeInt2(OutputStream os, int n) { + byte[] temp = { (byte) ((n >> 8) & 0xff), (byte) (n & 0xff) }; + writeBytes(os, temp); + } + + public static void writeInt4(OutputStream os, int n) { + byte[] temp = new byte[4]; + writeInt4tobytes(n, temp, 0); + writeBytes(os, temp); + } + + public static void writeInt2tobytes(int n, byte[] b, int offset) { + b[offset] = (byte) ((n >> 8) & 0xff); + b[offset + 1] = (byte) (n & 0xff); + } + + public static void writeInt4tobytes(int n, byte[] b, int offset) { + b[offset] = (byte) ((n >> 24) & 0xff); + b[offset + 1] = (byte) ((n >> 16) & 0xff); + b[offset + 2] = (byte) ((n >> 8) & 0xff); + b[offset + 3] = (byte) (n & 0xff); + } + + /** + * guaranteed to read exactly len bytes. throws error if it can't + */ + public static void readBytes(InputStream is, byte[] b, int offset, int len) { + if (len == 0) + return; + try { + int read = 0; + while (read < len) { + int n = is.read(b, offset + read, len - read); + if (n < 1) + throw new PngjInputException("error reading bytes, " + n + " !=" + len); + read += n; + } + } catch (IOException e) { + throw new PngjInputException("error reading", e); + } + } + + public static void skipBytes(InputStream is, int len) { + byte[] buf = new byte[8192 * 4]; + int read, remain = len; + try { + while (remain > 0) { + read = is.read(buf, 0, remain > buf.length ? buf.length : remain); + if (read < 0) + throw new PngjInputException("error reading (skipping) : EOF"); + remain -= read; + } + } catch (IOException e) { + throw new PngjInputException("error reading (skipping)", e); + } + } + + public static void writeBytes(OutputStream os, byte[] b) { + try { + os.write(b); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeBytes(OutputStream os, byte[] b, int offset, int n) { + try { + os.write(b, offset, n); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void logdebug(String msg) { + if (DEBUG) + System.out.println(msg); + } + + private static final ThreadLocal crcProvider = new ThreadLocal() { + protected CRC32 initialValue() { + return new CRC32(); + } + }; + + /** thread-singleton crc engine */ + public static CRC32 getCRC() { + return crcProvider.get(); + } + + // / filters + public static int filterRowNone(int r) { + return (int) (r & 0xFF); + } + + public static int filterRowSub(int r, int left) { + return ((int) (r - left) & 0xFF); + } + + public static int filterRowUp(int r, int up) { + return ((int) (r - up) & 0xFF); + } + + public static int filterRowAverage(int r, int left, int up) { + return (r - (left + up) / 2) & 0xFF; + } + + public static int filterRowPaeth(int r, int left, int up, int upleft) { // a = left, b = above, c = upper left + return (r - filterPaethPredictor(left, up, upleft)) & 0xFF; + } + + public static int unfilterRowNone(int r) { + return (int) (r & 0xFF); + } + + public static int unfilterRowSub(int r, int left) { + return ((int) (r + left) & 0xFF); + } + + public static int unfilterRowUp(int r, int up) { + return ((int) (r + up) & 0xFF); + } + + public static int unfilterRowAverage(int r, int left, int up) { + return (r + (left + up) / 2) & 0xFF; + } + + public static int unfilterRowPaeth(int r, int left, int up, int upleft) { // a = left, b = above, c = upper left + return (r + filterPaethPredictor(left, up, upleft)) & 0xFF; + } + + final static int filterPaethPredictor(final int a, final int b, final int c) { // a = left, b = above, c = upper + // left + // from http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html + + final int p = a + b - c;// ; initial estimate + final int pa = p >= a ? p - a : a - p; + final int pb = p >= b ? p - b : b - p; + final int pc = p >= c ? p - c : c - p; + // ; return nearest of a,b,c, + // ; breaking ties in order a,b,c. + if (pa <= pb && pa <= pc) + return a; + else if (pb <= pc) + return b; + else + return c; + } + + /* + * we put this methods here so as to not pollute the public interface of PngReader + */ + public final static void initCrcForTests(PngReader pngr) { + pngr.initCrctest(); + } + + public final static long getCrctestVal(PngReader pngr) { + return pngr.getCrctestVal(); + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java index 66c4b49f0..6cc39b0e6 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java @@ -11,23 +11,24 @@ import jogamp.opengl.util.pngj.chunks.ChunkHelper; /** - * Reads IDAT chunks + * Reads a sequence of contiguous IDAT chunks */ class PngIDatChunkInputStream extends InputStream { private final InputStream inputStream; private final CRC32 crcEngine; + private boolean checkCrc = true; private int lenLastChunk; private byte[] idLastChunk = new byte[4]; private int toReadThisChunk = 0; private boolean ended = false; - private long offset; // offset inside inputstream + private long offset; // offset inside whole inputstream (counting bytes before IDAT) // just informational static class IdatChunkInfo { public final int len; - public final int offset; + public final long offset; - private IdatChunkInfo(int len, int offset) { + private IdatChunkInfo(int len, long offset) { this.len = len; this.offset = offset; } @@ -38,16 +39,17 @@ class PngIDatChunkInputStream extends InputStream { /** * Constructor must be called just after reading length and id of first IDAT chunk **/ - PngIDatChunkInputStream(InputStream iStream, int lenFirstChunk, int offset) { - this.offset = (long) offset; + PngIDatChunkInputStream(InputStream iStream, int lenFirstChunk, long offset) { + this.offset = offset; inputStream = iStream; - crcEngine = new CRC32(); this.lenLastChunk = lenFirstChunk; toReadThisChunk = lenFirstChunk; // we know it's a IDAT System.arraycopy(ChunkHelper.b_IDAT, 0, idLastChunk, 0, 4); + crcEngine = new CRC32(); crcEngine.update(idLastChunk, 0, 4); foundChunksInfo.add(new IdatChunkInfo(lenLastChunk, offset - 8)); + // PngHelper.logdebug("IDAT Initial fragment: len=" + lenLastChunk); if (this.lenLastChunk == 0) endChunkGoForNext(); // rare, but... @@ -58,31 +60,33 @@ class PngIDatChunkInputStream extends InputStream { */ @Override public void close() throws IOException { - super.close(); // nothing + super.close(); // thsi does nothing } private void endChunkGoForNext() { - // Called after readging the last byte of chunk + // Called after readging the last byte of one IDAT chunk // Checks CRC, and read ID from next CHUNK // Those values are left in idLastChunk / lenLastChunk // Skips empty IDATS do { - int crc = PngHelper.readInt4(inputStream); // + int crc = PngHelperInternal.readInt4(inputStream); // offset += 4; - int crccalc = (int) crcEngine.getValue(); - if (lenLastChunk > 0 && crc != crccalc) - throw new PngjBadCrcException("error reading idat; offset: " + offset); - crcEngine.reset(); - lenLastChunk = PngHelper.readInt4(inputStream); - if (lenLastChunk < 0) - throw new PngjInputException("invalid len for chunk: " + lenLastChunk); + if (checkCrc) { + int crccalc = (int) crcEngine.getValue(); + if (lenLastChunk > 0 && crc != crccalc) + throw new PngjBadCrcException("error reading idat; offset: " + offset); + crcEngine.reset(); + } + lenLastChunk = PngHelperInternal.readInt4(inputStream); toReadThisChunk = lenLastChunk; - PngHelper.readBytes(inputStream, idLastChunk, 0, 4); + PngHelperInternal.readBytes(inputStream, idLastChunk, 0, 4); offset += 8; + // found a NON IDAT chunk? this stream is ended ended = !Arrays.equals(idLastChunk, ChunkHelper.b_IDAT); if (!ended) { - foundChunksInfo.add(new IdatChunkInfo(lenLastChunk, (int) (offset - 8))); - crcEngine.update(idLastChunk, 0, 4); + foundChunksInfo.add(new IdatChunkInfo(lenLastChunk, offset - 8)); + if (checkCrc) + crcEngine.update(idLastChunk, 0, 4); } // PngHelper.logdebug("IDAT ended. next len= " + lenLastChunk + " idat?" + // (!ended)); @@ -96,8 +100,9 @@ class PngIDatChunkInputStream extends InputStream { void forceChunkEnd() { if (!ended) { byte[] dummy = new byte[toReadThisChunk]; - PngHelper.readBytes(inputStream, dummy, 0, toReadThisChunk); - crcEngine.update(dummy, 0, toReadThisChunk); + PngHelperInternal.readBytes(inputStream, dummy, 0, toReadThisChunk); + if (checkCrc) + crcEngine.update(dummy, 0, toReadThisChunk); endChunkGoForNext(); } } @@ -107,11 +112,14 @@ class PngIDatChunkInputStream extends InputStream { */ @Override public int read(byte[] b, int off, int len) throws IOException { + if (ended) + return -1; // can happen only when raw reading, see Pngreader.readAndSkipsAllRows() if (toReadThisChunk == 0) - throw new RuntimeException("this should not happen"); + throw new PngjExceptionInternal("this should not happen"); int n = inputStream.read(b, off, len >= toReadThisChunk ? toReadThisChunk : len); if (n > 0) { - crcEngine.update(b, off, n); + if (checkCrc) + crcEngine.update(b, off, n); this.offset += n; toReadThisChunk -= n; } @@ -150,4 +158,11 @@ class PngIDatChunkInputStream extends InputStream { boolean isEnded() { return ended; } + + /** + * Disables CRC checking. This can make reading faster + */ + void disableCrcCheck() { + checkCrc = false; + } } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java index 8b9fa5dae..411d18819 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java @@ -7,23 +7,23 @@ import jogamp.opengl.util.pngj.chunks.ChunkRaw; /** - * outputs the stream for IDAT chunk , fragmented at fixed size (16384 default). + * outputs the stream for IDAT chunk , fragmented at fixed size (32k default). */ class PngIDatChunkOutputStream extends ProgressiveOutputStream { - private static final int SIZE_DEFAULT = 16384; + private static final int SIZE_DEFAULT = 32768; // 32k private final OutputStream outputStream; PngIDatChunkOutputStream(OutputStream outputStream) { - this(outputStream, SIZE_DEFAULT); + this(outputStream, 0); } PngIDatChunkOutputStream(OutputStream outputStream, int size) { - super(size); + super(size > 0 ? size : SIZE_DEFAULT); this.outputStream = outputStream; } @Override - public final void flushBuffer(byte[] b, int len) { + protected final void flushBuffer(byte[] b, int len) { ChunkRaw c = new ChunkRaw(len, ChunkHelper.b_IDAT, false); c.data = b; c.writeChunk(outputStream); diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngReader.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngReader.java index 7343893b6..8cb4295a5 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/PngReader.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngReader.java @@ -1,42 +1,66 @@ package jogamp.opengl.util.pngj; +import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; +import java.util.HashSet; +import java.util.zip.CRC32; import java.util.zip.InflaterInputStream; -import jogamp.opengl.util.pngj.PngIDatChunkInputStream.IdatChunkInfo; +import jogamp.opengl.util.pngj.ImageInfo; +import jogamp.opengl.util.pngj.ImageLine.SampleType; import jogamp.opengl.util.pngj.chunks.ChunkHelper; -import jogamp.opengl.util.pngj.chunks.ChunkList; import jogamp.opengl.util.pngj.chunks.ChunkLoadBehaviour; import jogamp.opengl.util.pngj.chunks.ChunkRaw; +import jogamp.opengl.util.pngj.chunks.ChunksList; import jogamp.opengl.util.pngj.chunks.PngChunk; +import jogamp.opengl.util.pngj.chunks.PngChunkIDAT; import jogamp.opengl.util.pngj.chunks.PngChunkIHDR; +import jogamp.opengl.util.pngj.chunks.PngChunkSkipped; import jogamp.opengl.util.pngj.chunks.PngMetadata; - /** - * Reads a PNG image, line by line + * Reads a PNG image, line by line. + *

+ * The reading sequence is as follows:
+ * 1. At construction time, the header and IHDR chunk are read (basic image info)
+ * 2. Afterwards you can set some additional global options. Eg. {@link #setUnpackedMode(boolean)}, + * {@link #setCrcCheckDisabled()}.
+ * 3. Optional: If you call getMetadata() or getChunksLisk() before start reading the rows, all the chunks before IDAT + * are automatically loaded and available
+ * 4a. The rows are read onen by one of the readRowXXX methods: {@link #readRowInt(int)}, + * {@link PngReader#readRowByte(int)}, etc, in order, from 0 to nrows-1 (you can skip or repeat rows, but not go + * backwards)
+ * 4b. Alternatively, you can read all rows, or a subset, in a single call: {@link #readRowsInt()}, + * {@link #readRowsByte()} ,etc. In general this consumes more memory, but for interlaced images this is equally + * efficient, and more so if reading a small subset of rows.
+ * 5. Read of the last row auyomatically loads the trailing chunks, and ends the reader.
+ * 6. end() forcibly finishes/aborts the reading and closes the stream */ public class PngReader { /** * Basic image info - final and inmutable. */ public final ImageInfo imgInfo; - protected final String filename; // not necesarily a file, can be a description - merely informative - private static int MAX_BYTES_CHUNKS_TO_LOAD = 640000; - private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS; + /** + * not necesarily a filename, can be a description - merely informative + */ + protected final String filename; + + private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS; // see setter/getter - private final InputStream is; - private InflaterInputStream idatIstream; - private PngIDatChunkInputStream iIdatCstream; + private boolean shouldCloseStream = true; // true: closes stream after ending - see setter/getter - protected int currentChunkGroup = -1; - protected int rowNum = -1; // current row number - private int offset = 0; - private int bytesChunksLoaded; // bytes loaded from anciallary chunks + // some performance/defensive limits + private long maxTotalBytesRead = 200 * 1024 * 1024; // 200MB + private int maxBytesMetadata = 5 * 1024 * 1024; // for ancillary chunks - see setter/getter + private int skipChunkMaxSize = 2 * 1024 * 1024; // chunks exceeding this size will be skipped (nor even CRC checked) + private String[] skipChunkIds = { "fdAT" }; // chunks with these ids will be skipped (nor even CRC checked) + private HashSet skipChunkIdsSet; // lazily created from skipChunksById + + protected final PngMetadata metadata; // this a wrapper over chunks + protected final ChunksList chunksList; protected ImageLine imgLine; @@ -45,12 +69,30 @@ public class PngReader { protected byte[] rowbprev = null; // rowb previous protected byte[] rowbfilter = null; // current line 'filtered': exactly as in uncompressed stream + // only set for interlaced PNG + private final boolean interlaced; + private final PngDeinterlacer deinterlacer; + + private boolean crcEnabled = true; + + // this only influences the 1-2-4 bitdepth format + private boolean unpackedMode = false; /** - * All chunks loaded. Criticals are included, except that all IDAT chunks appearance are replaced by a single - * dummy-marker IDAT chunk. These might be copied to the PngWriter + * Current chunk group, (0-6) already read or reading + *

+ * see {@link ChunksList} */ - private final ChunkList chunksList; - private final PngMetadata metadata; // this a wrapper over chunks + protected int currentChunkGroup = -1; + + protected int rowNum = -1; // last read row number, starting from 0 + private long offset = 0; // offset in InputStream = bytes read + private int bytesChunksLoaded; // bytes loaded from anciallary chunks + + protected final InputStream inputStream; + protected InflaterInputStream idatIstream; + protected PngIDatChunkInputStream iIdatCstream; + + protected CRC32 crctest; // If set to non null, it gets a CRC of the unfiltered bytes, to check for images equality /** * Constructs a PngReader from an InputStream. @@ -65,175 +107,234 @@ public class PngReader { */ public PngReader(InputStream inputStream, String filenameOrDescription) { this.filename = filenameOrDescription == null ? "" : filenameOrDescription; - this.is = inputStream; - this.chunksList = new ChunkList(null); - this.metadata = new PngMetadata(chunksList, true); - // reads header (magic bytes) - byte[] pngid = new byte[PngHelper.pngIdBytes.length]; - PngHelper.readBytes(is, pngid, 0, pngid.length); + this.inputStream = inputStream; + this.chunksList = new ChunksList(null); + this.metadata = new PngMetadata(chunksList); + // starts reading: signature + byte[] pngid = new byte[8]; + PngHelperInternal.readBytes(inputStream, pngid, 0, pngid.length); offset += pngid.length; - if (!Arrays.equals(pngid, PngHelper.pngIdBytes)) + if (!Arrays.equals(pngid, PngHelperInternal.getPngIdSignature())) throw new PngjInputException("Bad PNG signature"); // reads first chunk - currentChunkGroup = ChunkList.CHUNK_GROUP_0_IDHR; - int clen = PngHelper.readInt4(is); + currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR; + int clen = PngHelperInternal.readInt4(inputStream); offset += 4; if (clen != 13) - throw new RuntimeException("IDHR chunk len != 13 ?? " + clen); + throw new PngjInputException("IDHR chunk len != 13 ?? " + clen); byte[] chunkid = new byte[4]; - PngHelper.readBytes(is, chunkid, 0, 4); + PngHelperInternal.readBytes(inputStream, chunkid, 0, 4); if (!Arrays.equals(chunkid, ChunkHelper.b_IHDR)) throw new PngjInputException("IHDR not found as first chunk??? [" + ChunkHelper.toString(chunkid) + "]"); offset += 4; - ChunkRaw chunk = new ChunkRaw(clen, chunkid, true); - String chunkids = ChunkHelper.toString(chunkid); - offset += chunk.readChunkData(is); - PngChunkIHDR ihdr = (PngChunkIHDR) addChunkToList(chunk); + PngChunkIHDR ihdr = (PngChunkIHDR) readChunk(chunkid, clen, false); boolean alpha = (ihdr.getColormodel() & 0x04) != 0; boolean palette = (ihdr.getColormodel() & 0x01) != 0; boolean grayscale = (ihdr.getColormodel() == 0 || ihdr.getColormodel() == 4); + // creates ImgInfo and imgLine, and allocates buffers imgInfo = new ImageInfo(ihdr.getCols(), ihdr.getRows(), ihdr.getBitspc(), alpha, grayscale, palette); - imgLine = new ImageLine(imgInfo); - if (ihdr.getInterlaced() != 0) - throw new PngjUnsupportedException("PNG interlaced not supported by this library"); - if (ihdr.getFilmeth() != 0 || ihdr.getCompmeth() != 0) - throw new PngjInputException("compmethod o filtermethod unrecognized"); + // allocation: one extra byte for filter type one pixel + rowbfilter = new byte[imgInfo.bytesPerRow + 1]; + rowb = new byte[imgInfo.bytesPerRow + 1]; + rowbprev = new byte[rowb.length]; + interlaced = ihdr.getInterlaced() == 1; + deinterlacer = interlaced ? new PngDeinterlacer(imgInfo) : null; + // some checks + if (ihdr.getFilmeth() != 0 || ihdr.getCompmeth() != 0 || (ihdr.getInterlaced() & 0xFFFE) != 0) + throw new PngjInputException("compression method o filter method or interlaced unrecognized "); if (ihdr.getColormodel() < 0 || ihdr.getColormodel() > 6 || ihdr.getColormodel() == 1 || ihdr.getColormodel() == 5) throw new PngjInputException("Invalid colormodel " + ihdr.getColormodel()); if (ihdr.getBitspc() != 1 && ihdr.getBitspc() != 2 && ihdr.getBitspc() != 4 && ihdr.getBitspc() != 8 && ihdr.getBitspc() != 16) throw new PngjInputException("Invalid bit depth " + ihdr.getBitspc()); - // allocation: one extra byte for filter type one pixel - rowbfilter = new byte[imgInfo.bytesPerRow + 1]; - rowb = new byte[imgInfo.bytesPerRow + 1]; - rowbprev = new byte[rowb.length]; } - private static class FoundChunkInfo { - public final String id; - public final int len; - public final int offset; - public final boolean loaded; + private boolean firstChunksNotYetRead() { + return currentChunkGroup < ChunksList.CHUNK_GROUP_1_AFTERIDHR; + } + + /** + * Reads last Internally called after having read the last line. It reads extra chunks after IDAT, if present. + */ + private void readLastAndClose() { + // offset = iIdatCstream.getOffset(); + if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT) { + try { + idatIstream.close(); + } catch (Exception e) { + } + readLastChunks(); + } + close(); + } + + private void close() { + if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END) { // this could only happen if forced close + try { + idatIstream.close(); + } catch (Exception e) { + } + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; + } + if (shouldCloseStream) { + try { + inputStream.close(); + } catch (Exception e) { + throw new PngjInputException("error closing input stream!", e); + } + } + } + + // nbytes: NOT including the filter byte. leaves result in rowb + private void unfilterRow(int nbytes) { + int ftn = rowbfilter[0]; + FilterType ft = FilterType.getByVal(ftn); + if (ft == null) + throw new PngjInputException("Filter type " + ftn + " invalid"); + switch (ft) { + case FILTER_NONE: + unfilterRowNone(nbytes); + break; + case FILTER_SUB: + unfilterRowSub(nbytes); + break; + case FILTER_UP: + unfilterRowUp(nbytes); + break; + case FILTER_AVERAGE: + unfilterRowAverage(nbytes); + break; + case FILTER_PAETH: + unfilterRowPaeth(nbytes); + break; + default: + throw new PngjInputException("Filter type " + ftn + " not implemented"); + } + if (crctest != null) + crctest.update(rowb, 1, rowb.length - 1); + } + + private void unfilterRowAverage(final int nbytes) { + int i, j, x; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) { + x = j > 0 ? (rowb[j] & 0xff) : 0; + rowb[i] = (byte) (rowbfilter[i] + (x + (rowbprev[i] & 0xFF)) / 2); + } + } - private FoundChunkInfo(String id, int len, int offset, boolean loaded) { - this.id = id; - this.len = len; - this.offset = offset; - this.loaded = loaded; + private void unfilterRowNone(final int nbytes) { + for (int i = 1; i <= nbytes; i++) { + rowb[i] = (byte) (rowbfilter[i]); } + } - public String toString() { - return "chunk " + id + " len=" + len + " offset=" + offset + (this.loaded ? " " : " X "); + private void unfilterRowPaeth(final int nbytes) { + int i, j, x, y; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) { + x = j > 0 ? (rowb[j] & 0xFF) : 0; + y = j > 0 ? (rowbprev[j] & 0xFF) : 0; + rowb[i] = (byte) (rowbfilter[i] + PngHelperInternal.filterPaethPredictor(x, rowbprev[i] & 0xFF, y)); } } - private PngChunk addChunkToList(ChunkRaw chunk) { - // this requires that the currentChunkGroup is ok - PngChunk chunkType = PngChunk.factory(chunk, imgInfo); - if (!chunkType.crit) { - bytesChunksLoaded += chunk.len; + private void unfilterRowSub(final int nbytes) { + int i, j; + for (i = 1; i <= imgInfo.bytesPixel; i++) { + rowb[i] = (byte) (rowbfilter[i]); + } + for (j = 1, i = imgInfo.bytesPixel + 1; i <= nbytes; i++, j++) { + rowb[i] = (byte) (rowbfilter[i] + rowb[j]); } - if (bytesChunksLoaded > MAX_BYTES_CHUNKS_TO_LOAD) { - throw new PngjInputException("Chunk exceeded available space (" + MAX_BYTES_CHUNKS_TO_LOAD + ") chunk: " - + chunk + " See PngReader.MAX_BYTES_CHUNKS_TO_LOAD\n"); + } + + private void unfilterRowUp(final int nbytes) { + for (int i = 1; i <= nbytes; i++) { + rowb[i] = (byte) (rowbfilter[i] + rowbprev[i]); } - chunksList.appendReadChunk(chunkType, currentChunkGroup); - return chunkType; } /** - * Reads chunks before first IDAT. Position before: after IDHR (crc included) Position after: just after the first - * IDAT chunk id - * + * Reads chunks before first IDAT. Normally this is called automatically + *

+ * Position before: after IDHR (crc included) Position after: just after the first IDAT chunk id + *

* This can be called several times (tentatively), it does nothing if already run - * + *

* (Note: when should this be called? in the constructor? hardly, because we loose the opportunity to call * setChunkLoadBehaviour() and perhaps other settings before reading the first row? but sometimes we want to access * some metadata (plte, phys) before. Because of this, this method can be called explicitly but is also called - * implicititly in some methods (getMetatada(), getChunks()) - * - **/ - public void readFirstChunks() { + * implicititly in some methods (getMetatada(), getChunksList()) + */ + private final void readFirstChunks() { if (!firstChunksNotYetRead()) return; int clen = 0; boolean found = false; byte[] chunkid = new byte[4]; // it's important to reallocate in each iteration - currentChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR; + currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; while (!found) { - clen = PngHelper.readInt4(is); + clen = PngHelperInternal.readInt4(inputStream); offset += 4; if (clen < 0) break; - PngHelper.readBytes(is, chunkid, 0, 4); + PngHelperInternal.readBytes(inputStream, chunkid, 0, 4); offset += 4; if (Arrays.equals(chunkid, ChunkHelper.b_IDAT)) { found = true; - currentChunkGroup = ChunkList.CHUNK_GROUP_4_IDAT; + currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; // add dummy idat chunk to list - ChunkRaw chunk = new ChunkRaw(0, chunkid, false); - addChunkToList(chunk); + chunksList.appendReadChunk(new PngChunkIDAT(imgInfo, clen, offset - 8), currentChunkGroup); break; } else if (Arrays.equals(chunkid, ChunkHelper.b_IEND)) { throw new PngjInputException("END chunk found before image data (IDAT) at offset=" + offset); } - ChunkRaw chunk = new ChunkRaw(clen, chunkid, true); - String chunkids = ChunkHelper.toString(chunkid); - boolean loadchunk = ChunkHelper.shouldLoad(chunkids, chunkLoadBehaviour); - offset += chunk.readChunkData(is); - if (chunkids.equals(ChunkHelper.PLTE)) - currentChunkGroup = ChunkList.CHUNK_GROUP_2_PLTE; - if (loadchunk) - addChunkToList(chunk); - if (chunkids.equals(ChunkHelper.PLTE)) - currentChunkGroup = ChunkList.CHUNK_GROUP_3_AFTERPLTE; + if (Arrays.equals(chunkid, ChunkHelper.b_PLTE)) + currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE; + readChunk(chunkid, clen, false); + if (Arrays.equals(chunkid, ChunkHelper.b_PLTE)) + currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; } int idatLen = found ? clen : -1; if (idatLen < 0) throw new PngjInputException("first idat chunk not found!"); - iIdatCstream = new PngIDatChunkInputStream(is, idatLen, offset); + iIdatCstream = new PngIDatChunkInputStream(inputStream, idatLen, offset); idatIstream = new InflaterInputStream(iIdatCstream); + if (!crcEnabled) + iIdatCstream.disableCrcCheck(); } /** * Reads (and processes) chunks after last IDAT. **/ - private void readLastChunks() { + void readLastChunks() { // PngHelper.logdebug("idat ended? " + iIdatCstream.isEnded()); - currentChunkGroup = ChunkList.CHUNK_GROUP_5_AFTERIDAT; + currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; if (!iIdatCstream.isEnded()) iIdatCstream.forceChunkEnd(); int clen = iIdatCstream.getLenLastChunk(); byte[] chunkid = iIdatCstream.getIdLastChunk(); boolean endfound = false; boolean first = true; - boolean ignore = false; + boolean skip = false; while (!endfound) { - ignore = false; + skip = false; if (!first) { - clen = PngHelper.readInt4(is); + clen = PngHelperInternal.readInt4(inputStream); offset += 4; if (clen < 0) - throw new PngjInputException("bad len " + clen); - PngHelper.readBytes(is, chunkid, 0, 4); + throw new PngjInputException("bad chuck len " + clen); + PngHelperInternal.readBytes(inputStream, chunkid, 0, 4); offset += 4; } first = false; if (Arrays.equals(chunkid, ChunkHelper.b_IDAT)) { - // PngHelper.logdebug("extra IDAT chunk len - ignoring : "); - ignore = true; + skip = true; // extra dummy (empty?) idat chunk, it can happen, ignore it } else if (Arrays.equals(chunkid, ChunkHelper.b_IEND)) { - currentChunkGroup = ChunkList.CHUNK_GROUP_6_END; + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; endfound = true; } - ChunkRaw chunk = new ChunkRaw(clen, chunkid, true); - String chunkids = ChunkHelper.toString(chunkid); - boolean loadchunk = ChunkHelper.shouldLoad(chunkids, chunkLoadBehaviour); - offset += chunk.readChunkData(is); - if (loadchunk && !ignore) { - addChunkToList(chunk); - } + readChunk(chunkid, clen, skip); } if (!endfound) throw new PngjInputException("end chunk not found - offset=" + offset); @@ -241,173 +342,597 @@ public class PngReader { } /** - * Calls readRow(int[] buffer, int nrow) using internal ImageLine as buffer. This doesn't allocate or - * copy anything. + * Reads chunkd from input stream, adds to ChunksList, and returns it. If it's skipped, a PngChunkSkipped object is + * created + */ + private PngChunk readChunk(byte[] chunkid, int clen, boolean skipforced) { + if (clen < 0) + throw new PngjInputException("invalid chunk lenght: " + clen); + // skipChunksByIdSet is created lazyly, if fist IHDR has already been read + if (skipChunkIdsSet == null && currentChunkGroup > ChunksList.CHUNK_GROUP_0_IDHR) + skipChunkIdsSet = new HashSet(Arrays.asList(skipChunkIds)); + String chunkidstr = ChunkHelper.toString(chunkid); + boolean critical = ChunkHelper.isCritical(chunkidstr); + PngChunk pngChunk = null; + boolean skip = skipforced; + if (maxTotalBytesRead > 0 && clen + offset > maxTotalBytesRead) + throw new PngjInputException("Maximum total bytes to read exceeeded: " + maxTotalBytesRead + " offset:" + + offset + " clen=" + clen); + // an ancillary chunks can be skipped because of several reasons: + if (currentChunkGroup > ChunksList.CHUNK_GROUP_0_IDHR && !critical) + skip = skip || (skipChunkMaxSize > 0 && clen >= skipChunkMaxSize) || skipChunkIdsSet.contains(chunkidstr) + || (maxBytesMetadata > 0 && clen > maxBytesMetadata - bytesChunksLoaded) + || !ChunkHelper.shouldLoad(chunkidstr, chunkLoadBehaviour); + if (skip) { + PngHelperInternal.skipBytes(inputStream, clen); + PngHelperInternal.readInt4(inputStream); // skip - we dont call PngHelperInternal.skipBytes(inputStream, + // clen + 4) for risk of overflow + pngChunk = new PngChunkSkipped(chunkidstr, imgInfo, clen); + } else { + ChunkRaw chunk = new ChunkRaw(clen, chunkid, true); + chunk.readChunkData(inputStream, crcEnabled || critical); + pngChunk = PngChunk.factory(chunk, imgInfo); + if (!pngChunk.crit) + bytesChunksLoaded += chunk.len; + } + pngChunk.setOffset(offset - 8L); + chunksList.appendReadChunk(pngChunk, currentChunkGroup); + offset += clen + 4L; + return pngChunk; + } + + /** + * Logs/prints a warning. + *

+ * The default behaviour is print to stderr, but it can be overriden. + *

+ * This happens rarely - most errors are fatal. + */ + protected void logWarn(String warn) { + System.err.println(warn); + } + + /** + * @see #setChunkLoadBehaviour(ChunkLoadBehaviour) + */ + public ChunkLoadBehaviour getChunkLoadBehaviour() { + return chunkLoadBehaviour; + } + + /** + * Determines which ancillary chunks (metada) are to be loaded + * + * @param chunkLoadBehaviour + * {@link ChunkLoadBehaviour} + */ + public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) { + this.chunkLoadBehaviour = chunkLoadBehaviour; + } + + /** + * All loaded chunks (metada). If we have not yet end reading the image, this will include only the chunks before + * the pixels data (IDAT) + *

+ * Critical chunks are included, except that all IDAT chunks appearance are replaced by a single dummy-marker IDAT + * chunk. These might be copied to the PngWriter + *

+ * + * @see #getMetadata() + */ + public ChunksList getChunksList() { + if (firstChunksNotYetRead()) + readFirstChunks(); + return chunksList; + } + + int getCurrentChunkGroup() { + return currentChunkGroup; + } + + /** + * High level wrapper over chunksList * - * @return The ImageLine that also is available inside this object. + * @see #getChunksList() + */ + public PngMetadata getMetadata() { + if (firstChunksNotYetRead()) + readFirstChunks(); + return metadata; + } + + /** + * If called for first time, calls readRowInt. Elsewhere, it calls the appropiate readRowInt/readRowByte + *

+ * In general, specifying the concrete readRowInt/readRowByte is preferrable + * + * @see #readRowInt(int) {@link #readRowByte(int)} */ public ImageLine readRow(int nrow) { - readRow(imgLine.scanline, nrow); - imgLine.filterUsed = FilterType.getByVal(rowbfilter[0]); + if (imgLine == null) + imgLine = new ImageLine(imgInfo, SampleType.INT, unpackedMode); + return imgLine.sampleType != SampleType.BYTE ? readRowInt(nrow) : readRowByte(nrow); + } + + /** + * Reads the row as INT, storing it in the {@link #imgLine} property and returning it. + * + * The row must be greater or equal than the last read row. + * + * @param nrow + * Row number, from 0 to rows-1. Increasing order. + * @return ImageLine object, also available as field. Data is in {@link ImageLine#scanline} (int) field. + */ + public ImageLine readRowInt(int nrow) { + if (imgLine == null) + imgLine = new ImageLine(imgInfo, SampleType.INT, unpackedMode); + if (imgLine.getRown() == nrow) // already read + return imgLine; + readRowInt(imgLine.scanline, nrow); + imgLine.setFilterUsed(FilterType.getByVal(rowbfilter[0])); + imgLine.setRown(nrow); + return imgLine; + } + + /** + * Reads the row as BYTES, storing it in the {@link #imgLine} property and returning it. + * + * The row must be greater or equal than the last read row. This method allows to pass the same row that was last + * read. + * + * @param nrow + * Row number, from 0 to rows-1. Increasing order. + * @return ImageLine object, also available as field. Data is in {@link ImageLine#scanlineb} (byte) field. + */ + public ImageLine readRowByte(int nrow) { + if (imgLine == null) + imgLine = new ImageLine(imgInfo, SampleType.BYTE, unpackedMode); + if (imgLine.getRown() == nrow) // already read + return imgLine; + readRowByte(imgLine.scanlineb, nrow); + imgLine.setFilterUsed(FilterType.getByVal(rowbfilter[0])); imgLine.setRown(nrow); return imgLine; } + /** + * @see #readRowInt(int[], int) + */ + public final int[] readRow(int[] buffer, final int nrow) { + return readRowInt(buffer, nrow); + } + /** * Reads a line and returns it as a int[] array. + *

+ * You can pass (optionally) a prealocatted buffer. + *

+ * If the bitdepth is less than 8, the bytes are packed - unless {@link #unpackedMode} is true. + * + * @param buffer + * Prealocated buffer, or null. + * @param nrow + * Row number (0 is top). Most be strictly greater than the last read row. * + * @return The scanline in the same passwd buffer if it was allocated, a newly allocated one otherwise + */ + public final int[] readRowInt(int[] buffer, final int nrow) { + if (buffer == null) + buffer = new int[unpackedMode ? imgInfo.samplesPerRow : imgInfo.samplesPerRowPacked]; + if (!interlaced) { + if (nrow <= rowNum) + throw new PngjInputException("rows must be read in increasing order: " + nrow); + int bytesread = 0; + while (rowNum < nrow) + bytesread = readRowRaw(rowNum + 1); // read rows, perhaps skipping if necessary + decodeLastReadRowToInt(buffer, bytesread); + } else { // interlaced + if (deinterlacer.getImageInt() == null) + deinterlacer.setImageInt(readRowsInt().scanlines); // read all image and store it in deinterlacer + System.arraycopy(deinterlacer.getImageInt()[nrow], 0, buffer, 0, unpackedMode ? imgInfo.samplesPerRow + : imgInfo.samplesPerRowPacked); + } + return buffer; + } + + /** + * Reads a line and returns it as a byte[] array. + *

* You can pass (optionally) a prealocatted buffer. + *

+ * If the bitdepth is less than 8, the bytes are packed - unless {@link #unpackedMode} is true.
+ * If the bitdepth is 16, the least significant byte is lost. + *

* * @param buffer * Prealocated buffer, or null. * @param nrow - * Row number (0 is top). This is mostly for checking, because this library reads rows in sequence. + * Row number (0 is top). Most be strictly greater than the last read row. * * @return The scanline in the same passwd buffer if it was allocated, a newly allocated one otherwise */ - public int[] readRow(int[] buffer, int nrow) { - if (nrow < 0 || nrow >= imgInfo.rows) - throw new PngjInputException("invalid line"); - if (nrow != rowNum + 1) - throw new PngjInputException("invalid line (expected: " + (rowNum + 1)); + public final byte[] readRowByte(byte[] buffer, final int nrow) { + if (buffer == null) + buffer = new byte[unpackedMode ? imgInfo.samplesPerRow : imgInfo.samplesPerRowPacked]; + if (!interlaced) { + if (nrow <= rowNum) + throw new PngjInputException("rows must be read in increasing order: " + nrow); + int bytesread = 0; + while (rowNum < nrow) + bytesread = readRowRaw(rowNum + 1); // read rows, perhaps skipping if necessary + decodeLastReadRowToByte(buffer, bytesread); + } else { // interlaced + if (deinterlacer.getImageByte() == null) + deinterlacer.setImageByte(readRowsByte().scanlinesb); // read all image and store it in deinterlacer + System.arraycopy(deinterlacer.getImageByte()[nrow], 0, buffer, 0, unpackedMode ? imgInfo.samplesPerRow + : imgInfo.samplesPerRowPacked); + } + return buffer; + } + + /** + * @param nrow + * @deprecated Now {@link #readRow(int)} implements the same funcion. This method will be removed in future releases + */ + public ImageLine getRow(int nrow) { + return readRow(nrow); + } + + private void decodeLastReadRowToInt(int[] buffer, int bytesRead) { + if (imgInfo.bitDepth <= 8) + for (int i = 0, j = 1; i < bytesRead; i++) + buffer[i] = (rowb[j++] & 0xFF); // http://www.libpng.org/pub/png/spec/1.2/PNG-DataRep.html + else + for (int i = 0, j = 1; j <= bytesRead; i++) + buffer[i] = ((rowb[j++] & 0xFF) << 8) + (rowb[j++] & 0xFF); // 16 bitspc + if (imgInfo.packed && unpackedMode) + ImageLine.unpackInplaceInt(imgInfo, buffer, buffer, false); + } + + private void decodeLastReadRowToByte(byte[] buffer, int bytesRead) { + if (imgInfo.bitDepth <= 8) + System.arraycopy(rowb, 1, buffer, 0, bytesRead); + else + for (int i = 0, j = 1; j < bytesRead; i++, j += 2) + buffer[i] = rowb[j];// 16 bits in 1 byte: this discards the LSB!!! + if (imgInfo.packed && unpackedMode) + ImageLine.unpackInplaceByte(imgInfo, buffer, buffer, false); + } + + /** + * Reads a set of lines and returns it as a ImageLines object, which wraps matrix. Internally it reads all lines, + * but decodes and stores only the wanted ones. This starts and ends the reading, and cannot be combined with other + * reading methods. + *

+ * This it's more efficient (speed an memory) that doing calling readRowInt() for each desired line only if the + * image is interlaced. + *

+ * Notice that the columns in the matrix is not the pixel width of the image, but rather pixels x channels + * + * @see #readRowInt(int) to read about the format of each row + * + * @param rowOffset + * Number of rows to be skipped + * @param nRows + * Total number of rows to be read. -1: read all available + * @param rowStep + * Row increment. If 1, we read consecutive lines; if 2, we read even/odd lines, etc + * @return Set of lines as a ImageLines, which wraps a matrix + */ + public ImageLines readRowsInt(int rowOffset, int nRows, int rowStep) { + if (nRows < 0) + nRows = (imgInfo.rows - rowOffset) / rowStep; + if (rowStep < 1 || rowOffset < 0 || nRows * rowStep + rowOffset > imgInfo.rows) + throw new PngjInputException("bad args"); + ImageLines imlines = new ImageLines(imgInfo, SampleType.INT, unpackedMode, rowOffset, nRows, rowStep); + if (!interlaced) { + for (int j = 0; j < imgInfo.rows; j++) { + int bytesread = readRowRaw(j); // read and perhaps discards + int mrow = imlines.imageRowToMatrixRowStrict(j); + if (mrow >= 0) + decodeLastReadRowToInt(imlines.scanlines[mrow], bytesread); + } + } else { // and now, for something completely different (interlaced) + int[] buf = new int[unpackedMode ? imgInfo.samplesPerRow : imgInfo.samplesPerRowPacked]; + for (int p = 1; p <= 7; p++) { + deinterlacer.setPass(p); + for (int i = 0; i < deinterlacer.getRows(); i++) { + int bytesread = readRowRaw(i); + int j = deinterlacer.getCurrRowReal(); + int mrow = imlines.imageRowToMatrixRowStrict(j); + if (mrow >= 0) { + decodeLastReadRowToInt(buf, bytesread); + deinterlacer.deinterlaceInt(buf, imlines.scanlines[mrow], !unpackedMode); + } + } + } + } + end(); + return imlines; + } + + /** + * Same as readRowsInt(0, imgInfo.rows, 1) + * + * @see #readRowsInt(int, int, int) + */ + public ImageLines readRowsInt() { + return readRowsInt(0, imgInfo.rows, 1); + } + + /** + * Reads a set of lines and returns it as a ImageLines object, which wrapas a byte[][] matrix. Internally it reads + * all lines, but decodes and stores only the wanted ones. This starts and ends the reading, and cannot be combined + * with other reading methods. + *

+ * This it's more efficient (speed an memory) that doing calling readRowByte() for each desired line only if the + * image is interlaced. + *

+ * Notice that the columns in the matrix is not the pixel width of the image, but rather pixels x channels + * + * @see #readRowByte(int) to read about the format of each row. Notice that if the bitdepth is 16 this will lose + * information + * + * @param rowOffset + * Number of rows to be skipped + * @param nRows + * Total number of rows to be read. -1: read all available + * @param rowStep + * Row increment. If 1, we read consecutive lines; if 2, we read even/odd lines, etc + * @return Set of lines as a matrix + */ + public ImageLines readRowsByte(int rowOffset, int nRows, int rowStep) { + if (nRows < 0) + nRows = (imgInfo.rows - rowOffset) / rowStep; + if (rowStep < 1 || rowOffset < 0 || nRows * rowStep + rowOffset > imgInfo.rows) + throw new PngjInputException("bad args"); + ImageLines imlines = new ImageLines(imgInfo, SampleType.BYTE, unpackedMode, rowOffset, nRows, rowStep); + if (!interlaced) { + for (int j = 0; j < imgInfo.rows; j++) { + int bytesread = readRowRaw(j); // read and perhaps discards + int mrow = imlines.imageRowToMatrixRowStrict(j); + if (mrow >= 0) + decodeLastReadRowToByte(imlines.scanlinesb[mrow], bytesread); + } + } else { // and now, for something completely different (interlaced) + byte[] buf = new byte[unpackedMode ? imgInfo.samplesPerRow : imgInfo.samplesPerRowPacked]; + for (int p = 1; p <= 7; p++) { + deinterlacer.setPass(p); + for (int i = 0; i < deinterlacer.getRows(); i++) { + int bytesread = readRowRaw(i); + int j = deinterlacer.getCurrRowReal(); + int mrow = imlines.imageRowToMatrixRowStrict(j); + if (mrow >= 0) { + decodeLastReadRowToByte(buf, bytesread); + deinterlacer.deinterlaceByte(buf, imlines.scanlinesb[mrow], !unpackedMode); + } + } + } + } + end(); + return imlines; + } + + /** + * Same as readRowsByte(0, imgInfo.rows, 1) + * + * @see #readRowsByte(int, int, int) + */ + public ImageLines readRowsByte() { + return readRowsByte(0, imgInfo.rows, 1); + } + + /* + * For the interlaced case, nrow indicates the subsampled image - the pass must be set already. + * + * This must be called in strict order, both for interlaced or no interlaced. + * + * Updates rowNum. + * + * Leaves raw result in rowb + * + * Returns bytes actually read (not including the filter byte) + */ + private int readRowRaw(final int nrow) { + // if (nrow == 0 && firstChunksNotYetRead()) readFirstChunks(); - rowNum++; - if (buffer == null || buffer.length < imgInfo.samplesPerRowP) - buffer = new int[imgInfo.samplesPerRowP]; - // swap + if (nrow == 0 && interlaced) + Arrays.fill(rowb, (byte) 0); // new subimage: reset filters: this is enough, see the swap that happens lines + // below + int bytesRead = imgInfo.bytesPerRow; // NOT including the filter byte + if (interlaced) { + if (nrow < 0 || nrow > deinterlacer.getRows() || (nrow != 0 && nrow != deinterlacer.getCurrRowSubimg() + 1)) + throw new PngjInputException("invalid row in interlaced mode: " + nrow); + deinterlacer.setRow(nrow); + bytesRead = (imgInfo.bitspPixel * deinterlacer.getPixelsToRead() + 7) / 8; + if (bytesRead < 1) + throw new PngjExceptionInternal("wtf??"); + } else { // check for non interlaced + if (nrow < 0 || nrow >= imgInfo.rows || nrow != rowNum + 1) + throw new PngjInputException("invalid row: " + nrow); + } + rowNum = nrow; + // swap buffers byte[] tmp = rowb; rowb = rowbprev; rowbprev = tmp; // loads in rowbfilter "raw" bytes, with filter - PngHelper.readBytes(idatIstream, rowbfilter, 0, rowbfilter.length); + PngHelperInternal.readBytes(idatIstream, rowbfilter, 0, bytesRead + 1); + offset = iIdatCstream.getOffset(); + if (offset < 0) + throw new PngjExceptionInternal("bad offset ??" + offset); + if (maxTotalBytesRead > 0 && offset >= maxTotalBytesRead) + throw new PngjInputException("Reading IDAT: Maximum total bytes to read exceeeded: " + maxTotalBytesRead + + " offset:" + offset); rowb[0] = 0; - unfilterRow(); + unfilterRow(bytesRead); rowb[0] = rowbfilter[0]; - convertRowFromBytes(buffer); - return buffer; + if ((rowNum == imgInfo.rows - 1 && !interlaced) || (interlaced && deinterlacer.isAtLastRow())) + readLastAndClose(); + return bytesRead; } /** - * This should be called after having read the last line. It reads extra chunks after IDAT, if present. + * Reads all the (remaining) file, skipping the pixels data. This is much more efficient that calling readRow(), + * specially for big files (about 10 times faster!), because it doesn't even decompress the IDAT stream and disables + * CRC check Use this if you are not interested in reading pixels,only metadata. */ - public void end() { - offset = (int) iIdatCstream.getOffset(); - try { - idatIstream.close(); - } catch (Exception e) { - } - readLastChunks(); + public void readSkippingAllRows() { + if (firstChunksNotYetRead()) + readFirstChunks(); + // we read directly from the compressed stream, we dont decompress nor chec CRC + iIdatCstream.disableCrcCheck(); try { - is.close(); - } catch (Exception e) { - throw new PngjInputException("error closing input stream!", e); + int r; + do { + r = iIdatCstream.read(rowbfilter, 0, rowbfilter.length); + } while (r >= 0); + } catch (IOException e) { + throw new PngjInputException("error in raw read of IDAT", e); } + offset = iIdatCstream.getOffset(); + if (offset < 0) + throw new PngjExceptionInternal("bad offset ??" + offset); + if (maxTotalBytesRead > 0 && offset >= maxTotalBytesRead) + throw new PngjInputException("Reading IDAT: Maximum total bytes to read exceeeded: " + maxTotalBytesRead + + " offset:" + offset); + readLastAndClose(); } - private void convertRowFromBytes(int[] buffer) { - // http://www.libpng.org/pub/png/spec/1.2/PNG-DataRep.html - int i, j; - if (imgInfo.bitDepth <= 8) { - for (i = 0, j = 1; i < imgInfo.samplesPerRowP; i++) { - buffer[i] = (rowb[j++] & 0xFF); - } - } else { // 16 bitspc - for (i = 0, j = 1; i < imgInfo.samplesPerRowP; i++) { - buffer[i] = ((rowb[j++] & 0xFF) << 8) + (rowb[j++] & 0xFF); - } - } + /** + * Set total maximum bytes to read (0: unlimited; default: 200MB).
+ * These are the bytes read (not loaded) in the input stream. If exceeded, an exception will be thrown. + */ + public void setMaxTotalBytesRead(long maxTotalBytesToRead) { + this.maxTotalBytesRead = maxTotalBytesToRead; } - private void unfilterRow() { - int ftn = rowbfilter[0]; - FilterType ft = FilterType.getByVal(ftn); - if (ft == null) - throw new PngjInputException("Filter type " + ftn + " invalid"); - switch (ft) { - case FILTER_NONE: - unfilterRowNone(); - break; - case FILTER_SUB: - unfilterRowSub(); - break; - case FILTER_UP: - unfilterRowUp(); - break; - case FILTER_AVERAGE: - unfilterRowAverage(); - break; - case FILTER_PAETH: - unfilterRowPaeth(); - break; - default: - throw new PngjInputException("Filter type " + ftn + " not implemented"); - } + /** + * @return Total maximum bytes to read. + */ + public long getMaxTotalBytesRead() { + return maxTotalBytesRead; } - private void unfilterRowNone() { - for (int i = 1; i <= imgInfo.bytesPerRow; i++) { - rowb[i] = (byte) (rowbfilter[i]); - } + /** + * Set total maximum bytes to load from ancillary chunks (0: unlimited; default: 5Mb).
+ * If exceeded, some chunks will be skipped + */ + public void setMaxBytesMetadata(int maxBytesChunksToLoad) { + this.maxBytesMetadata = maxBytesChunksToLoad; } - private void unfilterRowSub() { - int i, j; - for (i = 1; i <= imgInfo.bytesPixel; i++) { - rowb[i] = (byte) (rowbfilter[i]); - } - for (j = 1, i = imgInfo.bytesPixel + 1; i <= imgInfo.bytesPerRow; i++, j++) { - rowb[i] = (byte) (rowbfilter[i] + rowb[j]); - } + /** + * @return Total maximum bytes to load from ancillary ckunks. + */ + public int getMaxBytesMetadata() { + return maxBytesMetadata; } - private void unfilterRowUp() { - for (int i = 1; i <= imgInfo.bytesPerRow; i++) { - rowb[i] = (byte) (rowbfilter[i] + rowbprev[i]); - } + /** + * Set maximum size in bytes for individual ancillary chunks (0: unlimited; default: 2MB).
+ * Chunks exceeding this length will be skipped (the CRC will not be checked) and the chunk will be saved as a + * PngChunkSkipped object. See also setSkipChunkIds + */ + public void setSkipChunkMaxSize(int skipChunksBySize) { + this.skipChunkMaxSize = skipChunksBySize; } - private void unfilterRowAverage() { - int i, j, x; - for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imgInfo.bytesPerRow; i++, j++) { - x = j > 0 ? (rowb[j] & 0xff) : 0; - rowb[i] = (byte) (rowbfilter[i] + (x + (rowbprev[i] & 0xFF)) / 2); - } + /** + * @return maximum size in bytes for individual ancillary chunks. + */ + public int getSkipChunkMaxSize() { + return skipChunkMaxSize; } - private void unfilterRowPaeth() { - int i, j, x, y; - for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imgInfo.bytesPerRow; i++, j++) { - x = j > 0 ? (rowb[j] & 0xFF) : 0; - y = j > 0 ? (rowbprev[j] & 0xFF) : 0; - rowb[i] = (byte) (rowbfilter[i] + FilterType.filterPaethPredictor(x, rowbprev[i] & 0xFF, y)); - } + /** + * Chunks ids to be skipped.
+ * These chunks will be skipped (the CRC will not be checked) and the chunk will be saved as a PngChunkSkipped + * object. See also setSkipChunkMaxSize + */ + public void setSkipChunkIds(String[] skipChunksById) { + this.skipChunkIds = skipChunksById == null ? new String[] {} : skipChunksById; } - public ChunkLoadBehaviour getChunkLoadBehaviour() { - return chunkLoadBehaviour; + /** + * @return Chunk-IDs to be skipped. + */ + public String[] getSkipChunkIds() { + return skipChunkIds; } - public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) { - this.chunkLoadBehaviour = chunkLoadBehaviour; + /** + * if true, input stream will be closed after ending read + *

+ * default=true + */ + public void setShouldCloseStream(boolean shouldCloseStream) { + this.shouldCloseStream = shouldCloseStream; } - private boolean firstChunksNotYetRead() { - return currentChunkGroup < ChunkList.CHUNK_GROUP_1_AFTERIDHR; + /** + * Normally this does nothing, but it can be used to force a premature closing. Its recommended practice to call it + * after reading the image pixels. + */ + public void end() { + if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END) + close(); } - public ChunkList getChunksList() { - if (firstChunksNotYetRead()) - readFirstChunks(); - return chunksList; + /** + * Interlaced PNG is accepted -though not welcomed- now... + */ + public boolean isInterlaced() { + return interlaced; } - public PngMetadata getMetadata() { - if (firstChunksNotYetRead()) - readFirstChunks(); - return metadata; + /** + * set/unset "unpackedMode"
+ * If false (default) packed types (bitdepth=1,2 or 4) will keep several samples packed in one element (byte or int)
+ * If true, samples will be unpacked on reading, and each element in the scanline will be sample. This implies more + * processing and memory, but it's the most efficient option if you intend to read individual pixels.
+ * This option should only be set before start reading. + * + * @param unPackedMode + */ + public void setUnpackedMode(boolean unPackedMode) { + this.unpackedMode = unPackedMode; + } + + /** + * @see PngReader#setUnpackedMode(boolean) + */ + public boolean isUnpackedMode() { + return unpackedMode; + } + + /** + * Disables the CRC integrity check in IDAT chunks and ancillary chunks, this gives a slight increase in reading + * speed for big files + */ + public void setCrcCheckDisabled() { + crcEnabled = false; + } + + /** + * Just for testing. TO be called after ending reading, only if initCrctest() was called before start + * + * @return CRC of the raw pixels values + */ + long getCrctestVal() { + return crctest.getValue(); + } + + /** + * Inits CRC object and enables CRC calculation + */ + void initCrctest() { + this.crctest = new CRC32(); } + /** + * Basic info, for debugging. + */ public String toString() { // basic info return "filename=" + filename + " " + imgInfo.toString(); } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngWriter.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngWriter.java index ee8472bf0..601cd96c0 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/PngWriter.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngWriter.java @@ -7,51 +7,84 @@ import java.util.List; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; +import jogamp.opengl.util.pngj.ImageLine.SampleType; import jogamp.opengl.util.pngj.chunks.ChunkCopyBehaviour; import jogamp.opengl.util.pngj.chunks.ChunkHelper; -import jogamp.opengl.util.pngj.chunks.ChunkList; +import jogamp.opengl.util.pngj.chunks.ChunksList; +import jogamp.opengl.util.pngj.chunks.ChunksListForWrite; import jogamp.opengl.util.pngj.chunks.PngChunk; import jogamp.opengl.util.pngj.chunks.PngChunkIEND; import jogamp.opengl.util.pngj.chunks.PngChunkIHDR; +import jogamp.opengl.util.pngj.chunks.PngChunkSkipped; import jogamp.opengl.util.pngj.chunks.PngChunkTextVar; import jogamp.opengl.util.pngj.chunks.PngMetadata; - /** - * Writes a PNG image, line by line. + * Writes a PNG image */ public class PngWriter { public final ImageInfo imgInfo; - protected int compLevel = 6; // zip compression level 0 - 9 - private int deflaterStrategy = Deflater.FILTERED; - protected FilterWriteStrategy filterStrat; + private final String filename; // optional, can be a description + + /** + * last read row number, starting from 0 + */ + protected int rowNum = -1; + + private final ChunksListForWrite chunksList; + private final PngMetadata metadata; // high level wrapper over chunkList + + /** + * Current chunk grounp, (0-6) already read or reading + *

+ * see {@link ChunksList} + */ protected int currentChunkGroup = -1; - protected int rowNum = -1; // current line number - // current line, one (packed) sample per element (layout differnt from rowb!) - protected int[] scanline = null; - protected byte[] rowb = null; // element 0 is filter type! - protected byte[] rowbprev = null; // rowb prev - protected byte[] rowbfilter = null; // current line with filter + /** + * PNG filter strategy + */ + protected FilterWriteStrategy filterStrat; - protected final OutputStream os; - protected final String filename; // optional, can be a description + /** + * zip compression level 0 - 9 + */ + private int compLevel = 6; + private boolean shouldCloseStream = true; // true: closes stream after ending write private PngIDatChunkOutputStream datStream; + private DeflaterOutputStream datStreamDeflated; - private final ChunkList chunkList; - private final PngMetadata metadata; // high level wrapper over chunkList + /** + * Deflate algortithm compression strategy + */ + private int deflaterStrategy = Deflater.FILTERED; + + private int[] histox = new int[256]; // auxiliar buffer, only used by reportResultsForFilter + + private int idatMaxSize = 0; // 0=use default (PngIDatChunkOutputStream 32768) + + private final OutputStream os; + + protected byte[] rowb = null; // element 0 is filter type! + protected byte[] rowbfilter = null; // current line with filter + + protected byte[] rowbprev = null; // rowb prev + + // this only influences the 1-2-4 bitdepth format - and if we pass a ImageLine to writeRow, this is ignored + private boolean unpackedMode = false; public PngWriter(OutputStream outputStream, ImageInfo imgInfo) { this(outputStream, imgInfo, "[NO FILENAME AVAILABLE]"); } /** - * Constructs a new PngWriter from a output stream. + * Constructs a new PngWriter from a output stream. After construction nothing is writen yet. You still can set some + * parameters (compression, filters) and queue chunks before start writing the pixels. *

* See also FileHelper.createPngWriter() if available. * @@ -67,171 +100,156 @@ public class PngWriter { this.os = outputStream; this.imgInfo = imgInfo; // prealloc - scanline = new int[imgInfo.samplesPerRowP]; rowb = new byte[imgInfo.bytesPerRow + 1]; rowbprev = new byte[rowb.length]; rowbfilter = new byte[rowb.length]; - datStream = new PngIDatChunkOutputStream(this.os); - chunkList = new ChunkList(imgInfo); - metadata = new PngMetadata(chunkList, false); - filterStrat = new FilterWriteStrategy(imgInfo, FilterType.FILTER_DEFAULT); + chunksList = new ChunksListForWrite(imgInfo); + metadata = new PngMetadata(chunksList); + filterStrat = new FilterWriteStrategy(imgInfo, FilterType.FILTER_DEFAULT); // can be changed } - /** - * Write id signature and also "IHDR" chunk - */ - private void writeSignatureAndIHDR() { - currentChunkGroup = ChunkList.CHUNK_GROUP_0_IDHR; - if (datStreamDeflated == null) { - Deflater def = new Deflater(compLevel); - def.setStrategy(deflaterStrategy); - datStreamDeflated = new DeflaterOutputStream(datStream, def, 8192); + private void init() { + datStream = new PngIDatChunkOutputStream(this.os, idatMaxSize); + Deflater def = new Deflater(compLevel); + def.setStrategy(deflaterStrategy); + datStreamDeflated = new DeflaterOutputStream(datStream, def); + writeSignatureAndIHDR(); + writeFirstChunks(); + } + + private void reportResultsForFilter(int rown, FilterType type, boolean tentative) { + Arrays.fill(histox, 0); + int s = 0, v; + for (int i = 1; i <= imgInfo.bytesPerRow; i++) { + v = rowbfilter[i]; + if (v < 0) + s -= (int) v; + else + s += (int) v; + histox[v & 0xFF]++; } - PngHelper.writeBytes(os, PngHelper.pngIdBytes); // signature - PngChunkIHDR ihdr = new PngChunkIHDR(imgInfo); - // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html - ihdr.setCols(imgInfo.cols); - ihdr.setRows(imgInfo.rows); - ihdr.setBitspc(imgInfo.bitDepth); - int colormodel = 0; - if (imgInfo.alpha) - colormodel += 0x04; - if (imgInfo.indexed) - colormodel += 0x01; - if (!imgInfo.greyscale) - colormodel += 0x02; - ihdr.setColormodel(colormodel); - ihdr.setCompmeth(0); // compression method 0=deflate - ihdr.setFilmeth(0); // filter method (0) - ihdr.setInterlaced(0); // we never interlace - ihdr.createChunk().writeChunk(os); + filterStrat.fillResultsForFilter(rown, type, s, histox, tentative); + } + private void writeEndChunk() { + PngChunkIEND c = new PngChunkIEND(imgInfo); + c.createRawChunk().writeChunk(os); } private void writeFirstChunks() { int nw = 0; - currentChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR; - nw = chunkList.writeChunks(os, currentChunkGroup); - currentChunkGroup = ChunkList.CHUNK_GROUP_2_PLTE; - nw = chunkList.writeChunks(os, currentChunkGroup); + currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + nw = chunksList.writeChunks(os, currentChunkGroup); + currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE; + nw = chunksList.writeChunks(os, currentChunkGroup); if (nw > 0 && imgInfo.greyscale) throw new PngjOutputException("cannot write palette for this format"); if (nw == 0 && imgInfo.indexed) throw new PngjOutputException("missing palette"); - currentChunkGroup = ChunkList.CHUNK_GROUP_3_AFTERPLTE; - nw = chunkList.writeChunks(os, currentChunkGroup); - currentChunkGroup = ChunkList.CHUNK_GROUP_4_IDAT; + currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + nw = chunksList.writeChunks(os, currentChunkGroup); + currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; } private void writeLastChunks() { // not including end - currentChunkGroup = ChunkList.CHUNK_GROUP_5_AFTERIDAT; - chunkList.writeChunks(os, currentChunkGroup); + currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + chunksList.writeChunks(os, currentChunkGroup); // should not be unwriten chunks - List pending = chunkList.getQueuedChunks(); + List pending = chunksList.getQueuedChunks(); if (!pending.isEmpty()) throw new PngjOutputException(pending.size() + " chunks were not written! Eg: " + pending.get(0).toString()); - currentChunkGroup = ChunkList.CHUNK_GROUP_6_END; - } - - private void writeEndChunk() { - PngChunkIEND c = new PngChunkIEND(imgInfo); - c.createChunk().writeChunk(os); + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; } /** - * Writes a full image row. This must be called sequentially from n=0 to n=rows-1 One integer per sample , in the - * natural order: R G B R G B ... (or R G B A R G B A... if has alpha) The values should be between 0 and 255 for 8 - * bitspc images, and between 0- 65535 form 16 bitspc images (this applies also to the alpha channel if present) The - * array can be reused. - * - * @param newrow - * Array of pixel values - * @param rown - * Row number, from 0 (top) to rows-1 (bottom). This is just used as a check. Pass -1 if you want to - * autocompute it + * Write id signature and also "IHDR" chunk */ - public void writeRow(int[] newrow, int rown) { - if (rown == 0) { - writeSignatureAndIHDR(); - writeFirstChunks(); - } - if (rown < -1 || rown > imgInfo.rows) - throw new RuntimeException("invalid value for row " + rown); - rowNum++; - if (rown >= 0 && rowNum != rown) - throw new RuntimeException("rows must be written in strict consecutive order: tried to write row " + rown - + ", expected=" + rowNum); - scanline = newrow; - // swap - byte[] tmp = rowb; - rowb = rowbprev; - rowbprev = tmp; - convertRowToBytes(); - filterRow(rown); - try { - datStreamDeflated.write(rowbfilter, 0, imgInfo.bytesPerRow + 1); - } catch (IOException e) { - throw new PngjOutputException(e); - } - } + private void writeSignatureAndIHDR() { + currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR; - /** - * Same as writeRow(int[] newrow, int rown), but does not check row number - * - * @param newrow - */ - public void writeRow(int[] newrow) { - writeRow(newrow, -1); - } + PngHelperInternal.writeBytes(os, PngHelperInternal.getPngIdSignature()); // signature + PngChunkIHDR ihdr = new PngChunkIHDR(imgInfo); + // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html + ihdr.setCols(imgInfo.cols); + ihdr.setRows(imgInfo.rows); + ihdr.setBitspc(imgInfo.bitDepth); + int colormodel = 0; + if (imgInfo.alpha) + colormodel += 0x04; + if (imgInfo.indexed) + colormodel += 0x01; + if (!imgInfo.greyscale) + colormodel += 0x02; + ihdr.setColormodel(colormodel); + ihdr.setCompmeth(0); // compression method 0=deflate + ihdr.setFilmeth(0); // filter method (0) + ihdr.setInterlaced(0); // we never interlace + ihdr.createRawChunk().writeChunk(os); - /** - * Writes line. See writeRow(int[] newrow, int rown) - */ - public void writeRow(ImageLine imgline, int rownumber) { - writeRow(imgline.scanline, rownumber); } - /** - * Writes line, checks that the row number is consistent with that of the ImageLine See writeRow(int[] newrow, int - * rown) - * - * @deprecated Better use writeRow(ImageLine imgline, int rownumber) - */ - public void writeRow(ImageLine imgline) { - writeRow(imgline.scanline, imgline.getRown()); - } + protected void encodeRowFromByte(byte[] row) { + if (row.length == imgInfo.samplesPerRowPacked) { + // some duplication of code - because this case is typical and it works faster this way + int j = 1; + if (imgInfo.bitDepth <= 8) { + for (byte x : row) { // optimized + rowb[j++] = x; + } + } else { // 16 bitspc + for (byte x : row) { // optimized + rowb[j] = x; + j += 2; + } + } + } else { + // perhaps we need to pack? + if (row.length >= imgInfo.samplesPerRow && unpackedMode) + ImageLine.packInplaceByte(imgInfo, row, row, false); // row is packed in place! + if (imgInfo.bitDepth <= 8) { + for (int i = 0, j = 1; i < imgInfo.samplesPerRowPacked; i++) { + rowb[j++] = row[i]; + } + } else { // 16 bitspc + for (int i = 0, j = 1; i < imgInfo.samplesPerRowPacked; i++) { + rowb[j++] = row[i]; + rowb[j++] = 0; + } + } - /** - * Finalizes the image creation and closes the stream. This MUST be called after writing the lines. - */ - public void end() { - if (rowNum != imgInfo.rows - 1) - throw new PngjOutputException("all rows have not been written"); - try { - datStreamDeflated.finish(); - datStream.flush(); - writeLastChunks(); - writeEndChunk(); - os.close(); - } catch (IOException e) { - throw new PngjOutputException(e); } } - private int[] histox = new int[256]; // auxiliar buffer, only used by reportResultsForFilter - - private void reportResultsForFilter(int rown, FilterType type, boolean tentative) { - Arrays.fill(histox, 0); - int s = 0, v; - for (int i = 1; i <= imgInfo.bytesPerRow; i++) { - v = rowbfilter[i]; - if (v < 0) - s -= (int) v; - else - s += (int) v; - histox[v & 0xFF]++; + protected void encodeRowFromInt(int[] row) { + // http://www.libpng.org/pub/png/spec/1.2/PNG-DataRep.html + if (row.length == imgInfo.samplesPerRowPacked) { + // some duplication of code - because this case is typical and it works faster this way + int j = 1; + if (imgInfo.bitDepth <= 8) { + for (int x : row) { // optimized + rowb[j++] = (byte) x; + } + } else { // 16 bitspc + for (int x : row) { // optimized + rowb[j++] = (byte) (x >> 8); + rowb[j++] = (byte) (x); + } + } + } else { + // perhaps we need to pack? + if (row.length >= imgInfo.samplesPerRow && unpackedMode) + ImageLine.packInplaceInt(imgInfo, row, row, false); // row is packed in place! + if (imgInfo.bitDepth <= 8) { + for (int i = 0, j = 1; i < imgInfo.samplesPerRowPacked; i++) { + rowb[j++] = (byte) (row[i]); + } + } else { // 16 bitspc + for (int i = 0, j = 1; i < imgInfo.samplesPerRowPacked; i++) { + rowb[j++] = (byte) (row[i] >> 8); + rowb[j++] = (byte) (row[i]); + } + } } - filterStrat.fillResultsForFilter(rown, type, s, histox, tentative); } private void filterRow(int rown) { @@ -268,123 +286,98 @@ public class PngWriter { filterRowPaeth(); break; default: - throw new PngjOutputException("Filter type " + filterType + " not implemented"); + throw new PngjUnsupportedException("Filter type " + filterType + " not implemented"); } reportResultsForFilter(rown, filterType, false); } - protected int sumRowbfilter() { // sums absolute value - int s = 0; - for (int i = 1; i <= imgInfo.bytesPerRow; i++) - if (rowbfilter[i] < 0) - s -= (int) rowbfilter[i]; - else - s += (int) rowbfilter[i]; - return s; + private void prepareEncodeRow(int rown) { + if (datStream == null) + init(); + rowNum++; + if (rown >= 0 && rowNum != rown) + throw new PngjOutputException("rows must be written in order: expected:" + rowNum + " passed:" + rown); + // swap + byte[] tmp = rowb; + rowb = rowbprev; + rowbprev = tmp; } - protected void filterRowNone() { - for (int i = 1; i <= imgInfo.bytesPerRow; i++) { - rowbfilter[i] = (byte) rowb[i]; + private void filterAndSend(int rown) { + filterRow(rown); + try { + datStreamDeflated.write(rowbfilter, 0, imgInfo.bytesPerRow + 1); + } catch (IOException e) { + throw new PngjOutputException(e); } } - protected void filterRowSub() { - int i, j; - for (i = 1; i <= imgInfo.bytesPixel; i++) - rowbfilter[i] = (byte) rowb[i]; - for (j = 1, i = imgInfo.bytesPixel + 1; i <= imgInfo.bytesPerRow; i++, j++) { - rowbfilter[i] = (byte) (rowb[i] - rowb[j]); + protected void filterRowAverage() { + int i, j, imax; + imax = imgInfo.bytesPerRow; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imax; i++, j++) { + rowbfilter[i] = (byte) (rowb[i] - ((rowbprev[i] & 0xFF) + (j > 0 ? (rowb[j] & 0xFF) : 0)) / 2); } } - protected void filterRowUp() { + protected void filterRowNone() { for (int i = 1; i <= imgInfo.bytesPerRow; i++) { - rowbfilter[i] = (byte) (rowb[i] - rowbprev[i]); - } - } - - protected void filterRowAverage() { - int i, j; - for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imgInfo.bytesPerRow; i++, j++) { - rowbfilter[i] = (byte) (rowb[i] - ((rowbprev[i] & 0xFF) + (j > 0 ? (rowb[j] & 0xFF) : 0)) / 2); + rowbfilter[i] = (byte) rowb[i]; } } protected void filterRowPaeth() { - int i, j; - for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imgInfo.bytesPerRow; i++, j++) { - rowbfilter[i] = (byte) (rowb[i] - FilterType.filterPaethPredictor(j > 0 ? (rowb[j] & 0xFF) : 0, - rowbprev[i] & 0xFF, j > 0 ? (rowbprev[j] & 0xFF) : 0)); + int i, j, imax; + imax = imgInfo.bytesPerRow; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imax; i++, j++) { + // rowbfilter[i] = (byte) (rowb[i] - PngHelperInternal.filterPaethPredictor(j > 0 ? (rowb[j] & 0xFF) : 0, + // rowbprev[i] & 0xFF, j > 0 ? (rowbprev[j] & 0xFF) : 0)); + rowbfilter[i] = (byte) PngHelperInternal.filterRowPaeth(rowb[i], j > 0 ? (rowb[j] & 0xFF) : 0, + rowbprev[i] & 0xFF, j > 0 ? (rowbprev[j] & 0xFF) : 0); } } - protected void convertRowToBytes() { - // http://www.libpng.org/pub/png/spec/1.2/PNG-DataRep.html + protected void filterRowSub() { int i, j; - if (imgInfo.bitDepth <= 8) { - for (i = 0, j = 1; i < imgInfo.samplesPerRowP; i++) { - rowb[j++] = (byte) (scanline[i]); - } - } else { // 16 bitspc - for (i = 0, j = 1; i < imgInfo.samplesPerRowP; i++) { - // x = (int) (scanline[i]) & 0xFFFF; - rowb[j++] = (byte) (scanline[i] >> 8); - rowb[j++] = (byte) (scanline[i]); - } + for (i = 1; i <= imgInfo.bytesPixel; i++) + rowbfilter[i] = (byte) rowb[i]; + for (j = 1, i = imgInfo.bytesPixel + 1; i <= imgInfo.bytesPerRow; i++, j++) { + // !!! rowbfilter[i] = (byte) (rowb[i] - rowb[j]); + rowbfilter[i] = (byte) PngHelperInternal.filterRowSub(rowb[i], rowb[j]); } } - // /// several getters / setters - all this setters are optional - - /** - * Filename or description, from the optional constructor argument. - */ - public String getFilename() { - return filename; + protected void filterRowUp() { + for (int i = 1; i <= imgInfo.bytesPerRow; i++) { + // rowbfilter[i] = (byte) (rowb[i] - rowbprev[i]); !!! + rowbfilter[i] = (byte) PngHelperInternal.filterRowUp(rowb[i], rowbprev[i]); + } } - /** - * Sets internal prediction filter type, or strategy to choose it. - *

- * This must be called just after constructor, before starting writing. - *

- * See also setCompLevel() - * - * @param filterType - * One of the five prediction types or strategy to choose it (see PngFilterType) Recommended - * values: DEFAULT (default) or AGGRESIVE - */ - public void setFilterType(FilterType filterType) { - filterStrat = new FilterWriteStrategy(imgInfo, filterType); + protected int sumRowbfilter() { // sums absolute value + int s = 0; + for (int i = 1; i <= imgInfo.bytesPerRow; i++) + if (rowbfilter[i] < 0) + s -= (int) rowbfilter[i]; + else + s += (int) rowbfilter[i]; + return s; } /** - * Sets compression level of ZIP algorithm. + * copy chunks from reader - copy_mask : see ChunksToWrite.COPY_XXX *

- * This must be called just after constructor, before starting writing. + * If we are after idat, only considers those chunks after IDAT in PngReader *

- * See also setFilterType() - * - * @param compLevel - * between 0 and 9 (default:6 , recommended: 6 or more) - */ - public void setCompLevel(int compLevel) { - if (compLevel < 0 || compLevel > 9) - throw new PngjException("Compression level invalid (" + compLevel + ") Must be 0..9"); - this.compLevel = compLevel; - } - - /** - * copy chunks from reader - copy_mask : see ChunksToWrite.COPY_XXX - * - * If we are after idat, only considers those chunks after IDAT in PngReader TODO: this should be more customizable + * TODO: this should be more customizable */ private void copyChunks(PngReader reader, int copy_mask, boolean onlyAfterIdat) { - boolean idatDone = currentChunkGroup >= ChunkList.CHUNK_GROUP_4_IDAT; + boolean idatDone = currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT; + if (onlyAfterIdat && reader.getCurrentChunkGroup() < ChunksList.CHUNK_GROUP_6_END) + throw new PngjExceptionInternal("tried to copy last chunks but reader has not ended"); for (PngChunk chunk : reader.getChunksList().getChunks()) { int group = chunk.getChunkGroup(); - if (group < ChunkList.CHUNK_GROUP_4_IDAT && idatDone) + if (group < ChunksList.CHUNK_GROUP_4_IDAT && idatDone) continue; boolean copy = false; if (chunk.crit) { @@ -413,9 +406,11 @@ public class PngWriter { && !(ChunkHelper.isUnknown(chunk) || text || chunk.id.equals(ChunkHelper.hIST) || chunk.id .equals(ChunkHelper.tIME))) copy = true; + if (chunk instanceof PngChunkSkipped) + copy = false; } if (copy) { - chunkList.queueChunk(PngChunk.cloneChunk(chunk, imgInfo), !chunk.allowsMultiple(), false); + chunksList.queue(PngChunk.cloneChunk(chunk, imgInfo)); } } } @@ -451,12 +446,228 @@ public class PngWriter { copyChunks(reader, copy_mask, true); } - public ChunkList getChunkList() { - return chunkList; + /** + * Computes compressed size/raw size, approximate. + *

+ * Actually: compressed size = total size of IDAT data , raw size = uncompressed pixel bytes = rows * (bytesPerRow + + * 1). + * + * This must be called after pngw.end() + */ + public double computeCompressionRatio() { + if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END) + throw new PngjOutputException("must be called after end()"); + double compressed = (double) datStream.getCountFlushed(); + double raw = (imgInfo.bytesPerRow + 1) * imgInfo.rows; + return compressed / raw; } + /** + * Finalizes the image creation and closes the stream. This MUST be called after writing the lines. + */ + public void end() { + if (rowNum != imgInfo.rows - 1) + throw new PngjOutputException("all rows have not been written"); + try { + datStreamDeflated.finish(); + datStream.flush(); + writeLastChunks(); + writeEndChunk(); + if (shouldCloseStream) + os.close(); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + /** + * returns the chunks list (queued and writen chunks) + */ + public ChunksListForWrite getChunksList() { + return chunksList; + } + + /** + * Filename or description, from the optional constructor argument. + */ + public String getFilename() { + return filename; + } + + /** + * High level wrapper over chunksList for metadata handling + */ public PngMetadata getMetadata() { return metadata; } + /** + * Sets compression level of ZIP algorithm. + *

+ * This must be called just after constructor, before starting writing. + *

+ * See also setFilterType() + * + * @param compLevel + * between 0 and 9 (default:6 , recommended: 6 or more) + */ + public void setCompLevel(int compLevel) { + if (compLevel < 0 || compLevel > 9) + throw new PngjOutputException("Compression level invalid (" + compLevel + ") Must be 0..9"); + this.compLevel = compLevel; + } + + /** + * Sets internal prediction filter type, or strategy to choose it. + *

+ * This must be called just after constructor, before starting writing. + *

+ * See also setCompLevel() + * + * @param filterType + * One of the five prediction types or strategy to choose it (see PngFilterType) Recommended + * values: DEFAULT (default) or AGGRESIVE + */ + public void setFilterType(FilterType filterType) { + filterStrat = new FilterWriteStrategy(imgInfo, filterType); + } + + /** + * Sets maximum size of IDAT fragments. This has little effect on performance you should rarely call this + *

+ * + * @param idatMaxSize + * default=0 : use defaultSize (32K) + */ + public void setIdatMaxSize(int idatMaxSize) { + this.idatMaxSize = idatMaxSize; + } + + /** + * if true, input stream will be closed after ending write + *

+ * default=true + */ + public void setShouldCloseStream(boolean shouldCloseStream) { + this.shouldCloseStream = shouldCloseStream; + } + + /** + * Deflater strategy: one of Deflater.FILTERED Deflater.HUFFMAN_ONLY Deflater.DEFAULT_STRATEGY + *

+ * Default: Deflater.FILTERED . This should be changed very rarely. + */ + public void setDeflaterStrategy(int deflaterStrategy) { + this.deflaterStrategy = deflaterStrategy; + } + + /** + * Writes line, checks that the row number is consistent with that of the ImageLine See writeRow(int[] newrow, int + * rown) + * + * @deprecated Better use writeRow(ImageLine imgline, int rownumber) + */ + public void writeRow(ImageLine imgline) { + writeRow(imgline.scanline, imgline.getRown()); + } + + /** + * Writes line. See writeRow(int[] newrow, int rown) + * + * The packed flag of the imageline is honoured! + * + * @see #writeRowInt(int[], int) + */ + public void writeRow(ImageLine imgline, int rownumber) { + unpackedMode = imgline.samplesUnpacked; + if (imgline.sampleType == SampleType.INT) + writeRowInt(imgline.scanline, rownumber); + else + writeRowByte(imgline.scanlineb, rownumber); + } + + /** + * Same as writeRow(int[] newrow, int rown), but does not check row number + * + * @param newrow + */ + public void writeRow(int[] newrow) { + writeRow(newrow, -1); + } + + /** + * Alias to writeRowInt + * + * @see #writeRowInt(int[], int) + */ + public void writeRow(int[] newrow, int rown) { + writeRowInt(newrow, rown); + } + + /** + * Writes a full image row. + *

+ * This must be called sequentially from n=0 to n=rows-1 One integer per sample , in the natural order: R G B R G B + * ... (or R G B A R G B A... if has alpha) The values should be between 0 and 255 for 8 bitspc images, and between + * 0- 65535 form 16 bitspc images (this applies also to the alpha channel if present) The array can be reused. + *

+ * Warning: the array might be modified in some cases (unpacked row with low bitdepth) + *

+ * + * @param newrow + * Array of pixel values. Warning: the array size should be exact (samplesPerRowP) + * @param rown + * Row number, from 0 (top) to rows-1 (bottom). This is just used as a check. Pass -1 if you want to + * autocompute it + */ + public void writeRowInt(int[] newrow, int rown) { + prepareEncodeRow(rown); + encodeRowFromInt(newrow); + filterAndSend(rown); + } + + /** + * Same semantics as writeRowInt but using bytes. Each byte is still a sample. If 16bitdepth, we are passing only + * the most significant byte (and hence losing some info) + * + * @see PngWriter#writeRowInt(int[], int) + */ + public void writeRowByte(byte[] newrow, int rown) { + prepareEncodeRow(rown); + encodeRowFromByte(newrow); + filterAndSend(rown); + } + + /** + * Writes all the pixels, calling writeRowInt() for each image row + */ + public void writeRowsInt(int[][] image) { + for (int i = 0; i < imgInfo.rows; i++) + writeRowInt(image[i], i); + } + + /** + * Writes all the pixels, calling writeRowByte() for each image row + */ + public void writeRowsByte(byte[][] image) { + for (int i = 0; i < imgInfo.rows; i++) + writeRowByte(image[i], i); + } + + public boolean isUnpackedMode() { + return unpackedMode; + } + + /** + * If false (default), and image has bitdepth 1-2-4, the scanlines passed are assumed to be already packed. + *

+ * If true, each element is a sample, the writer will perform the packing if necessary. + *

+ * Warning: when using {@link #writeRow(ImageLine, int)} (recommended) the packed flag of the ImageLine + * object overrides (and overwrites!) this field. + */ + public void setUseUnPackedMode(boolean useUnpackedMode) { + this.unpackedMode = useUnpackedMode; + } + } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngjExceptionInternal.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngjExceptionInternal.java new file mode 100644 index 000000000..963abc50e --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngjExceptionInternal.java @@ -0,0 +1,23 @@ +package jogamp.opengl.util.pngj; + +/** + * Exception for anomalous internal problems (sort of asserts) that point to some issue with the library + * + * @author Hernan J Gonzalez + * + */ +public class PngjExceptionInternal extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PngjExceptionInternal(String message, Throwable cause) { + super(message, cause); + } + + public PngjExceptionInternal(String message) { + super(message); + } + + public PngjExceptionInternal(Throwable cause) { + super(cause); + } +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/ProgressiveOutputStream.java b/src/jogl/classes/jogamp/opengl/util/pngj/ProgressiveOutputStream.java index bbec247fb..a5bad666c 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/ProgressiveOutputStream.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ProgressiveOutputStream.java @@ -8,6 +8,7 @@ import java.io.IOException; */ abstract class ProgressiveOutputStream extends ByteArrayOutputStream { private final int size; + private long countFlushed = 0; public ProgressiveOutputStream(int size) { this.size = size; @@ -60,6 +61,7 @@ abstract class ProgressiveOutputStream extends ByteArrayOutputStream { if (nb == 0) return; flushBuffer(buf, nb); + countFlushed += nb; int bytesleft = count - nb; count = bytesleft; if (bytesleft > 0) @@ -67,5 +69,9 @@ abstract class ProgressiveOutputStream extends ByteArrayOutputStream { } } - public abstract void flushBuffer(byte[] b, int n); + protected abstract void flushBuffer(byte[] b, int n); + + public long getCountFlushed() { + return countFlushed; + } } \ No newline at end of file diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkCopyBehaviour.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkCopyBehaviour.java index 43c0cb135..a2d976fac 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkCopyBehaviour.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkCopyBehaviour.java @@ -1,8 +1,9 @@ package jogamp.opengl.util.pngj.chunks; /** - * Chunk copy policy to apply when copyng from a pngReader to a pngWriter http://www.w3.org/TR/PNG/#14 + * Chunk copy policy to apply when copyng from a pngReader to a pngWriter. *

+ * http://www.w3.org/TR/PNG/#14
* These are masks, can be OR-ed **/ public class ChunkCopyBehaviour { diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java index 26dafd4eb..ed091d35a 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java @@ -8,11 +8,13 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Set; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; @@ -41,38 +43,83 @@ public class ChunkHelper { public static final String tEXt = "tEXt"; public static final String zTXt = "zTXt"; - public static Set KNOWN_CHUNKS_CRITICAL = PngHelper.asSet(IHDR, PLTE, IDAT, IEND); - + /** + * Converts to bytes using Latin1 (ISO-8859-1) + */ public static byte[] toBytes(String x) { - return x.getBytes(PngHelper.charsetLatin1); + return x.getBytes(PngHelperInternal.charsetLatin1); } + /** + * Converts to String using Latin1 (ISO-8859-1) + */ public static String toString(byte[] x) { - return new String(x, PngHelper.charsetLatin1); + return new String(x, PngHelperInternal.charsetLatin1); } - public static boolean isCritical(String id) { // critical chunk ? - // first letter is uppercase + /** + * Converts to String using Latin1 (ISO-8859-1) + */ + public static String toString(byte[] x, int offset, int len) { + return new String(x, offset, len, PngHelperInternal.charsetLatin1); + } + + /** + * Converts to bytes using UTF-8 + */ + public static byte[] toBytesUTF8(String x) { + return x.getBytes(PngHelperInternal.charsetUTF8); + } + + /** + * Converts to string using UTF-8 + */ + public static String toStringUTF8(byte[] x) { + return new String(x, PngHelperInternal.charsetUTF8); + } + + /** + * Converts to string using UTF-8 + */ + public static String toStringUTF8(byte[] x, int offset, int len) { + return new String(x, offset, len, PngHelperInternal.charsetUTF8); + } + + /** + * critical chunk : first letter is uppercase + */ + public static boolean isCritical(String id) { return (Character.isUpperCase(id.charAt(0))); } - public static boolean isPublic(String id) { // public chunk? - // second letter is uppercase + /** + * public chunk: second letter is uppercase + */ + public static boolean isPublic(String id) { // return (Character.isUpperCase(id.charAt(1))); } /** - * "Unknown" just means that our chunk factory (even when it has been augmented by client code) did not recognize its id + * Safe to copy chunk: fourth letter is lower case */ - public static boolean isUnknown(PngChunk c) { - return c instanceof PngChunkUNKNOWN; + public static boolean isSafeToCopy(String id) { + return (!Character.isUpperCase(id.charAt(3))); } - public static boolean isSafeToCopy(String id) { // safe to copy? - // fourth letter is lower case - return (!Character.isUpperCase(id.charAt(3))); + /** + * "Unknown" just means that our chunk factory (even when it has been augmented by client code) did not recognize + * its id + */ + public static boolean isUnknown(PngChunk c) { + return c instanceof PngChunkUNKNOWN; } + /** + * Finds position of null byte in array + * + * @param b + * @return -1 if not found + */ public static int posNullByte(byte[] b) { for (int i = 0; i < b.length; i++) if (b[i] == 0) @@ -80,6 +127,13 @@ public class ChunkHelper { return -1; } + /** + * Decides if a chunk should be loaded, according to a ChunkLoadBehaviour + * + * @param id + * @param behav + * @return true/false + */ public static boolean shouldLoad(String id, ChunkLoadBehaviour behav) { if (isCritical(id)) return true; @@ -131,4 +185,69 @@ public class ChunkHelper { return (v & mask) != 0; } + /** + * Returns only the chunks that "match" the predicate + * + * See also trimList() + */ + public static List filterList(List target, ChunkPredicate predicateKeep) { + List result = new ArrayList(); + for (PngChunk element : target) { + if (predicateKeep.match(element)) { + result.add(element); + } + } + return result; + } + + /** + * Remove (in place) the chunks that "match" the predicate + * + * See also filterList + */ + public static int trimList(List target, ChunkPredicate predicateRemove) { + Iterator it = target.iterator(); + int cont = 0; + while (it.hasNext()) { + PngChunk c = it.next(); + if (predicateRemove.match(c)) { + it.remove(); + cont++; + } + } + return cont; + } + + /** + * MY adhoc criteria: two chunks are "equivalent" ("practically equal") if they have same id and (perhaps, if + * multiple are allowed) if the match also in some "internal key" (eg: key for string values, palette for sPLT, etc) + * + * Notice that the use of this is optional, and that the PNG standard allows Text chunks that have same key + * + * @return true if "equivalent" + */ + public static final boolean equivalent(PngChunk c1, PngChunk c2) { + if (c1 == c2) + return true; + if (c1 == null || c2 == null || !c1.id.equals(c2.id)) + return false; + // same id + if (c1.getClass() != c2.getClass()) + return false; // should not happen + if (!c2.allowsMultiple()) + return true; + if (c1 instanceof PngChunkTextVar) { + return ((PngChunkTextVar) c1).getKey().equals(((PngChunkTextVar) c2).getKey()); + } + if (c1 instanceof PngChunkSPLT) { + return ((PngChunkSPLT) c1).getPalName().equals(((PngChunkSPLT) c2).getPalName()); + } + // unknown chunks that allow multiple? consider they don't match + return false; + } + + public static boolean isText(PngChunk c) { + return c instanceof PngChunkTextVar; + } + } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java deleted file mode 100644 index badbbd0e8..000000000 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java +++ /dev/null @@ -1,282 +0,0 @@ -package jogamp.opengl.util.pngj.chunks; - -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngjException; - - -/** - * All chunks that form an image, read or to be written - * - * chunks include all chunks, but IDAT is a single pseudo chunk without data - **/ -public class ChunkList { - // ref: http://www.w3.org/TR/PNG/#table53 - public static final int CHUNK_GROUP_0_IDHR = 0; // required - single - public static final int CHUNK_GROUP_1_AFTERIDHR = 1; // optional - multiple - public static final int CHUNK_GROUP_2_PLTE = 2; // optional - single - public static final int CHUNK_GROUP_3_AFTERPLTE = 3; // optional - multple - public static final int CHUNK_GROUP_4_IDAT = 4; // required (single pseudo chunk) - public static final int CHUNK_GROUP_5_AFTERIDAT = 5; // optional - multple - public static final int CHUNK_GROUP_6_END = 6; // only 1 chunk - requried - - /** - * All chunks, read, written (does not include IHDR, IDAT, END for written) - */ - private List chunks = new ArrayList(); - - /** - * chunks not yet writen - does not include IHDR, IDAT, END, perhaps yes PLTE - */ - private Set queuedChunks = new LinkedHashSet(); - - final ImageInfo imageInfo; // only required for writing - - public ChunkList(ImageInfo imfinfo) { - this.imageInfo = imfinfo; - } - - /** - * Adds chunk in next position. This is used when reading - */ - public void appendReadChunk(PngChunk chunk, int chunkGroup) { - chunk.setChunkGroup(chunkGroup); - chunks.add(chunk); - } - - public List getById(String id, boolean includeQueued, boolean includeProcessed) { - List list = new ArrayList(); - if (includeQueued) - for (PngChunk c : queuedChunks) - if (c.id.equals(id)) - list.add(c); - if (includeProcessed) - for (PngChunk c : chunks) - if (c.id.equals(id)) - list.add(c); - return list; - } - - /** - * Remove Chunk: only from queued - */ - public boolean removeChunk(PngChunk c) { - return queuedChunks.remove(c); - } - - /** - * add chunk to write queue - */ - public void queueChunk(PngChunk chunk, boolean replace, boolean priority) { - chunk.setPriority(priority); - if (replace) { - List current = getById(chunk.id, true, false); - for (PngChunk chunk2 : current) - removeChunk(chunk2); - } - queuedChunks.add(chunk); - } - - /** - * this should be called only for ancillary chunks and PLTE (groups 1 - 3 - 5) - **/ - private static boolean shouldWrite(PngChunk c, int currentGroup) { - if (currentGroup == CHUNK_GROUP_2_PLTE) - return c.id.equals(ChunkHelper.PLTE); - if (currentGroup % 2 == 0) - throw new RuntimeException("?"); - int minChunkGroup, maxChunkGroup; - if (c.mustGoBeforePLTE()) - minChunkGroup = maxChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR; - else if (c.mustGoBeforeIDAT()) { - maxChunkGroup = ChunkList.CHUNK_GROUP_3_AFTERPLTE; - minChunkGroup = c.mustGoAfterPLTE() ? ChunkList.CHUNK_GROUP_3_AFTERPLTE : ChunkList.CHUNK_GROUP_1_AFTERIDHR; - } else { - maxChunkGroup = ChunkList.CHUNK_GROUP_5_AFTERIDAT; - minChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR; - } - - int preferred = maxChunkGroup; - if (c.isWritePriority()) - preferred = minChunkGroup; - if (ChunkHelper.isUnknown(c) && c.getChunkGroup() > 0) - preferred = c.getChunkGroup(); - if (currentGroup == preferred) - return true; - if (currentGroup > preferred && currentGroup <= maxChunkGroup) - return true; - return false; - } - - public int writeChunks(OutputStream os, int currentGroup) { - int cont = 0; - Iterator it = queuedChunks.iterator(); - while (it.hasNext()) { - PngChunk c = it.next(); - if (!shouldWrite(c, currentGroup)) - continue; - c.write(os); - chunks.add(c); - c.setChunkGroup(currentGroup); - it.remove(); - cont++; - } - return cont; - } - - /** - * returns a copy of processed (read or writen) chunks - */ - public List getChunks() { - return new ArrayList(chunks); - } - - public List getChunksUnkown() { - List l = new ArrayList(); - for (PngChunk chunk : chunks) - if (ChunkHelper.isUnknown(chunk)) - l.add(chunk.id); - return l; - } - - /** - * returns a copy of queued (for write) chunks - */ - public List getQueuedChunks() { - return new ArrayList(queuedChunks); - } - - /** - * behaviour: - * - * a chunk already processed matches : exception a chunk queued matches and overwrite=true: replace it , return true - * a chunk queued matches and overwrite=false: do nothing, return false no matching: set it, return true - * - * @param c - * @param overwriteIfPresent - * @return true if added chunk - */ - public boolean setChunk(PngChunk c, boolean overwriteIfPresent) { - List list = getMatching(c, false, true); // processed - if (!list.isEmpty()) - throw new PngjException("chunk " + c.id + " already set "); - list = getMatching(c, true, false); // queued - if (!list.isEmpty()) { - if (overwriteIfPresent) { - for (PngChunk cx : list) - removeChunk(cx); - queueChunk(c, false, false); - return true; - } - return false; - } - queueChunk(c, false, false); - return true; - } - - /** - * returns only one chunk or null if nothing found - does not include queued - * - * If innerid!=null , the chunk is assumed to be PngChunkTextVar or PngChunkSPLT, and filtered by that id - * - * If more than one chunk (after filtering by inner id) is found, then an exception is thrown (failifMultiple=true) - * or the last one is returned (failifMultiple=false) - **/ - public PngChunk getChunk1(String id, String innerid, boolean failIfMultiple) { - List list = getChunks(id); - if (list.isEmpty()) - return null; - if (innerid != null) { - List list2 = new ArrayList(); - for (PngChunk c : list) { - if (c instanceof PngChunkTextVar) - if (((PngChunkTextVar) c).getKey().equals(innerid)) - list2.add(c); - if (c instanceof PngChunkSPLT) - if (((PngChunkSPLT) c).getPalName().equals(innerid)) - list2.add(c); - } - list = list2; - } - if (list.isEmpty()) - return null; - if (list.size() > 1 && failIfMultiple) - throw new PngjException("unexpected multiple chunks id=" + id); - return list.get(list.size() - 1); - } - - public PngChunk getChunk1(String id) { - return getChunk1(id, null, true); - } - - public List getChunks(String id) { // not including queued - return getById(id, false, true); - } - - private List getMatching(PngChunk cnew, boolean includeQueued, boolean includeProcessed) { - List list = new ArrayList(); - if (includeQueued) - for (PngChunk c : getQueuedChunks()) - if (matches(cnew, c)) - list.add(c); - if (includeProcessed) - for (PngChunk c : getChunks()) - if (matches(cnew, c)) - list.add(c); - return list; - } - - /** - * MY adhoc criteria: two chunks "match" if they have same id and (perhaps, if multiple are allowed) if the match - * also in some "internal key" (eg: key for string values, palette for sPLT, etc) - * - * @return true if "matches" - */ - public static boolean matches(PngChunk c2, PngChunk c1) { - if (c1 == null || c2 == null || !c1.id.equals(c2.id)) - return false; - // same id - if (c1.getClass() != c2.getClass()) - return false; // should not happen - if (!c2.allowsMultiple()) - return true; - if (c1 instanceof PngChunkTextVar) { - return ((PngChunkTextVar) c1).getKey().equals(((PngChunkTextVar) c2).getKey()); - } - if (c1 instanceof PngChunkSPLT) { - return ((PngChunkSPLT) c1).getPalName().equals(((PngChunkSPLT) c2).getPalName()); - } - // unknown chunks that allow multiple? consider they don't match - return false; - } - - public String toString() { - return "ChunkList: processed: " + chunks.size() + " queue: " + queuedChunks.size(); - } - - /** - * for debugging - */ - public String toStringFull() { - StringBuilder sb = new StringBuilder(toString()); - sb.append("\n Processed:\n"); - for (PngChunk chunk : chunks) { - sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); - } - if (!queuedChunks.isEmpty()) { - sb.append(" Queued:\n"); - for (PngChunk chunk : chunks) { - sb.append(chunk).append("\n"); - } - - } - return sb.toString(); - } - -} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java index a3f85355c..03d50c2c4 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java @@ -1,10 +1,25 @@ package jogamp.opengl.util.pngj.chunks; +/** + * Defines gral strategy about what to do with ancillary (non-critical) chunks when reading + */ public enum ChunkLoadBehaviour { - // what to do with non critical chunks when reading? - LOAD_CHUNK_NEVER, /* ignore non-critical chunks */ - LOAD_CHUNK_KNOWN, /* load chunk if 'known' */ - LOAD_CHUNK_IF_SAFE, /* load chunk if 'known' or safe to copy */ - LOAD_CHUNK_ALWAYS /* load chunk always */ - ; + /** + * All non-critical chunks are skipped + */ + LOAD_CHUNK_NEVER, + /** + * Ancillary chunks are loaded only if 'known' (registered with the factory). + */ + LOAD_CHUNK_KNOWN, + /** + * + * Load chunk if "known" or "safe to copy". + */ + LOAD_CHUNK_IF_SAFE, + /** + * Load all chunks.
+ * Notice that other restrictions might apply, see PngReader.skipChunkMaxSize PngReader.skipChunkIds + */ + LOAD_CHUNK_ALWAYS; } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkPredicate.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkPredicate.java new file mode 100644 index 000000000..a750ae34f --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkPredicate.java @@ -0,0 +1,14 @@ +package jogamp.opengl.util.pngj.chunks; + +/** + * Decides if another chunk "matches", according to some criterion + */ +public interface ChunkPredicate { + /** + * The other chunk matches with this one + * + * @param chunk + * @return true if match + */ + boolean match(PngChunk chunk); +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java index 6770d5e95..8dd0ef476 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java @@ -5,24 +5,47 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.zip.CRC32; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjBadCrcException; import jogamp.opengl.util.pngj.PngjOutputException; /** - * Wraps the raw chunk data Short lived object, to be created while serialing/deserializing Do not reuse it for - * different chunks - * - * see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html + * Raw (physical) chunk. + *

+ * Short lived object, to be created while serialing/deserializing Do not reuse it for different chunks.
+ * See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html */ public class ChunkRaw { + /** + * The length counts only the data field, not itself, the chunk type code, or the CRC. Zero is a valid length. + * Although encoders and decoders should treat the length as unsigned, its value must not exceed 231-1 bytes. + */ public final int len; - public final byte[] idbytes = new byte[4]; // 4 bytes - public byte[] data = null; // crc not included + + /** + * A 4-byte chunk type code. uppercase and lowercase ASCII letters + */ + public final byte[] idbytes = new byte[4]; + + /** + * The data bytes appropriate to the chunk type, if any. This field can be of zero length. Does not include crc + */ + public byte[] data = null; + /** + * A 4-byte CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk, including the chunk type + * code and chunk data fields, but not including the length field. + */ private int crcval = 0; - // public int offset=-1; // only for read chunks - informational + /** + * @param len + * : data len + * @param idbytes + * : chunk type (deep copied) + * @param alloc + * : it true, the data array will be allocced + */ public ChunkRaw(int len, byte[] idbytes, boolean alloc) { this.len = len; System.arraycopy(idbytes, 0, this.idbytes, 0, 4); @@ -30,54 +53,58 @@ public class ChunkRaw { allocData(); } - public void writeChunk(OutputStream os) { - if (idbytes.length != 4) - throw new PngjOutputException("bad chunkid [" + ChunkHelper.toString(idbytes) + "]"); - computeCrc(); - PngHelper.writeInt4(os, len); - PngHelper.writeBytes(os, idbytes); - if (len > 0) - PngHelper.writeBytes(os, data, 0, len); - // System.err.println("writing chunk " + this.toString() + "crc=" + crcval); - - PngHelper.writeInt4(os, crcval); + private void allocData() { + if (data == null || data.length < len) + data = new byte[len]; } /** - * called after setting data, before writing to os + * this is called after setting data, before writing to os */ - private void computeCrc() { - CRC32 crcengine = PngHelper.getCRC(); + private int computeCrc() { + CRC32 crcengine = PngHelperInternal.getCRC(); crcengine.reset(); crcengine.update(idbytes, 0, 4); if (len > 0) crcengine.update(data, 0, len); // - crcval = (int) crcengine.getValue(); + return (int) crcengine.getValue(); } - public String toString() { - return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len; + /** + * Computes the CRC and writes to the stream. If error, a PngjOutputException is thrown + */ + public void writeChunk(OutputStream os) { + if (idbytes.length != 4) + throw new PngjOutputException("bad chunkid [" + ChunkHelper.toString(idbytes) + "]"); + crcval = computeCrc(); + PngHelperInternal.writeInt4(os, len); + PngHelperInternal.writeBytes(os, idbytes); + if (len > 0) + PngHelperInternal.writeBytes(os, data, 0, len); + PngHelperInternal.writeInt4(os, crcval); } /** * position before: just after chunk id. positon after: after crc Data should be already allocated. Checks CRC * Return number of byte read. */ - public int readChunkData(InputStream is) { - PngHelper.readBytes(is, data, 0, len); - int crcori = PngHelper.readInt4(is); - computeCrc(); - if (crcori != crcval) - throw new PngjBadCrcException("crc invalid for chunk " + toString() + " calc=" + crcval + " read=" + crcori); + public int readChunkData(InputStream is, boolean checkCrc) { + PngHelperInternal.readBytes(is, data, 0, len); + crcval = PngHelperInternal.readInt4(is); + if (checkCrc) { + int crc = computeCrc(); + if (crc != crcval) + throw new PngjBadCrcException("chunk: " + this + " crc calc=" + crc + " read=" + crcval); + } return len + 4; } - public ByteArrayInputStream getAsByteStream() { // only the data + ByteArrayInputStream getAsByteStream() { // only the data return new ByteArrayInputStream(data); } - private void allocData() { - if (data == null || data.length < len) - data = new byte[len]; + public String toString() { + return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len; } + } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksList.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksList.java new file mode 100644 index 000000000..ad788f154 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksList.java @@ -0,0 +1,174 @@ +package jogamp.opengl.util.pngj.chunks; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import jogamp.opengl.util.pngj.ImageInfo; +import jogamp.opengl.util.pngj.PngjException; + +/** + * All chunks that form an image, read or to be written. + *

+ * chunks include all chunks, but IDAT is a single pseudo chunk without data + **/ +public class ChunksList { + // ref: http://www.w3.org/TR/PNG/#table53 + public static final int CHUNK_GROUP_0_IDHR = 0; // required - single + public static final int CHUNK_GROUP_1_AFTERIDHR = 1; // optional - multiple + public static final int CHUNK_GROUP_2_PLTE = 2; // optional - single + public static final int CHUNK_GROUP_3_AFTERPLTE = 3; // optional - multple + public static final int CHUNK_GROUP_4_IDAT = 4; // required (single pseudo chunk) + public static final int CHUNK_GROUP_5_AFTERIDAT = 5; // optional - multple + public static final int CHUNK_GROUP_6_END = 6; // only 1 chunk - requried + + /** + * All chunks, read (or written) + * + * But IDAT is a single pseudo chunk without data + */ + protected List chunks = new ArrayList(); + + final ImageInfo imageInfo; // only required for writing + + public ChunksList(ImageInfo imfinfo) { + this.imageInfo = imfinfo; + } + + /** + * Keys of processed (read or writen) chunks + * + * @return key:chunk id, val: number of occurrences + */ + public HashMap getChunksKeys() { + HashMap ck = new HashMap(); + for (PngChunk c : chunks) { + ck.put(c.id, ck.containsKey(c.id) ? ck.get(c.id) + 1 : 1); + } + return ck; + } + + /** + * Returns a copy of the list (but the chunks are not copied) This should not be used for general metadata + * handling + */ + public ArrayList getChunks() { + return new ArrayList(chunks); + } + + protected static List getXById(final List list, final String id, final String innerid) { + if (innerid == null) + return ChunkHelper.filterList(list, new ChunkPredicate() { + public boolean match(PngChunk c) { + return c.id.equals(id); + } + }); + else + return ChunkHelper.filterList(list, new ChunkPredicate() { + public boolean match(PngChunk c) { + if (!c.id.equals(id)) + return false; + if (c instanceof PngChunkTextVar && !((PngChunkTextVar) c).getKey().equals(innerid)) + return false; + if (c instanceof PngChunkSPLT && !((PngChunkSPLT) c).getPalName().equals(innerid)) + return false; + return true; + } + }); + } + + /** + * Adds chunk in next position. This is used onyl by the pngReader + */ + public void appendReadChunk(PngChunk chunk, int chunkGroup) { + chunk.setChunkGroup(chunkGroup); + chunks.add(chunk); + } + + /** + * All chunks with this ID + * + * @param id + * @return List, empty if none + */ + public List getById(final String id) { + return getById(id, null); + } + + /** + * If innerid!=null and the chunk is PngChunkTextVar or PngChunkSPLT, it's filtered by that id + * + * @param id + * @return innerid Only used for text and SPLT chunks + * @return List, empty if none + */ + public List getById(final String id, final String innerid) { + return getXById(chunks, id, innerid); + } + + /** + * Returns only one chunk + * + * @param id + * @return First chunk found, null if not found + */ + public PngChunk getById1(final String id) { + return getById1(id, false); + } + + /** + * Returns only one chunk or null if nothing found - does not include queued + *

+ * If more than one chunk is found, then an exception is thrown (failifMultiple=true or chunk is single) or the last + * one is returned (failifMultiple=false) + **/ + public PngChunk getById1(final String id, final boolean failIfMultiple) { + return getById1(id, null, failIfMultiple); + } + + /** + * Returns only one chunk or null if nothing found - does not include queued + *

+ * If more than one chunk (after filtering by inner id) is found, then an exception is thrown (failifMultiple=true + * or chunk is single) or the last one is returned (failifMultiple=false) + **/ + public PngChunk getById1(final String id, final String innerid, final boolean failIfMultiple) { + List list = getById(id, innerid); + if (list.isEmpty()) + return null; + if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple())) + throw new PngjException("unexpected multiple chunks id=" + id); + return list.get(list.size() - 1); + } + + /** + * Finds all chunks "equivalent" to this one + * + * @param c2 + * @return Empty if nothing found + */ + public List getEquivalent(final PngChunk c2) { + return ChunkHelper.filterList(chunks, new ChunkPredicate() { + public boolean match(PngChunk c) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + + public String toString() { + return "ChunkList: read: " + chunks.size(); + } + + /** + * for debugging + */ + public String toStringFull() { + StringBuilder sb = new StringBuilder(toString()); + sb.append("\n Read:\n"); + for (PngChunk chunk : chunks) { + sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); + } + return sb.toString(); + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksListForWrite.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksListForWrite.java new file mode 100644 index 000000000..204c4c2a5 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksListForWrite.java @@ -0,0 +1,171 @@ +package jogamp.opengl.util.pngj.chunks; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import jogamp.opengl.util.pngj.ImageInfo; +import jogamp.opengl.util.pngj.PngjException; +import jogamp.opengl.util.pngj.PngjOutputException; + +public class ChunksListForWrite extends ChunksList { + + /** + * chunks not yet writen - does not include IHDR, IDAT, END, perhaps yes PLTE + */ + private final List queuedChunks = new ArrayList(); + + // redundant, just for eficciency + private HashMap alreadyWrittenKeys = new HashMap(); + + public ChunksListForWrite(ImageInfo imfinfo) { + super(imfinfo); + } + + /** + * Same as getById(), but looking in the queued chunks + */ + public List getQueuedById(final String id) { + return getQueuedById(id, null); + } + + /** + * Same as getById(), but looking in the queued chunks + */ + public List getQueuedById(final String id, final String innerid) { + return getXById(queuedChunks, id, innerid); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id, final String innerid, final boolean failIfMultiple) { + List list = getQueuedById(id, innerid); + if (list.isEmpty()) + return null; + if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple())) + throw new PngjException("unexpected multiple chunks id=" + id); + return list.get(list.size() - 1); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id, final boolean failIfMultiple) { + return getQueuedById1(id, null, failIfMultiple); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id) { + return getQueuedById1(id, false); + } + + /** + * Remove Chunk: only from queued + * + * WARNING: this depends on c.equals() implementation, which is straightforward for SingleChunks. For + * MultipleChunks, it will normally check for reference equality! + */ + public boolean removeChunk(PngChunk c) { + return queuedChunks.remove(c); + } + + /** + * Adds chunk to queue + * + * Does not check for duplicated or anything + * + * @param c + */ + public boolean queue(PngChunk c) { + queuedChunks.add(c); + return true; + } + + /** + * this should be called only for ancillary chunks and PLTE (groups 1 - 3 - 5) + **/ + private static boolean shouldWrite(PngChunk c, int currentGroup) { + if (currentGroup == CHUNK_GROUP_2_PLTE) + return c.id.equals(ChunkHelper.PLTE); + if (currentGroup % 2 == 0) + throw new PngjOutputException("bad chunk group?"); + int minChunkGroup, maxChunkGroup; + if (c.getOrderingConstraint().mustGoBeforePLTE()) + minChunkGroup = maxChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + else if (c.getOrderingConstraint().mustGoBeforeIDAT()) { + maxChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + minChunkGroup = c.getOrderingConstraint().mustGoAfterPLTE() ? ChunksList.CHUNK_GROUP_3_AFTERPLTE + : ChunksList.CHUNK_GROUP_1_AFTERIDHR; + } else { + maxChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + minChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + } + + int preferred = maxChunkGroup; + if (c.hasPriority()) + preferred = minChunkGroup; + if (ChunkHelper.isUnknown(c) && c.getChunkGroup() > 0) + preferred = c.getChunkGroup(); + if (currentGroup == preferred) + return true; + if (currentGroup > preferred && currentGroup <= maxChunkGroup) + return true; + return false; + } + + public int writeChunks(OutputStream os, int currentGroup) { + int cont = 0; + Iterator it = queuedChunks.iterator(); + while (it.hasNext()) { + PngChunk c = it.next(); + if (!shouldWrite(c, currentGroup)) + continue; + if (ChunkHelper.isCritical(c.id) && !c.id.equals(ChunkHelper.PLTE)) + throw new PngjOutputException("bad chunk queued: " + c); + if (alreadyWrittenKeys.containsKey(c.id) && !c.allowsMultiple()) + throw new PngjOutputException("duplicated chunk does not allow multiple: " + c); + c.write(os); + chunks.add(c); + alreadyWrittenKeys.put(c.id, alreadyWrittenKeys.containsKey(c.id) ? alreadyWrittenKeys.get(c.id) + 1 : 1); + c.setChunkGroup(currentGroup); + it.remove(); + cont++; + } + return cont; + } + + /** + * warning: this is NOT a copy, do not modify + */ + public List getQueuedChunks() { + return queuedChunks; + } + + public String toString() { + return "ChunkList: written: " + chunks.size() + " queue: " + queuedChunks.size(); + } + + /** + * for debugging + */ + public String toStringFull() { + StringBuilder sb = new StringBuilder(toString()); + sb.append("\n Written:\n"); + for (PngChunk chunk : chunks) { + sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); + } + if (!queuedChunks.isEmpty()) { + sb.append(" Queued:\n"); + for (PngChunk chunk : queuedChunks) { + sb.append(chunk).append("\n"); + } + + } + return sb.toString(); + } +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java index 2df9fd1f3..1d630591e 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java @@ -6,26 +6,87 @@ import java.util.HashMap; import java.util.Map; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngjException; +import jogamp.opengl.util.pngj.PngjExceptionInternal; - -// see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html +/** + * Represents a instance of a PNG chunk. + *

+ * See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks + * .html + *

+ * Concrete classes should extend {@link PngChunkSingle} or {@link PngChunkMultiple} + *

+ * Note that some methods/fields are type-specific (getOrderingConstraint(), allowsMultiple()),
+ * some are 'almost' type-specific (id,crit,pub,safe; the exception is PngUKNOWN),
+ * and the rest are instance-specific + */ public abstract class PngChunk { - public final String id; // 4 letters + /** + * Chunk-id: 4 letters + */ + public final String id; + /** + * Autocomputed at creation time + */ public final boolean crit, pub, safe; - private int lenori = -1; // merely informational, for read chunks - private boolean writePriority = false; // for queued chunks protected final ImageInfo imgInfo; - private int chunkGroup = -1; // chunk group where it was read or writen + /** + * Possible ordering constraint for a PngChunk type -only relevant for ancillary chunks. Theoretically, there could + * be more general constraints, but these cover the constraints for standard chunks. + */ + public enum ChunkOrderingConstraint { + /** + * no ordering constraint + */ + NONE, + /** + * Must go before PLTE (and hence, also before IDAT) + */ + BEFORE_PLTE_AND_IDAT, + /** + * Must go after PLTE but before IDAT + */ + AFTER_PLTE_BEFORE_IDAT, + /** + * Must before IDAT (before or after PLTE) + */ + BEFORE_IDAT, + /** + * Does not apply + */ + NA; + + public boolean mustGoBeforePLTE() { + return this == BEFORE_PLTE_AND_IDAT; + } + + public boolean mustGoBeforeIDAT() { + return this == BEFORE_IDAT || this == BEFORE_PLTE_AND_IDAT || this == AFTER_PLTE_BEFORE_IDAT; + } + + public boolean mustGoAfterPLTE() { + return this == AFTER_PLTE_BEFORE_IDAT; + } + } + + private boolean priority = false; // For writing. Queued chunks with high priority will be written as soon as + // possible + + protected int chunkGroup = -1; // chunk group where it was read or writen + protected int length = -1; // merely informational, for read chunks + protected long offset = 0; // merely informational, for read chunks /** - * This static map defines which PngChunk class correspond to which ChunkID The client can add other chunks to this - * map statically, before reading + * This static map defines which PngChunk class correspond to which ChunkID + *

+ * The client can add other chunks to this map statically, before reading an image, calling + * PngChunk.factoryRegister(id,class) */ - public final static Map> factoryMap = new HashMap>(); + private final static Map> factoryMap = new HashMap>(); static { factoryMap.put(ChunkHelper.IDAT, PngChunkIDAT.class); factoryMap.put(ChunkHelper.IHDR, PngChunkIHDR.class); @@ -45,6 +106,32 @@ public abstract class PngChunk { factoryMap.put(ChunkHelper.sRGB, PngChunkSRGB.class); factoryMap.put(ChunkHelper.hIST, PngChunkHIST.class); factoryMap.put(ChunkHelper.sPLT, PngChunkSPLT.class); + // extended + factoryMap.put(PngChunkOFFS.ID, PngChunkOFFS.class); + factoryMap.put(PngChunkSTER.ID, PngChunkSTER.class); + } + + /** + * Registers a chunk-id (4 letters) to be associated with a PngChunk class + *

+ * This method should be called by user code that wants to add some chunks (not implmemented in this library) to the + * factory, so that the PngReader knows about it. + */ + public static void factoryRegister(String chunkId, Class chunkClass) { + factoryMap.put(chunkId, chunkClass); + } + + /** + * True if the chunk-id type is known. + *

+ * A chunk is known if we recognize its class, according with factoryMap + *

+ * This is not necessarily the same as being "STANDARD", or being implemented in this library + *

+ * Unknown chunks will be parsed as instances of {@link PngChunkUNKNOWN} + */ + public static boolean isKnown(String id) { + return factoryMap.containsKey(id); } protected PngChunk(String id, ImageInfo imgInfo) { @@ -55,29 +142,19 @@ public abstract class PngChunk { this.safe = ChunkHelper.isSafeToCopy(id); } - public abstract ChunkRaw createChunk(); - - public abstract void parseFromChunk(ChunkRaw c); - - // override to make deep copy from read data to write - public abstract void cloneDataFromRead(PngChunk other); - - @SuppressWarnings("unchecked") - public static T cloneChunk(T chunk, ImageInfo info) { - PngChunk cn = factoryFromId(chunk.id, info); - if (cn.getClass() != chunk.getClass()) - throw new PngjException("bad class cloning chunk: " + cn.getClass() + " " + chunk.getClass()); - cn.cloneDataFromRead(chunk); - return (T) cn; - } - + /** + * This factory creates the corresponding chunk and parses the raw chunk. This is used when reading. + */ public static PngChunk factory(ChunkRaw chunk, ImageInfo info) { PngChunk c = factoryFromId(ChunkHelper.toString(chunk.idbytes), info); - c.lenori = chunk.len; - c.parseFromChunk(chunk); + c.length = chunk.len; + c.parseFromRaw(chunk); return c; } + /** + * Creates one new blank chunk of the corresponding type, according to factoryMap (PngChunkUNKNOWN if not known) + */ public static PngChunk factoryFromId(String cid, ImageInfo info) { PngChunk chunk = null; try { @@ -87,66 +164,110 @@ public abstract class PngChunk { chunk = constr.newInstance(info); } } catch (Exception e) { - // this can happend for unkown chunks + // this can happen for unkown chunks } if (chunk == null) chunk = new PngChunkUNKNOWN(cid, info); return chunk; } - protected ChunkRaw createEmptyChunk(int len, boolean alloc) { + protected final ChunkRaw createEmptyChunk(int len, boolean alloc) { ChunkRaw c = new ChunkRaw(len, ChunkHelper.toBytes(id), alloc); return c; } - @Override - public String toString() { - return "chunk id= " + id + " (" + lenori + ") c=" + getClass().getSimpleName(); + /** + * Makes a clone (deep copy) calling {@link #cloneDataFromRead(PngChunk)} + */ + @SuppressWarnings("unchecked") + public static T cloneChunk(T chunk, ImageInfo info) { + PngChunk cn = factoryFromId(chunk.id, info); + if (cn.getClass() != chunk.getClass()) + throw new PngjExceptionInternal("bad class cloning chunk: " + cn.getClass() + " " + chunk.getClass()); + cn.cloneDataFromRead(chunk); + return (T) cn; } - void setPriority(boolean highPrioriy) { - writePriority = highPrioriy; + /** + * In which "chunkGroup" (see {@link ChunksList}for definition) this chunks instance was read or written. + *

+ * -1 if not read or written (eg, queued) + */ + final public int getChunkGroup() { + return chunkGroup; } - void write(OutputStream os) { - ChunkRaw c = createChunk(); - if (c == null) - throw new PngjException("null chunk ! creation failed for " + this); - c.writeChunk(os); + /** + * @see #getChunkGroup() + */ + final public void setChunkGroup(int chunkGroup) { + this.chunkGroup = chunkGroup; } - public boolean isWritePriority() { - return writePriority; + public boolean hasPriority() { + return priority; } - /** must be overriden - only relevant for ancillary chunks */ - public boolean allowsMultiple() { - return false; // override if allows multiple ocurrences + public void setPriority(boolean priority) { + this.priority = priority; } - /** mustGoBeforeXX/After must be overriden - only relevant for ancillary chunks */ - public boolean mustGoBeforeIDAT() { - return false; + final void write(OutputStream os) { + ChunkRaw c = createRawChunk(); + if (c == null) + throw new PngjExceptionInternal("null chunk ! creation failed for " + this); + c.writeChunk(os); } - public boolean mustGoBeforePLTE() { - return false; + public int getLength() { + return length; } - public boolean mustGoAfterPLTE() { - return false; - } + /* + * public void setLength(int length) { this.length = length; } + */ - static boolean isKnown(String id) { - return factoryMap.containsKey(id); + public long getOffset() { + return offset; } - public int getChunkGroup() { - return chunkGroup; + public void setOffset(long offset) { + this.offset = offset; } - public void setChunkGroup(int chunkGroup) { - this.chunkGroup = chunkGroup; + /** + * Creates the physical chunk. This is used when writing (serialization). Each particular chunk class implements its + * own logic. + * + * @return A newly allocated and filled raw chunk + */ + public abstract ChunkRaw createRawChunk(); + + /** + * Parses raw chunk and fill inside data. This is used when reading (deserialization). Each particular chunk class + * implements its own logic. + */ + public abstract void parseFromRaw(ChunkRaw c); + + /** + * Makes a copy of the chunk. + *

+ * This is used when copying chunks from a reader to a writer + *

+ * It should normally be a deep copy, and after the cloning this.equals(other) should return true + */ + public abstract void cloneDataFromRead(PngChunk other); + + public abstract boolean allowsMultiple(); // this is implemented in PngChunkMultiple/PngChunSingle + + /** + * see {@link ChunkOrderingConstraint} + */ + public abstract ChunkOrderingConstraint getOrderingConstraint(); + + @Override + public String toString() { + return "chunk id= " + id + " (len=" + length + " offset=" + offset + ") c=" + getClass().getSimpleName(); } } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java index 51bbcb832..4a8502a3d 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java @@ -1,14 +1,18 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; -/* +/** + * bKGD Chunk. + *

+ * see http://www.w3.org/TR/PNG/#11bKGD + *

+ * this chunk structure depends on the image type */ -public class PngChunkBKGD extends PngChunk { - // http://www.w3.org/TR/PNG/#11bKGD - // this chunk structure depends on the image type +public class PngChunkBKGD extends PngChunkSingle { + public final static String ID = ChunkHelper.bKGD; // only one of these is meaningful private int gray; private int red, green, blue; @@ -19,43 +23,38 @@ public class PngChunkBKGD extends PngChunk { } @Override - public boolean mustGoBeforeIDAT() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; } @Override - public boolean mustGoAfterPLTE() { - return true; - } - - @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = null; if (imgInfo.greyscale) { c = createEmptyChunk(2, true); - PngHelper.writeInt2tobytes(gray, c.data, 0); + PngHelperInternal.writeInt2tobytes(gray, c.data, 0); } else if (imgInfo.indexed) { c = createEmptyChunk(1, true); c.data[0] = (byte) paletteIndex; } else { c = createEmptyChunk(6, true); - PngHelper.writeInt2tobytes(red, c.data, 0); - PngHelper.writeInt2tobytes(green, c.data, 0); - PngHelper.writeInt2tobytes(blue, c.data, 0); + PngHelperInternal.writeInt2tobytes(red, c.data, 0); + PngHelperInternal.writeInt2tobytes(green, c.data, 0); + PngHelperInternal.writeInt2tobytes(blue, c.data, 0); } return c; } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { if (imgInfo.greyscale) { - gray = PngHelper.readInt2fromBytes(c.data, 0); + gray = PngHelperInternal.readInt2fromBytes(c.data, 0); } else if (imgInfo.indexed) { paletteIndex = (int) (c.data[0] & 0xff); } else { - red = PngHelper.readInt2fromBytes(c.data, 0); - green = PngHelper.readInt2fromBytes(c.data, 2); - blue = PngHelper.readInt2fromBytes(c.data, 4); + red = PngHelperInternal.readInt2fromBytes(c.data, 0); + green = PngHelperInternal.readInt2fromBytes(c.data, 2); + blue = PngHelperInternal.readInt2fromBytes(c.data, 4); } } @@ -119,4 +118,5 @@ public class PngChunkBKGD extends PngChunk { throw new PngjException("only rgb or rgba images support this"); return new int[] { red, green, blue }; } + } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkCHRM.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkCHRM.java index 4380761c7..25a4bf2d6 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkCHRM.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkCHRM.java @@ -1,12 +1,17 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; -/* +/** + * cHRM chunk. + *

+ * see http://www.w3.org/TR/PNG/#11cHRM */ -public class PngChunkCHRM extends PngChunk { +public class PngChunkCHRM extends PngChunkSingle { + public final static String ID = ChunkHelper.cHRM; + // http://www.w3.org/TR/PNG/#11cHRM private double whitex, whitey; private double redx, redy; @@ -14,46 +19,41 @@ public class PngChunkCHRM extends PngChunk { private double bluex, bluey; public PngChunkCHRM(ImageInfo info) { - super(ChunkHelper.cHRM, info); - } - - @Override - public boolean mustGoBeforeIDAT() { - return true; + super(ID, info); } @Override - public boolean mustGoBeforePLTE() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = null; c = createEmptyChunk(32, true); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(whitex), c.data, 0); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(whitey), c.data, 4); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(redx), c.data, 8); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(redy), c.data, 12); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(greenx), c.data, 16); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(greeny), c.data, 20); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(bluex), c.data, 24); - PngHelper.writeInt4tobytes(PngHelper.doubleToInt100000(bluey), c.data, 28); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitex), c.data, 0); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitey), c.data, 4); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redx), c.data, 8); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redy), c.data, 12); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greenx), c.data, 16); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greeny), c.data, 20); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluex), c.data, 24); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluey), c.data, 28); return c; } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { if (c.len != 32) throw new PngjException("bad chunk " + c); - whitex = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 0)); - whitey = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 4)); - redx = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 8)); - redy = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 12)); - greenx = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 16)); - greeny = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 20)); - bluex = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 24)); - bluey = PngHelper.intToDouble100000(PngHelper.readInt4fromBytes(c.data, 28)); + whitex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 0)); + whitey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 4)); + redx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 8)); + redy = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 12)); + greenx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 16)); + greeny = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 20)); + bluex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 24)); + bluey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 28)); } @Override diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java index 184ee9ffa..74640746e 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java @@ -1,42 +1,42 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; -/* +/** + * gAMA chunk. + *

+ * see http://www.w3.org/TR/PNG/#11gAMA */ -public class PngChunkGAMA extends PngChunk { +public class PngChunkGAMA extends PngChunkSingle { + public final static String ID = ChunkHelper.gAMA; + // http://www.w3.org/TR/PNG/#11gAMA private double gamma; public PngChunkGAMA(ImageInfo info) { - super(ChunkHelper.gAMA, info); - } - - @Override - public boolean mustGoBeforeIDAT() { - return true; + super(ID, info); } @Override - public boolean mustGoBeforePLTE() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = createEmptyChunk(4, true); int g = (int) (gamma * 100000 + 0.5); - PngHelper.writeInt4tobytes(g, c.data, 0); + PngHelperInternal.writeInt4tobytes(g, c.data, 0); return c; } @Override - public void parseFromChunk(ChunkRaw chunk) { + public void parseFromRaw(ChunkRaw chunk) { if (chunk.len != 4) throw new PngjException("bad chunk " + chunk); - int g = PngHelper.readInt4fromBytes(chunk.data, 0); + int g = PngHelperInternal.readInt4fromBytes(chunk.data, 0); gamma = ((double) g) / 100000.0; } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java index b0f02ea37..6dc3fd9ec 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java @@ -1,50 +1,48 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; -/* +/** + * hIST chunk. + *

+ * see http://www.w3.org/TR/PNG/#11hIST
+ * only for palette images */ -public class PngChunkHIST extends PngChunk { - // http://www.w3.org/TR/PNG/#11hIST - // only for palette images +public class PngChunkHIST extends PngChunkSingle { + public final static String ID = ChunkHelper.hIST; private int[] hist = new int[0]; // should have same lenght as palette public PngChunkHIST(ImageInfo info) { - super(ChunkHelper.hIST, info); + super(ID, info); } @Override - public boolean mustGoBeforeIDAT() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; } @Override - public boolean mustGoAfterPLTE() { - return true; - } - - @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { if (!imgInfo.indexed) throw new PngjException("only indexed images accept a HIST chunk"); int nentries = c.data.length / 2; hist = new int[nentries]; for (int i = 0; i < hist.length; i++) { - hist[i] = PngHelper.readInt2fromBytes(c.data, i * 2); + hist[i] = PngHelperInternal.readInt2fromBytes(c.data, i * 2); } } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { if (!imgInfo.indexed) throw new PngjException("only indexed images accept a HIST chunk"); ChunkRaw c = null; c = createEmptyChunk(hist.length * 2, true); for (int i = 0; i < hist.length; i++) { - PngHelper.writeInt2tobytes(hist[i], c.data, i * 2); + PngHelperInternal.writeInt2tobytes(hist[i], c.data, i * 2); } return c; } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java index db1c1ba64..399577d72 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java @@ -1,31 +1,32 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; +import jogamp.opengl.util.pngj.PngjException; -/* +/** + * iCCP chunk. + *

+ * see http://www.w3.org/TR/PNG/#11iCCP */ -public class PngChunkICCP extends PngChunk { +public class PngChunkICCP extends PngChunkSingle { + public final static String ID = ChunkHelper.iCCP; + // http://www.w3.org/TR/PNG/#11iCCP private String profileName; private byte[] compressedProfile; // copmression/decopmresion is done in getter/setter public PngChunkICCP(ImageInfo info) { - super(ChunkHelper.iCCP, info); - } - - @Override - public boolean mustGoBeforeIDAT() { - return true; + super(ID, info); } @Override - public boolean mustGoBeforePLTE() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = createEmptyChunk(profileName.length() + compressedProfile.length + 2, true); System.arraycopy(ChunkHelper.toBytes(profileName), 0, c.data, 0, profileName.length()); c.data[profileName.length()] = 0; @@ -35,12 +36,12 @@ public class PngChunkICCP extends PngChunk { } @Override - public void parseFromChunk(ChunkRaw chunk) { + public void parseFromRaw(ChunkRaw chunk) { int pos0 = ChunkHelper.posNullByte(chunk.data); - profileName = new String(chunk.data, 0, pos0, PngHelper.charsetLatin1); + profileName = new String(chunk.data, 0, pos0, PngHelperInternal.charsetLatin1); int comp = (chunk.data[pos0 + 1] & 0xff); if (comp != 0) - throw new RuntimeException("bad compression for ChunkTypeICCP"); + throw new PngjException("bad compression for ChunkTypeICCP"); int compdatasize = chunk.data.length - (pos0 + 2); compressedProfile = new byte[compdatasize]; System.arraycopy(chunk.data, pos0 + 2, compressedProfile, 0, compdatasize); @@ -64,7 +65,7 @@ public class PngChunkICCP extends PngChunk { } public void setProfileNameAndContent(String name, String profile) { - setProfileNameAndContent(name, profile.getBytes(PngHelper.charsetLatin1)); + setProfileNameAndContent(name, profile.getBytes(PngHelperInternal.charsetLatin1)); } public String getProfileName() { @@ -79,7 +80,7 @@ public class PngChunkICCP extends PngChunk { } public String getProfileAsString() { - return new String(getProfile(), PngHelper.charsetLatin1); + return new String(getProfile(), PngHelperInternal.charsetLatin1); } } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIDAT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIDAT.java index a7cb95dbf..b816db205 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIDAT.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIDAT.java @@ -2,21 +2,35 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -public class PngChunkIDAT extends PngChunk { +/** + * IDAT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IDAT + *

+ * This is dummy placeholder - we write/read this chunk (actually several) by special code. + */ +public class PngChunkIDAT extends PngChunkMultiple { + public final static String ID = ChunkHelper.IDAT; + // http://www.w3.org/TR/PNG/#11IDAT - // This is dummy placeholder - we write/read this chunk (actually several) - // by special code. - public PngChunkIDAT(ImageInfo i) { - super(ChunkHelper.IDAT, i); + public PngChunkIDAT(ImageInfo i, int len, long offset) { + super(ID, i); + this.length = len; + this.offset = offset; + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; } @Override - public ChunkRaw createChunk() {// does nothing + public ChunkRaw createRawChunk() {// does nothing return null; } @Override - public void parseFromChunk(ChunkRaw c) { // does nothing + public void parseFromRaw(ChunkRaw c) { // does nothing } @Override diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java index 0d5b266da..fbec564d8 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java @@ -2,21 +2,33 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -public class PngChunkIEND extends PngChunk { +/** + * IEND chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IEND + */ +public class PngChunkIEND extends PngChunkSingle { + public final static String ID = ChunkHelper.IEND; + // http://www.w3.org/TR/PNG/#11IEND // this is a dummy placeholder public PngChunkIEND(ImageInfo info) { - super(ChunkHelper.IEND, info); + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = new ChunkRaw(0, ChunkHelper.b_IEND, false); return c; } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { // this is not used } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java index fcb4150ff..94bfedd38 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java @@ -3,14 +3,20 @@ package jogamp.opengl.util.pngj.chunks; import java.io.ByteArrayInputStream; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; /** - * this is a special chunk! + * IHDR chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IHDR + *

+ * This is a special critical Chunk. */ -public class PngChunkIHDR extends PngChunk { +public class PngChunkIHDR extends PngChunkSingle { + public final static String ID = ChunkHelper.IHDR; + private int cols; private int rows; private int bitspc; @@ -22,16 +28,21 @@ public class PngChunkIHDR extends PngChunk { // http://www.w3.org/TR/PNG/#11IHDR // public PngChunkIHDR(ImageInfo info) { - super(ChunkHelper.IHDR, info); + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = new ChunkRaw(13, ChunkHelper.b_IHDR, true); int offset = 0; - PngHelper.writeInt4tobytes(cols, c.data, offset); + PngHelperInternal.writeInt4tobytes(cols, c.data, offset); offset += 4; - PngHelper.writeInt4tobytes(rows, c.data, offset); + PngHelperInternal.writeInt4tobytes(rows, c.data, offset); offset += 4; c.data[offset++] = (byte) bitspc; c.data[offset++] = (byte) colormodel; @@ -42,18 +53,18 @@ public class PngChunkIHDR extends PngChunk { } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { if (c.len != 13) throw new PngjException("Bad IDHR len " + c.len); ByteArrayInputStream st = c.getAsByteStream(); - cols = PngHelper.readInt4(st); - rows = PngHelper.readInt4(st); + cols = PngHelperInternal.readInt4(st); + rows = PngHelperInternal.readInt4(st); // bit depth: number of bits per channel - bitspc = PngHelper.readByte(st); - colormodel = PngHelper.readByte(st); - compmeth = PngHelper.readByte(st); - filmeth = PngHelper.readByte(st); - interlaced = PngHelper.readByte(st); + bitspc = PngHelperInternal.readByte(st); + colormodel = PngHelperInternal.readByte(st); + compmeth = PngHelperInternal.readByte(st); + filmeth = PngHelperInternal.readByte(st); + interlaced = PngHelperInternal.readByte(st); } @Override diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java index 4e5c7c74a..ab52d7c90 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java @@ -4,14 +4,17 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; /** - * UNTESTED! + * iTXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11iTXt */ public class PngChunkITXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.iTXt; private boolean compressed = false; private String langTag = ""; @@ -19,24 +22,24 @@ public class PngChunkITXT extends PngChunkTextVar { // http://www.w3.org/TR/PNG/#11iTXt public PngChunkITXT(ImageInfo info) { - super(ChunkHelper.iTXt, info); + super(ID, info); } @Override - public ChunkRaw createChunk() { - if (val.isEmpty() || key.isEmpty()) - return null; + public ChunkRaw createRawChunk() { + if (key.isEmpty()) + throw new PngjException("Text chunk key must be non empty"); try { ByteArrayOutputStream ba = new ByteArrayOutputStream(); - ba.write(key.getBytes(PngHelper.charsetLatin1)); + ba.write(ChunkHelper.toBytes(key)); ba.write(0); // separator ba.write(compressed ? 1 : 0); ba.write(0); // compression method (always 0) - ba.write(langTag.getBytes(PngHelper.charsetUTF8)); + ba.write(ChunkHelper.toBytes(langTag)); ba.write(0); // separator - ba.write(translatedTag.getBytes(PngHelper.charsetUTF8)); + ba.write(ChunkHelper.toBytesUTF8(translatedTag)); ba.write(0); // separator - byte[] textbytes = val.getBytes(PngHelper.charsetUTF8); + byte[] textbytes = ChunkHelper.toBytesUTF8(val); if (compressed) { textbytes = ChunkHelper.compressBytes(textbytes, true); } @@ -51,7 +54,7 @@ public class PngChunkITXT extends PngChunkTextVar { } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { int nullsFound = 0; int[] nullsIdx = new int[3]; for (int i = 0; i < c.data.length; i++) { @@ -66,20 +69,21 @@ public class PngChunkITXT extends PngChunkTextVar { } if (nullsFound != 3) throw new PngjException("Bad formed PngChunkITXT chunk"); - key = new String(c.data, 0, nullsIdx[0], PngHelper.charsetLatin1); + key = ChunkHelper.toString(c.data, 0, nullsIdx[0]); int i = nullsIdx[0] + 1; compressed = c.data[i] == 0 ? false : true; i++; if (compressed && c.data[i] != 0) throw new PngjException("Bad formed PngChunkITXT chunk - bad compression method "); - langTag = new String(c.data, i, nullsIdx[1] - i, PngHelper.charsetLatin1); - translatedTag = new String(c.data, nullsIdx[1] + 1, nullsIdx[2] - nullsIdx[1] - 1, PngHelper.charsetUTF8); + langTag = new String(c.data, i, nullsIdx[1] - i, PngHelperInternal.charsetLatin1); + translatedTag = new String(c.data, nullsIdx[1] + 1, nullsIdx[2] - nullsIdx[1] - 1, + PngHelperInternal.charsetUTF8); i = nullsIdx[2] + 1; if (compressed) { byte[] bytes = ChunkHelper.compressBytes(c.data, i, c.data.length - i, false); - val = new String(bytes, PngHelper.charsetUTF8); + val = ChunkHelper.toStringUTF8(bytes); } else { - val = new String(c.data, i, c.data.length - i, PngHelper.charsetUTF8); + val = ChunkHelper.toStringUTF8(c.data, i, c.data.length - i); } } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkMultiple.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkMultiple.java new file mode 100644 index 000000000..696edd431 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkMultiple.java @@ -0,0 +1,27 @@ +package jogamp.opengl.util.pngj.chunks; + +import jogamp.opengl.util.pngj.ImageInfo; + +/** + * PNG chunk type (abstract) that allows multiple instances in same image. + */ +public abstract class PngChunkMultiple extends PngChunk { + + protected PngChunkMultiple(String id, ImageInfo imgInfo) { + super(id, imgInfo); + } + + @Override + public final boolean allowsMultiple() { + return true; + } + + /** + * NOTE: this chunk uses the default Object's equals() hashCode() implementation. + * + * This is the right thing to do, normally. + * + * This is important, eg see ChunkList.removeFromList() + */ + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkOFFS.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkOFFS.java new file mode 100644 index 000000000..a3bab4995 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkOFFS.java @@ -0,0 +1,89 @@ +package jogamp.opengl.util.pngj.chunks; + +import jogamp.opengl.util.pngj.ImageInfo; +import jogamp.opengl.util.pngj.PngHelperInternal; +import jogamp.opengl.util.pngj.PngjException; + +/** + * oFFs chunk. + *

+ * see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs + */ +public class PngChunkOFFS extends PngChunkSingle { + public final static String ID = "oFFs"; + + // http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs + private long posX; + private long posY; + private int units; // 0: pixel 1:micrometer + + public PngChunkOFFS(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(9, true); + PngHelperInternal.writeInt4tobytes((int) posX, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) posY, c.data, 4); + c.data[8] = (byte) units; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 9) + throw new PngjException("bad chunk length " + chunk); + posX = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + if (posX < 0) + posX += 0x100000000L; + posY = PngHelperInternal.readInt4fromBytes(chunk.data, 4); + if (posY < 0) + posY += 0x100000000L; + units = PngHelperInternal.readInt1fromByte(chunk.data, 8); + } + + @Override + public void cloneDataFromRead(PngChunk other) { + PngChunkOFFS otherx = (PngChunkOFFS) other; + this.posX = otherx.posX; + this.posY = otherx.posY; + this.units = otherx.units; + } + + /** + * 0: pixel, 1:micrometer + */ + public int getUnits() { + return units; + } + + /** + * 0: pixel, 1:micrometer + */ + public void setUnits(int units) { + this.units = units; + } + + public long getPosX() { + return posX; + } + + public void setPosX(long posX) { + this.posX = posX; + } + + public long getPosY() { + return posY; + } + + public void setPosY(long posY) { + this.posY = posY; + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java index 47e2c492c..b0a1bb898 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java @@ -1,44 +1,50 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; +/** + * pHYs chunk. + *

+ * see http://www.w3.org/TR/PNG/#11pHYs + */ +public class PngChunkPHYS extends PngChunkSingle { + public final static String ID = ChunkHelper.pHYs; -public class PngChunkPHYS extends PngChunk { // http://www.w3.org/TR/PNG/#11pHYs private long pixelsxUnitX; private long pixelsxUnitY; private int units; // 0: unknown 1:metre public PngChunkPHYS(ImageInfo info) { - super(ChunkHelper.pHYs, info); + super(ID, info); } @Override - public boolean mustGoBeforeIDAT() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = createEmptyChunk(9, true); - PngHelper.writeInt4tobytes((int) pixelsxUnitX, c.data, 0); - PngHelper.writeInt4tobytes((int) pixelsxUnitY, c.data, 4); + PngHelperInternal.writeInt4tobytes((int) pixelsxUnitX, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) pixelsxUnitY, c.data, 4); c.data[8] = (byte) units; return c; } @Override - public void parseFromChunk(ChunkRaw chunk) { + public void parseFromRaw(ChunkRaw chunk) { if (chunk.len != 9) throw new PngjException("bad chunk length " + chunk); - pixelsxUnitX = PngHelper.readInt4fromBytes(chunk.data, 0); + pixelsxUnitX = PngHelperInternal.readInt4fromBytes(chunk.data, 0); if (pixelsxUnitX < 0) pixelsxUnitX += 0x100000000L; - pixelsxUnitY = PngHelper.readInt4fromBytes(chunk.data, 4); + pixelsxUnitY = PngHelperInternal.readInt4fromBytes(chunk.data, 4); if (pixelsxUnitY < 0) pixelsxUnitY += 0x100000000L; - units = PngHelper.readInt1fromByte(chunk.data, 8); + units = PngHelperInternal.readInt1fromByte(chunk.data, 8); } @Override diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java index 123080bb3..dbf5e53c0 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java @@ -3,10 +3,16 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; import jogamp.opengl.util.pngj.PngjException; -/* - * Palette chunk *this is critical* +/** + * PLTE chunk. + *

+ * see http://www.w3.org/TR/PNG/#11PLTE + *

+ * Critical chunk */ -public class PngChunkPLTE extends PngChunk { +public class PngChunkPLTE extends PngChunkSingle { + public final static String ID = ChunkHelper.PLTE; + // http://www.w3.org/TR/PNG/#11PLTE private int nentries = 0; /** @@ -15,11 +21,16 @@ public class PngChunkPLTE extends PngChunk { private int[] entries; public PngChunkPLTE(ImageInfo info) { - super(ChunkHelper.PLTE, info); + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { int len = 3 * nentries; int[] rgb = new int[3]; ChunkRaw c = createEmptyChunk(len, true); @@ -33,7 +44,7 @@ public class PngChunkPLTE extends PngChunk { } @Override - public void parseFromChunk(ChunkRaw chunk) { + public void parseFromRaw(ChunkRaw chunk) { setNentries(chunk.len / 3); for (int n = 0, i = 0; n < nentries; n++) { setEntry(n, (int) (chunk.data[i++] & 0xff), (int) (chunk.data[i++] & 0xff), (int) (chunk.data[i++] & 0xff)); diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java index 6850d260d..bc70c6e5e 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java @@ -1,31 +1,31 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; -/* +/** + * sBIT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sBIT + *

+ * this chunk structure depends on the image type */ -public class PngChunkSBIT extends PngChunk { +public class PngChunkSBIT extends PngChunkSingle { + public final static String ID = ChunkHelper.sBIT; // http://www.w3.org/TR/PNG/#11sBIT - // this chunk structure depends on the image type // significant bits private int graysb, alphasb; private int redsb, greensb, bluesb; public PngChunkSBIT(ImageInfo info) { - super(ChunkHelper.sBIT, info); + super(ID, info); } @Override - public boolean mustGoBeforeIDAT() { - return true; - } - - @Override - public boolean mustGoBeforePLTE() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; } private int getLen() { @@ -36,24 +36,24 @@ public class PngChunkSBIT extends PngChunk { } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { if (c.len != getLen()) throw new PngjException("bad chunk length " + c); if (imgInfo.greyscale) { - graysb = PngHelper.readInt1fromByte(c.data, 0); + graysb = PngHelperInternal.readInt1fromByte(c.data, 0); if (imgInfo.alpha) - alphasb = PngHelper.readInt1fromByte(c.data, 1); + alphasb = PngHelperInternal.readInt1fromByte(c.data, 1); } else { - redsb = PngHelper.readInt1fromByte(c.data, 0); - greensb = PngHelper.readInt1fromByte(c.data, 1); - bluesb = PngHelper.readInt1fromByte(c.data, 2); + redsb = PngHelperInternal.readInt1fromByte(c.data, 0); + greensb = PngHelperInternal.readInt1fromByte(c.data, 1); + bluesb = PngHelperInternal.readInt1fromByte(c.data, 2); if (imgInfo.alpha) - alphasb = PngHelper.readInt1fromByte(c.data, 3); + alphasb = PngHelperInternal.readInt1fromByte(c.data, 3); } } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = null; c = createEmptyChunk(getLen(), true); if (imgInfo.greyscale) { diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java index 953adb7d9..2ff65834d 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java @@ -4,11 +4,17 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; +/** + * sPLT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sPLT + */ +public class PngChunkSPLT extends PngChunkMultiple { + public final static String ID = ChunkHelper.sPLT; -public class PngChunkSPLT extends PngChunk { // http://www.w3.org/TR/PNG/#11sPLT private String palName; @@ -16,35 +22,30 @@ public class PngChunkSPLT extends PngChunk { private int[] palette; // 5 elements per entry public PngChunkSPLT(ImageInfo info) { - super(ChunkHelper.sPLT, info); + super(ID, info); } @Override - public boolean allowsMultiple() { - return true; // allows multiple, but pallete name should be different + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; } @Override - public boolean mustGoBeforeIDAT() { - return true; - } - - @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { try { ByteArrayOutputStream ba = new ByteArrayOutputStream(); - ba.write(palName.getBytes(PngHelper.charsetLatin1)); + ba.write(palName.getBytes(PngHelperInternal.charsetLatin1)); ba.write(0); // separator ba.write((byte) sampledepth); int nentries = getNentries(); for (int n = 0; n < nentries; n++) { for (int i = 0; i < 4; i++) { if (sampledepth == 8) - PngHelper.writeByte(ba, (byte) palette[n * 5 + i]); + PngHelperInternal.writeByte(ba, (byte) palette[n * 5 + i]); else - PngHelper.writeInt2(ba, palette[n * 5 + i]); + PngHelperInternal.writeInt2(ba, palette[n * 5 + i]); } - PngHelper.writeInt2(ba, palette[n * 5 + 4]); + PngHelperInternal.writeInt2(ba, palette[n * 5 + 4]); } byte[] b = ba.toByteArray(); ChunkRaw chunk = createEmptyChunk(b.length, false); @@ -56,7 +57,7 @@ public class PngChunkSPLT extends PngChunk { } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { int t = -1; for (int i = 0; i < c.data.length; i++) { // look for first zero if (c.data[i] == 0) { @@ -66,8 +67,8 @@ public class PngChunkSPLT extends PngChunk { } if (t <= 0 || t > c.data.length - 2) throw new PngjException("bad sPLT chunk: no separator found"); - palName = new String(c.data, 0, t, PngHelper.charsetLatin1); - sampledepth = PngHelper.readInt1fromByte(c.data, t + 1); + palName = new String(c.data, 0, t, PngHelperInternal.charsetLatin1); + sampledepth = PngHelperInternal.readInt1fromByte(c.data, t + 1); t += 2; int nentries = (c.data.length - t) / (sampledepth == 8 ? 6 : 10); palette = new int[nentries * 5]; @@ -75,21 +76,21 @@ public class PngChunkSPLT extends PngChunk { ne = 0; for (int i = 0; i < nentries; i++) { if (sampledepth == 8) { - r = PngHelper.readInt1fromByte(c.data, t++); - g = PngHelper.readInt1fromByte(c.data, t++); - b = PngHelper.readInt1fromByte(c.data, t++); - a = PngHelper.readInt1fromByte(c.data, t++); + r = PngHelperInternal.readInt1fromByte(c.data, t++); + g = PngHelperInternal.readInt1fromByte(c.data, t++); + b = PngHelperInternal.readInt1fromByte(c.data, t++); + a = PngHelperInternal.readInt1fromByte(c.data, t++); } else { - r = PngHelper.readInt2fromBytes(c.data, t); + r = PngHelperInternal.readInt2fromBytes(c.data, t); t += 2; - g = PngHelper.readInt2fromBytes(c.data, t); + g = PngHelperInternal.readInt2fromBytes(c.data, t); t += 2; - b = PngHelper.readInt2fromBytes(c.data, t); + b = PngHelperInternal.readInt2fromBytes(c.data, t); t += 2; - a = PngHelper.readInt2fromBytes(c.data, t); + a = PngHelperInternal.readInt2fromBytes(c.data, t); t += 2; } - f = PngHelper.readInt2fromBytes(c.data, t); + f = PngHelperInternal.readInt2fromBytes(c.data, t); t += 2; palette[ne++] = r; palette[ne++] = g; diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java index 774558785..e4d77d40a 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java @@ -1,12 +1,17 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; -/* +/** + * sRGB chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sRGB */ -public class PngChunkSRGB extends PngChunk { +public class PngChunkSRGB extends PngChunkSingle { + public final static String ID = ChunkHelper.sRGB; + // http://www.w3.org/TR/PNG/#11sRGB public static final int RENDER_INTENT_Perceptual = 0; @@ -17,28 +22,23 @@ public class PngChunkSRGB extends PngChunk { private int intent; public PngChunkSRGB(ImageInfo info) { - super(ChunkHelper.sRGB, info); - } - - @Override - public boolean mustGoBeforeIDAT() { - return true; + super(ID, info); } @Override - public boolean mustGoBeforePLTE() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { if (c.len != 1) throw new PngjException("bad chunk length " + c); - intent = PngHelper.readInt1fromByte(c.data, 0); + intent = PngHelperInternal.readInt1fromByte(c.data, 0); } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = null; c = createEmptyChunk(1, true); c.data[0] = (byte) intent; diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSTER.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSTER.java new file mode 100644 index 000000000..4dc5edec5 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSTER.java @@ -0,0 +1,60 @@ +package jogamp.opengl.util.pngj.chunks; + +import jogamp.opengl.util.pngj.ImageInfo; +import jogamp.opengl.util.pngj.PngjException; + +/** + * sTER chunk. + *

+ * see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER + */ +public class PngChunkSTER extends PngChunkSingle { + public final static String ID = "sTER"; + + // http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER + private byte mode; // 0: cross-fuse layout 1: diverging-fuse layout + + public PngChunkSTER(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(1, true); + c.data[0] = (byte) mode; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 1) + throw new PngjException("bad chunk length " + chunk); + mode = chunk.data[0]; + } + + @Override + public void cloneDataFromRead(PngChunk other) { + PngChunkSTER otherx = (PngChunkSTER) other; + this.mode = otherx.mode; + } + + /** + * 0: cross-fuse layout 1: diverging-fuse layout + */ + public byte getMode() { + return mode; + } + + /** + * 0: cross-fuse layout 1: diverging-fuse layout + */ + public void setMode(byte mode) { + this.mode = mode; + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSingle.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSingle.java new file mode 100644 index 000000000..286f39db0 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSingle.java @@ -0,0 +1,43 @@ +package jogamp.opengl.util.pngj.chunks; + +import jogamp.opengl.util.pngj.ImageInfo; + +/** + * PNG chunk type (abstract) that does not allow multiple instances in same image. + */ +public abstract class PngChunkSingle extends PngChunk { + + protected PngChunkSingle(String id, ImageInfo imgInfo) { + super(id, imgInfo); + } + + public final boolean allowsMultiple() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PngChunkSingle other = (PngChunkSingle) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSkipped.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSkipped.java new file mode 100644 index 000000000..f4c77b4e1 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSkipped.java @@ -0,0 +1,41 @@ +package jogamp.opengl.util.pngj.chunks; + +import jogamp.opengl.util.pngj.ImageInfo; +import jogamp.opengl.util.pngj.PngjException; + +/** + * Pseudo chunk type, for chunks that were skipped on reading + */ +public class PngChunkSkipped extends PngChunk { + + public PngChunkSkipped(String id, ImageInfo info, int clen) { + super(id, info); + this.length = clen; + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { + throw new PngjException("Non supported for a skipped chunk"); + } + + @Override + public void parseFromRaw(ChunkRaw c) { + throw new PngjException("Non supported for a skipped chunk"); + } + + @Override + public void cloneDataFromRead(PngChunk other) { + throw new PngjException("Non supported for a skipped chunk"); + } + + @Override + public boolean allowsMultiple() { + return true; + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java index c535fe34a..d97cd63c5 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java @@ -1,28 +1,40 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; +import jogamp.opengl.util.pngj.PngjException; +/** + * tEXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tEXt + */ public class PngChunkTEXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.tEXt; + public PngChunkTEXT(ImageInfo info) { - super(ChunkHelper.tEXt, info); + super(ID, info); } @Override - public ChunkRaw createChunk() { - if (val.isEmpty() || key.isEmpty()) - return null; - byte[] b = (key + "\0" + val).getBytes(PngHelper.charsetLatin1); + public ChunkRaw createRawChunk() { + if (key.isEmpty()) + throw new PngjException("Text chunk key must be non empty"); + byte[] b = (key + "\0" + val).getBytes(PngHelperInternal.charsetLatin1); ChunkRaw chunk = createEmptyChunk(b.length, false); chunk.data = b; return chunk; } @Override - public void parseFromChunk(ChunkRaw c) { - String[] k = (new String(c.data, PngHelper.charsetLatin1)).split("\0"); - key = k[0]; - val = k[1]; + public void parseFromRaw(ChunkRaw c) { + int i; + for (i = 0; i < c.data.length; i++) + if (c.data[i] == 0) + break; + key = new String(c.data, 0, i, PngHelperInternal.charsetLatin1); + i++; + val = i < c.data.length ? new String(c.data, i, c.data.length - i, PngHelperInternal.charsetLatin1) : ""; } @Override diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java index fa61f6237..8f34c78fe 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java @@ -3,22 +3,33 @@ package jogamp.opengl.util.pngj.chunks; import java.util.Calendar; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; +/** + * tIME chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tIME + */ +public class PngChunkTIME extends PngChunkSingle { + public final static String ID = ChunkHelper.tIME; -public class PngChunkTIME extends PngChunk { // http://www.w3.org/TR/PNG/#11tIME private int year, mon, day, hour, min, sec; public PngChunkTIME(ImageInfo info) { - super(ChunkHelper.tIME, info); + super(ID, info); } @Override - public ChunkRaw createChunk() { + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { ChunkRaw c = createEmptyChunk(7, true); - PngHelper.writeInt2tobytes(year, c.data, 0); + PngHelperInternal.writeInt2tobytes(year, c.data, 0); c.data[2] = (byte) mon; c.data[3] = (byte) day; c.data[4] = (byte) hour; @@ -28,15 +39,15 @@ public class PngChunkTIME extends PngChunk { } @Override - public void parseFromChunk(ChunkRaw chunk) { + public void parseFromRaw(ChunkRaw chunk) { if (chunk.len != 7) throw new PngjException("bad chunk " + chunk); - year = PngHelper.readInt2fromBytes(chunk.data, 0); - mon = PngHelper.readInt1fromByte(chunk.data, 2); - day = PngHelper.readInt1fromByte(chunk.data, 3); - hour = PngHelper.readInt1fromByte(chunk.data, 4); - min = PngHelper.readInt1fromByte(chunk.data, 5); - sec = PngHelper.readInt1fromByte(chunk.data, 6); + year = PngHelperInternal.readInt2fromBytes(chunk.data, 0); + mon = PngHelperInternal.readInt1fromByte(chunk.data, 2); + day = PngHelperInternal.readInt1fromByte(chunk.data, 3); + hour = PngHelperInternal.readInt1fromByte(chunk.data, 4); + min = PngHelperInternal.readInt1fromByte(chunk.data, 5); + sec = PngHelperInternal.readInt1fromByte(chunk.data, 6); } @Override @@ -69,6 +80,7 @@ public class PngChunkTIME extends PngChunk { min = minx; sec = secx; } + public int[] getYMDHMS() { return new int[] { year, mon, day, hour, min, sec }; } @@ -78,6 +90,4 @@ public class PngChunkTIME extends PngChunk { return String.format("%04d/%02d/%02d %02d:%02d:%02d", year, mon, day, hour, min, sec); } - - } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTRNS.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTRNS.java index 9365e5e8e..1de5c0833 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTRNS.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTRNS.java @@ -1,39 +1,41 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; -/* +/** + * tRNS chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tRNS + *

+ * this chunk structure depends on the image type */ -public class PngChunkTRNS extends PngChunk { +public class PngChunkTRNS extends PngChunkSingle { + public final static String ID = ChunkHelper.tRNS; + // http://www.w3.org/TR/PNG/#11tRNS - // this chunk structure depends on the image type - // only one of these is meaningful + + // only one of these is meaningful, depending on the image type private int gray; private int red, green, blue; private int[] paletteAlpha = new int[] {}; public PngChunkTRNS(ImageInfo info) { - super(ChunkHelper.tRNS, info); - } - - @Override - public boolean mustGoBeforeIDAT() { - return true; + super(ID, info); } @Override - public boolean mustGoAfterPLTE() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; } @Override - public ChunkRaw createChunk() { + public ChunkRaw createRawChunk() { ChunkRaw c = null; if (imgInfo.greyscale) { c = createEmptyChunk(2, true); - PngHelper.writeInt2tobytes(gray, c.data, 0); + PngHelperInternal.writeInt2tobytes(gray, c.data, 0); } else if (imgInfo.indexed) { c = createEmptyChunk(paletteAlpha.length, true); for (int n = 0; n < c.len; n++) { @@ -41,17 +43,17 @@ public class PngChunkTRNS extends PngChunk { } } else { c = createEmptyChunk(6, true); - PngHelper.writeInt2tobytes(red, c.data, 0); - PngHelper.writeInt2tobytes(green, c.data, 0); - PngHelper.writeInt2tobytes(blue, c.data, 0); + PngHelperInternal.writeInt2tobytes(red, c.data, 0); + PngHelperInternal.writeInt2tobytes(green, c.data, 0); + PngHelperInternal.writeInt2tobytes(blue, c.data, 0); } return c; } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { if (imgInfo.greyscale) { - gray = PngHelper.readInt2fromBytes(c.data, 0); + gray = PngHelperInternal.readInt2fromBytes(c.data, 0); } else if (imgInfo.indexed) { int nentries = c.data.length; paletteAlpha = new int[nentries]; @@ -59,9 +61,9 @@ public class PngChunkTRNS extends PngChunk { paletteAlpha[n] = (int) (c.data[n] & 0xff); } } else { - red = PngHelper.readInt2fromBytes(c.data, 0); - green = PngHelper.readInt2fromBytes(c.data, 2); - blue = PngHelper.readInt2fromBytes(c.data, 4); + red = PngHelperInternal.readInt2fromBytes(c.data, 0); + green = PngHelperInternal.readInt2fromBytes(c.data, 2); + blue = PngHelperInternal.readInt2fromBytes(c.data, 4); } } @@ -117,6 +119,18 @@ public class PngChunkTRNS extends PngChunk { paletteAlpha = palAlpha; } + /** + * to use when only one pallete index is set as totally transparent + */ + public void setIndexEntryAsTransparent(int palAlphaIndex) { + if (!imgInfo.indexed) + throw new PngjException("only indexed images support this"); + paletteAlpha = new int[] { palAlphaIndex + 1 }; + for (int i = 0; i < palAlphaIndex; i++) + paletteAlpha[i] = 255; + paletteAlpha[palAlphaIndex] = 0; + } + /** * WARNING: non deep copy */ diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java index 3d92a806f..ba3ffc30c 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java @@ -3,11 +3,9 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; /** - * superclass for three textual chunks (TEXT, ITXT, ZTXT) - * - * @author Hernan J Gonzalez + * Superclass (abstract) for three textual chunks (TEXT, ITXT, ZTXT) */ -public abstract class PngChunkTextVar extends PngChunk { +public abstract class PngChunkTextVar extends PngChunkMultiple { protected String key; // key/val: only for tEXt. lazy computed protected String val; @@ -28,8 +26,8 @@ public abstract class PngChunkTextVar extends PngChunk { } @Override - public boolean allowsMultiple() { - return true; + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; } public static class PngTxtInfo { diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java index 15a35935a..3803428e6 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java @@ -2,7 +2,12 @@ package jogamp.opengl.util.pngj.chunks; import jogamp.opengl.util.pngj.ImageInfo; -public class PngChunkUNKNOWN extends PngChunk { // unkown, custom or not +/** + * Placeholder for UNKNOWN (custom or not) chunks. + *

+ * For PngReader, a chunk is unknown if it's not registered in the chunk factory + */ +public class PngChunkUNKNOWN extends PngChunkMultiple { // unkown, custom or not private byte[] data; @@ -10,25 +15,25 @@ public class PngChunkUNKNOWN extends PngChunk { // unkown, custom or not super(id, info); } - @Override - public boolean allowsMultiple() { - return true; - } - private PngChunkUNKNOWN(PngChunkUNKNOWN c, ImageInfo info) { super(c.id, info); System.arraycopy(c.data, 0, data, 0, c.data.length); } @Override - public ChunkRaw createChunk() { + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { ChunkRaw p = createEmptyChunk(data.length, false); p.data = this.data; return p; } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { data = c.data; } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java index fd6c08273..64593eae4 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java @@ -4,26 +4,32 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import jogamp.opengl.util.pngj.ImageInfo; -import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; - +/** + * zTXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11zTXt + */ public class PngChunkZTXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.zTXt; + // http://www.w3.org/TR/PNG/#11zTXt public PngChunkZTXT(ImageInfo info) { - super(ChunkHelper.zTXt, info); + super(ID, info); } @Override - public ChunkRaw createChunk() { - if (val.isEmpty() || key.isEmpty()) - return null; + public ChunkRaw createRawChunk() { + if (key.isEmpty()) + throw new PngjException("Text chunk key must be non empty"); try { ByteArrayOutputStream ba = new ByteArrayOutputStream(); - ba.write(key.getBytes(PngHelper.charsetLatin1)); + ba.write(key.getBytes(PngHelperInternal.charsetLatin1)); ba.write(0); // separator ba.write(0); // compression method: 0 - byte[] textbytes = ChunkHelper.compressBytes(val.getBytes(PngHelper.charsetLatin1), true); + byte[] textbytes = ChunkHelper.compressBytes(val.getBytes(PngHelperInternal.charsetLatin1), true); ba.write(textbytes); byte[] b = ba.toByteArray(); ChunkRaw chunk = createEmptyChunk(b.length, false); @@ -35,7 +41,7 @@ public class PngChunkZTXT extends PngChunkTextVar { } @Override - public void parseFromChunk(ChunkRaw c) { + public void parseFromRaw(ChunkRaw c) { int nullsep = -1; for (int i = 0; i < c.data.length; i++) { // look for first zero if (c.data[i] != 0) @@ -45,12 +51,12 @@ public class PngChunkZTXT extends PngChunkTextVar { } if (nullsep < 0 || nullsep > c.data.length - 2) throw new PngjException("bad zTXt chunk: no separator found"); - key = new String(c.data, 0, nullsep, PngHelper.charsetLatin1); + key = new String(c.data, 0, nullsep, PngHelperInternal.charsetLatin1); int compmet = (int) c.data[nullsep + 1]; if (compmet != 0) throw new PngjException("bad zTXt chunk: unknown compression method"); byte[] uncomp = ChunkHelper.compressBytes(c.data, nullsep + 2, c.data.length - nullsep - 2, false); // uncompress - val = new String(uncomp, PngHelper.charsetLatin1); + val = new String(uncomp, PngHelperInternal.charsetLatin1); } @Override diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java index a82754588..52d1b22c1 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java @@ -1,68 +1,70 @@ package jogamp.opengl.util.pngj.chunks; -import jogamp.opengl.util.pngj.PngHelper; +import java.util.ArrayList; +import java.util.List; + +import jogamp.opengl.util.pngj.PngHelperInternal; import jogamp.opengl.util.pngj.PngjException; /** * We consider "image metadata" every info inside the image except for the most basic image info (IHDR chunk - ImageInfo * class) and the pixels values. - * + *

* This includes the palette (if present) and all the ancillary chunks - * + *

* This class provides a wrapper over the collection of chunks of a image (read or to write) and provides some high * level methods to access them - * */ public class PngMetadata { - private final ChunkList chunkList; + private final ChunksList chunkList; private final boolean readonly; - public PngMetadata(ChunkList chunks, boolean readonly) { + public PngMetadata(ChunksList chunks) { this.chunkList = chunks; - this.readonly = readonly; + if (chunks instanceof ChunksListForWrite) { + this.readonly = false; + } else { + this.readonly = true; + } } /** * Queues the chunk at the writer + *

+ * lazyOverwrite: if true, checks if there is a queued "equivalent" chunk and if so, overwrites it. However if that + * not check for already written chunks. */ - public boolean setChunk(PngChunk c, boolean overwriteIfPresent) { + public void queueChunk(final PngChunk c, boolean lazyOverwrite) { + ChunksListForWrite cl = getChunkListW(); if (readonly) throw new PngjException("cannot set chunk : readonly metadata"); - return chunkList.setChunk(c, overwriteIfPresent); + if (lazyOverwrite) { + ChunkHelper.trimList(cl.getQueuedChunks(), new ChunkPredicate() { + public boolean match(PngChunk c2) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + cl.queue(c); } - - /** - * Returns only one chunk or null if nothing found - does not include queued chunks - * - * If more than one chunk (after filtering by inner id) is found, then an exception is thrown (failifMultiple=true) - * or the last one is returned (failifMultiple=false) - * - * @param id Chunk id - * @param innerid if not null, the chunk is assumed to be PngChunkTextVar or PngChunkSPLT, and filtered by that 'internal id' - * @param failIfMultiple throw exception if more that one - * @return chunk (not cloned) - */ - public PngChunk getChunk1(String id, String innerid, boolean failIfMultiple) { - return chunkList.getChunk1(id, innerid, failIfMultiple); + public void queueChunk(final PngChunk c) { + queueChunk(c, true); } - /** - * Same as getChunk1(id, innerid=null, failIfMultiple=true); - */ - public PngChunk getChunk1(String id) { - return chunkList.getChunk1(id); + private ChunksListForWrite getChunkListW() { + return (ChunksListForWrite) chunkList; } // ///// high level utility methods follow //////////// // //////////// DPI - /** - * returns -1 if not found or dimension unknown - **/ + /** + * returns -1 if not found or dimension unknown + */ public double[] getDpi() { - PngChunk c = getChunk1(ChunkHelper.pHYs, null, true); + PngChunk c = chunkList.getById1(ChunkHelper.pHYs, true); if (c == null) return new double[] { -1, -1 }; else @@ -76,31 +78,68 @@ public class PngMetadata { public void setDpi(double x, double y) { PngChunkPHYS c = new PngChunkPHYS(chunkList.imageInfo); c.setAsDpi2(x, y); - setChunk(c, true); + queueChunk(c); } // //////////// TIME - public void setTimeNow(int secsAgo) { + /** + * Creates a time chunk with current time, less secsAgo seconds + *

+ * + * @return Returns the created-queued chunk, just in case you want to examine or modify it + */ + public PngChunkTIME setTimeNow(int secsAgo) { PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo); c.setNow(secsAgo); - setChunk(c, true); + queueChunk(c); + return c; + } + + public PngChunkTIME setTimeNow() { + return setTimeNow(0); } - public void setTimeYMDHMS(int yearx, int monx, int dayx, int hourx, int minx, int secx) { + /** + * Creates a time chunk with diven date-time + *

+ * + * @return Returns the created-queued chunk, just in case you want to examine or modify it + */ + public PngChunkTIME setTimeYMDHMS(int yearx, int monx, int dayx, int hourx, int minx, int secx) { PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo); c.setYMDHMS(yearx, monx, dayx, hourx, minx, secx); - setChunk(c, true); + queueChunk(c, true); + return c; + } + + /** + * null if not found + */ + public PngChunkTIME getTime() { + return (PngChunkTIME) chunkList.getById1(ChunkHelper.tIME); } public String getTimeAsString() { - PngChunk c = getChunk1(ChunkHelper.tIME, null, true); - return c != null ? ((PngChunkTIME) c).getAsString() : ""; + PngChunkTIME c = getTime(); + return c == null ? "" : c.getAsString(); } // //////////// TEXT - public void setText(String k, String val, boolean useLatin1, boolean compress) { + /** + * Creates a text chunk and queue it. + *

+ * + * @param k + * : key (latin1) + * @param val + * (arbitrary, should be latin1 if useLatin1) + * @param useLatin1 + * @param compress + * @return Returns the created-queued chunks, just in case you want to examine, touch it + */ + public PngChunkTextVar setText(String k, String val, boolean useLatin1, boolean compress) { if (compress && !useLatin1) throw new PngjException("cannot compress non latin text"); PngChunkTextVar c; @@ -115,21 +154,80 @@ public class PngMetadata { ((PngChunkITXT) c).setLangtag(k); // we use the same orig tag (this is not quite right) } c.setKeyVal(k, val); - setChunk(c, true); + queueChunk(c, true); + return c; } - public void setText(String k, String val) { - setText(k, val, false, val.length() > 400); + public PngChunkTextVar setText(String k, String val) { + return setText(k, val, false, false); } - /** tries all text chunks - returns null if not found */ + /** + * gets all text chunks with a given key + *

+ * returns null if not found + *

+ * Warning: this does not check the "lang" key of iTxt + */ + @SuppressWarnings("unchecked") + public List getTxtsForKey(String k) { + @SuppressWarnings("rawtypes") + List c = new ArrayList(); + c.addAll(chunkList.getById(ChunkHelper.tEXt, k)); + c.addAll(chunkList.getById(ChunkHelper.zTXt, k)); + c.addAll(chunkList.getById(ChunkHelper.iTXt, k)); + return c; + } + + /** + * Returns empty if not found, concatenated (with newlines) if multiple! - and trimmed + *

+ * Use getTxtsForKey() if you don't want this behaviour + */ public String getTxtForKey(String k) { - PngChunk c = getChunk1(ChunkHelper.tEXt, k, true); - if (c == null) - c = getChunk1(ChunkHelper.zTXt, k, true); - if (c == null) - c = getChunk1(ChunkHelper.iTXt, k, true); - return c != null ? ((PngChunkTextVar) c).getVal() : null; + List li = getTxtsForKey(k); + if (li.isEmpty()) + return ""; + StringBuilder t = new StringBuilder(); + for (PngChunkTextVar c : li) + t.append(c.getVal()).append("\n"); + return t.toString().trim(); + } + + /** + * Returns the palette chunk, if present + * + * @return null if not present + */ + public PngChunkPLTE getPLTE() { + return (PngChunkPLTE) chunkList.getById1(PngChunkPLTE.ID); + } + + /** + * Creates a new empty palette chunk, queues it for write and return it to the caller, who should fill its entries + */ + public PngChunkPLTE createPLTEChunk() { + PngChunkPLTE plte = new PngChunkPLTE(chunkList.imageInfo); + queueChunk(plte); + return plte; + } + + /** + * Returns the TRNS chunk, if present + * + * @return null if not present + */ + public PngChunkTRNS getTRNS() { + return (PngChunkTRNS) chunkList.getById1(PngChunkTRNS.ID); + } + + /** + * Creates a new empty TRNS chunk, queues it for write and return it to the caller, who should fill its entries + */ + public PngChunkTRNS createTRNSChunk() { + PngChunkTRNS trns = new PngChunkTRNS(chunkList.imageInfo); + queueChunk(trns); + return trns; } } diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/package.html b/src/jogl/classes/jogamp/opengl/util/pngj/package.html index 209b39c59..0b0e2c8c1 100644 --- a/src/jogl/classes/jogamp/opengl/util/pngj/package.html +++ b/src/jogl/classes/jogamp/opengl/util/pngj/package.html @@ -1,11 +1,10 @@

-Contains the main classes for the PNGJ library.

-Client code should rarely need more than the public members of this package. +PNGJ main package

-See also the nosandbox package if available. +Client code should rarely need more than the public members of this package.

-- cgit v1.2.3