diff options
author | Sven Gothel <[email protected]> | 2012-04-07 15:28:37 +0200 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2012-04-07 15:28:37 +0200 |
commit | 40830196070013432bc5f453eb31cfe4c64e0510 (patch) | |
tree | ca31a1e1e27adf9996963176c4f92b84e25623e9 /src/jogl/classes/jogamp/opengl/util/pngj | |
parent | 865c0588de57c9a020a435bc3f08be0f3f6ba162 (diff) |
Merge PNGJ 0.85 into namespace
PNGJ Version 0.85 (1 April 2012)
Apache 2.0 License
http://code.google.com/p/pngj/
Merged code:
- Changed namespace ar.com.hjg.pngj -> jogamp.opengl.util.pngj
to avoid collision when using a different version of PNGJ.
- Removed test and lossy packages and helper classes
to reduce footprint.
License information is added in main LICENSE.txt file.
Diffstat (limited to 'src/jogl/classes/jogamp/opengl/util/pngj')
44 files changed, 4525 insertions, 0 deletions
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java b/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java new file mode 100644 index 000000000..a34f73ab2 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java @@ -0,0 +1,94 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Internal PNG predictor filter, or strategy to select it.
+ *
+ */
+public enum FilterType {
+ /**
+ * No filter.
+ */
+ FILTER_NONE(0),
+ /**
+ * SUB filter (uses same row)
+ */
+ FILTER_SUB(1),
+ /**
+ * UP filter (uses previous row)
+ */
+ FILTER_UP(2),
+ /**
+ * AVERAGE filter
+ */
+ FILTER_AVERAGE(3),
+ /**
+ * PAETH predictor
+ */
+ FILTER_PAETH(4),
+ /**
+ * Default strategy: select one of the above filters depending on global image parameters
+ */
+ FILTER_DEFAULT(-1),
+ /**
+ * Aggresive strategy: select one of the above filters trying each of the filters (this is done every 8 rows)
+ */
+ FILTER_AGGRESSIVE(-2),
+ /**
+ * Uses all fiters, one for lines, cyciclally. Only for tests.
+ */
+ FILTER_ALTERNATE(-3),
+ /**
+ * Aggresive strategy: select one of the above filters trying each of the filters (this is done for every row!)
+ */
+ FILTER_VERYAGGRESSIVE(-4), ;
+ public final int val;
+
+ private FilterType(int val) {
+ this.val = val;
+ }
+
+ public static FilterType getByVal(int i) {
+ for (FilterType ft : values()) {
+ if (ft.val == i)
+ return ft;
+ }
+ 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 new file mode 100644 index 000000000..27586b292 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/FilterWriteStrategy.java @@ -0,0 +1,97 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Manages the writer strategy for selecting the internal png "filter"
+ */
+class FilterWriteStrategy {
+ private static final int COMPUTE_STATS_EVERY_N_LINES = 8;
+
+ final ImageInfo imgInfo;
+ public final FilterType configuredType; // can be negative (fin dout)
+ private FilterType currentType; // 0-4
+ private int lastRowTested = -1000000;
+ // performance of each filter (less is better) (can be negative)
+ private double[] lastSums = new double[5];
+ // performance of each filter (less is better) (can be negative)
+ private double[] lastEntropies = new double[5];
+ // a priori preference (NONE SUB UP AVERAGE PAETH)
+ private double[] preference = new double[] { 1.1, 1.1, 1.1, 1.1, 1.2 };
+ private int discoverEachLines = -1;
+ private double[] histogram1 = new double[256];
+
+ FilterWriteStrategy(ImageInfo imgInfo, FilterType configuredType) {
+ this.imgInfo = imgInfo;
+ this.configuredType = configuredType;
+ if (configuredType.val < 0) { // first guess
+ if ((imgInfo.rows < 8 && imgInfo.cols < 8) || imgInfo.indexed || imgInfo.bitDepth < 8)
+ currentType = FilterType.FILTER_NONE;
+ else
+ currentType = FilterType.FILTER_PAETH;
+ } else {
+ currentType = configuredType;
+ }
+ if (configuredType == FilterType.FILTER_AGGRESSIVE)
+ discoverEachLines = COMPUTE_STATS_EVERY_N_LINES;
+ if (configuredType == FilterType.FILTER_VERYAGGRESSIVE)
+ discoverEachLines = 1;
+ }
+
+ boolean shouldTestAll(int rown) {
+ if (discoverEachLines > 0 && lastRowTested + discoverEachLines <= rown) {
+ currentType = null;
+ return true;
+ } else
+ return false;
+ }
+
+ public void setPreference(double none, double sub, double up, double ave, double paeth) {
+ preference = new double[] { none, sub, up, ave, paeth };
+ }
+
+ public boolean computesStatistics() {
+ return (discoverEachLines > 0);
+ }
+
+ void fillResultsForFilter(int rown, FilterType type, double sum, int[] histo, boolean tentative) {
+ lastRowTested = rown;
+ lastSums[type.val] = sum;
+ if (histo != null) {
+ double v, alfa, beta, e;
+ alfa = rown == 0 ? 0.0 : 0.3;
+ beta = 1 - alfa;
+ e = 0.0;
+ for (int i = 0; i < 256; i++) {
+ v = ((double) histo[i]) / imgInfo.cols;
+ v = histogram1[i] * alfa + v * beta;
+ if (tentative)
+ e += v > 0.00000001 ? v * Math.log(v) : 0.0;
+ else
+ histogram1[i] = v;
+ }
+ lastEntropies[type.val] = (-e);
+ }
+ }
+
+ FilterType gimmeFilterType(int rown, boolean useEntropy) {
+ if (currentType == null) { // get better
+ if (rown == 0)
+ currentType = FilterType.FILTER_SUB;
+ else {
+ double bestval = Double.MAX_VALUE;
+ double val;
+ for (int i = 0; i < 5; i++) {
+ val = useEntropy ? lastEntropies[i] : lastSums[i];
+ val /= preference[i];
+ if (val <= bestval) {
+ bestval = val;
+ currentType = FilterType.getByVal(i);
+ }
+ }
+ }
+ }
+ if (configuredType == FilterType.FILTER_ALTERNATE) {
+ 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 new file mode 100644 index 000000000..2f6b89e9c --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ImageInfo.java @@ -0,0 +1,208 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Simple immutable wrapper for basic image info.
+ * <p>
+ * Some parameters are redundant, but the constructor receives an 'orthogonal' subset.
+ * <p>
+ * ref: http://www.w3.org/TR/PNG/#11IHDR
+ */
+public class ImageInfo {
+
+ // very big value ; actually we are ok with 2**22=4M, perhaps even more
+ private static final int MAX_COLS_ROWS_VAL = 1000000;
+
+ /**
+ * Image width, in pixels.
+ */
+ public final int cols;
+
+ /**
+ * Image height, in pixels
+ */
+ public final int rows;
+
+ /**
+ * Bits per sample (per channel) in the buffer (1-2-4-8-16). This is 8-16 for RGB/ARGB images, 1-2-4-8 for
+ * grayscale. For indexed images, number of bits per palette index (1-2-4-8)
+ */
+ 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.
+ */
+ public final int channels;
+
+ /**
+ * Flag: true if has alpha channel (RGBA/GA)
+ */
+ public final boolean alpha;
+
+ /**
+ * Flag: true if is grayscale (G/GA)
+ */
+ public final boolean greyscale;
+
+ /**
+ * Flag: true if image is indexed, i.e., it has a palette
+ */
+ public final boolean indexed;
+
+ /**
+ * Flag: true if image internally uses less than one byte per sample (bit depth 1-2-4)
+ */
+ public final boolean packed;
+
+ /**
+ * Bits used for each pixel in the buffer: channel * bitDepth
+ */
+ public final int bitspPixel;
+
+ /**
+ * rounded up value: this is only used internally for filter
+ */
+ public final int bytesPixel;
+
+ /**
+ * ceil(bitspp*cols/8)
+ */
+ public final int bytesPerRow;
+
+ /**
+ * Equals cols * channels
+ */
+ public final int samplesPerRow;
+
+ /**
+ * For internal use only. Samples available for our packed scanline. Equals samplesPerRow if not packed. Elsewhere,
+ * it's lower
+ */
+ final int samplesPerRowP;
+
+ /**
+ * Short constructor: assumes truecolor (RGB/RGBA)
+ */
+ public ImageInfo(int cols, int rows, int bitdepth, boolean alpha) {
+ this(cols, rows, bitdepth, alpha, false, false);
+ }
+
+ /**
+ * Full constructor
+ *
+ * @param cols
+ * Width in pixels
+ * @param rows
+ * Height in pixels
+ * @param bitdepth
+ * Bits per sample, in the buffer : 8-16 for RGB true color and greyscale
+ * @param alpha
+ * Flag: has an alpha channel (RGBA or GA)
+ * @param grayscale
+ * Flag: is gray scale (any bitdepth, with or without alpha)
+ * @param indexed
+ * Flag: has palette
+ */
+ public ImageInfo(int cols, int rows, int bitdepth, boolean alpha, boolean grayscale, boolean indexed) {
+ this.cols = cols;
+ this.rows = rows;
+ this.alpha = alpha;
+ this.indexed = indexed;
+ this.greyscale = grayscale;
+ if (greyscale && indexed)
+ throw new PngjException("palette and greyscale are mutually exclusive");
+ this.channels = (grayscale || indexed) ? (alpha ? 2 : 1) : (alpha ? 4 : 3);
+ // http://www.w3.org/TR/PNG/#11IHDR
+ this.bitDepth = bitdepth;
+ this.packed = bitdepth < 8;
+ this.bitspPixel = (channels * this.bitDepth);
+ this.bytesPixel = (bitspPixel + 7) / 8;
+ this.bytesPerRow = (bitspPixel * cols + 7) / 8;
+ this.samplesPerRow = channels * this.cols;
+ this.samplesPerRowP = packed ? bytesPerRow : samplesPerRow;
+ // several checks
+ switch (this.bitDepth) {
+ case 1:
+ case 2:
+ case 4:
+ if (!(this.indexed || this.greyscale))
+ throw new PngjException("only indexed or grayscale can have bitdepth=" + this.bitDepth);
+ break;
+ case 8:
+ break;
+ case 16:
+ if (this.indexed)
+ throw new PngjException("indexed can't have bitdepth=" + this.bitDepth);
+ break;
+ default:
+ throw new PngjException("invalid bitdepth=" + this.bitDepth);
+ }
+ if (cols < 1 || cols > MAX_COLS_ROWS_VAL)
+ throw new PngjException("invalid cols=" + cols + " ???");
+ if (rows < 1 || rows > MAX_COLS_ROWS_VAL)
+ throw new PngjException("invalid rows=" + rows + " ???");
+ }
+
+ @Override
+ 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
+ + ", greyscale=" + greyscale + ", indexed=" + indexed + ", packed=" + packed + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ 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;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ImageInfo other = (ImageInfo) obj;
+ if (alpha != other.alpha)
+ 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)
+ return false;
+ if (greyscale != other.greyscale)
+ 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 new file mode 100644 index 000000000..bfbb35b7c --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ImageLine.java @@ -0,0 +1,175 @@ +package jogamp.opengl.util.pngj;
+
+import java.util.Arrays;
+
+/**
+ * Lightweight wrapper for an image scanline, used for read and write.
+ * <p>
+ * This object can be (usually it is) reused while iterating over the image lines.
+ * <p>
+ * See <code>scanline</code> field, to understand the format.
+ */
+public class ImageLine {
+ public final ImageInfo imgInfo;
+
+ /**
+ * tracks the current row number (from 0 to rows-1)
+ */
+ private int rown = 0;
+
+ /**
+ * 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)
+ * <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>
+ * <p>
+ * To convert a indexed line to RGB balues, see <code>ImageLineHelper.tf_palIdx2RGB()</code> (can't do the reverse)
+ */
+ public final int[] scanline; // see explanation above!!
+
+ 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
+
+ public ImageLine(ImageInfo imgInfo) {
+ this.imgInfo = imgInfo;
+ channels = imgInfo.channels;
+ scanline = new int[imgInfo.samplesPerRowP];
+ this.bitDepth = imgInfo.bitDepth;
+ }
+
+ /** This row number inside the image (0 is top) */
+ public int getRown() {
+ return rown;
+ }
+
+ /** Increments row number */
+ public void incRown() {
+ this.rown++;
+ }
+
+ /** Sets row number */
+ 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);
+ }
+
+ /**
+ * Returns a copy from scanline, in byte array.
+ *
+ * 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;
+ }
+
+ /**
+ * 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];
+ if (bitDepth >= 8)
+ System.arraycopy(scanline, 0, buf, 0, scanline.length);
+ else {
+ int mask, offset, v;
+ int mask0 = getMaskForPackedFormats();
+ int offset0 = 8 - bitDepth;
+ 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++;
+ }
+ }
+ }
+ 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;
+ 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++;
+ }
+ }
+ }
+ }
+
+ 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("?");
+ }
+
+ public FilterType getFilterUsed() {
+ return filterUsed;
+ }
+
+ /**
+ * Basic info
+ */
+ public String toString() {
+ return "row=" + rown + " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java new file mode 100644 index 000000000..1016b1b64 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java @@ -0,0 +1,213 @@ +package jogamp.opengl.util.pngj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.CRC32;
+
+/**
+ * Some utility static methods.
+ * <p>
+ * See also <code>FileHelper</code> (if not sandboxed).
+ * <p>
+ * Client code should rarely need these methods.
+ */
+public class PngHelper {
+ /**
+ * Default charset, used internally by PNG for several things
+ */
+ public static Charset charsetLatin1 = Charset.forName("ISO-8859-1");
+ public static Charset charsetUTF8 = Charset.forName("UTF-8"); // only for some chunks
+
+ static boolean DEBUG = false;
+
+ public static int readByte(InputStream is) {
+ try {
+ return is.read();
+ } catch (IOException e) {
+ throw new PngjOutputException(e);
+ }
+ }
+
+ /**
+ * -1 if eof
+ *
+ * PNG uses "network byte order"
+ */
+ public static int readInt2(InputStream is) {
+ try {
+ int b1 = is.read();
+ int b2 = is.read();
+ if (b1 == -1 || b2 == -1)
+ return -1;
+ return (b1 << 8) + b2;
+ } catch (IOException e) {
+ throw new PngjInputException("error reading readInt2", e);
+ }
+ }
+
+ /**
+ * -1 if eof
+ */
+ public static int readInt4(InputStream is) {
+ try {
+ int b1 = is.read();
+ int b2 = is.read();
+ int b3 = is.read();
+ int b4 = is.read();
+ if (b1 == -1 || b2 == -1 || b3 == -1 || b4 == -1)
+ return -1;
+ return (b1 << 24) + (b2 << 16) + (b3 << 8) + b4;
+ } catch (IOException e) {
+ throw new PngjInputException("error reading readInt4", e);
+ }
+ }
+
+ public static int readInt1fromByte(byte[] b, int offset) {
+ return (b[offset] & 0xff);
+ }
+
+ public static int readInt2fromBytes(byte[] b, int offset) {
+ return ((b[offset] & 0xff) << 16) | ((b[offset + 1] & 0xff));
+ }
+
+ public static int readInt4fromBytes(byte[] b, int offset) {
+ return ((b[offset] & 0xff) << 24) | ((b[offset + 1] & 0xff) << 16) | ((b[offset + 2] & 0xff) << 8)
+ | (b[offset + 3] & 0xff);
+ }
+
+ public static void writeByte(OutputStream os, byte b) {
+ try {
+ os.write(b);
+ } catch (IOException e) {
+ throw new PngjOutputException(e);
+ }
+ }
+
+ public static void writeInt2(OutputStream os, int n) {
+ byte[] temp = { (byte) ((n >> 8) & 0xff), (byte) (n & 0xff) };
+ writeBytes(os, temp);
+ }
+
+ public static void writeInt4(OutputStream os, int n) {
+ byte[] temp = new byte[4];
+ writeInt4tobytes(n, temp, 0);
+ writeBytes(os, temp);
+ }
+
+ public static void writeInt2tobytes(int n, byte[] b, int offset) {
+ b[offset] = (byte) ((n >> 8) & 0xff);
+ b[offset + 1] = (byte) (n & 0xff);
+ }
+
+ public static void writeInt4tobytes(int n, byte[] b, int offset) {
+ b[offset] = (byte) ((n >> 24) & 0xff);
+ b[offset + 1] = (byte) ((n >> 16) & 0xff);
+ b[offset + 2] = (byte) ((n >> 8) & 0xff);
+ b[offset + 3] = (byte) (n & 0xff);
+ }
+
+ /**
+ * guaranteed to read exactly len bytes. throws error if it cant
+ */
+ public static void readBytes(InputStream is, byte[] b, int offset, int len) {
+ if (len == 0)
+ return;
+ try {
+ int read = 0;
+ while (read < len) {
+ int n = is.read(b, offset + read, len - read);
+ if (n < 1)
+ throw new RuntimeException("error reading bytes, " + n + " !=" + len);
+ read += n;
+ }
+ } catch (IOException e) {
+ throw new PngjInputException("error reading", e);
+ }
+ }
+
+ public static void writeBytes(OutputStream os, byte[] b) {
+ try {
+ os.write(b);
+ } catch (IOException e) {
+ throw new PngjOutputException(e);
+ }
+ }
+
+ public static void writeBytes(OutputStream os, byte[] b, int offset, int n) {
+ try {
+ os.write(b, offset, n);
+ } catch (IOException e) {
+ throw new PngjOutputException(e);
+ }
+ }
+
+ public static void logdebug(String msg) {
+ if (DEBUG)
+ System.out.println(msg);
+ }
+
+ public static Set<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();
+ }
+ };
+
+ /** thread-singleton crc engine */
+ public static CRC32 getCRC() {
+ return crcProvider.get();
+ }
+
+ static final byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 }; // png magic
+
+ public static double resMetersToDpi(long res) {
+ return (double) res * 0.0254;
+ }
+
+ public static long resDpiToMeters(double dpi) {
+ return (long) (dpi / 0.0254 + 0.5);
+ }
+
+ public static int doubleToInt100000(double d) {
+ return (int) (d * 100000.0 + 0.5);
+ }
+
+ public static double intToDouble100000(int i) {
+ return i / 100000.0;
+ }
+
+ public static int clampTo_0_255(int i) {
+ return i > 255 ? 255 : (i < 0 ? 0 : i);
+ }
+
+ public static int clampTo_0_65535(int i) {
+ return i > 65535 ? 65535 : (i < 0 ? 0 : i);
+ }
+
+ public static int clampTo_128_127(int x) {
+ return x > 127 ? 127 : (x < -128 ? -128 : x);
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java new file mode 100644 index 000000000..66c4b49f0 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java @@ -0,0 +1,153 @@ +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.zip.CRC32;
+
+import jogamp.opengl.util.pngj.chunks.ChunkHelper;
+
+
+/**
+ * Reads IDAT chunks
+ */
+class PngIDatChunkInputStream extends InputStream {
+ private final InputStream inputStream;
+ private final CRC32 crcEngine;
+ private int lenLastChunk;
+ private byte[] idLastChunk = new byte[4];
+ private int toReadThisChunk = 0;
+ private boolean ended = false;
+ private long offset; // offset inside inputstream
+
+ // just informational
+ static class IdatChunkInfo {
+ public final int len;
+ public final int offset;
+
+ private IdatChunkInfo(int len, int offset) {
+ this.len = len;
+ this.offset = offset;
+ }
+ }
+
+ List<IdatChunkInfo> foundChunksInfo = new ArrayList<IdatChunkInfo>();
+
+ /**
+ * 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;
+ 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.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...
+ }
+
+ /**
+ * does NOT close the associated stream!
+ */
+ @Override
+ public void close() throws IOException {
+ super.close(); // nothing
+ }
+
+ private void endChunkGoForNext() {
+ // Called after readging the last byte of 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); //
+ 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);
+ toReadThisChunk = lenLastChunk;
+ PngHelper.readBytes(inputStream, idLastChunk, 0, 4);
+ offset += 8;
+ ended = !Arrays.equals(idLastChunk, ChunkHelper.b_IDAT);
+ if (!ended) {
+ foundChunksInfo.add(new IdatChunkInfo(lenLastChunk, (int) (offset - 8)));
+ crcEngine.update(idLastChunk, 0, 4);
+ }
+ // PngHelper.logdebug("IDAT ended. next len= " + lenLastChunk + " idat?" +
+ // (!ended));
+ } while (lenLastChunk == 0 && !ended);
+ // rarely condition is true (empty IDAT ??)
+ }
+
+ /**
+ * sometimes last row read does not fully consumes the chunk here we read the reamaing dummy bytes
+ */
+ void forceChunkEnd() {
+ if (!ended) {
+ byte[] dummy = new byte[toReadThisChunk];
+ PngHelper.readBytes(inputStream, dummy, 0, toReadThisChunk);
+ crcEngine.update(dummy, 0, toReadThisChunk);
+ endChunkGoForNext();
+ }
+ }
+
+ /**
+ * This can return less than len, but never 0 Returns -1 if "pseudo file" ended prematurely. That is our error.
+ */
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (toReadThisChunk == 0)
+ throw new RuntimeException("this should not happen");
+ int n = inputStream.read(b, off, len >= toReadThisChunk ? toReadThisChunk : len);
+ if (n > 0) {
+ crcEngine.update(b, off, n);
+ this.offset += n;
+ toReadThisChunk -= n;
+ }
+ if (toReadThisChunk == 0) { // end of chunk: prepare for next
+ endChunkGoForNext();
+ }
+ return n;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return this.read(b, 0, b.length);
+ }
+
+ @Override
+ public int read() throws IOException {
+ // PngHelper.logdebug("read() should go here");
+ // inneficient - but this should be used rarely
+ byte[] b1 = new byte[1];
+ int r = this.read(b1, 0, 1);
+ return r < 0 ? -1 : (int) b1[0];
+ }
+
+ int getLenLastChunk() {
+ return lenLastChunk;
+ }
+
+ byte[] getIdLastChunk() {
+ return idLastChunk;
+ }
+
+ long getOffset() {
+ return offset;
+ }
+
+ boolean isEnded() {
+ return ended;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java new file mode 100644 index 000000000..8b9fa5dae --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java @@ -0,0 +1,31 @@ +package jogamp.opengl.util.pngj;
+
+import java.io.OutputStream;
+
+import jogamp.opengl.util.pngj.chunks.ChunkHelper;
+import jogamp.opengl.util.pngj.chunks.ChunkRaw;
+
+
+/**
+ * outputs the stream for IDAT chunk , fragmented at fixed size (16384 default).
+ */
+class PngIDatChunkOutputStream extends ProgressiveOutputStream {
+ private static final int SIZE_DEFAULT = 16384;
+ private final OutputStream outputStream;
+
+ PngIDatChunkOutputStream(OutputStream outputStream) {
+ this(outputStream, SIZE_DEFAULT);
+ }
+
+ PngIDatChunkOutputStream(OutputStream outputStream, int size) {
+ super(size);
+ this.outputStream = outputStream;
+ }
+
+ @Override
+ public 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 new file mode 100644 index 000000000..7343893b6 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngReader.java @@ -0,0 +1,415 @@ +package jogamp.opengl.util.pngj;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.zip.InflaterInputStream;
+
+import jogamp.opengl.util.pngj.PngIDatChunkInputStream.IdatChunkInfo;
+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.PngChunk;
+import jogamp.opengl.util.pngj.chunks.PngChunkIHDR;
+import jogamp.opengl.util.pngj.chunks.PngMetadata;
+
+
+/**
+ * Reads a PNG image, line by line
+ */
+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;
+
+ private final InputStream is;
+ private InflaterInputStream idatIstream;
+ private PngIDatChunkInputStream iIdatCstream;
+
+ protected int currentChunkGroup = -1;
+ protected int rowNum = -1; // current row number
+ private int offset = 0;
+ private int bytesChunksLoaded; // bytes loaded from anciallary chunks
+
+ protected ImageLine imgLine;
+
+ // line as bytes, counting from 1 (index 0 is reserved for filter type)
+ protected byte[] rowb = null;
+ protected byte[] rowbprev = null; // rowb previous
+ protected byte[] rowbfilter = null; // current line 'filtered': exactly as in uncompressed stream
+
+ /**
+ * 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
+ */
+ private final ChunkList chunksList;
+ private final PngMetadata metadata; // this a wrapper over chunks
+
+ /**
+ * Constructs a PngReader from an InputStream.
+ * <p>
+ * See also <code>FileHelper.createPngReader(File f)</code> if available.
+ *
+ * Reads only the signature and first chunk (IDHR)
+ *
+ * @param filenameOrDescription
+ * : Optional, can be a filename or a description. Just for error/debug messages
+ *
+ */
+ 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);
+ offset += pngid.length;
+ if (!Arrays.equals(pngid, PngHelper.pngIdBytes))
+ throw new PngjInputException("Bad PNG signature");
+ // reads first chunk
+ currentChunkGroup = ChunkList.CHUNK_GROUP_0_IDHR;
+ int clen = PngHelper.readInt4(is);
+ offset += 4;
+ if (clen != 13)
+ throw new RuntimeException("IDHR chunk len != 13 ?? " + clen);
+ byte[] chunkid = new byte[4];
+ PngHelper.readBytes(is, 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);
+ boolean alpha = (ihdr.getColormodel() & 0x04) != 0;
+ boolean palette = (ihdr.getColormodel() & 0x01) != 0;
+ boolean grayscale = (ihdr.getColormodel() == 0 || ihdr.getColormodel() == 4);
+ 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");
+ 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 FoundChunkInfo(String id, int len, int offset, boolean loaded) {
+ this.id = id;
+ this.len = len;
+ this.offset = offset;
+ this.loaded = loaded;
+ }
+
+ public String toString() {
+ return "chunk " + id + " len=" + len + " offset=" + offset + (this.loaded ? " " : " X ");
+ }
+ }
+
+ private PngChunk addChunkToList(ChunkRaw chunk) {
+ // this requires that the currentChunkGroup is ok
+ PngChunk chunkType = PngChunk.factory(chunk, imgInfo);
+ if (!chunkType.crit) {
+ bytesChunksLoaded += chunk.len;
+ }
+ 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");
+ }
+ 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
+ *
+ * This can be called several times (tentatively), it does nothing if already run
+ *
+ * (Note: when should this be called? in the constructor? hardly, because we loose the opportunity to call
+ * setChunkLoadBehaviour() and perhaps other settings before reading the first row? but sometimes we want to access
+ * some metadata (plte, phys) before. Because of this, this method can be called explicitly but is also called
+ * implicititly in some methods (getMetatada(), getChunks())
+ *
+ **/
+ public void readFirstChunks() {
+ 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;
+ while (!found) {
+ clen = PngHelper.readInt4(is);
+ offset += 4;
+ if (clen < 0)
+ break;
+ PngHelper.readBytes(is, chunkid, 0, 4);
+ offset += 4;
+ if (Arrays.equals(chunkid, ChunkHelper.b_IDAT)) {
+ found = true;
+ currentChunkGroup = ChunkList.CHUNK_GROUP_4_IDAT;
+ // add dummy idat chunk to list
+ ChunkRaw chunk = new ChunkRaw(0, chunkid, false);
+ addChunkToList(chunk);
+ 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;
+ }
+ int idatLen = found ? clen : -1;
+ if (idatLen < 0)
+ throw new PngjInputException("first idat chunk not found!");
+ iIdatCstream = new PngIDatChunkInputStream(is, idatLen, offset);
+ idatIstream = new InflaterInputStream(iIdatCstream);
+ }
+
+ /**
+ * Reads (and processes) chunks after last IDAT.
+ **/
+ private void readLastChunks() {
+ // PngHelper.logdebug("idat ended? " + iIdatCstream.isEnded());
+ currentChunkGroup = ChunkList.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;
+ while (!endfound) {
+ ignore = false;
+ if (!first) {
+ clen = PngHelper.readInt4(is);
+ offset += 4;
+ if (clen < 0)
+ throw new PngjInputException("bad len " + clen);
+ PngHelper.readBytes(is, chunkid, 0, 4);
+ offset += 4;
+ }
+ first = false;
+ if (Arrays.equals(chunkid, ChunkHelper.b_IDAT)) {
+ // PngHelper.logdebug("extra IDAT chunk len - ignoring : ");
+ ignore = true;
+ } else if (Arrays.equals(chunkid, ChunkHelper.b_IEND)) {
+ currentChunkGroup = ChunkList.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);
+ }
+ }
+ if (!endfound)
+ throw new PngjInputException("end chunk not found - offset=" + offset);
+ // PngHelper.logdebug("end chunk found ok offset=" + offset);
+ }
+
+ /**
+ * Calls <code>readRow(int[] buffer, int nrow)</code> using internal ImageLine as buffer. This doesn't allocate or
+ * copy anything.
+ *
+ * @return The ImageLine that also is available inside this object.
+ */
+ public ImageLine readRow(int nrow) {
+ readRow(imgLine.scanline, nrow);
+ imgLine.filterUsed = FilterType.getByVal(rowbfilter[0]);
+ imgLine.setRown(nrow);
+ return imgLine;
+ }
+
+ /**
+ * Reads a line and returns it as a int[] array.
+ *
+ * You can pass (optionally) a prealocatted buffer.
+ *
+ * @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.
+ *
+ * @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));
+ if (nrow == 0 && firstChunksNotYetRead())
+ readFirstChunks();
+ rowNum++;
+ if (buffer == null || buffer.length < imgInfo.samplesPerRowP)
+ buffer = new int[imgInfo.samplesPerRowP];
+ // swap
+ byte[] tmp = rowb;
+ rowb = rowbprev;
+ rowbprev = tmp;
+ // loads in rowbfilter "raw" bytes, with filter
+ PngHelper.readBytes(idatIstream, rowbfilter, 0, rowbfilter.length);
+ rowb[0] = 0;
+ unfilterRow();
+ rowb[0] = rowbfilter[0];
+ convertRowFromBytes(buffer);
+ return buffer;
+ }
+
+ /**
+ * This should be called after having read the last line. It reads extra chunks after IDAT, if present.
+ */
+ public void end() {
+ offset = (int) iIdatCstream.getOffset();
+ try {
+ idatIstream.close();
+ } catch (Exception e) {
+ }
+ readLastChunks();
+ try {
+ is.close();
+ } catch (Exception e) {
+ throw new PngjInputException("error closing input stream!", e);
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ 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");
+ }
+ }
+
+ private void unfilterRowNone() {
+ for (int i = 1; i <= imgInfo.bytesPerRow; i++) {
+ rowb[i] = (byte) (rowbfilter[i]);
+ }
+ }
+
+ 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]);
+ }
+ }
+
+ private void unfilterRowUp() {
+ for (int i = 1; i <= imgInfo.bytesPerRow; i++) {
+ rowb[i] = (byte) (rowbfilter[i] + rowbprev[i]);
+ }
+ }
+
+ 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);
+ }
+ }
+
+ 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));
+ }
+ }
+
+ public ChunkLoadBehaviour getChunkLoadBehaviour() {
+ return chunkLoadBehaviour;
+ }
+
+ public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) {
+ this.chunkLoadBehaviour = chunkLoadBehaviour;
+ }
+
+ private boolean firstChunksNotYetRead() {
+ return currentChunkGroup < ChunkList.CHUNK_GROUP_1_AFTERIDHR;
+ }
+
+ public ChunkList getChunksList() {
+ if (firstChunksNotYetRead())
+ readFirstChunks();
+ return chunksList;
+ }
+
+ public PngMetadata getMetadata() {
+ if (firstChunksNotYetRead())
+ readFirstChunks();
+ return metadata;
+ }
+
+ 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 new file mode 100644 index 000000000..ee8472bf0 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngWriter.java @@ -0,0 +1,462 @@ +package jogamp.opengl.util.pngj;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+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.PngChunk;
+import jogamp.opengl.util.pngj.chunks.PngChunkIEND;
+import jogamp.opengl.util.pngj.chunks.PngChunkIHDR;
+import jogamp.opengl.util.pngj.chunks.PngChunkTextVar;
+import jogamp.opengl.util.pngj.chunks.PngMetadata;
+
+
+/**
+ * Writes a PNG image, line by line.
+ */
+public class PngWriter {
+
+ public final ImageInfo imgInfo;
+
+ protected int compLevel = 6; // zip compression level 0 - 9
+ private int deflaterStrategy = Deflater.FILTERED;
+ protected FilterWriteStrategy filterStrat;
+
+ 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
+
+ protected final OutputStream os;
+ protected final String filename; // optional, can be a description
+
+ private PngIDatChunkOutputStream datStream;
+ private DeflaterOutputStream datStreamDeflated;
+
+ private final ChunkList chunkList;
+ private final PngMetadata metadata; // high level wrapper over chunkList
+
+ public PngWriter(OutputStream outputStream, ImageInfo imgInfo) {
+ this(outputStream, imgInfo, "[NO FILENAME AVAILABLE]");
+ }
+
+ /**
+ * Constructs a new PngWriter from a output stream.
+ * <p>
+ * See also <code>FileHelper.createPngWriter()</code> if available.
+ *
+ * @param outputStream
+ * Opened stream for binary writing
+ * @param imgInfo
+ * Basic image parameters
+ * @param filenameOrDescription
+ * Optional, just for error/debug messages
+ */
+ public PngWriter(OutputStream outputStream, ImageInfo imgInfo, String filenameOrDescription) {
+ this.filename = filenameOrDescription == null ? "" : filenameOrDescription;
+ 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);
+ }
+
+ /**
+ * 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);
+ }
+ 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);
+
+ }
+
+ 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);
+ 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;
+ }
+
+ private void writeLastChunks() { // not including end
+ currentChunkGroup = ChunkList.CHUNK_GROUP_5_AFTERIDAT;
+ chunkList.writeChunks(os, currentChunkGroup);
+ // should not be unwriten chunks
+ List<PngChunk> pending = chunkList.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);
+ }
+
+ /**
+ * 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
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Same as writeRow(int[] newrow, int rown), but does not check row number
+ *
+ * @param newrow
+ */
+ public void writeRow(int[] newrow) {
+ writeRow(newrow, -1);
+ }
+
+ /**
+ * 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());
+ }
+
+ /**
+ * 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]++;
+ }
+ filterStrat.fillResultsForFilter(rown, type, s, histox, tentative);
+ }
+
+ private void filterRow(int rown) {
+ // warning: filters operation rely on: "previos row" (rowbprev) is
+ // initialized to 0 the first time
+ if (filterStrat.shouldTestAll(rown)) {
+ filterRowNone();
+ reportResultsForFilter(rown, FilterType.FILTER_NONE, true);
+ filterRowSub();
+ reportResultsForFilter(rown, FilterType.FILTER_SUB, true);
+ filterRowUp();
+ reportResultsForFilter(rown, FilterType.FILTER_UP, true);
+ filterRowAverage();
+ reportResultsForFilter(rown, FilterType.FILTER_AVERAGE, true);
+ filterRowPaeth();
+ reportResultsForFilter(rown, FilterType.FILTER_PAETH, true);
+ }
+ FilterType filterType = filterStrat.gimmeFilterType(rown, true);
+ rowbfilter[0] = (byte) filterType.val;
+ switch (filterType) {
+ case FILTER_NONE:
+ filterRowNone();
+ break;
+ case FILTER_SUB:
+ filterRowSub();
+ break;
+ case FILTER_UP:
+ filterRowUp();
+ break;
+ case FILTER_AVERAGE:
+ filterRowAverage();
+ break;
+ case FILTER_PAETH:
+ filterRowPaeth();
+ break;
+ default:
+ throw new PngjOutputException("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;
+ }
+
+ protected void filterRowNone() {
+ for (int i = 1; i <= imgInfo.bytesPerRow; i++) {
+ rowbfilter[i] = (byte) rowb[i];
+ }
+ }
+
+ 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 filterRowUp() {
+ 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);
+ }
+ }
+
+ 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));
+ }
+ }
+
+ protected void convertRowToBytes() {
+ // 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++) {
+ 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]);
+ }
+ }
+ }
+
+ // /// several getters / setters - all this setters are optional
+
+ /**
+ * Filename or description, from the optional constructor argument.
+ */
+ public String getFilename() {
+ return filename;
+ }
+
+ /**
+ * 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 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 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
+ */
+ private void copyChunks(PngReader reader, int copy_mask, boolean onlyAfterIdat) {
+ boolean idatDone = currentChunkGroup >= ChunkList.CHUNK_GROUP_4_IDAT;
+ for (PngChunk chunk : reader.getChunksList().getChunks()) {
+ int group = chunk.getChunkGroup();
+ if (group < ChunkList.CHUNK_GROUP_4_IDAT && idatDone)
+ continue;
+ boolean copy = false;
+ if (chunk.crit) {
+ if (chunk.id.equals(ChunkHelper.PLTE)) {
+ if (imgInfo.indexed && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_PALETTE))
+ copy = true;
+ if (!imgInfo.greyscale && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALL))
+ copy = true;
+ }
+ } else { // ancillary
+ boolean text = (chunk instanceof PngChunkTextVar);
+ boolean safe = chunk.safe;
+ // notice that these if are not exclusive
+ if (ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALL))
+ copy = true;
+ if (safe && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALL_SAFE))
+ copy = true;
+ if (chunk.id.equals(ChunkHelper.tRNS)
+ && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_TRANSPARENCY))
+ copy = true;
+ if (chunk.id.equals(ChunkHelper.pHYs) && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_PHYS))
+ copy = true;
+ if (text && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_TEXTUAL))
+ copy = true;
+ if (ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALMOSTALL)
+ && !(ChunkHelper.isUnknown(chunk) || text || chunk.id.equals(ChunkHelper.hIST) || chunk.id
+ .equals(ChunkHelper.tIME)))
+ copy = true;
+ }
+ if (copy) {
+ chunkList.queueChunk(PngChunk.cloneChunk(chunk, imgInfo), !chunk.allowsMultiple(), false);
+ }
+ }
+ }
+
+ /**
+ * Copies first (pre IDAT) ancillary chunks from a PngReader.
+ * <p>
+ * Should be called when creating an image from another, before starting writing lines, to copy relevant chunks.
+ * <p>
+ *
+ * @param reader
+ * : PngReader object, already opened.
+ * @param copy_mask
+ * : Mask bit (OR), see <code>ChunksToWrite.COPY_XXX</code> constants
+ */
+ public void copyChunksFirst(PngReader reader, int copy_mask) {
+ copyChunks(reader, copy_mask, false);
+ }
+
+ /**
+ * Copies last (post IDAT) ancillary chunks from a PngReader.
+ * <p>
+ * Should be called when creating an image from another, after writing all lines, before closing the writer, to copy
+ * additional chunks.
+ * <p>
+ *
+ * @param reader
+ * : PngReader object, already opened and fully read.
+ * @param copy_mask
+ * : Mask bit (OR), see <code>ChunksToWrite.COPY_XXX</code> constants
+ */
+ public void copyChunksLast(PngReader reader, int copy_mask) {
+ copyChunks(reader, copy_mask, true);
+ }
+
+ public ChunkList getChunkList() {
+ return chunkList;
+ }
+
+ public PngMetadata getMetadata() {
+ return metadata;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngjBadCrcException.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngjBadCrcException.java new file mode 100644 index 000000000..3b74f862f --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngjBadCrcException.java @@ -0,0 +1,20 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Exception thrown by bad CRC check
+ */
+public class PngjBadCrcException extends PngjInputException {
+ private static final long serialVersionUID = 1L;
+
+ public PngjBadCrcException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PngjBadCrcException(String message) {
+ super(message);
+ }
+
+ public PngjBadCrcException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngjException.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngjException.java new file mode 100644 index 000000000..4a45cb5bf --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngjException.java @@ -0,0 +1,23 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Generic exception
+ *
+ * @author Hernan J Gonzalez
+ *
+ */
+public class PngjException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public PngjException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PngjException(String message) {
+ super(message);
+ }
+
+ public PngjException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngjInputException.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngjInputException.java new file mode 100644 index 000000000..5cc36b99a --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngjInputException.java @@ -0,0 +1,20 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Exception thrown by reading process
+ */
+public class PngjInputException extends PngjException {
+ private static final long serialVersionUID = 1L;
+
+ public PngjInputException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PngjInputException(String message) {
+ super(message);
+ }
+
+ public PngjInputException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngjOutputException.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngjOutputException.java new file mode 100644 index 000000000..c8cd36acb --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngjOutputException.java @@ -0,0 +1,20 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Exception thrown by writing process
+ */
+public class PngjOutputException extends PngjException {
+ private static final long serialVersionUID = 1L;
+
+ public PngjOutputException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PngjOutputException(String message) {
+ super(message);
+ }
+
+ public PngjOutputException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/PngjUnsupportedException.java b/src/jogl/classes/jogamp/opengl/util/pngj/PngjUnsupportedException.java new file mode 100644 index 000000000..0801e33bb --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/PngjUnsupportedException.java @@ -0,0 +1,24 @@ +package jogamp.opengl.util.pngj;
+
+/**
+ * Exception thrown because of some valid feature of PNG standard that this library does not support
+ */
+public class PngjUnsupportedException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public PngjUnsupportedException() {
+ super();
+ }
+
+ public PngjUnsupportedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PngjUnsupportedException(String message) {
+ super(message);
+ }
+
+ public PngjUnsupportedException(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 new file mode 100644 index 000000000..bbec247fb --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/ProgressiveOutputStream.java @@ -0,0 +1,71 @@ +package jogamp.opengl.util.pngj;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * stream that outputs to memory and allows to flush fragments every 'size' bytes to some other destination
+ */
+abstract class ProgressiveOutputStream extends ByteArrayOutputStream {
+ private final int size;
+
+ public ProgressiveOutputStream(int size) {
+ this.size = size;
+ }
+
+ @Override
+ public final void close() throws IOException {
+ flush();
+ super.close();
+ }
+
+ @Override
+ public final void flush() throws IOException {
+ super.flush();
+ checkFlushBuffer(true);
+ }
+
+ @Override
+ public final void write(byte[] b, int off, int len) {
+ super.write(b, off, len);
+ checkFlushBuffer(false);
+ }
+
+ @Override
+ public final void write(byte[] b) throws IOException {
+ super.write(b);
+ checkFlushBuffer(false);
+ }
+
+ @Override
+ public final void write(int arg0) {
+ super.write(arg0);
+ checkFlushBuffer(false);
+ }
+
+ @Override
+ public final synchronized void reset() {
+ super.reset();
+ }
+
+ /**
+ * if it's time to flush data (or if forced==true) calls abstract method flushBuffer() and cleans those bytes from
+ * own buffer
+ */
+ private final void checkFlushBuffer(boolean forced) {
+ while (forced || count >= size) {
+ int nb = size;
+ if (nb > count)
+ nb = count;
+ if (nb == 0)
+ return;
+ flushBuffer(buf, nb);
+ int bytesleft = count - nb;
+ count = bytesleft;
+ if (bytesleft > 0)
+ System.arraycopy(buf, nb, buf, 0, bytesleft);
+ }
+ }
+
+ public abstract void flushBuffer(byte[] b, int n);
+}
\ 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 new file mode 100644 index 000000000..43c0cb135 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkCopyBehaviour.java @@ -0,0 +1,24 @@ +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
+ * <p>
+ * These are masks, can be OR-ed
+ **/
+public class ChunkCopyBehaviour {
+
+ /** dont copy anywhing */
+ public static final int COPY_NONE = 0;
+
+ /** copy the palette */
+ public static final int COPY_PALETTE = 1;
+
+ /** copy all 'safe to copy' chunks */
+ public static final int COPY_ALL_SAFE = 1 << 2;
+ public static final int COPY_ALL = 1 << 3; // includes palette!
+ public static final int COPY_PHYS = 1 << 4; // dpi
+ public static final int COPY_TEXTUAL = 1 << 5; // all textual types
+ public static final int COPY_TRANSPARENCY = 1 << 6; //
+ public static final int COPY_UNKNOWN = 1 << 7; // all unknown (by the factory!)
+ public static final int COPY_ALMOSTALL = 1 << 8; // almost all known (except HIST and TIME and textual)
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java new file mode 100644 index 000000000..26dafd4eb --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java @@ -0,0 +1,134 @@ +package jogamp.opengl.util.pngj.chunks;
+
+// see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
+// http://www.w3.org/TR/PNG/#5Chunk-naming-conventions
+// http://www.w3.org/TR/PNG/#table53
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Set;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+
+public class ChunkHelper {
+ public static final String IHDR = "IHDR";
+ public static final String PLTE = "PLTE";
+ public static final String IDAT = "IDAT";
+ public static final String IEND = "IEND";
+ public static final byte[] b_IHDR = toBytes(IHDR);
+ public static final byte[] b_PLTE = toBytes(PLTE);
+ public static final byte[] b_IDAT = toBytes(IDAT);
+ public static final byte[] b_IEND = toBytes(IEND);
+
+ public static final String cHRM = "cHRM";
+ public static final String gAMA = "gAMA";
+ public static final String iCCP = "iCCP";
+ public static final String sBIT = "sBIT";
+ public static final String sRGB = "sRGB";
+ public static final String bKGD = "bKGD";
+ public static final String hIST = "hIST";
+ public static final String tRNS = "tRNS";
+ public static final String pHYs = "pHYs";
+ public static final String sPLT = "sPLT";
+ public static final String tIME = "tIME";
+ public static final String iTXt = "iTXt";
+ 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);
+
+ public static byte[] toBytes(String x) {
+ return x.getBytes(PngHelper.charsetLatin1);
+ }
+
+ public static String toString(byte[] x) {
+ return new String(x, PngHelper.charsetLatin1);
+ }
+
+ public static boolean isCritical(String id) { // critical chunk ?
+ // first letter is uppercase
+ return (Character.isUpperCase(id.charAt(0)));
+ }
+
+ public static boolean isPublic(String id) { // public chunk?
+ // second letter is uppercase
+ 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
+ */
+ public static boolean isUnknown(PngChunk c) {
+ return c instanceof PngChunkUNKNOWN;
+ }
+
+ public static boolean isSafeToCopy(String id) { // safe to copy?
+ // fourth letter is lower case
+ return (!Character.isUpperCase(id.charAt(3)));
+ }
+
+ public static int posNullByte(byte[] b) {
+ for (int i = 0; i < b.length; i++)
+ if (b[i] == 0)
+ return i;
+ return -1;
+ }
+
+ public static boolean shouldLoad(String id, ChunkLoadBehaviour behav) {
+ if (isCritical(id))
+ return true;
+ boolean kwown = PngChunk.isKnown(id);
+ switch (behav) {
+ case LOAD_CHUNK_ALWAYS:
+ return true;
+ case LOAD_CHUNK_IF_SAFE:
+ return kwown || isSafeToCopy(id);
+ case LOAD_CHUNK_KNOWN:
+ return kwown;
+ case LOAD_CHUNK_NEVER:
+ return false;
+ }
+ return false; // should not reach here
+ }
+
+ public final static byte[] compressBytes(byte[] ori, boolean compress) {
+ return compressBytes(ori, 0, ori.length, compress);
+ }
+
+ public static byte[] compressBytes(byte[] ori, int offset, int len, boolean compress) {
+ try {
+ ByteArrayInputStream inb = new ByteArrayInputStream(ori, offset, len);
+ InputStream in = compress ? inb : new InflaterInputStream(inb);
+ ByteArrayOutputStream outb = new ByteArrayOutputStream();
+ OutputStream out = compress ? new DeflaterOutputStream(outb) : outb;
+ shovelInToOut(in, out);
+ in.close();
+ out.close();
+ return outb.toByteArray();
+ } catch (Exception e) {
+ throw new PngjException(e);
+ }
+ }
+
+ /**
+ * Shovels all data from an input stream to an output stream.
+ */
+ private static void shovelInToOut(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024];
+ int len;
+ while ((len = in.read(buffer)) > 0) {
+ out.write(buffer, 0, len);
+ }
+ }
+
+ public static boolean maskMatch(int v, int mask) {
+ return (v & mask) != 0;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java new file mode 100644 index 000000000..badbbd0e8 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java @@ -0,0 +1,282 @@ +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 new file mode 100644 index 000000000..a3f85355c --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java @@ -0,0 +1,10 @@ +package jogamp.opengl.util.pngj.chunks;
+
+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 */
+ ;
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java new file mode 100644 index 000000000..6770d5e95 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java @@ -0,0 +1,83 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.CRC32;
+
+import jogamp.opengl.util.pngj.PngHelper;
+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
+ */
+public class ChunkRaw {
+ public final int len;
+ public final byte[] idbytes = new byte[4]; // 4 bytes
+ public byte[] data = null; // crc not included
+ private int crcval = 0;
+
+ // public int offset=-1; // only for read chunks - informational
+ public ChunkRaw(int len, byte[] idbytes, boolean alloc) {
+ this.len = len;
+ System.arraycopy(idbytes, 0, this.idbytes, 0, 4);
+ if (alloc)
+ 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);
+ }
+
+ /**
+ * called after setting data, before writing to os
+ */
+ private void computeCrc() {
+ CRC32 crcengine = PngHelper.getCRC();
+ crcengine.reset();
+ crcengine.update(idbytes, 0, 4);
+ if (len > 0)
+ crcengine.update(data, 0, len); //
+ crcval = (int) crcengine.getValue();
+ }
+
+ public String toString() {
+ return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len;
+ }
+
+ /**
+ * 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);
+ return len + 4;
+ }
+
+ public ByteArrayInputStream getAsByteStream() { // only the data
+ return new ByteArrayInputStream(data);
+ }
+
+ private void allocData() {
+ if (data == null || data.length < len)
+ data = new byte[len];
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java new file mode 100644 index 000000000..2df9fd1f3 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java @@ -0,0 +1,152 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+import java.util.Map;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngjException;
+
+
+// see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
+public abstract class PngChunk {
+
+ public final String id; // 4 letters
+ 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
+
+ /**
+ * This static map defines which PngChunk class correspond to which ChunkID The client can add other chunks to this
+ * map statically, before reading
+ */
+ public 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);
+ factoryMap.put(ChunkHelper.PLTE, PngChunkPLTE.class);
+ factoryMap.put(ChunkHelper.IEND, PngChunkIEND.class);
+ factoryMap.put(ChunkHelper.tEXt, PngChunkTEXT.class);
+ factoryMap.put(ChunkHelper.iTXt, PngChunkITXT.class);
+ factoryMap.put(ChunkHelper.zTXt, PngChunkZTXT.class);
+ factoryMap.put(ChunkHelper.bKGD, PngChunkBKGD.class);
+ factoryMap.put(ChunkHelper.gAMA, PngChunkGAMA.class);
+ factoryMap.put(ChunkHelper.pHYs, PngChunkPHYS.class);
+ factoryMap.put(ChunkHelper.iCCP, PngChunkICCP.class);
+ factoryMap.put(ChunkHelper.tIME, PngChunkTIME.class);
+ factoryMap.put(ChunkHelper.tRNS, PngChunkTRNS.class);
+ factoryMap.put(ChunkHelper.cHRM, PngChunkCHRM.class);
+ factoryMap.put(ChunkHelper.sBIT, PngChunkSBIT.class);
+ factoryMap.put(ChunkHelper.sRGB, PngChunkSRGB.class);
+ factoryMap.put(ChunkHelper.hIST, PngChunkHIST.class);
+ factoryMap.put(ChunkHelper.sPLT, PngChunkSPLT.class);
+ }
+
+ protected PngChunk(String id, ImageInfo imgInfo) {
+ this.id = id;
+ this.imgInfo = imgInfo;
+ this.crit = ChunkHelper.isCritical(id);
+ this.pub = ChunkHelper.isPublic(id);
+ 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;
+ }
+
+ public static PngChunk factory(ChunkRaw chunk, ImageInfo info) {
+ PngChunk c = factoryFromId(ChunkHelper.toString(chunk.idbytes), info);
+ c.lenori = chunk.len;
+ c.parseFromChunk(chunk);
+ return c;
+ }
+
+ public static PngChunk factoryFromId(String cid, ImageInfo info) {
+ PngChunk chunk = null;
+ try {
+ Class<? extends PngChunk> cla = factoryMap.get(cid);
+ if (cla != null) {
+ Constructor<? extends PngChunk> constr = cla.getConstructor(ImageInfo.class);
+ chunk = constr.newInstance(info);
+ }
+ } catch (Exception e) {
+ // this can happend for unkown chunks
+ }
+ if (chunk == null)
+ chunk = new PngChunkUNKNOWN(cid, info);
+ return chunk;
+ }
+
+ protected 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();
+ }
+
+ void setPriority(boolean highPrioriy) {
+ writePriority = highPrioriy;
+ }
+
+ void write(OutputStream os) {
+ ChunkRaw c = createChunk();
+ if (c == null)
+ throw new PngjException("null chunk ! creation failed for " + this);
+ c.writeChunk(os);
+ }
+
+ public boolean isWritePriority() {
+ return writePriority;
+ }
+
+ /** must be overriden - only relevant for ancillary chunks */
+ public boolean allowsMultiple() {
+ return false; // override if allows multiple ocurrences
+ }
+
+ /** mustGoBeforeXX/After must be overriden - only relevant for ancillary chunks */
+ public boolean mustGoBeforeIDAT() {
+ return false;
+ }
+
+ public boolean mustGoBeforePLTE() {
+ return false;
+ }
+
+ public boolean mustGoAfterPLTE() {
+ return false;
+ }
+
+ static boolean isKnown(String id) {
+ return factoryMap.containsKey(id);
+ }
+
+ public int getChunkGroup() {
+ return chunkGroup;
+ }
+
+ public void setChunkGroup(int chunkGroup) {
+ this.chunkGroup = chunkGroup;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java new file mode 100644 index 000000000..51bbcb832 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java @@ -0,0 +1,122 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ */
+public class PngChunkBKGD extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11bKGD
+ // this chunk structure depends on the image type
+ // only one of these is meaningful
+ private int gray;
+ private int red, green, blue;
+ private int paletteIndex;
+
+ public PngChunkBKGD(ImageInfo info) {
+ super(ChunkHelper.bKGD, info);
+ }
+
+ @Override
+ public boolean mustGoBeforeIDAT() {
+ return true;
+ }
+
+ @Override
+ public boolean mustGoAfterPLTE() {
+ return true;
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = null;
+ if (imgInfo.greyscale) {
+ c = createEmptyChunk(2, true);
+ PngHelper.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);
+ }
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ if (imgInfo.greyscale) {
+ gray = PngHelper.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);
+ }
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkBKGD otherx = (PngChunkBKGD) other;
+ gray = otherx.gray;
+ red = otherx.red;
+ green = otherx.red;
+ blue = otherx.red;
+ paletteIndex = otherx.paletteIndex;
+ }
+
+ /**
+ * Set gray value (0-255 if bitdept=8)
+ *
+ * @param gray
+ */
+ public void setGray(int gray) {
+ if (!imgInfo.greyscale)
+ throw new PngjException("only gray images support this");
+ this.gray = gray;
+ }
+
+ public int getGray() {
+ if (!imgInfo.greyscale)
+ throw new PngjException("only gray images support this");
+ return gray;
+ }
+
+ /**
+ * Set pallette index
+ *
+ */
+ public void setPaletteIndex(int i) {
+ if (!imgInfo.indexed)
+ throw new PngjException("only indexed (pallete) images support this");
+ this.paletteIndex = i;
+ }
+
+ public int getPaletteIndex() {
+ if (!imgInfo.indexed)
+ throw new PngjException("only indexed (pallete) images support this");
+ return paletteIndex;
+ }
+
+ /**
+ * Set rgb values
+ *
+ */
+ public void setRGB(int r, int g, int b) {
+ if (imgInfo.greyscale || imgInfo.indexed)
+ throw new PngjException("only rgb or rgba images support this");
+ red = r;
+ green = g;
+ blue = b;
+ }
+
+ public int[] getRGB() {
+ if (imgInfo.greyscale || imgInfo.indexed)
+ 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 new file mode 100644 index 000000000..4380761c7 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkCHRM.java @@ -0,0 +1,88 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ */
+public class PngChunkCHRM extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11cHRM
+ private double whitex, whitey;
+ private double redx, redy;
+ private double greenx, greeny;
+ private double bluex, bluey;
+
+ public PngChunkCHRM(ImageInfo info) {
+ super(ChunkHelper.cHRM, info);
+ }
+
+ @Override
+ public boolean mustGoBeforeIDAT() {
+ return true;
+ }
+
+ @Override
+ public boolean mustGoBeforePLTE() {
+ return true;
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ 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);
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(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));
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkCHRM otherx = (PngChunkCHRM) other;
+ whitex = otherx.whitex;
+ whitey = otherx.whitex;
+ redx = otherx.redx;
+ redy = otherx.redy;
+ greenx = otherx.greenx;
+ greeny = otherx.greeny;
+ bluex = otherx.bluex;
+ bluey = otherx.bluey;
+ }
+
+ public void setChromaticities(double whitex, double whitey, double redx, double redy, double greenx, double greeny,
+ double bluex, double bluey) {
+ this.whitex = whitex;
+ this.redx = redx;
+ this.greenx = greenx;
+ this.bluex = bluex;
+ this.whitey = whitey;
+ this.redy = redy;
+ this.greeny = greeny;
+ this.bluey = bluey;
+ }
+
+ public double[] getChromaticities() {
+ return new double[] { whitex, whitey, redx, redy, greenx, greeny, bluex, bluey };
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java new file mode 100644 index 000000000..184ee9ffa --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java @@ -0,0 +1,56 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ */
+public class PngChunkGAMA extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11gAMA
+ private double gamma;
+
+ public PngChunkGAMA(ImageInfo info) {
+ super(ChunkHelper.gAMA, info);
+ }
+
+ @Override
+ public boolean mustGoBeforeIDAT() {
+ return true;
+ }
+
+ @Override
+ public boolean mustGoBeforePLTE() {
+ return true;
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = createEmptyChunk(4, true);
+ int g = (int) (gamma * 100000 + 0.5);
+ PngHelper.writeInt4tobytes(g, c.data, 0);
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw chunk) {
+ if (chunk.len != 4)
+ throw new PngjException("bad chunk " + chunk);
+ int g = PngHelper.readInt4fromBytes(chunk.data, 0);
+ gamma = ((double) g) / 100000.0;
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ gamma = ((PngChunkGAMA) other).gamma;
+ }
+
+ public double getGamma() {
+ return gamma;
+ }
+
+ public void setGamma(double gamma) {
+ this.gamma = gamma;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java new file mode 100644 index 000000000..b0f02ea37 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java @@ -0,0 +1,67 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ */
+public class PngChunkHIST extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11hIST
+ // only for palette images
+
+ private int[] hist = new int[0]; // should have same lenght as palette
+
+ public PngChunkHIST(ImageInfo info) {
+ super(ChunkHelper.hIST, info);
+ }
+
+ @Override
+ public boolean mustGoBeforeIDAT() {
+ return true;
+ }
+
+ @Override
+ public boolean mustGoAfterPLTE() {
+ return true;
+ }
+
+ @Override
+ public void parseFromChunk(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);
+ }
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ 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);
+ }
+ return c;
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkHIST otherx = (PngChunkHIST) other;
+ hist = new int[otherx.hist.length];
+ System.arraycopy(otherx.hist, 0, hist, 0, otherx.hist.length);
+ }
+
+ public int[] getHist() {
+ return hist;
+ }
+
+ public void setHist(int[] hist) {
+ this.hist = hist;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java new file mode 100644 index 000000000..db1c1ba64 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java @@ -0,0 +1,85 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+
+/*
+ */
+public class PngChunkICCP extends PngChunk {
+ // 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;
+ }
+
+ @Override
+ public boolean mustGoBeforePLTE() {
+ return true;
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ 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;
+ c.data[profileName.length() + 1] = 0;
+ System.arraycopy(compressedProfile, 0, c.data, profileName.length() + 2, compressedProfile.length);
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw chunk) {
+ int pos0 = ChunkHelper.posNullByte(chunk.data);
+ profileName = new String(chunk.data, 0, pos0, PngHelper.charsetLatin1);
+ int comp = (chunk.data[pos0 + 1] & 0xff);
+ if (comp != 0)
+ throw new RuntimeException("bad compression for ChunkTypeICCP");
+ int compdatasize = chunk.data.length - (pos0 + 2);
+ compressedProfile = new byte[compdatasize];
+ System.arraycopy(chunk.data, pos0 + 2, compressedProfile, 0, compdatasize);
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkICCP otherx = (PngChunkICCP) other;
+ profileName = otherx.profileName;
+ compressedProfile = new byte[otherx.compressedProfile.length];
+ System.arraycopy(otherx.compressedProfile, 0, compressedProfile, 0, otherx.compressedProfile.length); // deep
+ // copy
+ }
+
+ /**
+ * The profile should be uncompressed bytes
+ */
+ public void setProfileNameAndContent(String name, byte[] profile) {
+ profileName = name;
+ compressedProfile = ChunkHelper.compressBytes(profile, true);
+ }
+
+ public void setProfileNameAndContent(String name, String profile) {
+ setProfileNameAndContent(name, profile.getBytes(PngHelper.charsetLatin1));
+ }
+
+ public String getProfileName() {
+ return profileName;
+ }
+
+ /**
+ * uncompressed
+ **/
+ public byte[] getProfile() {
+ return ChunkHelper.compressBytes(compressedProfile, false);
+ }
+
+ public String getProfileAsString() {
+ return new String(getProfile(), PngHelper.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 new file mode 100644 index 000000000..a7cb95dbf --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIDAT.java @@ -0,0 +1,25 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+
+public class PngChunkIDAT extends PngChunk {
+ // 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);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {// does nothing
+ return null;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) { // does nothing
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java new file mode 100644 index 000000000..0d5b266da --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java @@ -0,0 +1,26 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+
+public class PngChunkIEND extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11IEND
+ // this is a dummy placeholder
+ public PngChunkIEND(ImageInfo info) {
+ super(ChunkHelper.IEND, info);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = new ChunkRaw(0, ChunkHelper.b_IEND, false);
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ // this is not used
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java new file mode 100644 index 000000000..fcb4150ff --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java @@ -0,0 +1,126 @@ +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.PngjException;
+
+
+/**
+ * this is a special chunk!
+ */
+public class PngChunkIHDR extends PngChunk {
+ private int cols;
+ private int rows;
+ private int bitspc;
+ private int colormodel;
+ private int compmeth;
+ private int filmeth;
+ private int interlaced;
+
+ // http://www.w3.org/TR/PNG/#11IHDR
+ //
+ public PngChunkIHDR(ImageInfo info) {
+ super(ChunkHelper.IHDR, info);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = new ChunkRaw(13, ChunkHelper.b_IHDR, true);
+ int offset = 0;
+ PngHelper.writeInt4tobytes(cols, c.data, offset);
+ offset += 4;
+ PngHelper.writeInt4tobytes(rows, c.data, offset);
+ offset += 4;
+ c.data[offset++] = (byte) bitspc;
+ c.data[offset++] = (byte) colormodel;
+ c.data[offset++] = (byte) compmeth;
+ c.data[offset++] = (byte) filmeth;
+ c.data[offset++] = (byte) interlaced;
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(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);
+ // 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);
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkIHDR otherx = (PngChunkIHDR) other;
+ cols = otherx.cols;
+ rows = otherx.rows;
+ bitspc = otherx.bitspc;
+ colormodel = otherx.colormodel;
+ compmeth = otherx.compmeth;
+ filmeth = otherx.filmeth;
+ interlaced = otherx.interlaced;
+ }
+
+ public int getCols() {
+ return cols;
+ }
+
+ public void setCols(int cols) {
+ this.cols = cols;
+ }
+
+ public int getRows() {
+ return rows;
+ }
+
+ public void setRows(int rows) {
+ this.rows = rows;
+ }
+
+ public int getBitspc() {
+ return bitspc;
+ }
+
+ public void setBitspc(int bitspc) {
+ this.bitspc = bitspc;
+ }
+
+ public int getColormodel() {
+ return colormodel;
+ }
+
+ public void setColormodel(int colormodel) {
+ this.colormodel = colormodel;
+ }
+
+ public int getCompmeth() {
+ return compmeth;
+ }
+
+ public void setCompmeth(int compmeth) {
+ this.compmeth = compmeth;
+ }
+
+ public int getFilmeth() {
+ return filmeth;
+ }
+
+ public void setFilmeth(int filmeth) {
+ this.filmeth = filmeth;
+ }
+
+ public int getInterlaced() {
+ return interlaced;
+ }
+
+ public void setInterlaced(int interlaced) {
+ this.interlaced = interlaced;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java new file mode 100644 index 000000000..4e5c7c74a --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java @@ -0,0 +1,119 @@ +package jogamp.opengl.util.pngj.chunks;
+
+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.PngjException;
+
+
+/**
+ * UNTESTED!
+ */
+public class PngChunkITXT extends PngChunkTextVar {
+
+ private boolean compressed = false;
+ private String langTag = "";
+ private String translatedTag = "";
+
+ // http://www.w3.org/TR/PNG/#11iTXt
+ public PngChunkITXT(ImageInfo info) {
+ super(ChunkHelper.iTXt, info);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ if (val.isEmpty() || key.isEmpty())
+ return null;
+ try {
+ ByteArrayOutputStream ba = new ByteArrayOutputStream();
+ ba.write(key.getBytes(PngHelper.charsetLatin1));
+ ba.write(0); // separator
+ ba.write(compressed ? 1 : 0);
+ ba.write(0); // compression method (always 0)
+ ba.write(langTag.getBytes(PngHelper.charsetUTF8));
+ ba.write(0); // separator
+ ba.write(translatedTag.getBytes(PngHelper.charsetUTF8));
+ ba.write(0); // separator
+ byte[] textbytes = val.getBytes(PngHelper.charsetUTF8);
+ if (compressed) {
+ textbytes = ChunkHelper.compressBytes(textbytes, true);
+ }
+ ba.write(textbytes);
+ byte[] b = ba.toByteArray();
+ ChunkRaw chunk = createEmptyChunk(b.length, false);
+ chunk.data = b;
+ return chunk;
+ } catch (IOException e) {
+ throw new PngjException(e);
+ }
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ int nullsFound = 0;
+ int[] nullsIdx = new int[3];
+ for (int i = 0; i < c.data.length; i++) {
+ if (c.data[i] != 0)
+ continue;
+ nullsIdx[nullsFound] = i;
+ nullsFound++;
+ if (nullsFound == 1)
+ i += 2;
+ if (nullsFound == 3)
+ break;
+ }
+ if (nullsFound != 3)
+ throw new PngjException("Bad formed PngChunkITXT chunk");
+ key = new String(c.data, 0, nullsIdx[0], PngHelper.charsetLatin1);
+ 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);
+ i = nullsIdx[2] + 1;
+ if (compressed) {
+ byte[] bytes = ChunkHelper.compressBytes(c.data, i, c.data.length - i, false);
+ val = new String(bytes, PngHelper.charsetUTF8);
+ } else {
+ val = new String(c.data, i, c.data.length - i, PngHelper.charsetUTF8);
+ }
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkITXT otherx = (PngChunkITXT) other;
+ key = otherx.key;
+ val = otherx.val;
+ compressed = otherx.compressed;
+ langTag = otherx.langTag;
+ translatedTag = otherx.translatedTag;
+ }
+
+ public boolean isCompressed() {
+ return compressed;
+ }
+
+ public void setCompressed(boolean compressed) {
+ this.compressed = compressed;
+ }
+
+ public String getLangtag() {
+ return langTag;
+ }
+
+ public void setLangtag(String langtag) {
+ this.langTag = langtag;
+ }
+
+ public String getTranslatedTag() {
+ return translatedTag;
+ }
+
+ public void setTranslatedTag(String translatedTag) {
+ this.translatedTag = translatedTag;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java new file mode 100644 index 000000000..47e2c492c --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java @@ -0,0 +1,108 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+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);
+ }
+
+ @Override
+ public boolean mustGoBeforeIDAT() {
+ return true;
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = createEmptyChunk(9, true);
+ PngHelper.writeInt4tobytes((int) pixelsxUnitX, c.data, 0);
+ PngHelper.writeInt4tobytes((int) pixelsxUnitY, c.data, 4);
+ c.data[8] = (byte) units;
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw chunk) {
+ if (chunk.len != 9)
+ throw new PngjException("bad chunk length " + chunk);
+ pixelsxUnitX = PngHelper.readInt4fromBytes(chunk.data, 0);
+ if (pixelsxUnitX < 0)
+ pixelsxUnitX += 0x100000000L;
+ pixelsxUnitY = PngHelper.readInt4fromBytes(chunk.data, 4);
+ if (pixelsxUnitY < 0)
+ pixelsxUnitY += 0x100000000L;
+ units = PngHelper.readInt1fromByte(chunk.data, 8);
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkPHYS otherx = (PngChunkPHYS) other;
+ this.pixelsxUnitX = otherx.pixelsxUnitX;
+ this.pixelsxUnitY = otherx.pixelsxUnitY;
+ this.units = otherx.units;
+ }
+
+ public long getPixelsxUnitX() {
+ return pixelsxUnitX;
+ }
+
+ public void setPixelsxUnitX(long pixelsxUnitX) {
+ this.pixelsxUnitX = pixelsxUnitX;
+ }
+
+ public long getPixelsxUnitY() {
+ return pixelsxUnitY;
+ }
+
+ public void setPixelsxUnitY(long pixelsxUnitY) {
+ this.pixelsxUnitY = pixelsxUnitY;
+ }
+
+ public int getUnits() {
+ return units;
+ }
+
+ public void setUnits(int units) {
+ this.units = units;
+ }
+
+ // special getters / setters
+
+ /**
+ * returns -1 if the physicial unit is unknown, or X-Y are not equal
+ */
+ public double getAsDpi() {
+ if (units != 1 || pixelsxUnitX != pixelsxUnitY)
+ return -1;
+ return ((double) pixelsxUnitX) * 0.0254;
+ }
+
+ /**
+ * returns -1 if the physicial unit is unknown
+ */
+ public double[] getAsDpi2() {
+ if (units != 1)
+ return new double[] { -1, -1 };
+ return new double[] { ((double) pixelsxUnitX) * 0.0254, ((double) pixelsxUnitY) * 0.0254 };
+ }
+
+ public void setAsDpi(double dpi) {
+ units = 1;
+ pixelsxUnitX = (long) (dpi / 0.0254 + 0.5);
+ pixelsxUnitY = pixelsxUnitX;
+ }
+
+ public void setAsDpi2(double dpix, double dpiy) {
+ units = 1;
+ pixelsxUnitX = (long) (dpix / 0.0254 + 0.5);
+ pixelsxUnitY = (long) (dpiy / 0.0254 + 0.5);
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java new file mode 100644 index 000000000..123080bb3 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java @@ -0,0 +1,93 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ * Palette chunk *this is critical*
+ */
+public class PngChunkPLTE extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11PLTE
+ private int nentries = 0;
+ /**
+ * RGB8 packed in one integer
+ */
+ private int[] entries;
+
+ public PngChunkPLTE(ImageInfo info) {
+ super(ChunkHelper.PLTE, info);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ int len = 3 * nentries;
+ int[] rgb = new int[3];
+ ChunkRaw c = createEmptyChunk(len, true);
+ for (int n = 0, i = 0; n < nentries; n++) {
+ getEntryRgb(n, rgb);
+ c.data[i++] = (byte) rgb[0];
+ c.data[i++] = (byte) rgb[1];
+ c.data[i++] = (byte) rgb[2];
+ }
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(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));
+ }
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkPLTE otherx = (PngChunkPLTE) other;
+ this.setNentries(otherx.getNentries());
+ System.arraycopy(otherx.entries, 0, entries, 0, nentries);
+ }
+
+ public void setNentries(int n) {
+ nentries = n;
+ if (nentries < 1 || nentries > 256)
+ throw new PngjException("invalid pallette - nentries=" + nentries);
+ if (entries == null || entries.length != nentries) { // alloc
+ entries = new int[nentries];
+ }
+ }
+
+ public int getNentries() {
+ return nentries;
+ }
+
+ public void setEntry(int n, int r, int g, int b) {
+ entries[n] = ((r << 16) | (g << 8) | b);
+ }
+
+ public int getEntry(int n) {
+ return entries[n];
+ }
+
+ public void getEntryRgb(int n, int[] rgb) {
+ getEntryRgb(n, rgb, 0);
+ }
+
+ public void getEntryRgb(int n, int[] rgb, int offset) {
+ int v = entries[n];
+ rgb[offset + 0] = ((v & 0xff0000) >> 16);
+ rgb[offset + 1] = ((v & 0xff00) >> 8);
+ rgb[offset + 2] = (v & 0xff);
+ }
+
+ public int minBitDepth() {
+ if (nentries <= 2)
+ return 1;
+ else if (nentries <= 4)
+ return 2;
+ else if (nentries <= 16)
+ return 4;
+ else
+ return 8;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java new file mode 100644 index 000000000..6850d260d --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java @@ -0,0 +1,124 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ */
+public class PngChunkSBIT extends PngChunk {
+ // 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);
+ }
+
+ @Override
+ public boolean mustGoBeforeIDAT() {
+ return true;
+ }
+
+ @Override
+ public boolean mustGoBeforePLTE() {
+ return true;
+ }
+
+ private int getLen() {
+ int len = imgInfo.greyscale ? 1 : 3;
+ if (imgInfo.alpha)
+ len += 1;
+ return len;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ if (c.len != getLen())
+ throw new PngjException("bad chunk length " + c);
+ if (imgInfo.greyscale) {
+ graysb = PngHelper.readInt1fromByte(c.data, 0);
+ if (imgInfo.alpha)
+ alphasb = PngHelper.readInt1fromByte(c.data, 1);
+ } else {
+ redsb = PngHelper.readInt1fromByte(c.data, 0);
+ greensb = PngHelper.readInt1fromByte(c.data, 1);
+ bluesb = PngHelper.readInt1fromByte(c.data, 2);
+ if (imgInfo.alpha)
+ alphasb = PngHelper.readInt1fromByte(c.data, 3);
+ }
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = null;
+ c = createEmptyChunk(getLen(), true);
+ if (imgInfo.greyscale) {
+ c.data[0] = (byte) graysb;
+ if (imgInfo.alpha)
+ c.data[1] = (byte) alphasb;
+ } else {
+ c.data[0] = (byte) redsb;
+ c.data[1] = (byte) greensb;
+ c.data[2] = (byte) bluesb;
+ if (imgInfo.alpha)
+ c.data[3] = (byte) alphasb;
+ }
+ return c;
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkSBIT otherx = (PngChunkSBIT) other;
+ graysb = otherx.graysb;
+ redsb = otherx.redsb;
+ greensb = otherx.greensb;
+ bluesb = otherx.bluesb;
+ alphasb = otherx.alphasb;
+ }
+
+ public void setGraysb(int gray) {
+ if (!imgInfo.greyscale)
+ throw new PngjException("only greyscale images support this");
+ graysb = gray;
+ }
+
+ public int getGraysb() {
+ if (!imgInfo.greyscale)
+ throw new PngjException("only greyscale images support this");
+ return graysb;
+ }
+
+ public void setAlphasb(int a) {
+ if (!imgInfo.alpha)
+ throw new PngjException("only images with alpha support this");
+ alphasb = a;
+ }
+
+ public int getAlphasb() {
+ if (!imgInfo.alpha)
+ throw new PngjException("only images with alpha support this");
+ return alphasb;
+ }
+
+ /**
+ * Set rgb values
+ *
+ */
+ public void setRGB(int r, int g, int b) {
+ if (imgInfo.greyscale || imgInfo.indexed)
+ throw new PngjException("only rgb or rgba images support this");
+ redsb = r;
+ greensb = g;
+ bluesb = b;
+ }
+
+ public int[] getRGB() {
+ if (imgInfo.greyscale || imgInfo.indexed)
+ throw new PngjException("only rgb or rgba images support this");
+ return new int[] { redsb, greensb, bluesb };
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java new file mode 100644 index 000000000..953adb7d9 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java @@ -0,0 +1,139 @@ +package jogamp.opengl.util.pngj.chunks; + +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.PngjException; + + +public class PngChunkSPLT extends PngChunk { + // http://www.w3.org/TR/PNG/#11sPLT + + private String palName; + private int sampledepth; // 8/16 + private int[] palette; // 5 elements per entry + + public PngChunkSPLT(ImageInfo info) { + super(ChunkHelper.sPLT, info); + } + + @Override + public boolean allowsMultiple() { + return true; // allows multiple, but pallete name should be different + } + + @Override + public boolean mustGoBeforeIDAT() { + return true; + } + + @Override + public ChunkRaw createChunk() { + try { + ByteArrayOutputStream ba = new ByteArrayOutputStream(); + ba.write(palName.getBytes(PngHelper.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]); + else + PngHelper.writeInt2(ba, palette[n * 5 + i]); + } + PngHelper.writeInt2(ba, palette[n * 5 + 4]); + } + byte[] b = ba.toByteArray(); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } catch (IOException e) { + throw new PngjException(e); + } + } + + @Override + public void parseFromChunk(ChunkRaw c) { + int t = -1; + for (int i = 0; i < c.data.length; i++) { // look for first zero + if (c.data[i] == 0) { + t = i; + break; + } + } + 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); + t += 2; + int nentries = (c.data.length - t) / (sampledepth == 8 ? 6 : 10); + palette = new int[nentries * 5]; + int r, g, b, a, f, ne; + 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++); + } else { + r = PngHelper.readInt2fromBytes(c.data, t); + t += 2; + g = PngHelper.readInt2fromBytes(c.data, t); + t += 2; + b = PngHelper.readInt2fromBytes(c.data, t); + t += 2; + a = PngHelper.readInt2fromBytes(c.data, t); + t += 2; + } + f = PngHelper.readInt2fromBytes(c.data, t); + t += 2; + palette[ne++] = r; + palette[ne++] = g; + palette[ne++] = b; + palette[ne++] = a; + palette[ne++] = f; + } + } + + @Override + public void cloneDataFromRead(PngChunk other) { + PngChunkSPLT otherx = (PngChunkSPLT) other; + palName = otherx.palName; + sampledepth = otherx.sampledepth; + palette = new int[otherx.palette.length]; + System.arraycopy(otherx.palette, 0, palette, 0, palette.length); + } + + public int getNentries() { + return palette.length / 5; + } + + public String getPalName() { + return palName; + } + + public void setPalName(String palName) { + this.palName = palName; + } + + public int getSampledepth() { + return sampledepth; + } + + public void setSampledepth(int sampledepth) { + this.sampledepth = sampledepth; + } + + public int[] getPalette() { + return palette; + } + + public void setPalette(int[] palette) { + this.palette = palette; + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java new file mode 100644 index 000000000..774558785 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java @@ -0,0 +1,61 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ */
+public class PngChunkSRGB extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11sRGB
+
+ public static final int RENDER_INTENT_Perceptual = 0;
+ public static final int RENDER_INTENT_Relative_colorimetric = 1;
+ public static final int RENDER_INTENT_Saturation = 2;
+ public static final int RENDER_INTENT_Absolute_colorimetric = 3;
+
+ private int intent;
+
+ public PngChunkSRGB(ImageInfo info) {
+ super(ChunkHelper.sRGB, info);
+ }
+
+ @Override
+ public boolean mustGoBeforeIDAT() {
+ return true;
+ }
+
+ @Override
+ public boolean mustGoBeforePLTE() {
+ return true;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ if (c.len != 1)
+ throw new PngjException("bad chunk length " + c);
+ intent = PngHelper.readInt1fromByte(c.data, 0);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = null;
+ c = createEmptyChunk(1, true);
+ c.data[0] = (byte) intent;
+ return c;
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkSRGB otherx = (PngChunkSRGB) other;
+ intent = otherx.intent;
+ }
+
+ public int getIntent() {
+ return intent;
+ }
+
+ public void setIntent(int intent) {
+ this.intent = intent;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java new file mode 100644 index 000000000..c535fe34a --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java @@ -0,0 +1,34 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+
+public class PngChunkTEXT extends PngChunkTextVar {
+ public PngChunkTEXT(ImageInfo info) {
+ super(ChunkHelper.tEXt, info);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ if (val.isEmpty() || key.isEmpty())
+ return null;
+ byte[] b = (key + "\0" + val).getBytes(PngHelper.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];
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkTEXT otherx = (PngChunkTEXT) other;
+ key = otherx.key;
+ val = otherx.val;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java new file mode 100644 index 000000000..37e617acb --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java @@ -0,0 +1,83 @@ +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.PngjException;
+
+
+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);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = createEmptyChunk(7, true);
+ PngHelper.writeInt2tobytes(year, c.data, 0);
+ c.data[2] = (byte) mon;
+ c.data[3] = (byte) day;
+ c.data[4] = (byte) hour;
+ c.data[5] = (byte) min;
+ c.data[6] = (byte) sec;
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(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);
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkTIME x = (PngChunkTIME) other;
+ year = x.year;
+ mon = x.mon;
+ day = x.day;
+ hour = x.hour;
+ min = x.min;
+ sec = x.sec;
+ }
+
+ public void setNow(int secsAgo) {
+ Calendar d = Calendar.getInstance();
+ d.setTimeInMillis(System.currentTimeMillis() - 1000 * (long) secsAgo);
+ year = d.get(Calendar.YEAR);
+ mon = d.get(Calendar.MONTH) + 1;
+ day = d.get(Calendar.DAY_OF_MONTH);
+ hour = d.get(Calendar.HOUR_OF_DAY);
+ min = d.get(Calendar.MINUTE);
+ sec = d.get(Calendar.SECOND);
+ }
+
+ public void setYMDHMS(int yearx, int monx, int dayx, int hourx, int minx, int secx) {
+ year = yearx;
+ mon = monx;
+ day = dayx;
+ hour = hourx;
+ min = minx;
+ sec = secx;
+ }
+ public int[] getYMDHMS() {
+ return new int[] { year, mon, day, hour, min, sec };
+ }
+
+ /** format YYYY/MM/DD HH:mm:SS */
+ public String getAsString() {
+ return String.format("%04/%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 new file mode 100644 index 000000000..9365e5e8e --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTRNS.java @@ -0,0 +1,129 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+import jogamp.opengl.util.pngj.PngHelper;
+import jogamp.opengl.util.pngj.PngjException;
+
+/*
+ */
+public class PngChunkTRNS extends PngChunk {
+ // http://www.w3.org/TR/PNG/#11tRNS
+ // this chunk structure depends on the image type
+ // only one of these is meaningful
+ 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;
+ }
+
+ @Override
+ public boolean mustGoAfterPLTE() {
+ return true;
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ ChunkRaw c = null;
+ if (imgInfo.greyscale) {
+ c = createEmptyChunk(2, true);
+ PngHelper.writeInt2tobytes(gray, c.data, 0);
+ } else if (imgInfo.indexed) {
+ c = createEmptyChunk(paletteAlpha.length, true);
+ for (int n = 0; n < c.len; n++) {
+ c.data[n] = (byte) paletteAlpha[n];
+ }
+ } else {
+ c = createEmptyChunk(6, true);
+ PngHelper.writeInt2tobytes(red, c.data, 0);
+ PngHelper.writeInt2tobytes(green, c.data, 0);
+ PngHelper.writeInt2tobytes(blue, c.data, 0);
+ }
+ return c;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ if (imgInfo.greyscale) {
+ gray = PngHelper.readInt2fromBytes(c.data, 0);
+ } else if (imgInfo.indexed) {
+ int nentries = c.data.length;
+ paletteAlpha = new int[nentries];
+ for (int n = 0; n < nentries; n++) {
+ 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);
+ }
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkTRNS otherx = (PngChunkTRNS) other;
+ gray = otherx.gray;
+ red = otherx.red;
+ green = otherx.red;
+ blue = otherx.red;
+ if (otherx.paletteAlpha != null) {
+ paletteAlpha = new int[otherx.paletteAlpha.length];
+ System.arraycopy(otherx.paletteAlpha, 0, paletteAlpha, 0, paletteAlpha.length);
+ }
+ }
+
+ /**
+ * Set rgb values
+ *
+ */
+ public void setRGB(int r, int g, int b) {
+ if (imgInfo.greyscale || imgInfo.indexed)
+ throw new PngjException("only rgb or rgba images support this");
+ red = r;
+ green = g;
+ blue = b;
+ }
+
+ public int[] getRGB() {
+ if (imgInfo.greyscale || imgInfo.indexed)
+ throw new PngjException("only rgb or rgba images support this");
+ return new int[] { red, green, blue };
+ }
+
+ public void setGray(int g) {
+ if (!imgInfo.greyscale)
+ throw new PngjException("only grayscale images support this");
+ gray = g;
+ }
+
+ public int getGray() {
+ if (!imgInfo.greyscale)
+ throw new PngjException("only grayscale images support this");
+ return gray;
+ }
+
+ /**
+ * WARNING: non deep copy
+ */
+ public void setPalletteAlpha(int[] palAlpha) {
+ if (!imgInfo.indexed)
+ throw new PngjException("only indexed images support this");
+ paletteAlpha = palAlpha;
+ }
+
+ /**
+ * WARNING: non deep copy
+ */
+ public int[] getPalletteAlpha() {
+ if (!imgInfo.indexed)
+ throw new PngjException("only indexed images support this");
+ return paletteAlpha;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java new file mode 100644 index 000000000..3d92a806f --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java @@ -0,0 +1,61 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+
+/**
+ * superclass for three textual chunks (TEXT, ITXT, ZTXT)
+ *
+ * @author Hernan J Gonzalez
+ */
+public abstract class PngChunkTextVar extends PngChunk {
+ protected String key; // key/val: only for tEXt. lazy computed
+ protected String val;
+
+ // http://www.w3.org/TR/PNG/#11keywords
+ public final static String KEY_Title = "Title"; // Short (one line) title or caption for image
+ public final static String KEY_Author = "Author"; // Name of image's creator
+ public final static String KEY_Description = "Description"; // Description of image (possibly long)
+ public final static String KEY_Copyright = "Copyright"; // Copyright notice
+ public final static String KEY_Creation_Time = "Creation Time"; // Time of original image creation
+ public final static String KEY_Software = "Software"; // Software used to create the image
+ public final static String KEY_Disclaimer = "Disclaimer"; // Legal disclaimer
+ public final static String KEY_Warning = "Warning"; // Warning of nature of content
+ public final static String KEY_Source = "Source"; // Device used to create the image
+ public final static String KEY_Comment = "Comment"; // Miscellaneous comment
+
+ protected PngChunkTextVar(String id, ImageInfo info) {
+ super(id, info);
+ }
+
+ @Override
+ public boolean allowsMultiple() {
+ return true;
+ }
+
+ public static class PngTxtInfo {
+ public String title;
+ public String author;
+ public String description;
+ public String creation_time;// = (new Date()).toString();
+ public String software;
+ public String disclaimer;
+ public String warning;
+ public String source;
+ public String comment;
+
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getVal() {
+ return val;
+ }
+
+ public void setKeyVal(String key, String val) {
+ this.key = key;
+ this.val = val;
+ }
+
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java new file mode 100644 index 000000000..15a35935a --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java @@ -0,0 +1,51 @@ +package jogamp.opengl.util.pngj.chunks;
+
+import jogamp.opengl.util.pngj.ImageInfo;
+
+public class PngChunkUNKNOWN extends PngChunk { // unkown, custom or not
+
+ private byte[] data;
+
+ public PngChunkUNKNOWN(String id, ImageInfo info) {
+ 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() {
+ ChunkRaw p = createEmptyChunk(data.length, false);
+ p.data = this.data;
+ return p;
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ data = c.data;
+ }
+
+ /* does not copy! */
+ public byte[] getData() {
+ return data;
+ }
+
+ /* does not copy! */
+ public void setData(byte[] data) {
+ this.data = data;
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ // THIS SHOULD NOT BE CALLED IF ALREADY CLONED WITH COPY CONSTRUCTOR
+ PngChunkUNKNOWN c = (PngChunkUNKNOWN) other;
+ data = c.data; // not deep copy
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java new file mode 100644 index 000000000..fd6c08273 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java @@ -0,0 +1,62 @@ +package jogamp.opengl.util.pngj.chunks;
+
+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.PngjException;
+
+
+public class PngChunkZTXT extends PngChunkTextVar {
+ // http://www.w3.org/TR/PNG/#11zTXt
+ public PngChunkZTXT(ImageInfo info) {
+ super(ChunkHelper.zTXt, info);
+ }
+
+ @Override
+ public ChunkRaw createChunk() {
+ if (val.isEmpty() || key.isEmpty())
+ return null;
+ try {
+ ByteArrayOutputStream ba = new ByteArrayOutputStream();
+ ba.write(key.getBytes(PngHelper.charsetLatin1));
+ ba.write(0); // separator
+ ba.write(0); // compression method: 0
+ byte[] textbytes = ChunkHelper.compressBytes(val.getBytes(PngHelper.charsetLatin1), true);
+ ba.write(textbytes);
+ byte[] b = ba.toByteArray();
+ ChunkRaw chunk = createEmptyChunk(b.length, false);
+ chunk.data = b;
+ return chunk;
+ } catch (IOException e) {
+ throw new PngjException(e);
+ }
+ }
+
+ @Override
+ public void parseFromChunk(ChunkRaw c) {
+ int nullsep = -1;
+ for (int i = 0; i < c.data.length; i++) { // look for first zero
+ if (c.data[i] != 0)
+ continue;
+ nullsep = i;
+ break;
+ }
+ 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);
+ 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);
+ }
+
+ @Override
+ public void cloneDataFromRead(PngChunk other) {
+ PngChunkZTXT otherx = (PngChunkZTXT) other;
+ key = otherx.key;
+ val = otherx.val;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java new file mode 100644 index 000000000..a82754588 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java @@ -0,0 +1,135 @@ +package jogamp.opengl.util.pngj.chunks; + +import jogamp.opengl.util.pngj.PngHelper; +import jogamp.opengl.util.pngj.PngjException; + +/** + * We consider "image metadata" every info inside the image except for the most basic image info (IHDR chunk - ImageInfo + * class) and the pixels values. + * + * This includes the palette (if present) and all the ancillary chunks + * + * This class provides a wrapper over the collection of chunks of a image (read or to write) and provides some high + * level methods to access them + * + */ +public class PngMetadata { + private final ChunkList chunkList; + private final boolean readonly; + + public PngMetadata(ChunkList chunks, boolean readonly) { + this.chunkList = chunks; + this.readonly = readonly; + } + + /** + * Queues the chunk at the writer + */ + public boolean setChunk(PngChunk c, boolean overwriteIfPresent) { + if (readonly) + throw new PngjException("cannot set chunk : readonly metadata"); + return chunkList.setChunk(c, overwriteIfPresent); + } + + + /** + * 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); + } + + /** + * Same as getChunk1(id, innerid=null, failIfMultiple=true); + */ + public PngChunk getChunk1(String id) { + return chunkList.getChunk1(id); + } + + // ///// high level utility methods follow //////////// + + // //////////// DPI + + /** + * returns -1 if not found or dimension unknown + **/ + public double[] getDpi() { + PngChunk c = getChunk1(ChunkHelper.pHYs, null, true); + if (c == null) + return new double[] { -1, -1 }; + else + return ((PngChunkPHYS) c).getAsDpi2(); + } + + public void setDpi(double x) { + setDpi(x, x); + } + + public void setDpi(double x, double y) { + PngChunkPHYS c = new PngChunkPHYS(chunkList.imageInfo); + c.setAsDpi2(x, y); + setChunk(c, true); + } + + // //////////// TIME + + public void setTimeNow(int secsAgo) { + PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo); + c.setNow(secsAgo); + setChunk(c, true); + } + + public void 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); + } + + public String getTimeAsString() { + PngChunk c = getChunk1(ChunkHelper.tIME, null, true); + return c != null ? ((PngChunkTIME) c).getAsString() : ""; + } + + // //////////// TEXT + + public void setText(String k, String val, boolean useLatin1, boolean compress) { + if (compress && !useLatin1) + throw new PngjException("cannot compress non latin text"); + PngChunkTextVar c; + if (useLatin1) { + if (compress) { + c = new PngChunkZTXT(chunkList.imageInfo); + } else { + c = new PngChunkTEXT(chunkList.imageInfo); + } + } else { + c = new PngChunkITXT(chunkList.imageInfo); + ((PngChunkITXT) c).setLangtag(k); // we use the same orig tag (this is not quite right) + } + c.setKeyVal(k, val); + setChunk(c, true); + } + + public void setText(String k, String val) { + setText(k, val, false, val.length() > 400); + } + + /** tries all text chunks - returns null if not found */ + 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; + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/package.html b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/package.html new file mode 100644 index 000000000..137406695 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/package.html @@ -0,0 +1,9 @@ +<html>
+<body bgcolor="white">
+<p>
+Contains the code related to chunk management for the PNGJ library.</p>
+<p>
+Only needed by client code if some special chunk handling is required.
+</p>
+</body>
+</html>
diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/package.html b/src/jogl/classes/jogamp/opengl/util/pngj/package.html new file mode 100644 index 000000000..209b39c59 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/pngj/package.html @@ -0,0 +1,11 @@ +<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.
+</p>
+<p>
+See also the <code>nosandbox</code> package if available.
+</p>
+</body>
+</html>
|