+++ /dev/null
-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
--- /dev/null
+# 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
+
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
# Export script
#
# Version:
+# - 1.1.0: allow multiple targets
# - 1.0.0: add a version comment
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
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;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
-import javax.imageio.ImageIO;
-
/**
* This class offer some utilities based around Streams.
*
}
}
- /**
- * 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).
*
return loader.getResourceAsStream(name);
}
-
- /**
- * Return the EXIF transformation flag of this image if any.
- *
- * <p>
- * Note: this code has been found on internet; thank you anonymous coder.
- * </p>
- *
- * @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;
- }
}
--- /dev/null
+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
+ * <ul>
+ * <li>space (blank)</li>
+ * <li>low shade (░)</li>
+ * <li>medium shade (▒)</li>
+ * <li>high shade (▓)</li>
+ * <li>full block (█)</li>
+ * </ul>
+ */
+ 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; <b>MUST</b> 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.
+ *
+ * <p>
+ * 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).
+ * </p>
+ * 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 };
+ }
+}
--- /dev/null
+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.
+ *
+ * <p>
+ * Note: this code has been found on internet; thank you anonymous coder.
+ * </p>
+ *
+ * @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;
+ }
+}
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;
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;
}
}
- /**
- * 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}.
*
*
* @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());
* @param bundle
* the bundle to copy
*/
- private void resetMap(ResourceBundle bundle) {
+ protected void resetMap(ResourceBundle bundle) {
this.map.clear();
if (bundle != null) {
}
}
}
-
+
/**
* Take a snapshot of the changes in memory in this {@link Bundle} made by
* the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the
import java.util.Map;
import java.util.UnknownFormatConversionException;
-import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.ImageUtils;
/**
* Small class to help with serialisation.
@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());
}
@Override
protected Object fromString(String content) {
try {
- return StringUtils.toImage(content);
+ return ImageUtils.fromBase64(content);
} catch (IOException e) {
throw new UnknownFormatConversionException(e.getMessage());
}