summaryrefslogtreecommitdiffstats
path: root/src/jogl/classes
diff options
context:
space:
mode:
Diffstat (limited to 'src/jogl/classes')
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java49
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/FilterWriteStrategy.java4
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/ImageInfo.java38
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/ImageLine.java355
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/ImageLineHelper.java318
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/ImageLines.java101
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/PngDeinterlacer.java277
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/PngHelperInternal.java (renamed from src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java)139
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java63
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java10
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/PngReader.java959
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/PngWriter.java677
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/PngjExceptionInternal.java23
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/ProgressiveOutputStream.java8
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkCopyBehaviour.java3
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java151
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java282
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java27
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkPredicate.java14
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java97
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksList.java174
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunksListForWrite.java171
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java239
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java44
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkCHRM.java58
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java30
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java32
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java35
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIDAT.java28
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java20
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java41
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java36
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkMultiple.java27
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkOFFS.java89
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java30
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java23
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java40
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java55
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java28
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSTER.java60
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSingle.java43
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSkipped.java41
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java32
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java38
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTRNS.java60
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java10
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java21
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java28
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java198
-rw-r--r--src/jogl/classes/jogamp/opengl/util/pngj/package.html5
50 files changed, 3891 insertions, 1440 deletions
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)
+ * <p>
+ * This equals the number of elements in the scanline array if working with packedMode=true
+ * <p>
+ * 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).
* <p>
- * 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 <code>int</code> is a "sample" (one for
+ * channel), (0-255 or 0-65535) in the corresponding PNG sequence: <code>R G B R G B...</code> or
+ * <code>R G B A R G B A...</tt>
+ * or <code>g g g ...</code> or <code>i i i</code> (palette index)
* <p>
- * For bitdepth 1/2/4 , each element is a PACKED byte! To get an unpacked copy, see <code>tf_pack()</code> and its
- * inverse <code>tf_unpack()</code>
+ * For bitdepth=1/2/4 , and if samplesUnpacked=false, each value is a PACKED byte!
* <p>
- * To convert a indexed line to RGB balues, see <code>ImageLineHelper.tf_palIdx2RGB()</code> (can't do the reverse)
+ * To convert a indexed line to RGB balues, see <code>ImageLineHelper.palIdx2RGB()</code> (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 <code>scale==true<code>, 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 <code>scale==true<code>, 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.
- * <p>
- * You can (OPTIONALLY) pass an preallocated array to use.
- * <p>
- * 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.
- * <p>
- * 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.
+ * <p>
+ * Not essential at all, some methods are probably to be removed if future releases.
+ * <p>
+ * 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 <code>int[]</code>
+ * <p>
+ * You can (OPTIONALLY) pass an preallocated array, that will be filled and returned. If null, it will be allocated
+ * <p>
+ * If <code>scale==true<code>, it scales the value (just a bit shift) towards 0-255.
+ * <p>
+ * 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
+ * <p>
+ * If <code>scale==true<code>, 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/PngHelperInternal.java
index 1016b1b64..63edf8d17 100644
--- a/src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java
+++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngHelperInternal.java
@@ -4,31 +4,46 @@ 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.
+ * Some utility static methods for internal use.
* <p>
- * See also <code>FileHelper</code> (if not sandboxed).
+ * Client code should not normally use this class
* <p>
- * Client code should rarely need these methods.
*/
-public class PngHelper {
+public class PngHelperInternal {
/**
* 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
+ /**
+ * 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 PngjOutputException(e);
+ throw new PngjInputException("error reading byte", e);
}
}
@@ -111,7 +126,7 @@ public class PngHelper {
}
/**
- * guaranteed to read exactly len bytes. throws error if it cant
+ * 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)
@@ -121,7 +136,7 @@ public class PngHelper {
while (read < len) {
int n = is.read(b, offset + read, len - read);
if (n < 1)
- throw new RuntimeException("error reading bytes, " + n + " !=" + len);
+ throw new PngjInputException("error reading bytes, " + n + " !=" + len);
read += n;
}
} catch (IOException e) {
@@ -129,6 +144,21 @@ public class PngHelper {
}
}
+ 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);
@@ -150,25 +180,6 @@ public class PngHelper {
System.out.println(msg);
}
- public static Set<String> asSet(String... values) {
- return new HashSet<String>(java.util.Arrays.asList(values));
- }
-
- public static Set<String> unionSets(Set<String> set1, Set<String> set2) {
- Set<String> s = new HashSet<String>();
- s.addAll(set1);
- s.addAll(set2);
- return s;
- }
-
- public static Set<String> unionSets(Set<String> set1, Set<String> set2, Set<String> set3) {
- Set<String> s = new HashSet<String>();
- s.addAll(set1);
- s.addAll(set2);
- s.addAll(set3);
- return s;
- }
-
private static final ThreadLocal<CRC32> crcProvider = new ThreadLocal<CRC32>() {
protected CRC32 initialValue() {
return new CRC32();
@@ -180,34 +191,74 @@ public class PngHelper {
return crcProvider.get();
}
- static final byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 }; // png magic
+ // / filters
+ public static int filterRowNone(int r) {
+ return (int) (r & 0xFF);
+ }
- public static double resMetersToDpi(long res) {
- return (double) res * 0.0254;
+ public static int filterRowSub(int r, int left) {
+ return ((int) (r - left) & 0xFF);
}
- public static long resDpiToMeters(double dpi) {
- return (long) (dpi / 0.0254 + 0.5);
+ public static int filterRowUp(int r, int up) {
+ return ((int) (r - up) & 0xFF);
}
- public static int doubleToInt100000(double d) {
- return (int) (d * 100000.0 + 0.5);
+ public static int filterRowAverage(int r, int left, int up) {
+ return (r - (left + up) / 2) & 0xFF;
}
- public static double intToDouble100000(int i) {
- return i / 100000.0;
+ 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 clampTo_0_255(int i) {
- return i > 255 ? 255 : (i < 0 ? 0 : i);
+ public static int unfilterRowNone(int r) {
+ return (int) (r & 0xFF);
}
- public static int clampTo_0_65535(int i) {
- return i > 65535 ? 65535 : (i < 0 ? 0 : i);
+ 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 static int clampTo_128_127(int x) {
- return x > 127 ? 127 : (x < -128 ? -128 : x);
+ 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.
+ * <p>
+ * The reading sequence is as follows: <br>
+ * 1. At construction time, the header and IHDR chunk are read (basic image info) <br>
+ * 2. Afterwards you can set some additional global options. Eg. {@link #setUnpackedMode(boolean)},
+ * {@link #setCrcCheckDisabled()}.<br>
+ * 3. Optional: If you call getMetadata() or getChunksLisk() before start reading the rows, all the chunks before IDAT
+ * are automatically loaded and available <br>
+ * 4a. The rows are read onen by one of the <tt>readRowXXX</tt> 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)<br>
+ * 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.<br>
+ * 5. Read of the last row auyomatically loads the trailing chunks, and ends the reader.<br>
+ * 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<String> 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
+ * <p>
+ * 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
+ * <p>
+ * Position before: after IDHR (crc included) Position after: just after the first IDAT chunk id
+ * <P>
* This can be called several times (tentatively), it does nothing if already run
- *
+ * <p>
* (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 <code>readRow(int[] buffer, int nrow)</code> 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<String>(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.
+ * <p>
+ * The default behaviour is print to stderr, but it can be overriden.
+ * <p>
+ * 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)
+ * <p>
+ * 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
+ * <p>
+ *
+ * @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
+ * <p>
+ * 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.
+ * <p>
+ * You can pass (optionally) a prealocatted buffer.
+ * <p>
+ * 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.
+ * <p>
* You can pass (optionally) a prealocatted buffer.
+ * <p>
+ * If the bitdepth is less than 8, the bytes are packed - unless {@link #unpackedMode} is true. <br>
+ * If the bitdepth is 16, the least significant byte is lost.
+ * <p>
*
* @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.
+ * <p>
+ * This it's more efficient (speed an memory) that doing calling readRowInt() for each desired line only if the
+ * image is interlaced.
+ * <p>
+ * 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.
+ * <p>
+ * This it's more efficient (speed an memory) that doing calling readRowByte() for each desired line only if the
+ * image is interlaced.
+ * <p>
+ * 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). <br>
+ * 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).<br>
+ * 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). <br>
+ * 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. <br>
+ * 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
+ * <p>
+ * 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"<br>
+ * If false (default) packed types (bitdepth=1,2 or 4) will keep several samples packed in one element (byte or int) <br>
+ * 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. <br>
+ * 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
+ * <p>
+ * 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.
* <p>
* See also <code>FileHelper.createPngWriter()</code> 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<PngChunk> pending = chunkList.getQueuedChunks();
+ List<PngChunk> 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.
- * <p>
- * This must be called just after constructor, before starting writing.
- * <p>
- * See also setCompLevel()
- *
- * @param filterType
- * One of the five prediction types or strategy to choose it (see <code>PngFilterType</code>) 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
* <p>
- * This must be called just after constructor, before starting writing.
+ * If we are after idat, only considers those chunks after IDAT in PngReader
* <p>
- * 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.
+ * <p>
+ * 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.
+ * <p>
+ * This must be called just after constructor, before starting writing.
+ * <p>
+ * 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.
+ * <p>
+ * This must be called just after constructor, before starting writing.
+ * <p>
+ * See also setCompLevel()
+ *
+ * @param filterType
+ * One of the five prediction types or strategy to choose it (see <code>PngFilterType</code>) 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
+ * <p>
+ *
+ * @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
+ * <p>
+ * default=true
+ */
+ public void setShouldCloseStream(boolean shouldCloseStream) {
+ this.shouldCloseStream = shouldCloseStream;
+ }
+
+ /**
+ * Deflater strategy: one of Deflater.FILTERED Deflater.HUFFMAN_ONLY Deflater.DEFAULT_STRATEGY
+ * <p>
+ * 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 <tt>packed</tt> 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.
+ * <p>
+ * 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.
+ * <p>
+ * Warning: the array might be modified in some cases (unpacked row with low bitdepth)
+ * <p>
+ *
+ * @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.
+ * <p>
+ * If true, each element is a sample, the writer will perform the packing if necessary.
+ * <p>
+ * Warning: when using {@link #writeRow(ImageLine, int)} (recommended) the <tt>packed</tt> 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.
* <p>
+ * http://www.w3.org/TR/PNG/#14 <br>
* 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<String> 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<PngChunk> filterList(List<PngChunk> target, ChunkPredicate predicateKeep) {
+ List<PngChunk> result = new ArrayList<PngChunk>();
+ 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<PngChunk> target, ChunkPredicate predicateRemove) {
+ Iterator<PngChunk> 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<PngChunk> chunks = new ArrayList<PngChunk>();
-
- /**
- * chunks not yet writen - does not include IHDR, IDAT, END, perhaps yes PLTE
- */
- private Set<PngChunk> queuedChunks = new LinkedHashSet<PngChunk>();
-
- 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<PngChunk> getById(String id, boolean includeQueued, boolean includeProcessed) {
- List<PngChunk> list = new ArrayList<PngChunk>();
- 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<PngChunk> 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<PngChunk> 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<PngChunk> getChunks() {
- return new ArrayList<PngChunk>(chunks);
- }
-
- public List<String> getChunksUnkown() {
- List<String> l = new ArrayList<String>();
- for (PngChunk chunk : chunks)
- if (ChunkHelper.isUnknown(chunk))
- l.add(chunk.id);
- return l;
- }
-
- /**
- * returns a copy of queued (for write) chunks
- */
- public List<PngChunk> getQueuedChunks() {
- return new ArrayList<PngChunk>(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<PngChunk> 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<PngChunk> list = getChunks(id);
- if (list.isEmpty())
- return null;
- if (innerid != null) {
- List<PngChunk> list2 = new ArrayList<PngChunk>();
- 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<PngChunk> getChunks(String id) { // not including queued
- return getById(id, false, true);
- }
-
- private List<PngChunk> getMatching(PngChunk cnew, boolean includeQueued, boolean includeProcessed) {
- List<PngChunk> list = new ArrayList<PngChunk>();
- 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. <br>
+ * 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.
+ * <p>
+ * Short lived object, to be created while serialing/deserializing Do not reuse it for different chunks. <br>
+ * 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.
+ * <p>
+ * 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<PngChunk> chunks = new ArrayList<PngChunk>();
+
+ 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<String, Integer> getChunksKeys() {
+ HashMap<String, Integer> ck = new HashMap<String, Integer>();
+ 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) <b> This should not be used for general metadata
+ * handling
+ */
+ public ArrayList<PngChunk> getChunks() {
+ return new ArrayList<PngChunk>(chunks);
+ }
+
+ protected static List<PngChunk> getXById(final List<PngChunk> 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<? extends PngChunk> 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<? extends PngChunk> 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
+ * <p>
+ * 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
+ * <p>
+ * 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<? extends PngChunk> 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<PngChunk> 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<PngChunk> queuedChunks = new ArrayList<PngChunk>();
+
+ // redundant, just for eficciency
+ private HashMap<String, Integer> alreadyWrittenKeys = new HashMap<String, Integer>();
+
+ public ChunksListForWrite(ImageInfo imfinfo) {
+ super(imfinfo);
+ }
+
+ /**
+ * Same as getById(), but looking in the queued chunks
+ */
+ public List<? extends PngChunk> getQueuedById(final String id) {
+ return getQueuedById(id, null);
+ }
+
+ /**
+ * Same as getById(), but looking in the queued chunks
+ */
+ public List<? extends PngChunk> 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<? extends PngChunk> 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<PngChunk> 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<PngChunk> 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.
+ * <p>
+ * See <a
+ * href="http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html">http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks
+ * .html</a> </a>
+ * <p>
+ * Concrete classes should extend {@link PngChunkSingle} or {@link PngChunkMultiple}
+ * <p>
+ * Note that some methods/fields are type-specific (getOrderingConstraint(), allowsMultiple()),<br>
+ * some are 'almost' type-specific (id,crit,pub,safe; the exception is PngUKNOWN), <br>
+ * 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
+ * <p>
+ * The client can add other chunks to this map statically, before reading an image, calling
+ * PngChunk.factoryRegister(id,class)
*/
- public final static Map<String, Class<? extends PngChunk>> factoryMap = new HashMap<String, Class<? extends PngChunk>>();
+ private final static Map<String, Class<? extends PngChunk>> factoryMap = new HashMap<String, Class<? extends PngChunk>>();
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
+ * <p>
+ * 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<? extends PngChunk> chunkClass) {
+ factoryMap.put(chunkId, chunkClass);
+ }
+
+ /**
+ * True if the chunk-id type is known.
+ * <p>
+ * A chunk is known if we recognize its class, according with <code>factoryMap</code>
+ * <p>
+ * This is not necessarily the same as being "STANDARD", or being implemented in this library
+ * <p>
+ * 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 extends PngChunk> 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 extends PngChunk> 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.
+ * <p>
+ * -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.
+ * <p>
+ * This is used when copying chunks from a reader to a writer
+ * <p>
+ * 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.
+ * <p>
+ * see http://www.w3.org/TR/PNG/#11bKGD
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * see http://www.w3.org/TR/PNG/#11hIST <br>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * see http://www.w3.org/TR/PNG/#11IDAT
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * see http://www.w3.org/TR/PNG/#11IHDR
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * see http://www.w3.org/TR/PNG/#11PLTE
+ * <p>
+ * 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.
+ * <p>
+ * see http://www.w3.org/TR/PNG/#11sBIT
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * see http://www.w3.org/TR/PNG/#11tRNS
+ * <p>
+ * 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);
}
}
@@ -118,6 +120,18 @@ public class PngChunkTRNS extends PngChunk {
}
/**
+ * 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
*/
public int[] getPalletteAlpha() {
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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
- *
+ * <p>
* This includes the palette (if present) and all the ancillary chunks
- *
+ * <p>
* 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
+ * <p>
+ * 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
+ * <p>
+ *
+ * @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
+ * <p>
+ *
+ * @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.
+ * <p>
+ *
+ * @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
+ * <p>
+ * returns null if not found
+ * <p>
+ * Warning: this does not check the "lang" key of iTxt
+ */
+ @SuppressWarnings("unchecked")
+ public List<? extends PngChunkTextVar> 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
+ * <p>
+ * 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<? extends PngChunkTextVar> 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 @@
<html>
<body bgcolor="white">
<p>
-Contains the main classes for the PNGJ library.<p>
-Client code should rarely need more than the public members of this package.
+PNGJ main package
</p>
<p>
-See also the <code>nosandbox</code> package if available.
+Client code should rarely need more than the public members of this package.
</p>
</body>
</html>