From b771aed5070864bbcbae286c8de74478f6837618 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Thu, 6 Jul 2017 22:03:12 +0200 Subject: [PATCH] Version 2.0.0 (small API change) --- VERSION | 2 +- changelog | 199 -------- changelog.md | 166 +++++++ configure.sh | 4 +- export.sh | 16 +- src/be/nikiroo/utils/IOUtils.java | 265 ----------- src/be/nikiroo/utils/ImageText.java | 475 +++++++++++++++++++ src/be/nikiroo/utils/ImageUtils.java | 400 ++++++++++++++++ src/be/nikiroo/utils/StringUtils.java | 107 +---- src/be/nikiroo/utils/resources/Bundle.java | 4 +- src/be/nikiroo/utils/serial/SerialUtils.java | 6 +- 11 files changed, 1060 insertions(+), 584 deletions(-) delete mode 100644 changelog create mode 100644 changelog.md create mode 100644 src/be/nikiroo/utils/ImageText.java create mode 100644 src/be/nikiroo/utils/ImageUtils.java diff --git a/VERSION b/VERSION index 266146b..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.3 +2.0.0 diff --git a/changelog b/changelog deleted file mode 100644 index 2069eac..0000000 --- a/changelog +++ /dev/null @@ -1,199 +0,0 @@ -Version 1.6.3 -------------- - -Version.java - Fix toString issues + test + update scripts - -Version 1.6.2 -------------- - -Version.java - Now supports "tag" on the versions (i.e., 0.0.4-niki1) - -> tag is "niki", tagVersion is 1 - -Version 1.6.1 -------------- - -Serialisation utilities - Now supports enums and BufferedImages - -Version 1.6.0 -------------- - -Serialisation utilities - Server class to send/receive objects via network easily - Serialiser now supports Arrays + fixes - -Version 1.5.1 -------------- - -Serialisation utilities - SerialUtils is now public and can be used to dynamically create an - Object - The Importer is now easier to use - -Version 1.5.0 -------------- - -Bundles: change in Bundles and meta data - The meta data is more complete now, but it breaks compatibility with - both Bundles and @Meta - A description can now be added to a bundle item in the graphical - editor as a tooltip - -Serialisation utilities - A new set of utilities to quickly serialise objects - -Version 1.4.3 -------------- - -Bugfix: unhtml - Also replace non-breakable spaces by normal spaces - -Version 1.4.2 -------------- - -Bugfix: Deltree - Deltree was not OK for files... - -Version 1.4.1 -------------- - -Progress - Better handling of min==max case - New methods .done() and .add(int step) - -Version 1.4.0 -------------- - -R/W Bundles - Bundle is now Read/Write - -Bundle Configuration - New UI controls to configure the Bundles graphically - -Version 1.3.6 -------------- - -Fix for Java 1.6 compat - Java 1.6 cannot compile it due to variables with ambigous names (which - Java 1.8 can identify) - -Version 1.3.5 -------------- - -Improve ProgressBar UI - It now shows all the progression bars of the different steps of - progression at the same time - -Version 1.3.4 -------------- - -Improve TestCase error reporting - We know display the full stack trace even for AssertionErrors - -Extends Version - ...with new methods: isOlderThan(Version) and isNewerThan(Version) - -Version 1.3.3 -------------- - -New Version class - Which can parse versions from the running program - -Version 1.2.3 -------------- - -Add openResource and getVersion in IOUtils - The file VERSION is supposed to exist - -Give more informartion on AssertErrors - The TestCase were not always helpful in case of AssertExceptions; they - now print the stacktrace (they only used to do it for non-assert - exceptions) - -Fix configure.sh - The VERSION file was not added, the Main method was not the correct - one (so it was not producing working runnable JAR, yet it stated so) - -Version 1.2.2 -------------- - -Fix bug in Bundle regarding \t handling - ...tests should be written (later) - -Version 1.2.1 -------------- - -New drawEllipse3D method - ...in UIUtils - -Version 1.1.1 -------------- - -Add UI component for Progress - Still a WIP, it only show the current progress bar, still not the - children bars (it's planned) - -Version 1.1.0 -------------- - -Add progress reporting, move to ui package - A new progress reporting system (and tests) in the new ui package - (some other classes have been moved into ui, too: WrapLayout and - UIUtils) - -Version 1.0.0 -------------- - -Add WrapLayout and UIUtils - A FlowLayout that automatically wrap to the next line (from existing - code found on internet) and a method to set a fake-native look & feel - -Version 0.9.7 -------------- - -Improve toImage and allow non-resetable InputStreams - ...though they are then automatically saved onto disk then re-opened, - then the file is deleted at the end of the process -- bad perfs - Worse, it does it even if no EXIF metadata are present (because it - cannot know that before reading the Stream, and cannot save a - partially, non-resetable Stream to disk) - -Reoarganize some methods from String to IO - -Version 0.9.6 -------------- - -New test system - Now some unit tests have been added, as well as the support classes - -Version 0.9.5 -------------- - -Resource bundle bug - UTF-8 strings were sometimes wrangled - It is fixed by using a Bundle#Control, whih sadly is only available in - Java 1.6+ - -Version 0.9.4 -------------- - -Compatibility bug - Again... because of some useless imports made there for a wrong jDoc - comment - -Version 0.9.3 -------------- - -Compatibility bug - The library did not work with JDK versions prior to 1.8 because - of a dependency on Base64 - A new (public domain) class was used instead, which is compatible with - Java 1.5 this time - -Version 0.9.2 -------------- - -Initial version - ...on GIT diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..68ef60c --- /dev/null +++ b/changelog.md @@ -0,0 +1,166 @@ +# nikiroo-utils + +## Version 2.0.0 + +- API change + - IOUtils is now split between itself and ImageUtils -- some changes required in dependant projects + - Some slight renaming in StringUtils/IOUtils/ImageUtils + +- New class ImageText + - To create ASCII art + +## Version 1.6.3 + +- Version.java + - Fix toString issues + test + update scripts + +## Version 1.6.2 + +- Version.java + - Now supports "tag" on the versions (i.e., 0.0.4-niki1 -> tag is "niki", tagVersion is 1) + +## Version 1.6.1 + +- Serialisation utilities + - Now supports enums and BufferedImages + +## Version 1.6.0 + +- Serialisation utilities + - Server class to send/receive objects via network easily + - Serialiser now supports Arrays + fixes + +## Version 1.5.1 + +- Serialisation utilities + - SerialUtils is now public and can be used to dynamically create an Object + - The Importer is now easier to use + +## Version 1.5.0 + +- Bundles: change in Bundles and meta data + - The meta data is more complete now, but it breaks compatibility with both Bundles and @Meta + - A description can now be added to a bundle item in the graphical editor as a tooltip + +- Serialisation utilities + - A new set of utilities to quickly serialise objects + +## Version 1.4.3 + +- Bugfix: unhtml + - Also replace non-breakable spaces by normal spaces + +## Version 1.4.2 + +- Bugfix: Deltree + - Deltree was not OK for files... + +## Version 1.4.1 + +- Progress + - Better handling of min==max case + - New methods .done() and .add(int step) + +## Version 1.4.0 + +- R/W Bundles + - Bundle is now Read/Write + +- Bundle Configuration + - New UI controls to configure the Bundles graphically + +## Version 1.3.6 + +- Fix for Java 1.6 compat + - Java 1.6 cannot compile it due to variables with ambigous names (which + - Java 1.8 can identify) + +## Version 1.3.5 + +- Improve ProgressBar UI + - It now shows all the progression bars of the different steps of progression at the same time + +## Version 1.3.4 + +- Improve TestCase error reporting + - We know display the full stack trace even for AssertionErrors + +- Extends Version + - ...with new methods: isOlderThan(Version) and isNewerThan(Version) + +## Version 1.3.3 + +- New Version class + - Which can parse versions from the running program + +## Version 1.2.3 + +- Add openResource and getVersion in IOUtils + - The file VERSION is supposed to exist + +- Give more informartion on AssertErrors + - The TestCase were not always helpful in case of AssertExceptions; they now print the stacktrace (they only used to do it for non-assert exceptions) + +- Fix configure.sh + - The VERSION file was not added, the Main method was not the correct one (so it was not producing working runnable JAR, yet it stated so) + +## Version 1.2.2 + +- Fix bug in Bundle regarding \t handling + - ...tests should be written (later) + +## Version 1.2.1 + +- New drawEllipse3D method + - ...in UIUtils + +## Version 1.1.1 + +- Add UI component for Progress + - Still a WIP, it only show the current progress bar, still not the children bars (it's planned) + +## Version 1.1.0 + +- Add progress reporting, move to ui package + - A new progress reporting system (and tests) in the new ui package (some other classes have been moved into ui, too: WrapLayout and UIUtils) + +## Version 1.0.0 + +- Add WrapLayout and UIUtils + - A FlowLayout that automatically wrap to the next line (from existing code found on internet) and a method to set a fake-native look & feel + +## Version 0.9.7 + +- Improve toImage and allow non-resetable InputStreams + - ...though they are then automatically saved onto disk then re-opened, then the file is deleted at the end of the process -- bad perfs + - Worse, it does it even if no EXIF metadata are present (because it cannot know that before reading the Stream, and cannot save a partially, non-resetable Stream to disk) + +- Reoarganize some methods from String to IO + +## Version 0.9.6 + +- New test system + - Now some unit tests have been added, as well as the support classes + +## Version 0.9.5 + +- Resource bundle bug + - UTF-8 strings were sometimes wrangled + - It is fixed by using a Bundle#Control, whih sadly is only available in Java 1.6+ + +## Version 0.9.4 + +- Compatibility bug + - Again... because of some useless imports made there for a wrong jDoc comment + +## Version 0.9.3 + +- Compatibility bug + - The library did not work with JDK versions prior to 1.8 because of a dependency on Base64 + - A new (public domain) class was used instead, which is compatible with Java 1.5 this time + +## Version 0.9.2 + +- Initial version + - ...on GIT + diff --git a/configure.sh b/configure.sh index b88e39a..a27a91e 100755 --- a/configure.sh +++ b/configure.sh @@ -50,8 +50,8 @@ echo "TEST = be/nikiroo/utils/test/Test" >> Makefile echo "TEST_PARAMS = $cols $ok $ko" >> Makefile echo "NAME = nikiroo-utils" >> Makefile echo "PREFIX = $PREFIX" >> Makefile -echo "JAR_FLAGS += -C bin/ be -C bin/ VERSION" >> Makefile -echo "SJAR_FLAGS += -C src/ be" >> Makefile +echo "JAR_FLAGS += -C bin/ be -C bin/ org -C bin/ VERSION" >> Makefile +echo "SJAR_FLAGS += -C src/ org -C src/ be" >> Makefile cat Makefile.base >> Makefile diff --git a/export.sh b/export.sh index b0fdddf..0cff17b 100755 --- a/export.sh +++ b/export.sh @@ -3,6 +3,7 @@ # Export script # # Version: +# - 1.1.0: allow multiple targets # - 1.0.0: add a version comment cd "`dirname "$0"`" @@ -10,16 +11,19 @@ cd "`dirname "$0"`" if [ "$1" = "" ]; then echo "You need to specify where to export it" >&2 exit 1 -elif [ ! -d "$1/libs" ]; then - echo "The target export directory is not compatible" >&2 - exit 2 fi LIBNAME="`cat configure.sh | grep '^echo "NAME = ' | cut -d'"' -f2 | cut -d= -f2`" LIBNAME="`echo $LIBNAME`" make mrpropre -./configure.sh && make \ - && cp "$LIBNAME"-`cat VERSION`-sources.jar "$1"/libs/ \ - && cp "$LIBNAME".jar "$1"/libs/ +./configure.sh && make +if [ $? = 0 ]; then + while [ "$1" != "" ]; do + mkdir -p "$1"/libs/ + cp "$LIBNAME"-`cat VERSION`-sources.jar "$1"/libs/ + cp "$LIBNAME".jar "$1"/libs/ + shift + done +fi diff --git a/src/be/nikiroo/utils/IOUtils.java b/src/be/nikiroo/utils/IOUtils.java index b056643..83dcd50 100644 --- a/src/be/nikiroo/utils/IOUtils.java +++ b/src/be/nikiroo/utils/IOUtils.java @@ -1,9 +1,5 @@ package be.nikiroo.utils; -import java.awt.Image; -import java.awt.geom.AffineTransform; -import java.awt.image.AffineTransformOp; -import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -16,8 +12,6 @@ import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import javax.imageio.ImageIO; - /** * This class offer some utilities based around Streams. * @@ -214,116 +208,6 @@ public class IOUtils { } } - /** - * Convert the given {@link InputStream} (which should allow calls to - * {@link InputStream#reset() for better perfs}) into an {@link Image} - * object, respecting the EXIF transformations if any. - * - * @param in - * the 'resetable' {@link InputStream} - * - * @return the {@link Image} object - * - * @throws IOException - * in case of IO error - */ - public static BufferedImage toImage(InputStream in) throws IOException { - MarkableFileInputStream tmpIn = null; - File tmp = null; - try { - in.reset(); - } catch (IOException e) { - tmp = File.createTempFile("fanfic-tmp-image", ".tmp"); - tmp.deleteOnExit(); - IOUtils.write(in, tmp); - tmpIn = new MarkableFileInputStream(new FileInputStream(tmp)); - } - - int orientation; - try { - orientation = getExifTransorm(in); - } catch (Exception e) { - // no EXIF transform, ok - orientation = -1; - } - - in.reset(); - BufferedImage image = ImageIO.read(in); - - if (image == null) { - if (tmp != null) { - tmp.delete(); - tmpIn.close(); - } - throw new IOException("Failed to convert input to image"); - } - - // Note: this code has been found on internet; - // thank you anonymous coder. - int width = image.getWidth(); - int height = image.getHeight(); - AffineTransform affineTransform = new AffineTransform(); - - switch (orientation) { - case 1: - affineTransform = null; - break; - case 2: // Flip X - affineTransform.scale(-1.0, 1.0); - affineTransform.translate(-width, 0); - break; - case 3: // PI rotation - affineTransform.translate(width, height); - affineTransform.rotate(Math.PI); - break; - case 4: // Flip Y - affineTransform.scale(1.0, -1.0); - affineTransform.translate(0, -height); - break; - case 5: // - PI/2 and Flip X - affineTransform.rotate(-Math.PI / 2); - affineTransform.scale(-1.0, 1.0); - break; - case 6: // -PI/2 and -width - affineTransform.translate(height, 0); - affineTransform.rotate(Math.PI / 2); - break; - case 7: // PI/2 and Flip - affineTransform.scale(-1.0, 1.0); - affineTransform.translate(-height, 0); - affineTransform.translate(0, width); - affineTransform.rotate(3 * Math.PI / 2); - break; - case 8: // PI / 2 - affineTransform.translate(0, width); - affineTransform.rotate(3 * Math.PI / 2); - break; - default: - affineTransform = null; - break; - } - - if (affineTransform != null) { - AffineTransformOp affineTransformOp = new AffineTransformOp( - affineTransform, AffineTransformOp.TYPE_BILINEAR); - - BufferedImage transformedImage = new BufferedImage(width, height, - image.getType()); - transformedImage = affineTransformOp - .filter(image, transformedImage); - - image = transformedImage; - } - // - - if (tmp != null) { - tmp.delete(); - tmpIn.close(); - } - - return image; - } - /** * Open the given /-separated resource (from the binary root). * @@ -340,153 +224,4 @@ public class IOUtils { return loader.getResourceAsStream(name); } - - /** - * Return the EXIF transformation flag of this image if any. - * - *

- * Note: this code has been found on internet; thank you anonymous coder. - *

- * - * @param in - * the data {@link InputStream} - * - * @return the transformation flag if any - * - * @throws IOException - * in case of IO error - */ - private static int getExifTransorm(InputStream in) throws IOException { - int[] exif_data = new int[100]; - int set_flag = 0; - int is_motorola = 0; - - /* Read File head, check for JPEG SOI + Exif APP1 */ - for (int i = 0; i < 4; i++) - exif_data[i] = in.read(); - - if (exif_data[0] != 0xFF || exif_data[1] != 0xD8 - || exif_data[2] != 0xFF || exif_data[3] != 0xE1) - return -2; - - /* Get the marker parameter length count */ - int length = (in.read() << 8 | in.read()); - - /* Length includes itself, so must be at least 2 */ - /* Following Exif data length must be at least 6 */ - if (length < 8) - return -1; - length -= 8; - /* Read Exif head, check for "Exif" */ - for (int i = 0; i < 6; i++) - exif_data[i] = in.read(); - - if (exif_data[0] != 0x45 || exif_data[1] != 0x78 - || exif_data[2] != 0x69 || exif_data[3] != 0x66 - || exif_data[4] != 0 || exif_data[5] != 0) - return -1; - - /* Read Exif body */ - length = length > exif_data.length ? exif_data.length : length; - for (int i = 0; i < length; i++) - exif_data[i] = in.read(); - - if (length < 12) - return -1; /* Length of an IFD entry */ - - /* Discover byte order */ - if (exif_data[0] == 0x49 && exif_data[1] == 0x49) - is_motorola = 0; - else if (exif_data[0] == 0x4D && exif_data[1] == 0x4D) - is_motorola = 1; - else - return -1; - - /* Check Tag Mark */ - if (is_motorola == 1) { - if (exif_data[2] != 0) - return -1; - if (exif_data[3] != 0x2A) - return -1; - } else { - if (exif_data[3] != 0) - return -1; - if (exif_data[2] != 0x2A) - return -1; - } - - /* Get first IFD offset (offset to IFD0) */ - int offset; - if (is_motorola == 1) { - if (exif_data[4] != 0) - return -1; - if (exif_data[5] != 0) - return -1; - offset = exif_data[6]; - offset <<= 8; - offset += exif_data[7]; - } else { - if (exif_data[7] != 0) - return -1; - if (exif_data[6] != 0) - return -1; - offset = exif_data[5]; - offset <<= 8; - offset += exif_data[4]; - } - if (offset > length - 2) - return -1; /* check end of data segment */ - - /* Get the number of directory entries contained in this IFD */ - int number_of_tags; - if (is_motorola == 1) { - number_of_tags = exif_data[offset]; - number_of_tags <<= 8; - number_of_tags += exif_data[offset + 1]; - } else { - number_of_tags = exif_data[offset + 1]; - number_of_tags <<= 8; - number_of_tags += exif_data[offset]; - } - if (number_of_tags == 0) - return -1; - offset += 2; - - /* Search for Orientation Tag in IFD0 */ - for (;;) { - if (offset > length - 12) - return -1; /* check end of data segment */ - /* Get Tag number */ - int tagnum; - if (is_motorola == 1) { - tagnum = exif_data[offset]; - tagnum <<= 8; - tagnum += exif_data[offset + 1]; - } else { - tagnum = exif_data[offset + 1]; - tagnum <<= 8; - tagnum += exif_data[offset]; - } - if (tagnum == 0x0112) - break; /* found Orientation Tag */ - if (--number_of_tags == 0) - return -1; - offset += 12; - } - - /* Get the Orientation value */ - if (is_motorola == 1) { - if (exif_data[offset + 8] != 0) - return -1; - set_flag = exif_data[offset + 9]; - } else { - if (exif_data[offset + 9] != 0) - return -1; - set_flag = exif_data[offset + 8]; - } - if (set_flag > 8) - return -1; - - return set_flag; - } } diff --git a/src/be/nikiroo/utils/ImageText.java b/src/be/nikiroo/utils/ImageText.java new file mode 100644 index 0000000..00bf3e4 --- /dev/null +++ b/src/be/nikiroo/utils/ImageText.java @@ -0,0 +1,475 @@ +package be.nikiroo.utils; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; + +/** + * This class converts an {@link Image} into a textual representation that can + * be displayed to the user in a TUI. + * + * @author niki + */ +public class ImageText { + private Image image; + private Dimension size; + private String text; + private boolean ready; + private Mode mode; + private boolean invert; + + /** + * Th rendering modes supported by this {@link ImageText} to convert + * {@link Image}s into text. + * + * @author niki + * + */ + public enum Mode { + /** + * Use 5 different "colours" which are actually Unicode + * {@link Character}s representing + * + */ + DITHERING, + /** + * Use "block" Unicode {@link Character}s up to quarter blocks, thus in + * effect doubling the resolution both in vertical and horizontal space. + * Note that since 2 {@link Character}s next to each other are square, + * we will use 4 blocks per 2 blocks for w/h resolution. + */ + DOUBLE_RESOLUTION, + /** + * Use {@link Character}s from both {@link Mode#DOUBLE_RESOLUTION} and + * {@link Mode#DITHERING}. + */ + DOUBLE_DITHERING, + /** + * Only use ASCII {@link Character}s. + */ + ASCII, + } + + /** + * Create a new {@link ImageText} with the given parameters. Defaults to + * {@link Mode#DOUBLE_DITHERING} and no colour inversion. + * + * @param image + * the source {@link Image} + * @param size + * the final text size to target + */ + public ImageText(Image image, Dimension size) { + this(image, size, Mode.DOUBLE_DITHERING, false); + } + + /** + * Create a new {@link ImageText} with the given parameters. + * + * @param image + * the source {@link Image} + * @param size + * the final text size to target + * @param mode + * the mode of conversion + * @param invert + * TRUE to invert colours rendering + */ + public ImageText(Image image, Dimension size, Mode mode, boolean invert) { + setImage(image); + setSize(size); + setMode(mode); + setColorInvert(invert); + } + + /** + * Change the source {@link Image}. + * + * @param image + * the new {@link Image} + */ + public void setImage(Image image) { + this.text = null; + this.ready = false; + this.image = image; + } + + /** + * Change the target size of this {@link ImageText}. + * + * @param size + * the new size + */ + public void setSize(Dimension size) { + this.text = null; + this.ready = false; + this.size = size; + } + + /** + * Change the image-to-text mode. + * + * @param mode + * the new {@link Mode} + */ + public void setMode(Mode mode) { + this.mode = mode; + this.text = null; + this.ready = false; + } + + /** + * Set the colour-invert mode. + * + * @param invert + * TRUE to inverse the colours + */ + public void setColorInvert(boolean invert) { + this.invert = invert; + this.text = null; + this.ready = false; + } + + /** + * Check if the colours are inverted. + * + * @return TRUE if the colours are inverted + */ + public boolean isColorInvert() { + return invert; + } + + /** + * Return the textual representation of the included {@link Image}. + * + * @return the {@link String} representation + */ + public String getText() { + if (text == null) { + if (image == null || size == null || size.width == 0 + || size.height == 0) + return ""; + + int mult = 1; + if (mode == Mode.DOUBLE_RESOLUTION || mode == Mode.DOUBLE_DITHERING) + mult = 2; + + int w = size.width * mult; + int h = size.height * mult; + + BufferedImage buff = new BufferedImage(w, h, + BufferedImage.TYPE_INT_ARGB); + + Graphics gfx = buff.getGraphics(); + + Dimension srcSize = getSize(image); + srcSize = new Dimension(srcSize.width * 2, srcSize.height); + int x = 0; + int y = 0; + + if (srcSize.width < srcSize.height) { + double ratio = (double) size.width / (double) size.height; + ratio *= (double) srcSize.height / (double) srcSize.width; + + h = (int) Math.round(ratio * h); + y = (buff.getHeight() - h) / 2; + } else { + double ratio = (double) size.height / (double) size.width; + ratio *= (double) srcSize.width / (double) srcSize.height; + + w = (int) Math.round(ratio * w); + x = (buff.getWidth() - w) / 2; + } + + if (gfx.drawImage(image, x, y, w, h, new ImageObserver() { + public boolean imageUpdate(Image img, int infoflags, int x, + int y, int width, int height) { + ImageText.this.ready = true; + return true; + } + })) { + ready = true; + } + + while (!ready) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + gfx.dispose(); + + StringBuilder builder = new StringBuilder(); + + for (int row = 0; row < buff.getHeight(); row += mult) { + if (row > 0) + builder.append('\n'); + + for (int col = 0; col < buff.getWidth(); col += mult) { + if (mult == 1) { + char car = ' '; + float brightness = getBrightness(buff.getRGB(col, row)); + if (mode == Mode.DITHERING) + car = getDitheringChar(brightness, " ░▒▓█"); + if (mode == Mode.ASCII) + car = getDitheringChar(brightness, " .-+=o8#"); + + builder.append(car); + } else if (mult == 2) { + builder.append(getBlockChar( // + buff.getRGB(col, row),// + buff.getRGB(col + 1, row),// + buff.getRGB(col, row + 1),// + buff.getRGB(col + 1, row + 1),// + mode == Mode.DOUBLE_DITHERING// + )); + } + } + } + + text = builder.toString(); + } + + return text; + } + + @Override + public String toString() { + return getText(); + } + + /** + * Return the size of the given {@link Image}. + * + * @param img + * the image to measure + * + * @return the size + */ + static private Dimension getSize(Image img) { + Dimension size = null; + while (size == null) { + int w = img.getWidth(null); + int h = img.getHeight(null); + if (w > -1 && h > -1) { + size = new Dimension(w, h); + } else { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + } + + return size; + } + + /** + * Return the {@link Character} corresponding to the given brightness level + * from the evenly-separated given {@link Character}s. + * + * @param brightness + * the brightness level + * @param cars + * the {@link Character}s to choose from, from less bright to + * most bright; MUST contain at least one + * {@link Character} + * + * @return the {@link Character} to use + */ + private char getDitheringChar(float brightness, String cars) { + int index = Math.round(brightness * (cars.length() - 1)); + return cars.charAt(index); + } + + /** + * Return the {@link Character} corresponding to the 4 given colours in + * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode. + * + * @param upperleft + * the upper left colour + * @param upperright + * the upper right colour + * @param lowerleft + * the lower left colour + * @param lowerright + * the lower right colour + * @param dithering + * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for + * {@link Mode#DOUBLE_RESOLUTION} + * + * @return the {@link Character} to use + */ + private char getBlockChar(int upperleft, int upperright, int lowerleft, + int lowerright, boolean dithering) { + int choice = 0; + if (getBrightness(upperleft) > 0.5f) + choice += 1; + if (getBrightness(upperright) > 0.5f) + choice += 2; + if (getBrightness(lowerleft) > 0.5f) + choice += 4; + if (getBrightness(lowerright) > 0.5f) + choice += 8; + + switch (choice) { + case 0: + return ' '; + case 1: + return '▘'; + case 2: + return '▝'; + case 3: + return '▀'; + case 4: + return '▖'; + case 5: + return '▌'; + case 6: + return '▞'; + case 7: + return '▛'; + case 8: + return '▗'; + case 9: + return '▚'; + case 10: + return '▐'; + case 11: + return '▜'; + case 12: + return '▄'; + case 13: + return '▙'; + case 14: + return '▟'; + case 15: + if (dithering) { + float avg = 0; + avg += getBrightness(upperleft); + avg += getBrightness(upperright); + avg += getBrightness(lowerleft); + avg += getBrightness(lowerright); + avg /= 4; + + return getDitheringChar(avg, " ░▒▓█"); + } else { + return '█'; + } + } + + return ' '; + } + + /** + * Temporary array used so not to create a lot of new ones. + */ + private float[] tmp = new float[4]; + + /** + * Return the brightness value to use from the given ARGB colour. + * + * @param argb + * the argb colour + * + * @return the brightness to sue for computations + */ + private float getBrightness(int argb) { + if (invert) + return 1 - rgb2hsb(argb, tmp)[2]; + return rgb2hsb(argb, tmp)[2]; + } + + /** + * Convert the given ARGB colour in HSL/HSB, either into the supplied array + * or into a new one if array is NULL. + * + *

+ * ARGB pixels are given in 0xAARRGGBB format, while the returned array will + * contain Hue, Saturation, Lightness/Brightness, Alpha, in this order. H, + * S, L and A are all ranging from 0 to 1 (indeed, H is in 1/360th). + *

+ * pixel + * + * @param argb + * the ARGB colour pixel to convert + * @param array + * the array to convert into or NULL to create a new one + * + * @return the array containing the HSL/HSB converted colour + */ + static float[] rgb2hsb(int argb, float[] array) { + int a, r, g, b; + a = ((argb & 0xff000000) >> 24); + r = ((argb & 0x00ff0000) >> 16); + g = ((argb & 0x0000ff00) >> 8); + b = ((argb & 0x000000ff)); + + if (array == null) + array = new float[4]; + Color.RGBtoHSB(r, g, b, array); + + array[3] = a; + + return array; + + // // other implementation: + // + // float a, r, g, b; + // a = ((argb & 0xff000000) >> 24) / 255.0f; + // r = ((argb & 0x00ff0000) >> 16) / 255.0f; + // g = ((argb & 0x0000ff00) >> 8) / 255.0f; + // b = ((argb & 0x000000ff)) / 255.0f; + // + // float rgbMin, rgbMax; + // rgbMin = Math.min(r, Math.min(g, b)); + // rgbMax = Math.max(r, Math.max(g, b)); + // + // float l; + // l = (rgbMin + rgbMax) / 2; + // + // float s; + // if (rgbMin == rgbMax) { + // s = 0; + // } else { + // if (l <= 0.5) { + // s = (rgbMax - rgbMin) / (rgbMax + rgbMin); + // } else { + // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin); + // } + // } + // + // float h; + // if (r > g && r > b) { + // h = (g - b) / (rgbMax - rgbMin); + // } else if (g > b) { + // h = 2.0f + (b - r) / (rgbMax - rgbMin); + // } else { + // h = 4.0f + (r - g) / (rgbMax - rgbMin); + // } + // h /= 6; // from 0 to 1 + // + // return new float[] { h, s, l, a }; + // + // // // natural mode: + // // + // // int aa = (int) Math.round(100 * a); + // // int hh = (int) (360 * h); + // // if (hh < 0) + // // hh += 360; + // // int ss = (int) Math.round(100 * s); + // // int ll = (int) Math.round(100 * l); + // // + // // return new int[] { hh, ss, ll, aa }; + } +} diff --git a/src/be/nikiroo/utils/ImageUtils.java b/src/be/nikiroo/utils/ImageUtils.java new file mode 100644 index 0000000..afeb5d3 --- /dev/null +++ b/src/be/nikiroo/utils/ImageUtils.java @@ -0,0 +1,400 @@ +package be.nikiroo.utils; + +import java.awt.Dimension; +import java.awt.Image; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import be.nikiroo.utils.ImageText.Mode; + +/** + * This class offer some utilities based around images. + * + * @author niki + */ +public class ImageUtils { + /** + * Convert the given {@link Image} object into a Base64 representation of + * the same {@link Image} object. + * + * @param image + * the {@link Image} object to convert + * + * @return the Base64 representation + * + * @throws IOException + * in case of IO error + */ + static public String toBase64(BufferedImage image) throws IOException { + return toBase64(image, null); + } + + /** + * Convert the given {@link Image} object into a Base64 representation of + * the same {@link Image}. object. + * + * @param image + * the {@link Image} object to convert + * @param format + * the image format to use to serialise it (default is PNG) + * + * @return the Base64 representation + * + * @throws IOException + * in case of IO error + */ + static public String toBase64(BufferedImage image, String format) + throws IOException { + if (format == null) { + format = "png"; + } + + String imageString = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + ImageIO.write(image, format, out); + byte[] imageBytes = out.toByteArray(); + + imageString = new String(Base64.encodeBytes(imageBytes)); + + out.close(); + + return imageString; + } + + /** + * Convert the given image into a Base64 representation of the same + * {@link File}. + * + * @param in + * the image to convert + * + * @return the Base64 representation + * + * @throws IOException + * in case of IO error + */ + static public String toBase64(InputStream in) throws IOException { + String fileString = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + byte[] buf = new byte[8192]; + + int c = 0; + while ((c = in.read(buf, 0, buf.length)) > 0) { + out.write(buf, 0, c); + } + out.flush(); + in.close(); + + fileString = new String(Base64.encodeBytes(out.toByteArray())); + out.close(); + + return fileString; + } + + /** + * Convert the given Base64 representation of an image into an {@link Image} + * object. + * + * @param b64data + * the {@link Image} in Base64 format + * + * @return the {@link Image} object + * + * @throws IOException + * in case of IO error + */ + static public BufferedImage fromBase64(String b64data) throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream( + Base64.decode(b64data)); + return fromStream(in); + } + + /** + * Convert the given {@link InputStream} (which should allow calls to + * {@link InputStream#reset()} for better perfs) into an {@link Image} + * object, respecting the EXIF transformations if any. + * + * @param in + * the 'resetable' {@link InputStream} + * + * @return the {@link Image} object + * + * @throws IOException + * in case of IO error + */ + static public BufferedImage fromStream(InputStream in) throws IOException { + MarkableFileInputStream tmpIn = null; + File tmp = null; + try { + in.reset(); + } catch (IOException e) { + tmp = File.createTempFile(".tmp-image", ".tmp"); + tmp.deleteOnExit(); + IOUtils.write(in, tmp); + tmpIn = new MarkableFileInputStream(new FileInputStream(tmp)); + } + + int orientation; + try { + orientation = getExifTransorm(in); + } catch (Exception e) { + // no EXIF transform, ok + orientation = -1; + } + + in.reset(); + BufferedImage image = ImageIO.read(in); + + if (image == null) { + if (tmp != null) { + tmp.delete(); + tmpIn.close(); + } + throw new IOException("Failed to convert input to image"); + } + + // Note: this code has been found on Internet; + // thank you anonymous coder. + int width = image.getWidth(); + int height = image.getHeight(); + AffineTransform affineTransform = new AffineTransform(); + + switch (orientation) { + case 1: + affineTransform = null; + break; + case 2: // Flip X + affineTransform.scale(-1.0, 1.0); + affineTransform.translate(-width, 0); + break; + case 3: // PI rotation + affineTransform.translate(width, height); + affineTransform.rotate(Math.PI); + break; + case 4: // Flip Y + affineTransform.scale(1.0, -1.0); + affineTransform.translate(0, -height); + break; + case 5: // - PI/2 and Flip X + affineTransform.rotate(-Math.PI / 2); + affineTransform.scale(-1.0, 1.0); + break; + case 6: // -PI/2 and -width + affineTransform.translate(height, 0); + affineTransform.rotate(Math.PI / 2); + break; + case 7: // PI/2 and Flip + affineTransform.scale(-1.0, 1.0); + affineTransform.translate(-height, 0); + affineTransform.translate(0, width); + affineTransform.rotate(3 * Math.PI / 2); + break; + case 8: // PI / 2 + affineTransform.translate(0, width); + affineTransform.rotate(3 * Math.PI / 2); + break; + default: + affineTransform = null; + break; + } + + if (affineTransform != null) { + AffineTransformOp affineTransformOp = new AffineTransformOp( + affineTransform, AffineTransformOp.TYPE_BILINEAR); + + BufferedImage transformedImage = new BufferedImage(width, height, + image.getType()); + transformedImage = affineTransformOp + .filter(image, transformedImage); + + image = transformedImage; + } + // + + if (tmp != null) { + tmp.delete(); + tmpIn.close(); + } + + return image; + } + + /** + * A shorthand method to create an {@link ImageText} and return its output. + * + * @param image + * the source {@link Image} + * @param size + * the final text size to target + * @param mode + * the mode of conversion + * @param invert + * TRUE to invert colours rendering + * + * @return the text image + */ + static public String toAscii(Image image, Dimension size, Mode mode, + boolean invert) { + return new ImageText(image, size, mode, invert).toString(); + } + + /** + * Return the EXIF transformation flag of this image if any. + * + *

+ * Note: this code has been found on internet; thank you anonymous coder. + *

+ * + * @param in + * the data {@link InputStream} + * + * @return the transformation flag if any + * + * @throws IOException + * in case of IO error + */ + static private int getExifTransorm(InputStream in) throws IOException { + int[] exif_data = new int[100]; + int set_flag = 0; + int is_motorola = 0; + + /* Read File head, check for JPEG SOI + Exif APP1 */ + for (int i = 0; i < 4; i++) + exif_data[i] = in.read(); + + if (exif_data[0] != 0xFF || exif_data[1] != 0xD8 + || exif_data[2] != 0xFF || exif_data[3] != 0xE1) + return -2; + + /* Get the marker parameter length count */ + int length = (in.read() << 8 | in.read()); + + /* Length includes itself, so must be at least 2 */ + /* Following Exif data length must be at least 6 */ + if (length < 8) + return -1; + length -= 8; + /* Read Exif head, check for "Exif" */ + for (int i = 0; i < 6; i++) + exif_data[i] = in.read(); + + if (exif_data[0] != 0x45 || exif_data[1] != 0x78 + || exif_data[2] != 0x69 || exif_data[3] != 0x66 + || exif_data[4] != 0 || exif_data[5] != 0) + return -1; + + /* Read Exif body */ + length = length > exif_data.length ? exif_data.length : length; + for (int i = 0; i < length; i++) + exif_data[i] = in.read(); + + if (length < 12) + return -1; /* Length of an IFD entry */ + + /* Discover byte order */ + if (exif_data[0] == 0x49 && exif_data[1] == 0x49) + is_motorola = 0; + else if (exif_data[0] == 0x4D && exif_data[1] == 0x4D) + is_motorola = 1; + else + return -1; + + /* Check Tag Mark */ + if (is_motorola == 1) { + if (exif_data[2] != 0) + return -1; + if (exif_data[3] != 0x2A) + return -1; + } else { + if (exif_data[3] != 0) + return -1; + if (exif_data[2] != 0x2A) + return -1; + } + + /* Get first IFD offset (offset to IFD0) */ + int offset; + if (is_motorola == 1) { + if (exif_data[4] != 0) + return -1; + if (exif_data[5] != 0) + return -1; + offset = exif_data[6]; + offset <<= 8; + offset += exif_data[7]; + } else { + if (exif_data[7] != 0) + return -1; + if (exif_data[6] != 0) + return -1; + offset = exif_data[5]; + offset <<= 8; + offset += exif_data[4]; + } + if (offset > length - 2) + return -1; /* check end of data segment */ + + /* Get the number of directory entries contained in this IFD */ + int number_of_tags; + if (is_motorola == 1) { + number_of_tags = exif_data[offset]; + number_of_tags <<= 8; + number_of_tags += exif_data[offset + 1]; + } else { + number_of_tags = exif_data[offset + 1]; + number_of_tags <<= 8; + number_of_tags += exif_data[offset]; + } + if (number_of_tags == 0) + return -1; + offset += 2; + + /* Search for Orientation Tag in IFD0 */ + for (;;) { + if (offset > length - 12) + return -1; /* check end of data segment */ + /* Get Tag number */ + int tagnum; + if (is_motorola == 1) { + tagnum = exif_data[offset]; + tagnum <<= 8; + tagnum += exif_data[offset + 1]; + } else { + tagnum = exif_data[offset + 1]; + tagnum <<= 8; + tagnum += exif_data[offset]; + } + if (tagnum == 0x0112) + break; /* found Orientation Tag */ + if (--number_of_tags == 0) + return -1; + offset += 12; + } + + /* Get the Orientation value */ + if (is_motorola == 1) { + if (exif_data[offset + 8] != 0) + return -1; + set_flag = exif_data[offset + 9]; + } else { + if (exif_data[offset + 9] != 0) + return -1; + set_flag = exif_data[offset + 8]; + } + if (set_flag > 8) + return -1; + + return set_flag; + } +} diff --git a/src/be/nikiroo/utils/StringUtils.java b/src/be/nikiroo/utils/StringUtils.java index af69845..a50ec28 100644 --- a/src/be/nikiroo/utils/StringUtils.java +++ b/src/be/nikiroo/utils/StringUtils.java @@ -1,12 +1,7 @@ package be.nikiroo.utils; -import java.awt.Image; -import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.Normalizer; @@ -17,8 +12,6 @@ import java.util.Date; import java.util.Scanner; import java.util.regex.Pattern; -import javax.imageio.ImageIO; - import org.unbescape.html.HtmlEscape; import org.unbescape.html.HtmlEscapeLevel; import org.unbescape.html.HtmlEscapeType; @@ -201,104 +194,6 @@ public class StringUtils { } } - /** - * Convert the given {@link Image} object into a Base64 representation of - * the same {@link Image}. object. - * - * @param image - * the {@link Image} object to convert - * - * @return the Base64 representation - * - * @throws IOException - * in case of IO error - */ - static public String fromImage(BufferedImage image) throws IOException { - return fromImage(image, null); - } - - /** - * Convert the given {@link Image} object into a Base64 representation of - * the same {@link Image}. object. - * - * @param image - * the {@link Image} object to convert - * @param format - * the image format to use to serialise it (default is PNG) - * - * @return the Base64 representation - * - * @throws IOException - * in case of IO error - */ - static public String fromImage(BufferedImage image, String format) - throws IOException { - if (format == null) { - format = "png"; - } - - String imageString = null; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - ImageIO.write(image, format, out); - byte[] imageBytes = out.toByteArray(); - - imageString = new String(Base64.encodeBytes(imageBytes)); - - out.close(); - - return imageString; - } - - /** - * Convert the given image into a Base64 representation of the same - * {@link File}. - * - * @param in - * the image to convert - * - * @return the Base64 representation - * - * @throws IOException - * in case of IO error - */ - static public String fromStream(InputStream in) throws IOException { - String fileString = null; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - byte[] buf = new byte[8192]; - - int c = 0; - while ((c = in.read(buf, 0, buf.length)) > 0) { - out.write(buf, 0, c); - } - out.flush(); - in.close(); - - fileString = new String(Base64.encodeBytes(out.toByteArray())); - out.close(); - - return fileString; - } - - /** - * Convert the given Base64 representation of an image into an {@link Image} - * object. - * - * @param b64data - * the {@link Image} in Base64 format - * - * @return the {@link Image} object - * - * @throws IOException - * in case of IO error - */ - static public BufferedImage toImage(String b64data) throws IOException { - ByteArrayInputStream in = new ByteArrayInputStream( - Base64.decode(b64data)); - return IOUtils.toImage(in); - } - /** * Return a hash of the given {@link String}. * @@ -307,7 +202,7 @@ public class StringUtils { * * @return the hash */ - static public String getHash(String input) { + static public String getMd5Hash(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(input.getBytes()); diff --git a/src/be/nikiroo/utils/resources/Bundle.java b/src/be/nikiroo/utils/resources/Bundle.java index bad7f3e..1dbb251 100644 --- a/src/be/nikiroo/utils/resources/Bundle.java +++ b/src/be/nikiroo/utils/resources/Bundle.java @@ -657,7 +657,7 @@ public class Bundle> { * @param bundle * the bundle to copy */ - private void resetMap(ResourceBundle bundle) { + protected void resetMap(ResourceBundle bundle) { this.map.clear(); if (bundle != null) { @@ -671,7 +671,7 @@ public class Bundle> { } } } - + /** * Take a snapshot of the changes in memory in this {@link Bundle} made by * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the diff --git a/src/be/nikiroo/utils/serial/SerialUtils.java b/src/be/nikiroo/utils/serial/SerialUtils.java index c28faed..be18769 100644 --- a/src/be/nikiroo/utils/serial/SerialUtils.java +++ b/src/be/nikiroo/utils/serial/SerialUtils.java @@ -11,7 +11,7 @@ import java.util.HashMap; import java.util.Map; import java.util.UnknownFormatConversionException; -import be.nikiroo.utils.StringUtils; +import be.nikiroo.utils.ImageUtils; /** * Small class to help with serialisation. @@ -88,7 +88,7 @@ public class SerialUtils { @Override protected String toString(Object value) { try { - return StringUtils.fromImage((BufferedImage) value); + return ImageUtils.toBase64((BufferedImage) value); } catch (IOException e) { throw new UnknownFormatConversionException(e.getMessage()); } @@ -102,7 +102,7 @@ public class SerialUtils { @Override protected Object fromString(String content) { try { - return StringUtils.toImage(content); + return ImageUtils.fromBase64(content); } catch (IOException e) { throw new UnknownFormatConversionException(e.getMessage()); } -- 2.27.0