From 40830196070013432bc5f453eb31cfe4c64e0510 Mon Sep 17 00:00:00 2001 From: Sven Gothel Date: Sat, 7 Apr 2012 15:28:37 +0200 Subject: 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. --- .../jogamp/opengl/util/pngj/FilterType.java | 94 +++++ .../opengl/util/pngj/FilterWriteStrategy.java | 97 +++++ .../classes/jogamp/opengl/util/pngj/ImageInfo.java | 208 ++++++++++ .../classes/jogamp/opengl/util/pngj/ImageLine.java | 175 ++++++++ .../classes/jogamp/opengl/util/pngj/PngHelper.java | 213 ++++++++++ .../opengl/util/pngj/PngIDatChunkInputStream.java | 153 +++++++ .../opengl/util/pngj/PngIDatChunkOutputStream.java | 31 ++ .../classes/jogamp/opengl/util/pngj/PngReader.java | 415 ++++++++++++++++++ .../classes/jogamp/opengl/util/pngj/PngWriter.java | 462 +++++++++++++++++++++ .../opengl/util/pngj/PngjBadCrcException.java | 20 + .../jogamp/opengl/util/pngj/PngjException.java | 23 + .../opengl/util/pngj/PngjInputException.java | 20 + .../opengl/util/pngj/PngjOutputException.java | 20 + .../opengl/util/pngj/PngjUnsupportedException.java | 24 ++ .../opengl/util/pngj/ProgressiveOutputStream.java | 71 ++++ .../util/pngj/chunks/ChunkCopyBehaviour.java | 24 ++ .../opengl/util/pngj/chunks/ChunkHelper.java | 134 ++++++ .../jogamp/opengl/util/pngj/chunks/ChunkList.java | 282 +++++++++++++ .../util/pngj/chunks/ChunkLoadBehaviour.java | 10 + .../jogamp/opengl/util/pngj/chunks/ChunkRaw.java | 83 ++++ .../jogamp/opengl/util/pngj/chunks/PngChunk.java | 152 +++++++ .../opengl/util/pngj/chunks/PngChunkBKGD.java | 122 ++++++ .../opengl/util/pngj/chunks/PngChunkCHRM.java | 88 ++++ .../opengl/util/pngj/chunks/PngChunkGAMA.java | 56 +++ .../opengl/util/pngj/chunks/PngChunkHIST.java | 67 +++ .../opengl/util/pngj/chunks/PngChunkICCP.java | 85 ++++ .../opengl/util/pngj/chunks/PngChunkIDAT.java | 25 ++ .../opengl/util/pngj/chunks/PngChunkIEND.java | 26 ++ .../opengl/util/pngj/chunks/PngChunkIHDR.java | 126 ++++++ .../opengl/util/pngj/chunks/PngChunkITXT.java | 119 ++++++ .../opengl/util/pngj/chunks/PngChunkPHYS.java | 108 +++++ .../opengl/util/pngj/chunks/PngChunkPLTE.java | 93 +++++ .../opengl/util/pngj/chunks/PngChunkSBIT.java | 124 ++++++ .../opengl/util/pngj/chunks/PngChunkSPLT.java | 139 +++++++ .../opengl/util/pngj/chunks/PngChunkSRGB.java | 61 +++ .../opengl/util/pngj/chunks/PngChunkTEXT.java | 34 ++ .../opengl/util/pngj/chunks/PngChunkTIME.java | 83 ++++ .../opengl/util/pngj/chunks/PngChunkTRNS.java | 129 ++++++ .../opengl/util/pngj/chunks/PngChunkTextVar.java | 61 +++ .../opengl/util/pngj/chunks/PngChunkUNKNOWN.java | 51 +++ .../opengl/util/pngj/chunks/PngChunkZTXT.java | 62 +++ .../opengl/util/pngj/chunks/PngMetadata.java | 135 ++++++ .../jogamp/opengl/util/pngj/chunks/package.html | 9 + .../classes/jogamp/opengl/util/pngj/package.html | 11 + 44 files changed, 4525 insertions(+) create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/FilterWriteStrategy.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/ImageInfo.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/ImageLine.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngHelper.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkInputStream.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngIDatChunkOutputStream.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngReader.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngWriter.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngjBadCrcException.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngjException.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngjInputException.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngjOutputException.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/PngjUnsupportedException.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/ProgressiveOutputStream.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkCopyBehaviour.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkHelper.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkList.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkRaw.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunk.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkBKGD.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkCHRM.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkGAMA.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkHIST.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkICCP.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIDAT.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIEND.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkIHDR.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkITXT.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPHYS.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkPLTE.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSBIT.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSPLT.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkSRGB.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTEXT.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTIME.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTRNS.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkTextVar.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkUNKNOWN.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngChunkZTXT.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/PngMetadata.java create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/chunks/package.html create mode 100644 src/jogl/classes/jogamp/opengl/util/pngj/package.html (limited to 'src/jogl/classes/jogamp/opengl') diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java b/src/jogl/classes/jogamp/opengl/util/pngj/FilterType.java 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. + *

+ * Some parameters are redundant, but the constructor receives an 'orthogonal' subset. + *

+ * 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. + *

+ * This object can be (usually it is) reused while iterating over the image lines. + *

+ * See scanline 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). + *

+ * 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) + *

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

+ * To convert a indexed line to RGB balues, see ImageLineHelper.tf_palIdx2RGB() (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. + *

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

+ * If scale==TRUE scales the value (just a bit shift). + */ + public int[] tf_unpack(int[] buf, boolean scale) { + int len = scanline.length; + if (bitDepth == 1) + len *= 8; + else if (bitDepth == 2) + len *= 4; + else if (bitDepth == 4) + len *= 2; + if (buf == null) + buf = new int[len]; + 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. + *

+ * 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. + *

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

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

+ * See also FileHelper.createPngReader(File f) 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 readRow(int[] buffer, int nrow) 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. + *

+ * See also FileHelper.createPngWriter() 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 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. + *

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

+ * See also setCompLevel() + * + * @param filterType + * One of the five prediction types or strategy to choose it (see PngFilterType) Recommended + * values: DEFAULT (default) or AGGRESIVE + */ + public void setFilterType(FilterType filterType) { + filterStrat = new FilterWriteStrategy(imgInfo, filterType); + } + + /** + * Sets compression level of ZIP algorithm. + *

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

+ * See also setFilterType() + * + * @param compLevel + * between 0 and 9 (default:6 , recommended: 6 or more) + */ + public void setCompLevel(int compLevel) { + if (compLevel < 0 || compLevel > 9) + throw new 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. + *

+ * Should be called when creating an image from another, before starting writing lines, to copy relevant chunks. + *

+ * + * @param reader + * : PngReader object, already opened. + * @param copy_mask + * : Mask bit (OR), see ChunksToWrite.COPY_XXX constants + */ + public void copyChunksFirst(PngReader reader, int copy_mask) { + copyChunks(reader, copy_mask, false); + } + + /** + * Copies last (post IDAT) ancillary chunks from a PngReader. + *

+ * Should be called when creating an image from another, after writing all lines, before closing the writer, to copy + * additional chunks. + *

+ * + * @param reader + * : PngReader object, already opened and fully read. + * @param copy_mask + * : Mask bit (OR), see ChunksToWrite.COPY_XXX 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 + *

+ * 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 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 chunks = new ArrayList(); + + /** + * chunks not yet writen - does not include IHDR, IDAT, END, perhaps yes PLTE + */ + private Set queuedChunks = new LinkedHashSet(); + + final ImageInfo imageInfo; // only required for writing + + public ChunkList(ImageInfo imfinfo) { + this.imageInfo = imfinfo; + } + + /** + * Adds chunk in next position. This is used when reading + */ + public void appendReadChunk(PngChunk chunk, int chunkGroup) { + chunk.setChunkGroup(chunkGroup); + chunks.add(chunk); + } + + public List getById(String id, boolean includeQueued, boolean includeProcessed) { + List list = new ArrayList(); + if (includeQueued) + for (PngChunk c : queuedChunks) + if (c.id.equals(id)) + list.add(c); + if (includeProcessed) + for (PngChunk c : chunks) + if (c.id.equals(id)) + list.add(c); + return list; + } + + /** + * Remove Chunk: only from queued + */ + public boolean removeChunk(PngChunk c) { + return queuedChunks.remove(c); + } + + /** + * add chunk to write queue + */ + public void queueChunk(PngChunk chunk, boolean replace, boolean priority) { + chunk.setPriority(priority); + if (replace) { + List current = getById(chunk.id, true, false); + for (PngChunk chunk2 : current) + removeChunk(chunk2); + } + queuedChunks.add(chunk); + } + + /** + * this should be called only for ancillary chunks and PLTE (groups 1 - 3 - 5) + **/ + private static boolean shouldWrite(PngChunk c, int currentGroup) { + if (currentGroup == CHUNK_GROUP_2_PLTE) + return c.id.equals(ChunkHelper.PLTE); + if (currentGroup % 2 == 0) + throw new RuntimeException("?"); + int minChunkGroup, maxChunkGroup; + if (c.mustGoBeforePLTE()) + minChunkGroup = maxChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR; + else if (c.mustGoBeforeIDAT()) { + maxChunkGroup = ChunkList.CHUNK_GROUP_3_AFTERPLTE; + minChunkGroup = c.mustGoAfterPLTE() ? ChunkList.CHUNK_GROUP_3_AFTERPLTE : ChunkList.CHUNK_GROUP_1_AFTERIDHR; + } else { + maxChunkGroup = ChunkList.CHUNK_GROUP_5_AFTERIDAT; + minChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR; + } + + int preferred = maxChunkGroup; + if (c.isWritePriority()) + preferred = minChunkGroup; + if (ChunkHelper.isUnknown(c) && c.getChunkGroup() > 0) + preferred = c.getChunkGroup(); + if (currentGroup == preferred) + return true; + if (currentGroup > preferred && currentGroup <= maxChunkGroup) + return true; + return false; + } + + public int writeChunks(OutputStream os, int currentGroup) { + int cont = 0; + Iterator it = queuedChunks.iterator(); + while (it.hasNext()) { + PngChunk c = it.next(); + if (!shouldWrite(c, currentGroup)) + continue; + c.write(os); + chunks.add(c); + c.setChunkGroup(currentGroup); + it.remove(); + cont++; + } + return cont; + } + + /** + * returns a copy of processed (read or writen) chunks + */ + public List getChunks() { + return new ArrayList(chunks); + } + + public List getChunksUnkown() { + List l = new ArrayList(); + for (PngChunk chunk : chunks) + if (ChunkHelper.isUnknown(chunk)) + l.add(chunk.id); + return l; + } + + /** + * returns a copy of queued (for write) chunks + */ + public List getQueuedChunks() { + return new ArrayList(queuedChunks); + } + + /** + * behaviour: + * + * a chunk already processed matches : exception a chunk queued matches and overwrite=true: replace it , return true + * a chunk queued matches and overwrite=false: do nothing, return false no matching: set it, return true + * + * @param c + * @param overwriteIfPresent + * @return true if added chunk + */ + public boolean setChunk(PngChunk c, boolean overwriteIfPresent) { + List list = getMatching(c, false, true); // processed + if (!list.isEmpty()) + throw new PngjException("chunk " + c.id + " already set "); + list = getMatching(c, true, false); // queued + if (!list.isEmpty()) { + if (overwriteIfPresent) { + for (PngChunk cx : list) + removeChunk(cx); + queueChunk(c, false, false); + return true; + } + return false; + } + queueChunk(c, false, false); + return true; + } + + /** + * returns only one chunk or null if nothing found - does not include queued + * + * If innerid!=null , the chunk is assumed to be PngChunkTextVar or PngChunkSPLT, and filtered by that id + * + * If more than one chunk (after filtering by inner id) is found, then an exception is thrown (failifMultiple=true) + * or the last one is returned (failifMultiple=false) + **/ + public PngChunk getChunk1(String id, String innerid, boolean failIfMultiple) { + List list = getChunks(id); + if (list.isEmpty()) + return null; + if (innerid != null) { + List list2 = new ArrayList(); + for (PngChunk c : list) { + if (c instanceof PngChunkTextVar) + if (((PngChunkTextVar) c).getKey().equals(innerid)) + list2.add(c); + if (c instanceof PngChunkSPLT) + if (((PngChunkSPLT) c).getPalName().equals(innerid)) + list2.add(c); + } + list = list2; + } + if (list.isEmpty()) + return null; + if (list.size() > 1 && failIfMultiple) + throw new PngjException("unexpected multiple chunks id=" + id); + return list.get(list.size() - 1); + } + + public PngChunk getChunk1(String id) { + return getChunk1(id, null, true); + } + + public List getChunks(String id) { // not including queued + return getById(id, false, true); + } + + private List getMatching(PngChunk cnew, boolean includeQueued, boolean includeProcessed) { + List list = new ArrayList(); + if (includeQueued) + for (PngChunk c : getQueuedChunks()) + if (matches(cnew, c)) + list.add(c); + if (includeProcessed) + for (PngChunk c : getChunks()) + if (matches(cnew, c)) + list.add(c); + return list; + } + + /** + * MY adhoc criteria: two chunks "match" if they have same id and (perhaps, if multiple are allowed) if the match + * also in some "internal key" (eg: key for string values, palette for sPLT, etc) + * + * @return true if "matches" + */ + public static boolean matches(PngChunk c2, PngChunk c1) { + if (c1 == null || c2 == null || !c1.id.equals(c2.id)) + return false; + // same id + if (c1.getClass() != c2.getClass()) + return false; // should not happen + if (!c2.allowsMultiple()) + return true; + if (c1 instanceof PngChunkTextVar) { + return ((PngChunkTextVar) c1).getKey().equals(((PngChunkTextVar) c2).getKey()); + } + if (c1 instanceof PngChunkSPLT) { + return ((PngChunkSPLT) c1).getPalName().equals(((PngChunkSPLT) c2).getPalName()); + } + // unknown chunks that allow multiple? consider they don't match + return false; + } + + public String toString() { + return "ChunkList: processed: " + chunks.size() + " queue: " + queuedChunks.size(); + } + + /** + * for debugging + */ + public String toStringFull() { + StringBuilder sb = new StringBuilder(toString()); + sb.append("\n Processed:\n"); + for (PngChunk chunk : chunks) { + sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); + } + if (!queuedChunks.isEmpty()) { + sb.append(" Queued:\n"); + for (PngChunk chunk : chunks) { + sb.append(chunk).append("\n"); + } + + } + return sb.toString(); + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java b/src/jogl/classes/jogamp/opengl/util/pngj/chunks/ChunkLoadBehaviour.java 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> factoryMap = new HashMap>(); + 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 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 cla = factoryMap.get(cid); + if (cla != null) { + Constructor 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 @@ + + +

+Contains the code related to chunk management for the PNGJ library.

+

+Only needed by client code if some special chunk handling is required. +

+ + 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 @@ + + +

+Contains the main classes for the PNGJ library.

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

+

+See also the nosandbox package if available. +

+ + -- cgit v1.2.3