diff --git a/.classpath b/.classpath deleted file mode 100644 index fb50116..0000000 --- a/.classpath +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/.gitignore b/.gitignore index 32858aa..05599fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + *.class # Mobile Tools for Java (J2ME) diff --git a/.project b/.project deleted file mode 100644 index 7f1269e..0000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - Asciimg - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 3a21537..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,11 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.8 diff --git a/bin/.gitignore b/bin/.gitignore deleted file mode 100644 index 8884ed1..0000000 --- a/bin/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/io/ -/examples/ diff --git a/compile.sh b/compile.sh deleted file mode 100755 index 5d9b005..0000000 --- a/compile.sh +++ /dev/null @@ -1 +0,0 @@ -javac -d bin `tree -fi | grep -e '\.java'` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..cb9da0d --- /dev/null +++ b/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + io.korhner + asciimg + 1.00-SNAPSHOT + jar + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.8 + 1.8 + + + + + \ No newline at end of file diff --git a/src/io/korhner/asciimg/image/AsciiImgCache.java b/src/main/java/io/korhner/asciimg/image/AsciiImgCache.java similarity index 100% rename from src/io/korhner/asciimg/image/AsciiImgCache.java rename to src/main/java/io/korhner/asciimg/image/AsciiImgCache.java diff --git a/src/io/korhner/asciimg/image/character_fit_strategy/BestCharacterFitStrategy.java b/src/main/java/io/korhner/asciimg/image/character_fit_strategy/BestCharacterFitStrategy.java similarity index 100% rename from src/io/korhner/asciimg/image/character_fit_strategy/BestCharacterFitStrategy.java rename to src/main/java/io/korhner/asciimg/image/character_fit_strategy/BestCharacterFitStrategy.java diff --git a/src/io/korhner/asciimg/image/character_fit_strategy/ColorSquareErrorFitStrategy.java b/src/main/java/io/korhner/asciimg/image/character_fit_strategy/ColorSquareErrorFitStrategy.java similarity index 100% rename from src/io/korhner/asciimg/image/character_fit_strategy/ColorSquareErrorFitStrategy.java rename to src/main/java/io/korhner/asciimg/image/character_fit_strategy/ColorSquareErrorFitStrategy.java diff --git a/src/io/korhner/asciimg/image/character_fit_strategy/StructuralSimilarityFitStrategy.java b/src/main/java/io/korhner/asciimg/image/character_fit_strategy/StructuralSimilarityFitStrategy.java similarity index 100% rename from src/io/korhner/asciimg/image/character_fit_strategy/StructuralSimilarityFitStrategy.java rename to src/main/java/io/korhner/asciimg/image/character_fit_strategy/StructuralSimilarityFitStrategy.java diff --git a/src/io/korhner/asciimg/image/converter/AsciiConverter.java b/src/main/java/io/korhner/asciimg/image/converter/AsciiConverter.java similarity index 100% rename from src/io/korhner/asciimg/image/converter/AsciiConverter.java rename to src/main/java/io/korhner/asciimg/image/converter/AsciiConverter.java diff --git a/src/io/korhner/asciimg/image/converter/AsciiToImageConverter.java b/src/main/java/io/korhner/asciimg/image/converter/AsciiToImageConverter.java similarity index 100% rename from src/io/korhner/asciimg/image/converter/AsciiToImageConverter.java rename to src/main/java/io/korhner/asciimg/image/converter/AsciiToImageConverter.java diff --git a/src/io/korhner/asciimg/image/converter/AsciiToStringConverter.java b/src/main/java/io/korhner/asciimg/image/converter/AsciiToStringConverter.java similarity index 100% rename from src/io/korhner/asciimg/image/converter/AsciiToStringConverter.java rename to src/main/java/io/korhner/asciimg/image/converter/AsciiToStringConverter.java diff --git a/src/io/korhner/asciimg/image/converter/GifToAsciiConvert.java b/src/main/java/io/korhner/asciimg/image/converter/GifToAsciiConvert.java similarity index 100% rename from src/io/korhner/asciimg/image/converter/GifToAsciiConvert.java rename to src/main/java/io/korhner/asciimg/image/converter/GifToAsciiConvert.java diff --git a/src/io/korhner/asciimg/image/matrix/GrayscaleMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/GrayscaleMatrix.java similarity index 100% rename from src/io/korhner/asciimg/image/matrix/GrayscaleMatrix.java rename to src/main/java/io/korhner/asciimg/image/matrix/GrayscaleMatrix.java diff --git a/src/io/korhner/asciimg/image/matrix/TiledGrayscaleMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/TiledGrayscaleMatrix.java similarity index 100% rename from src/io/korhner/asciimg/image/matrix/TiledGrayscaleMatrix.java rename to src/main/java/io/korhner/asciimg/image/matrix/TiledGrayscaleMatrix.java diff --git a/src/io/korhner/asciimg/utils/AnimatedGifEncoder.java b/src/main/java/io/korhner/asciimg/utils/AnimatedGifEncoder.java similarity index 96% rename from src/io/korhner/asciimg/utils/AnimatedGifEncoder.java rename to src/main/java/io/korhner/asciimg/utils/AnimatedGifEncoder.java index d875766..aa4c149 100644 --- a/src/io/korhner/asciimg/utils/AnimatedGifEncoder.java +++ b/src/main/java/io/korhner/asciimg/utils/AnimatedGifEncoder.java @@ -1,469 +1,469 @@ -package io.korhner.asciimg.utils; - -import java.io.*; -import java.awt.*; -import java.awt.image.*; - -/** - * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or - * more frames. - *
- * Example:
- *    AnimatedGifEncoder e = new AnimatedGifEncoder();
- *    e.start(outputFileName);
- *    e.setDelay(1000);   // 1 frame per sec
- *    e.addFrame(image1);
- *    e.addFrame(image2);
- *    e.finish();
- * 
- * No copyright asserted on the source code of this class. May be used - * for any purpose, however, refer to the Unisys LZW patent for restrictions - * on use of the associated LZWEncoder class. Please forward any corrections - * to questions at fmsware.com. - * - * @author Kevin Weiner, FM Software - * @version 1.03 November 2003 - * - */ - -public class AnimatedGifEncoder { - - protected int width; // image size - protected int height; - protected Color transparent = null; // transparent color if given - protected int transIndex; // transparent index in color table - protected int repeat = -1; // no repeat - protected int delay = 0; // frame delay (hundredths) - protected boolean started = false; // ready to output frames - protected OutputStream out; - protected BufferedImage image; // current frame - protected byte[] pixels; // BGR byte array from frame - protected byte[] indexedPixels; // converted frame indexed to palette - protected int colorDepth; // number of bit planes - protected byte[] colorTab; // RGB palette - protected boolean[] usedEntry = new boolean[256]; // active palette entries - protected int palSize = 7; // color table size (bits-1) - protected int dispose = -1; // disposal code (-1 = use default) - protected boolean closeStream = false; // close stream when finished - protected boolean firstFrame = true; - protected boolean sizeSet = false; // if false, get size from first frame - protected int sample = 10; // default sample interval for quantizer - - /** - * Sets the delay time between each frame, or changes it - * for subsequent frames (applies to last frame added). - * - * @param ms int delay time in milliseconds - */ - public void setDelay(int ms) { - delay = Math.round(ms / 10.0f); - } - - /** - * Sets the GIF frame disposal code for the last added frame - * and any subsequent frames. Default is 0 if no transparent - * color has been set, otherwise 2. - * @param code int disposal code. - */ - public void setDispose(int code) { - if (code >= 0) { - dispose = code; - } - } - - /** - * Sets the number of times the set of GIF frames - * should be played. Default is 1; 0 means play - * indefinitely. Must be invoked before the first - * image is added. - * - * @param iter int number of iterations. - * @return - */ - public void setRepeat(int iter) { - if (iter >= 0) { - repeat = iter; - } - } - - /** - * Sets the transparent color for the last added frame - * and any subsequent frames. - * Since all colors are subject to modification - * in the quantization process, the color in the final - * palette for each frame closest to the given color - * becomes the transparent color for that frame. - * May be set to null to indicate no transparent color. - * - * @param c Color to be treated as transparent on display. - */ - public void setTransparent(Color c) { - transparent = c; - } - - /** - * Adds next GIF frame. The frame is not written immediately, but is - * actually deferred until the next frame is received so that timing - * data can be inserted. Invoking finish() flushes all - * frames. If setSize was not invoked, the size of the - * first image is used for all subsequent frames. - * - * @param im BufferedImage containing frame to write. - * @return true if successful. - */ - public boolean addFrame(BufferedImage im) { - if ((im == null) || !started) { - return false; - } - boolean ok = true; - try { - if (!sizeSet) { - // use first frame's size - setSize(im.getWidth(), im.getHeight()); - } - image = im; - getImagePixels(); // convert to correct format if necessary - analyzePixels(); // build color table & map pixels - if (firstFrame) { - writeLSD(); // logical screen descriptior - writePalette(); // global color table - if (repeat >= 0) { - // use NS app extension to indicate reps - writeNetscapeExt(); - } - } - writeGraphicCtrlExt(); // write graphic control extension - writeImageDesc(); // image descriptor - if (!firstFrame) { - writePalette(); // local color table - } - writePixels(); // encode and write pixel data - firstFrame = false; - } catch (IOException e) { - ok = false; - } - - return ok; - } - - /** - * Flushes any pending data and closes output file. - * If writing to an OutputStream, the stream is not - * closed. - */ - public boolean finish() { - if (!started) return false; - boolean ok = true; - started = false; - try { - out.write(0x3b); // gif trailer - out.flush(); - if (closeStream) { - out.close(); - } - } catch (IOException e) { - ok = false; - } - - // reset for subsequent use - transIndex = 0; - out = null; - image = null; - pixels = null; - indexedPixels = null; - colorTab = null; - closeStream = false; - firstFrame = true; - - return ok; - } - - /** - * Sets frame rate in frames per second. Equivalent to - * setDelay(1000/fps). - * - * @param fps float frame rate (frames per second) - */ - public void setFrameRate(float fps) { - if (fps != 0f) { - delay = Math.round(100f / fps); - } - } - - /** - * Sets quality of color quantization (conversion of images - * to the maximum 256 colors allowed by the GIF specification). - * Lower values (minimum = 1) produce better colors, but slow - * processing significantly. 10 is the default, and produces - * good color mapping at reasonable speeds. Values greater - * than 20 do not yield significant improvements in speed. - * - * @param quality int greater than 0. - * @return - */ - public void setQuality(int quality) { - if (quality < 1) quality = 1; - sample = quality; - } - - /** - * Sets the GIF frame size. The default size is the - * size of the first frame added if this method is - * not invoked. - * - * @param w int frame width. - * @param h int frame width. - */ - public void setSize(int w, int h) { - if (started && !firstFrame) return; - width = w; - height = h; - if (width < 1) width = 320; - if (height < 1) height = 240; - sizeSet = true; - } - - /** - * Initiates GIF file creation on the given stream. The stream - * is not closed automatically. - * - * @param os OutputStream on which GIF images are written. - * @return false if initial write failed. - */ - public boolean start(OutputStream os) { - if (os == null) return false; - boolean ok = true; - closeStream = false; - out = os; - try { - writeString("GIF89a"); // header - } catch (IOException e) { - ok = false; - } - return started = ok; - } - - /** - * Initiates writing of a GIF file with the specified name. - * - * @param file String containing output file name. - * @return false if open or initial write failed. - */ - public boolean start(String file) { - boolean ok = true; - try { - out = new BufferedOutputStream(new FileOutputStream(file)); - ok = start(out); - closeStream = true; - } catch (IOException e) { - ok = false; - } - return started = ok; - } - - /** - * Analyzes image colors and creates color map. - */ - protected void analyzePixels() { - int len = pixels.length; - int nPix = len / 3; - indexedPixels = new byte[nPix]; - NeuQuant nq = new NeuQuant(pixels, len, sample); - // initialize quantizer - colorTab = nq.process(); // create reduced palette - // convert map from BGR to RGB - for (int i = 0; i < colorTab.length; i += 3) { - byte temp = colorTab[i]; - colorTab[i] = colorTab[i + 2]; - colorTab[i + 2] = temp; - usedEntry[i / 3] = false; - } - // map image pixels to new palette - int k = 0; - for (int i = 0; i < nPix; i++) { - int index = - nq.map(pixels[k++] & 0xff, - pixels[k++] & 0xff, - pixels[k++] & 0xff); - usedEntry[index] = true; - indexedPixels[i] = (byte) index; - } - pixels = null; - colorDepth = 8; - palSize = 7; - // get closest match to transparent color if specified - if (transparent != null) { - transIndex = findClosest(transparent); - } - } - - /** - * Returns index of palette color closest to c - * - */ - protected int findClosest(Color c) { - if (colorTab == null) return -1; - int r = c.getRed(); - int g = c.getGreen(); - int b = c.getBlue(); - int minpos = 0; - int dmin = 256 * 256 * 256; - int len = colorTab.length; - for (int i = 0; i < len;) { - int dr = r - (colorTab[i++] & 0xff); - int dg = g - (colorTab[i++] & 0xff); - int db = b - (colorTab[i] & 0xff); - int d = dr * dr + dg * dg + db * db; - int index = i / 3; - if (usedEntry[index] && (d < dmin)) { - dmin = d; - minpos = index; - } - i++; - } - return minpos; - } - - /** - * Extracts image pixels into byte array "pixels" - */ - protected void getImagePixels() { - int w = image.getWidth(); - int h = image.getHeight(); - int type = image.getType(); - if ((w != width) - || (h != height) - || (type != BufferedImage.TYPE_3BYTE_BGR)) { - // create new image with right size/format - BufferedImage temp = - new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); - Graphics2D g = temp.createGraphics(); - g.drawImage(image, 0, 0, null); - image = temp; - } - pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); - } - - /** - * Writes Graphic Control Extension - */ - protected void writeGraphicCtrlExt() throws IOException { - out.write(0x21); // extension introducer - out.write(0xf9); // GCE label - out.write(4); // data block size - int transp, disp; - if (transparent == null) { - transp = 0; - disp = 0; // dispose = no action - } else { - transp = 1; - disp = 2; // force clear if using transparent color - } - if (dispose >= 0) { - disp = dispose & 7; // user override - } - disp <<= 2; - - // packed fields - out.write(0 | // 1:3 reserved - disp | // 4:6 disposal - 0 | // 7 user input - 0 = none - transp); // 8 transparency flag - - writeShort(delay); // delay x 1/100 sec - out.write(transIndex); // transparent color index - out.write(0); // block terminator - } - - /** - * Writes Image Descriptor - */ - protected void writeImageDesc() throws IOException { - out.write(0x2c); // image separator - writeShort(0); // image position x,y = 0,0 - writeShort(0); - writeShort(width); // image size - writeShort(height); - // packed fields - if (firstFrame) { - // no LCT - GCT is used for first (or only) frame - out.write(0); - } else { - // specify normal LCT - out.write(0x80 | // 1 local color table 1=yes - 0 | // 2 interlace - 0=no - 0 | // 3 sorted - 0=no - 0 | // 4-5 reserved - palSize); // 6-8 size of color table - } - } - - /** - * Writes Logical Screen Descriptor - */ - protected void writeLSD() throws IOException { - // logical screen size - writeShort(width); - writeShort(height); - // packed fields - out.write((0x80 | // 1 : global color table flag = 1 (gct used) - 0x70 | // 2-4 : color resolution = 7 - 0x00 | // 5 : gct sort flag = 0 - palSize)); // 6-8 : gct size - - out.write(0); // background color index - out.write(0); // pixel aspect ratio - assume 1:1 - } - - /** - * Writes Netscape application extension to define - * repeat count. - */ - protected void writeNetscapeExt() throws IOException { - out.write(0x21); // extension introducer - out.write(0xff); // app extension label - out.write(11); // block size - writeString("NETSCAPE" + "2.0"); // app id + auth code - out.write(3); // sub-block size - out.write(1); // loop sub-block id - writeShort(repeat); // loop count (extra iterations, 0=repeat forever) - out.write(0); // block terminator - } - - /** - * Writes color table - */ - protected void writePalette() throws IOException { - out.write(colorTab, 0, colorTab.length); - int n = (3 * 256) - colorTab.length; - for (int i = 0; i < n; i++) { - out.write(0); - } - } - - /** - * Encodes and writes pixel data - */ - protected void writePixels() throws IOException { - LZWEncoder encoder = - new LZWEncoder(width, height, indexedPixels, colorDepth); - encoder.encode(out); - } - - /** - * Write 16-bit value to output stream, LSB first - */ - protected void writeShort(int value) throws IOException { - out.write(value & 0xff); - out.write((value >> 8) & 0xff); - } - - /** - * Writes string to output stream - */ - protected void writeString(String s) throws IOException { - for (int i = 0; i < s.length(); i++) { - out.write((byte) s.charAt(i)); - } - } -} +package io.korhner.asciimg.utils; + +import java.io.*; +import java.awt.*; +import java.awt.image.*; + +/** + * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or + * more frames. + *
+ * Example:
+ *    AnimatedGifEncoder e = new AnimatedGifEncoder();
+ *    e.start(outputFileName);
+ *    e.setDelay(1000);   // 1 frame per sec
+ *    e.addFrame(image1);
+ *    e.addFrame(image2);
+ *    e.finish();
+ * 
+ * No copyright asserted on the source code of this class. May be used + * for any purpose, however, refer to the Unisys LZW patent for restrictions + * on use of the associated LZWEncoder class. Please forward any corrections + * to questions at fmsware.com. + * + * @author Kevin Weiner, FM Software + * @version 1.03 November 2003 + * + */ + +public class AnimatedGifEncoder { + + protected int width; // image size + protected int height; + protected Color transparent = null; // transparent color if given + protected int transIndex; // transparent index in color table + protected int repeat = -1; // no repeat + protected int delay = 0; // frame delay (hundredths) + protected boolean started = false; // ready to output frames + protected OutputStream out; + protected BufferedImage image; // current frame + protected byte[] pixels; // BGR byte array from frame + protected byte[] indexedPixels; // converted frame indexed to palette + protected int colorDepth; // number of bit planes + protected byte[] colorTab; // RGB palette + protected boolean[] usedEntry = new boolean[256]; // active palette entries + protected int palSize = 7; // color table size (bits-1) + protected int dispose = -1; // disposal code (-1 = use default) + protected boolean closeStream = false; // close stream when finished + protected boolean firstFrame = true; + protected boolean sizeSet = false; // if false, get size from first frame + protected int sample = 10; // default sample interval for quantizer + + /** + * Sets the delay time between each frame, or changes it + * for subsequent frames (applies to last frame added). + * + * @param ms int delay time in milliseconds + */ + public void setDelay(int ms) { + delay = Math.round(ms / 10.0f); + } + + /** + * Sets the GIF frame disposal code for the last added frame + * and any subsequent frames. Default is 0 if no transparent + * color has been set, otherwise 2. + * @param code int disposal code. + */ + public void setDispose(int code) { + if (code >= 0) { + dispose = code; + } + } + + /** + * Sets the number of times the set of GIF frames + * should be played. Default is 1; 0 means play + * indefinitely. Must be invoked before the first + * image is added. + * + * @param iter int number of iterations. + * @return + */ + public void setRepeat(int iter) { + if (iter >= 0) { + repeat = iter; + } + } + + /** + * Sets the transparent color for the last added frame + * and any subsequent frames. + * Since all colors are subject to modification + * in the quantization process, the color in the final + * palette for each frame closest to the given color + * becomes the transparent color for that frame. + * May be set to null to indicate no transparent color. + * + * @param c Color to be treated as transparent on display. + */ + public void setTransparent(Color c) { + transparent = c; + } + + /** + * Adds next GIF frame. The frame is not written immediately, but is + * actually deferred until the next frame is received so that timing + * data can be inserted. Invoking finish() flushes all + * frames. If setSize was not invoked, the size of the + * first image is used for all subsequent frames. + * + * @param im BufferedImage containing frame to write. + * @return true if successful. + */ + public boolean addFrame(BufferedImage im) { + if ((im == null) || !started) { + return false; + } + boolean ok = true; + try { + if (!sizeSet) { + // use first frame's size + setSize(im.getWidth(), im.getHeight()); + } + image = im; + getImagePixels(); // convert to correct format if necessary + analyzePixels(); // build color table & map pixels + if (firstFrame) { + writeLSD(); // logical screen descriptior + writePalette(); // global color table + if (repeat >= 0) { + // use NS app extension to indicate reps + writeNetscapeExt(); + } + } + writeGraphicCtrlExt(); // write graphic control extension + writeImageDesc(); // image descriptor + if (!firstFrame) { + writePalette(); // local color table + } + writePixels(); // encode and write pixel data + firstFrame = false; + } catch (IOException e) { + ok = false; + } + + return ok; + } + + /** + * Flushes any pending data and closes output file. + * If writing to an OutputStream, the stream is not + * closed. + */ + public boolean finish() { + if (!started) return false; + boolean ok = true; + started = false; + try { + out.write(0x3b); // gif trailer + out.flush(); + if (closeStream) { + out.close(); + } + } catch (IOException e) { + ok = false; + } + + // reset for subsequent use + transIndex = 0; + out = null; + image = null; + pixels = null; + indexedPixels = null; + colorTab = null; + closeStream = false; + firstFrame = true; + + return ok; + } + + /** + * Sets frame rate in frames per second. Equivalent to + * setDelay(1000/fps). + * + * @param fps float frame rate (frames per second) + */ + public void setFrameRate(float fps) { + if (fps != 0f) { + delay = Math.round(100f / fps); + } + } + + /** + * Sets quality of color quantization (conversion of images + * to the maximum 256 colors allowed by the GIF specification). + * Lower values (minimum = 1) produce better colors, but slow + * processing significantly. 10 is the default, and produces + * good color mapping at reasonable speeds. Values greater + * than 20 do not yield significant improvements in speed. + * + * @param quality int greater than 0. + * @return + */ + public void setQuality(int quality) { + if (quality < 1) quality = 1; + sample = quality; + } + + /** + * Sets the GIF frame size. The default size is the + * size of the first frame added if this method is + * not invoked. + * + * @param w int frame width. + * @param h int frame width. + */ + public void setSize(int w, int h) { + if (started && !firstFrame) return; + width = w; + height = h; + if (width < 1) width = 320; + if (height < 1) height = 240; + sizeSet = true; + } + + /** + * Initiates GIF file creation on the given stream. The stream + * is not closed automatically. + * + * @param os OutputStream on which GIF images are written. + * @return false if initial write failed. + */ + public boolean start(OutputStream os) { + if (os == null) return false; + boolean ok = true; + closeStream = false; + out = os; + try { + writeString("GIF89a"); // header + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + /** + * Initiates writing of a GIF file with the specified name. + * + * @param file String containing output file name. + * @return false if open or initial write failed. + */ + public boolean start(String file) { + boolean ok = true; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + ok = start(out); + closeStream = true; + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + /** + * Analyzes image colors and creates color map. + */ + protected void analyzePixels() { + int len = pixels.length; + int nPix = len / 3; + indexedPixels = new byte[nPix]; + NeuQuant nq = new NeuQuant(pixels, len, sample); + // initialize quantizer + colorTab = nq.process(); // create reduced palette + // convert map from BGR to RGB + for (int i = 0; i < colorTab.length; i += 3) { + byte temp = colorTab[i]; + colorTab[i] = colorTab[i + 2]; + colorTab[i + 2] = temp; + usedEntry[i / 3] = false; + } + // map image pixels to new palette + int k = 0; + for (int i = 0; i < nPix; i++) { + int index = + nq.map(pixels[k++] & 0xff, + pixels[k++] & 0xff, + pixels[k++] & 0xff); + usedEntry[index] = true; + indexedPixels[i] = (byte) index; + } + pixels = null; + colorDepth = 8; + palSize = 7; + // get closest match to transparent color if specified + if (transparent != null) { + transIndex = findClosest(transparent); + } + } + + /** + * Returns index of palette color closest to c + * + */ + protected int findClosest(Color c) { + if (colorTab == null) return -1; + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + int minpos = 0; + int dmin = 256 * 256 * 256; + int len = colorTab.length; + for (int i = 0; i < len;) { + int dr = r - (colorTab[i++] & 0xff); + int dg = g - (colorTab[i++] & 0xff); + int db = b - (colorTab[i] & 0xff); + int d = dr * dr + dg * dg + db * db; + int index = i / 3; + if (usedEntry[index] && (d < dmin)) { + dmin = d; + minpos = index; + } + i++; + } + return minpos; + } + + /** + * Extracts image pixels into byte array "pixels" + */ + protected void getImagePixels() { + int w = image.getWidth(); + int h = image.getHeight(); + int type = image.getType(); + if ((w != width) + || (h != height) + || (type != BufferedImage.TYPE_3BYTE_BGR)) { + // create new image with right size/format + BufferedImage temp = + new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g = temp.createGraphics(); + g.drawImage(image, 0, 0, null); + image = temp; + } + pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + } + + /** + * Writes Graphic Control Extension + */ + protected void writeGraphicCtrlExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xf9); // GCE label + out.write(4); // data block size + int transp, disp; + if (transparent == null) { + transp = 0; + disp = 0; // dispose = no action + } else { + transp = 1; + disp = 2; // force clear if using transparent color + } + if (dispose >= 0) { + disp = dispose & 7; // user override + } + disp <<= 2; + + // packed fields + out.write(0 | // 1:3 reserved + disp | // 4:6 disposal + 0 | // 7 user input - 0 = none + transp); // 8 transparency flag + + writeShort(delay); // delay x 1/100 sec + out.write(transIndex); // transparent color index + out.write(0); // block terminator + } + + /** + * Writes Image Descriptor + */ + protected void writeImageDesc() throws IOException { + out.write(0x2c); // image separator + writeShort(0); // image position x,y = 0,0 + writeShort(0); + writeShort(width); // image size + writeShort(height); + // packed fields + if (firstFrame) { + // no LCT - GCT is used for first (or only) frame + out.write(0); + } else { + // specify normal LCT + out.write(0x80 | // 1 local color table 1=yes + 0 | // 2 interlace - 0=no + 0 | // 3 sorted - 0=no + 0 | // 4-5 reserved + palSize); // 6-8 size of color table + } + } + + /** + * Writes Logical Screen Descriptor + */ + protected void writeLSD() throws IOException { + // logical screen size + writeShort(width); + writeShort(height); + // packed fields + out.write((0x80 | // 1 : global color table flag = 1 (gct used) + 0x70 | // 2-4 : color resolution = 7 + 0x00 | // 5 : gct sort flag = 0 + palSize)); // 6-8 : gct size + + out.write(0); // background color index + out.write(0); // pixel aspect ratio - assume 1:1 + } + + /** + * Writes Netscape application extension to define + * repeat count. + */ + protected void writeNetscapeExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xff); // app extension label + out.write(11); // block size + writeString("NETSCAPE" + "2.0"); // app id + auth code + out.write(3); // sub-block size + out.write(1); // loop sub-block id + writeShort(repeat); // loop count (extra iterations, 0=repeat forever) + out.write(0); // block terminator + } + + /** + * Writes color table + */ + protected void writePalette() throws IOException { + out.write(colorTab, 0, colorTab.length); + int n = (3 * 256) - colorTab.length; + for (int i = 0; i < n; i++) { + out.write(0); + } + } + + /** + * Encodes and writes pixel data + */ + protected void writePixels() throws IOException { + LZWEncoder encoder = + new LZWEncoder(width, height, indexedPixels, colorDepth); + encoder.encode(out); + } + + /** + * Write 16-bit value to output stream, LSB first + */ + protected void writeShort(int value) throws IOException { + out.write(value & 0xff); + out.write((value >> 8) & 0xff); + } + + /** + * Writes string to output stream + */ + protected void writeString(String s) throws IOException { + for (int i = 0; i < s.length(); i++) { + out.write((byte) s.charAt(i)); + } + } +} diff --git a/src/io/korhner/asciimg/utils/ArrayUtils.java b/src/main/java/io/korhner/asciimg/utils/ArrayUtils.java similarity index 100% rename from src/io/korhner/asciimg/utils/ArrayUtils.java rename to src/main/java/io/korhner/asciimg/utils/ArrayUtils.java diff --git a/src/io/korhner/asciimg/utils/GifDecoder.java b/src/main/java/io/korhner/asciimg/utils/GifDecoder.java similarity index 95% rename from src/io/korhner/asciimg/utils/GifDecoder.java rename to src/main/java/io/korhner/asciimg/utils/GifDecoder.java index b1a139d..228f8c8 100644 --- a/src/io/korhner/asciimg/utils/GifDecoder.java +++ b/src/main/java/io/korhner/asciimg/utils/GifDecoder.java @@ -1,782 +1,782 @@ -package io.korhner.asciimg.utils; -import java.awt.AlphaComposite; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferInt; -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -/** - * Class GifDecoder - Decodes a GIF file into one or more frames. - *
- * Example:
- *    GifDecoder d = new GifDecoder();
- *    d.read("sample.gif");
- *    int n = d.getFrameCount();
- *    for (int i = 0; i < n; i++) {
- *       BufferedImage frame = d.getFrame(i);  // frame i
- *       int t = d.getDelay(i);  // display duration of frame in milliseconds
- *       // do something with frame
- *    }
- * 
- * No copyright asserted on the source code of this class. May be used for - * any purpose, however, refer to the Unisys LZW patent for any additional - * restrictions. Please forward any corrections to questions at fmsware.com. - * - * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick. - * @version 1.03 November 2003 - * - */ - -public class GifDecoder { - - /** - * File read status: No errors. - */ - public static final int STATUS_OK = 0; - - /** - * File read status: Error decoding file (may be partially decoded) - */ - public static final int STATUS_FORMAT_ERROR = 1; - - /** - * File read status: Unable to open source. - */ - public static final int STATUS_OPEN_ERROR = 2; - - protected BufferedInputStream in; - protected int status; - - protected int width; // full image width - protected int height; // full image height - protected boolean gctFlag; // global color table used - protected int gctSize; // size of global color table - protected int loopCount = 1; // iterations; 0 = repeat forever - - protected int[] gct; // global color table - protected int[] lct; // local color table - protected int[] act; // active color table - - protected int bgIndex; // background color index - protected int bgColor; // background color - protected int lastBgColor; // previous bg color - protected int pixelAspect; // pixel aspect ratio - - protected boolean lctFlag; // local color table flag - protected boolean interlace; // interlace flag - protected int lctSize; // local color table size - - protected int ix, iy, iw, ih; // current image rectangle - protected Rectangle lastRect; // last image rect - protected BufferedImage image; // current frame - protected BufferedImage lastImage; // previous frame - - protected byte[] block = new byte[256]; // current data block - protected int blockSize = 0; // block size - - // last graphic control extension info - protected int dispose = 0; - // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev - protected int lastDispose = 0; - protected boolean transparency = false; // use transparent color - protected int delay = 0; // delay in milliseconds - protected int transIndex; // transparent color index - - protected static final int MaxStackSize = 4096; - // max decoder pixel stack size - - // LZW decoder working arrays - protected short[] prefix; - protected byte[] suffix; - protected byte[] pixelStack; - protected byte[] pixels; - - protected List frames; // frames read from current file - protected int frameCount; - - static class GifFrame { - public GifFrame(BufferedImage im, int del) { - image = im; - delay = del; - } - public BufferedImage image; - public int delay; - } - - /** - * Gets display duration for specified frame. - * - * @param n int index of frame - * @return delay in milliseconds - */ - public int getDelay(int n) { - // - delay = -1; - if ((n >= 0) && (n < frameCount)) { - delay = ((GifFrame) frames.get(n)).delay; - } - return delay; - } - - /** - * Gets the number of frames read from file. - * @return frame count - */ - public int getFrameCount() { - return frameCount; - } - - /** - * Gets the first (or only) image read. - * - * @return BufferedImage containing first frame, or null if none. - */ - public BufferedImage getImage() { - return getFrame(0); - } - - /** - * Gets the "Netscape" iteration count, if any. - * A count of 0 means repeat indefinitiely. - * - * @return iteration count if one was specified, else 1. - */ - public int getLoopCount() { - return loopCount; - } - - /** - * Creates new frame image from current data (and previous - * frames as specified by their disposition codes). - */ - protected void setPixels() { - // expose destination image's pixels as int array - int[] dest = - ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - - // fill in starting image contents based on last image's dispose code - if (lastDispose > 0) { - if (lastDispose == 3) { - // use image before last - int n = frameCount - 2; - if (n > 0) { - lastImage = getFrame(n - 1); - } else { - lastImage = null; - } - } - - if (lastImage != null) { - int[] prev = - ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData(); - System.arraycopy(prev, 0, dest, 0, width * height); - // copy pixels - - if (lastDispose == 2) { - // fill last image rect area with background color - Graphics2D g = image.createGraphics(); - Color c = null; - if (transparency) { - c = new Color(0, 0, 0, 0); // assume background is transparent - } else { - c = new Color(lastBgColor); // use given background color - } - g.setColor(c); - g.setComposite(AlphaComposite.Src); // replace area - g.fill(lastRect); - g.dispose(); - } - } - } - - // copy each source line to the appropriate place in the destination - int pass = 1; - int inc = 8; - int iline = 0; - for (int i = 0; i < ih; i++) { - int line = i; - if (interlace) { - if (iline >= ih) { - pass++; - switch (pass) { - case 2 : - iline = 4; - break; - case 3 : - iline = 2; - inc = 4; - break; - case 4 : - iline = 1; - inc = 2; - } - } - line = iline; - iline += inc; - } - line += iy; - if (line < height) { - int k = line * width; - int dx = k + ix; // start of line in dest - int dlim = dx + iw; // end of dest line - if ((k + width) < dlim) { - dlim = k + width; // past dest edge - } - int sx = i * iw; // start of line in source - while (dx < dlim) { - // map color and insert in destination - int index = ((int) pixels[sx++]) & 0xff; - int c = act[index]; - if (c != 0) { - dest[dx] = c; - } - dx++; - } - } - } - } - - /** - * Gets the image contents of frame n. - * - * @return BufferedImage representation of frame, or null if n is invalid. - */ - public BufferedImage getFrame(int n) { - BufferedImage im = null; - if ((n >= 0) && (n < frameCount)) { - im = ((GifFrame) frames.get(n)).image; - } - return im; - } - - /** - * Gets image size. - * - * @return GIF image dimensions - */ - public Dimension getFrameSize() { - return new Dimension(width, height); - } - - /** - * Reads GIF image from stream - * - * @param BufferedInputStream containing GIF file. - * @return read status code (0 = no errors) - */ - public int read(BufferedInputStream is) { - init(); - if (is != null) { - in = is; - readHeader(); - if (!err()) { - readContents(); - if (frameCount < 0) { - status = STATUS_FORMAT_ERROR; - } - } - } else { - status = STATUS_OPEN_ERROR; - } - try { - is.close(); - } catch (IOException e) { - } - return status; - } - - /** - * Reads GIF image from stream - * - * @param InputStream containing GIF file. - * @return read status code (0 = no errors) - */ - public int read(InputStream is) { - init(); - if (is != null) { - if (!(is instanceof BufferedInputStream)) - is = new BufferedInputStream(is); - in = (BufferedInputStream) is; - readHeader(); - if (!err()) { - readContents(); - if (frameCount < 0) { - status = STATUS_FORMAT_ERROR; - } - } - } else { - status = STATUS_OPEN_ERROR; - } - try { - is.close(); - } catch (IOException e) { - } - return status; - } - - /** - * Reads GIF file from specified file/URL source - * (URL assumed if name contains ":/" or "file:") - * - * @param name String containing source - * @return read status code (0 = no errors) - */ - public int read(String name) { - status = STATUS_OK; - try { - name = name.trim().toLowerCase(); - if ((name.indexOf("file:") >= 0) || - (name.indexOf(":/") > 0)) { - URL url = new URL(name); - in = new BufferedInputStream(url.openStream()); - } else { - in = new BufferedInputStream(new FileInputStream(name)); - } - status = read(in); - } catch (IOException e) { - status = STATUS_OPEN_ERROR; - } - - return status; - } - - /** - * Decodes LZW image data into pixel array. - * Adapted from John Cristy's ImageMagick. - */ - protected void decodeImageData() { - int NullCode = -1; - int npix = iw * ih; - int available, - clear, - code_mask, - code_size, - end_of_information, - in_code, - old_code, - bits, - code, - count, - i, - datum, - data_size, - first, - top, - bi, - pi; - - if ((pixels == null) || (pixels.length < npix)) { - pixels = new byte[npix]; // allocate new pixel array - } - if (prefix == null) prefix = new short[MaxStackSize]; - if (suffix == null) suffix = new byte[MaxStackSize]; - if (pixelStack == null) pixelStack = new byte[MaxStackSize + 1]; - - // Initialize GIF data stream decoder. - - data_size = read(); - clear = 1 << data_size; - end_of_information = clear + 1; - available = clear + 2; - old_code = NullCode; - code_size = data_size + 1; - code_mask = (1 << code_size) - 1; - for (code = 0; code < clear; code++) { - prefix[code] = 0; - suffix[code] = (byte) code; - } - - // Decode GIF pixel stream. - - datum = bits = count = first = top = pi = bi = 0; - - for (i = 0; i < npix;) { - if (top == 0) { - if (bits < code_size) { - // Load bytes until there are enough bits for a code. - if (count == 0) { - // Read a new data block. - count = readBlock(); - if (count <= 0) - break; - bi = 0; - } - datum += (((int) block[bi]) & 0xff) << bits; - bits += 8; - bi++; - count--; - continue; - } - - // Get the next code. - - code = datum & code_mask; - datum >>= code_size; - bits -= code_size; - - // Interpret the code - - if ((code > available) || (code == end_of_information)) - break; - if (code == clear) { - // Reset decoder. - code_size = data_size + 1; - code_mask = (1 << code_size) - 1; - available = clear + 2; - old_code = NullCode; - continue; - } - if (old_code == NullCode) { - pixelStack[top++] = suffix[code]; - old_code = code; - first = code; - continue; - } - in_code = code; - if (code == available) { - pixelStack[top++] = (byte) first; - code = old_code; - } - while (code > clear) { - pixelStack[top++] = suffix[code]; - code = prefix[code]; - } - first = ((int) suffix[code]) & 0xff; - - // Add a new string to the string table, - - if (available >= MaxStackSize) - break; - pixelStack[top++] = (byte) first; - prefix[available] = (short) old_code; - suffix[available] = (byte) first; - available++; - if (((available & code_mask) == 0) - && (available < MaxStackSize)) { - code_size++; - code_mask += available; - } - old_code = in_code; - } - - // Pop a pixel off the pixel stack. - - top--; - pixels[pi++] = pixelStack[top]; - i++; - } - - for (i = pi; i < npix; i++) { - pixels[i] = 0; // clear missing pixels - } - - } - - /** - * Returns true if an error was encountered during reading/decoding - */ - protected boolean err() { - return status != STATUS_OK; - } - - /** - * Initializes or re-initializes reader - */ - protected void init() { - status = STATUS_OK; - frameCount = 0; - frames = new ArrayList<>(); - gct = null; - lct = null; - } - - /** - * Reads a single byte from the input stream. - */ - protected int read() { - int curByte = 0; - try { - curByte = in.read(); - } catch (IOException e) { - status = STATUS_FORMAT_ERROR; - } - return curByte; - } - - /** - * Reads next variable length block from input. - * - * @return number of bytes stored in "buffer" - */ - protected int readBlock() { - blockSize = read(); - int n = 0; - if (blockSize > 0) { - try { - int count = 0; - while (n < blockSize) { - count = in.read(block, n, blockSize - n); - if (count == -1) - break; - n += count; - } - } catch (IOException e) { - } - - if (n < blockSize) { - status = STATUS_FORMAT_ERROR; - } - } - return n; - } - - /** - * Reads color table as 256 RGB integer values - * - * @param ncolors int number of colors to read - * @return int array containing 256 colors (packed ARGB with full alpha) - */ - protected int[] readColorTable(int ncolors) { - int nbytes = 3 * ncolors; - int[] tab = null; - byte[] c = new byte[nbytes]; - int n = 0; - try { - n = in.read(c); - } catch (IOException e) { - } - if (n < nbytes) { - status = STATUS_FORMAT_ERROR; - } else { - tab = new int[256]; // max size to avoid bounds checks - int i = 0; - int j = 0; - while (i < ncolors) { - int r = ((int) c[j++]) & 0xff; - int g = ((int) c[j++]) & 0xff; - int b = ((int) c[j++]) & 0xff; - tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; - } - } - return tab; - } - - /** - * Main file parser. Reads GIF content blocks. - */ - protected void readContents() { - // read GIF file content blocks - boolean done = false; - while (!(done || err())) { - int code = read(); - switch (code) { - - case 0x2C : // image separator - readImage(); - break; - - case 0x21 : // extension - code = read(); - switch (code) { - case 0xf9 : // graphics control extension - readGraphicControlExt(); - break; - - case 0xff : // application extension - readBlock(); - String app = ""; - for (int i = 0; i < 11; i++) { - app += (char) block[i]; - } - if (app.equals("NETSCAPE2.0")) { - readNetscapeExt(); - } - else - skip(); // don't care - break; - - default : // uninteresting extension - skip(); - } - break; - - case 0x3b : // terminator - done = true; - break; - - case 0x00 : // bad byte, but keep going and see what happens - break; - - default : - status = STATUS_FORMAT_ERROR; - } - } - } - - /** - * Reads Graphics Control Extension values - */ - protected void readGraphicControlExt() { - read(); // block size - int packed = read(); // packed fields - dispose = (packed & 0x1c) >> 2; // disposal method - if (dispose == 0) { - dispose = 1; // elect to keep old image if discretionary - } - transparency = (packed & 1) != 0; - delay = readShort() * 10; // delay in milliseconds - transIndex = read(); // transparent color index - read(); // block terminator - } - - /** - * Reads GIF file header information. - */ - protected void readHeader() { - String id = ""; - for (int i = 0; i < 6; i++) { - id += (char) read(); - } - if (!id.startsWith("GIF")) { - status = STATUS_FORMAT_ERROR; - return; - } - - readLSD(); - if (gctFlag && !err()) { - gct = readColorTable(gctSize); - bgColor = gct[bgIndex]; - } - } - - /** - * Reads next frame image - */ - protected void readImage() { - ix = readShort(); // (sub)image position & size - iy = readShort(); - iw = readShort(); - ih = readShort(); - - int packed = read(); - lctFlag = (packed & 0x80) != 0; // 1 - local color table flag - interlace = (packed & 0x40) != 0; // 2 - interlace flag - // 3 - sort flag - // 4-5 - reserved - lctSize = 2 << (packed & 7); // 6-8 - local color table size - - if (lctFlag) { - lct = readColorTable(lctSize); // read table - act = lct; // make local table active - } else { - act = gct; // make global table active - if (bgIndex == transIndex) - bgColor = 0; - } - int save = 0; - if (transparency) { - save = act[transIndex]; - act[transIndex] = 0; // set transparent color if specified - } - - if (act == null) { - status = STATUS_FORMAT_ERROR; // no color table defined - } - - if (err()) return; - - decodeImageData(); // decode pixel data - skip(); - - if (err()) return; - - frameCount++; - - // create new image to receive frame data - image = - new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); - - setPixels(); // transfer pixel data to image - - frames.add(new GifFrame(image, delay)); // add image to frame list - - if (transparency) { - act[transIndex] = save; - } - resetFrame(); - - } - - /** - * Reads Logical Screen Descriptor - */ - protected void readLSD() { - - // logical screen size - width = readShort(); - height = readShort(); - - // packed fields - int packed = read(); - gctFlag = (packed & 0x80) != 0; // 1 : global color table flag - // 2-4 : color resolution - // 5 : gct sort flag - gctSize = 2 << (packed & 7); // 6-8 : gct size - - bgIndex = read(); // background color index - pixelAspect = read(); // pixel aspect ratio - } - - /** - * Reads Netscape extenstion to obtain iteration count - */ - protected void readNetscapeExt() { - do { - readBlock(); - if (block[0] == 1) { - // loop count sub-block - int b1 = ((int) block[1]) & 0xff; - int b2 = ((int) block[2]) & 0xff; - loopCount = (b2 << 8) | b1; - } - } while ((blockSize > 0) && !err()); - } - - /** - * Reads next 16-bit value, LSB first - */ - protected int readShort() { - // read 16-bit value, LSB first - return read() | (read() << 8); - } - - /** - * Resets frame state for reading next image. - */ - protected void resetFrame() { - lastDispose = dispose; - lastRect = new Rectangle(ix, iy, iw, ih); - lastImage = image; - lastBgColor = bgColor; - lct = null; - } - - /** - * Skips variable length blocks up to and including - * next zero length block. - */ - protected void skip() { - do { - readBlock(); - } while ((blockSize > 0) && !err()); - } -} +package io.korhner.asciimg.utils; +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +/** + * Class GifDecoder - Decodes a GIF file into one or more frames. + *
+ * Example:
+ *    GifDecoder d = new GifDecoder();
+ *    d.read("sample.gif");
+ *    int n = d.getFrameCount();
+ *    for (int i = 0; i < n; i++) {
+ *       BufferedImage frame = d.getFrame(i);  // frame i
+ *       int t = d.getDelay(i);  // display duration of frame in milliseconds
+ *       // do something with frame
+ *    }
+ * 
+ * No copyright asserted on the source code of this class. May be used for + * any purpose, however, refer to the Unisys LZW patent for any additional + * restrictions. Please forward any corrections to questions at fmsware.com. + * + * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick. + * @version 1.03 November 2003 + * + */ + +public class GifDecoder { + + /** + * File read status: No errors. + */ + public static final int STATUS_OK = 0; + + /** + * File read status: Error decoding file (may be partially decoded) + */ + public static final int STATUS_FORMAT_ERROR = 1; + + /** + * File read status: Unable to open source. + */ + public static final int STATUS_OPEN_ERROR = 2; + + protected BufferedInputStream in; + protected int status; + + protected int width; // full image width + protected int height; // full image height + protected boolean gctFlag; // global color table used + protected int gctSize; // size of global color table + protected int loopCount = 1; // iterations; 0 = repeat forever + + protected int[] gct; // global color table + protected int[] lct; // local color table + protected int[] act; // active color table + + protected int bgIndex; // background color index + protected int bgColor; // background color + protected int lastBgColor; // previous bg color + protected int pixelAspect; // pixel aspect ratio + + protected boolean lctFlag; // local color table flag + protected boolean interlace; // interlace flag + protected int lctSize; // local color table size + + protected int ix, iy, iw, ih; // current image rectangle + protected Rectangle lastRect; // last image rect + protected BufferedImage image; // current frame + protected BufferedImage lastImage; // previous frame + + protected byte[] block = new byte[256]; // current data block + protected int blockSize = 0; // block size + + // last graphic control extension info + protected int dispose = 0; + // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev + protected int lastDispose = 0; + protected boolean transparency = false; // use transparent color + protected int delay = 0; // delay in milliseconds + protected int transIndex; // transparent color index + + protected static final int MaxStackSize = 4096; + // max decoder pixel stack size + + // LZW decoder working arrays + protected short[] prefix; + protected byte[] suffix; + protected byte[] pixelStack; + protected byte[] pixels; + + protected List frames; // frames read from current file + protected int frameCount; + + static class GifFrame { + public GifFrame(BufferedImage im, int del) { + image = im; + delay = del; + } + public BufferedImage image; + public int delay; + } + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame + * @return delay in milliseconds + */ + public int getDelay(int n) { + // + delay = -1; + if ((n >= 0) && (n < frameCount)) { + delay = ((GifFrame) frames.get(n)).delay; + } + return delay; + } + + /** + * Gets the number of frames read from file. + * @return frame count + */ + public int getFrameCount() { + return frameCount; + } + + /** + * Gets the first (or only) image read. + * + * @return BufferedImage containing first frame, or null if none. + */ + public BufferedImage getImage() { + return getFrame(0); + } + + /** + * Gets the "Netscape" iteration count, if any. + * A count of 0 means repeat indefinitiely. + * + * @return iteration count if one was specified, else 1. + */ + public int getLoopCount() { + return loopCount; + } + + /** + * Creates new frame image from current data (and previous + * frames as specified by their disposition codes). + */ + protected void setPixels() { + // expose destination image's pixels as int array + int[] dest = + ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + + // fill in starting image contents based on last image's dispose code + if (lastDispose > 0) { + if (lastDispose == 3) { + // use image before last + int n = frameCount - 2; + if (n > 0) { + lastImage = getFrame(n - 1); + } else { + lastImage = null; + } + } + + if (lastImage != null) { + int[] prev = + ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData(); + System.arraycopy(prev, 0, dest, 0, width * height); + // copy pixels + + if (lastDispose == 2) { + // fill last image rect area with background color + Graphics2D g = image.createGraphics(); + Color c = null; + if (transparency) { + c = new Color(0, 0, 0, 0); // assume background is transparent + } else { + c = new Color(lastBgColor); // use given background color + } + g.setColor(c); + g.setComposite(AlphaComposite.Src); // replace area + g.fill(lastRect); + g.dispose(); + } + } + } + + // copy each source line to the appropriate place in the destination + int pass = 1; + int inc = 8; + int iline = 0; + for (int i = 0; i < ih; i++) { + int line = i; + if (interlace) { + if (iline >= ih) { + pass++; + switch (pass) { + case 2 : + iline = 4; + break; + case 3 : + iline = 2; + inc = 4; + break; + case 4 : + iline = 1; + inc = 2; + } + } + line = iline; + iline += inc; + } + line += iy; + if (line < height) { + int k = line * width; + int dx = k + ix; // start of line in dest + int dlim = dx + iw; // end of dest line + if ((k + width) < dlim) { + dlim = k + width; // past dest edge + } + int sx = i * iw; // start of line in source + while (dx < dlim) { + // map color and insert in destination + int index = ((int) pixels[sx++]) & 0xff; + int c = act[index]; + if (c != 0) { + dest[dx] = c; + } + dx++; + } + } + } + } + + /** + * Gets the image contents of frame n. + * + * @return BufferedImage representation of frame, or null if n is invalid. + */ + public BufferedImage getFrame(int n) { + BufferedImage im = null; + if ((n >= 0) && (n < frameCount)) { + im = ((GifFrame) frames.get(n)).image; + } + return im; + } + + /** + * Gets image size. + * + * @return GIF image dimensions + */ + public Dimension getFrameSize() { + return new Dimension(width, height); + } + + /** + * Reads GIF image from stream + * + * @param BufferedInputStream containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(BufferedInputStream is) { + init(); + if (is != null) { + in = is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + try { + is.close(); + } catch (IOException e) { + } + return status; + } + + /** + * Reads GIF image from stream + * + * @param InputStream containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(InputStream is) { + init(); + if (is != null) { + if (!(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + in = (BufferedInputStream) is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + try { + is.close(); + } catch (IOException e) { + } + return status; + } + + /** + * Reads GIF file from specified file/URL source + * (URL assumed if name contains ":/" or "file:") + * + * @param name String containing source + * @return read status code (0 = no errors) + */ + public int read(String name) { + status = STATUS_OK; + try { + name = name.trim().toLowerCase(); + if ((name.indexOf("file:") >= 0) || + (name.indexOf(":/") > 0)) { + URL url = new URL(name); + in = new BufferedInputStream(url.openStream()); + } else { + in = new BufferedInputStream(new FileInputStream(name)); + } + status = read(in); + } catch (IOException e) { + status = STATUS_OPEN_ERROR; + } + + return status; + } + + /** + * Decodes LZW image data into pixel array. + * Adapted from John Cristy's ImageMagick. + */ + protected void decodeImageData() { + int NullCode = -1; + int npix = iw * ih; + int available, + clear, + code_mask, + code_size, + end_of_information, + in_code, + old_code, + bits, + code, + count, + i, + datum, + data_size, + first, + top, + bi, + pi; + + if ((pixels == null) || (pixels.length < npix)) { + pixels = new byte[npix]; // allocate new pixel array + } + if (prefix == null) prefix = new short[MaxStackSize]; + if (suffix == null) suffix = new byte[MaxStackSize]; + if (pixelStack == null) pixelStack = new byte[MaxStackSize + 1]; + + // Initialize GIF data stream decoder. + + data_size = read(); + clear = 1 << data_size; + end_of_information = clear + 1; + available = clear + 2; + old_code = NullCode; + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + for (code = 0; code < clear; code++) { + prefix[code] = 0; + suffix[code] = (byte) code; + } + + // Decode GIF pixel stream. + + datum = bits = count = first = top = pi = bi = 0; + + for (i = 0; i < npix;) { + if (top == 0) { + if (bits < code_size) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) + break; + bi = 0; + } + datum += (((int) block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + continue; + } + + // Get the next code. + + code = datum & code_mask; + datum >>= code_size; + bits -= code_size; + + // Interpret the code + + if ((code > available) || (code == end_of_information)) + break; + if (code == clear) { + // Reset decoder. + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + available = clear + 2; + old_code = NullCode; + continue; + } + if (old_code == NullCode) { + pixelStack[top++] = suffix[code]; + old_code = code; + first = code; + continue; + } + in_code = code; + if (code == available) { + pixelStack[top++] = (byte) first; + code = old_code; + } + while (code > clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = ((int) suffix[code]) & 0xff; + + // Add a new string to the string table, + + if (available >= MaxStackSize) + break; + pixelStack[top++] = (byte) first; + prefix[available] = (short) old_code; + suffix[available] = (byte) first; + available++; + if (((available & code_mask) == 0) + && (available < MaxStackSize)) { + code_size++; + code_mask += available; + } + old_code = in_code; + } + + // Pop a pixel off the pixel stack. + + top--; + pixels[pi++] = pixelStack[top]; + i++; + } + + for (i = pi; i < npix; i++) { + pixels[i] = 0; // clear missing pixels + } + + } + + /** + * Returns true if an error was encountered during reading/decoding + */ + protected boolean err() { + return status != STATUS_OK; + } + + /** + * Initializes or re-initializes reader + */ + protected void init() { + status = STATUS_OK; + frameCount = 0; + frames = new ArrayList<>(); + gct = null; + lct = null; + } + + /** + * Reads a single byte from the input stream. + */ + protected int read() { + int curByte = 0; + try { + curByte = in.read(); + } catch (IOException e) { + status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" + */ + protected int readBlock() { + blockSize = read(); + int n = 0; + if (blockSize > 0) { + try { + int count = 0; + while (n < blockSize) { + count = in.read(block, n, blockSize - n); + if (count == -1) + break; + n += count; + } + } catch (IOException e) { + } + + if (n < blockSize) { + status = STATUS_FORMAT_ERROR; + } + } + return n; + } + + /** + * Reads color table as 256 RGB integer values + * + * @param ncolors int number of colors to read + * @return int array containing 256 colors (packed ARGB with full alpha) + */ + protected int[] readColorTable(int ncolors) { + int nbytes = 3 * ncolors; + int[] tab = null; + byte[] c = new byte[nbytes]; + int n = 0; + try { + n = in.read(c); + } catch (IOException e) { + } + if (n < nbytes) { + status = STATUS_FORMAT_ERROR; + } else { + tab = new int[256]; // max size to avoid bounds checks + int i = 0; + int j = 0; + while (i < ncolors) { + int r = ((int) c[j++]) & 0xff; + int g = ((int) c[j++]) & 0xff; + int b = ((int) c[j++]) & 0xff; + tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } + return tab; + } + + /** + * Main file parser. Reads GIF content blocks. + */ + protected void readContents() { + // read GIF file content blocks + boolean done = false; + while (!(done || err())) { + int code = read(); + switch (code) { + + case 0x2C : // image separator + readImage(); + break; + + case 0x21 : // extension + code = read(); + switch (code) { + case 0xf9 : // graphics control extension + readGraphicControlExt(); + break; + + case 0xff : // application extension + readBlock(); + String app = ""; + for (int i = 0; i < 11; i++) { + app += (char) block[i]; + } + if (app.equals("NETSCAPE2.0")) { + readNetscapeExt(); + } + else + skip(); // don't care + break; + + default : // uninteresting extension + skip(); + } + break; + + case 0x3b : // terminator + done = true; + break; + + case 0x00 : // bad byte, but keep going and see what happens + break; + + default : + status = STATUS_FORMAT_ERROR; + } + } + } + + /** + * Reads Graphics Control Extension values + */ + protected void readGraphicControlExt() { + read(); // block size + int packed = read(); // packed fields + dispose = (packed & 0x1c) >> 2; // disposal method + if (dispose == 0) { + dispose = 1; // elect to keep old image if discretionary + } + transparency = (packed & 1) != 0; + delay = readShort() * 10; // delay in milliseconds + transIndex = read(); // transparent color index + read(); // block terminator + } + + /** + * Reads GIF file header information. + */ + protected void readHeader() { + String id = ""; + for (int i = 0; i < 6; i++) { + id += (char) read(); + } + if (!id.startsWith("GIF")) { + status = STATUS_FORMAT_ERROR; + return; + } + + readLSD(); + if (gctFlag && !err()) { + gct = readColorTable(gctSize); + bgColor = gct[bgIndex]; + } + } + + /** + * Reads next frame image + */ + protected void readImage() { + ix = readShort(); // (sub)image position & size + iy = readShort(); + iw = readShort(); + ih = readShort(); + + int packed = read(); + lctFlag = (packed & 0x80) != 0; // 1 - local color table flag + interlace = (packed & 0x40) != 0; // 2 - interlace flag + // 3 - sort flag + // 4-5 - reserved + lctSize = 2 << (packed & 7); // 6-8 - local color table size + + if (lctFlag) { + lct = readColorTable(lctSize); // read table + act = lct; // make local table active + } else { + act = gct; // make global table active + if (bgIndex == transIndex) + bgColor = 0; + } + int save = 0; + if (transparency) { + save = act[transIndex]; + act[transIndex] = 0; // set transparent color if specified + } + + if (act == null) { + status = STATUS_FORMAT_ERROR; // no color table defined + } + + if (err()) return; + + decodeImageData(); // decode pixel data + skip(); + + if (err()) return; + + frameCount++; + + // create new image to receive frame data + image = + new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + + setPixels(); // transfer pixel data to image + + frames.add(new GifFrame(image, delay)); // add image to frame list + + if (transparency) { + act[transIndex] = save; + } + resetFrame(); + + } + + /** + * Reads Logical Screen Descriptor + */ + protected void readLSD() { + + // logical screen size + width = readShort(); + height = readShort(); + + // packed fields + int packed = read(); + gctFlag = (packed & 0x80) != 0; // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 << (packed & 7); // 6-8 : gct size + + bgIndex = read(); // background color index + pixelAspect = read(); // pixel aspect ratio + } + + /** + * Reads Netscape extenstion to obtain iteration count + */ + protected void readNetscapeExt() { + do { + readBlock(); + if (block[0] == 1) { + // loop count sub-block + int b1 = ((int) block[1]) & 0xff; + int b2 = ((int) block[2]) & 0xff; + loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + /** + * Reads next 16-bit value, LSB first + */ + protected int readShort() { + // read 16-bit value, LSB first + return read() | (read() << 8); + } + + /** + * Resets frame state for reading next image. + */ + protected void resetFrame() { + lastDispose = dispose; + lastRect = new Rectangle(ix, iy, iw, ih); + lastImage = image; + lastBgColor = bgColor; + lct = null; + } + + /** + * Skips variable length blocks up to and including + * next zero length block. + */ + protected void skip() { + do { + readBlock(); + } while ((blockSize > 0) && !err()); + } +} diff --git a/src/io/korhner/asciimg/utils/LZWEncoder.java b/src/main/java/io/korhner/asciimg/utils/LZWEncoder.java similarity index 96% rename from src/io/korhner/asciimg/utils/LZWEncoder.java rename to src/main/java/io/korhner/asciimg/utils/LZWEncoder.java index 3ee2f14..3652ec8 100644 --- a/src/io/korhner/asciimg/utils/LZWEncoder.java +++ b/src/main/java/io/korhner/asciimg/utils/LZWEncoder.java @@ -1,302 +1,302 @@ -package io.korhner.asciimg.utils; -import java.io.OutputStream; -import java.io.IOException; - -//============================================================================== -// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. -// K Weiner 12/00 - -class LZWEncoder { - - private static final int EOF = -1; - - private int imgW, imgH; - private byte[] pixAry; - private int initCodeSize; - private int remaining; - private int curPixel; - - // GIFCOMPR.C - GIF Image compression routines - // - // Lempel-Ziv compression based on 'compress'. GIF modifications by - // David Rowley (mgardi@watdcsu.waterloo.edu) - - // General DEFINEs - - static final int BITS = 12; - - static final int HSIZE = 5003; // 80% occupancy - - // GIF Image compression - modified 'compress' - // - // Based on: compress.c - File compression ala IEEE Computer, June 1984. - // - // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) - // Jim McKie (decvax!mcvax!jim) - // Steve Davies (decvax!vax135!petsd!peora!srd) - // Ken Turkowski (decvax!decwrl!turtlevax!ken) - // James A. Woods (decvax!ihnp4!ames!jaw) - // Joe Orost (decvax!vax135!petsd!joe) - - int n_bits; // number of bits/code - int maxbits = BITS; // user settable max # bits/code - int maxcode; // maximum code, given n_bits - int maxmaxcode = 1 << BITS; // should NEVER generate this code - - int[] htab = new int[HSIZE]; - int[] codetab = new int[HSIZE]; - - int hsize = HSIZE; // for dynamic table sizing - - int free_ent = 0; // first unused entry - - // block compression parameters -- after all codes are used up, - // and compression rate changes, start over. - boolean clear_flg = false; - - // Algorithm: use open addressing double hashing (no chaining) on the - // prefix code / next character combination. We do a variant of Knuth's - // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime - // secondary probe. Here, the modular division first probe is gives way - // to a faster exclusive-or manipulation. Also do block compression with - // an adaptive reset, whereby the code table is cleared when the compression - // ratio decreases, but after the table fills. The variable-length output - // codes are re-sized at this point, and a special CLEAR code is generated - // for the decompressor. Late addition: construct the table according to - // file size for noticeable speed improvement on small files. Please direct - // questions about this implementation to ames!jaw. - - int g_init_bits; - - int ClearCode; - int EOFCode; - - // output - // - // Output the given code. - // Inputs: - // code: A n_bits-bit integer. If == -1, then EOF. This assumes - // that n_bits =< wordsize - 1. - // Outputs: - // Outputs code to the file. - // Assumptions: - // Chars are 8 bits long. - // Algorithm: - // Maintain a BITS character long buffer (so that 8 codes will - // fit in it exactly). Use the VAX insv instruction to insert each - // code in turn. When the buffer fills up empty it and start over. - - int cur_accum = 0; - int cur_bits = 0; - - int masks[] = - { - 0x0000, - 0x0001, - 0x0003, - 0x0007, - 0x000F, - 0x001F, - 0x003F, - 0x007F, - 0x00FF, - 0x01FF, - 0x03FF, - 0x07FF, - 0x0FFF, - 0x1FFF, - 0x3FFF, - 0x7FFF, - 0xFFFF }; - - // Number of characters so far in this 'packet' - int a_count; - - // Define the storage for the packet accumulator - byte[] accum = new byte[256]; - - //---------------------------------------------------------------------------- - LZWEncoder(int width, int height, byte[] pixels, int color_depth) { - imgW = width; - imgH = height; - pixAry = pixels; - initCodeSize = Math.max(2, color_depth); - } - - // Add a character to the end of the current packet, and if it is 254 - // characters, flush the packet to disk. - void char_out(byte c, OutputStream outs) throws IOException { - accum[a_count++] = c; - if (a_count >= 254) - flush_char(outs); - } - - // Clear out the hash table - - // table clear for block compress - void cl_block(OutputStream outs) throws IOException { - cl_hash(hsize); - free_ent = ClearCode + 2; - clear_flg = true; - - output(ClearCode, outs); - } - - // reset code table - void cl_hash(int hsize) { - for (int i = 0; i < hsize; ++i) - htab[i] = -1; - } - - void compress(int init_bits, OutputStream outs) throws IOException { - int fcode; - int i /* = 0 */; - int c; - int ent; - int disp; - int hsize_reg; - int hshift; - - // Set up the globals: g_init_bits - initial number of bits - g_init_bits = init_bits; - - // Set up the necessary values - clear_flg = false; - n_bits = g_init_bits; - maxcode = MAXCODE(n_bits); - - ClearCode = 1 << (init_bits - 1); - EOFCode = ClearCode + 1; - free_ent = ClearCode + 2; - - a_count = 0; // clear packet - - ent = nextPixel(); - - hshift = 0; - for (fcode = hsize; fcode < 65536; fcode *= 2) - ++hshift; - hshift = 8 - hshift; // set hash code range bound - - hsize_reg = hsize; - cl_hash(hsize_reg); // clear hash table - - output(ClearCode, outs); - - outer_loop : while ((c = nextPixel()) != EOF) { - fcode = (c << maxbits) + ent; - i = (c << hshift) ^ ent; // xor hashing - - if (htab[i] == fcode) { - ent = codetab[i]; - continue; - } else if (htab[i] >= 0) // non-empty slot - { - disp = hsize_reg - i; // secondary hash (after G. Knott) - if (i == 0) - disp = 1; - do { - if ((i -= disp) < 0) - i += hsize_reg; - - if (htab[i] == fcode) { - ent = codetab[i]; - continue outer_loop; - } - } while (htab[i] >= 0); - } - output(ent, outs); - ent = c; - if (free_ent < maxmaxcode) { - codetab[i] = free_ent++; // code -> hashtable - htab[i] = fcode; - } else - cl_block(outs); - } - // Put out the final code. - output(ent, outs); - output(EOFCode, outs); - } - - //---------------------------------------------------------------------------- - void encode(OutputStream os) throws IOException { - os.write(initCodeSize); // write "initial code size" byte - - remaining = imgW * imgH; // reset navigation variables - curPixel = 0; - - compress(initCodeSize + 1, os); // compress and write the pixel data - - os.write(0); // write block terminator - } - - // Flush the packet to disk, and reset the accumulator - void flush_char(OutputStream outs) throws IOException { - if (a_count > 0) { - outs.write(a_count); - outs.write(accum, 0, a_count); - a_count = 0; - } - } - - final int MAXCODE(int n_bits) { - return (1 << n_bits) - 1; - } - - //---------------------------------------------------------------------------- - // Return the next pixel from the image - //---------------------------------------------------------------------------- - private int nextPixel() { - if (remaining == 0) - return EOF; - - --remaining; - - byte pix = pixAry[curPixel++]; - - return pix & 0xff; - } - - void output(int code, OutputStream outs) throws IOException { - cur_accum &= masks[cur_bits]; - - if (cur_bits > 0) - cur_accum |= (code << cur_bits); - else - cur_accum = code; - - cur_bits += n_bits; - - while (cur_bits >= 8) { - char_out((byte) (cur_accum & 0xff), outs); - cur_accum >>= 8; - cur_bits -= 8; - } - - // If the next entry is going to be too big for the code size, - // then increase it, if possible. - if (free_ent > maxcode || clear_flg) { - if (clear_flg) { - maxcode = MAXCODE(n_bits = g_init_bits); - clear_flg = false; - } else { - ++n_bits; - if (n_bits == maxbits) - maxcode = maxmaxcode; - else - maxcode = MAXCODE(n_bits); - } - } - - if (code == EOFCode) { - // At EOF, write the rest of the buffer. - while (cur_bits > 0) { - char_out((byte) (cur_accum & 0xff), outs); - cur_accum >>= 8; - cur_bits -= 8; - } - - flush_char(outs); - } - } -} +package io.korhner.asciimg.utils; +import java.io.OutputStream; +import java.io.IOException; + +//============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 + +class LZWEncoder { + + private static final int EOF = -1; + + private int imgW, imgH; + private byte[] pixAry; + private int initCodeSize; + private int remaining; + private int curPixel; + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + static final int BITS = 12; + + static final int HSIZE = 5003; // 80% occupancy + + // GIF Image compression - modified 'compress' + // + // Based on: compress.c - File compression ala IEEE Computer, June 1984. + // + // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + // Jim McKie (decvax!mcvax!jim) + // Steve Davies (decvax!vax135!petsd!peora!srd) + // Ken Turkowski (decvax!decwrl!turtlevax!ken) + // James A. Woods (decvax!ihnp4!ames!jaw) + // Joe Orost (decvax!vax135!petsd!joe) + + int n_bits; // number of bits/code + int maxbits = BITS; // user settable max # bits/code + int maxcode; // maximum code, given n_bits + int maxmaxcode = 1 << BITS; // should NEVER generate this code + + int[] htab = new int[HSIZE]; + int[] codetab = new int[HSIZE]; + + int hsize = HSIZE; // for dynamic table sizing + + int free_ent = 0; // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + boolean clear_flg = false; + + // Algorithm: use open addressing double hashing (no chaining) on the + // prefix code / next character combination. We do a variant of Knuth's + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + // secondary probe. Here, the modular division first probe is gives way + // to a faster exclusive-or manipulation. Also do block compression with + // an adaptive reset, whereby the code table is cleared when the compression + // ratio decreases, but after the table fills. The variable-length output + // codes are re-sized at this point, and a special CLEAR code is generated + // for the decompressor. Late addition: construct the table according to + // file size for noticeable speed improvement on small files. Please direct + // questions about this implementation to ames!jaw. + + int g_init_bits; + + int ClearCode; + int EOFCode; + + // output + // + // Output the given code. + // Inputs: + // code: A n_bits-bit integer. If == -1, then EOF. This assumes + // that n_bits =< wordsize - 1. + // Outputs: + // Outputs code to the file. + // Assumptions: + // Chars are 8 bits long. + // Algorithm: + // Maintain a BITS character long buffer (so that 8 codes will + // fit in it exactly). Use the VAX insv instruction to insert each + // code in turn. When the buffer fills up empty it and start over. + + int cur_accum = 0; + int cur_bits = 0; + + int masks[] = + { + 0x0000, + 0x0001, + 0x0003, + 0x0007, + 0x000F, + 0x001F, + 0x003F, + 0x007F, + 0x00FF, + 0x01FF, + 0x03FF, + 0x07FF, + 0x0FFF, + 0x1FFF, + 0x3FFF, + 0x7FFF, + 0xFFFF }; + + // Number of characters so far in this 'packet' + int a_count; + + // Define the storage for the packet accumulator + byte[] accum = new byte[256]; + + //---------------------------------------------------------------------------- + LZWEncoder(int width, int height, byte[] pixels, int color_depth) { + imgW = width; + imgH = height; + pixAry = pixels; + initCodeSize = Math.max(2, color_depth); + } + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + void char_out(byte c, OutputStream outs) throws IOException { + accum[a_count++] = c; + if (a_count >= 254) + flush_char(outs); + } + + // Clear out the hash table + + // table clear for block compress + void cl_block(OutputStream outs) throws IOException { + cl_hash(hsize); + free_ent = ClearCode + 2; + clear_flg = true; + + output(ClearCode, outs); + } + + // reset code table + void cl_hash(int hsize) { + for (int i = 0; i < hsize; ++i) + htab[i] = -1; + } + + void compress(int init_bits, OutputStream outs) throws IOException { + int fcode; + int i /* = 0 */; + int c; + int ent; + int disp; + int hsize_reg; + int hshift; + + // Set up the globals: g_init_bits - initial number of bits + g_init_bits = init_bits; + + // Set up the necessary values + clear_flg = false; + n_bits = g_init_bits; + maxcode = MAXCODE(n_bits); + + ClearCode = 1 << (init_bits - 1); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + a_count = 0; // clear packet + + ent = nextPixel(); + + hshift = 0; + for (fcode = hsize; fcode < 65536; fcode *= 2) + ++hshift; + hshift = 8 - hshift; // set hash code range bound + + hsize_reg = hsize; + cl_hash(hsize_reg); // clear hash table + + output(ClearCode, outs); + + outer_loop : while ((c = nextPixel()) != EOF) { + fcode = (c << maxbits) + ent; + i = (c << hshift) ^ ent; // xor hashing + + if (htab[i] == fcode) { + ent = codetab[i]; + continue; + } else if (htab[i] >= 0) // non-empty slot + { + disp = hsize_reg - i; // secondary hash (after G. Knott) + if (i == 0) + disp = 1; + do { + if ((i -= disp) < 0) + i += hsize_reg; + + if (htab[i] == fcode) { + ent = codetab[i]; + continue outer_loop; + } + } while (htab[i] >= 0); + } + output(ent, outs); + ent = c; + if (free_ent < maxmaxcode) { + codetab[i] = free_ent++; // code -> hashtable + htab[i] = fcode; + } else + cl_block(outs); + } + // Put out the final code. + output(ent, outs); + output(EOFCode, outs); + } + + //---------------------------------------------------------------------------- + void encode(OutputStream os) throws IOException { + os.write(initCodeSize); // write "initial code size" byte + + remaining = imgW * imgH; // reset navigation variables + curPixel = 0; + + compress(initCodeSize + 1, os); // compress and write the pixel data + + os.write(0); // write block terminator + } + + // Flush the packet to disk, and reset the accumulator + void flush_char(OutputStream outs) throws IOException { + if (a_count > 0) { + outs.write(a_count); + outs.write(accum, 0, a_count); + a_count = 0; + } + } + + final int MAXCODE(int n_bits) { + return (1 << n_bits) - 1; + } + + //---------------------------------------------------------------------------- + // Return the next pixel from the image + //---------------------------------------------------------------------------- + private int nextPixel() { + if (remaining == 0) + return EOF; + + --remaining; + + byte pix = pixAry[curPixel++]; + + return pix & 0xff; + } + + void output(int code, OutputStream outs) throws IOException { + cur_accum &= masks[cur_bits]; + + if (cur_bits > 0) + cur_accum |= (code << cur_bits); + else + cur_accum = code; + + cur_bits += n_bits; + + while (cur_bits >= 8) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if (free_ent > maxcode || clear_flg) { + if (clear_flg) { + maxcode = MAXCODE(n_bits = g_init_bits); + clear_flg = false; + } else { + ++n_bits; + if (n_bits == maxbits) + maxcode = maxmaxcode; + else + maxcode = MAXCODE(n_bits); + } + } + + if (code == EOFCode) { + // At EOF, write the rest of the buffer. + while (cur_bits > 0) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + flush_char(outs); + } + } +} diff --git a/src/io/korhner/asciimg/utils/NeuQuant.java b/src/main/java/io/korhner/asciimg/utils/NeuQuant.java similarity index 96% rename from src/io/korhner/asciimg/utils/NeuQuant.java rename to src/main/java/io/korhner/asciimg/utils/NeuQuant.java index c247d3e..6db9164 100644 --- a/src/io/korhner/asciimg/utils/NeuQuant.java +++ b/src/main/java/io/korhner/asciimg/utils/NeuQuant.java @@ -1,459 +1,459 @@ -/* NeuQuant Neural-Net Quantization Algorithm - * ------------------------------------------ - * - * Copyright (c) 1994 Anthony Dekker - * - * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. - * See "Kohonen neural networks for optimal colour quantization" - * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. - * for a discussion of the algorithm. - * - * Any party obtaining a copy of these files from the author, directly or - * indirectly, is granted, free of charge, a full and unrestricted irrevocable, - * world-wide, paid up, royalty-free, nonexclusive right and license to deal - * in this software and documentation files (the "Software"), including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons who receive - * copies from any such party to do so, with the only requirement being - * that this copyright notice remain intact. - */ - -// Ported to Java 12/00 K Weiner - -package io.korhner.asciimg.utils; - -public class NeuQuant { - - protected static final int netsize = 256; /* number of colours used */ - - /* four primes near 500 - assume no image has a length so large */ - /* that it is divisible by all four primes */ - protected static final int prime1 = 499; - protected static final int prime2 = 491; - protected static final int prime3 = 487; - protected static final int prime4 = 503; - - protected static final int minpicturebytes = (3 * prime4); - /* minimum size for input image */ - - /* Program Skeleton - ---------------- - [select samplefac in range 1..30] - [read image from input file] - pic = (unsigned char*) malloc(3*width*height); - initnet(pic,3*width*height,samplefac); - learn(); - unbiasnet(); - [write output image header, using writecolourmap(f)] - inxbuild(); - write output image using inxsearch(b,g,r) */ - - /* Network Definitions - ------------------- */ - - protected static final int maxnetpos = (netsize - 1); - protected static final int netbiasshift = 4; /* bias for colour values */ - protected static final int ncycles = 100; /* no. of learning cycles */ - - /* defs for freq and bias */ - protected static final int intbiasshift = 16; /* bias for fractions */ - protected static final int intbias = (((int) 1) << intbiasshift); - protected static final int gammashift = 10; /* gamma = 1024 */ - protected static final int gamma = (((int) 1) << gammashift); - protected static final int betashift = 10; - protected static final int beta = (intbias >> betashift); /* beta = 1/1024 */ - protected static final int betagamma = - (intbias << (gammashift - betashift)); - - /* defs for decreasing radius factor */ - protected static final int initrad = (netsize >> 3); /* for 256 cols, radius starts */ - protected static final int radiusbiasshift = 6; /* at 32.0 biased by 6 bits */ - protected static final int radiusbias = (((int) 1) << radiusbiasshift); - protected static final int initradius = (initrad * radiusbias); /* and decreases by a */ - protected static final int radiusdec = 30; /* factor of 1/30 each cycle */ - - /* defs for decreasing alpha factor */ - protected static final int alphabiasshift = 10; /* alpha starts at 1.0 */ - protected static final int initalpha = (((int) 1) << alphabiasshift); - - protected int alphadec; /* biased by 10 bits */ - - /* radbias and alpharadbias used for radpower calculation */ - protected static final int radbiasshift = 8; - protected static final int radbias = (((int) 1) << radbiasshift); - protected static final int alpharadbshift = (alphabiasshift + radbiasshift); - protected static final int alpharadbias = (((int) 1) << alpharadbshift); - - /* Types and Global Variables - -------------------------- */ - - protected byte[] thepicture; /* the input image itself */ - protected int lengthcount; /* lengthcount = H*W*3 */ - - protected int samplefac; /* sampling factor 1..30 */ - - // typedef int pixel[4]; /* BGRc */ - protected int[][] network; /* the network itself - [netsize][4] */ - - protected int[] netindex = new int[256]; - /* for network lookup - really 256 */ - - protected int[] bias = new int[netsize]; - /* bias and freq arrays for learning */ - protected int[] freq = new int[netsize]; - protected int[] radpower = new int[initrad]; - /* radpower for precomputation */ - - /* Initialise network in range (0,0,0) to (255,255,255) and set parameters - ----------------------------------------------------------------------- */ - public NeuQuant(byte[] thepic, int len, int sample) { - - int i; - int[] p; - - thepicture = thepic; - lengthcount = len; - samplefac = sample; - - network = new int[netsize][]; - for (i = 0; i < netsize; i++) { - network[i] = new int[4]; - p = network[i]; - p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize; - freq[i] = intbias / netsize; /* 1/netsize */ - bias[i] = 0; - } - } - - public byte[] colorMap() { - byte[] map = new byte[3 * netsize]; - int[] index = new int[netsize]; - for (int i = 0; i < netsize; i++) - index[network[i][3]] = i; - int k = 0; - for (int i = 0; i < netsize; i++) { - int j = index[i]; - map[k++] = (byte) (network[j][0]); - map[k++] = (byte) (network[j][1]); - map[k++] = (byte) (network[j][2]); - } - return map; - } - - /* Insertion sort of network and building of netindex[0..255] (to do after unbias) - ------------------------------------------------------------------------------- */ - public void inxbuild() { - - int i, j, smallpos, smallval; - int[] p; - int[] q; - int previouscol, startpos; - - previouscol = 0; - startpos = 0; - for (i = 0; i < netsize; i++) { - p = network[i]; - smallpos = i; - smallval = p[1]; /* index on g */ - /* find smallest in i..netsize-1 */ - for (j = i + 1; j < netsize; j++) { - q = network[j]; - if (q[1] < smallval) { /* index on g */ - smallpos = j; - smallval = q[1]; /* index on g */ - } - } - q = network[smallpos]; - /* swap p (i) and q (smallpos) entries */ - if (i != smallpos) { - j = q[0]; - q[0] = p[0]; - p[0] = j; - j = q[1]; - q[1] = p[1]; - p[1] = j; - j = q[2]; - q[2] = p[2]; - p[2] = j; - j = q[3]; - q[3] = p[3]; - p[3] = j; - } - /* smallval entry is now in position i */ - if (smallval != previouscol) { - netindex[previouscol] = (startpos + i) >> 1; - for (j = previouscol + 1; j < smallval; j++) - netindex[j] = i; - previouscol = smallval; - startpos = i; - } - } - netindex[previouscol] = (startpos + maxnetpos) >> 1; - for (j = previouscol + 1; j < 256; j++) - netindex[j] = maxnetpos; /* really 256 */ - } - - /* Main Learning Loop - ------------------ */ - public void learn() { - - int i, j, b, g, r; - int radius, rad, alpha, step, delta, samplepixels; - byte[] p; - int pix, lim; - - if (lengthcount < minpicturebytes) - samplefac = 1; - alphadec = 30 + ((samplefac - 1) / 3); - p = thepicture; - pix = 0; - lim = lengthcount; - samplepixels = lengthcount / (3 * samplefac); - delta = samplepixels / ncycles; - alpha = initalpha; - radius = initradius; - - rad = radius >> radiusbiasshift; - if (rad <= 1) - rad = 0; - for (i = 0; i < rad; i++) - radpower[i] = - alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); - - //fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad); - - if (lengthcount < minpicturebytes) - step = 3; - else if ((lengthcount % prime1) != 0) - step = 3 * prime1; - else { - if ((lengthcount % prime2) != 0) - step = 3 * prime2; - else { - if ((lengthcount % prime3) != 0) - step = 3 * prime3; - else - step = 3 * prime4; - } - } - - i = 0; - while (i < samplepixels) { - b = (p[pix + 0] & 0xff) << netbiasshift; - g = (p[pix + 1] & 0xff) << netbiasshift; - r = (p[pix + 2] & 0xff) << netbiasshift; - j = contest(b, g, r); - - altersingle(alpha, j, b, g, r); - if (rad != 0) - alterneigh(rad, j, b, g, r); /* alter neighbours */ - - pix += step; - if (pix >= lim) - pix -= lengthcount; - - i++; - if (delta == 0) - delta = 1; - if (i % delta == 0) { - alpha -= alpha / alphadec; - radius -= radius / radiusdec; - rad = radius >> radiusbiasshift; - if (rad <= 1) - rad = 0; - for (j = 0; j < rad; j++) - radpower[j] = - alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); - } - } - //fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha); - } - - /* Search for BGR values 0..255 (after net is unbiased) and return colour index - ---------------------------------------------------------------------------- */ - public int map(int b, int g, int r) { - - int i, j, dist, a, bestd; - int[] p; - int best; - - bestd = 1000; /* biggest possible dist is 256*3 */ - best = -1; - i = netindex[g]; /* index on g */ - j = i - 1; /* start at netindex[g] and work outwards */ - - while ((i < netsize) || (j >= 0)) { - if (i < netsize) { - p = network[i]; - dist = p[1] - g; /* inx key */ - if (dist >= bestd) - i = netsize; /* stop iter */ - else { - i++; - if (dist < 0) - dist = -dist; - a = p[0] - b; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - a = p[2] - r; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - bestd = dist; - best = p[3]; - } - } - } - } - if (j >= 0) { - p = network[j]; - dist = g - p[1]; /* inx key - reverse dif */ - if (dist >= bestd) - j = -1; /* stop iter */ - else { - j--; - if (dist < 0) - dist = -dist; - a = p[0] - b; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - a = p[2] - r; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - bestd = dist; - best = p[3]; - } - } - } - } - } - return (best); - } - public byte[] process() { - learn(); - unbiasnet(); - inxbuild(); - return colorMap(); - } - - /* Unbias network to give byte values 0..255 and record position i to prepare for sort - ----------------------------------------------------------------------------------- */ - public void unbiasnet() { - - int i, j; - - for (i = 0; i < netsize; i++) { - network[i][0] >>= netbiasshift; - network[i][1] >>= netbiasshift; - network[i][2] >>= netbiasshift; - network[i][3] = i; /* record colour no */ - } - } - - /* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] - --------------------------------------------------------------------------------- */ - protected void alterneigh(int rad, int i, int b, int g, int r) { - - int j, k, lo, hi, a, m; - int[] p; - - lo = i - rad; - if (lo < -1) - lo = -1; - hi = i + rad; - if (hi > netsize) - hi = netsize; - - j = i + 1; - k = i - 1; - m = 1; - while ((j < hi) || (k > lo)) { - a = radpower[m++]; - if (j < hi) { - p = network[j++]; - try { - p[0] -= (a * (p[0] - b)) / alpharadbias; - p[1] -= (a * (p[1] - g)) / alpharadbias; - p[2] -= (a * (p[2] - r)) / alpharadbias; - } catch (Exception e) { - } // prevents 1.3 miscompilation - } - if (k > lo) { - p = network[k--]; - try { - p[0] -= (a * (p[0] - b)) / alpharadbias; - p[1] -= (a * (p[1] - g)) / alpharadbias; - p[2] -= (a * (p[2] - r)) / alpharadbias; - } catch (Exception e) { - } - } - } - } - - /* Move neuron i towards biased (b,g,r) by factor alpha - ---------------------------------------------------- */ - protected void altersingle(int alpha, int i, int b, int g, int r) { - - /* alter hit neuron */ - int[] n = network[i]; - n[0] -= (alpha * (n[0] - b)) / initalpha; - n[1] -= (alpha * (n[1] - g)) / initalpha; - n[2] -= (alpha * (n[2] - r)) / initalpha; - } - - /* Search for biased BGR values - ---------------------------- */ - protected int contest(int b, int g, int r) { - - /* finds closest neuron (min dist) and updates freq */ - /* finds best neuron (min dist-bias) and returns position */ - /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ - /* bias[i] = gamma*((1/netsize)-freq[i]) */ - - int i, dist, a, biasdist, betafreq; - int bestpos, bestbiaspos, bestd, bestbiasd; - int[] n; - - bestd = ~(((int) 1) << 31); - bestbiasd = bestd; - bestpos = -1; - bestbiaspos = bestpos; - - for (i = 0; i < netsize; i++) { - n = network[i]; - dist = n[0] - b; - if (dist < 0) - dist = -dist; - a = n[1] - g; - if (a < 0) - a = -a; - dist += a; - a = n[2] - r; - if (a < 0) - a = -a; - dist += a; - if (dist < bestd) { - bestd = dist; - bestpos = i; - } - biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); - if (biasdist < bestbiasd) { - bestbiasd = biasdist; - bestbiaspos = i; - } - betafreq = (freq[i] >> betashift); - freq[i] -= betafreq; - bias[i] += (betafreq << gammashift); - } - freq[bestpos] += beta; - bias[bestpos] -= betagamma; - return (bestbiaspos); - } -} +/* NeuQuant Neural-Net Quantization Algorithm + * ------------------------------------------ + * + * Copyright (c) 1994 Anthony Dekker + * + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. + * See "Kohonen neural networks for optimal colour quantization" + * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. + * for a discussion of the algorithm. + * + * Any party obtaining a copy of these files from the author, directly or + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, + * world-wide, paid up, royalty-free, nonexclusive right and license to deal + * in this software and documentation files (the "Software"), including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons who receive + * copies from any such party to do so, with the only requirement being + * that this copyright notice remain intact. + */ + +// Ported to Java 12/00 K Weiner + +package io.korhner.asciimg.utils; + +public class NeuQuant { + + protected static final int netsize = 256; /* number of colours used */ + + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + protected static final int prime1 = 499; + protected static final int prime2 = 491; + protected static final int prime3 = 487; + protected static final int prime4 = 503; + + protected static final int minpicturebytes = (3 * prime4); + /* minimum size for input image */ + + /* Program Skeleton + ---------------- + [select samplefac in range 1..30] + [read image from input file] + pic = (unsigned char*) malloc(3*width*height); + initnet(pic,3*width*height,samplefac); + learn(); + unbiasnet(); + [write output image header, using writecolourmap(f)] + inxbuild(); + write output image using inxsearch(b,g,r) */ + + /* Network Definitions + ------------------- */ + + protected static final int maxnetpos = (netsize - 1); + protected static final int netbiasshift = 4; /* bias for colour values */ + protected static final int ncycles = 100; /* no. of learning cycles */ + + /* defs for freq and bias */ + protected static final int intbiasshift = 16; /* bias for fractions */ + protected static final int intbias = (((int) 1) << intbiasshift); + protected static final int gammashift = 10; /* gamma = 1024 */ + protected static final int gamma = (((int) 1) << gammashift); + protected static final int betashift = 10; + protected static final int beta = (intbias >> betashift); /* beta = 1/1024 */ + protected static final int betagamma = + (intbias << (gammashift - betashift)); + + /* defs for decreasing radius factor */ + protected static final int initrad = (netsize >> 3); /* for 256 cols, radius starts */ + protected static final int radiusbiasshift = 6; /* at 32.0 biased by 6 bits */ + protected static final int radiusbias = (((int) 1) << radiusbiasshift); + protected static final int initradius = (initrad * radiusbias); /* and decreases by a */ + protected static final int radiusdec = 30; /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + protected static final int alphabiasshift = 10; /* alpha starts at 1.0 */ + protected static final int initalpha = (((int) 1) << alphabiasshift); + + protected int alphadec; /* biased by 10 bits */ + + /* radbias and alpharadbias used for radpower calculation */ + protected static final int radbiasshift = 8; + protected static final int radbias = (((int) 1) << radbiasshift); + protected static final int alpharadbshift = (alphabiasshift + radbiasshift); + protected static final int alpharadbias = (((int) 1) << alpharadbshift); + + /* Types and Global Variables + -------------------------- */ + + protected byte[] thepicture; /* the input image itself */ + protected int lengthcount; /* lengthcount = H*W*3 */ + + protected int samplefac; /* sampling factor 1..30 */ + + // typedef int pixel[4]; /* BGRc */ + protected int[][] network; /* the network itself - [netsize][4] */ + + protected int[] netindex = new int[256]; + /* for network lookup - really 256 */ + + protected int[] bias = new int[netsize]; + /* bias and freq arrays for learning */ + protected int[] freq = new int[netsize]; + protected int[] radpower = new int[initrad]; + /* radpower for precomputation */ + + /* Initialise network in range (0,0,0) to (255,255,255) and set parameters + ----------------------------------------------------------------------- */ + public NeuQuant(byte[] thepic, int len, int sample) { + + int i; + int[] p; + + thepicture = thepic; + lengthcount = len; + samplefac = sample; + + network = new int[netsize][]; + for (i = 0; i < netsize; i++) { + network[i] = new int[4]; + p = network[i]; + p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize; + freq[i] = intbias / netsize; /* 1/netsize */ + bias[i] = 0; + } + } + + public byte[] colorMap() { + byte[] map = new byte[3 * netsize]; + int[] index = new int[netsize]; + for (int i = 0; i < netsize; i++) + index[network[i][3]] = i; + int k = 0; + for (int i = 0; i < netsize; i++) { + int j = index[i]; + map[k++] = (byte) (network[j][0]); + map[k++] = (byte) (network[j][1]); + map[k++] = (byte) (network[j][2]); + } + return map; + } + + /* Insertion sort of network and building of netindex[0..255] (to do after unbias) + ------------------------------------------------------------------------------- */ + public void inxbuild() { + + int i, j, smallpos, smallval; + int[] p; + int[] q; + int previouscol, startpos; + + previouscol = 0; + startpos = 0; + for (i = 0; i < netsize; i++) { + p = network[i]; + smallpos = i; + smallval = p[1]; /* index on g */ + /* find smallest in i..netsize-1 */ + for (j = i + 1; j < netsize; j++) { + q = network[j]; + if (q[1] < smallval) { /* index on g */ + smallpos = j; + smallval = q[1]; /* index on g */ + } + } + q = network[smallpos]; + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) { + j = q[0]; + q[0] = p[0]; + p[0] = j; + j = q[1]; + q[1] = p[1]; + p[1] = j; + j = q[2]; + q[2] = p[2]; + p[2] = j; + j = q[3]; + q[3] = p[3]; + p[3] = j; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) { + netindex[previouscol] = (startpos + i) >> 1; + for (j = previouscol + 1; j < smallval; j++) + netindex[j] = i; + previouscol = smallval; + startpos = i; + } + } + netindex[previouscol] = (startpos + maxnetpos) >> 1; + for (j = previouscol + 1; j < 256; j++) + netindex[j] = maxnetpos; /* really 256 */ + } + + /* Main Learning Loop + ------------------ */ + public void learn() { + + int i, j, b, g, r; + int radius, rad, alpha, step, delta, samplepixels; + byte[] p; + int pix, lim; + + if (lengthcount < minpicturebytes) + samplefac = 1; + alphadec = 30 + ((samplefac - 1) / 3); + p = thepicture; + pix = 0; + lim = lengthcount; + samplepixels = lengthcount / (3 * samplefac); + delta = samplepixels / ncycles; + alpha = initalpha; + radius = initradius; + + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (i = 0; i < rad; i++) + radpower[i] = + alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); + + //fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad); + + if (lengthcount < minpicturebytes) + step = 3; + else if ((lengthcount % prime1) != 0) + step = 3 * prime1; + else { + if ((lengthcount % prime2) != 0) + step = 3 * prime2; + else { + if ((lengthcount % prime3) != 0) + step = 3 * prime3; + else + step = 3 * prime4; + } + } + + i = 0; + while (i < samplepixels) { + b = (p[pix + 0] & 0xff) << netbiasshift; + g = (p[pix + 1] & 0xff) << netbiasshift; + r = (p[pix + 2] & 0xff) << netbiasshift; + j = contest(b, g, r); + + altersingle(alpha, j, b, g, r); + if (rad != 0) + alterneigh(rad, j, b, g, r); /* alter neighbours */ + + pix += step; + if (pix >= lim) + pix -= lengthcount; + + i++; + if (delta == 0) + delta = 1; + if (i % delta == 0) { + alpha -= alpha / alphadec; + radius -= radius / radiusdec; + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (j = 0; j < rad; j++) + radpower[j] = + alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); + } + } + //fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha); + } + + /* Search for BGR values 0..255 (after net is unbiased) and return colour index + ---------------------------------------------------------------------------- */ + public int map(int b, int g, int r) { + + int i, j, dist, a, bestd; + int[] p; + int best; + + bestd = 1000; /* biggest possible dist is 256*3 */ + best = -1; + i = netindex[g]; /* index on g */ + j = i - 1; /* start at netindex[g] and work outwards */ + + while ((i < netsize) || (j >= 0)) { + if (i < netsize) { + p = network[i]; + dist = p[1] - g; /* inx key */ + if (dist >= bestd) + i = netsize; /* stop iter */ + else { + i++; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + if (j >= 0) { + p = network[j]; + dist = g - p[1]; /* inx key - reverse dif */ + if (dist >= bestd) + j = -1; /* stop iter */ + else { + j--; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + } + return (best); + } + public byte[] process() { + learn(); + unbiasnet(); + inxbuild(); + return colorMap(); + } + + /* Unbias network to give byte values 0..255 and record position i to prepare for sort + ----------------------------------------------------------------------------------- */ + public void unbiasnet() { + + int i, j; + + for (i = 0; i < netsize; i++) { + network[i][0] >>= netbiasshift; + network[i][1] >>= netbiasshift; + network[i][2] >>= netbiasshift; + network[i][3] = i; /* record colour no */ + } + } + + /* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] + --------------------------------------------------------------------------------- */ + protected void alterneigh(int rad, int i, int b, int g, int r) { + + int j, k, lo, hi, a, m; + int[] p; + + lo = i - rad; + if (lo < -1) + lo = -1; + hi = i + rad; + if (hi > netsize) + hi = netsize; + + j = i + 1; + k = i - 1; + m = 1; + while ((j < hi) || (k > lo)) { + a = radpower[m++]; + if (j < hi) { + p = network[j++]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } // prevents 1.3 miscompilation + } + if (k > lo) { + p = network[k--]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } + } + } + } + + /* Move neuron i towards biased (b,g,r) by factor alpha + ---------------------------------------------------- */ + protected void altersingle(int alpha, int i, int b, int g, int r) { + + /* alter hit neuron */ + int[] n = network[i]; + n[0] -= (alpha * (n[0] - b)) / initalpha; + n[1] -= (alpha * (n[1] - g)) / initalpha; + n[2] -= (alpha * (n[2] - r)) / initalpha; + } + + /* Search for biased BGR values + ---------------------------- */ + protected int contest(int b, int g, int r) { + + /* finds closest neuron (min dist) and updates freq */ + /* finds best neuron (min dist-bias) and returns position */ + /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ + /* bias[i] = gamma*((1/netsize)-freq[i]) */ + + int i, dist, a, biasdist, betafreq; + int bestpos, bestbiaspos, bestd, bestbiasd; + int[] n; + + bestd = ~(((int) 1) << 31); + bestbiasd = bestd; + bestpos = -1; + bestbiaspos = bestpos; + + for (i = 0; i < netsize; i++) { + n = network[i]; + dist = n[0] - b; + if (dist < 0) + dist = -dist; + a = n[1] - g; + if (a < 0) + a = -a; + dist += a; + a = n[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + bestpos = i; + } + biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); + if (biasdist < bestbiasd) { + bestbiasd = biasdist; + bestbiaspos = i; + } + betafreq = (freq[i] >> betashift); + freq[i] -= betafreq; + bias[i] += (betafreq << gammashift); + } + freq[bestpos] += beta; + bias[bestpos] -= betagamma; + return (bestbiaspos); + } +} diff --git a/src/Examples.java b/src/test/java/io/korhner/asciimg/Examples.java similarity index 99% rename from src/Examples.java rename to src/test/java/io/korhner/asciimg/Examples.java index a15cba9..c760951 100644 --- a/src/Examples.java +++ b/src/test/java/io/korhner/asciimg/Examples.java @@ -1,3 +1,5 @@ +package io.korhner.asciimg; + import io.korhner.asciimg.image.AsciiImgCache; import io.korhner.asciimg.image.character_fit_strategy.BestCharacterFitStrategy; import io.korhner.asciimg.image.character_fit_strategy.ColorSquareErrorFitStrategy; diff --git a/src/GifExamples.java b/src/test/java/io/korhner/asciimg/GifExamples.java similarity index 97% rename from src/GifExamples.java rename to src/test/java/io/korhner/asciimg/GifExamples.java index 2a06070..08b7c7c 100644 --- a/src/GifExamples.java +++ b/src/test/java/io/korhner/asciimg/GifExamples.java @@ -1,3 +1,5 @@ +package io.korhner.asciimg; + import io.korhner.asciimg.image.AsciiImgCache; import io.korhner.asciimg.image.character_fit_strategy.BestCharacterFitStrategy; import io.korhner.asciimg.image.character_fit_strategy.StructuralSimilarityFitStrategy; @@ -31,6 +33,5 @@ public static void main(String[] args) throws IOException { GifToAsciiConvert asciiConvert = new GifToAsciiConvert(smallFontCache, ssimStrategy); asciiConvert.convertGitToAscii(srcFilePath, disFilePath, delay,0); - } }