diff --git a/README.md b/README.md
index 462d4b0..b830aa7 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,73 @@
-asciimg
-========
+# asciimg
-Asciimg is an extensible Ascii art generator written in Java.
-For more info refer to this blog post:
-
-http://korhner.github.io/java/image-processing/ascii-art-generator-part-2/
+Asciimg is an extensible ASCII art generator written in Java.
+For more info refer to [this blog post](http://korhner.github.io/java/image-processing/ascii-art-generator-part-2/)
## Example usage
-
-// initialize cache
-AsciiImgCache cache = AsciiImgCache.create(new Font("Courier",Font.BOLD, 6));
+ // initialize cache
+ AsciiImgCache cache = AsciiImgCache.create(new Font("Courier", Font.BOLD, 6));
-// load image
-BufferedImage portraitImage = ImageIO.read(new File("image.png"));
+ // load image
+ BufferedImage portraitImage = ImageIO.read(new File("input_image.png"));
-// initialize converters
-AsciiToImageConverter imageConverter =
- new AsciiToImageConverter(cache, new ColorSquareErrorFitStrategy());
-AsciiToStringConverter stringConverter =
- new AsciiToStringConverter(cache, new StructuralSimilarityFitStrategy());
+ // initialize converters
+ AsciiToImageConverter imageConverter =
+ new AsciiToImageConverter(cache, new ColorSquareErrorFitStrategy());
+ AsciiToStringConverter stringConverter =
+ new AsciiToStringConverter(cache, new StructuralSimilarityFitStrategy());
-// image output
-ImageIO.write(imageConverter.convertImage(portraitImage), "png",
- new File("ascii_art.png"));
-// string converter, output to console
-System.out.println(stringConverter.convertImage(portraitImage));
-
+ // image output
+ ImageIO.write(imageConverter.convertImage(portraitImage), "png",
+ new File("ascii_art.png"));
+ // string converter, output to console
+ System.out.println(stringConverter.convertImage(portraitImage));
## Example output
Here are some sample images generated with various parameters:
-Original picture
+
-16 pts font, MSE
-16 pts font, SSIM
-10 pts font with 3 characters, MSE
-10 pts font with 3 characters, SSIM
-6 pts font, MSE
-6 pts font, SSIM
+---------------------------------------
-## Architecture:
+
+
+- - -
+
+
+
+- - -
+
+
+
+- - -
+
+
+
+- - -
+
+
+
+- - -
+
+
+
+## Architecture

## AsciiImgCache
-Before any ascii art rendering takes place, it is necessary to create an instance of this class.
+Before any ASCII art rendering takes place, it is necessary to create an instance of this class.
It takes a font and a list of characters to use as parameters and it creates a map of images for every character.
-There is also a default list of characters if you don't want to bother comming up with your own.
+There is also a default list of characters if you don't want to bother comming up with your own.
### BestCharacterFitStrategy
-This is the abstraction of the algorithm used for determining how similar a part of the source image with each character is.
-The implementation should compare two images and return a float error. Each character will be compared and the one that returns the lowest error will be selected.
-Currently there two implementations available: ColorSquareErrorFitStrategy and StructuralSimilarityFitStrategy.
+This is the abstraction of the algorithm used for determining how similar a part of the source image with each character is.
+The implementation should compare two images and return a float error. Each character will be compared and the one that returns the lowest error will be selected.
+Currently there two implementations available: `ColorSquareErrorFitStrategy` and `StructuralSimilarityFitStrategy`.
#### ColorSquareErrorFitStrategy
@@ -64,13 +76,12 @@ Very simple to understand, it compares every pixel and calculates Mean squared e
#### StructuralSimilarityFitStrategy
The structural similarity (SSIM) index algorithm claims to reproduce human perception and its aim is to improve on traditional methods like MSE.
-Uou can read more on Wikipedia if you want to know more.
+You can read more on [Wikipedia](http://en.wikipedia.org/wiki/Structural_similarity) if you want to know more.
I experimented a bit with it and implemented a version that seemed to produce the best results for this case.
### AsciiConverter
This is the hearth of the process and it contains all the logic for tiling source image and utilizing concrete implementations for calculating character best fit.
-However, it doesn't know how to create the concrete ascii art - it needs to be subclassed.
-There are two implementations currently: AsciiToImageConverter and AsciiToStringConverter - which as you probably guessed, produce image and string output.
-
+However, it doesn't know how to create the concrete ASCII art - it needs to be subclassed.
+There are currently two implementations: `AsciiToImageConverter` and `AsciiToStringConverter` - which, as you probably guessed, produce image and string output.
diff --git a/examples/test-ascii.gif b/examples/test-ascii.gif
deleted file mode 100644
index b423e0b..0000000
Binary files a/examples/test-ascii.gif and /dev/null differ
diff --git a/pom.xml b/pom.xml
index cb9da0d..e41b8f4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,22 +1,437 @@
4.0.0
+
+
+
+ UTF-8
+ ${project.build.encoding}
+ ${project.build.encoding}
+ io.korhner.asciimg
+ 8
+
+
io.korhner
asciimg
1.00-SNAPSHOT
- jar
-
+ bundle
+
+ ASCII-Img
+ An ASCII image generator written in Java
+ https://github.com/hoijui/asciimg
+ 2015
+
+
+
+ MIT
+ https://opensource.org/licenses/MIT
+ repo
+
+
+
+
+ GitHub
+ https://github.com/hoijui/asciimg/issues/
+
+
+
+
+ korhner
+ Ivan Korhner
+ korhner@gmail.com
+
+ project founder
+
+
+
+ hoijui
+ Robin Vobruba
+ hoijui.quaero@gmail.com
+
+ maintainer
+
+
+
+
+
+ scm:git:git://github.com/hoijui/asciimg
+ scm:git:git@github.com:hoijui/asciimg.git
+ http://github.com/hoijui/asciimg
+ HEAD
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 2.17
+
+ ${project.build.sourceEncoding}
+ true
+ src/main/resources/checkstyle.xml
+ java.header.regex.template.file=${project.basedir}/src/main/resources/java_header_regex_template.txt
+ ${project.basedir}/src/main/resources/checkstyle-suppressions.xml
+ checkstyle.suppressions.file
+
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.7
+
+ true
+ true
+ ${project.build.sourceEncoding}
+ 50
+ 1.${project.java.version}
+
+ src/main/resources/pmd.xml
+
+
+
+
+
+ org.codehaus.mojo
+ findbugs-maven-plugin
+ 3.0.4
+
+
+
+ org.apache.maven.plugins
+ maven-changelog-plugin
+ 2.3
+
+
org.apache.maven.plugins
+ maven-surefire-report-plugin
+ 2.20.1
+
+
+
+ org.codehaus.mojo
+ jdepend-maven-plugin
+ 2.0
+
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 2.9
+
+
+
+
+
+
+
maven-compiler-plugin
- 2.5.1
+ 3.6.1
+
+ 1.${project.java.version}
+ 1.${project.java.version}
+ ${project.build.sourceEncoding}
+ true
+ -Xlint:unchecked
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.0.2
+
+ ${project.build.resourceEncoding}
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.0.0
+
+ ${project.build.sourceEncoding}
+ ${project.build.sourceEncoding}
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 2.8.2
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.0.1
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 3.6
+
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 2.5.3
- 1.8
- 1.8
+ true
+ false
+ release
+ deploy
+
+ forked-path
+
+
+ maven-jar-plugin
+ 3.0.2
+
+
+
+
+
+
+
+ ${project.pkgName}
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ 3.2.0
+ true
+
+
+ *
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 2.17
+
+ ${project.build.sourceEncoding}
+ true
+ src/main/resources/checkstyle.xml
+ java.header.regex.template.file=${project.basedir}/src/main/resources/java_header_regex_template.txt
+ ${project.basedir}/src/main/resources/checkstyle-suppressions.xml
+ checkstyle.suppressions.file
+
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.7
+
+ true
+ true
+ ${project.build.sourceEncoding}
+ 50
+ 1.${project.java.version}
+
+ src/main/resources/pmd.xml
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 2.9
+
+
-
\ No newline at end of file
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+
+
+
+ Extensive-Reports
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 2.5
+
+ ${project.build.sourceEncoding}
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.0.0
+
+ ${project.build.sourceEncoding}
+ ${project.build.sourceEncoding}
+
+
+
+
+ org.apache.maven.plugins
+ maven-changelog-plugin
+ 2.3
+
+
+
+ org.codehaus.mojo
+ cobertura-maven-plugin
+ 2.7
+
+
+
+ org.codehaus.mojo
+ javancss-maven-plugin
+ 2.1
+
+
+
+ org.codehaus.mojo
+ sonar-maven-plugin
+ 3.4.0.905
+
+
+
+ org.codehaus.mojo
+ emma-maven-plugin
+ 1.0-alpha-3
+
+ ${project.build.directory}
+
+
+
+
+
+
+
+ release
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ ${gpg.keyname}
+ ${gpg.keyname}
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.7
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.0.0
+
+ false
+ ${project.build.sourceEncoding}
+ ${project.build.sourceEncoding}
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.0.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
+
+
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
diff --git a/src/main/java/io/korhner/asciimg/image/AsciiImgCache.java b/src/main/java/io/korhner/asciimg/image/AsciiImgCache.java
index 5bac801..a81419c 100644
--- a/src/main/java/io/korhner/asciimg/image/AsciiImgCache.java
+++ b/src/main/java/io/korhner/asciimg/image/AsciiImgCache.java
@@ -1,12 +1,39 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
package io.korhner.asciimg.image;
-import io.korhner.asciimg.image.matrix.GrayscaleMatrix;
+import io.korhner.asciimg.image.matrix.BasicImageMatrixInfo;
+import io.korhner.asciimg.image.matrix.BasicInt1DImageMatrix;
+import io.korhner.asciimg.image.matrix.GrayScaleMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
import java.awt.Color;
-import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
-import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
@@ -18,47 +45,51 @@
import java.util.Map.Entry;
/**
- * Character cache that keeps a map of precalculated pixel data of each
- * character that is eligible for ascii art.
+ * Character cache that keeps a map of pre-calculated pixel data of each
+ * character that is eligible for ASCII art.
*/
-public class AsciiImgCache implements
- Iterable> {
+public final class AsciiImgCache implements Iterable>> {
+
+ private final Map> imageCache;
+
+ /** Some empirically chosen characters that give good results. */
+ private static final char[] DEFAULT_CHARACTERS
+ = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".toCharArray();
+
+ /** Dimension of character image data. */
+ private final ImageMatrixDimensions characterImageSize;
/**
* Calculate character rectangle for the given font metrics.
*
- * @param fontMetrics
- * the font metrics
+ * @param font used to calculate the font metrics
* @return the rectangle
*/
- private static Dimension calculateCharacterRectangle(final Font font,
- final char[] characters) {
- BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
- Graphics g = img.getGraphics();
- Graphics2D graphics = (Graphics2D) g;
- graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
- RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
- graphics.setFont(font);
- FontMetrics fm = graphics.getFontMetrics();
-
- Dimension maxCharacter = new Dimension();
- for (int i = 0; i < characters.length; i++) {
- String character = Character.toString(characters[i]);
-
- Rectangle characterRectangle = new TextLayout(character,
- fm.getFont(), fm.getFontRenderContext()).getOutline(null)
- .getBounds();
-
- if (maxCharacter.width < characterRectangle.getWidth()) {
- maxCharacter.width = (int) characterRectangle.getWidth();
+ private static ImageMatrixDimensions calculateCharacterRectangle(final Font font, final char[] characters) {
+ final BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
+ final Graphics2D graphics2D = (Graphics2D) img.getGraphics();
+ graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ graphics2D.setFont(font);
+ final FontMetrics fontMetrics = graphics2D.getFontMetrics();
+
+ int maxCharacterWidth = 0;
+ int maxCharacterHeight = 0;
+ for (final char chr : characters) {
+ final String character = Character.toString(chr);
+
+ final Rectangle characterRectangle = new TextLayout(character, fontMetrics.getFont(),
+ fontMetrics.getFontRenderContext()).getOutline(null).getBounds();
+
+ if (maxCharacterWidth < characterRectangle.getWidth()) {
+ maxCharacterWidth = (int) characterRectangle.getWidth();
}
- if (maxCharacter.height < characterRectangle.getHeight()) {
- maxCharacter.height = (int) characterRectangle.getHeight();
+ if (maxCharacterHeight < characterRectangle.getHeight()) {
+ maxCharacterHeight = (int) characterRectangle.getHeight();
}
}
- return maxCharacter;
+ return new ImageMatrixDimensions(maxCharacterWidth, maxCharacterHeight);
}
/**
@@ -66,28 +97,27 @@ private static Dimension calculateCharacterRectangle(final Font font,
*
* @param font
* the font
- * @return the ascii img cache
+ * @return the ASCII img cache
*/
public static AsciiImgCache create(final Font font) {
- return create(font, defaultCharacters);
+
+ return create(font, DEFAULT_CHARACTERS);
}
/**
- * Initialize a new character cache with supplied font.
+ * Initialize a new character cache with the supplied font.
*
- * @param font
- * the font
- * @return the ascii img cache
+ * @param font the font used for the characters
+ * @param characters the characters whose images are to be cached
+ * @return the ASCII img cache
*/
public static AsciiImgCache create(final Font font, final char[] characters) {
- Dimension maxCharacterImageSize = calculateCharacterRectangle(font,
- characters);
- Map imageCache = createCharacterImages(
+ final ImageMatrixDimensions maxCharacterImageSize = calculateCharacterRectangle(font, characters);
+ final Map> imageCache = createCharacterImages(
font, maxCharacterImageSize, characters);
- return new AsciiImgCache(maxCharacterImageSize, imageCache, characters);
-
+ return new AsciiImgCache(maxCharacterImageSize, imageCache);
}
/**
@@ -99,65 +129,53 @@ public static AsciiImgCache create(final Font font, final char[] characters) {
* the character size
* @return the map
*/
- private static Map createCharacterImages(
- final Font font, final Dimension characterSize,
- final char[] characters) {
+ private static Map> createCharacterImages(
+ final Font font,
+ final ImageMatrixDimensions characterSize,
+ final char[] characters)
+ {
// create each image
- BufferedImage img = new BufferedImage(characterSize.width,
- characterSize.height, BufferedImage.TYPE_INT_ARGB);
- Graphics g = img.getGraphics();
- Graphics2D graphics = (Graphics2D) g;
- graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
- RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
- graphics.setFont(font);
- FontMetrics fm = graphics.getFontMetrics();
-
- Map imageCache = new HashMap<>();
-
- for (int i = 0; i < characters.length; i++) {
- String character = Character.toString(characters[i]);
-
- g.setColor(Color.WHITE);
- g.fillRect(0, 0, characterSize.width, characterSize.height);
- g.setColor(Color.BLACK);
-
- Rectangle rect = new TextLayout(character, fm.getFont(),
- fm.getFontRenderContext()).getOutline(null).getBounds();
-
- g.drawString(character, 0,
- (int) (rect.getHeight() - rect.getMaxY()));
-
- int[] pixels = img.getRGB(0, 0, characterSize.width,
- characterSize.height, null, 0, characterSize.width);
- GrayscaleMatrix matrix = new GrayscaleMatrix(pixels,
- characterSize.width, characterSize.height);
- imageCache.put(characters[i], matrix);
- }
+ final BufferedImage img = new BufferedImage(characterSize.getWidth(), characterSize.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ final Graphics2D graphics2D = (Graphics2D) img.getGraphics();
+ graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ graphics2D.setFont(font);
+ final FontMetrics fontMetrics = graphics2D.getFontMetrics();
- return imageCache;
- }
+ final Map> imageCache = new HashMap<>();
- /** A map of characters to their bitmaps. */
- protected final Map imageCache;
+ for (final char chr : characters) {
+ final String character = Character.toString(chr);
- /** Some empirically chosen characters that give good results. */
- private static final char[] defaultCharacters = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
- .toCharArray();
+ graphics2D.setColor(Color.WHITE);
+ graphics2D.fillRect(0, 0, characterSize.getWidth(), characterSize.getHeight());
+ graphics2D.setColor(Color.BLACK);
- /** Dimension of character image data. */
- private final Dimension characterImageSize;
+ final Rectangle rect = new TextLayout(character, fontMetrics.getFont(),
+ fontMetrics.getFontRenderContext()).getOutline(null).getBounds();
+
+ graphics2D.drawString(character, 0, (int) (rect.getHeight() - rect.getMaxY()));
+
+ final int[] imagePixels = img.getRGB(0, 0, characterSize.getWidth(),
+ characterSize.getHeight(), null, 0, characterSize.getWidth());
+ final ImageMatrix argbMatrix = new BasicInt1DImageMatrix(
+ new BasicImageMatrixInfo(4, Integer.class, 8),
+ imagePixels, characterSize.getWidth());
+ final GrayScaleMatrix matrix = new GrayScaleMatrix(argbMatrix);
+ imageCache.put(chr, matrix);
+ }
+
+ return imageCache;
+ }
/**
- * Instantiates a new ascii img cache.
+ * Instantiates a new ASCII img cache.
*
* @param characterImageSize
* the character image size
* @param imageCache
* the image cache
*/
- private AsciiImgCache(final Dimension characterImageSize,
- final Map imageCache,
- final char[] characters) {
+ private AsciiImgCache(final ImageMatrixDimensions characterImageSize, final Map> imageCache) {
this.characterImageSize = characterImageSize;
this.imageCache = imageCache;
}
@@ -167,16 +185,22 @@ private AsciiImgCache(final Dimension characterImageSize,
*
* @return character image dimensions
*/
- public Dimension getCharacterImageSize() {
+ public ImageMatrixDimensions getCharacterImageSize() {
+
return characterImageSize;
}
- /**
- * @see java.lang.Iterable#iterator()
- */
@Override
- public Iterator> iterator() {
- return imageCache.entrySet().iterator();
+ public Iterator>> iterator() {
+
+ return getImageCache().entrySet().iterator();
}
+ /**
+ * Returns the image cache.
+ * @return a map of characters to their bitmaps
+ */
+ protected Map> getImageCache() {
+ return imageCache;
+ }
}
diff --git a/src/main/java/io/korhner/asciimg/image/character_fit_strategy/BestCharacterFitStrategy.java b/src/main/java/io/korhner/asciimg/image/character_fit_strategy/BestCharacterFitStrategy.java
deleted file mode 100644
index fd06160..0000000
--- a/src/main/java/io/korhner/asciimg/image/character_fit_strategy/BestCharacterFitStrategy.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.korhner.asciimg.image.character_fit_strategy;
-
-import io.korhner.asciimg.image.matrix.GrayscaleMatrix;
-
-/**
- * Encapsulates the algorith for choosing best fit character.
- */
-public interface BestCharacterFitStrategy {
-
- /**
- * Returns the error between the character and tile matrices. The character
- * with minimun error wins.
- *
- * @param character
- * the character
- * @param tile
- * the tile
- * @return error. Less values mean better fit. Least value character will be
- * chosen as best fit.
- */
- float calculateError(final GrayscaleMatrix character,
- final GrayscaleMatrix tile);
-}
diff --git a/src/main/java/io/korhner/asciimg/image/character_fit_strategy/ColorSquareErrorFitStrategy.java b/src/main/java/io/korhner/asciimg/image/character_fit_strategy/ColorSquareErrorFitStrategy.java
deleted file mode 100644
index 8e503df..0000000
--- a/src/main/java/io/korhner/asciimg/image/character_fit_strategy/ColorSquareErrorFitStrategy.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.korhner.asciimg.image.character_fit_strategy;
-
-import io.korhner.asciimg.image.matrix.GrayscaleMatrix;
-
-/**
- * Calculates squared mean error between each pixel.
- */
-public class ColorSquareErrorFitStrategy implements BestCharacterFitStrategy {
-
- /**
- * @see io.korhner.asciimg.image.character_fit_strategy.BestCharacterFitStrategy#calculateError(io.korhner.asciimg.image.matrix.GrayscaleMatrix, io.korhner.asciimg.image.matrix.GrayscaleMatrix)
- */
- @Override
- public float calculateError(GrayscaleMatrix character, GrayscaleMatrix tile) {
- float error = 0;
- for (int i = 0; i < character.getData().length; i++) {
- error += (character.getData()[i] - tile.getData()[i])
- * (character.getData()[i] - tile.getData()[i]);
- }
-
- return error / character.getData().length;
-
- }
-
-}
diff --git a/src/main/java/io/korhner/asciimg/image/character_fit_strategy/StructuralSimilarityFitStrategy.java b/src/main/java/io/korhner/asciimg/image/character_fit_strategy/StructuralSimilarityFitStrategy.java
deleted file mode 100644
index 10b0d35..0000000
--- a/src/main/java/io/korhner/asciimg/image/character_fit_strategy/StructuralSimilarityFitStrategy.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package io.korhner.asciimg.image.character_fit_strategy;
-
-import io.korhner.asciimg.image.matrix.GrayscaleMatrix;
-
-/**
- * Calculates Structural Similarity index (SSIM) between the images.
- *
- * See http://en.wikipedia.org/wiki/Structural_similarity for more info.
- */
-public class StructuralSimilarityFitStrategy implements
- BestCharacterFitStrategy {
-
- private final float K1 = 0.01f;
- private final float K2 = 0.03f;
- private float L = 255f;
-
- @Override
- public float calculateError(GrayscaleMatrix character, GrayscaleMatrix tile) {
-
- float C1 = K1 * L;
- C1 *= C1;
- float C2 = K2 * L;
- C2 *= C2;
-
- final int imgLength = character.getData().length;
-
- float score = 0f;
- for (int i = 0; i < imgLength; i++) {
- float pixelImg1 = character.getData()[i];
- float pixelImg2 = tile.getData()[i];
-
- score += (2 * pixelImg1 * pixelImg2 + C1) * (2 + C2)
- / (pixelImg1 * pixelImg1 + pixelImg2 * pixelImg2 + C1) / C2;
- }
-
- // average and convert score to error
- return 1 - (score / imgLength);
-
- }
-
-}
diff --git a/src/main/java/io/korhner/asciimg/image/converter/AbstractToAsciiConverter.java b/src/main/java/io/korhner/asciimg/image/converter/AbstractToAsciiConverter.java
new file mode 100644
index 0000000..eee82b0
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/converter/AbstractToAsciiConverter.java
@@ -0,0 +1,86 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.converter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.exporter.MultiFrameAsciiExporter;
+import io.korhner.asciimg.image.importer.ImageImporter;
+import io.korhner.asciimg.image.strategy.CharacterFitStrategy;
+
+/**
+ * Basic interface implementation.
+ *
+ * @param input type of the image (or similar) to be converted to ASCII art
+ * @param input type of the converted image or animation, the ASCII art
+ */
+public abstract class AbstractToAsciiConverter implements ToAsciiConverter {
+
+ private ImageImporter importer;
+ private CharacterFitStrategy characterFitStrategy;
+ private AsciiImgCache characterCache;
+ private MultiFrameAsciiExporter exporter;
+
+ protected AbstractToAsciiConverter() { }
+
+ @Override
+ public ImageImporter getImporter() {
+ return importer;
+ }
+
+ @Override
+ public void setImporter(final ImageImporter importer) {
+ this.importer = importer;
+ }
+
+ @Override
+ public CharacterFitStrategy getCharacterFitStrategy() {
+ return this.characterFitStrategy;
+ }
+
+ @Override
+ public void setCharacterFitStrategy(final CharacterFitStrategy characterFitStrategy) {
+ this.characterFitStrategy = characterFitStrategy;
+ }
+
+ protected AsciiImgCache getCharacterCache() {
+ return characterCache;
+ }
+
+ @Override
+ public void setCharacterCache(final AsciiImgCache characterCache) {
+ this.characterCache = characterCache;
+ }
+
+ @Override
+ public MultiFrameAsciiExporter getExporter() {
+ return exporter;
+ }
+
+ @Override
+ public void setExporter(final MultiFrameAsciiExporter exporter) {
+ this.exporter = exporter;
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/converter/AsciiConverter.java b/src/main/java/io/korhner/asciimg/image/converter/AsciiConverter.java
deleted file mode 100644
index 12e6d39..0000000
--- a/src/main/java/io/korhner/asciimg/image/converter/AsciiConverter.java
+++ /dev/null
@@ -1,188 +0,0 @@
-package io.korhner.asciimg.image.converter;
-
-import io.korhner.asciimg.image.AsciiImgCache;
-import io.korhner.asciimg.image.character_fit_strategy.BestCharacterFitStrategy;
-import io.korhner.asciimg.image.matrix.GrayscaleMatrix;
-import io.korhner.asciimg.image.matrix.TiledGrayscaleMatrix;
-import io.korhner.asciimg.utils.ArrayUtils;
-
-import java.awt.Dimension;
-import java.awt.image.BufferedImage;
-import java.util.Map.Entry;
-
-/**
- * A class used to convert an image to an ascii art. Output and conversion
- * argorithm are decoupled.
- *
- * @param
- * output type of the ascii art
- */
-public abstract class AsciiConverter {
-
- /** The character cache. */
- protected AsciiImgCache characterCache;
-
- /**
- * The character fit strategy used to determine the best character for each
- * source image tile.
- */
- protected BestCharacterFitStrategy characterFitStrategy;
-
- /** The output. */
- protected Output output;
-
- /**
- * Instantiates a new ascii converter.
- *
- * @param characterCache
- * the character cache
- * @param characterFitStrategy
- * the character fit strategy
- */
- public AsciiConverter(final AsciiImgCache characterCache,
- final BestCharacterFitStrategy characterFitStrategy) {
- this.characterCache = characterCache;
- this.characterFitStrategy = characterFitStrategy;
- }
-
- /**
- * Override this to insert the character at a specified position in the
- * output.
- *
- * @param characterEntry
- * character choosen as best fit
- * @param sourceImagePixels
- * source image pixels. Can be
- * @param tileX
- * the tile x
- * @param tileY
- * the tile y
- * @param imageWidth
- * the image width
- */
- protected abstract void addCharacterToOutput(
- final Entry characterEntry,
- final int[] sourceImagePixels, final int tileX, final int tileY,
- final int imageWidth);
-
- /**
- * Produces an output that is an ascii art of the supplied image.
- *
- * @param source
- * the source
- * @return the buffered image
- */
- public Output convertImage(final BufferedImage source) {
- // dimension of each tile
- Dimension tileSize = this.characterCache.getCharacterImageSize();
-
- // round the width and height so we avoid partial characters
- int outputImageWidth = (source.getWidth() / tileSize.width)
- * tileSize.width;
- int outputImageHeight = (source.getHeight() / tileSize.height)
- * tileSize.height;
-
- // extract pixels from source image
- int[] imagePixels = source.getRGB(0, 0, outputImageWidth,
- outputImageHeight, null, 0, outputImageWidth);
-
- // process the pixels to a grayscale matrix
- GrayscaleMatrix sourceMatrix = new GrayscaleMatrix(imagePixels,
- outputImageWidth, outputImageHeight);
-
- // divide matrix into tiles for easy processing
- TiledGrayscaleMatrix tiledMatrix = new TiledGrayscaleMatrix(
- sourceMatrix, tileSize.width, tileSize.height);
-
- this.output = initializeOutput(outputImageWidth, outputImageHeight);
-
- // compare each tile to every character to determine best fit
- for (int i = 0; i < tiledMatrix.getTileCount(); i++) {
-
- GrayscaleMatrix tile = tiledMatrix.getTile(i);
-
- float minError = Float.MAX_VALUE;
- Entry bestFit = null;
-
- for (Entry charImage : characterCache) {
- GrayscaleMatrix charPixels = charImage.getValue();
-
- float error = this.characterFitStrategy.calculateError(
- charPixels, tile);
-
- if (error < minError) {
- minError = error;
- bestFit = charImage;
- }
- }
-
- int tileX = ArrayUtils.convert1DtoX(i, tiledMatrix.getTilesX());
- int tileY = ArrayUtils.convert1DtoY(i, tiledMatrix.getTilesX());
-
- // copy character to output
- addCharacterToOutput(bestFit, imagePixels, tileX, tileY,
- outputImageWidth);
- }
-
- finalizeOutput(imagePixels, outputImageWidth, outputImageHeight);
-
- return this.output;
-
- }
-
- /**
- * Override this if any action needs to be done at the end of the
- * conversion.
- *
- * @param sourceImagePixels
- * source image pixels data. Can be
- * @param imageWidth
- * source image width
- * @param imageHeight
- * source image height
- */
- protected abstract void finalizeOutput(final int[] sourceImagePixels,
- final int imageWidth, final int imageHeight);
-
- /**
- * Gets the character fit strategy.
- *
- * @return the character fit strategy
- */
- public BestCharacterFitStrategy getCharacterFitStrategy() {
- return this.characterFitStrategy;
- }
-
- /**
- * Override this to return an empty output object that will be filled during
- * the ascii art conversion.
- *
- * @param imageWidth
- * source image width
- * @param imageHeight
- * source image height
- * @return the output
- */
- protected abstract Output initializeOutput(final int imageWidth,
- final int imageHeight);
-
- /**
- * Sets the character cache.
- *
- * @param characterCache new character cache
- */
- public void setCharacterCache(final AsciiImgCache characterCache) {
- this.characterCache = characterCache;
- }
-
- /**
- * Sets the character fit strategy.
- *
- * @param characterFitStrategy
- * new character fit strategy
- */
- public void setCharacterFitStrategy(
- final BestCharacterFitStrategy characterFitStrategy) {
- this.characterFitStrategy = characterFitStrategy;
- }
-}
diff --git a/src/main/java/io/korhner/asciimg/image/converter/AsciiToImageConverter.java b/src/main/java/io/korhner/asciimg/image/converter/AsciiToImageConverter.java
deleted file mode 100644
index 110a2ff..0000000
--- a/src/main/java/io/korhner/asciimg/image/converter/AsciiToImageConverter.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package io.korhner.asciimg.image.converter;
-
-import io.korhner.asciimg.image.AsciiImgCache;
-import io.korhner.asciimg.image.character_fit_strategy.BestCharacterFitStrategy;
-import io.korhner.asciimg.image.matrix.GrayscaleMatrix;
-import io.korhner.asciimg.utils.ArrayUtils;
-
-import java.awt.Color;
-import java.awt.image.BufferedImage;
-import java.util.Map.Entry;
-
-/**
- * Converts ascii art to a BufferedImage.
- */
-public class AsciiToImageConverter extends AsciiConverter {
-
- /**
- * Instantiates a new ascii to image converter.
- *
- * @param characterCacher
- * the character cacher
- * @param characterFitStrategy
- * the character fit strategy
- */
- public AsciiToImageConverter(final AsciiImgCache characterCacher,
- final BestCharacterFitStrategy characterFitStrategy) {
- super(characterCacher, characterFitStrategy);
- }
-
- /**
- * Copy image data over the source pixels image.
- *
- * @see io.korhner.asciimg.image.converter.AsciiConverter#addCharacterToOutput(java.util.Map.Entry,
- * int[], int, int, int)
- */
- @Override
- public void addCharacterToOutput(
- final Entry characterEntry,
- final int[] sourceImagePixels, final int tileX, final int tileY, final int imageWidth) {
- int startCoordinateX = tileX
- * this.characterCache.getCharacterImageSize().width;
- int startCoordinateY = tileY
- * this.characterCache.getCharacterImageSize().height;
-
- // copy winner character
- for (int i = 0; i < characterEntry.getValue().getData().length; i++) {
- int xOffset = i % this.characterCache.getCharacterImageSize().width;
- int yOffset = i / this.characterCache.getCharacterImageSize().width;
-
- int component = (int) characterEntry.getValue().getData()[i];
- sourceImagePixels[ArrayUtils.convert2DTo1D(startCoordinateX
- + xOffset, startCoordinateY + yOffset, imageWidth)] = new Color(
- component, component, component).getRGB();
- }
-
- }
-
- /**
- * Write pixels to output image.
- *
- * @see io.korhner.asciimg.image.converter.AsciiConverter#finalizeOutput(int[],
- * int, int)
- */
- @Override
- protected void finalizeOutput(final int[] sourceImagePixels, final int imageWidth,
- final int imageHeight) {
- this.output.setRGB(0, 0, imageWidth, imageHeight, sourceImagePixels, 0,
- imageWidth);
-
- }
-
- /**
- * Create an empty buffered image.
- *
- * @see io.korhner.asciimg.image.converter.AsciiConverter#initializeOutput(int,
- * int)
- */
- @Override
- protected BufferedImage initializeOutput(final int imageWidth, final int imageHeight) {
- return new BufferedImage(imageWidth, imageHeight,
- BufferedImage.TYPE_INT_ARGB);
- }
-
-}
diff --git a/src/main/java/io/korhner/asciimg/image/converter/AsciiToStringConverter.java b/src/main/java/io/korhner/asciimg/image/converter/AsciiToStringConverter.java
deleted file mode 100644
index fd038db..0000000
--- a/src/main/java/io/korhner/asciimg/image/converter/AsciiToStringConverter.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package io.korhner.asciimg.image.converter;
-
-import io.korhner.asciimg.image.AsciiImgCache;
-import io.korhner.asciimg.image.character_fit_strategy.BestCharacterFitStrategy;
-import io.korhner.asciimg.image.matrix.GrayscaleMatrix;
-
-import java.util.Map.Entry;
-
-/**
- * Converts ascii art to String.
- */
-public class AsciiToStringConverter extends AsciiConverter {
-
- /**
- * Instantiates a new ascii to string converter.
- *
- * @param characterCacher
- * the character cacher
- * @param characterFitStrategy
- * the character fit strategy
- */
- public AsciiToStringConverter(final AsciiImgCache characterCacher,
- final BestCharacterFitStrategy characterFitStrategy) {
- super(characterCacher, characterFitStrategy);
- }
-
- /**
- * Creates an empty string buffer;
- *
- * @see io.korhner.asciimg.image.converter.AsciiConverter#initializeOutput(int,
- * int)
- */
- @Override
- protected StringBuffer initializeOutput(final int imageWidth,
- final int imageHeight) {
- return new StringBuffer();
- }
-
- /**
- * @see io.korhner.asciimg.image.converter.AsciiConverter#finalizeOutput(int[],
- * int, int)
- */
- @Override
- protected void finalizeOutput(final int[] sourceImagePixels,
- final int imageWidth, int imageHeight) {
-
- }
-
- /**
- * Append choosen character to StringBuffer.
- *
- * @see io.korhner.asciimg.image.converter.AsciiConverter#addCharacterToOutput(java.util.Map.Entry,
- * int[], int, int, int)
- */
- @Override
- public void addCharacterToOutput(
- final Entry characterEntry,
- final int[] sourceImagePixels, final int tileX, final int tileY,
- final int imageWidth) {
-
- this.output.append(characterEntry.getKey());
-
- // append new line at the end of the row
- if ((tileX + 1)
- * this.characterCache.getCharacterImageSize().getWidth() == imageWidth) {
- this.output.append(System.lineSeparator());
- }
-
- }
-
-}
diff --git a/src/main/java/io/korhner/asciimg/image/converter/GifToAsciiConvert.java b/src/main/java/io/korhner/asciimg/image/converter/GifToAsciiConvert.java
deleted file mode 100644
index 7641802..0000000
--- a/src/main/java/io/korhner/asciimg/image/converter/GifToAsciiConvert.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package io.korhner.asciimg.image.converter;
-
-import io.korhner.asciimg.image.AsciiImgCache;
-import io.korhner.asciimg.image.character_fit_strategy.BestCharacterFitStrategy;
-import io.korhner.asciimg.utils.AnimatedGifEncoder;
-import io.korhner.asciimg.utils.GifDecoder;
-
-public class GifToAsciiConvert extends AsciiToImageConverter{
-
- public GifToAsciiConvert(AsciiImgCache characterCacher,
- BestCharacterFitStrategy characterFitStrategy) {
- super(characterCacher, characterFitStrategy);
- }
-
- /**
- *
- * @param srcFilePath
- * @param disFilePath
- * @param delay--the delay time(ms) between each frame
- * @param repeat--he number of times the set of GIF frames should be played.0 means play indefinitely.
- * @return
- */
- public int convertGitToAscii(String srcFilePath,String disFilePath,int delay,int repeat){
- GifDecoder decoder = new GifDecoder();
- int status = decoder.read(srcFilePath);
- if(status!=0){
- return -1;//srcfile not exist or open failed!
- }
- AnimatedGifEncoder e = new AnimatedGifEncoder();
- boolean openStatus = e.start(disFilePath);
- if(openStatus){
- e.setDelay(delay); // 1 frame per delay(ms)
- e.setRepeat(repeat);
- // initialize converters
- int frameCount = decoder.getFrameCount();
- for(int i=0;i
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.converter;
+
+import io.korhner.asciimg.image.exporter.MultiFrameAsciiExporter;
+import io.korhner.asciimg.image.importer.BufferedImageImageImporter;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class GifToAsciiConverter extends AbstractToAsciiConverter {
+
+ public GifToAsciiConverter() {}
+
+ @Override
+ public void convert(final InputStream source) throws IOException {
+
+ getImporter().setSource(source);
+
+ // initialize converters
+ final int frameCount = getImporter().getFrames();
+ final MultiFrameAsciiExporter exporter = getExporter();
+ exporter.setCharacterCache(getCharacterCache());
+ exporter.initFrames(frameCount);
+ for (int i = 0; i < frameCount; i++) {
+ final ImageToAsciiConverter frameConverter = new ImageToAsciiConverter();
+ final BufferedImageImageImporter frameImporter = new BufferedImageImageImporter();
+ frameConverter.setImporter(frameImporter);
+ frameConverter.setCharacterFitStrategy(getCharacterFitStrategy());
+ frameConverter.setCharacterCache(getCharacterCache());
+ frameConverter.setExporter(exporter);
+ frameConverter.convert(getImporter().read());
+ }
+ exporter.finalizeFrames();
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/converter/ImageToAsciiConverter.java b/src/main/java/io/korhner/asciimg/image/converter/ImageToAsciiConverter.java
new file mode 100644
index 0000000..14f3b41
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/converter/ImageToAsciiConverter.java
@@ -0,0 +1,98 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.converter;
+
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
+import io.korhner.asciimg.image.matrix.ReferencingTiledImageMatrix;
+import io.korhner.asciimg.image.strategy.CharacterFinder;
+import io.korhner.asciimg.image.transformer.ToGrayscaleImageTransformer;
+import io.korhner.asciimg.image.transformer.TruncatingImageTransformer;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.Map.Entry;
+
+/**
+ * A class used to convert an abstract 32bit ARGB image to an ASCII art.
+ * Output and conversion algorithm are decoupled.
+ */
+public class ImageToAsciiConverter extends AbstractToAsciiConverter {
+
+ public ImageToAsciiConverter() {}
+
+ public void convert(final ImageMatrix input) { // HACK
+
+ // truncate to tile-able size
+ final TruncatingImageTransformer truncater = new TruncatingImageTransformer();
+ truncater.setCharacterCache(getCharacterCache());
+ final ImageMatrix truncated = truncater.transform(input);
+
+ // convert to gray-scale
+ final ToGrayscaleImageTransformer grayScaler = new ToGrayscaleImageTransformer();
+ final ImageMatrix grayScaled = grayScaler.transform(truncated);
+
+ // dimension of each tile
+ final ImageMatrixDimensions tileSize = getCharacterCache().getCharacterImageSize();
+
+ // divide matrix into tiles for easy processing
+ final ReferencingTiledImageMatrix tiledMatrix = new ReferencingTiledImageMatrix<>(
+ grayScaled.getMetaData(), grayScaled, tileSize);
+
+ getExporter().setCharacterCache(getCharacterCache());
+ getExporter().init(tiledMatrix.getSizeInTiles());
+
+ // find best fitting character for each tile
+ // NOTE We go through Y in the outer loop to improve locality
+ // -> low level performance optimization
+ for (int tileY = 0; tileY < tiledMatrix.getSizeInTiles().getHeight(); tileY++) {
+ for (int tileX = 0; tileX < tiledMatrix.getSizeInTiles().getWidth(); tileX++) {
+ // find best fit
+ final Entry> bestFit = new CharacterFinder(
+ getCharacterCache(),
+ getCharacterFitStrategy()).findBestFit(tiledMatrix.getTile(tileX, tileY));
+
+ // copy character to output
+ getExporter().addCharacter(bestFit, tileX, tileY);
+ }
+ }
+
+ getExporter().imageEnd();
+ }
+
+ @Override
+ public void convert(final BufferedImage source) throws IOException {
+
+ getExporter().initFrames(1);
+
+ getImporter().setSource(source);
+ final ImageMatrix input = getImporter().read();
+
+ convert(input);
+
+ getExporter().finalizeFrames();
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/converter/ToAsciiConverter.java b/src/main/java/io/korhner/asciimg/image/converter/ToAsciiConverter.java
new file mode 100644
index 0000000..41c10b4
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/converter/ToAsciiConverter.java
@@ -0,0 +1,72 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.converter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.exporter.MultiFrameAsciiExporter;
+import io.korhner.asciimg.image.importer.ImageImporter;
+import io.korhner.asciimg.image.strategy.CharacterFitStrategy;
+
+import java.io.IOException;
+
+/**
+ * A class used to convert an image to an ASCII art.
+ *
+ * @param
+ * input type of the image (or similar) to be converted to ASCII art
+ * @param
+ * input type of the image (or similar) to be converted to ASCII art
+ */
+public interface ToAsciiConverter {
+
+ ImageImporter getImporter();
+
+ void setImporter(ImageImporter importer);
+
+ /**
+ * The character fit strategy used to determine the best character for each
+ * source image tile.
+ *
+ * @return the character fit strategy
+ */
+ CharacterFitStrategy getCharacterFitStrategy();
+
+ void setCharacterFitStrategy(CharacterFitStrategy characterFitStrategy);
+
+ void setCharacterCache(AsciiImgCache characterCache);
+
+ MultiFrameAsciiExporter getExporter();
+
+ void setExporter(MultiFrameAsciiExporter exporter);
+
+ /**
+ * Produces an output that is an ASCII art of the supplied image.
+ *
+ * @param source the source, non-ASCII image
+ * @throws IOException on any kind of source input error, or output error
+ */
+ void convert(I source) throws IOException;
+}
diff --git a/src/main/java/io/korhner/asciimg/image/exporter/AnimatedGifMultiFrameAsciiExporter.java b/src/main/java/io/korhner/asciimg/image/exporter/AnimatedGifMultiFrameAsciiExporter.java
new file mode 100644
index 0000000..166cdbb
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/exporter/AnimatedGifMultiFrameAsciiExporter.java
@@ -0,0 +1,131 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.exporter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
+import io.korhner.asciimg.utils.AnimatedGifEncoder;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.Map;
+
+public class AnimatedGifMultiFrameAsciiExporter implements MultiFrameAsciiExporter {
+
+ private AsciiImgCache characterCache;
+ private AnimatedGifEncoder encoder;
+ private ByteArrayOutputStream gifBufferStream;
+ /**
+ * The delay time(ms) between each frame.
+ */
+ private int delay;
+ /**
+ * The number of times the set of GIF frames should be played; 0 means play indefinitely.
+ */
+ private int repeat;
+ private MultiFrameAsciiExporter> frameExporter;
+
+ public AnimatedGifMultiFrameAsciiExporter() {}
+
+ public void setDelay(final int delay) {
+ this.delay = delay;
+ }
+
+ public void setRepeat(final int repeat) {
+ this.repeat = repeat;
+ }
+
+ @Override
+ public void setCharacterCache(final AsciiImgCache characterCache) {
+ this.characterCache = characterCache;
+ }
+
+ /**
+ * Copy image data over the source pixels image.
+ */
+ @Override
+ public void addCharacter(
+ final Map.Entry> characterEntry,
+ final int tileX,
+ final int tileY)
+ {
+ frameExporter.addCharacter(characterEntry, tileX, tileY);
+ }
+
+ /**
+ * Called at the beginning of a frame.
+ */
+ @Override
+ public void init(final ImageMatrixDimensions targetDimensions) {
+
+ frameExporter = new ImageAsciiExporter();
+ frameExporter.setCharacterCache(characterCache);
+ frameExporter.initFrames(1);
+ frameExporter.init(targetDimensions);
+ }
+
+ /**
+ * Called at the end of a frame.
+ */
+ @Override
+ public void imageEnd() {
+
+ frameExporter.imageEnd();
+ frameExporter.finalizeFrames();
+ encoder.addFrame(frameExporter.getOutput().get(0));
+ frameExporter = null;
+ }
+
+ /**
+ * Called at the beginning of the animation (before the first frame).
+ */
+ @Override
+ public void initFrames(final int numFrames) {
+
+ encoder = new AnimatedGifEncoder();
+ gifBufferStream = new ByteArrayOutputStream();
+ final boolean openStatus = encoder.start(gifBufferStream);
+ if (openStatus) {
+ encoder.setDelay(delay); // 1 frame per delay(ms)
+ encoder.setRepeat(repeat);
+ }
+ }
+
+ /**
+ * Called at the end of the animation (after tha last frame).
+ */
+ @Override
+ public void finalizeFrames() {
+
+ encoder.finish();
+ }
+
+ @Override
+ public byte[] getOutput() {
+ return gifBufferStream.toByteArray();
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/exporter/AsciiExporter.java b/src/main/java/io/korhner/asciimg/image/exporter/AsciiExporter.java
new file mode 100644
index 0000000..9dc7906
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/exporter/AsciiExporter.java
@@ -0,0 +1,79 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.exporter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
+
+import java.util.Map.Entry;
+
+/**
+ * Exports ASCII art to custom output formats.
+ * This is a state-full object, so each exporter may only be used
+ * for one single image, and may only be used once.
+ *
+ * @param
+ * output container type of the ASCII art
+ */
+public interface AsciiExporter {
+
+ void setCharacterCache(AsciiImgCache characterCache);
+
+ /**
+ * Initializes the inner state of this exporter
+ * to be ready to call {@link #addCharacter}.
+ *
+ * @param targetDimensions
+ * dimensions of the ASCII art "image" in characters
+ */
+ void init(ImageMatrixDimensions targetDimensions);
+
+ /**
+ * Appends one ASCII art character to the internal output.
+ *
+ * @param characterEntry
+ * character chosen as best fit
+ * @param tileX
+ * the tile x position
+ * @param tileY
+ * the tile y position
+ */
+ void addCharacter(
+ Entry> characterEntry,
+ int tileX,
+ int tileY);
+
+ /**
+ * Finalizes the inner state, including the output of this exporter.
+ */
+ void imageEnd();
+
+ /**
+ * @return the output container.
+ */
+ O getOutput();
+}
diff --git a/src/main/java/io/korhner/asciimg/image/exporter/ImageAsciiExporter.java b/src/main/java/io/korhner/asciimg/image/exporter/ImageAsciiExporter.java
new file mode 100644
index 0000000..6e4213c
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/exporter/ImageAsciiExporter.java
@@ -0,0 +1,104 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.exporter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Converts ASCII art to a BufferedImage.
+ */
+public class ImageAsciiExporter implements MultiFrameAsciiExporter> {
+
+ private AsciiImgCache characterCache;
+ private List output;
+ private BufferedImage currentOutput;
+
+ public ImageAsciiExporter() {}
+
+ @Override
+ public void setCharacterCache(final AsciiImgCache characterCache) {
+ this.characterCache = characterCache;
+ }
+
+ @Override
+ public void initFrames(final int numFrame) {
+ output = new ArrayList<>(numFrame);
+ }
+
+ /**
+ * Copy image data over the source pixels image.
+ */
+ @Override
+ public void addCharacter(
+ final Entry> characterEntry,
+ final int tileX,
+ final int tileY)
+ {
+ final int startCoordinateX = tileX * characterCache.getCharacterImageSize().getWidth();
+ final int startCoordinateY = tileY * characterCache.getCharacterImageSize().getHeight();
+
+ // copy winner character
+ for (int cpx = 0; cpx < characterEntry.getValue().getDimensions().getWidth(); cpx++) {
+ for (int cpy = 0; cpy < characterEntry.getValue().getDimensions().getHeight(); cpy++) {
+ final int component = (int) characterEntry.getValue().getValue(cpx, cpy);
+ currentOutput.setRGB(
+ startCoordinateX + cpx,
+ startCoordinateY + cpy,
+ new Color(component, component, component).getRGB());
+ }
+ }
+ }
+
+ @Override
+ public void imageEnd() {}
+
+ /**
+ * Create an empty buffered image.
+ */
+ @Override
+ public void init(final ImageMatrixDimensions targetDimensions) {
+
+ final int imageWidth = targetDimensions.getWidth() * characterCache.getCharacterImageSize().getWidth();
+ final int imageHeight = targetDimensions.getHeight() * characterCache.getCharacterImageSize().getHeight();
+ currentOutput = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
+ output.add(currentOutput);
+ }
+
+ @Override
+ public List getOutput() {
+ return output;
+ }
+
+ @Override
+ public void finalizeFrames() {}
+}
diff --git a/src/main/java/io/korhner/asciimg/image/exporter/MultiFrameAsciiExporter.java b/src/main/java/io/korhner/asciimg/image/exporter/MultiFrameAsciiExporter.java
new file mode 100644
index 0000000..00c8b07
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/exporter/MultiFrameAsciiExporter.java
@@ -0,0 +1,52 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.exporter;
+
+/**
+ * Exports multi-frame ASCII art to custom output formats.
+ * These formats might typically be GIF or AVI.
+ *
+ * @param
+ * output container type of the ASCII art
+ */
+public interface MultiFrameAsciiExporter extends AsciiExporter {
+
+
+ /**
+ * Initializes the inner state of this exporter
+ * to be ready to start exporting the next frame.
+ *
+ * @param numFrame
+ * number of frames of the complete animation
+ */
+ void initFrames(int numFrame);
+
+
+ /**
+ * Tells this exporter to finalize the current frame.
+ */
+ void finalizeFrames();
+}
diff --git a/src/main/java/io/korhner/asciimg/image/exporter/TextAsciiExporter.java b/src/main/java/io/korhner/asciimg/image/exporter/TextAsciiExporter.java
new file mode 100644
index 0000000..8be740e
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/exporter/TextAsciiExporter.java
@@ -0,0 +1,93 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.exporter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Converts ASCII art to text.
+ */
+public class TextAsciiExporter implements MultiFrameAsciiExporter> {
+
+ /** Width of the output in characters. */
+ private int width;
+ private List output;
+ private StringBuilder currentOutput;
+
+ public TextAsciiExporter() {}
+
+ @Override
+ public void setCharacterCache(final AsciiImgCache characterCache) {}
+
+ @Override
+ public void initFrames(final int numFrame) {
+ output = new ArrayList<>(numFrame);
+ }
+
+ @Override
+ public void init(final ImageMatrixDimensions targetDimensions) {
+
+ this.width = targetDimensions.getWidth();
+ // each tile and each new-line is a char
+ currentOutput = new StringBuilder((targetDimensions.getWidth() + 1) * targetDimensions.getHeight());
+ }
+
+ @Override
+ public void imageEnd() {
+ output.add(currentOutput.toString());
+ }
+
+ /**
+ * Append chosen character to the output buffer.
+ */
+ @Override
+ public void addCharacter(
+ final Entry> characterEntry,
+ final int tileX,
+ final int tileY)
+ {
+ currentOutput.append(characterEntry.getKey());
+
+ // append new line at the end of the row
+ if ((tileX + 1) == width) {
+ currentOutput.append(System.lineSeparator());
+ }
+ }
+
+ @Override
+ public List getOutput() {
+ return output;
+ }
+
+ @Override
+ public void finalizeFrames() {}
+}
diff --git a/src/main/java/io/korhner/asciimg/image/importer/BufferedImageImageImporter.java b/src/main/java/io/korhner/asciimg/image/importer/BufferedImageImageImporter.java
new file mode 100644
index 0000000..3baf558
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/importer/BufferedImageImageImporter.java
@@ -0,0 +1,74 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.importer;
+
+import io.korhner.asciimg.image.matrix.BasicImageMatrixInfo;
+import io.korhner.asciimg.image.matrix.BasicInt1DImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
+import io.korhner.asciimg.image.matrix.ImageMatrixInfo;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+
+/**
+ * A class used to read/import an image through AWT.
+ */
+public class BufferedImageImageImporter implements ImageImporter {
+
+ public static final ImageMatrixInfo META_DATA
+ = new BasicImageMatrixInfo(4, Integer.class, 8);
+ private BufferedImage source;
+
+ @Override
+ public void setSource(final BufferedImage source) {
+ this.source = source;
+ }
+
+ @Override
+ public int getFrames() {
+ return 1;
+ }
+
+ @Override
+ public ImageMatrix read() throws IOException {
+
+ if (source == null) {
+ throw new IOException("Input source not set");
+ }
+
+ final ImageMatrixDimensions sourcePixelsSize = new ImageMatrixDimensions(source.getWidth(), source.getHeight());
+
+ // extract pixels from source image
+ final int[] imagePixels = source.getRGB(
+ 0, 0,
+ sourcePixelsSize.getWidth(), sourcePixelsSize.getHeight(),
+ null, 0, sourcePixelsSize.getWidth());
+
+ // process the pixels to a gray-scale matrix
+ return new BasicInt1DImageMatrix(META_DATA, imagePixels, sourcePixelsSize.getWidth());
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/importer/GifImageImporter.java b/src/main/java/io/korhner/asciimg/image/importer/GifImageImporter.java
new file mode 100644
index 0000000..212b8b0
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/importer/GifImageImporter.java
@@ -0,0 +1,90 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.importer;
+
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.utils.GifDecoder;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class GifImageImporter implements ImageImporter {
+
+ private InputStream source;
+ private GifDecoder decoder;
+ private int frameCount = -1;
+ private int framesRead = -1;
+
+ @Override
+ public void setSource(final InputStream source) {
+ this.source = source;
+ }
+
+ private void initDecoder() throws IOException {
+
+ if (decoder == null) {
+ if (source == null) {
+ throw new IOException("Input source not set");
+ }
+
+ decoder = new GifDecoder();
+ final int status = decoder.read(source);
+ if (status != 0) {
+ throw new IOException(String.format(
+ "Failed to read GIF source from '%s', error: %d",
+ String.valueOf(source), status));
+ }
+ frameCount = decoder.getFrameCount();
+ framesRead = 0;
+ }
+ }
+
+ @Override
+ public int getFrames() throws IOException {
+
+ initDecoder();
+
+ return frameCount;
+ }
+
+ @Override
+ public ImageMatrix read() throws IOException {
+
+ initDecoder();
+
+ ImageMatrix result;
+ if (framesRead < frameCount) {
+ final BufferedImageImageImporter frameImporter = new BufferedImageImageImporter();
+ frameImporter.setSource(decoder.getFrame(framesRead));
+ result = frameImporter.read();
+ framesRead++;
+ } else {
+ throw new IOException("No more frames to be read");
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/importer/ImageImporter.java b/src/main/java/io/korhner/asciimg/image/importer/ImageImporter.java
new file mode 100644
index 0000000..25c58c7
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/importer/ImageImporter.java
@@ -0,0 +1,67 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.importer;
+
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import java.io.IOException;
+
+/**
+ * Used to read (import) different image types to the internal format.
+ *
+ * @param
+ * input type of the image (or similar) to be imported
+ * @param
+ * output data-point value type of the internal version of the imported image
+ */
+public interface ImageImporter {
+
+ /**
+ * Sets the source, an image (or similar) that is to be read, later on.
+ *
+ * @param source the source, non-ASCII image
+ * @throws IOException on any kind of input error
+ */
+ void setSource(I source) throws IOException;
+
+ /**
+ * Indicates the number of frames contained in the associated source image (or similar).
+ * This will usually be 1, but potentially more in the case of a GIF, for example.
+ * The {@link #read()} method may be called this many times to get all the frames.
+ *
+ * @return usually 1, but potentially more, for example in the case of an animated GIF
+ * @throws IOException if this info could not be read from the assigned source
+ */
+ int getFrames() throws IOException;
+
+ /**
+ * Reads an image (or similar) into the internal format.
+ *
+ * @return the contents of the imported source,
+ * which is usually either an image or a frame of an animation
+ * @throws IOException on any kind of input error
+ */
+ ImageMatrix read() throws IOException;
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/AbstractImageMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/AbstractImageMatrix.java
new file mode 100644
index 0000000..99dfbcb
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/AbstractImageMatrix.java
@@ -0,0 +1,59 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * Abstract implementation of {@link ImageMatrix}, just missing the data.
+ */
+public abstract class AbstractImageMatrix implements ImageMatrix {
+
+ private final ImageMatrixInfo metaData;
+ private final ImageMatrixDimensions dimensions;
+
+ /**
+ * Creates an image with the given dimensions.
+ *
+ * @param metaData
+ * image meta data
+ * @param dimensions
+ * image width and height
+ */
+ public AbstractImageMatrix(final ImageMatrixInfo metaData, final ImageMatrixDimensions dimensions) {
+
+ this.metaData = metaData;
+ this.dimensions = dimensions;
+ }
+
+ @Override
+ public ImageMatrixInfo getMetaData() {
+ return metaData;
+ }
+
+ @Override
+ public ImageMatrixDimensions getDimensions() {
+ return dimensions;
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/BasicImageMatrixInfo.java b/src/main/java/io/korhner/asciimg/image/matrix/BasicImageMatrixInfo.java
new file mode 100644
index 0000000..bce0f28
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/BasicImageMatrixInfo.java
@@ -0,0 +1,81 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * Contains basic image meta data.
+ */
+public class BasicImageMatrixInfo implements ImageMatrixInfo {
+
+ private final int valuesPerDataPoint;
+ private final Class dataPointClass;
+ private final int bitsPerValue;
+
+ public BasicImageMatrixInfo(
+ final int valuesPerDataPoint,
+ final Class dataPointClass,
+ final int bitsPerValue)
+ {
+ this.valuesPerDataPoint = valuesPerDataPoint;
+ this.dataPointClass = dataPointClass;
+ this.bitsPerValue = bitsPerValue;
+ }
+
+ @Override
+ public boolean isGrayScale() {
+ return !isColored() && !isBlackAndWhite();
+ }
+
+ @Override
+ public boolean isBlackAndWhite() {
+ return !isColored() && bitsPerValue == 1;
+ }
+
+ @Override
+ public boolean isColored() {
+ return valuesPerDataPoint > 2;
+ }
+
+ @Override
+ public boolean isWithAlpha() {
+ return valuesPerDataPoint == 2 || valuesPerDataPoint == 4;
+ }
+
+ @Override
+ public int getValuesPerDataPoint() {
+ return valuesPerDataPoint;
+ }
+
+ @Override
+ public Class getDataPointClass() {
+ return dataPointClass;
+ }
+
+ @Override
+ public int getBitsPerValue() {
+ return bitsPerValue;
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/BasicInt1DImageMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/BasicInt1DImageMatrix.java
new file mode 100644
index 0000000..67cc9ab
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/BasicInt1DImageMatrix.java
@@ -0,0 +1,64 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+import io.korhner.asciimg.utils.ArrayUtils;
+
+/**
+ * Basic implementation of {@link ImageMatrix}, backed by an int[].
+ */
+public class BasicInt1DImageMatrix extends AbstractImageMatrix {
+
+ /**
+ * The images data points.
+ */
+ private final int[] data;
+
+ /**
+ * Creates an empty image with the given dimensions.
+ *
+ * @param metaData
+ * image meta data
+ * @param data
+ * image data points
+ * @param width
+ * image width in number of data points
+ */
+ public BasicInt1DImageMatrix(final ImageMatrixInfo metaData, final int[] data, final int width) {
+ super(metaData, new ImageMatrixDimensions(width, data.length / width));
+
+ if (data.length % width != 0) {
+ throw new IllegalArgumentException("width does not divide data");
+ }
+
+ this.data = data;
+ }
+
+ @Override
+ public Integer getValue(final int posX, final int posY) {
+ return data[ArrayUtils.convert2DTo1D(posX, posY, getDimensions().getWidth())];
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/GrayScaleMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/GrayScaleMatrix.java
new file mode 100644
index 0000000..37fbc0d
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/GrayScaleMatrix.java
@@ -0,0 +1,76 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * Encapsulates a gray-scale image.
+ * Gray scale values are ints with values from black=0, to white=255.
+ */
+public class GrayScaleMatrix extends AbstractImageMatrix {
+
+ public static final ImageMatrixInfo META_DATA
+ = new BasicImageMatrixInfo(1, Short.class, 8);
+ private final ImageMatrix argbImage;
+
+ /**
+ * Instantiates a new gray-scale matrix, backed by an ARGB bitmap image.
+ *
+ * @param argbImage
+ * pixel data in 32bit ARGB format
+ */
+ public GrayScaleMatrix(final ImageMatrix argbImage) {
+ super(META_DATA, argbImage.getDimensions());
+
+ this.argbImage = argbImage;
+ }
+
+ /**
+ * Convert 32bit ARGB color to 8bit gray-scale value.
+ *
+ * @param rgbColor
+ * ARGB color with 8bit per color component
+ * @return gray-scale value between 0 and 255.
+ */
+ private static short convertRGBToGrayScale(final int rgbColor) {
+
+ // extract components
+ final int red = (rgbColor >> 16) & 0xFF;
+ final int green = (rgbColor >> 8) & 0xFF;
+ final int blue = rgbColor & 0xFF;
+
+ // convert to gray-scale
+ final float grayScale = 0.3f * red + 0.59f * green + 0.11f * blue;
+
+ // This should not be required
+ //return (short) Math.min(Math.round(grayScale), 255);
+ return (short) grayScale;
+ }
+
+ @Override
+ public Short getValue(final int posX, final int posY) {
+ return convertRGBToGrayScale(argbImage.getValue(posX, posY));
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/GrayscaleMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/GrayscaleMatrix.java
deleted file mode 100644
index 5e231c2..0000000
--- a/src/main/java/io/korhner/asciimg/image/matrix/GrayscaleMatrix.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package io.korhner.asciimg.image.matrix;
-
-import io.korhner.asciimg.utils.ArrayUtils;
-
-/**
- * A class that encapsulates a grayscale image. Color values are floats with
- * values between 0.0f and 255.0f.
- */
-public class GrayscaleMatrix {
-
- /**
- * Creates a new matrix from a sub region.
- *
- * @param source
- * source matrix
- * @param width
- * sub region width
- * @param height
- * subregion height
- * @param startPixelX
- * x coordinate of sub region start
- * @param startPixelY
- * y coordinate of sub region start
- * @return matrix containing the specified sub region
- */
- public static GrayscaleMatrix createFromRegion(
- final GrayscaleMatrix source, final int width, final int height,
- final int startPixelX, final int startPixelY) {
- if (width <= 0 || height <= 0 || width > source.width
- || height > source.height) {
- throw new IllegalArgumentException("Illegal sub region size!");
- }
-
- GrayscaleMatrix output = new GrayscaleMatrix(width, height);
-
- for (int i = 0; i < output.data.length; i++) {
- int xOffset = i % width;
- int yOffset = i / width;
-
- int index = ArrayUtils.convert2DTo1D(startPixelX + xOffset,
- startPixelY + yOffset, source.width);
- output.data[i] = source.data[index];
- }
-
- return output;
- }
-
- /** Grayscale pixel data. Values are between 0.0f and 255.0f. */
- private final float data[];
-
- /** Image width. */
- private final int width;
-
- /** Image height. */
- private final int height;
-
- /**
- * Creates an empty image with the given dimensions.
- *
- * @param width
- * image width
- * @param height
- * image height
- */
- public GrayscaleMatrix(final int width, final int height) {
- this.data = new float[width * height];
- this.width = width;
- this.height = height;
- }
-
- /**
- * Instantiates a new grayscale matrix from a ARGB bitmap image.
- *
- * @param pixels
- * pixel data in ARGB format
- * @param width
- * image width
- * @param height
- * image height
- */
- public GrayscaleMatrix(final int[] pixels, final int width, final int height) {
- this(width, height);
-
- if (width * height != pixels.length) {
- throw new IllegalArgumentException(
- "Pixels array does not match specified width and height!");
- }
-
- for (int i = 0; i < this.data.length; i++) {
- this.data[i] = convertRGBToGrayscale(pixels[i]);
- }
- }
-
- /**
- * Convert ARGB color to grayscale float.
- *
- * @param rgbColor
- * ARGB color
- * @return Grayscale float with value between 0.0f and 255.0f.
- */
- private float convertRGBToGrayscale(final int rgbColor) {
- // extract components
- int red = (rgbColor >> 16) & 0xFF;
- int green = (rgbColor >> 8) & 0xFF;
- int blue = rgbColor & 0xFF;
-
- // convert to grayscale
- return 0.3f * red + 0.59f * green + 0.11f * blue;
- }
-
- /**
- * Gets a reference to pixel array.
- *
- * @return pixel array
- */
- public float[] getData() {
- return this.data;
- }
-
- /**
- * Gets the image height.
- *
- * @return the height
- */
- public int getHeight() {
- return this.height;
- }
-
- /**
- * Gets the image width.
- *
- * @return image width
- */
- public int getWidth() {
- return this.width;
- }
-
-}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrix.java
new file mode 100644
index 0000000..f7aeab2
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrix.java
@@ -0,0 +1,49 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * Encapsulates an images raw, basic data.
+ * This could be characters of an ASCII image, or gray scale or RGB values of a bitmap image.
+ */
+public interface ImageMatrix {
+
+ ImageMatrixInfo getMetaData();
+
+ /**
+ * Returns the value at a specified position.
+ *
+ * @param posX x-coordinate of the data point to fetch
+ * @param posY y-coordinate of the data point to fetch
+ * @return data point value
+ */
+ T getValue(final int posX, final int posY);
+
+ /**
+ * @return the images dimensions
+ */
+ ImageMatrixDimensions getDimensions();
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrixDimensions.java b/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrixDimensions.java
new file mode 100644
index 0000000..951473b
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrixDimensions.java
@@ -0,0 +1,49 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * Contains 2D image dimensions.
+ */
+public class ImageMatrixDimensions {
+
+ private final int width;
+ private final int height;
+
+ public ImageMatrixDimensions(final int width, final int height) {
+
+ this.width = width;
+ this.height = height;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrixInfo.java b/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrixInfo.java
new file mode 100644
index 0000000..7734709
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/ImageMatrixInfo.java
@@ -0,0 +1,62 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * Contains basic image meta data.
+ */
+public interface ImageMatrixInfo {
+
+ boolean isGrayScale();
+ boolean isBlackAndWhite();
+ boolean isColored();
+ boolean isWithAlpha();
+
+ /**
+ * Indicates the number of values per data point.
+ * Examples:
+ * - ARGB: 4
+ * - RGB: 3
+ * - grey scale: 1
+ * - black & white: 1
+ * @return the number of values per data point
+ */
+ int getValuesPerDataPoint();
+
+ Class getDataPointClass();
+
+ /**
+ * Indicates the number of bits used to represent each value (see {@link #getValuesPerDataPoint()}).
+ * Examples:
+ * - ARGB: 8
+ * - RGB: 8
+ * - grey scale: 8
+ * - black & white: 1
+ * @return the number of bits used to represent each value withing the data-point
+ */
+ int getBitsPerValue();
+
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/ReferencingTiledImageMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/ReferencingTiledImageMatrix.java
new file mode 100644
index 0000000..1adce9b
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/ReferencingTiledImageMatrix.java
@@ -0,0 +1,160 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+import io.korhner.asciimg.utils.ArrayUtils;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Referencing implementation of {@link TiledImageMatrix}.
+ */
+public class ReferencingTiledImageMatrix extends AbstractList> implements TiledImageMatrix {
+
+ private final ImageMatrix original;
+ private final ImageMatrixInfo metaData;
+
+ /** The tiles. */
+ private final List> tiles;
+
+ /** Dimensions of a tile in data points. */
+ private final ImageMatrixDimensions tileSize;
+
+ /** Number of tiles on the x and y axes. */
+ private final ImageMatrixDimensions sizeInTiles;
+
+ /**
+ * Instantiates a new tiled image matrix.
+ *
+ * @param metaData
+ * image meta data
+ * @param original
+ * the source matrix
+ * @param tileDimensions
+ * the tile width and height
+ */
+ public ReferencingTiledImageMatrix(
+ final ImageMatrixInfo metaData,
+ final ImageMatrix original,
+ final ImageMatrixDimensions tileDimensions)
+ {
+ final int tileWidth = tileDimensions.getWidth();
+ if (tileWidth <= 0) {
+ throw new IllegalArgumentException("Tile width has to be positive!");
+ }
+ final int tileHeight = tileDimensions.getHeight();
+ if (tileHeight <= 0) {
+ throw new IllegalArgumentException("Tile height has to be positive!");
+ }
+ final int imageWidth = original.getDimensions().getWidth();
+ if (tileWidth > imageWidth) {
+ throw new IllegalArgumentException("Tile width larger then original images width!");
+ }
+ final int imageHeight = original.getDimensions().getHeight();
+ if (tileHeight > imageHeight) {
+ throw new IllegalArgumentException("Tile height larger then original images height!");
+ }
+ // we won't allow partial tiles
+ if (imageWidth % tileWidth != 0) {
+ throw new IllegalArgumentException("Tile width does not divide the original images width!");
+ }
+ if (imageHeight % tileHeight != 0) {
+ throw new IllegalArgumentException("Tile height does not divide the original images height!");
+ }
+
+ this.original = original;
+ this.metaData = metaData;
+ this.tileSize = tileDimensions;
+
+ this.sizeInTiles = new ImageMatrixDimensions(
+ imageWidth / tileWidth,
+ imageHeight / tileHeight);
+
+ tiles = new ArrayList<>(imageWidth * imageHeight);
+
+ // create each tile as a sub-region, referencing the original matrix
+ for (int y = 0; y < sizeInTiles.getHeight(); y++) {
+ for (int x = 0; x < sizeInTiles.getWidth(); x++) {
+ tiles.add(new RegionImageMatrix<>(
+ original,
+ tileDimensions,
+ tileWidth * x, tileHeight * y));
+ }
+ }
+ }
+
+ @Override
+ public ImageMatrixInfo getMetaData() {
+ return metaData;
+ }
+
+ @Override
+ public ImageMatrixDimensions getDimensions() {
+ return original.getDimensions();
+ }
+
+ @Override
+ public V getValue(final int posX, final int posY) {
+ return original.getValue(posX, posY);
+ }
+
+ @Override
+ public ImageMatrix getTile(final int index) {
+ return this.tiles.get(index);
+ }
+
+ @Override
+ public ImageMatrix getTile(final int x, final int y) {
+ return this.tiles.get(ArrayUtils.convert2DTo1D(x, y, sizeInTiles.getWidth()));
+ }
+
+ @Override
+ public int getTileCount() {
+ return this.tiles.size();
+ }
+
+ @Override
+ public ImageMatrixDimensions getTileSize() {
+ return tileSize;
+ }
+
+ @Override
+ public ImageMatrixDimensions getSizeInTiles() {
+ return sizeInTiles;
+ }
+
+ @Override
+ public ImageMatrix get(final int index) {
+ return getTile(index);
+ }
+
+ @Override
+ public int size() {
+ return getTileCount();
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/RegionImageMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/RegionImageMatrix.java
new file mode 100644
index 0000000..412d63b
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/RegionImageMatrix.java
@@ -0,0 +1,73 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * A referencing representation of a sub-region of an image.
+ */
+public class RegionImageMatrix extends AbstractImageMatrix {
+
+ /**
+ * Origin/source/referencing image.
+ */
+ private final ImageMatrix origin;
+ private final int startPixelX;
+ private final int startPixelY;
+
+ /**
+ * @param origin
+ * original/referenced image
+ * @param regionDimensions
+ * width and height of the sub-region (in number of data points) to be represented
+ * @param startPixelX
+ * start data point index of the sub-region on the x-axis
+ * @param startPixelY
+ * start data point index of the sub-region on the y-axis
+ */
+ public RegionImageMatrix(
+ final ImageMatrix origin,
+ final ImageMatrixDimensions regionDimensions,
+ final int startPixelX, final int startPixelY)
+ {
+ super(origin.getMetaData(), regionDimensions);
+
+ this.origin = origin;
+ this.startPixelX = startPixelX;
+ this.startPixelY = startPixelY;
+
+ assert startPixelX >= 0;
+ assert startPixelY >= 0;
+ assert regionDimensions.getWidth() > 0;
+ assert regionDimensions.getHeight() > 0;
+ assert startPixelX + regionDimensions.getWidth() <= origin.getDimensions().getWidth();
+ assert startPixelY + regionDimensions.getHeight() <= origin.getDimensions().getHeight();
+ }
+
+ @Override
+ public T getValue(final int posX, final int posY) {
+ return origin.getValue(startPixelX + posX, startPixelY + posY);
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/TiledGrayscaleMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/TiledGrayscaleMatrix.java
deleted file mode 100644
index 5b1ab03..0000000
--- a/src/main/java/io/korhner/asciimg/image/matrix/TiledGrayscaleMatrix.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package io.korhner.asciimg.image.matrix;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * A class for for creating mutliple tiles from an input grayscale matrix.
- */
-public class TiledGrayscaleMatrix {
-
- /** The tiles. */
- private final List tiles;
-
- /** Width of a tile. */
- private final int tileWidth;
-
- /** Height of a tile. */
- private final int tileHeight;
-
- /** Number of tiles on x axis. */
- private final int tilesX;
-
- /** Number of tiles on y axis. */
- private final int tilesY;
-
- /**
- * Instantiates a new tiled grayscale matrix.
- *
- * @param matrix
- * the source matrix
- * @param tileWidth
- * the tile width
- * @param tileHeight
- * the tile height
- */
- public TiledGrayscaleMatrix(final GrayscaleMatrix matrix,
- final int tileWidth, final int tileHeight) {
-
- if (matrix.getWidth() < tileWidth || matrix.getHeight() < tileHeight) {
- throw new IllegalArgumentException(
- "Tile size must be smaller than original matrix!");
- }
-
- if (tileWidth <= 0 || tileHeight <= 0) {
- throw new IllegalArgumentException("Illegal tile size!");
- }
-
- this.tileWidth = tileWidth;
- this.tileHeight = tileHeight;
-
- // we won't allow partial tiles
- this.tilesX = matrix.getWidth() / tileWidth;
- this.tilesY = matrix.getHeight() / tileHeight;
- int roundedWidth = tilesX * tileWidth;
- int roundedHeight = tilesY * tileHeight;
-
- tiles = new ArrayList(roundedWidth * roundedHeight);
-
- // create each tile as a subregion from source matrix
- for (int i = 0; i < tilesY; i++) {
- for (int j = 0; j < tilesX; j++) {
- tiles.add(GrayscaleMatrix.createFromRegion(matrix, tileWidth,
- tileHeight, this.tileWidth * j, this.tileHeight * i));
- }
- }
- }
-
- /**
- * Gets the tile at a specific index.
- *
- * @param index
- * tile index
- * @return the tile
- */
- public GrayscaleMatrix getTile(final int index) {
- return this.tiles.get(index);
- }
-
- /**
- * Gets the number of tiles.
- *
- * @return the number of tiles
- */
- public int getTileCount() {
- return this.tiles.size();
- }
-
- /**
- * Gets the tile y size.
- *
- * @return the tile y size
- */
- public int getTileHeight() {
- return this.tileHeight;
- }
-
- /**
- * Gets the number of tiles on x axis.
- *
- * @return number of tiles on x axis
- */
- public int getTilesX() {
- return this.tilesX;
- }
-
- /**
- * Gets the number of tiles on y axis.
- *
- * @return number of tiles on y axis
- */
- public int getTilesY() {
- return this.tilesY;
- }
-
- /**
- * Gets the tile width.
- *
- * @return tile width
- */
- public int getTileWidth() {
- return this.tileWidth;
- }
-}
diff --git a/src/main/java/io/korhner/asciimg/image/matrix/TiledImageMatrix.java b/src/main/java/io/korhner/asciimg/image/matrix/TiledImageMatrix.java
new file mode 100644
index 0000000..61c0842
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/matrix/TiledImageMatrix.java
@@ -0,0 +1,72 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.matrix;
+
+/**
+ * Separates an input image into multiple tiles.
+ * You may want ot think of the original image a sa chess-board,
+ * and the tiles as the fields of the board.
+ *
+ * @param
+ * data point value type, as in, the class of a "pixel" of the image
+ */
+public interface TiledImageMatrix extends ImageMatrix, Iterable> {
+
+ /**
+ * Gets the tile at a specific index.
+ *
+ * @param index
+ * tile index
+ * @return the tile
+ */
+ ImageMatrix getTile(final int index);
+
+ /**
+ * Gets the tile at a specific y and z location.
+ *
+ * @param x
+ * x location of the tile to fetch
+ * @param y
+ * y location of the tile to fetch
+ * @return the tile
+ */
+ ImageMatrix getTile(final int x, final int y);
+
+ /**
+ * @return the number of tiles
+ */
+ int getTileCount();
+
+ /**
+ * @return size of a tile in data points
+ */
+ ImageMatrixDimensions getTileSize();
+
+ /**
+ * @return number of tiles on the x and y axis
+ */
+ ImageMatrixDimensions getSizeInTiles();
+}
diff --git a/src/main/java/io/korhner/asciimg/image/strategy/CharacterFinder.java b/src/main/java/io/korhner/asciimg/image/strategy/CharacterFinder.java
new file mode 100644
index 0000000..6c133d5
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/strategy/CharacterFinder.java
@@ -0,0 +1,82 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.strategy;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+
+import java.util.Map;
+
+/**
+ * Encapsulates the algorithm for choosing the best fit character.
+ */
+public class CharacterFinder {
+
+ private final AsciiImgCache characterCache;
+ private final CharacterFitStrategy characterFitStrategy;
+
+ public CharacterFinder(final AsciiImgCache characterCache, final CharacterFitStrategy characterFitStrategy) {
+
+ this.characterCache = characterCache;
+ this.characterFitStrategy = characterFitStrategy;
+ }
+
+ /**
+ * Returns the best fit character for a given tile (part of an image).
+ *
+ * @param tile
+ * the tile to find a fit for
+ * @return the character with minimum error
+ */
+ public Map.Entry> findBestFit(final ImageMatrix tile) {
+
+ Map.Entry> bestFit = null;
+
+ // XXX This could be speed up (in case of a large character set), by arranging characters in a tree, with the non-leafs being lower-resolution representations of their children. this requires creating a lower-resolution version of each tile, though.
+ // TODO Maybe check how libcaca does this, as it is probably quite heavily optimized
+ float minError = Float.MAX_VALUE;
+ for (final Map.Entry> charImage : getCharacterCache()) {
+ final ImageMatrix charPixels = charImage.getValue();
+
+ final float error = getCharacterFitStrategy().calculateError(charPixels, tile);
+
+ if (error < minError) {
+ minError = error;
+ bestFit = charImage;
+ }
+ }
+
+ return bestFit;
+ }
+
+ protected AsciiImgCache getCharacterCache() {
+ return characterCache;
+ }
+
+ protected CharacterFitStrategy getCharacterFitStrategy() {
+ return characterFitStrategy;
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/strategy/CharacterFitStrategy.java b/src/main/java/io/korhner/asciimg/image/strategy/CharacterFitStrategy.java
new file mode 100644
index 0000000..c630e14
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/strategy/CharacterFitStrategy.java
@@ -0,0 +1,47 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.strategy;
+
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+
+/**
+ * Evaluates how well a given character fits to a tile (part of an image).
+ */
+public interface CharacterFitStrategy {
+
+ /**
+ * Returns the error between the character and tile matrices.
+ * The character with minimum error wins.
+ *
+ * @param character
+ * the character
+ * @param tile
+ * the tile
+ * @return error. Less values mean better fit. Least value character will be
+ * chosen as best fit.
+ */
+ float calculateError(final ImageMatrix character, final ImageMatrix tile);
+}
diff --git a/src/main/java/io/korhner/asciimg/image/strategy/ColorSquareErrorCharacterFitStrategy.java b/src/main/java/io/korhner/asciimg/image/strategy/ColorSquareErrorCharacterFitStrategy.java
new file mode 100644
index 0000000..30b8789
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/strategy/ColorSquareErrorCharacterFitStrategy.java
@@ -0,0 +1,54 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.strategy;
+
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+
+/**
+ * Calculates the squared mean error over all pixels between two images.
+ */
+public class ColorSquareErrorCharacterFitStrategy implements CharacterFitStrategy {
+
+ @Override
+ public float calculateError(final ImageMatrix character, final ImageMatrix tile) {
+
+ float error = 0;
+
+ // calculate sum of squared difference over all character pixels
+ for (int cpx = 0; cpx < character.getDimensions().getWidth(); cpx++) {
+ for (int cpy = 0; cpy < character.getDimensions().getHeight(); cpy++) {
+ final short pixelVal1 = character.getValue(cpx, cpy);
+ final short pixelVal2 = tile.getValue(cpx, cpy);
+
+ final float colorDiff = pixelVal1 - pixelVal2;
+ error += colorDiff * colorDiff;
+ }
+ }
+
+ final int numPixels = character.getDimensions().getWidth() * character.getDimensions().getHeight();
+ return error / numPixels;
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/strategy/StructuralSimilarityCharacterFitStrategy.java b/src/main/java/io/korhner/asciimg/image/strategy/StructuralSimilarityCharacterFitStrategy.java
new file mode 100644
index 0000000..24296f4
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/strategy/StructuralSimilarityCharacterFitStrategy.java
@@ -0,0 +1,61 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.strategy;
+
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+
+/**
+ * Calculates Structural Similarity index (SSIM) between the images.
+ *
+ * See http://en.wikipedia.org/wiki/Structural_similarity for more info.
+ */
+public class StructuralSimilarityCharacterFitStrategy implements CharacterFitStrategy {
+
+ private static final float K_1 = 0.01f;
+ private static final float K_2 = 0.03f;
+ private static final float L = 255f;
+ private static final float C_1 = (float) Math.pow(K_1 * L, 2);
+ private static final float C_2 = (float) Math.pow(K_2 * L, 2);
+
+ @Override
+ public float calculateError(final ImageMatrix character, final ImageMatrix tile) {
+
+ float score = 0f;
+ for (int cpx = 0; cpx < character.getDimensions().getWidth(); cpx++) {
+ for (int cpy = 0; cpy < character.getDimensions().getHeight(); cpy++) {
+ final float pixelVal1 = character.getValue(cpx, cpy);
+ final float pixelVal2 = tile.getValue(cpx, cpy);
+
+ score += (2 * pixelVal1 * pixelVal2 + C_1) * (2 + C_2)
+ / (pixelVal1 * pixelVal1 + pixelVal2 * pixelVal2 + C_1) / C_2;
+ }
+ }
+
+ final int numPixels = character.getDimensions().getWidth() * character.getDimensions().getHeight();
+ // average and convert score to error
+ return 1 - (score / numPixels);
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/transformer/ImageTransformer.java b/src/main/java/io/korhner/asciimg/image/transformer/ImageTransformer.java
new file mode 100644
index 0000000..5143edf
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/transformer/ImageTransformer.java
@@ -0,0 +1,48 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.transformer;
+
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+
+/**
+ * Transforms one internal image representation into an other.
+ *
+ * @param
+ * data-point value type of the input image
+ * @param
+ * data-point value type of the output image
+ */
+public interface ImageTransformer {
+
+ /**
+ * Transforms one internal image representation into an other.
+ *
+ * @param source
+ * data-point value type of the input image
+ * @return data-point value type of the output image
+ */
+ ImageMatrix transform(ImageMatrix source);
+}
diff --git a/src/main/java/io/korhner/asciimg/image/transformer/ToGrayscaleImageTransformer.java b/src/main/java/io/korhner/asciimg/image/transformer/ToGrayscaleImageTransformer.java
new file mode 100644
index 0000000..578a317
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/transformer/ToGrayscaleImageTransformer.java
@@ -0,0 +1,41 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.transformer;
+
+import io.korhner.asciimg.image.matrix.GrayScaleMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+
+/**
+ * Transforms a colored input into gray-scale output.
+ */
+public class ToGrayscaleImageTransformer implements ImageTransformer {
+
+ @Override
+ public ImageMatrix transform(final ImageMatrix source) {
+
+ return new GrayScaleMatrix(source);
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/image/transformer/TruncatingImageTransformer.java b/src/main/java/io/korhner/asciimg/image/transformer/TruncatingImageTransformer.java
new file mode 100644
index 0000000..bd66d36
--- /dev/null
+++ b/src/main/java/io/korhner/asciimg/image/transformer/TruncatingImageTransformer.java
@@ -0,0 +1,69 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.transformer;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.matrix.ImageMatrix;
+import io.korhner.asciimg.image.matrix.ImageMatrixDimensions;
+import io.korhner.asciimg.image.matrix.RegionImageMatrix;
+
+/**
+ * Discards superfluous pixels on the right and on the top of the image.
+ */
+public class TruncatingImageTransformer implements ImageTransformer {
+
+ private AsciiImgCache characterCache;
+
+ public void setCharacterCache(final AsciiImgCache characterCache) {
+ this.characterCache = characterCache;
+ }
+
+ protected AsciiImgCache getCharacterCache() {
+ return characterCache;
+ }
+
+ @Override
+ public ImageMatrix transform(final ImageMatrix source) {
+
+ // dimension of each tile
+ final ImageMatrixDimensions tileSize = getCharacterCache().getCharacterImageSize();
+
+ final ImageMatrixDimensions sourcePixelsSize = source.getDimensions();
+ // the number of characters that fit fully into the source image
+ // only these will be used, and pixels to the right and below the image that are not covered by these
+ // will be ignored, and thus are not represented in the output
+ final ImageMatrixDimensions destCharactersSize = new ImageMatrixDimensions(
+ sourcePixelsSize.getWidth() / tileSize.getWidth(),
+ sourcePixelsSize.getHeight() / tileSize.getHeight());
+ // destination image width and height in pixels; truncated, so we avoid partial characters
+ final ImageMatrixDimensions truncatedPixelsSize = new ImageMatrixDimensions(
+ destCharactersSize.getWidth() * tileSize.getWidth(),
+ destCharactersSize.getHeight() * tileSize.getHeight());
+
+ // do the truncating
+ return new RegionImageMatrix<>(source, truncatedPixelsSize, 0, 0);
+ }
+}
diff --git a/src/main/java/io/korhner/asciimg/utils/AnimatedGifEncoder.java b/src/main/java/io/korhner/asciimg/utils/AnimatedGifEncoder.java
index aa4c149..e49d2f4 100644
--- a/src/main/java/io/korhner/asciimg/utils/AnimatedGifEncoder.java
+++ b/src/main/java/io/korhner/asciimg/utils/AnimatedGifEncoder.java
@@ -1,12 +1,16 @@
package io.korhner.asciimg.utils;
-import java.io.*;
-import java.awt.*;
-import java.awt.image.*;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
/**
- * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or
- * more frames.
+ * Encodes a GIF file consisting of one or more frames.
*
* Example:
* AnimatedGifEncoder e = new AnimatedGifEncoder();
@@ -16,91 +20,105 @@
* e.addFrame(image2);
* e.finish();
*
- * No copyright asserted on the source code of this class. May be used
+ * 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
+ * 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
+ private static final int DEFAULT_WIDTH = 320;
+ private static final int DEFAULT_HEIGHT = 240;
+
+ private int width; // image size
+ private int height;
+ private Color transparent; // transparent color if given
+ private int transIndex; // transparent index input color table
+ private int repeat; // no repeat
+ private int delay; // frame delay (hundredths)
+ private boolean started; // ready to output frames
+ private OutputStream out;
+ private BufferedImage image; // current frame
+ private byte[] pixels; // BGR byte array from frame
+ private byte[] indexedPixels; // converted frame indexed to palette
+ private int colorDepth; // number of bit planes
+ private byte[] colorTab; // RGB palette
+ private final boolean[] usedEntry; // active palette entries
+ private int palSize; // color table size (bits-1)
+ private int dispose; // disposal code (-1 = use default)
+ private boolean closeStream; // imageEnd stream when finished
+ private boolean firstFrame;
+ private boolean sizeSet; // if false, get size from first frame
+ private int sample; // default sample interval for quantizer
+
+ public AnimatedGifEncoder() {
+ transparent = null;
+ repeat = -1;
+ delay = 0;
+ started = false;
+ usedEntry = new boolean[256];
+ palSize = 7;
+ dispose = -1;
+ closeStream = false;
+ firstFrame = true;
+ sizeSet = false;
+ sample = 10;
+ }
/**
* 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
+ * @param delayMs delay time in milliseconds
*/
- public void setDelay(int ms) {
- delay = Math.round(ms / 10.0f);
+ public void setDelay(final int delayMs) {
+ delay = Math.round(delayMs / 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.
+ * @param code disposal code.
*/
- public void setDispose(int code) {
+ public void setDispose(final 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
+ * @param iterations number of iterations.
*/
- public void setRepeat(int iter) {
- if (iter >= 0) {
- repeat = iter;
+ public void setRepeat(final int iterations) {
+ if (iterations >= 0) {
+ repeat = iterations;
}
}
-
+
/**
* 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
+ * input the quantization process, the color input 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.
+ * @param color to be treated as transparent onto the display.
*/
- public void setTransparent(Color c) {
- transparent = c;
+ public void setTransparent(final Color color) {
+ transparent = color;
}
-
+
/**
* Adds next GIF frame. The frame is not written immediately, but is
* actually deferred until the next frame is received so that timing
@@ -108,185 +126,211 @@ public void setTransparent(Color c) {
* 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.
+ * @param img contains the 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();
+ public boolean addFrame(final BufferedImage img) {
+
+ boolean success;
+ if ((img == null) || !started) {
+ success = false;
+ } else {
+ try {
+ if (!sizeSet) {
+ // use first frame's size
+ setSize(img.getWidth(), img.getHeight());
}
+ this.image = img;
+ getImagePixels(); // convert to correct format if necessary
+ analyzePixels(); // build color table & map pixels
+ if (firstFrame) {
+ writeLSD(); // logical screen descriptor
+ 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;
+ success = true;
+ } catch (final IOException exc) {
+ success = false;
}
- 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;
+ return success;
}
-
+
/**
* Flushes any pending data and closes output file.
- * If writing to an OutputStream, the stream is not
+ * If writing to a stream, the stream is not
* closed.
+ * @return true if the output stream was successfully 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();
+
+ boolean success;
+ if (started) {
+ started = false;
+ try {
+ out.write(0x3b); // gif trailer
+ out.flush();
+ if (closeStream) {
+ out.close();
+ }
+ success = true;
+ } catch (final IOException exc) {
+ success = false;
}
- } 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;
+ // reset for subsequent use
+ transIndex = 0;
+ out = null;
+ image = null;
+ pixels = null;
+ indexedPixels = null;
+ colorTab = null;
+ closeStream = false;
+ firstFrame = true;
+ } else {
+ success = false;
+ }
- return ok;
+ return success;
}
-
+
/**
- * Sets frame rate in frames per second. Equivalent to
+ * Sets frame rate input frames per second. Equivalent to
* setDelay(1000/fps).
*
- * @param fps float frame rate (frames per second)
+ * @param fps frame rate (frames per second)
*/
- public void setFrameRate(float fps) {
+ public void setFrameRate(final 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.
+ * than 20 do not yield significant improvements input speed.
*
- * @param quality int greater than 0.
- * @return
+ * @param quality greater than 0.
*/
- public void setQuality(int quality) {
- if (quality < 1) quality = 1;
- sample = quality;
+ public void setQuality(final int quality) {
+ if (quality < 1) {
+ sample = 1;
+ } else {
+ sample = quality;
+ }
}
-
+
/**
- * Sets the GIF frame size. The default size is the
- * size of the first frame added if this method is
+ * 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.
+ * @param newWidth frame width.
+ * @param newHeight 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;
+ public void setSize(final int newWidth, final int newHeight) {
+ if (!started || firstFrame) {
+ width = newWidth;
+ height = newHeight;
+ if (width < 1) {
+ width = DEFAULT_WIDTH;
+ }
+ if (height < 1) {
+ height = DEFAULT_HEIGHT;
+ }
+ sizeSet = true;
+ }
}
-
+
/**
- * Initiates GIF file creation on the given stream. The stream
- * is not closed automatically.
+ * Initiates GIF file creation on the given stream.
+ * The stream is not closed automatically.
*
- * @param os OutputStream on which GIF images are written.
+ * @param output stream onto 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;
+ public boolean start(final OutputStream output) {
+
+ boolean success;
+ if (output == null) {
+ success = false;
+ } else {
+ closeStream = false;
+ this.out = output;
+ try {
+ writeString("GIF89a"); // header
+ success = true;
+ } catch (final IOException exc) {
+ success = false;
+ }
+ started = success;
}
- return started = ok;
+
+ return success;
}
-
+
/**
* 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;
+ public boolean start(final String file) {
+
+ boolean success;
try {
out = new BufferedOutputStream(new FileOutputStream(file));
- ok = start(out);
+ success = start(out);
closeStream = true;
- } catch (IOException e) {
- ok = false;
+ } catch (IOException exc) {
+ success = false;
}
- return started = ok;
+ started = success;
+
+ return success;
}
-
+
/**
- * Analyzes image colors and creates color map.
+ * Analyzes image colors and creates a color map.
*/
protected void analyzePixels() {
- int len = pixels.length;
- int nPix = len / 3;
+ final int len = pixels.length;
+ final int nPix = len / 3;
indexedPixels = new byte[nPix];
- NeuQuant nq = new NeuQuant(pixels, len, sample);
+ final NeuQuant neuQuant = new NeuQuant(pixels, len, sample);
// initialize quantizer
- colorTab = nq.process(); // create reduced palette
+ colorTab = neuQuant.process(); // create reduced palette
// convert map from BGR to RGB
for (int i = 0; i < colorTab.length; i += 3) {
- byte temp = colorTab[i];
+ final 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);
+ int pixColorIdx = 0;
+ for (int pixelIdx = 0; pixelIdx < nPix; pixelIdx++) {
+ final int index = neuQuant.map(
+ pixels[pixColorIdx++] & 0xff,
+ pixels[pixColorIdx++] & 0xff,
+ pixels[pixColorIdx++] & 0xff);
usedEntry[index] = true;
- indexedPixels[i] = (byte) index;
+ indexedPixels[pixelIdx] = (byte) index;
}
pixels = null;
colorDepth = 8;
@@ -296,87 +340,98 @@ protected void analyzePixels() {
transIndex = findClosest(transparent);
}
}
-
+
/**
- * Returns index of palette color closest to c
- *
+ * Returns the index of the palette color closest to the supplied color.
+ * @param color color to look for
+ * @return pixel index with most similar color
*/
- 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;
+ protected int findClosest(final Color color) {
+
+ int minPos;
+ if (colorTab == null) {
+ minPos = -1;
+ } else {
+ final int red = color.getRed();
+ final int green = color.getGreen();
+ final int blue = color.getBlue();
+ minPos = 0;
+ int diffMin = 256 * 256 * 256;
+ final int len = colorTab.length;
+ for (int i = 0; i < len;) {
+ final int diffRed = red - (colorTab[i++] & 0xff);
+ final int diffGreen = green - (colorTab[i++] & 0xff);
+ final int diffBlue = blue - (colorTab[i] & 0xff);
+ final int diffTotal = diffRed * diffRed + diffGreen * diffGreen + diffBlue * diffBlue;
+ final int index = i / 3;
+ if (usedEntry[index] && (diffTotal < diffMin)) {
+ diffMin = diffTotal;
+ minPos = index;
+ }
+ i++;
}
- i++;
}
- return minpos;
+
+ 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)) {
+ final int imageWidth = image.getWidth();
+ final int imageHeight = image.getHeight();
+ final int type = image.getType();
+ if ((imageWidth != width)
+ || (imageHeight != 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);
+ final BufferedImage temp = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
+ final Graphics2D graphics = temp.createGraphics();
+ graphics.drawImage(image, 0, 0, null);
image = temp;
}
pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
}
-
+
/**
- * Writes Graphic Control Extension
+ * Writes Graphic Control Extension.
+ * @throws IOException on output error
*/
protected void writeGraphicCtrlExt() throws IOException {
out.write(0x21); // extension introducer
out.write(0xf9); // GCE label
out.write(4); // data block size
- int transp, disp;
+ final int trans;
+ int dis;
if (transparent == null) {
- transp = 0;
- disp = 0; // dispose = no action
+ trans = 0;
+ dis = 0; // dispose = no action
} else {
- transp = 1;
- disp = 2; // force clear if using transparent color
+ trans = 1;
+ dis = 2; // force clear if using transparent color
}
if (dispose >= 0) {
- disp = dispose & 7; // user override
+ dis = dispose & 7; // user override
}
- disp <<= 2;
+ dis <<= 2;
// packed fields
- out.write(0 | // 1:3 reserved
- disp | // 4:6 disposal
- 0 | // 7 user input - 0 = none
- transp); // 8 transparency flag
+ out.write(
+// 0 | // 1:3 reserved
+ dis | // 4:6 disposal
+// 0 | // 7 user input - 0 = none
+ trans); // 8 transparency flag
writeShort(delay); // delay x 1/100 sec
out.write(transIndex); // transparent color index
out.write(0); // block terminator
}
-
+
/**
- * Writes Image Descriptor
+ * Writes an Image Descriptor.
+ * @throws IOException on output error
*/
protected void writeImageDesc() throws IOException {
out.write(0x2c); // image separator
@@ -390,34 +445,38 @@ protected void writeImageDesc() throws IOException {
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
+ 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
+ * Writes Logical Screen Descriptor.
+ * @throws IOException on output error
*/
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(
+ 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.
+ * @throws IOException on output error
*/
protected void writeNetscapeExt() throws IOException {
out.write(0x21); // extension introducer
@@ -429,41 +488,46 @@ protected void writeNetscapeExt() throws IOException {
writeShort(repeat); // loop count (extra iterations, 0=repeat forever)
out.write(0); // block terminator
}
-
+
/**
- * Writes color table
+ * Writes a color table.
+ * @throws IOException on output error
*/
protected void writePalette() throws IOException {
out.write(colorTab, 0, colorTab.length);
- int n = (3 * 256) - colorTab.length;
- for (int i = 0; i < n; i++) {
+ final int num = (3 * 256) - colorTab.length;
+ for (int i = 0; i < num; i++) {
out.write(0);
}
}
-
+
/**
- * Encodes and writes pixel data
+ * Encodes and writes pixel data.
+ * @throws IOException on output error
*/
protected void writePixels() throws IOException {
- LZWEncoder encoder =
- new LZWEncoder(width, height, indexedPixels, colorDepth);
+ final LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
encoder.encode(out);
}
-
+
/**
- * Write 16-bit value to output stream, LSB first
+ * Write 16-bit value, LSB first.
+ * @param value to be written
+ * @throws IOException on output error
*/
- protected void writeShort(int value) throws IOException {
+ protected void writeShort(final int value) throws IOException {
out.write(value & 0xff);
out.write((value >> 8) & 0xff);
}
-
+
/**
- * Writes string to output stream
+ * Writes a string.
+ * @param str to be written
+ * @throws IOException on output error
*/
- protected void writeString(String s) throws IOException {
- for (int i = 0; i < s.length(); i++) {
- out.write((byte) s.charAt(i));
+ protected void writeString(final String str) throws IOException {
+ for (int i = 0; i < str.length(); i++) {
+ out.write((byte) str.charAt(i));
}
}
}
diff --git a/src/main/java/io/korhner/asciimg/utils/ArrayUtils.java b/src/main/java/io/korhner/asciimg/utils/ArrayUtils.java
index 1497148..b2e758c 100644
--- a/src/main/java/io/korhner/asciimg/utils/ArrayUtils.java
+++ b/src/main/java/io/korhner/asciimg/utils/ArrayUtils.java
@@ -1,9 +1,36 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
package io.korhner.asciimg.utils;
/**
* An utility class used for various array utilities.
*/
-public class ArrayUtils {
+public final class ArrayUtils {
+
+ private ArrayUtils() {}
/**
* Converts from 1D array index to 1D on x axis.
@@ -34,16 +61,15 @@ public static int convert1DtoY(final int index, final int arrayWidth) {
/**
* Converts from 2D array index to 1D.
*
- * @param x
- * The index on x axis.
- * @param y
- * The index on x axis.
+ * @param xPos
+ * The index on xPos axis.
+ * @param yPos
+ * The index on xPos axis.
* @param arrayWidth
- * 2D Array width (length of rows on x axis).
+ * 2D Array width (length of rows on xPos axis).
* @return Corresponding index if the array was 1D.
*/
- public static int convert2DTo1D(final int x, final int y,
- final int arrayWidth) {
- return y * arrayWidth + x;
+ public static int convert2DTo1D(final int xPos, final int yPos, final int arrayWidth) {
+ return yPos * arrayWidth + xPos;
}
}
diff --git a/src/main/java/io/korhner/asciimg/utils/GifDecoder.java b/src/main/java/io/korhner/asciimg/utils/GifDecoder.java
index 228f8c8..061730f 100644
--- a/src/main/java/io/korhner/asciimg/utils/GifDecoder.java
+++ b/src/main/java/io/korhner/asciimg/utils/GifDecoder.java
@@ -1,4 +1,5 @@
package io.korhner.asciimg.utils;
+
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
@@ -16,26 +17,25 @@
/**
* Class GifDecoder - Decodes a GIF file into one or more frames.
- *
+ *
+ * {@code
* 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
+ * int t = d.getDelay(i); // display duration of frame input 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 {
/**
@@ -53,76 +53,92 @@ public class GifDecoder {
*/
public static final int STATUS_OPEN_ERROR = 2;
- protected BufferedInputStream in;
- protected int status;
+ private BufferedInputStream input;
+ private 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
+ private int width; // full image width
+ private int height; // full image height
+ private boolean gctFlag; // global color table used
+ private int gctSize; // size of global color table
+ private int loopCount; // iterations; 0 = repeat forever
- protected int[] gct; // global color table
- protected int[] lct; // local color table
- protected int[] act; // active color table
+ private int[] gct; // global color table
+ private int[] lct; // local color table
+ private 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
+ private int bgIndex; // background color index
+ private int bgColor; // background color
+ private int lastBgColor; // previous bg color
+ private int pixelAspect; // pixel aspect ratio
- protected boolean lctFlag; // local color table flag
- protected boolean interlace; // interlace flag
- protected int lctSize; // local color table size
+ private boolean lctFlag; // local color table flag
+ private boolean interlace; // interlace flag
+ private 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
+ // current image rectangle
+ private int imgX;
+ private int imgY;
+ private int imgWidth;
+ private int imgHeight;
+ private Rectangle lastRect; // last image rect
+ private BufferedImage image; // current frame
+ private BufferedImage lastImage; // previous frame
- protected byte[] block = new byte[256]; // current data block
- protected int blockSize = 0; // block size
+ private final byte[] block; // current data block
+ private int blockSize; // 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;
+ private int dispose;
+ // 0=no action; 1=leave input place; 2=restore to bg; 3=restore to prev
+ private int lastDispose;
+ private boolean transparency; // use transparent color
+ private int delay; // delay input milliseconds
+ private int transIndex; // transparent color index
+
// max decoder pixel stack size
+ private static final int MAX_STACK_SIZE = 4096;
// 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;
+ private short[] prefix;
+ private byte[] suffix;
+ private byte[] pixelStack;
+ private byte[] pixels;
+
+ private List frames; // frames read from current file
+ private int frameCount;
+
+ public GifDecoder() {
+ loopCount = 1;
+ block = new byte[256];
+ blockSize = 0;
+ dispose = 0;
+ lastDispose = 0;
+ transparency = false;
+ delay = 0;
+ }
static class GifFrame {
- public GifFrame(BufferedImage im, int del) {
- image = im;
- delay = del;
+
+ public final BufferedImage image;
+ public final int delay;
+
+ GifFrame(final BufferedImage image, final int delay) {
+ this.image = image;
+ this.delay = delay;
}
- public BufferedImage image;
- public int delay;
}
/**
* Gets display duration for specified frame.
*
- * @param n int index of frame
- * @return delay in milliseconds
+ * @param frameIdx int index of frame
+ * @return delay input milliseconds
*/
- public int getDelay(int n) {
- //
+ public int getDelay(final int frameIdx) {
+
delay = -1;
- if ((n >= 0) && (n < frameCount)) {
- delay = ((GifFrame) frames.get(n)).delay;
+ if ((frameIdx >= 0) && (frameIdx < frameCount)) {
+ delay = frames.get(frameIdx).delay;
}
return delay;
}
@@ -146,7 +162,7 @@ public BufferedImage getImage() {
/**
* Gets the "Netscape" iteration count, if any.
- * A count of 0 means repeat indefinitiely.
+ * A count of 0 means repeat indefinitely.
*
* @return iteration count if one was specified, else 1.
*/
@@ -160,102 +176,103 @@ public int getLoopCount() {
*/
protected void setPixels() {
// expose destination image's pixels as int array
- int[] dest =
- ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
+ final int[] dest = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
- // fill in starting image contents based on last image's dispose code
+ // fill input 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);
+ final int num = frameCount - 2;
+ if (num > 0) {
+ lastImage = getFrame(num - 1);
} else {
lastImage = null;
}
}
if (lastImage != null) {
- int[] prev =
- ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData();
- System.arraycopy(prev, 0, dest, 0, width * height);
+ final int[] prev = ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData();
// copy pixels
+ System.arraycopy(prev, 0, dest, 0, width * height);
if (lastDispose == 2) {
// fill last image rect area with background color
- Graphics2D g = image.createGraphics();
- Color c = null;
+ final Graphics2D graphics = image.createGraphics();
+ final Color color;
if (transparency) {
- c = new Color(0, 0, 0, 0); // assume background is transparent
+ color = new Color(0, 0, 0, 0); // assume background is transparent
} else {
- c = new Color(lastBgColor); // use given background color
+ color = new Color(lastBgColor); // use given background color
}
- g.setColor(c);
- g.setComposite(AlphaComposite.Src); // replace area
- g.fill(lastRect);
- g.dispose();
+ graphics.setColor(color);
+ graphics.setComposite(AlphaComposite.Src); // replace area
+ graphics.fill(lastRect);
+ graphics.dispose();
}
}
}
- // copy each source line to the appropriate place in the destination
+ // copy each source line to the appropriate place input the destination
int pass = 1;
int inc = 8;
- int iline = 0;
- for (int i = 0; i < ih; i++) {
+ int iLine = 0;
+ for (int i = 0; i < imgHeight; i++) {
int line = i;
if (interlace) {
- if (iline >= ih) {
+ if (iLine >= imgHeight) {
pass++;
switch (pass) {
case 2 :
- iline = 4;
+ iLine = 4;
break;
case 3 :
- iline = 2;
+ iLine = 2;
inc = 4;
break;
case 4 :
- iline = 1;
+ iLine = 1;
inc = 2;
+ break;
+ default:
}
}
- line = iline;
- iline += inc;
+ line = iLine;
+ iLine += inc;
}
- line += iy;
+ line += imgY;
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
+ final int k = line * width;
+ int destX = k + imgX; // start of line input dest
+ int destLim = destX + imgWidth; // end of dest line
+ if ((k + width) < destLim) {
+ destLim = 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;
+ int sourceX = i * imgWidth; // start of line input source
+ while (destX < destLim) {
+ // map color and insert input destination
+ final int index = ((int) pixels[sourceX++]) & 0xff;
+ final int color = act[index];
+ if (color != 0) {
+ dest[destX] = color;
}
- dx++;
+ destX++;
}
}
}
}
/**
- * Gets the image contents of frame n.
+ * Gets the image contents of frame frameNum.
*
- * @return BufferedImage representation of frame, or null if n is invalid.
+ * @param frameNum number of the frame to be fetched
+ * @return BufferedImage representation of frame, or null if frameNum is invalid.
*/
- public BufferedImage getFrame(int n) {
- BufferedImage im = null;
- if ((n >= 0) && (n < frameCount)) {
- im = ((GifFrame) frames.get(n)).image;
+ public BufferedImage getFrame(final int frameNum) {
+ BufferedImage img = null;
+ if ((frameNum >= 0) && (frameNum < frameCount)) {
+ img = frames.get(frameNum).image;
}
- return im;
+ return img;
}
/**
@@ -264,19 +281,22 @@ public BufferedImage getFrame(int n) {
* @return GIF image dimensions
*/
public Dimension getFrameSize() {
+
return new Dimension(width, height);
}
/**
* Reads GIF image from stream
*
- * @param BufferedInputStream containing GIF file.
+ * @param inp containing GIF file.
* @return read status code (0 = no errors)
*/
- public int read(BufferedInputStream is) {
+ public int read(final BufferedInputStream inp) {
init();
- if (is != null) {
- in = is;
+ if (inp == null) {
+ status = STATUS_OPEN_ERROR;
+ } else {
+ this.input = inp;
readHeader();
if (!err()) {
readContents();
@@ -284,65 +304,51 @@ public int read(BufferedInputStream is) {
status = STATUS_FORMAT_ERROR;
}
}
- } else {
- status = STATUS_OPEN_ERROR;
- }
- try {
- is.close();
- } catch (IOException e) {
+ try {
+ inp.close();
+ } catch (final IOException exc) {
+ }
}
+
return status;
}
/**
* Reads GIF image from stream
*
- * @param InputStream containing GIF file.
+ * @param inp 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;
- }
- }
+ public int read(final InputStream inp) {
+
+ BufferedInputStream binp;
+ if (inp instanceof BufferedInputStream) {
+ binp = (BufferedInputStream) inp;
} else {
- status = STATUS_OPEN_ERROR;
- }
- try {
- is.close();
- } catch (IOException e) {
+ binp = new BufferedInputStream(inp);
}
- return status;
+ return read(binp);
}
/**
- * Reads GIF file from specified file/URL source
+ * 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) {
+ public int read(final 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());
+ final String nameTrimmed = name.trim().toLowerCase();
+ if (nameTrimmed.contains("file:") || (nameTrimmed.indexOf(":/") > 0)) {
+ final URL url = new URL(nameTrimmed);
+ input = new BufferedInputStream(url.openStream());
} else {
- in = new BufferedInputStream(new FileInputStream(name));
+ input = new BufferedInputStream(new FileInputStream(nameTrimmed));
}
- status = read(in);
- } catch (IOException e) {
+ status = read(input);
+ } catch (final IOException exc) {
status = STATUS_OPEN_ERROR;
}
@@ -354,42 +360,32 @@ public int read(String name) {
* 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
+ final int nullCode = -1;
+ final int nPix = imgWidth * imgHeight;
+
+ if ((pixels == null) || (pixels.length < nPix)) {
+ pixels = new byte[nPix]; // allocate new pixel array
+ }
+ if (prefix == null) {
+ prefix = new short[MAX_STACK_SIZE];
+ }
+ if (suffix == null) {
+ suffix = new byte[MAX_STACK_SIZE];
+ }
+ if (pixelStack == null) {
+ pixelStack = new byte[MAX_STACK_SIZE + 1];
}
- 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;
+ final int dataSize = read();
+ final int clear = 1 << dataSize;
+ final int endOfInformation = clear + 1;
+ int available = clear + 2;
+ int oldCode = nullCode;
+ int codeSize = dataSize + 1;
+ int codeMask = (1 << codeSize) - 1;
+ int code;
for (code = 0; code < clear; code++) {
prefix[code] = 0;
suffix[code] = (byte) code;
@@ -397,54 +393,62 @@ protected void decodeImageData() {
// Decode GIF pixel stream.
- datum = bits = count = first = top = pi = bi = 0;
+ int datum = 0;
+ int bits = 0;
+ int count = 0;
+ int first = 0;
+ int top = 0;
+ int pixelIdx;
+ int byteIdx = 0;
- for (i = 0; i < npix;) {
+ for (pixelIdx = 0; pixelIdx < nPix;) {
if (top == 0) {
- if (bits < code_size) {
+ if (bits < codeSize) {
// Load bytes until there are enough bits for a code.
if (count == 0) {
// Read a new data block.
count = readBlock();
- if (count <= 0)
+ if (count <= 0) {
break;
- bi = 0;
+ }
+ byteIdx = 0;
}
- datum += (((int) block[bi]) & 0xff) << bits;
+ datum += (((int) block[byteIdx]) & 0xff) << bits;
bits += 8;
- bi++;
+ byteIdx++;
count--;
continue;
}
// Get the next code.
- code = datum & code_mask;
- datum >>= code_size;
- bits -= code_size;
+ code = datum & codeMask;
+ datum >>= codeSize;
+ bits -= codeSize;
// Interpret the code
- if ((code > available) || (code == end_of_information))
+ if ((code > available) || (code == endOfInformation)) {
break;
+ }
if (code == clear) {
// Reset decoder.
- code_size = data_size + 1;
- code_mask = (1 << code_size) - 1;
+ codeSize = dataSize + 1;
+ codeMask = (1 << codeSize) - 1;
available = clear + 2;
- old_code = NullCode;
+ oldCode = nullCode;
continue;
}
- if (old_code == NullCode) {
+ if (oldCode == nullCode) {
pixelStack[top++] = suffix[code];
- old_code = code;
+ oldCode = code;
first = code;
continue;
}
- in_code = code;
+ final int inCode = code;
if (code == available) {
pixelStack[top++] = (byte) first;
- code = old_code;
+ code = oldCode;
}
while (code > clear) {
pixelStack[top++] = suffix[code];
@@ -454,35 +458,36 @@ protected void decodeImageData() {
// Add a new string to the string table,
- if (available >= MaxStackSize)
+ if (available >= MAX_STACK_SIZE) {
break;
+ }
pixelStack[top++] = (byte) first;
- prefix[available] = (short) old_code;
+ prefix[available] = (short) oldCode;
suffix[available] = (byte) first;
available++;
- if (((available & code_mask) == 0)
- && (available < MaxStackSize)) {
- code_size++;
- code_mask += available;
+ if (((available & codeMask) == 0)
+ && (available < MAX_STACK_SIZE))
+ {
+ codeSize++;
+ codeMask += available;
}
- old_code = in_code;
+ oldCode = inCode;
}
// Pop a pixel off the pixel stack.
top--;
- pixels[pi++] = pixelStack[top];
- i++;
+ pixels[pixelIdx++] = pixelStack[top];
}
- for (i = pi; i < npix; i++) {
- pixels[i] = 0; // clear missing pixels
+ for (int ggg = pixelIdx; ggg < nPix; ggg++) {
+ pixels[ggg] = 0; // clear missing pixels
}
-
}
/**
- * Returns true if an error was encountered during reading/decoding
+ * Checks if an error was encountered during reading/decoding
+ * @return true if an error was encountered, false otherwise
*/
protected boolean err() {
return status != STATUS_OK;
@@ -501,12 +506,13 @@ protected void init() {
/**
* Reads a single byte from the input stream.
+ * @return the byte read
*/
protected int read() {
int curByte = 0;
try {
- curByte = in.read();
- } catch (IOException e) {
+ curByte = input.read();
+ } catch (final IOException exc) {
status = STATUS_FORMAT_ERROR;
}
return curByte;
@@ -515,56 +521,56 @@ protected int read() {
/**
* Reads next variable length block from input.
*
- * @return number of bytes stored in "buffer"
+ * @return number of bytes stored input "buffer"
*/
protected int readBlock() {
blockSize = read();
- int n = 0;
+ int totalBytesRead = 0;
if (blockSize > 0) {
try {
- int count = 0;
- while (n < blockSize) {
- count = in.read(block, n, blockSize - n);
- if (count == -1)
+ while (totalBytesRead < blockSize) {
+ final int count = input.read(block, totalBytesRead, blockSize - totalBytesRead);
+ if (count == -1) {
break;
- n += count;
+ }
+ totalBytesRead += count;
}
- } catch (IOException e) {
+ } catch (final IOException exc) {
}
- if (n < blockSize) {
+ if (totalBytesRead < blockSize) {
status = STATUS_FORMAT_ERROR;
}
}
- return n;
+ return totalBytesRead;
}
/**
* Reads color table as 256 RGB integer values
*
- * @param ncolors int number of colors to read
+ * @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;
+ protected int[] readColorTable(final int nColors) {
+ final int nBytes = 3 * nColors;
int[] tab = null;
- byte[] c = new byte[nbytes];
- int n = 0;
+ final byte[] color = new byte[nBytes];
+ int nColorComps = 0;
try {
- n = in.read(c);
- } catch (IOException e) {
+ nColorComps = input.read(color);
+ } catch (IOException exc) {
}
- if (n < nbytes) {
+ if (nColorComps < 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;
+ while (i < nColors) {
+ final int red = ((int) color[j++]) & 0xff;
+ final int green = ((int) color[j++]) & 0xff;
+ final int blue = ((int) color[j++]) & 0xff;
+ tab[i++] = 0xff000000 | (red << 16) | (green << 8) | blue;
}
}
return tab;
@@ -593,15 +599,15 @@ protected void readContents() {
case 0xff : // application extension
readBlock();
- String app = "";
+ final StringBuilder app = new StringBuilder();
for (int i = 0; i < 11; i++) {
- app += (char) block[i];
+ app.append((char) block[i]);
}
- if (app.equals("NETSCAPE2.0")) {
+ if (app.toString().equals("NETSCAPE2.0")) {
readNetscapeExt();
- }
- else
+ } else {
skip(); // don't care
+ }
break;
default : // uninteresting extension
@@ -627,13 +633,13 @@ protected void readContents() {
*/
protected void readGraphicControlExt() {
read(); // block size
- int packed = read(); // packed fields
+ final 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
+ delay = readShort() * 10; // delay input milliseconds
transIndex = read(); // transparent color index
read(); // block terminator
}
@@ -642,11 +648,11 @@ protected void readGraphicControlExt() {
* Reads GIF file header information.
*/
protected void readHeader() {
- String id = "";
+ final StringBuilder idBuilder = new StringBuilder("");
for (int i = 0; i < 6; i++) {
- id += (char) read();
+ idBuilder.append((char) read());
}
- if (!id.startsWith("GIF")) {
+ if (!idBuilder.toString().startsWith("GIF")) {
status = STATUS_FORMAT_ERROR;
return;
}
@@ -662,12 +668,12 @@ protected void readHeader() {
* Reads next frame image
*/
protected void readImage() {
- ix = readShort(); // (sub)image position & size
- iy = readShort();
- iw = readShort();
- ih = readShort();
+ imgX = readShort(); // (sub)image position & size
+ imgY = readShort();
+ imgWidth = readShort();
+ imgHeight = readShort();
- int packed = read();
+ final int packed = read();
lctFlag = (packed & 0x80) != 0; // 1 - local color table flag
interlace = (packed & 0x40) != 0; // 2 - interlace flag
// 3 - sort flag
@@ -679,8 +685,9 @@ protected void readImage() {
act = lct; // make local table active
} else {
act = gct; // make global table active
- if (bgIndex == transIndex)
+ if (bgIndex == transIndex) {
bgColor = 0;
+ }
}
int save = 0;
if (transparency) {
@@ -692,28 +699,26 @@ protected void readImage() {
status = STATUS_FORMAT_ERROR; // no color table defined
}
- if (err()) return;
-
- decodeImageData(); // decode pixel data
- skip();
+ if (!err()) {
+ decodeImageData(); // decode pixel data
+ skip();
- if (err()) return;
-
- frameCount++;
+ if (!err()) {
+ frameCount++;
- // create new image to receive frame data
- image =
- new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
+ // create new image to receive frame data
+ image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
- setPixels(); // transfer pixel data to image
+ setPixels(); // transfer pixel data to image
- frames.add(new GifFrame(image, delay)); // add image to frame list
+ frames.add(new GifFrame(image, delay)); // add image to frame list
- if (transparency) {
- act[transIndex] = save;
+ if (transparency) {
+ act[transIndex] = save;
+ }
+ resetFrame();
+ }
}
- resetFrame();
-
}
/**
@@ -726,7 +731,7 @@ protected void readLSD() {
height = readShort();
// packed fields
- int packed = read();
+ final int packed = read();
gctFlag = (packed & 0x80) != 0; // 1 : global color table flag
// 2-4 : color resolution
// 5 : gct sort flag
@@ -737,22 +742,23 @@ protected void readLSD() {
}
/**
- * Reads Netscape extenstion to obtain iteration count
+ * Reads Netscape extension 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;
+ final int block1 = ((int) block[1]) & 0xff;
+ final int block2 = ((int) block[2]) & 0xff;
+ loopCount = (block2 << 8) | block1;
}
} while ((blockSize > 0) && !err());
}
/**
- * Reads next 16-bit value, LSB first
+ * Reads next 16-bit value, LSB first.
+ * @return the double-byte read
*/
protected int readShort() {
// read 16-bit value, LSB first
@@ -764,7 +770,7 @@ protected int readShort() {
*/
protected void resetFrame() {
lastDispose = dispose;
- lastRect = new Rectangle(ix, iy, iw, ih);
+ lastRect = new Rectangle(imgX, imgY, imgWidth, imgHeight);
lastImage = image;
lastBgColor = bgColor;
lct = null;
diff --git a/src/main/java/io/korhner/asciimg/utils/LZWEncoder.java b/src/main/java/io/korhner/asciimg/utils/LZWEncoder.java
index 3652ec8..7fd57fd 100644
--- a/src/main/java/io/korhner/asciimg/utils/LZWEncoder.java
+++ b/src/main/java/io/korhner/asciimg/utils/LZWEncoder.java
@@ -1,4 +1,5 @@
package io.korhner.asciimg.utils;
+
import java.io.OutputStream;
import java.io.IOException;
@@ -10,9 +11,10 @@ class LZWEncoder {
private static final int EOF = -1;
- private int imgW, imgH;
- private byte[] pixAry;
- private int initCodeSize;
+ private final int imgW;
+ private final int imgH;
+ private final byte[] pixels;
+ private final int initCodeSize;
private int remaining;
private int curPixel;
@@ -23,9 +25,9 @@ class LZWEncoder {
// General DEFINEs
- static final int BITS = 12;
+ private static final int BITS = 12;
- static final int HSIZE = 5003; // 80% occupancy
+ private static final int H_SIZE = 5003; // 80% occupancy
// GIF Image compression - modified 'compress'
//
@@ -38,21 +40,21 @@ class LZWEncoder {
// 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
+ private int nBits; // number of bits/code
+ private final int maxBits; // user settable max # bits/code
+ private int maxCode; // maximum code, given nBits
+ private final int maxMaxCode; // should NEVER generate this code
- int[] htab = new int[HSIZE];
- int[] codetab = new int[HSIZE];
+ private final int[] hTab;
+ private final int[] codeTab;
- int hsize = HSIZE; // for dynamic table sizing
+ private final int hSize; // for dynamic table sizing
- int free_ent = 0; // first unused entry
+ private int freeEnt; // first unused entry
// block compression parameters -- after all codes are used up,
// and compression rate changes, start over.
- boolean clear_flg = false;
+ private boolean clearFlag;
// Algorithm: use open addressing double hashing (no chaining) on the
// prefix code / next character combination. We do a variant of Knuth's
@@ -66,31 +68,30 @@ class LZWEncoder {
// file size for noticeable speed improvement on small files. Please direct
// questions about this implementation to ames!jaw.
- int g_init_bits;
+ private int gInitBits;
- int ClearCode;
- int EOFCode;
+ private int clearCode;
+ private 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.
+ // code: A nBits-bit integer. If == -1, then EOF. This assumes
+ // that nBits =< 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.
+ // fit input it exactly). Use the VAX insv instruction to insert each
+ // code input turn. When the buffer fills up empty it and start over.
- int cur_accum = 0;
- int cur_bits = 0;
+ private int curAccum;
+ private int curBits;
- int masks[] =
- {
+ private static final int[] MASKS = {
0x0000,
0x0001,
0x0003,
@@ -107,196 +108,218 @@ class LZWEncoder {
0x1FFF,
0x3FFF,
0x7FFF,
- 0xFFFF };
+ 0xFFFF
+ };
- // Number of characters so far in this 'packet'
- int a_count;
+ // Number of characters so far input this 'packet'
+ private int aCount;
// Define the storage for the packet accumulator
- byte[] accum = new byte[256];
+ private final byte[] accum;
//----------------------------------------------------------------------------
- LZWEncoder(int width, int height, byte[] pixels, int color_depth) {
+ LZWEncoder(final int width, final int height, final byte[] pixels, final int colorDepth) {
imgW = width;
imgH = height;
- pixAry = pixels;
- initCodeSize = Math.max(2, color_depth);
+ this.pixels = pixels;
+ initCodeSize = Math.max(2, colorDepth);
+ maxMaxCode = 1 << BITS;
+ maxBits = BITS;
+ hTab = new int[H_SIZE];
+ codeTab = new int[H_SIZE];
+ hSize = H_SIZE;
+ freeEnt = 0;
+ clearFlag = false;
+ curAccum = 0;
+ curBits = 0;
+ accum = new byte[256];
}
-
- // 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);
+
+ /**
+ * Add a character to the end of the current packet, and if it is 254
+ * characters, flush the packet to disk.
+ */
+ private void charOut(final byte chr, final OutputStream outs) throws IOException {
+ accum[aCount++] = chr;
+ if (aCount >= 254) {
+ flushChar(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;
+ /** table clear for block compress */
+ private void clBlock(final OutputStream outs) throws IOException {
+ clHash(hSize);
+ freeEnt = clearCode + 2;
+ clearFlag = true;
- output(ClearCode, outs);
+ output(clearCode, outs);
}
-
- // reset code table
- void cl_hash(int hsize) {
- for (int i = 0; i < hsize; ++i)
- htab[i] = -1;
+
+ /** reset code table */
+ private void clHash(final int hSizeReset) {
+ for (int i = 0; i < hSizeReset; ++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;
+ private void compress(final int initBits, final OutputStream outs) throws IOException {
+
+ // Set up the globals: gInitBits - initial number of bits
+ gInitBits = initBits;
// Set up the necessary values
- clear_flg = false;
- n_bits = g_init_bits;
- maxcode = MAXCODE(n_bits);
+ clearFlag = false;
+ nBits = gInitBits;
+ maxCode = maxCode(nBits);
- ClearCode = 1 << (init_bits - 1);
- EOFCode = ClearCode + 1;
- free_ent = ClearCode + 2;
+ clearCode = 1 << (initBits - 1);
+ eofCode = clearCode + 1;
+ freeEnt = clearCode + 2;
- a_count = 0; // clear packet
+ aCount = 0; // clear packet
- ent = nextPixel();
+ int ent = nextPixel();
- hshift = 0;
- for (fcode = hsize; fcode < 65536; fcode *= 2)
- ++hshift;
- hshift = 8 - hshift; // set hash code range bound
+ int hShift = 0;
+ int fCode;
+ 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
+ final int hSizeReg = hSize;
+ clHash(hSizeReg); // clear hash table
- output(ClearCode, outs);
+ output(clearCode, outs);
- outer_loop : while ((c = nextPixel()) != EOF) {
- fcode = (c << maxbits) + ent;
- i = (c << hshift) ^ ent; // xor hashing
+ int disp;
+ outer_loop : for (int color = nextPixel(); color != EOF; color = nextPixel()) {
+ fCode = (color << maxBits) + ent;
+ int i = (color << hShift) ^ ent; // xor hashing
- if (htab[i] == fcode) {
- ent = codetab[i];
+ 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)
+ } else if (hTab[i] >= 0) { // non-empty slot
+ disp = hSizeReg - i; // secondary hash (after G. Knott)
+ if (i == 0) {
disp = 1;
+ }
do {
- if ((i -= disp) < 0)
- i += hsize_reg;
+ i -= disp;
+ if (i < 0) {
+ i += hSizeReg;
+ }
- if (htab[i] == fcode) {
- ent = codetab[i];
+ if (hTab[i] == fCode) {
+ ent = codeTab[i];
continue outer_loop;
}
- } while (htab[i] >= 0);
+ } 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);
+ ent = color;
+ if (freeEnt < maxMaxCode) {
+ codeTab[i] = freeEnt++; // code -> hashtable
+ hTab[i] = fCode;
+ } else {
+ clBlock(outs);
+ }
}
// Put out the final code.
output(ent, outs);
- output(EOFCode, outs);
+ output(eofCode, outs);
}
-
+
//----------------------------------------------------------------------------
- void encode(OutputStream os) throws IOException {
- os.write(initCodeSize); // write "initial code size" byte
+ public void encode(final OutputStream output) throws IOException {
+ output.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
+ compress(initCodeSize + 1, output); // compress and write the pixel data
- os.write(0); // write block terminator
+ output.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;
+ private void flushChar(final OutputStream outs) throws IOException {
+ if (aCount > 0) {
+ outs.write(aCount);
+ outs.write(accum, 0, aCount);
+ aCount = 0;
}
}
-
- final int MAXCODE(int n_bits) {
- return (1 << n_bits) - 1;
+
+ private static int maxCode(final int nBits) {
+ return (1 << nBits) - 1;
}
-
+
//----------------------------------------------------------------------------
// Return the next pixel from the image
//----------------------------------------------------------------------------
private int nextPixel() {
- if (remaining == 0)
- return EOF;
- --remaining;
+ int nextPixel;
+ if (remaining == 0) {
+ nextPixel = EOF;
+ } else {
+ --remaining;
+
+ final byte pix = pixels[curPixel++];
- byte pix = pixAry[curPixel++];
+ nextPixel = pix & 0xff;
+ }
- return pix & 0xff;
+ return nextPixel;
}
-
- 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;
+ private void output(final int code, final OutputStream outs) throws IOException {
+ curAccum &= MASKS[curBits];
+
+ if (curBits > 0) {
+ curAccum |= (code << curBits);
+ } else {
+ curAccum = code;
+ }
- cur_bits += n_bits;
+ curBits += nBits;
- while (cur_bits >= 8) {
- char_out((byte) (cur_accum & 0xff), outs);
- cur_accum >>= 8;
- cur_bits -= 8;
+ while (curBits >= 8) {
+ charOut((byte) (curAccum & 0xff), outs);
+ curAccum >>= 8;
+ curBits -= 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;
+ if (freeEnt > maxCode || clearFlag) {
+ if (clearFlag) {
+ nBits = gInitBits;
+ maxCode = maxCode(nBits);
+ clearFlag = false;
} else {
- ++n_bits;
- if (n_bits == maxbits)
- maxcode = maxmaxcode;
- else
- maxcode = MAXCODE(n_bits);
+ ++nBits;
+ if (nBits == maxBits) {
+ maxCode = maxMaxCode;
+ } else {
+ maxCode = maxCode(nBits);
+ }
}
}
- if (code == EOFCode) {
+ 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;
+ while (curBits > 0) {
+ charOut((byte) (curAccum & 0xff), outs);
+ curAccum >>= 8;
+ curBits -= 8;
}
- flush_char(outs);
+ flushChar(outs);
}
}
}
diff --git a/src/main/java/io/korhner/asciimg/utils/NeuQuant.java b/src/main/java/io/korhner/asciimg/utils/NeuQuant.java
index 6db9164..0ebf918 100644
--- a/src/main/java/io/korhner/asciimg/utils/NeuQuant.java
+++ b/src/main/java/io/korhner/asciimg/utils/NeuQuant.java
@@ -22,150 +22,177 @@
package io.korhner.asciimg.utils;
+import java.util.Arrays;
+
public class NeuQuant {
- protected static final int netsize = 256; /* number of colours used */
+ /** number of colours used */
+ protected static final int NET_SIZE = 256;
- /* 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;
+ /*
+ four primes near 500 - assume no image has a length so large
+ that it is divisible by all four primes
+ */
+ protected static final int PRIME_1 = 499;
+ protected static final int PRIME_2 = 491;
+ protected static final int PRIME_3 = 487;
+ protected static final int PRIME_4 = 503;
- protected static final int minpicturebytes = (3 * prime4);
- /* minimum size for input image */
+ /** minimum size for input image */
+ protected static final int MIN_PICTURE_BYTES = (3 * PRIME_4);
/* Program Skeleton
----------------
- [select samplefac in range 1..30]
+ [select sampleFac input range 1..30]
[read image from input file]
pic = (unsigned char*) malloc(3*width*height);
- initnet(pic,3*width*height,samplefac);
+ initNet(pic,3*width*height,sampleFac);
learn();
- unbiasnet();
- [write output image header, using writecolourmap(f)]
- inxbuild();
- write output image using inxsearch(b,g,r) */
+ 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);
+ protected static final int MAX_NET_POS = (NET_SIZE - 1);
+ /** bias for colour values */
+ protected static final int NET_BIAS_SHIFT = 4;
+ /** no. of learning cycles */
+ protected static final int N_CYCLES = 100;
+
+ /* definitions for freq and bias */
+ /** bias for fractions */
+ protected static final int INT_BIAS_SHIFT = 16;
+ protected static final int INT_BIAS = (1 << INT_BIAS_SHIFT);
+ /** GAMMA = 1024 */
+ protected static final int GAMMA_SHIFT = 10;
+ protected static final int GAMMA = (1 << GAMMA_SHIFT);
+ protected static final int BETA_SHIFT = 10;
+ /** BETA = 1/1024 */
+ protected static final int BETA = (INT_BIAS >> BETA_SHIFT);
+ protected static final int BETA_GAMMA = (INT_BIAS << (GAMMA_SHIFT - BETA_SHIFT));
+
+ /* definitions for decreasing radius factor */
+ /** for 256 cols, radius starts */
+ protected static final int INIT_RAD = (NET_SIZE >> 3);
+ /** at 32.0 biased by 6 bits */
+ protected static final int RADIUS_BIAS_SHIFT = 6;
+ protected static final int RADIUS_BIAS = (1 << RADIUS_BIAS_SHIFT);
+ /** and decreases by a */
+ protected static final int INIT_RADIUS = (INIT_RAD * RADIUS_BIAS);
+ /** factor of 1/30 each cycle */
+ protected static final int RADIUS_DEC = 30;
+
+ /* definitions for decreasing alpha factor */
+ /** alpha starts at 1.0 */
+ protected static final int ALPHA_BIAS_SHIFT = 10;
+ protected static final int INIT_ALPHA = (1 << ALPHA_BIAS_SHIFT);
+
+ /** biased by 10 bits */
+ private int alphaDec;
+
+ /* RAD_BIAS and ALPHA_RAD_BIAS used for radPower calculation */
+ protected static final int RAD_BIAS_SHIFT = 8;
+ protected static final int RAD_BIAS = (1 << RAD_BIAS_SHIFT);
+ protected static final int ALPHA_RAD_B_SHIFT = (ALPHA_BIAS_SHIFT + RAD_BIAS_SHIFT);
+ protected static final int ALPHA_RAD_BIAS = (1 << ALPHA_RAD_B_SHIFT);
/* 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++) {
+ /** the input image itself */
+ private final byte[] picture;
+ /** lengthCount = H*W*3 */
+ private final int lengthCount;
+
+ /** sampling factor 1..30 */
+ private int sampleFac;
+
+ ///** BGRc */
+ //typedef int pixel[4];
+ /** the network itself - [NET_SIZE][4] */
+ private final int[][] network;
+
+ /** for network lookup - really 256 */
+ private final int[] netIndex;
+
+ /** bias and freq arrays for learning */
+ private final int[] bias;
+ private final int[] freq;
+ /** radPower for pre-computation */
+ private final int[] radPower;
+
+ /**
+ * Initialise network input range (0,0,0) to (255,255,255) and set parameters
+ *
+ * @param picture the input image itself
+ * @param lengthCount H*W*3
+ * @param sampleFac sampling factor 1..30
+ */
+ public NeuQuant(final byte[] picture, final int lengthCount, final int sampleFac) {
+
+ this.picture = picture;
+ this.lengthCount = lengthCount;
+ this.sampleFac = sampleFac;
+
+ network = new int[NET_SIZE][];
+ bias = new int[NET_SIZE];
+ freq = new int[NET_SIZE];
+ for (int i = 0; i < NET_SIZE; 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 */
+ final int[] p = network[i];
+ Arrays.fill(p, 0, 3, (i << (NET_BIAS_SHIFT + 8)) / NET_SIZE);
+ freq[i] = INT_BIAS / NET_SIZE; /* 1/NET_SIZE */
bias[i] = 0;
}
+ netIndex = new int[256];
+ radPower = new int[INIT_RAD];
}
-
+
public byte[] colorMap() {
- byte[] map = new byte[3 * netsize];
- int[] index = new int[netsize];
- for (int i = 0; i < netsize; i++)
+ final byte[] map = new byte[3 * NET_SIZE];
+ final int[] index = new int[NET_SIZE];
+ for (int i = 0; i < NET_SIZE; i++) {
index[network[i][3]] = i;
+ }
int k = 0;
- for (int i = 0; i < netsize; i++) {
- int j = index[i];
+ for (int i = 0; i < NET_SIZE; i++) {
+ final 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;
+ /**
+ * Insertion sort of network and building of netIndex[0..255] (to do after unbias)
+ */
+ public void inXBuild() {
+
+ int i;
+ int j;
int[] p;
int[] q;
- int previouscol, startpos;
-
- previouscol = 0;
- startpos = 0;
- for (i = 0; i < netsize; i++) {
+ int previousCol = 0;
+ int startPos = 0;
+ for (i = 0; i < NET_SIZE; 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++) {
+ int smallPos = i;
+ int smallVal = p[1]; /* index on g */
+ /* find smallest input i..NET_SIZE-1 */
+ for (j = i + 1; j < NET_SIZE; j++) {
q = network[j];
- if (q[1] < smallval) { /* index on g */
- smallpos = j;
- smallval = q[1]; /* index on g */
+ 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) {
+ q = network[smallPos];
+ /* swap p (i) and q (smallPos) entries */
+ if (i != smallPos) {
j = q[0];
q[0] = p[0];
p[0] = j;
@@ -179,130 +206,148 @@ public void inxbuild() {
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;
+ /* smallVal entry is now input 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 */
+ netIndex[previousCol] = (startPos + MAX_NET_POS) >> 1;
+ for (j = previousCol + 1; j < 256; j++) {
+ netIndex[j] = MAX_NET_POS; /* really 256 */
+ }
}
-
- /* Main Learning Loop
- ------------------ */
+
+ /**
+ * 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)
+ if (lengthCount < MIN_PICTURE_BYTES) {
+ sampleFac = 1;
+ }
+ alphaDec = 30 + ((sampleFac - 1) / 3);
+ final byte[] p = picture;
+ int pix = 0;
+ final int lim = lengthCount;
+ final int samplePixels = lengthCount / (3 * sampleFac);
+ int delta = samplePixels / N_CYCLES;
+ int alpha = INIT_ALPHA;
+ int radius = INIT_RADIUS;
+
+ int rad = radius >> RADIUS_BIAS_SHIFT;
+ if (rad <= 1) {
rad = 0;
- for (i = 0; i < rad; i++)
- radpower[i] =
- alpha * (((rad * rad - i * i) * radbias) / (rad * rad));
+ }
+ int i;
+ for (i = 0; i < rad; i++) {
+ radPower[i] = alpha * (((rad * rad - i * i) * RAD_BIAS) / (rad * rad));
+ }
//fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad);
- if (lengthcount < minpicturebytes)
+ final int step;
+ if (lengthCount < MIN_PICTURE_BYTES) {
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;
+ } else {
+ if ((lengthCount % PRIME_1) == 0) {
+ if ((lengthCount % PRIME_2) == 0) {
+ if ((lengthCount % PRIME_3) == 0) {
+ step = 3 * PRIME_4;
+ } else {
+ step = 3 * PRIME_3;
+ }
+ } else {
+ step = 3 * PRIME_2;
+ }
+ } else {
+ step = 3 * PRIME_1;
}
}
+ int j;
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 */
+ while (i < samplePixels) {
+ final int blue = (p[pix ] & 0xff) << NET_BIAS_SHIFT;
+ final int green = (p[pix + 1] & 0xff) << NET_BIAS_SHIFT;
+ final int red = (p[pix + 2] & 0xff) << NET_BIAS_SHIFT;
+ j = contest(blue, green, red);
+
+ alterSingle(alpha, j, blue, green, red);
+ if (rad != 0) {
+ alterNeigh(rad, j, blue, green, red); /* alter neighbours */
+ }
pix += step;
- if (pix >= lim)
- pix -= lengthcount;
+ if (pix >= lim) {
+ pix -= lengthCount;
+ }
i++;
- if (delta == 0)
+ if (delta == 0) {
delta = 1;
+ }
if (i % delta == 0) {
- alpha -= alpha / alphadec;
- radius -= radius / radiusdec;
- rad = radius >> radiusbiasshift;
- if (rad <= 1)
+ alpha -= alpha / alphaDec;
+ radius -= radius / RADIUS_DEC;
+ rad = radius >> RADIUS_BIAS_SHIFT;
+ if (rad <= 1) {
rad = 0;
- for (j = 0; j < rad; j++)
- radpower[j] =
- alpha * (((rad * rad - j * j) * radbias) / (rad * rad));
+ }
+ for (j = 0; j < rad; j++) {
+ radPower[j] = alpha * (((rad * rad - j * j) * RAD_BIAS) / (rad * rad));
+ }
}
}
- //fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha);
+ //fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/INIT_ALPHA);
}
-
- /* 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;
+ /**
+ * Search for BGR values (after net is unbiased).
+ *
+ * @param blue intensity of blue 0..255
+ * @param green intensity of green 0..255
+ * @param red intensity of red 0..255
+ * @return colour index
+ */
+ public int map(final int blue, final int green, final int red) {
+
+ int bestD = 1000; /* biggest possible dist is 256*3 */
+ int best = -1;
+ int i = netIndex[green]; /* index on green */
+ int j = i - 1; /* start at netIndex[green] and work outwards */
+
+ int a;
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) {
+ while ((i < NET_SIZE) || (j >= 0)) {
+ int dist;
+ if (i < NET_SIZE) {
p = network[i];
- dist = p[1] - g; /* inx key */
- if (dist >= bestd)
- i = netsize; /* stop iter */
- else {
+ dist = p[1] - green; /* inx key */
+ if (dist >= bestD) {
+ i = NET_SIZE; /* stop loop */
+ } else {
i++;
- if (dist < 0)
+ if (dist < 0) {
dist = -dist;
- a = p[0] - b;
- if (a < 0)
+ }
+ a = p[0] - blue;
+ if (a < 0) {
a = -a;
+ }
dist += a;
- if (dist < bestd) {
- a = p[2] - r;
- if (a < 0)
+ if (dist < bestD) {
+ a = p[2] - red;
+ if (a < 0) {
a = -a;
+ }
dist += a;
- if (dist < bestd) {
- bestd = dist;
+ if (dist < bestD) {
+ bestD = dist;
best = p[3];
}
}
@@ -310,150 +355,182 @@ public int map(int b, int g, int r) {
}
if (j >= 0) {
p = network[j];
- dist = g - p[1]; /* inx key - reverse dif */
- if (dist >= bestd)
- j = -1; /* stop iter */
- else {
+ dist = green - p[1]; /* inx key - reverse dif */
+ if (dist >= bestD) {
+ j = -1; /* stop loop */
+ } else {
j--;
- if (dist < 0)
+ if (dist < 0) {
dist = -dist;
- a = p[0] - b;
- if (a < 0)
+ }
+ a = p[0] - blue;
+ if (a < 0) {
a = -a;
+ }
dist += a;
- if (dist < bestd) {
- a = p[2] - r;
- if (a < 0)
+ if (dist < bestD) {
+ a = p[2] - red;
+ if (a < 0) {
a = -a;
+ }
dist += a;
- if (dist < bestd) {
- bestd = dist;
+ if (dist < bestD) {
+ bestD = dist;
best = p[3];
}
}
}
}
}
- return (best);
+ return best;
}
+
public byte[] process() {
learn();
- unbiasnet();
- inxbuild();
+ 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;
+ /**
+ * Unbias network to give byte values 0..255 and record position i to prepare for sort
+ */
+ public void unbiasNet() {
- for (i = 0; i < netsize; i++) {
- network[i][0] >>= netbiasshift;
- network[i][1] >>= netbiasshift;
- network[i][2] >>= netbiasshift;
+ for (int i = 0; i < NET_SIZE; i++) {
+ network[i][0] >>= NET_BIAS_SHIFT;
+ network[i][1] >>= NET_BIAS_SHIFT;
+ network[i][2] >>= NET_BIAS_SHIFT;
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;
+ /**
+ * Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[rad]^2)) input radPower[|i-j|].
+ *
+ * @param rad radius of the area/neighbouring neurons we want to change
+ * @param i index of the central neuron
+ * @param blue blue part of the bias
+ * @param green green part of the bias
+ * @param red red part of the bias
+ */
+ protected void alterNeigh(final int rad, final int i, final int blue, final int green, final int red) {
+
+ int j;
+ int k;
+ int low;
+ int high;
+ int a;
+ int m;
+ int[] neuron;
+
+ low = i - rad;
+ if (low < -1) {
+ low = -1;
+ }
+ high = i + rad;
+ if (high > NET_SIZE) {
+ high = NET_SIZE;
+ }
j = i + 1;
k = i - 1;
m = 1;
- while ((j < hi) || (k > lo)) {
- a = radpower[m++];
- if (j < hi) {
- p = network[j++];
+ while ((j < high) || (k > low)) {
+ a = radPower[m++];
+ if (j < high) {
+ neuron = 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
+ neuron[0] -= (a * (neuron[0] - blue)) / ALPHA_RAD_BIAS;
+ neuron[1] -= (a * (neuron[1] - green)) / ALPHA_RAD_BIAS;
+ neuron[2] -= (a * (neuron[2] - red)) / ALPHA_RAD_BIAS;
+ } catch (final Exception exc) {
+ // prevents 1.3 mis-compilation
+ }
}
- if (k > lo) {
- p = network[k--];
+ if (k > low) {
+ neuron = 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) {
+ neuron[0] -= (a * (neuron[0] - blue)) / ALPHA_RAD_BIAS;
+ neuron[1] -= (a * (neuron[1] - green)) / ALPHA_RAD_BIAS;
+ neuron[2] -= (a * (neuron[2] - red)) / ALPHA_RAD_BIAS;
+ } catch (final Exception exc) {
}
}
}
}
-
- /* Move neuron i towards biased (b,g,r) by factor alpha
- ---------------------------------------------------- */
- protected void altersingle(int alpha, int i, int b, int g, int r) {
+
+ /**
+ * Move neuron neuronIdx towards biased (blue,green,red) by factor alpha.
+ *
+ * @param alpha how much to move
+ * @param neuronIdx index of the neuron to be moved
+ * @param blue blue part of the bias
+ * @param green green part of the bias
+ * @param red red part of the bias
+ */
+ protected void alterSingle(final int alpha, final int neuronIdx, final int blue, final int green, final int red) {
/* 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;
+ final int[] neuron = network[neuronIdx];
+ neuron[0] -= (alpha * (neuron[0] - blue)) / INIT_ALPHA;
+ neuron[1] -= (alpha * (neuron[1] - green)) / INIT_ALPHA;
+ neuron[2] -= (alpha * (neuron[2] - red)) / INIT_ALPHA;
}
-
- /* Search for biased BGR values
- ---------------------------- */
- protected int contest(int b, int g, int r) {
+
+ /**
+ * Search for biased BGR values
+ *
+ * @param blue intensity of blue 0..255
+ * @param green intensity of green 0..255
+ * @param red intensity of red 0..255
+ * @return position of the best bias
+ */
+ protected int contest(final int blue, final int green, final int red) {
/* 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]) */
+ /* bias[i] = GAMMA*((1/NET_SIZE)-freq[i]) */
- int i, dist, a, biasdist, betafreq;
- int bestpos, bestbiaspos, bestd, bestbiasd;
- int[] n;
+ int a;
- bestd = ~(((int) 1) << 31);
- bestbiasd = bestd;
- bestpos = -1;
- bestbiaspos = bestpos;
+ @SuppressWarnings("NumericOverflow") int bestD = ~(1 << 31);
+ int bestBiasD = bestD;
+ int bestPos = -1;
+ int bestBiasPos = bestPos;
- for (i = 0; i < netsize; i++) {
- n = network[i];
- dist = n[0] - b;
- if (dist < 0)
+ for (int i = 0; i < NET_SIZE; i++) {
+ final int[] neuron = network[i];
+ int dist = neuron[0] - blue;
+ if (dist < 0) {
dist = -dist;
- a = n[1] - g;
- if (a < 0)
+ }
+ a = neuron[1] - green;
+ if (a < 0) {
a = -a;
+ }
dist += a;
- a = n[2] - r;
- if (a < 0)
+ a = neuron[2] - red;
+ if (a < 0) {
a = -a;
+ }
dist += a;
- if (dist < bestd) {
- bestd = dist;
- bestpos = i;
+ if (dist < bestD) {
+ bestD = dist;
+ bestPos = i;
}
- biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift));
- if (biasdist < bestbiasd) {
- bestbiasd = biasdist;
- bestbiaspos = i;
+ final int biasDist = dist - ((bias[i]) >> (INT_BIAS_SHIFT - NET_BIAS_SHIFT));
+ if (biasDist < bestBiasD) {
+ bestBiasD = biasDist;
+ bestBiasPos = i;
}
- betafreq = (freq[i] >> betashift);
- freq[i] -= betafreq;
- bias[i] += (betafreq << gammashift);
+ final int betaFreq = (freq[i] >> BETA_SHIFT);
+ freq[i] -= betaFreq;
+ bias[i] += (betaFreq << GAMMA_SHIFT);
}
- freq[bestpos] += beta;
- bias[bestpos] -= betagamma;
- return (bestbiaspos);
+ freq[bestPos] += BETA;
+ bias[bestPos] -= BETA_GAMMA;
+ return bestBiasPos;
}
}
diff --git a/src/main/resources/checkstyle-suppressions.xml b/src/main/resources/checkstyle-suppressions.xml
new file mode 100644
index 0000000..2619a3b
--- /dev/null
+++ b/src/main/resources/checkstyle-suppressions.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/src/main/resources/checkstyle.xml b/src/main/resources/checkstyle.xml
new file mode 100644
index 0000000..3ca6add
--- /dev/null
+++ b/src/main/resources/checkstyle.xml
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/java_header_regex_template.txt b/src/main/resources/java_header_regex_template.txt
new file mode 100644
index 0000000..ac2fba0
--- /dev/null
+++ b/src/main/resources/java_header_regex_template.txt
@@ -0,0 +1,25 @@
+^/\*$
+^ \* The MIT License \(MIT\)$
+^ \*$
+^ \* Copyright \([cC]\) \d\d\d\d(-\d\d\d\d)? korhner \$
+^ \* Copyright \([cC]\) \d\d\d\d(-\d\d\d\d)? .+$
+^ \*$
+^ \* Permission is hereby granted, free of charge, to any person obtaining a copy$
+^ \* of this software and associated documentation files \(the "Software"\), to deal$
+^ \* in the Software without restriction, including without limitation the rights$
+^ \* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell$
+^ \* copies of the Software, and to permit persons to whom the Software is$
+^ \* furnished to do so, subject to the following conditions\:$
+^ \*$
+^ \* The above copyright notice and this permission notice shall be included in all$
+^ \* copies or substantial portions of the Software\.$
+^ \*$
+^ \* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR$
+^ \* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,$
+^ \* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE$
+^ \* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER$
+^ \* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,$
+^ \* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE$
+^ \* SOFTWARE\.$
+^ \*/$
+^$
\ No newline at end of file
diff --git a/src/main/resources/pmd.xml b/src/main/resources/pmd.xml
new file mode 100644
index 0000000..0c49a77
--- /dev/null
+++ b/src/main/resources/pmd.xml
@@ -0,0 +1,48 @@
+
+
+
+ PMD rules selection suitable for JavaOSC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/io/korhner/asciimg/Examples.java b/src/test/java/io/korhner/asciimg/Examples.java
deleted file mode 100644
index c760951..0000000
--- a/src/test/java/io/korhner/asciimg/Examples.java
+++ /dev/null
@@ -1,83 +0,0 @@
-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;
-import io.korhner.asciimg.image.character_fit_strategy.StructuralSimilarityFitStrategy;
-import io.korhner.asciimg.image.converter.AsciiToImageConverter;
-import io.korhner.asciimg.image.converter.AsciiToStringConverter;
-
-import java.awt.Font;
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-
-import javax.imageio.ImageIO;
-
-public class Examples {
-
- public static void main(String[] args) throws IOException {
-
- // initialize caches
- AsciiImgCache smallFontCache = AsciiImgCache.create(new Font("Courier",
- Font.BOLD, 6));
- AsciiImgCache mediumBlackAndWhiteCache = AsciiImgCache.create(new Font(
- "Courier", Font.BOLD, 10), new char[] {'\\', ' ', '/'});
- AsciiImgCache largeFontCache = AsciiImgCache.create(new Font("Courier",
- Font.PLAIN, 16));
-
- // load image
- BufferedImage portraitImage = ImageIO.read(new File(
- "examples/portrait.png"));
-
- // initialize algorithms
- BestCharacterFitStrategy squareErrorStrategy = new ColorSquareErrorFitStrategy();
- BestCharacterFitStrategy ssimStrategy = new StructuralSimilarityFitStrategy();
-
- // initialize converters
- AsciiToImageConverter imageConverter = new AsciiToImageConverter(
- smallFontCache, squareErrorStrategy);
- AsciiToStringConverter stringConverter = new AsciiToStringConverter(
- largeFontCache, ssimStrategy);
-
- // small font images, square error
- imageConverter.setCharacterCache(smallFontCache);
- imageConverter.setCharacterFitStrategy(squareErrorStrategy);
- ImageIO.write(imageConverter.convertImage(portraitImage), "png",
- new File("examples/portrait_small_square_error.png"));
-
- // medium font images, square error
- imageConverter.setCharacterCache(mediumBlackAndWhiteCache);
- imageConverter.setCharacterFitStrategy(squareErrorStrategy);
- ImageIO.write(imageConverter.convertImage(portraitImage), "png",
- new File("examples/portrait_medium_square_error.png"));
-
- // large font images, square error
- imageConverter.setCharacterCache(largeFontCache);
- imageConverter.setCharacterFitStrategy(squareErrorStrategy);
- ImageIO.write(imageConverter.convertImage(portraitImage), "png",
- new File("examples/portrait_large_square_error.png"));
-
- // small font images, ssim
- imageConverter.setCharacterCache(smallFontCache);
- imageConverter.setCharacterFitStrategy(ssimStrategy);
- ImageIO.write(imageConverter.convertImage(portraitImage), "png",
- new File("examples/portrait_small_ssim.png"));
-
- // medium font images, ssim error
- imageConverter.setCharacterCache(mediumBlackAndWhiteCache);
- imageConverter.setCharacterFitStrategy(ssimStrategy);
- ImageIO.write(imageConverter.convertImage(portraitImage), "png",
- new File("examples/portrait_medium_ssim.png"));
-
- // large font images, ssim
- imageConverter.setCharacterCache(largeFontCache);
- imageConverter.setCharacterFitStrategy(ssimStrategy);
- ImageIO.write(imageConverter.convertImage(portraitImage), "png",
- new File("examples/portrait_large_ssim.png"));
-
- // string converter, output to console
- System.out.println(stringConverter.convertImage(portraitImage));
-
- }
-}
diff --git a/src/test/java/io/korhner/asciimg/GifExamples.java b/src/test/java/io/korhner/asciimg/GifExamples.java
deleted file mode 100644
index 08b7c7c..0000000
--- a/src/test/java/io/korhner/asciimg/GifExamples.java
+++ /dev/null
@@ -1,37 +0,0 @@
-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;
-import io.korhner.asciimg.image.converter.AsciiToImageConverter;
-import io.korhner.asciimg.image.converter.GifToAsciiConvert;
-import io.korhner.asciimg.utils.AnimatedGifEncoder;
-import io.korhner.asciimg.utils.GifDecoder;
-
-import java.awt.Font;
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.imageio.ImageIO;
-
-public class GifExamples {
-
- public static void main(String[] args) throws IOException {
-
- // initialize caches
- AsciiImgCache smallFontCache = AsciiImgCache.create(new Font("Courier",Font.BOLD, 6));
- // initialize ssimStrategy
- BestCharacterFitStrategy ssimStrategy = new StructuralSimilarityFitStrategy();
-
- String srcFilePath = "examples/test.gif";
- String disFilePath = "examples/test-ascii.gif";
- int delay = 100;//ms
-
- GifToAsciiConvert asciiConvert = new GifToAsciiConvert(smallFontCache, ssimStrategy);
-
- asciiConvert.convertGitToAscii(srcFilePath, disFilePath, delay,0);
- }
-}
diff --git a/src/test/java/io/korhner/asciimg/image/converter/GifToAsciiConverterTest.java b/src/test/java/io/korhner/asciimg/image/converter/GifToAsciiConverterTest.java
new file mode 100644
index 0000000..4cd75ad
--- /dev/null
+++ b/src/test/java/io/korhner/asciimg/image/converter/GifToAsciiConverterTest.java
@@ -0,0 +1,93 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.converter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.importer.GifImageImporter;
+import io.korhner.asciimg.image.strategy.CharacterFitStrategy;
+import io.korhner.asciimg.image.strategy.ColorSquareErrorCharacterFitStrategy;
+import io.korhner.asciimg.image.strategy.StructuralSimilarityCharacterFitStrategy;
+import io.korhner.asciimg.image.exporter.AnimatedGifMultiFrameAsciiExporter;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.awt.Font;
+import java.io.*;
+
+public class GifToAsciiConverterTest {
+
+ public static final String ORIGIN_RESOURCE_PATH = "/examples/animation/orig";
+ public static final String EXPECTED_RESOURCE_PATH = "/examples/animation/ascii_expected_%s";
+ public static final String RESOURCE_SUFFIX = ".gif";
+
+ private void testAnimationConversion(final CharacterFitStrategy characterFitStrategy, final String specifier) throws IOException {
+
+ // initialize caches
+ final AsciiImgCache smallFontCache = AsciiImgCache.create(new Font("Courier",Font.BOLD, 6));
+ final AnimatedGifMultiFrameAsciiExporter exporter = new AnimatedGifMultiFrameAsciiExporter();
+
+ final int delay = 100; // ms
+ final int repeat = 0; // times
+
+ final GifToAsciiConverter asciiConvert = new GifToAsciiConverter();
+ asciiConvert.setImporter(new GifImageImporter());
+ asciiConvert.setCharacterFitStrategy(characterFitStrategy);
+ asciiConvert.setCharacterCache(smallFontCache);
+ asciiConvert.setExporter(exporter);
+ exporter.setDelay(delay);
+ exporter.setRepeat(repeat);
+
+ final String expectedResStr = String.format(EXPECTED_RESOURCE_PATH, specifier);
+
+ final InputStream origSrc = getClass().getResourceAsStream(ORIGIN_RESOURCE_PATH + RESOURCE_SUFFIX);
+ asciiConvert.convert(origSrc);
+ final byte[] actual = exporter.getOutput();
+ final File actualTestImgFile = File.createTempFile(new File(expectedResStr).getName(), RESOURCE_SUFFIX);
+ if (ImageToAsciiConverterTest.DELETE_FILES) {
+ actualTestImgFile.deleteOnExit();
+ }
+ final OutputStream output = new FileOutputStream(actualTestImgFile);
+ output.write(actual);
+ output.close();
+
+ final InputStream expectedSrc = getClass().getResourceAsStream(expectedResStr + RESOURCE_SUFFIX);
+ final byte[] expected = ImageToAsciiConverterTest.readFully(expectedSrc);
+
+ // NOTE It is probably unlikely that we will get the exact same result on different systems,
+ // so we might have to revise or disable this check.
+ Assert.assertArrayEquals("generated and expected animated giff differ", expected, actual);
+ }
+
+ @Test
+ public void testAnimationConversionSsim() throws IOException {
+ testAnimationConversion(new StructuralSimilarityCharacterFitStrategy(), "ssim");
+ }
+
+ @Test
+ public void testAnimationConversionSquareError() throws IOException {
+ testAnimationConversion(new ColorSquareErrorCharacterFitStrategy(), "square_error");
+ }
+}
diff --git a/src/test/java/io/korhner/asciimg/image/converter/ImageToAsciiConverterTest.java b/src/test/java/io/korhner/asciimg/image/converter/ImageToAsciiConverterTest.java
new file mode 100644
index 0000000..f4467b8
--- /dev/null
+++ b/src/test/java/io/korhner/asciimg/image/converter/ImageToAsciiConverterTest.java
@@ -0,0 +1,307 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 korhner
+ * Copyright (c) 2018 hoijui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.korhner.asciimg.image.converter;
+
+import io.korhner.asciimg.image.AsciiImgCache;
+import io.korhner.asciimg.image.importer.BufferedImageImageImporter;
+import io.korhner.asciimg.image.strategy.CharacterFitStrategy;
+import io.korhner.asciimg.image.strategy.ColorSquareErrorCharacterFitStrategy;
+import io.korhner.asciimg.image.strategy.StructuralSimilarityCharacterFitStrategy;
+import io.korhner.asciimg.image.exporter.ImageAsciiExporter;
+import io.korhner.asciimg.image.exporter.TextAsciiExporter;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.awt.Font;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.util.List;
+import java.util.Scanner;
+import javax.imageio.ImageIO;
+
+public class ImageToAsciiConverterTest {
+
+ /**
+ * Indicates whether to delete files created during unit test runs.
+ * You might want to manually set this to false, in case of test errors,
+ * so you can manually inspect them after tests finished.
+ */
+ public static final boolean DELETE_FILES = true;
+ private static final String ORIGIN_RESOURCE_PATH = "/examples/portrait/orig";
+ private static final String EXPECTED_RESOURCE_PATH = "/examples/portrait/ascii_expected_%s";
+ private static final String ACTUAL_NAME = "ascii_actual_%s_";
+ private static final String RESOURCE_SUFFIX_IMG = ".png";
+ private static final String RESOURCE_SUFFIX_TXT = ".txt";
+
+ public static byte[] readFully(final InputStream input) throws IOException {
+
+ final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ int nRead;
+ byte[] data = new byte[16384];
+
+ while ((nRead = input.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+
+ buffer.flush();
+
+ return buffer.toByteArray();
+ }
+
+ private void convertToText(
+ final BufferedImage origImage,
+ final String specifier,
+ final CharacterFitStrategy characterFitStrategy,
+ final AsciiImgCache cache,
+ final ImageToAsciiConverter converter)
+ throws IOException
+ {
+ converter.setImporter(new BufferedImageImageImporter());
+ converter.setCharacterCache(cache);
+ converter.setCharacterFitStrategy(characterFitStrategy);
+ final TextAsciiExporter textAsciiExporter = new TextAsciiExporter();
+ converter.setExporter(textAsciiExporter);
+
+ converter.convert(origImage);
+
+ // extract result ("actual")
+ final String actual = ((List) converter.getExporter().getOutput()).get(0);
+
+ final String expectedResourcePath = String.format(EXPECTED_RESOURCE_PATH, specifier);
+ // write "actual" to file
+ if (!ImageToAsciiConverterTest.DELETE_FILES || true) {
+ final String actualFilePrefix = String.format(ACTUAL_NAME, specifier);
+ final File actualFile = File.createTempFile(actualFilePrefix, RESOURCE_SUFFIX_TXT);
+ final PrintWriter out = new PrintWriter(actualFile);
+ out.print(actual);
+ out.close();
+ }
+
+ // read "expected" from resource/file
+ final InputStream expectedIn = getClass().getResourceAsStream(expectedResourcePath + RESOURCE_SUFFIX_TXT);
+ final Scanner s = new Scanner(expectedIn).useDelimiter("\\A");
+ final String expected = s.hasNext() ? s.next() : "";
+
+ // compare "expected" and "actual"
+ Assert.assertEquals(expected, actual);
+ }
+
+ private void convertToImageAndCheck(
+ final BufferedImage origImage,
+ final String expectedResourcePath,
+ final CharacterFitStrategy characterFitStrategy,
+ final AsciiImgCache cache,
+ final ImageToAsciiConverter converter)
+ throws IOException
+ {
+ converter.setImporter(new BufferedImageImageImporter());
+ converter.setCharacterCache(cache);
+ converter.setCharacterFitStrategy(characterFitStrategy);
+ final ImageAsciiExporter imageAsciiExporter = new ImageAsciiExporter();
+ converter.setExporter(imageAsciiExporter);
+
+ converter.convert(origImage);
+
+ final BufferedImage expected = ImageIO.read(getClass().getResourceAsStream(expectedResourcePath + RESOURCE_SUFFIX_IMG));
+ final BufferedImage actual = imageAsciiExporter.getOutput().get(0);
+
+ // TODO implement comparison
+// actual.getData().getDataBuffer().getSize()
+
+ if (!DELETE_FILES) {
+ final File actualTestImgFile = File.createTempFile(new File(expectedResourcePath).getName(), RESOURCE_SUFFIX_IMG);
+ System.err.println("Writing actual file to: " + actualTestImgFile.getAbsolutePath());
+ ImageIO.write(actual, "png", actualTestImgFile);
+ }
+ }
+
+ private static AsciiImgCache smallFontCache;
+ private static AsciiImgCache mediumBlackAndWhiteCache;
+ private static AsciiImgCache largeFontCache;
+ private static BufferedImage portraitImage;
+ private static CharacterFitStrategy squareErrorStrategy;
+ private static CharacterFitStrategy ssimStrategy;
+ private static ImageToAsciiConverter imageConverter;
+ private static ImageToAsciiConverter stringConverter;
+
+ @BeforeClass
+ public static void initConversionRequirements() throws IOException {
+
+ // initialize caches
+ smallFontCache = AsciiImgCache.create(
+ new Font("Courier", Font.BOLD, 6));
+ mediumBlackAndWhiteCache = AsciiImgCache.create(
+ new Font("Courier", Font.BOLD, 10), new char[]{'\\', ' ', '/'});
+ largeFontCache = AsciiImgCache.create(
+ new Font("Courier", Font.PLAIN, 16));
+
+ // load image
+ portraitImage = ImageIO.read(ImageToAsciiConverterTest.class.getResourceAsStream(
+ ORIGIN_RESOURCE_PATH + RESOURCE_SUFFIX_IMG));
+
+ // initialize algorithms
+ squareErrorStrategy = new ColorSquareErrorCharacterFitStrategy();
+ ssimStrategy = new StructuralSimilarityCharacterFitStrategy();
+
+ // initialize converters
+ imageConverter = new ImageToAsciiConverter();
+ stringConverter = new ImageToAsciiConverter();
+ }
+
+ @Test
+ public void testToImageSmallFontSquareError() throws IOException {
+
+ convertToImageAndCheck(
+ portraitImage,
+ String.format(EXPECTED_RESOURCE_PATH, "small_square_error"),
+ squareErrorStrategy,
+ smallFontCache,
+ imageConverter);
+ }
+
+ @Test
+ public void testToImageMediumBwFontSquareError() throws IOException {
+
+ convertToImageAndCheck(
+ portraitImage,
+ String.format(EXPECTED_RESOURCE_PATH, "medium_square_error"),
+ squareErrorStrategy,
+ mediumBlackAndWhiteCache,
+ imageConverter);
+ }
+
+ @Test
+ public void testToImageLargeFontSquareError() throws IOException {
+
+ convertToImageAndCheck(
+ portraitImage,
+ String.format(EXPECTED_RESOURCE_PATH, "large_square_error"),
+ squareErrorStrategy,
+ largeFontCache,
+ imageConverter);
+ }
+
+ @Test
+ public void testToImageSmallFontSsim() throws IOException {
+
+ convertToImageAndCheck(
+ portraitImage,
+ String.format(EXPECTED_RESOURCE_PATH, "small_ssim"),
+ ssimStrategy,
+ smallFontCache,
+ imageConverter);
+ }
+
+ @Test
+ public void testToImageMediumBwFontSsim() throws IOException {
+
+ convertToImageAndCheck(
+ portraitImage,
+ String.format(EXPECTED_RESOURCE_PATH, "medium_ssim"),
+ ssimStrategy,
+ mediumBlackAndWhiteCache,
+ imageConverter);
+ }
+
+ @Test
+ public void testToImageLargeFontSsim() throws IOException {
+
+ convertToImageAndCheck(
+ portraitImage,
+ String.format(EXPECTED_RESOURCE_PATH, "large_ssim"),
+ ssimStrategy,
+ largeFontCache,
+ imageConverter);
+ }
+
+ @Test
+ public void testToTextSmallFontSquareError() throws IOException {
+
+ convertToText(
+ portraitImage,
+ "small_square_error",
+ squareErrorStrategy,
+ smallFontCache,
+ stringConverter);
+ }
+
+ @Test
+ public void testToTextMediumBwFontSquareError() throws IOException {
+
+ convertToText(
+ portraitImage,
+ "mediumBw_square_error",
+ squareErrorStrategy,
+ mediumBlackAndWhiteCache,
+ stringConverter);
+ }
+
+ @Test
+ public void testToTextLargeFontSquareError() throws IOException {
+
+ convertToText(
+ portraitImage,
+ "large_square_error",
+ squareErrorStrategy,
+ largeFontCache,
+ stringConverter);
+ }
+
+ @Test
+ public void testToTextSmallFontSsim() throws IOException {
+
+ convertToText(
+ portraitImage,
+ "small_ssim",
+ ssimStrategy,
+ smallFontCache,
+ stringConverter);
+ }
+
+ @Test
+ public void testToTextMediumBwFontSsim() throws IOException {
+
+ convertToText(
+ portraitImage,
+ "mediumBw_ssim",
+ ssimStrategy,
+ mediumBlackAndWhiteCache,
+ stringConverter);
+ }
+
+ @Test
+ public void testToTextLargeFontSsim() throws IOException {
+
+ convertToText(
+ portraitImage,
+ "large_ssim",
+ ssimStrategy,
+ largeFontCache,
+ stringConverter);
+ }
+}
diff --git a/src/test/resources/examples/animation/ascii_expected_square_error.gif b/src/test/resources/examples/animation/ascii_expected_square_error.gif
new file mode 100644
index 0000000..ea3338d
Binary files /dev/null and b/src/test/resources/examples/animation/ascii_expected_square_error.gif differ
diff --git a/src/test/resources/examples/animation/ascii_expected_ssim.gif b/src/test/resources/examples/animation/ascii_expected_ssim.gif
new file mode 100644
index 0000000..9885dc5
Binary files /dev/null and b/src/test/resources/examples/animation/ascii_expected_ssim.gif differ
diff --git a/examples/test.gif b/src/test/resources/examples/animation/orig.gif
similarity index 100%
rename from examples/test.gif
rename to src/test/resources/examples/animation/orig.gif
diff --git a/examples/portrait_large_square_error.png b/src/test/resources/examples/portrait/ascii_expected_large_square_error.png
similarity index 100%
rename from examples/portrait_large_square_error.png
rename to src/test/resources/examples/portrait/ascii_expected_large_square_error.png
diff --git a/src/test/resources/examples/portrait/ascii_expected_large_square_error.txt b/src/test/resources/examples/portrait/ascii_expected_large_square_error.txt
new file mode 100644
index 0000000..1fbe40e
--- /dev/null
+++ b/src/test/resources/examples/portrait/ascii_expected_large_square_error.txt
@@ -0,0 +1,68 @@
+ d@@@@@@@@@@@@@@@@@Q
+ \$@@@@@@@@@@@@@@@@@@@@Q
+ d@@@@@@@@@@@@@@@@@@@@@@@@/
+ $@@@@@@@@@@@@@@@@@@@@@@@@@@/
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@/
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+ $@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+ Q@@@@@@@@@@@@%\/)@@@@@@@@@@@@@@@@@\
+ @@@@@@@@ww` >< _w@@@@@@@@@@@@
+ Q@@@@@@~ w@@@@@@@@@@b
+ @@@@@@ @@@@@@@@@@
+ q@@@@@~ @@@@@@@@@b
+ @@@@@> @@@@@@@@@\
+ /@@@@@ w@@@@@@@@@
+ @@@@@> w@@@@@@@@
+ @@@@@ <@@@@@@@@b
+ <@@@@@ @@@@@@@@$
+ Q@@@@> W@@@@@@@@>
+ q@@@@ //\/ \\ <@@@@@@@@.
+ q@@@@ d@@@@@@@Q/ \QQ@@@@@@Q @@@@@@@@8
+ Q@@@@ w\/\QQQww@@ W@@@QQQ/QQ@) w@@@@@@M@
+ @@@@@ @@@@@@@@@/% <`Q@@@@@@@@> <@@@@@@M'
+ @@@@ w@@@w@@~w@ \@ @@@w@@@ <@@@@@@@
+ >@@@ `@@@QQ\%< w>%QQb@@w <@@@@<`
+ *@@ .ww ~www Q@@@@<
+ @) Q@@@@
+ /@ @@@@@
+ <@ @>$@@
+ <@/ @@ <@@@@@
+ <@@ \ \/ q@@@@m
+ >@ @@/\@@~ > $@@@@w
+ W/ w@@w > @@@@%/
+ >\ @@@@
+ <
+ \ `_www_` @@@/Q
+ <\ > \/ /> $@@w`
+ <\\
+ >\/ \Q@@@@>
+ >W@Q/ \@@@><>
+ w@@@/ Q@@@>>
+ >@@@Q/ Q@@@>>
+ w@@@@QQQ@@@@@>
+ w@@@@@@@@@w
+ <@@@@@@@>
+
+ #/
+ $ <@b
+Q@ @@
+@m q@/
+@ @b
+@ @b
+@ @
+b @
+ $
+ $b
+ $b
+ @$
+ @@
+ @@
+ @@
+ @@
+ @@
+ @@
+ @b
+ W@b
+ q@b
+ d@ >//
diff --git a/examples/portrait_large_ssim.png b/src/test/resources/examples/portrait/ascii_expected_large_ssim.png
similarity index 100%
rename from examples/portrait_large_ssim.png
rename to src/test/resources/examples/portrait/ascii_expected_large_ssim.png
diff --git a/src/test/resources/examples/portrait/ascii_expected_large_ssim.txt b/src/test/resources/examples/portrait/ascii_expected_large_ssim.txt
new file mode 100644
index 0000000..17d0f7b
--- /dev/null
+++ b/src/test/resources/examples/portrait/ascii_expected_large_ssim.txt
@@ -0,0 +1,68 @@
+ /%%@@@@@@@@@@@@@@@\
+ /@@@@@@@@@@@@@@@@@@@@\
+ /%@@@@@@@@@@@@@@@@@@@@@@%
+ /@@@@@@M@@@@@@@@@@M@@@@@@@%
+ %%@@@@@@@@%@@@@@@@@@@@@@@@@@%
+ %@@@@@@@@@@Q@@@@@@@@@@@@@#@@@@%
+ Q@@@@@@@@@@%w%%<@@@@@@@@@@@@@@@@%
+ (@@@@@@@@@@@<\ / >%@@@@@@@@@@@@@@@\
+ %%@@@@@@w~ `w@@@@@@@@@@@%
+ (@@@@@w` ~%M@@@@@@@@\
+ %@@@@% %@@@@#@@@%
+ \@@@@@ %@M@M@@@@\
+ Q@@@@> @@@#@@@@@\
+ /@@@@> @@@#M@@@\
+ @@@@@ <@MM##@@%
+ %@@@> %@$B@@@@\
+ @@@@> %@M$B@@@%
+ \@@@@ <@###fp@@>
+ %@@@@ /// %@M@$U#@`
+ (@@@@ /@%@@@@@\/ //@@@@@%\ <@@BL@@w8
+ (@@@@ < //\//.w%\ <@@w~\\\//>> <@#$###\>
+ <>@@@ \%@@@@@@\ > /%@@@@@%\ <@@###@m
+ @@@@ <%@m<%% ~\ \* @@%~@@> @@@@@@\
+ >\%@ %%Q//\\ >>\(\/W@> <@@@@
+ %@ `` < ~ <%@%%
+ %/ <@@>/
+ \ />>%%
+ < > 8%%
+ \q@@W
+ <\ ##@@*
+ >> \ (q@@%>
+ > %%/ /@` {#@@%
+ > ~%@` @@@@
+ \ #@@<
+ <\\/ \ /\/\>< (@@<
+ \ `~~~` @@>/
+ Q%>>
+ \ ~>~ /%>>
+ >\ \>>>>
+ <>\ \%%<
+ <>%%/ /@%>
+ <%%\ \Q%>
+ <%%@%Q\\%@%%>
+ <%@@@@@@>>
+ >w%%%w>
+ <>>
+ #
+ / @
+ @ @%
+@. &@
+@ @
+@ @b
+' @
+ #
+ 8
+ $
+ #)
+ &|
+ @@
+ @j
+ @@
+ @@
+ @$
+ @}
+ @m
+ @
+ q@
+ (@
diff --git a/src/test/resources/examples/portrait/ascii_expected_mediumBw_square_error.txt b/src/test/resources/examples/portrait/ascii_expected_mediumBw_square_error.txt
new file mode 100644
index 0000000..bef3203
--- /dev/null
+++ b/src/test/resources/examples/portrait/ascii_expected_mediumBw_square_error.txt
@@ -0,0 +1,102 @@
+ //\//\\//////\/////\\//\///////\/\\\/
+ \\\/\\\//\////\\//\\///\\\//\\\\//\////\//
+ \/\\////\/\\\/\/////\\/\\//\/\//\\\/\\//\\\\\/
+ ///\\\\//\\\/\\\\///\/\\/\//\//\\\\/\//\//\\\//\\/
+ /\//\\/\/\/\//\\////\//\//\//\/\\/\\\/\///\\/\\\\\\\\
+ ///\/\\\//\\///\///\\\////\\/\/\//\\\/\/\\//\///\////\\/
+ //\///\\\\\//\//////\\\/\\/\/\///\\\\//////\/////\/\/\/\\/\
+ ///\\/\/\//\\/\\//\///\\/\\////\//\\\/\/\\/\\\//\///\//\\//\\\
+ ////\/\/////\///\/\\\\/\\/\/\\\\\//\/\////\\\//\\\///\\//\\\//\\
+ \/\\\/\//\///\///\/\\\////\\\/\\\\/\\\\/\/\//\////\\\/\\/////\/\/\
+ //\///\//\\\\//\\/\//\\/\/\//\\\\\//\\\\///\//\\/\//////\/\/\\////\\
+ /\\//\\///\/\\\\//////\/\//\\ /\/ //\\//\\\\\\//\/\/\\////////\\////\
+ \/\\\\\\//\\//\////\ / /\\\\//\/\\//\/\\\\\//\\/\\
+ \\\/\/\\/\\///\ /\\\\\/\\\///\//\/\///\
+ /\/\\\////\//\ \\/\\/////\//\//\\\\\
+ \/\//\/////\\ /\\//\/\\/\///\/\//\/
+ //\//\/\///\ \\\//\/\////\\\/\/\/
+ \/\////\\//\ /\\/\\\/\//\///\\\\
+ ///\\/////\ /\///\\//\/\///\\\\\
+ /\\/\\\///\ /\/\\//\\/\/\/\\////
+ ///\\\////\ /\\////\\//\\//\/\\
+ /\\\////\\/ /\\///\\/\///\/\\\\
+ \//\/\\\//\ \/\\/\\/\\/\/\//\\/
+ \/\/\\////\ \/\\\\//\////\\\\\
+ \\\\\\\//\ /\/\/\\/\/\//\//\\/
+ \/\\/\///\ /\\\\/\\\\\//\\//\\
+ \//\\/\\// /\\\\\////\\//\/\\\
+ \\\\\/\/\/ /\/ /\\\\//\\/\////\/
+ /\\//\\/\/ //\\/\\\\/\\\\/ \\\///\\//\/\ /\\\/\////\/\\\\\\
+ \\/\/\\\/\ //\/////\/\\\\\\\\\\/ \\//\/\\/////\/\\\\/ /\\\//\\/\\//\//\\
+ //\\/\\/\/ /\\ /\\\\/\////\\\\\ ///////\\\\///\/\////\/ /\/\\\\//\//\/\///
+ ////\/\/\/ \\/\\\///\\/////\/ /\/ ///\\\//\\/\\/\\///\\/\ //\//\\/\\///\\/\
+ /\\\///\\/ /\/\/\\//\/\\/\/\/\/ \ / \/\/\/\////\\/\/\\\\ \\/\\\\/////\\//
+ ///\///// /\/\///\/\/\\/\ /\\ //\ ///\\///\\\///\ \\\\/\\////\\\\
+ \\\\/\/\ /\/\\\ /\/\\ / /\ /\ \ //\/\\\////\ //\////\/////\\
+ //\\\\ //\\//\//\/\\/ /\\\/\\\/\///\ ////\/\//\
+ \\\\ \/ \ ////\ \\/\//\//\\
+ \\\/ \\\/\//\/\
+ \\/ \//\\\\///
+ \\ \///\\//\/
+ \//\ \///\\\/\//
+ \//\ \/\ \/\\\/\\
+ \\/\/ /\\///\//\\
+ \//\\ \//\\\///\\
+ /\\// /\/\/\\\\\\
+ \\//\ \///\\//\///
+ \\/\ \\// \\/// \ \/\\\/\\/\//
+ //\ /\\\\\///// /\/\/\/\\\
+ //\ //\\//\ //\/\//\//
+ \/\ \///\//\\/ /
+ // ///\/\//
+ \\ \\//\///\\\ \//\\\\\\//\ \/\/\/\\/
+ / //\\\\/\/\/\//// /\/\//\/ /
+ \/ ///\\\\///
+ \/ ///////\
+ /\\/ \\\/\///\ \//\/////
+ /\/\ \\/\/\\//\/
+ //\\/\\ \\//\//\//\\
+ \ /\\/\ \\//\//\\\//
+ \\\/\\/ /\/////\\/ \
+ ///\\\\\/ \//\\/\\/\
+ \////\/\\/ \//\//\\\
+ \/\\\\/\\// \\\///////
+ /\\\\/\\\//\\////////\//\\
+ ///\\/\/\\\////////\/\
+ /////\/\\\//\/\//\\
+ \\//////\\\///\\
+ //\\\/\\/\\/
+
+ \\
+ \/ \\\
+ \//\ /\\\
+ /\/ \\//
+/\/ ///\
+\/ \\//
+\/ \/\/
+/\ ///\
+\ /\\\
+/ //
+/ \/
+\ \/
+ \\
+ \\/
+ //\
+ //\
+ /\\
+ \\\
+ \//
+ //\
+ //\/
+ \///
+ \/\/
+ \//\
+ \\\/
+ /\\/
+ /\//
+ //\/
+ ////
+ ////
+ \\/\
+ \//
+ \\\\ //\\
diff --git a/src/test/resources/examples/portrait/ascii_expected_mediumBw_ssim.txt b/src/test/resources/examples/portrait/ascii_expected_mediumBw_ssim.txt
new file mode 100644
index 0000000..b1c1c05
--- /dev/null
+++ b/src/test/resources/examples/portrait/ascii_expected_mediumBw_ssim.txt
@@ -0,0 +1,102 @@
+ ///////\\\\\////\\\\/\//\\\\/\//\\/\
+ \\//\\\\/\\\\/\/\//\\\///\\////\\/\///\/
+ /\\/\/\////\\//\\/\//\/////\//\///\////////\
+ \//\/\/\////\///\\/\/\/\\/\//\\\///\/\\/\\///\\\\/
+ //////\/\/\/\\//\/\\/\\//\/\\/\//\///\/\\\\/\/////\\
+ ///\////\\//\\////\///\\/\\/\/\//\///\/\//\\/////\\\\///
+ //\\/////\\\\//\\\\\/\/\//\/\/\\/\////\/\\\/\\\\\///\/\\/\\
+ //\\/\/\/\\//\//////\\\/\///\\\/\\/////\///////\/\\\/\\//\\/\
+ ///\/\/\\\\\/\\//\//\//\///\/\\//\///\\\\\///\\///\\\//\\//\\/\\
+ /\//\\\\\///\/\\\////\///\\\\/\\//\/////\/\\\////\///\/\\\//\//\\\
+ ////\\\\\////\/////\/\//\/ \\/\///\/\\/\\/\///\\\\\/\/\//\\\/\
+ /\/////\\\/\///\\/\//\\/\/ \//\\\//\///\/\////\/\\/\///\//\
+ \\/\////\\//\\///// \\\//\/\//\\/\//////\//\\\
+ \/\\/\//\///// \\\/\\///\\\/\\/\/\\\\
+ ////\/\\\\\// \\\//\\/\\/\\/\\////\
+ //\\/\\\\/\\ \\\\/\\/\\\///\/\/\/
+ ///\\/\/\// \\/\\/\/\\\\///\//\
+ \/\\\///\/ /\/\///\//\/\\\/\/\
+ /\\//\\/// /\\\\//\\/\/\\\///\
+ /\/\///\\/ \\//\\////\/\//\\/
+ //\///\/\/\ \/\\\\\//\//\\/\\\
+ ////\\/\// \/\\\//\/\\\\\/\\
+ /\\/\/////\ /\/\//\\/\/\/\//\
+ \/\\//\\\\ \/////\//\\\\///\\
+ \//////\// \\/\//\/\////\\/\
+ /\//\/\/\ /\//\\/////\\///\\
+ \\//\//\/ \\///\\\\///\///\\
+ \////\/\// /\///\\//\/\\\\\/
+ \/\\//\// //\/\/\\\/\\\\/ ////\\//\\ \\/\/\\\\/\/////\
+ //\\//// ////// \\/\\\///\\\\ ////\//\////\/\\\\\ /\//\\/\\//\\/\/\\
+ \//\//\/ /\ \\\/\ /\/\//\\\\/// /\ \\////\\/\\\\\///
+ /////\/\/ \/\\//\\/\//\\/\ \ / \\//\/\\///\/\\\ //\\//\///\\///\
+ /\\//\\// \//\/\/\\//\///\\\ ///\\/\\\\//\/\\\\ \\////\\\\\//\
+ /\/\\\\\/ \\/\\/ \\\// \\ // /\//// \//\// \\/\//\/\\///\
+ \/\//\\ \//\/ \// \ /\ /\/\ \/\//\ //\\\//\/\ /\
+ /\\\\ /\\/\\//\/ \\\\\//// /\////\/\
+ \\/\ \\\\/\//\
+ \\/ \\/\//\//
+ \\/ //\/\\///
+ \\ \///\\//\/
+ / /// \/\\//
+ / / \/\///
+ /\ \\\\/\/\/
+ /\\ \\\//////\\
+ \\/ //\/\/\\/\/
+ \/\\ \\\\/\/////
+ /\ \\\/ \/// \\/////\/\ /
+ /\ \\\\\/\/// //\/\/\/\\
+ / \\/// \\/\//\\/
+ /\ \\\/\/\\ /
+ /\///\//
+ \ \ \\\\/ //\//\\
+ /\\\\/\/\/\// /\/\// /
+ //\//\\/
+ /\/////\
+ \\/ /// //////
+ /\/\\//
+ //\/ \//
+ \\/ \//\//
+ \\/\\ \/////\
+ /\\\\\ \//\\/\
+ \/\/\\ \//\//\
+ \\\\/\\/ \//\\///
+ \\\\//\\\/\\/\/////\\//\
+ \\\\/\/\\////\\\\///
+ ///\/\///\\/////\
+ \//\/\//\\///\
+ \\\\\\/
+
+ \\
+ //\
+ // //\
+ /\/ \/\/
+/\/ \\\\
+// \/\
+\\ \\\/
+\ /\\
+/ //
+/ \\
+ \/
+ \\
+ \/
+ \/
+ /\\
+ /\\
+ \/\
+ ///
+ /\\
+ \\/
+ //\
+ \\\
+ \/\
+ \\/
+ ////
+ /\//
+ \/\/
+ /\//
+ \\\/
+ /\\
+ //\
+ /\\
+ \//
diff --git a/examples/portrait_medium_square_error.png b/src/test/resources/examples/portrait/ascii_expected_medium_square_error.png
similarity index 100%
rename from examples/portrait_medium_square_error.png
rename to src/test/resources/examples/portrait/ascii_expected_medium_square_error.png
diff --git a/examples/portrait_medium_ssim.png b/src/test/resources/examples/portrait/ascii_expected_medium_ssim.png
similarity index 100%
rename from examples/portrait_medium_ssim.png
rename to src/test/resources/examples/portrait/ascii_expected_medium_ssim.png
diff --git a/examples/portrait_small_square_error.png b/src/test/resources/examples/portrait/ascii_expected_small_square_error.png
similarity index 100%
rename from examples/portrait_small_square_error.png
rename to src/test/resources/examples/portrait/ascii_expected_small_square_error.png
diff --git a/src/test/resources/examples/portrait/ascii_expected_small_square_error.txt b/src/test/resources/examples/portrait/ascii_expected_small_square_error.txt
new file mode 100644
index 0000000..f228cd9
--- /dev/null
+++ b/src/test/resources/examples/portrait/ascii_expected_small_square_error.txt
@@ -0,0 +1,146 @@
+ {QQ#Q#pp#p###p#p#pp#p###pp#ppp####p#p#p##$/
+ /Q#Q#p##p#p#p##p####pp#p###p##p##p#p##pppp#pp${\
+ {QQQQ#pp######pp##pp###p#p####p###pp##########p#b$/
+ {QQQ#p###p#pp###pp###p##p######p###ppp#p#####p#p#p##p$/
+ /$QQp#pp###pp#p#p#p###p#ppp#p####ppp#####pp##p#p#####pppQ$
+ {Q#p#ppp###pp###pppp##p#ppp##p#####p###p#pppp#pp####p#p###p$\
+ Q#pppp#####p#p##ppp###p#p#p#p######p#p########p#p#p#p######p#$%/
+ {QQ##p###p#p####pp##p#pp#p#p###p#####pp######p###p#######ppp#p##Q$/
+ {Q#Qppp#pp##pp####p#pp####p#p###p####p#ppp#p#p##p######p##p##pp#ppp$>
+ {Qp#QQQp###p##p##p######p#p#pp#####pp###pppppppp#pp##pp#####p####p##p#$/
+ {QQ#######p####p#p##p#p##ppQ###pp#pp##p##ppppp#####p#######p##p##p#ppp#p$\
+ QQ#p###p##pp####p##p#ppp##Qp#pppp###p##p#pp###p#######p##p#p#p###p#p#p###$\
+ /Qpp###p#####p#####p#pp#pp#%#pQp#pp####p###pp#p###pp#######p####p##p#pp#pp#p$\
+ \Q##p####pp####p#p##ppppppppQ###Q##mmQ#####p#p#pppppppppp##p#########pppp###p$p/
+ Q#p####p#p######pp#p#p#p##p#Q{\{m%%%mmQ#p#####p##ppp##p##p#p##p##p#p#p####p#p##{
+ {###p##p####p#p##ppp###p##pQp%Q%%/>>>>\>###pp##pppp###pp#####p#pp##p###p#p#p##ppQ}
+ {#p##p###ppppppp##ppp#p###pm%m{>m>>/{{>>>/mmmmmm###ppp#pppp#p#ppp##p######p##pppppQ/
+ Qp##########p#p##pppppmmm>>>>>>/`>/>`>>>>> ``>m##pp#p#####pp###p#p########pp#p\
+ QpQQ###p##pp###ppppm>>>>` > > >/>>>>. ``mm##pp######p##p##pp###pp#pp#$
+ \QppQp##p###p#pppmm> > `m##p###p#p###pp#pp##p###pp$/
+ Qppp##pp##p#p#pm>> >>%#####p#####p####p#p#ppp$/
+ p#p#p#####ppppm>> >mp#pp#p#pp#p####p#ppp#p#$
+ {###p######pppm. >>m##p#pp####p#p##p##pppQ)
+ ##pp##p###pppm/ >m##ppp#p##p#ppp#pp#pp#$`
+ [Q#p###pp#ppp%/ >`Q#pp###p###p#p###p#ppp\
+ Qp####pp#pppm> >###p###p##p#p#######p#/
+ jpp##p###p#pp> >#######p###ppp#ppppp#$\
+ QQ#p######ppp>> >#########p##p#p#p#pp#$/
+ {Q#####p#p#pp>> >>#p####p###p###p#pppp$8
+ {Q##pp###pppm>> >>#p##########pppp###pQ/
+ /Q#p####pppppm>. >>{##p###pppp###p#pp#Qp/
+ {pp#####p#ppm>> >>{Q#p######p#p#####pp$$/
+ ;{####p#p##pp{>> ~>%Q##p##p##p##########>
+ %Q#ppppp#ppm>>> >%Q#p#p####ppp#p#pppp${
+ %#p#####p##pp>> >>Q####pp##p##p#####pp$
+ /m###p##pp#p{>> >\%###p###p###p####p##$/
+ >Q####ppp#pp>> >>{Q####p####pp#pp#pp##Q\
+ {##p#p##p#pp> >>>Q#########p#p#p##p#p#>>
+ QQ##ppp###pm> >>{Q#p##p##p##p#pp##p#pQ/
+ %p##p##pp#p> {{{//\\ / >>>>%#p##p##p#pp##p#ppp>`>~
+ Q#p##ppp##p> {{QQQ$$$$QQQ$${{\ \/{{{{Q$$QQ{/ >>>>{####p####p#####pp#p\> >
+ Q#pppp##ppp> {QQ#p##p###p#####QQ$${// \{{$$Q#pp$p#ppp#QQ$${>>>>>{Q#p####p######p#pqpmp '
+ Q#pp##p##pp> \{fmmmmm>mmmmmmpp#p##pp$${ {{QQ#p####p#pppppppmpp#p$/>>>>%####p########p#pb${m/
+ Q#####p##pp> {mm>>>\\/\\\{/\//>>mm#p#pp{> \{Q###ppppmmm{/{/\/{\{>{mmp{/>>>{#####p####p##p##/#%\>
+ #pmp##p#p#$> >>{{{{{{{QQQQ##b##Q${/>>mm{{> {{%pmmmm{j$$BQBQQ$Q${Q{{{{mm>>>>>Q##p###pp###pp##{#)%/
+ pp{##ppp##p>> >>{QQQQQ#ppppQ#ppQ$$fB{/>>>{> >>{>>>>{QppQ#Q$QQQ###$pQ$Q{{>>>>>>Q#p######p##p#pp>b{>.
+ #$mm###p#pp> >>%QQQ#ppQ##p##p/p#p#$pQ//>>\ >>>>>>{pQ####p#p#p##p#pp#pQQ>>>>>>Q###pp#p#p####ppmm >
+ `###pp###pp> >`>%Q#p###ppop###p#p _#Q{/>>>> >>>>>{Q#p>qp#p#pp#m##pp#p#pm>>>>>>%##p#p#######ppp>.
+ \qbmp###p#>> >>mQ###pp> %pp#QQ> >>%$ >> >> {#pm `Qm#pQQm {###pppm>> >>>Q##p#p###pp#pppp\
+ >Q/##pp##p>> `>mpp#$$/ _mmm /{>>>m/ >> /#%Q{{\/`mm%pw\{Q#p#pm>> >>>>Q###p##ppppmomm>/
+ ~m$###pp/> >>m#pp#QpQQ$QQQ{\{\/>{>> >>>/>\>QQQQ$QpQpppp%>>> >>{Q#p#ppppppm>
+ .\`%#ppp$> >>>mmpmpmm>>>>>>. `>> ```>>>>>mm%mm#m>>> >>{Q#p#ppppp$/>;
+ >>#ppp8>> >>>>>>> >>m>>>>>> >>{Qpp#pp#Q$pm{\
+ #Qpp>> >>{Qp#pp#ppQp) `
+ \oQp$\ >>>{Q#p#pm%Qm$\
+ >oQp{ >>>{ppmpmmQp$#>
+ >>>Q{> >>>QQQQm{Q#pQ#8
+ >>\%Q/>> >>>{QQmm>QpppQp$
+ >{{/{{>>> >>>Q%m>>\#pppm#$
+ >>{{{%>>> >>>{m>>{Qp###Qp$>
+ >>>{{Q$/>> >>>{>>Qpppp#Q$#$>
+ >>{%QQ$>>> >>>>>{Q###pp#p##Q$>
+ >>>{{%QQ/>>> >>>>{##p##ppppp##$
+ >>{{QQQ}>>> > >>>>>{p#p##p#ppQ#$p
+ >>>{{%Q$>>>> >>>>>Q##pp##pppmp8b
+ >>>{Q@mp>>>> //\ \//\// >>>>>>#pppp#ppp#{#>>
+ >>>{{%Q\>>>> %Q${/>/ {{$Q#p> >>>>>>>>####pppp#pmmQ>
+ >>>>{{%$>>/>>> `m###${{\{{$pppm> >>>>>>> {#p#p#ppppp#{{
+ >>>>>{{8>>>>/>>> >m##pQQQppmm> > >>>>>/>>>Q####p##p#Qm>}
+ >>>>Q%>>>>>>>> `>m#pppm>> >>>>>>>>> ######pppmm/ >
+ >>{{{>>>>>>>> > >>>>> > >>>>>>>> pp##p#pp%m/>\
+ >>>{{\>>>>>>>> >>>>>>> >[###p#ppQQm> (
+ >>>{{ >>>>>>> /> >>>>>> >#pp##pQQp>>> >
+ >>>{\>>>>>>> >\{{/\\\//\\//\/\/////{{{{{{{{Q@m>> >>>>>>>>>\ppp#pppm>>> /
+ >>>{>>>>>>>> `m%Q$QQ{{{{{{{{{{{{QQpmppm. >>>>> >>#p#pppm{>>>>{>
+ >>{\>>>>>>>> `>~mmmmmmm%mmmm>>>>>>> >>>>> > >{pppppQ%{{>>{>
+ >>>m\>>>>>>>> >>>>>>>> >>>>/> >>>>>>>\Qpp#p$$${{$@>
+ >>>m\>>>>>>/>>> >>\>>>> >>>>>>/>> >>>>>>>\Qpppppp#pm>>
+ >>>{\>>>>>>>>> >>>{{/>>>> >>>\{>>>> >>>>>>>>QppQpp%{{>>>>
+ >>>{\>>>>>>> >>>>QQQQ{Q@mm>>>> >>>>>/{QQpppQm{{>>>
+ >>>{{{>>>>>>> >>>>>>>>>>>> >>>>>{{{QpQ%%{{>>>>
+ >>>{{>{>>>>> >>>/{QQ{{QQQQ{{>>>>
+ >>>>>{{{>>>> >>{{Q#Q%{>{Q%m>>>>>
+ >>>>>{{Q${/\>> >>{{{QpQQ%{>>m{{>>>>
+ >>>>{{QQQ$/\> >/{{Qpppp%{>/>{>>>>>
+ >>>>{{QQQpQ$>/ \{{QpppQmm{>>>>>>>>>
+ >>>>{{%QQp#Q{\ \{Qpppp%m%{>>>>>>>>
+ >>>>{{mm%Q###$/> \{$pppppmm>>>>>>>>>
+ >>>>>>{%QQ#p#p$/>/ /\{{#ppppmm>>>>>>>>>> >>
+ >>>>>>>>%%#####Q${/\>//>>>//\/\{{QQpppppm>>>>>>>>>>>
+ >>>>>>>>%Q##p#pp$${{{{{{{{{{QQ#pppppmm>>>>>>>> >>
+ >>>>>>>>%Qpp#p###p#ppppp##p#ppppppm>>>>>
+ >>>>>{mQp###pp#p#pp##p#p#pppmm>>>>
+ >>>>>m%%#p#p#p#p#ppppppppm>>>>>
+ >>>{%%%Q##pp#pp#pppppmm>>>>>
+ >>>>%%QQppppppppmmm>>>>>
+ >>>>>%%mm%%%%mm>>>>>>
+ >>>>>>>>>>>>>>>>>>
+ >>>>>>>>>>>>
+ `#$$
+ / ~###/
+ {Qm Qppp/
+ $#pm >#p##>
+ \Qppm ##p#$
+{##pm o###$>
+##pm >#p#p/
+ppp Q##p$
+##m >p##$
+#p #p#$
+## #p## >>>>
+#8 #pp$ >>
+#m >Qp#$ > >>
+p> Q#pp > >
+# >ppm >
+$ >pp>
+p #p>
+> ##>
+ Q#$
+ ##$
+ #p#
+ ###
+ >p#p/
+ >p##/
+ >###>
+ Q###b
+ Q###p
+ Q###$
+ [##pp >>
+ [p##$
+ [p##p >
+ [p##p >
+ ####p >
+ #p##p > >
+ ####> >>>
+ ####> >>
+ #p#p> >>
+ #p#p> >>>>>
+ p##p > >>>>
+ p##p >>>>>>>>
+ pp## > >>>>>>
+ #### > >>>>>>>>>>\
+ Q###$ >>>>>>>>>>>>>>
+ [#p#$ > >>>>>>>>>>>>>>>
+ [pp#p >>>>>>>>>>>>>>>>>>
+ #p#pp >>>>>>>>>/>>>>>>>>
+ ##ppk >>>>>>{{{>>>>>>>>>
diff --git a/examples/portrait_small_ssim.png b/src/test/resources/examples/portrait/ascii_expected_small_ssim.png
similarity index 100%
rename from examples/portrait_small_ssim.png
rename to src/test/resources/examples/portrait/ascii_expected_small_ssim.png
diff --git a/src/test/resources/examples/portrait/ascii_expected_small_ssim.txt b/src/test/resources/examples/portrait/ascii_expected_small_ssim.txt
new file mode 100644
index 0000000..e8525aa
--- /dev/null
+++ b/src/test/resources/examples/portrait/ascii_expected_small_ssim.txt
@@ -0,0 +1,146 @@
+ {{{@{@@@@@@@@@@@@@@@@o00@0000@000#p00000@$\
+ /{{@@@@@@@@0p@o@@@@@@@@@000000000p000000p000@@/\
+ {{@@@@@@@o0Q@@@@@Q@@@Q00p00#0o@@000000000@00000Q@{/
+ {{{@@@@@@@Q@@@@@@@@Q@@o0@Q0@o00o000@p0Q0@00p#0000@0@@@
+ \{@@@@@@0o@@p@o0@@@@@o@@@@@@Qo0Qp00000000000#o00000@00p@@{
+ {@@@o@@@@@@@o@000@@@@o@@@@@@@@@00p0p0000#00000p00000@0p0@o@{\
+ m@@@@@@oQ00p0000000Q@Q@@m@@Q@@0p00$p00000000p0p000000oo00Q0p@@{/
+ /{@@@@@@@@@0p0p000#0@@@@Q@@@Q@000o00#000000##pp00p##000p0@@@@pp0@{/
+ {{{@@@@@Q@0#po0#00p#o@@@@Q@o@@QQ0o@00B#Up@@@0000p000l##0p0@p0@@0p@@\>
+ /{@@@@@@@o@@@@p000000oQQ@@@@@@@Q@0qp000#Up@b0p@@@b@000p00p00p8p@o@@@@@{/
+ \{{@@@@@oo@0@po@op000o@@@{@Q@@@@@@000p@Q@0@@o@@@Qo@00p0000000U0p@@@@@@Q@{\
+ @@QQQ@@o00@0##o0Qo000@@@@Q%b@b@@b@@@@0@@@@@@@@o@0@Q000080000#00000o@o@@@@{\
+ /{@@@@@poo000@00B##p0pQo@@@{@@@@Q@@Q{@@Q@oo@@@@@@@@@@000000p000000000p@0@@@@{\
+ {@0QQp@@@@p000@000b@0@@@@Qm{@{@{o@>{{@Qo00@o@@@@@@@@@@o00Q@00#p0000000p@o@@@@{/
+ {@@@@O@pp0p000##0p@@Q@Q@@@m@{{\/>{{{/>{@@o@0o0@o@@@@@@Q@Q@@@@oQ@000pB00000@@@@@{
+ {@@Q@@@@@@@@00000o@@o@@@@@@{{{{>`>>>\>{@@@@@@o@@@@@@@@@@@@000p@00o0p000p0p0@@@@\
+ \@@Q@@@@@@o@@@@@@@@@Q@Q@@o@{{>{>>>>/{>>>>`>>>{mm@@@@@@Q@@@@@poo0p0o@@@00000000@@@@{
+ {@@@@@@o@000#@@@@@@@@@m>>>>>>>>/`>/>`>` > ``>o@@@o0@@Qo0p00pp0p@po000000@@@@\
+ Q@@@@oo00000o@0@o@@>>>> >> ` ``mQo0Q@@@@0pp0pp00008p0p@00@@{
+ \@@@@@o#p0000@@@{>>> >m@@@0pp0pp0p000000p@@@0@@@/
+ {@@@Q@p@0800@@m{>> >{o@@p0000p000000000@Q@@@@/
+ @@@0p0000#p@@{>> `>{@@0ppp0p00000p00p0@@@@{
+ {@@o@@00000@@{> `{@@00B0p#p#0p0#p000p@@{>
+ Q@0@@00000@@{>/ >{@0@ph0pp#pppppp000p@@{`
+ {@o@@000b@@@{/ `{@0pp$#00$pp000000Q@@@\
+ @@@@o00oop@@{> >{@#p0udB00p0p00p00@@@@/
+ {@@@@@@00p@@{> >@00000####p0b0p000@@@@\
+ {@@@@@o000@@{> >@q00pp0p#h#00p0@0p@@@{>
+ {{{@o0o00po@@>> >@b0p##0#0$0p0#o0p@@@{{
+ {Q@o@@@00p@@{> >>@p0Bp0000f#00pp@0@@@{/
+ /{@@@@000@@@{>> >{@001##0#0M00000p@@{{/
+ {@@Q0000po@@{> >>{@0#fz0iB0$B#000@o@@}
+ {Q@@o@oQ@@@{\> >{{00pz#Bf00###pp0oo@@>
+ {@@@0pp@o@@{>> >{{@@#0p#Buip00pq$00Q@\
+ {{@@@@@0@@@@{> >{@0q0qB#BBB#b000ppp@{
+ m@@@@@o@@@{>> \{@@0b0ph0IIIp#pp0p0@@
+ {@@@@@@@@@{> >>{o00d0$uBIMMu##00pQ@@\
+ \@@oo@o@p@@{> >>{@0@qI00BBBBpUB0#0Q@{>>
+ {@@@0@@o@0@> >>{@@00BduB00b#p0#0@@o{/
+ {@@00@o@@0@> \{\//\ / >>{@p00p0#piBBI###000> ~
+ {@@@@@@po0@> /{{{@@@{{{@@{{{\\ /{{{{{{{{{{/ >{@Q0#Bf$##BM#B#p0p&@\>
+ {@@0Q@0opQ@> {{{@@Q@@@@o@@@oQ@@@{{/ \{{{Q@@@@@@@@@@@@{{\ >>{@00000BB0##p0p#p@mr{
+ {@@@@o@00@@ {{{{>>>`>>`>moo@@@@@@@{{{ \{{@@@p0O0@p@@p@o@om{@@@\> >>{@00#B0qM0#B0#p0pQ{{`/
+ {@@@o@@o0@{ {{>>> >/\\\\/\/ >>>mo@@@@\ >{{@@@@@@om>`\/>/\//\>>>{m{/ >>{@@00Z0##qpi##B#p/@%\>
+ @{m@@@@00@@ >>>\{{{{{{{@@o{Wa@@}{/>>>{{>> <{{{mm>>{{{@Q%@Q@@{{{{{{{{/>> >>>{Q0pM##bqf#0B0#0{a>{
+ {{>@@@@@@@@ >{{{{{@Q@@@@@@@pp@{{@\/>>>{> >>>>>>{L@{@@Q@@@@@@Q@{{{{{/>>>>>>{@0000#0lBIB00@@>{{ .
+ {{/<@@@@@@@> >{{{@@@@@@0@o0p.b@@@@{{//>>\ >>>>>{@@@@00n[p0pQp@@@@@@{{>> >>>{@0o000p0p000p0@{>
+ `@a@@@@@@@@> >{{@@@@0@m~@o0pp{@ `@@{> >> >> >{{@m`{@@p#b@@m@0p@@@@{>> >>>{Q0ppo0p00#000@@>
+ \o@o@@@@@@> >{Q@@@@{> m{{@{{> >o{ > {Qo> @{@@{@> >@Qo@@m>> >>>{@@00000#0@#0@@@\
+ >{/o@@@@@@> >{@@@@{/ `m@> /<>>>{/ {{<{/\ `mm{@.\{Q@@@m>> >>{@@00Q@@@@o><_>`
+ `>%@@@@@/ >{{@@@@@J{{{{<{\{\><>> >>>/>\<{{{{{{Q@@@{{>>> >/{@@@@@@@@@>.
+ `{@@@@{ >>>{{{{@{>>>>>> > `>>>`>{{{{{>>>> >>>{@@@@@@@@8/ >
+ >>{@@@{> >>>>>>> `>>>>>> >\{@@@@@@@{@{>\
+ {@@{> >>{{{@@@Q@{{{>
+ ~{@{\ >{@@@@@{{Q{}\
+ `{{/ >{{{{{{{@{{{\
+ >>{{ >{{{{{{{@{W@{
+ >>{{/ >\{{{>>{@@@{@{
+ >>>/{\> >{{{>>\0@@{{@{
+ >>{\> >>{>>>\Lp@@@{{{
+ >>>{{{/ >>>>$#00@@{@{{
+ >>>{{{{ >>>\{pM#pp@@{@{$
+ >>>{{{{/> >>>>@00000p@@@@@)
+ >>{{{{\ > >{ppp0#po@@@@{>
+ >>>{{{{ > >>>>{#0p000@@@{{>m
+ >>>{{{{>>> / / \// >>>> op0p00p@@@{@>>
+ >>>{{{/ >> {@@{/> /{{@@@> >>>> @000bpp@@@{>
+ >>>>>{{{ >>> >@@@{/\>\{Q@@{> >>>>> \qpq0q0@@@{%/{
+ >>>{{{ >>>> `{@@@@@@@@{>> >>>/> {0#00p0Q@@{>>{
+ >>>{{ >>>> `>>m@@@{> >>>>> &000000@@{>/ >
+ >>>{> >>>> >>> >>>> bB00pp@@{>/ \
+ >><>\ >>> >>>>> {]#00#0{{{> /
+ >>>{ >>> / > &p000@@{{>>
+ >>>\ >> >\{/ \//\\/>\/ /////\\/\\/{{>>> \p@@@@{{{>>> /
+ >>{ >> `"@{{{{{{{{{{{{{{{{{{@@{om. > J@Q@@{{{>>>>{
+ >>/> >>> ```>>>>>>>>>>>>`>>` {p@@@{{{\>> {.
+ >>/ >>>>>> > \@@@Q@@{{{\{>
+ >>>\>>>>>> >> > > >> \@@@{{{{@@>>
+ >>{\>>>>>> >>\/>>> > >\\>>> > >>>{@@{{{{{>>>
+ >>>\>>>>> >>>{{{{{{{{>>>> >>\{{{{{{{{>>>
+ >>>>>>>> > `>>>>>> > >>{{/{{{{{{>>>
+ >>>>>>>>>> >>/\{{{\{{{{{>>>>
+ >>>>{{/>>> >>>{{{{{>{{{{>>>
+ >>>>>{{{//> >/{@{{{{\>>{>>>>>
+ >>>>{{{{{/> >/{@@{{{{{>>>>>>>
+ >>{{{{{@@{>> /{@@@{{{{>>>>>>>>
+ >>>>{{{{@@@{\ \{@@@@{{{>>>>>>>
+ >>>>>{{{@@@@/> \{@@@@{{>>>>>>>>>
+ >>>>>{{{@@@@{/> \>{@@@@{{>>>>>>>
+ >>>>>>{{{@@@@@{//\ >/ /\/{{@@@@@{{>>>>>>
+ >>>>>>{{{@@@@@@{{{{\/{{{{{{@@@@@@{{>>>>>
+ >>>{{{@@@@@@@@@@@@@@@@@@@@@@{{>>>>
+ >>>{{{@@@@@@@@@@@@@@@@@@{{>>>>
+ `>>{{{@@@@@@@@@@@@@@@{{{>>>
+ >>>{{{{{@@@@@@@@@{{{{>>>>
+ >>>{{{{{{{{{{{{{{{>>>>
+ >>>>>{{{{{{{{>>>>>>
+ >>>>>>>>>>>>>>>
+ >>>>>>>>
+ `m$\
+ / `Q0@
+ {@m {bpQ\
+ {@@> `Q00r
+ @@@. o@@@\
+{@@@> `@Q0@
+@@@m #@@@/
+@@@ ~@@@\
+@@> `b@@]
+@@ ]@o0
+@q dpo@
+@? [o@]
+@> {p@]
+@ `0@!
+d `b@W
+l pp>
+m op/
+> d0\
+ %#b
+ [0]
+ [0q
+ $0d
+ p0p
+ p00/
+ @0p\
+ \000;
+ {000k
+ {000b
+ {00bM
+ {000b
+ [b0#p
+ [p00b
+ qp00:
+ d000!
+ ]00p/
+ $00#>
+ #p00>
+ 8000
+ p00@ >
+ b00b >
+ p00d >>
+ q00q >
+ <#00] > >>>>
+ {00p] >> >>>>>>>
+ [000n >>>>>>>>>>>>>
+ ppm >>>>>>>>>>>>>>>
+ #0#0k >>>>>\>>>>>>>>>>
diff --git a/examples/portrait.png b/src/test/resources/examples/portrait/orig.png
similarity index 100%
rename from examples/portrait.png
rename to src/test/resources/examples/portrait/orig.png