From d7f7b7e774b168ee5f41a52eca4e45fd6d07bc37 Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Mon, 29 Oct 2018 22:30:09 +0900 Subject: [PATCH 01/17] Issue #39: Implement LaTeX math support. --- .gitignore | 5 + pom.xml | 172 ++++++++++++ .../core/CommandLineConverter.java | 2 +- .../ascii2image/core/ConversionOptions.java | 13 +- .../ascii2image/core/ProcessingOptions.java | 7 + .../ascii2image/graphics/BitmapRenderer.java | 37 ++- .../ascii2image/graphics/Diagram.java | 80 +++--- .../ascii2image/graphics/DiagramText.java | 144 +++++++++- .../ascii2image/graphics/SVGBuilder.java | 51 +--- .../ascii2image/text/StringUtils.java | 37 ++- .../ascii2image/text/TextGrid.java | 249 ++++++++++++------ test-resources/text/art-latexmath-1.txt | 38 +++ test/java/sandbox/Sandbox.java | 145 ++++++++++ 13 files changed, 770 insertions(+), 210 deletions(-) create mode 100644 pom.xml create mode 100644 test-resources/text/art-latexmath-1.txt create mode 100644 test/java/sandbox/Sandbox.java diff --git a/.gitignore b/.gitignore index 65bb389..1194eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,9 @@ build/testclasses/ releases/ tests/images/ tests/images-expected/ + +# Files copied by mvn's build procedure +tests/text/ +tests/build.xml target/ +/.lein-failures diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..215e9b8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,172 @@ + + + 4.0.0 + stathissideris + ditaa + jar + 0.11.0 + ditaa + command-line utility that can convert diagrams drawn using ascii art into proper bitmap graphics + + https://github.com/stathissideris/ditaa + + + GNU Lesser General Public License v3.0 + https://www.gnu.org/licenses/lgpl-3.0.en.html + + + + https://github.com/dakusui/ditaa + scm:git:git://github.com/dakusui/ditaa.git + scm:git:ssh://git@github.com/dakusui/ditaa.git + 95a481e5a01d07317edc4af4cbdfb4c821090437 + + + src/java + test/java + + + resources + + + target + target/classes + + + maven-resources-plugin + 3.1.0 + + + copy-resources + + validate + + copy-resources + + + ${basedir}/tests + + + test-resources + true + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.7 + + + add-source + generate-sources + + add-source + + + + src/java + + + + + + + + + + central + https://repo1.maven.org/maven2/ + + false + + + true + + + + clojars + https://repo.clojars.org/ + + true + + + true + + + + + + + + + org.clojure + clojure + 1.9.0 + + + commons-cli + commons-cli + 1.4 + + + net.htmlparser.jericho + jericho-html + 3.4 + + + org.apache.xmlgraphics + batik-gvt + 1.9 + + + org.apache.xmlgraphics + batik-codec + 1.9 + + + org.apache.xmlgraphics + batik-bridge + 1.9 + + + org.scilab.forge + jlatexmath + 1.0.7 + + + junit + junit + 4.12 + test + + + com.github.dakusui + thincrest + 3.5.1 + + + + + diff --git a/src/java/org/stathissideris/ascii2image/core/CommandLineConverter.java b/src/java/org/stathissideris/ascii2image/core/CommandLineConverter.java index 2020be4..34e3fb9 100644 --- a/src/java/org/stathissideris/ascii2image/core/CommandLineConverter.java +++ b/src/java/org/stathissideris/ascii2image/core/CommandLineConverter.java @@ -41,7 +41,6 @@ import java.io.UnsupportedEncodingException; /** - * * @author Efstathios Sideris */ public class CommandLineConverter { @@ -67,6 +66,7 @@ public static void main(String[] args) { cmdLnOptions.addOption("d", "debug", false, "Renders the debug grid over the resulting image."); cmdLnOptions.addOption("r", "round-corners", false, "Causes all corners to be rendered as round corners."); cmdLnOptions.addOption("E", "no-separation", false, "Prevents the separation of common edges of shapes."); + cmdLnOptions.addOption("L", "latex-math", false, "Enable LaTeX math mode."); cmdLnOptions.addOption("h", "html", false, "In this case the input is an HTML file. The contents of the
 tags are rendered as diagrams and saved in the images directory and a new HTML file is produced with the appropriate  tags.");
     cmdLnOptions.addOption("T", "transparent", false, "Causes the diagram to be rendered on a transparent background. Overrides --background.");
 
diff --git a/src/java/org/stathissideris/ascii2image/core/ConversionOptions.java b/src/java/org/stathissideris/ascii2image/core/ConversionOptions.java
index 52a7eb1..2157267 100644
--- a/src/java/org/stathissideris/ascii2image/core/ConversionOptions.java
+++ b/src/java/org/stathissideris/ascii2image/core/ConversionOptions.java
@@ -29,9 +29,9 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.HashMap;
+import java.util.Objects;
 
 /**
- *
  * @author Efstathios Sideris
  */
 public class ConversionOptions {
@@ -49,8 +49,10 @@ public void setDebug(boolean value) {
   public ConversionOptions() {
   }
 
-  /** Parse a color from a 6- or 8-digit hex string.  For example, FF0000 is red.
-   *  If eight digits, last two digits are alpha. */
+  /**
+   * Parse a color from a 6- or 8-digit hex string.  For example, FF0000 is red.
+   * If eight digits, last two digits are alpha.
+   */
   public static Color parseColor(String hexString) {
     if (hexString.length() == 6) {
       return new Color(Integer.parseInt(hexString, 16));
@@ -80,6 +82,7 @@ public ConversionOptions(CommandLine cmdLine) throws UnsupportedEncodingExceptio
 
     processingOptions.setAllCornersAreRound(cmdLine.hasOption("round-corners"));
     processingOptions.setPerformSeparationOfCommonEdges(!cmdLine.hasOption("no-separation"));
+    processingOptions.enableLaTeXmath(cmdLine.hasOption("latex-math"));
     renderingOptions.setAntialias(!cmdLine.hasOption("no-antialias"));
     renderingOptions.setFixedSlope(cmdLine.hasOption("fixed-slope"));
 
@@ -107,6 +110,10 @@ public ConversionOptions(CommandLine cmdLine) throws UnsupportedEncodingExceptio
       processingOptions.setCharacterEncoding(encoding);
     }
 
+    if (cmdLine.hasOption("latex"))
+      processingOptions.enableLaTeXmath(
+          Objects.equals(cmdLine.getOptionValue("latex", "no"), "yes"));
+
     if (cmdLine.hasOption("svg")) {
       renderingOptions.setImageType(RenderingOptions.ImageType.SVG);
     }
diff --git a/src/java/org/stathissideris/ascii2image/core/ProcessingOptions.java b/src/java/org/stathissideris/ascii2image/core/ProcessingOptions.java
index 6d56f9f..c7b52dc 100644
--- a/src/java/org/stathissideris/ascii2image/core/ProcessingOptions.java
+++ b/src/java/org/stathissideris/ascii2image/core/ProcessingOptions.java
@@ -35,6 +35,7 @@ public class ProcessingOptions {
   private boolean overwriteFiles                 = false;
   private boolean performSeparationOfCommonEdges = true;
   private boolean allCornersAreRound             = false;
+  private boolean latexMathEnabled = false;
 
   public static final int USE_TAGS          = 0;
   public static final int RENDER_TAGS       = 1;
@@ -237,5 +238,11 @@ public CustomShapeDefinition getFromCustomShapes(String tagName) {
     return customShapes.get(tagName);
   }
 
+  public void enableLaTeXmath(boolean b) {
+    this.latexMathEnabled = b;
+  }
 
+  public boolean isLaTeXmathEnabled() {
+    return this.latexMathEnabled;
+  }
 }
diff --git a/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java b/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java
index 4ff40fe..28b13d6 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java
@@ -338,16 +338,19 @@ public RenderedImage render(Diagram diagram, BufferedImage image, RenderingOptio
     Iterator textIt = diagram.getTextObjects().iterator();
     while (textIt.hasNext()) {
       DiagramText text = textIt.next();
-      g2.setFont(text.getFont());
-      if (text.hasOutline()) {
-        g2.setColor(text.getOutlineColor());
-        g2.drawString(text.getText(), text.getXPos() + 1, text.getYPos());
-        g2.drawString(text.getText(), text.getXPos() - 1, text.getYPos());
-        g2.drawString(text.getText(), text.getXPos(), text.getYPos() + 1);
-        g2.drawString(text.getText(), text.getXPos(), text.getYPos() - 1);
-      }
-      g2.setColor(text.getColor());
-      g2.drawString(text.getText(), text.getXPos(), text.getYPos());
+      text.drawOn(g2);
+			/*
+			g2.setFont(text.getFont());
+			if(text.hasOutline()){
+				g2.setColor(text.getOutlineColor());
+				g2.drawString(text.getText(), text.getXPos() + 1, text.getYPos());
+				g2.drawString(text.getText(), text.getXPos() - 1, text.getYPos());
+				g2.drawString(text.getText(), text.getXPos(), text.getYPos() + 1);
+				g2.drawString(text.getText(), text.getXPos(), text.getYPos() - 1);
+			}
+			g2.setColor(text.getColor());
+			g2.drawString(text.getText(), text.getXPos(), text.getYPos());
+			*/
     }
 
     if (options.renderDebugLines() || DEBUG_LINES) {
@@ -387,20 +390,10 @@ public TextCanvas(ArrayList textObjects) {
     }
 
     public void paint(Graphics g) {
-      Graphics g2 = (Graphics2D) g;
+      Graphics2D g2 = (Graphics2D) g;
       Iterator textIt = textObjects.iterator();
       while (textIt.hasNext()) {
-        DiagramText text = (DiagramText) textIt.next();
-        g2.setFont(text.getFont());
-        if (text.hasOutline()) {
-          g2.setColor(text.getOutlineColor());
-          g2.drawString(text.getText(), text.getXPos() + 1, text.getYPos());
-          g2.drawString(text.getText(), text.getXPos() - 1, text.getYPos());
-          g2.drawString(text.getText(), text.getXPos(), text.getYPos() + 1);
-          g2.drawString(text.getText(), text.getXPos(), text.getYPos() - 1);
-        }
-        g2.setColor(text.getColor());
-        g2.drawString(text.getText(), text.getXPos(), text.getYPos());
+        textIt.next().drawOn(g2);
       }
     }
   }
diff --git a/src/java/org/stathissideris/ascii2image/graphics/Diagram.java b/src/java/org/stathissideris/ascii2image/graphics/Diagram.java
index 80cd53c..342f4a8 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/Diagram.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/Diagram.java
@@ -35,7 +35,6 @@
 import java.util.Iterator;
 
 /**
- *
  * @author Efstathios Sideris
  */
 public class Diagram {
@@ -53,54 +52,53 @@ public class Diagram {
 
 
   /**
-   *
    * 

An outline of the inner workings of this very important (and monstrous) * constructor is presented here. Boundary processing is the first step * of the process:

* *
    - *
  1. Copy the grid into a work grid and remove all type-on-line - * and point markers from the work grid
  2. - *
  3. Split grid into distinct shapes by plotting the grid - * onto an AbstractionGrid and its getDistinctShapes() method.
  4. - *
  5. Find all the possible boundary sets of each of the - * distinct shapes. This can produce duplicate shapes (if the boundaries - * are the same when filling from the inside and the outside).
  6. - *
  7. Remove duplicate boundaries.
  8. - *
  9. Remove obsolete boundaries. Obsolete boundaries are the ones that are - * the sum of their parts when plotted as filled shapes. (see method - * removeObsoleteShapes())
  10. - *
  11. Separate the found boundary sets to open, closed or mixed - * (See CellSet class on how its done).
  12. - *
  13. Are there any closed boundaries? - *
      - *
    • YES. Subtract all the closed boundaries from each of the - * open ones. That should convert the mixed shapes into open.
    • - *
    • NO. In this (harder) case, we use the method - * breakTrulyMixedBoundaries() of CellSet to break boundaries - * into open and closed shapes (would work in any case, but it's - * probably slower than the other method). This method is based - * on tracing from the lines' ends and splitting when we get to - * an intersection.
    • - *
    - *
  14. - *
  15. If we had to eliminate any mixed shapes, we separate the found - * boundary sets again to open, closed or mixed.
  16. + *
  17. Copy the grid into a work grid and remove all type-on-line + * and point markers from the work grid
  18. + *
  19. Split grid into distinct shapes by plotting the grid + * onto an AbstractionGrid and its getDistinctShapes() method.
  20. + *
  21. Find all the possible boundary sets of each of the + * distinct shapes. This can produce duplicate shapes (if the boundaries + * are the same when filling from the inside and the outside).
  22. + *
  23. Remove duplicate boundaries.
  24. + *
  25. Remove obsolete boundaries. Obsolete boundaries are the ones that are + * the sum of their parts when plotted as filled shapes. (see method + * removeObsoleteShapes())
  26. + *
  27. Separate the found boundary sets to open, closed or mixed + * (See CellSet class on how its done).
  28. + *
  29. Are there any closed boundaries? + *
      + *
    • YES. Subtract all the closed boundaries from each of the + * open ones. That should convert the mixed shapes into open.
    • + *
    • NO. In this (harder) case, we use the method + * breakTrulyMixedBoundaries() of CellSet to break boundaries + * into open and closed shapes (would work in any case, but it's + * probably slower than the other method). This method is based + * on tracing from the lines' ends and splitting when we get to + * an intersection.
    • + *
    + *
  30. + *
  31. If we had to eliminate any mixed shapes, we separate the found + * boundary sets again to open, closed or mixed.
  32. *
* *

At this stage, the boundary processing is all complete and we * proceed with using those boundaries to create the shapes:

* *
    - *
  1. Create closed shapes.
  2. - *
  3. Create open shapes. That's when the line end corrections are - * also applied, concerning the positioning of the ends of lines - * see methods connectEndsToAnchors() and moveEndsToCellEdges() of - * DiagramShape.
  4. - *
  5. Assign color codes to closed shapes.
  6. - *
  7. Assign extended markup tags to closed shapes.

    - *
  8. Create arrowheads.

    - *
  9. Create point markers.

    + *
  10. Create closed shapes.
  11. + *
  12. Create open shapes. That's when the line end corrections are + * also applied, concerning the positioning of the ends of lines + * see methods connectEndsToAnchors() and moveEndsToCellEdges() of + * DiagramShape.
  13. + *
  14. Assign color codes to closed shapes.
  15. + *
  16. Assign extended markup tags to closed shapes.

    + *
  17. Create arrowheads.

    + *
  18. Create point markers.

    *
* *

Finally, the text processing occurs: [pending]

@@ -565,11 +563,12 @@ else if (type == CellSet.TYPE_MIXED) int maxX = getCellMaxX(lastCell); DiagramText textObject; + boolean laTeXmathEnabled = options.processingOptions.isLaTeXmathEnabled(); if (FontMeasurer.instance().getWidthFor(string, font) > maxX - minX) { //does not fit horizontally Font lessWideFont = FontMeasurer.instance().getFontFor(maxX - minX, string); - textObject = new DiagramText(minX, y, string, lessWideFont); + textObject = new DiagramText(minX, y, string, lessWideFont, laTeXmathEnabled); } else - textObject = new DiagramText(minX, y, string, font); + textObject = new DiagramText(minX, y, string, font, laTeXmathEnabled); textObject.centerVerticallyBetween(getCellMinY(cell), getCellMaxY(cell)); @@ -648,7 +647,6 @@ public ArrayList getAllDiagramShapes() { * when plotted as filled shapes. * * @return true if it removed any obsolete. - * */ private boolean removeObsoleteShapes(TextGrid grid, ArrayList sets) { if (DEBUG) diff --git a/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java b/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java index 47767e7..58675aa 100644 --- a/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java +++ b/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java @@ -1,34 +1,48 @@ /** * ditaa - Diagrams Through Ascii Art - * + *

* Copyright (C) 2004-2011 Efstathios Sideris - * + *

* ditaa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. - * + *

* ditaa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. - * + *

* You should have received a copy of the GNU Lesser General Public * License along with ditaa. If not, see . */ package org.stathissideris.ascii2image.graphics; +import org.scilab.forge.jlatexmath.TeXConstants; +import org.scilab.forge.jlatexmath.TeXFormula; +import org.scilab.forge.jlatexmath.TeXIcon; +import org.stathissideris.ascii2image.core.RenderingOptions; +import org.stathissideris.ascii2image.text.StringUtils; + +import javax.swing.JLabel; import java.awt.Color; import java.awt.Font; +import java.awt.Graphics2D; import java.awt.geom.Rectangle2D; +import java.util.Iterator; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.stathissideris.ascii2image.graphics.SVGBuilder.colorToHex; /** - * * @author Efstathios Sideris */ public class DiagramText extends DiagramComponent { - public static final Color DEFAULT_COLOR = Color.black; + public static final Color DEFAULT_COLOR = Color.black; + public static final Pattern TEXT_SPLITTING_REGEX = Pattern.compile("([^$]+|\\$[^$]*\\$)"); + private final boolean latexMathEnabled; private String text; private Font font; @@ -38,7 +52,7 @@ public class DiagramText extends DiagramComponent { private boolean hasOutline = false; private Color outlineColor = Color.white; - public DiagramText(int x, int y, String text, Font font) { + public DiagramText(int x, int y, String text, Font font, boolean latexMathEnabled) { if (text == null) throw new IllegalArgumentException("DiagramText cannot be initialised with a null string"); if (font == null) @@ -48,6 +62,7 @@ public DiagramText(int x, int y, String text, Font font) { this.yPos = y; this.text = text; this.font = font; + this.latexMathEnabled = latexMathEnabled; } public void centerInBounds(Rectangle2D bounds) { @@ -95,6 +110,76 @@ public String getText() { return text; } + public void drawOn(Graphics2D g2) { + g2.setFont(this.getFont()); + if (this.hasOutline()) { + g2.setColor(this.getOutlineColor()); + Stream.of(1, -1) + .peek(d -> draw(g2, this.getXPos() + d, this.getYPos(), this.getColor())) + .peek(d -> draw(g2, this.getXPos(), this.getYPos() + d, this.getColor())) + .forEach(d -> { + }); + } + g2.setColor(this.getColor()); + draw(g2, this.getXPos(), this.getYPos(), getColor()); + } + + private void draw(Graphics2D g2, int xPos, int yPos, Color color) { + Iterator i = StringUtils.createTextSplitter(TEXT_SPLITTING_REGEX, this.getText()); + int x = xPos; + while (i.hasNext()) { + String text = i.next(); + if (isTeXFormula(text)) + x += drawTeXFormula(g2, + text, + x, yPos, color, + font.getSize()); + else + x += drawString(g2, + text, + x, yPos, color, + font); + } + } + + public void renderOn(StringBuilder svgBuildingBuffer, RenderingOptions options) { + if (this.hasOutline()) { + Stream.of(1, -1) + .peek(d -> render(svgBuildingBuffer, options, + this.getXPos() + d, this.getYPos(), + this.getOutlineColor())) + .peek(d -> render(svgBuildingBuffer, options, + this.getXPos(), this.getYPos() + d, + this.getOutlineColor())) + .forEach(d -> { + }); + } + render(svgBuildingBuffer, options, this.getXPos(), this.getYPos(), getColor()); + } + + private void render(StringBuilder svgBuildingBuffer, RenderingOptions options, + int xPos, int yPos, Color color) { + Iterator i = StringUtils.createTextSplitter(TEXT_SPLITTING_REGEX, this.getText()); + int x = xPos; + while (i.hasNext()) { + String token = i.next(); + if (isTeXFormula(token)) + x += renderTeXFormula(svgBuildingBuffer, options, + token, + x, yPos, color, + font.getSize()); + else + x += renderString(svgBuildingBuffer, options, + token, + x, yPos, color, + font); + } + } + + private boolean isTeXFormula(String text) { + return this.latexMathEnabled && text.startsWith("$"); + } + /** * @return */ @@ -188,5 +273,50 @@ public void setOutlineColor(Color outlineColor) { this.outlineColor = outlineColor; } + public static int drawTeXFormula(Graphics2D g2, String text, int x, int y, Color color, float fontSize) { + TeXFormula formula = new TeXFormula(text); + TeXIcon icon = formula.new TeXIconBuilder() + .setStyle(TeXConstants.STYLE_DISPLAY) + .setSize(fontSize) + .build(); + /* 12 is a magic number to adjust vertical position */ + icon.paintIcon(new JLabel() {{ + setForeground(color); + }}, g2, x, y - 12); + return icon.getIconWidth(); + } + private static int drawString(Graphics2D g2, String text, int xPos, int yPos, Color color, Font font) { + g2.setColor(color); + g2.setFont(font); + g2.drawString(text, xPos, yPos); + return FontMeasurer.instance().getWidthFor(text, font); + } + + @SuppressWarnings("unused") + private static int renderTeXFormula(StringBuilder svgBuildingBuffer, RenderingOptions options, String text, int x, int yPos, Color color, int size) { + throw new UnsupportedOperationException("Rendering LaTeX formula in .svg format is not currently supported."); + } + + private static int renderString(StringBuilder svgBuildingBuffer, RenderingOptions options, String text, int xPos, int yPos, Color color, Font font) { + String TEXT_ELEMENT = " " + + "\n"; + /* Prefer normal font weight + if (font.isBold()) { + style = " font-weight='bold'"; + } + */ + + svgBuildingBuffer.append( + String.format(TEXT_ELEMENT, + xPos, + yPos, + options.getFontFamily(), + font.getSize(), + colorToHex(color), + text + ) + ); + return FontMeasurer.instance().getWidthFor(text, font); + } } diff --git a/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java b/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java index 83c1031..2247620 100644 --- a/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java +++ b/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java @@ -5,7 +5,6 @@ import org.stathissideris.ascii2image.core.ShapeAreaComparator; import java.awt.Color; -import java.awt.Font; import java.awt.geom.GeneralPath; import java.awt.geom.PathIterator; import java.util.ArrayList; @@ -289,56 +288,10 @@ private void backgroundLayer() { } private void renderTexts() { - - for (DiagramText diagramText : diagram.getTextObjects()) { - - Font font = diagramText.getFont(); - String text = diagramText.getText(); - - int xPos = diagramText.getXPos(); - int yPos = diagramText.getYPos(); - - renderText(text, xPos, yPos, font, diagramText.getColor()); - - if (diagramText.hasOutline()) { - - Color outlineColor = diagramText.getOutlineColor(); - - renderText(text, xPos + 1, yPos, font, outlineColor); - renderText(text, xPos - 1, yPos, font, outlineColor); - renderText(text, xPos, yPos + 1, font, outlineColor); - renderText(text, xPos, yPos - 1, font, outlineColor); - - } - } - - } - - private void renderText(String text, int xPos, int yPos, Font font, Color color) { - - String TEXT_ELEMENT = " " + - "\n"; - - /* Prefer normal font weight - if (font.isBold()) { - style = " font-weight='bold'"; - } - */ - - layer3.append( - String.format(TEXT_ELEMENT, - xPos, - yPos, - options.getFontFamily(), - font.getSize(), - colorToHex(color), - text - ) - ); - + diagram.getTextObjects().forEach(each -> each.renderOn(this.layer3, this.options)); } - private static String colorToHex(Color color) { + public static String colorToHex(Color color) { return String.format("#%s%s%s", toHex(color.getRed()), toHex(color.getGreen()), diff --git a/src/java/org/stathissideris/ascii2image/text/StringUtils.java b/src/java/org/stathissideris/ascii2image/text/StringUtils.java index 9a073b6..7f37c70 100644 --- a/src/java/org/stathissideris/ascii2image/text/StringUtils.java +++ b/src/java/org/stathissideris/ascii2image/text/StringUtils.java @@ -1,23 +1,28 @@ /** * ditaa - Diagrams Through Ascii Art - * + *

* Copyright (C) 2004-2011 Efstathios Sideris - * + *

* ditaa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. - * + *

* ditaa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. - * + *

* You should have received a copy of the GNU Lesser General Public * License along with ditaa. If not, see . */ package org.stathissideris.ascii2image.text; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * @author sideris * @@ -61,7 +66,6 @@ public static boolean isBlank(String s) { } /** - * * Converts the first character of string into a capital letter * * @param string @@ -173,4 +177,27 @@ public static void main(String[] args) { } + + public static Iterator createTextSplitter(Pattern pattern, CharSequence s) { + return new Iterator() { + Pattern regex = pattern; + CharSequence rest = s; + + @Override + public boolean hasNext() { + return rest.length() > 0; + } + + @Override + public String next() { + Matcher m = regex.matcher(rest); + if (m.find()) { + String ret = m.group(1); + rest = rest.subSequence(ret.length(), rest.length()); + return ret; + } + throw new NoSuchElementException(); + } + }; + } } diff --git a/src/java/org/stathissideris/ascii2image/text/TextGrid.java b/src/java/org/stathissideris/ascii2image/text/TextGrid.java index 564a408..04ba75f 100644 --- a/src/java/org/stathissideris/ascii2image/text/TextGrid.java +++ b/src/java/org/stathissideris/ascii2image/text/TextGrid.java @@ -1,18 +1,18 @@ /** * ditaa - Diagrams Through Ascii Art - * + *

* Copyright (C) 2004-2011 Efstathios Sideris - * + *

* ditaa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. - * + *

* ditaa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. - * + *

* You should have received a copy of the GNU Lesser General Public * License along with ditaa. If not, see . */ @@ -32,20 +32,27 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Stack; +import java.util.function.IntUnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.stream.Collectors.toList; +import static org.stathissideris.ascii2image.text.StringUtils.createTextSplitter; + /** * * @author Efstathios Sideris */ -public class TextGrid { - - private static final boolean DEBUG = false; +public class TextGrid{ + private static final boolean DEBUG = false; + private static final char PLAIN_MODE = 'P'; + public static final char LATEX_MODE = 'L'; private ArrayList rows; + private List modeRows; private static char[] boundaries = { '/', '\\', '|', '-', '*', '=', ':' }; private static char[] undisputableBoundaries = { '|', '-', '*', '=', ':' }; @@ -91,6 +98,48 @@ public class TextGrid { markupTags.add("o"); } + private void updateModeRows() { + // Collectors.toList creates ArrayList and you see no performance penalty + // caused by using LinkedList here. + this.modeRows = this.rows.stream().map(TextGrid::transformRowToModeRow).collect(toList()); + } + + public static String transformRowToModeRow(CharSequence row) { + StringBuilder b = new StringBuilder(); + row.chars().map(new IntUnaryOperator() { + /** + * This field hold current mode of a cell in the grid. + * Only 2 values can be assigned, which are 'P' and 'L'. + * They respectively represent 'Plain', which is normal ditaa mode, and 'LaTeX', which is LaTeX + * formula mode. + */ + int cur = PLAIN_MODE; + + @Override + public int applyAsInt(int operand) { + if (cur == PLAIN_MODE) { + if (operand == '$') { + cur = LATEX_MODE; + return LATEX_MODE; + } + return PLAIN_MODE; + } else if (cur == LATEX_MODE) { + if (operand == '$') { + cur = PLAIN_MODE; + return LATEX_MODE; + } + return this.cur; + } + throw new IllegalStateException(); + } + }).forEach(c -> b.append((char) c)); + String ret = b.toString(); + if (ret.endsWith("L")) + if (row.charAt(row.length() - 1) != '$') + throw new IllegalArgumentException("LaTex mode was started but not finished."); + return ret; + } + public void addToMarkupTags(Collection tags) { markupTags.addAll(tags); } @@ -113,6 +162,7 @@ public static void main(String[] args) throws Exception { public TextGrid() { rows = new ArrayList(); + this.updateModeRows(); } public TextGrid(int width, int height) { @@ -120,6 +170,7 @@ public TextGrid(int width, int height) { rows = new ArrayList(); for (int i = 0; i < height; i++) rows.add(new StringBuilder(space)); + this.updateModeRows(); } public static TextGrid makeSameSizeAs(TextGrid grid) { @@ -132,6 +183,7 @@ public TextGrid(TextGrid otherGrid) { for (StringBuilder row : otherGrid.getRows()) { rows.add(new StringBuilder(row)); } + this.updateModeRows(); } public void clear() { @@ -373,7 +425,7 @@ public void replacePointMarkersOnLine() { char c = get(xi, yi); Cell cell = new Cell(xi, yi); if (StringUtils.isOneOf(c, pointMarkers) - && isStarOnLine(cell)) { + && isStarOnLine(cell) && isInPlainMode(cell)) { boolean isOnHorizontalLine = false; if (StringUtils.isOneOf(get(cell.getEast()), horizontalLines)) @@ -411,6 +463,8 @@ public CellSet getPointMarkersOnLine() { int height = getHeight(); for (int yi = 0; yi < height; yi++) { for (int xi = 0; xi < width; xi++) { + if (!isInPlainMode(xi, yi)) + continue; char c = get(xi, yi); if (StringUtils.isOneOf(c, pointMarkers) && isStarOnLine(new Cell(xi, yi))) { @@ -424,21 +478,30 @@ && isStarOnLine(new Cell(xi, yi))) { public void replaceHumanColorCodes() { int height = getHeight(); - for (int y = 0; y < height; y++) { - String row = rows.get(y).toString(); - Iterator it = humanColorCodes.keySet().iterator(); - while (it.hasNext()) { - String humanCode = (String) it.next(); - String hexCode = (String) humanColorCodes.get(humanCode); - if (hexCode != null) { - humanCode = "c" + humanCode; - hexCode = "c" + hexCode; - row = row.replaceAll(humanCode, hexCode); - rows.set(y, new StringBuilder(row)); //TODO: this is not the most efficient way to do this - row = rows.get(y).toString(); - } - } + for (int y = 0; y < height; y++) + rows.set(y, replaceHumanColorCodes(y, rows.get(y))); + } + + private StringBuilder replaceHumanColorCodes(int rowIndex, StringBuilder in) { + Pattern p = Pattern.compile("(c.{0,3}|[^c]+)"); + StringBuilder ret = new StringBuilder(in.length()); + Iterator i = createTextSplitter(p, in); + while (i.hasNext()) { + String next = i.next(); + if (isInPlainMode(ret.length(), rowIndex) && looksColorCode(next)) { + String replacement = humanColorCodes.get(next.substring(1, 4)); + if (replacement != null) + ret.append(String.format("c%s", replacement)); + else + ret.append(next); + } else + ret.append(next); } + return ret; + } + + private static boolean looksColorCode(String word) { + return word.length() >= 4 && word.startsWith("c"); } @@ -664,7 +727,8 @@ public void removeBoundaries() { Iterator it = toBeRemoved.iterator(); while (it.hasNext()) { Cell cell = (Cell) it.next(); - set(cell, ' '); + if (isInPlainMode(cell)) + set(cell, ' '); } } @@ -693,6 +757,8 @@ public ArrayList findColorCodes() { for (int yi = 0; yi < height; yi++) { for (int xi = 0; xi < width - 3; xi++) { Cell cell = new Cell(xi, yi); + if (!isInPlainMode(cell)) + continue; String s = getStringAt(cell, 4); Matcher matcher = colorCodePattern.matcher(s); if (matcher.matches()) { @@ -719,6 +785,8 @@ public ArrayList findMarkupTags() { int height = getHeight(); for (int y = 0; y < height; y++) { for (int x = 0; x < width - 3; x++) { + if (!isInPlainMode(x, y)) + continue; Cell cell = new Cell(x, y); char c = get(cell); if (c == '{') { @@ -798,6 +866,8 @@ public boolean isBoundary(int x, int y) { } public boolean isBoundary(Cell cell) { + if (!isInPlainMode(cell)) + return false; char c = get(cell.x, cell.y); if (0 == c) return false; @@ -828,14 +898,14 @@ public static boolean isHorizontalLine(char c) { } public boolean isHorizontalLine(Cell cell) { - return isHorizontalLine(cell.x, cell.y); + return isHorizontalLine(cell.x, cell.y) && isInPlainMode(cell); } public boolean isHorizontalLine(int x, int y) { char c = get(x, y); if (0 == c) return false; - return StringUtils.isOneOf(c, horizontalLines); + return StringUtils.isOneOf(c, horizontalLines) && isInPlainMode(x, y); } public static boolean isVerticalLine(char c) { @@ -843,7 +913,7 @@ public static boolean isVerticalLine(char c) { } public boolean isVerticalLine(Cell cell) { - return isVerticalLine(cell.x, cell.y); + return isVerticalLine(cell.x, cell.y) && isInPlainMode(cell); } public boolean isVerticalLine(int x, int y) { @@ -864,15 +934,15 @@ public boolean isLinesEnd(int x, int y) { * @return */ public boolean isLinesEnd(Cell cell) { - return matchesAny(cell, GridPatternGroup.linesEndCriteria); + return matchesAny(cell, GridPatternGroup.linesEndCriteria) && isInPlainMode(cell); } public boolean isVerticalLinesEnd(Cell cell) { - return matchesAny(cell, GridPatternGroup.verticalLinesEndCriteria); + return matchesAny(cell, GridPatternGroup.verticalLinesEndCriteria) && isInPlainMode(cell); } public boolean isHorizontalLinesEnd(Cell cell) { - return matchesAny(cell, GridPatternGroup.horizontalLinesEndCriteria); + return matchesAny(cell, GridPatternGroup.horizontalLinesEndCriteria) && isInPlainMode(cell); } @@ -881,7 +951,7 @@ public boolean isPointCell(Cell cell) { isCorner(cell) || isIntersection(cell) || isStub(cell) - || isLinesEnd(cell)); + || isLinesEnd(cell)) && isInPlainMode(cell); } @@ -964,20 +1034,21 @@ public boolean isArrowhead(Cell cell) { } public boolean isNorthArrowhead(Cell cell) { - return get(cell) == '^'; + return get(cell) == '^' && isInPlainMode(cell); } public boolean isEastArrowhead(Cell cell) { - return get(cell) == '>'; + return get(cell) == '>' && isInPlainMode(cell); } public boolean isWestArrowhead(Cell cell) { - return get(cell) == '<'; + return get(cell) == '<' && isInPlainMode(cell); } public boolean isSouthArrowhead(Cell cell) { return (get(cell) == 'v' || get(cell) == 'V') - && isVerticalLine(cell.getNorth()); + && isVerticalLine(cell.getNorth()) + && isInPlainMode(cell); } // unicode for bullets @@ -1000,7 +1071,8 @@ public boolean isBullet(Cell cell) { if ((c == 'o' || c == '*') && isBlank(cell.getEast()) && isBlank(cell.getWest()) - && Character.isLetterOrDigit(get(cell.getEast().getEast()))) + && Character.isLetterOrDigit(get(cell.getEast().getEast())) + && isInPlainMode(cell)) return true; return false; } @@ -1716,68 +1788,73 @@ public boolean initialiseWithLines(ArrayList lines, ProcessingOpt done = true; } rows = new ArrayList(lines.subList(0, i + 2)); + this.updateModeRows(); + try { - if (options != null) - fixTabs(options.getTabSize()); - else - fixTabs(options.DEFAULT_TAB_SIZE); + if (options != null) + fixTabs(options.getTabSize()); + else + fixTabs(options.DEFAULT_TAB_SIZE); - // make all lines of equal length - // add blank outline around the buffer to prevent fill glitch - // convert tabs to spaces (or remove them if setting is 0) + // make all lines of equal length + // add blank outline around the buffer to prevent fill glitch + // convert tabs to spaces (or remove them if setting is 0) - int blankBorderSize = 2; + int blankBorderSize = 2; - int maxLength = 0; - int index = 0; + int maxLength = 0; + int index = 0; - String encoding = null; - if (options != null) - encoding = options.getCharacterEncoding(); + String encoding = null; + if (options != null) + encoding = options.getCharacterEncoding(); - Iterator it = rows.iterator(); - while (it.hasNext()) { - String row = it.next().toString(); - if (encoding != null) { - byte[] bytes = row.getBytes(); - row = new String(bytes, encoding); + Iterator it = rows.iterator(); + while (it.hasNext()) { + String row = it.next().toString(); + if (encoding != null) { + byte[] bytes = row.getBytes(); + row = new String(bytes, encoding); + } + if (row.length() > maxLength) + maxLength = row.length(); + rows.set(index, new StringBuilder(row)); + index++; } - if (row.length() > maxLength) - maxLength = row.length(); - rows.set(index, new StringBuilder(row)); - index++; - } - it = rows.iterator(); - ArrayList newRows = new ArrayList(); - //TODO: make the following depend on blankBorderSize + it = rows.iterator(); + ArrayList newRows = new ArrayList(); + //TODO: make the following depend on blankBorderSize - StringBuilder topBottomRow = - new StringBuilder(StringUtils.repeatString(" ", maxLength + blankBorderSize * 2)); + StringBuilder topBottomRow = + new StringBuilder(StringUtils.repeatString(" ", maxLength + blankBorderSize * 2)); - newRows.add(topBottomRow); - newRows.add(topBottomRow); - while (it.hasNext()) { - StringBuilder row = it.next(); + newRows.add(topBottomRow); + newRows.add(topBottomRow); + while (it.hasNext()) { + StringBuilder row = it.next(); - if (row.length() < maxLength) { - String borderString = StringUtils.repeatString(" ", blankBorderSize); - StringBuilder newRow = new StringBuilder(); + if (row.length() < maxLength) { + String borderString = StringUtils.repeatString(" ", blankBorderSize); + StringBuilder newRow = new StringBuilder(); - newRow.append(borderString); - newRow.append(row); - newRow.append(StringUtils.repeatString(" ", maxLength - row.length())); - newRow.append(borderString); + newRow.append(borderString); + newRow.append(row); + newRow.append(StringUtils.repeatString(" ", maxLength - row.length())); + newRow.append(borderString); - newRows.add(newRow); - } else { //TODO: why is the following line like that? - newRows.add(new StringBuilder(" ").append(row).append(" ")); + newRows.add(newRow); + } else { //TODO: why is the following line like that? + newRows.add(new StringBuilder(" ").append(row).append(" ")); + } } + //TODO: make the following depend on blankBorderSize + newRows.add(topBottomRow); + newRows.add(topBottomRow); + rows = newRows; + } finally { + this.updateModeRows(); } - //TODO: make the following depend on blankBorderSize - newRows.add(topBottomRow); - newRows.add(topBottomRow); - rows = newRows; replaceBullets(); replaceHumanColorCodes(); @@ -1820,6 +1897,14 @@ protected ArrayList getRows() { return rows; } + private boolean isInPlainMode(Cell cell) { + return this.modeRows.get(cell.y).charAt(cell.x) == PLAIN_MODE; + } + + private boolean isInPlainMode(int x, int y) { + return this.modeRows.get(y).charAt(x) == PLAIN_MODE; + } + public class CellColorPair { public CellColorPair(Cell cell, Color color) { this.cell = cell; diff --git a/test-resources/text/art-latexmath-1.txt b/test-resources/text/art-latexmath-1.txt new file mode 100644 index 0000000..f1f2a58 --- /dev/null +++ b/test-resources/text/art-latexmath-1.txt @@ -0,0 +1,38 @@ + +$Box_1$ $Box^2$ ++---------------------+ +------+ /---------\ +|$\sum_{i=0}^{n}x^i$ | |$cBLU$| | | +| +--->|cRED +-=-+cGRE$C_k$| +|{io} | |cXYZ | |{o} | ++----------+----------+ +---+--+ \---------/ + | | + | : + | V + | +-------------------+ + +---------->*$A_i$ hello $B^i$ | + | +----+ + | |c8FA| + +--------------+----+ + +$|Set| = o-*-Freunde-*-nicht=*=diese-=-*- * töne$ + +o Quick brown fox jumps over +* a lazy dog. + +$Q_u^i$, $C_k$, $B_r^{own}$, $F_{ox}$ jumps + +over a lazy $d\cdot\frac{o}{g}$. + + +$\forall x \in X, \quad \exists y \leq \epsilon$ + + +$\sin A \cos B =$ + + $ \frac{1}{2}\left[ \sin(A-B)+\sin(A+B) \right]$ + + +$\frac{d}{dx}\left( \int_{0}^{x} f(u)\,du\right)=f(x).$ + + + $v \sim \mathcal{N} (m,\sigma^2)$ \ No newline at end of file diff --git a/test/java/sandbox/Sandbox.java b/test/java/sandbox/Sandbox.java new file mode 100644 index 0000000..1bb98e2 --- /dev/null +++ b/test/java/sandbox/Sandbox.java @@ -0,0 +1,145 @@ +package sandbox; + +import org.junit.Test; +import org.scilab.forge.jlatexmath.TeXConstants; +import org.scilab.forge.jlatexmath.TeXFormula; +import org.scilab.forge.jlatexmath.TeXIcon; +import org.stathissideris.ascii2image.graphics.DiagramText; +import org.stathissideris.ascii2image.text.StringUtils; +import org.stathissideris.ascii2image.text.TextGrid; + +import javax.imageio.ImageIO; +import javax.swing.JLabel; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.io.File; +import java.io.IOException; +import java.util.Iterator; + +import static com.github.dakusui.crest.Crest.asString; +import static com.github.dakusui.crest.Crest.assertThat; + +public class Sandbox { + @Test + public void latexMath() throws IOException { + String latex = "$\\sum_{i=0}^{n}x^i$"; + TeXFormula formula = new TeXFormula(latex); + + // Note: Old interface for creating icons: + // TeXIcon icon = formula.createTeXIcon(TeXConstants.STYLE_DISPLAY, 20); + // Note: New interface using builder pattern (inner class): + TeXIcon icon = formula.new TeXIconBuilder().setStyle(TeXConstants.STYLE_DISPLAY).setSize(20) + .build(); + + icon.setInsets(new Insets(5, 5, 5, 5)); + + BufferedImage image = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = image.createGraphics(); + g2.setColor(Color.white); + g2.fillRect(0, 0, icon.getIconWidth(), icon.getIconHeight()); + JLabel jl = new JLabel(); + jl.setForeground(new Color(0, 0, 0)); + icon.paintIcon(jl, g2, 0, 0); + File file = new File("target/Example1.png"); + ImageIO.write(image, "png", file.getAbsoluteFile()); + } + + @Test + public void latexMath2() throws IOException { + String latex = "$\\sum_{i=0}^{n}x^i$"; + + BufferedImage image = new BufferedImage( + 640, 480, + BufferedImage.TYPE_INT_ARGB + ); + DiagramText.drawTeXFormula(image.createGraphics(), latex, 0, 0, Color.black, 20); + File file = new File("target/Example1.png"); + ImageIO.write(image, "png", file.getAbsoluteFile()); + } + + /** + * This method is based on a discussion How to compare images for similarity made in the Stackoverflow.com + * If 2 images are identical, this method will return 1.0. + * + * @param fileA input image A + * @param fileB input image B + * @return The similarity. + */ + public static double compareImages(File fileA, File fileB) { + double percentage = 0; + try { + // take buffer data from both image files // + BufferedImage biA = ImageIO.read(fileA); + DataBuffer dbA = biA.getData().getDataBuffer(); + int sizeA = dbA.getSize(); + BufferedImage biB = ImageIO.read(fileB); + DataBuffer dbB = biB.getData().getDataBuffer(); + int sizeB = dbB.getSize(); + int count = 0; + // compare data-buffer objects // + if (sizeA == sizeB) { + + for (int i = 0; i < sizeA; i++) { + + if (dbA.getElem(i) == dbB.getElem(i)) { + count = count + 1; + } + + } + percentage = ((double) count * 100) / sizeA; + } else { + System.out.println("Both the images are not of same size"); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + return percentage; + } + + + @Test + public void givenStringContainingLaTeXFormula4whenTransform$thenResultIsCorrect() { + assertThat( + TextGrid.transformRowToModeRow("xyz$xyz^^^$xyz"), + asString().equalTo("PPPLLLLLLLLPPP").$() + ); + } + + @Test(expected = IllegalArgumentException.class) + public void givenStringContainingUnfinishedLaTeXFormula$whenTransform$thenIllegalArgumentException() { + TextGrid.transformRowToModeRow("xyz$xyz^^^_xyz"); + } + + @Test + public void givenStringNotContainingLaTeXFormula$whenTransform$thenResultIsCorrect() { + assertThat( + TextGrid.transformRowToModeRow("xyz_xyz^^^_xyz"), + asString().equalTo("PPPPPPPPPPPPPP").$() + ); + } + + @Test + public void givenEmptyString$whenTransform$thenResultIsEmpty() { + assertThat( + TextGrid.transformRowToModeRow(""), + asString().equalTo("").$() + ); + } + + + @Test + public void testSplit() { + String s = "hello$HELLO$ world"; + Iterator i = StringUtils.createTextSplitter(DiagramText.TEXT_SPLITTING_REGEX, s); + + while (i.hasNext()) + System.out.println(i.next()); + } + +} From 155f7ec6476b235a69f26c835e8598ce86c9edae Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Mon, 29 Oct 2018 22:38:54 +0900 Subject: [PATCH 02/17] Add a method to do rendering asciiart containing LaTeX math formulae. --- test/java/sandbox/Sandbox.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/java/sandbox/Sandbox.java b/test/java/sandbox/Sandbox.java index 1bb98e2..49b190e 100644 --- a/test/java/sandbox/Sandbox.java +++ b/test/java/sandbox/Sandbox.java @@ -4,6 +4,7 @@ import org.scilab.forge.jlatexmath.TeXConstants; import org.scilab.forge.jlatexmath.TeXFormula; import org.scilab.forge.jlatexmath.TeXIcon; +import org.stathissideris.ascii2image.core.CommandLineConverter; import org.stathissideris.ascii2image.graphics.DiagramText; import org.stathissideris.ascii2image.text.StringUtils; import org.stathissideris.ascii2image.text.TextGrid; @@ -142,4 +143,8 @@ public void testSplit() { System.out.println(i.next()); } + @Test + public void renderLatexAsciiArt() { + CommandLineConverter.main(new String[] { "tests/text/art-latexmath-1.txt", "-o", "--latex-math" }); + } } From 61a46ff4f9a501f31dc508b7106df2bb29d4c964 Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Mon, 29 Oct 2018 22:45:33 +0900 Subject: [PATCH 03/17] Remove unintentionally introduced

element in Javadoc. --- .../stathissideris/ascii2image/graphics/DiagramText.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java b/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java index 58675aa..f4a0c42 100644 --- a/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java +++ b/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java @@ -1,18 +1,18 @@ /** * ditaa - Diagrams Through Ascii Art - *

+ * * Copyright (C) 2004-2011 Efstathios Sideris - *

+ * * ditaa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. - *

+ * * ditaa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. - *

+ * * You should have received a copy of the GNU Lesser General Public * License along with ditaa. If not, see . */ From d6a9a28687146283d13a95532e2707bfa53d83fe Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Mon, 29 Oct 2018 22:57:08 +0900 Subject: [PATCH 04/17] Remove unintentionally introduced

element in Javadoc. --- .../org/stathissideris/ascii2image/text/StringUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/java/org/stathissideris/ascii2image/text/StringUtils.java b/src/java/org/stathissideris/ascii2image/text/StringUtils.java index 7f37c70..570c4f8 100644 --- a/src/java/org/stathissideris/ascii2image/text/StringUtils.java +++ b/src/java/org/stathissideris/ascii2image/text/StringUtils.java @@ -1,18 +1,18 @@ /** * ditaa - Diagrams Through Ascii Art - *

+ * * Copyright (C) 2004-2011 Efstathios Sideris - *

+ * * ditaa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. - *

+ * * ditaa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. - *

+ * * You should have received a copy of the GNU Lesser General Public * License along with ditaa. If not, see . */ From 49601061c7f65ac643e04487c2bb01dcb2e31619 Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Thu, 1 Nov 2018 07:57:14 +0900 Subject: [PATCH 05/17] Add a mechanism to do assertion for images. --- test/java/sandbox/Sandbox.java | 74 ++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/test/java/sandbox/Sandbox.java b/test/java/sandbox/Sandbox.java index 49b190e..351da3b 100644 --- a/test/java/sandbox/Sandbox.java +++ b/test/java/sandbox/Sandbox.java @@ -1,5 +1,6 @@ package sandbox; +import com.github.dakusui.crest.Crest; import org.junit.Test; import org.scilab.forge.jlatexmath.TeXConstants; import org.scilab.forge.jlatexmath.TeXFormula; @@ -19,9 +20,13 @@ import java.io.File; import java.io.IOException; import java.util.Iterator; +import java.util.function.Predicate; +import static com.github.dakusui.crest.Crest.asObject; import static com.github.dakusui.crest.Crest.asString; import static com.github.dakusui.crest.Crest.assertThat; +import static com.github.dakusui.crest.Crest.callOn; +import static java.lang.String.format; public class Sandbox { @Test @@ -91,7 +96,7 @@ public static double compareImages(File fileA, File fileB) { } } - percentage = ((double) count * 100) / sizeA; + percentage = ((double) count) / sizeA; } else { System.out.println("Both the images are not of same size"); } @@ -144,7 +149,70 @@ public void testSplit() { } @Test - public void renderLatexAsciiArt() { + public void renderAsLatexAsciiArt() { CommandLineConverter.main(new String[] { "tests/text/art-latexmath-1.txt", "-o", "--latex-math" }); } -} + + @Test + public void renderAsNonLatexAsciiArt() { + CommandLineConverter.main(new String[] { "tests/text/art-latexmath-1.txt", "-o" }); + } + + public static BufferedImage getDifferenceImage(BufferedImage img1, BufferedImage img2) { + // convert images to pixel arrays... + final int w = img1.getWidth(), + h = img1.getHeight(), + highlight = Color.MAGENTA.getRGB(); + final int[] p1 = img1.getRGB(0, 0, w, h, null, 0, w); + final int[] p2 = img2.getRGB(0, 0, w, h, null, 0, w); + // compare img1 to img2, pixel by pixel. If different, highlight img1's pixel... + for (int i = 0; i < p1.length; i++) { + if (p1[i] != p2[i]) { + p1[i] = highlight; + } + } + // save img1's pixels to a new BufferedImage, and return it... + // (May require TYPE_INT_ARGB) + final BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + out.setRGB(0, 0, w, h, p1, 0, w); + return out; + } + + public static File getDifferenceImageFile(File img1, File img2) throws Throwable { + File ret; + ImageIO.write( + getDifferenceImage( + ImageIO.read(img1), + ImageIO.read(img2)), + "png", + ret = new File("output.png")); + return ret; + } + + @Test + public void testDiffImage() throws IOException { + ImageIO.write( + getDifferenceImage( + ImageIO.read(new File("tests/text/art-latexmath-1.png")), + ImageIO.read(new File("tests/text/art-latexmath-2.png"))), + "png", + new File("output.png")); + } + + @Test + public void testImages() throws Throwable { + File actual = new File("tests/text/art-latexmath-1.png"); + File expected = new File("tests/text/art-latexmath-2.png"); + assertThat( + actual, + asObject().check( + callOn(Sandbox.class, "getDifferenceImageFile", expected, actual).$(), similarImpageTo(expected, 0.999999)).$() + ); + } + + private Predicate similarImpageTo(File expected, double threshold) { + return Crest.predicate( + format("similarImageTo(%s,%s)", expected, threshold), + actual -> compareImages(expected, actual) > threshold); + } +} \ No newline at end of file From ca76be031c72b889c5d2d9e064d680eca8c8cee5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Fri, 2 Nov 2018 08:01:43 +0900 Subject: [PATCH 06/17] WIP: implement utility methods for testing. --- .gitignore | 1 + test/java/sandbox/Sandbox.java | 104 +++++++++++++++++++++------------ 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 1194eb3..45a0329 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ tests/text/ tests/build.xml target/ /.lein-failures +/testImages.png diff --git a/test/java/sandbox/Sandbox.java b/test/java/sandbox/Sandbox.java index 351da3b..415c624 100644 --- a/test/java/sandbox/Sandbox.java +++ b/test/java/sandbox/Sandbox.java @@ -1,7 +1,10 @@ package sandbox; import com.github.dakusui.crest.Crest; +import com.github.dakusui.crest.utils.printable.Functions; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestName; import org.scilab.forge.jlatexmath.TeXConstants; import org.scilab.forge.jlatexmath.TeXFormula; import org.scilab.forge.jlatexmath.TeXIcon; @@ -20,6 +23,7 @@ import java.io.File; import java.io.IOException; import java.util.Iterator; +import java.util.function.Function; import java.util.function.Predicate; import static com.github.dakusui.crest.Crest.asObject; @@ -29,6 +33,9 @@ import static java.lang.String.format; public class Sandbox { + @Rule + public TestName name = new TestName(); + @Test public void latexMath() throws IOException { String latex = "$\\sum_{i=0}^{n}x^i$"; @@ -50,7 +57,7 @@ public void latexMath() throws IOException { JLabel jl = new JLabel(); jl.setForeground(new Color(0, 0, 0)); icon.paintIcon(jl, g2, 0, 0); - File file = new File("target/Example1.png"); + File file = createFile("target/Example1.png"); ImageIO.write(image, "png", file.getAbsoluteFile()); } @@ -158,7 +165,58 @@ public void renderAsNonLatexAsciiArt() { CommandLineConverter.main(new String[] { "tests/text/art-latexmath-1.txt", "-o" }); } - public static BufferedImage getDifferenceImage(BufferedImage img1, BufferedImage img2) { + @Test + public void testDiffImage() throws IOException { + ImageIO.write( + getDifferenceImage( + ImageIO.read(createFile("tests/text/art-latexmath-1.png")), + ImageIO.read(createFile("tests/text/art-latexmath-2.png"))), + "png", + createFile("output.png")); + } + + @Test + public void testImages() { + File actual = createFile("tests/text/art-latexmath-1.png"); + File expected = createFile("tests/text/art-latexmath-2.png"); + assertThat( + actual, + asObject().check( + callOn(Sandbox.class, "imageDiff", expected, Functions.THIS, this.name).$(), + similarImpageTo(expected, 0.999999)).$()); + } + + private Function imageDiffWith(File expected, String out) { + return Crest.function( + String.format("imageDiffWith[%s, out:%s]", expected, out), + actual -> { + File ret = new File(out); + try { + imageDiff(expected, (File) actual, ret); + } catch (IOException e) { + throw new RuntimeException(e); + } + return ret; + }); + } + + public static File imageDiff(File img1, File img2, TestName name) throws IOException { + File out = createFile(name.getMethodName()); + ImageIO.write(getDifferenceImage(ImageIO.read(img1), ImageIO.read(img2)), "png", out); + return out; + } + + private static void imageDiff(File img1, File img2, File out) throws IOException { + ImageIO.write(getDifferenceImage(ImageIO.read(img1), ImageIO.read(img2)), "png", out); + } + + private Predicate similarImpageTo(File expected, double threshold) { + return Crest.predicate( + format("similarImageTo(%s,%s)", expected, threshold), + actual -> compareImages(expected, actual) > threshold); + } + + private static BufferedImage getDifferenceImage(BufferedImage img1, BufferedImage img2) { // convert images to pixel arrays... final int w = img1.getWidth(), h = img1.getHeight(), @@ -178,41 +236,11 @@ public static BufferedImage getDifferenceImage(BufferedImage img1, BufferedImage return out; } - public static File getDifferenceImageFile(File img1, File img2) throws Throwable { - File ret; - ImageIO.write( - getDifferenceImage( - ImageIO.read(img1), - ImageIO.read(img2)), - "png", - ret = new File("output.png")); - return ret; - } - - @Test - public void testDiffImage() throws IOException { - ImageIO.write( - getDifferenceImage( - ImageIO.read(new File("tests/text/art-latexmath-1.png")), - ImageIO.read(new File("tests/text/art-latexmath-2.png"))), - "png", - new File("output.png")); - } - - @Test - public void testImages() throws Throwable { - File actual = new File("tests/text/art-latexmath-1.png"); - File expected = new File("tests/text/art-latexmath-2.png"); - assertThat( - actual, - asObject().check( - callOn(Sandbox.class, "getDifferenceImageFile", expected, actual).$(), similarImpageTo(expected, 0.999999)).$() - ); - } - - private Predicate similarImpageTo(File expected, double threshold) { - return Crest.predicate( - format("similarImageTo(%s,%s)", expected, threshold), - actual -> compareImages(expected, actual) > threshold); + private static File createFile(String s) { + return new File(s) { + public String toString() { + return "file://" + getAbsolutePath(); + } + }; } } \ No newline at end of file From 9b9b2bb65dc6ba0d6243edaecf20d849b616e048 Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Sat, 3 Nov 2018 13:37:12 +0900 Subject: [PATCH 07/17] Add first tests --- .../text/latex/_example/expected.png | Bin 0 -> 30245 bytes test-resources/text/latex/_example/in.txt | 38 +++ .../text/latex/_example/options.txt | 1 + .../text/latex/all-default/expected.png | Bin 0 -> 30245 bytes test-resources/text/latex/all-default/in.txt | 38 +++ .../text/latex/all-default/options.txt | 0 test-resources/text/latex/all/expected.png | Bin 0 -> 26278 bytes test-resources/text/latex/all/in.txt | 38 +++ test-resources/text/latex/all/options.txt | 1 + .../ascii2image/test/latex/LaTeXModeTest.java | 37 +++ .../test/latex/LaTeXModeTestBase.java | 220 ++++++++++++++++++ .../test/latex/LaTeXModeTestExample.java | 14 ++ .../ascii2image/test/latex/TestBase.java | 56 +++++ test/java/sandbox/Sandbox.java | 86 ++----- 14 files changed, 457 insertions(+), 72 deletions(-) create mode 100644 test-resources/text/latex/_example/expected.png create mode 100644 test-resources/text/latex/_example/in.txt create mode 100644 test-resources/text/latex/_example/options.txt create mode 100644 test-resources/text/latex/all-default/expected.png create mode 100644 test-resources/text/latex/all-default/in.txt create mode 100644 test-resources/text/latex/all-default/options.txt create mode 100644 test-resources/text/latex/all/expected.png create mode 100644 test-resources/text/latex/all/in.txt create mode 100644 test-resources/text/latex/all/options.txt create mode 100644 test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTest.java create mode 100644 test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestBase.java create mode 100644 test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestExample.java create mode 100644 test/java/org/stathissideris/ascii2image/test/latex/TestBase.java diff --git a/test-resources/text/latex/_example/expected.png b/test-resources/text/latex/_example/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a0548cb6f08ce7da8054f1c20371159a07c38d61 GIT binary patch literal 30245 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 fU-g*(?4r?w&AEpLulL~fujf=0H5Dr5EQ0|cRED +-=-+cGRE$C_k$| +|{io} | |cXYZ | |{o} | ++----------+----------+ +---+--+ \---------/ + | | + | : + | V + | +-------------------+ + +---------->*$A_i$ hello $B^i$ | + | +----+ + | |c8FA| + +--------------+----+ + +$|Set| = o-*-Freunde-*-nicht=*=diese-=-*- * töne$ + +o Quick brown fox jumps over +* a lazy dog. + +$Q_u^i$, $C_k$, $B_r^{own}$, $F_{ox}$ jumps + +over a lazy $d\cdot\frac{o}{g}$. + + +$\forall x \in X, \quad \exists y \leq \epsilon$ + + +$\sin A \cos B =$ + + $ \frac{1}{2}\left[ \sin(A-B)+\sin(A+B) \right]$ + + +$\frac{d}{dx}\left( \int_{0}^{x} f(u)\,du\right)=f(x).$ + + + $v \sim \mathcal{N} (m,\sigma^2)$ \ No newline at end of file diff --git a/test-resources/text/latex/_example/options.txt b/test-resources/text/latex/_example/options.txt new file mode 100644 index 0000000..2db6cb4 --- /dev/null +++ b/test-resources/text/latex/_example/options.txt @@ -0,0 +1 @@ +--latex diff --git a/test-resources/text/latex/all-default/expected.png b/test-resources/text/latex/all-default/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a0548cb6f08ce7da8054f1c20371159a07c38d61 GIT binary patch literal 30245 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 fU-g*(?4r?w&AEpLulL~fujf=0H5Dr5EQ0|cRED +-=-+cGRE$C_k$| +|{io} | |cXYZ | |{o} | ++----------+----------+ +---+--+ \---------/ + | | + | : + | V + | +-------------------+ + +---------->*$A_i$ hello $B^i$ | + | +----+ + | |c8FA| + +--------------+----+ + +$|Set| = o-*-Freunde-*-nicht=*=diese-=-*- * töne$ + +o Quick brown fox jumps over +* a lazy dog. + +$Q_u^i$, $C_k$, $B_r^{own}$, $F_{ox}$ jumps + +over a lazy $d\cdot\frac{o}{g}$. + + +$\forall x \in X, \quad \exists y \leq \epsilon$ + + +$\sin A \cos B =$ + + $ \frac{1}{2}\left[ \sin(A-B)+\sin(A+B) \right]$ + + +$\frac{d}{dx}\left( \int_{0}^{x} f(u)\,du\right)=f(x).$ + + + $v \sim \mathcal{N} (m,\sigma^2)$ \ No newline at end of file diff --git a/test-resources/text/latex/all-default/options.txt b/test-resources/text/latex/all-default/options.txt new file mode 100644 index 0000000..e69de29 diff --git a/test-resources/text/latex/all/expected.png b/test-resources/text/latex/all/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..924f0985777e7a1a99151522f2b7e147128d5a7c GIT binary patch literal 26278 zcmb5WcRZZ!w=SMYBRUbCAc*Kuqekzd6M`s-h~6S0NOaLl^xlo05WNdRM1qMsjOde)m56oU`}u^ZVm{KVvfUJoj^#wXW-0*P0+TRXN=26xT0YxPYr5FRgLm0){I5 zr;LdK@A&*}EVyvtnT>+9gx0Ikl@$EPBu^X9Y(j(C(xU>U{mmm2Yo65>DY3=%3p(<0(-xGD?%U?DKs=SFCjmC_)y-CrJh+*Vv9s*7Z6uR-~gyiHRA+d7`7EKYjd2L`+;)Tif2evSNMP8!kpdLPDmk zrq(O_)aV7L-rnBH z$;te$nUrll-rheVV`G0!PKMzzGv|ebglum+TUc0F#(w_%IX3p@8oMdtR`gi4L)KuO z=ZTxRc#{}=Ms;;{dAYhLiywih9htHDkDfe!Ox5m>m#p&o_3QXA#uu@pBU;p`rE{FK@P>znZy|{-sX8PrO$Al$s#Owk z#C(uiapY7~%1A=ZoWjDM)>e9EW{b1a6D%yOA0Liosr{|TKC3wQ#dBX;P<{4{sn*QQ zjDdk+S|*vAO-k~rflL0oZo9#EVPSZS?2L?8Ci0VOYikv=OifMacGMQRgoN~+d#oz8 zsJTrd627i~Lklqbv5fcc>1=In^T#r0za{X@&(CX7lVcS<`Xozr=I!tA z54#B~EyJ6PjO-4stG#^$lar$(k1z|DvjjieclM zn#Hh?5M@=>5(MHdD{EWO?CNUhyVccI5k5?E?;=&wQIb2jgaR9C2{u+%nu)0h1||g+ z68H_V(mL;K~iD!WdC!7EkPBi=zK?K~FWvvYFF$*{OJ zkUK%JV;??zm^}RkBPB%nmdJq%;r3k@0!jad^H_rj`*+w;b2&f6|M>>N`AcpVWMx%V zIDPv*m;XTc6=CI?mnWk^Jj}aPlYdc3rDA&v3#qSmu^t-trJbNG@b-k1lSO`ZcPJJp zdkXF5k=Y82*yi>q_c5l1ABgXcqZW)t_Pf_E)rkwAB=jHgc9m%vK7E=K9X)VHWXhW? zfz3EN=k}=-6RzvPJtfNC`lL@b6O_Tp;qRWcPP2D8?xGWy7VYc&M;Sm z>&tQ8A6Ievy*?4IKf57gW<9h=Hs9>hLOpi<=14Q~WE+gO5B-TLUnWn{osDfTG2w*Y zC;D`Fg6!dEZ)*=es~GkuIFbq%<9+(;29MLHuX`$ChNl