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 +![Original picture](http://korhner.github.io//assets/img/asciimg/orig.png "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: +![16 pts font, MSE](http://korhner.github.io/assets/img/asciimg/large_square_error.png "16 pts font, MSE") + +- - - + +![16 pts font, SSIM](http://korhner.github.io/assets/img/asciimg/large_ssim.png "16 pts font, SSIM") + +- - - + +![10 pts font with 3 characters, MSE](http://korhner.github.io/assets/img/asciimg/medium_square_error.png "10 pts font with 3 characters, MSE") + +- - - + +![10 pts font with 3 characters, SSIM](http://korhner.github.io/assets/img/asciimg/medium_ssim.png "10 pts font with 3 characters, SSIM") + +- - - + +![6 pts font, MSE](http://korhner.github.io/assets/img/asciimg/small_square_error.png "6 pts font, MSE") + +- - - + +![6 pts font, SSIM](http://korhner.github.io/assets/img/asciimg/small_ssim.png "6 pts font, SSIM") + +## Architecture ![Architecture](http://korhner.github.io/assets/img/asciimg/asciimg_cls_diagram.png) ## 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@@%> + > %%/ /@` {#@@% + > ~%@` @@@@< (@@< + \ `~~~` @@>/ + 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