diff --git a/.gitignore b/.gitignore index 65bb389..fd0a089 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,11 @@ build/testclasses/ releases/ tests/images/ tests/images-expected/ + +# pom.xml for ditaa is available at https://gist.github.com/dakusui/c6b7532cbaae0ed7317036c389d761f4 +pom.xml +# Files copied by mvn's build procedure +tests/text/ +tests/latex/ +tests/build.xml target/ diff --git a/README.md b/README.md index 711b77d..a7d03bd 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ No separation (with the `-E` option) chosen. If the overwrite option is selected, the image file is instead overwriten. + -L,--latex-math Enable LaTeX math mode. -r,--round-corners Causes all corners to be rendered as round corners. -S,--no-shadows Turns off the drop-shadow effect. @@ -358,6 +359,60 @@ must be a space before the 'o' as well as after it. See below: ![](https://rawgit.com/stathissideris/ditaa/master/doc/images/bullet.png) +#### LaTeX mode. +If you place LaTeX formulae inside 2 ```$```s, it will be rendered using [```jlatexmath```](https://github.com/opencollab/jlatexmath). That is, if you have a following input files. +```ditaa + +$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)$ +``` + +This will be rendered as follows. + +![art-latexmath-1](https://user-images.githubusercontent.com/529265/47648438-5d92be00-dbbd-11e8-90c0-e2aa1b4ee858.png) + +To enable this feature, you need to give ```ditaa``` an option ```-L``` or ```--latex```. + +##### Limitations + +* This feature is only available when you are generating ```.png``` files. [Issue-44](https://github.com/stathissideris/ditaa/issues/44) +* Generally, LaTeX formulae are rendered narrower than the widths they occupy in ascii arts. You will sometimes see blanks after your formulae, especially when they are complicated ones, and there is no workaround to adjust this as of now. To mitigate this, you need to wait for [Issue-34](https://github.com/stathissideris/ditaa/issues/34). + #### HTML mode When `ditaa` is run using the `--html` option, the input is an HTML diff --git a/project.clj b/project.clj index df0aefe..037328c 100644 --- a/project.clj +++ b/project.clj @@ -9,7 +9,9 @@ [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.apache.xmlgraphics/batik-bridge "1.9"] + [com.github.dakusui/thincrest "3.6.0"] + [org.scilab.forge/jlatexmath "1.0.7"]] :main org.stathissideris.ascii2image.core.CommandLineConverter :java-source-paths ["src/java"] :profiles {:dev {:dependencies [[junit/junit "4.12"]] 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..6453a70 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java
@@ -338,16 +338,7 @@ 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);
     }
 
     if (options.renderDebugLines() || DEBUG_LINES) {
@@ -387,20 +378,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..f4a0c42 100644 --- a/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java +++ b/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java @@ -18,17 +18,31 @@ */ 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..570c4f8 100644 --- a/src/java/org/stathissideris/ascii2image/text/StringUtils.java +++ b/src/java/org/stathissideris/ascii2image/text/StringUtils.java @@ -18,6 +18,11 @@ */ 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..bf1ee2b 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; + private static final boolean DEBUG = false; + private static final char PLAIN_MODE = 'P'; + public static final char LATEX_MODE = 'L'; + public static final Pattern COLORCODELIKE_REGEX = Pattern.compile("(c.{0,3}|[^c]+)"); 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() { @@ -336,7 +388,6 @@ public boolean add(TextGrid grid) { * Replaces letters or numbers that are on horizontal or vertical * lines, with the appropriate character that will make the line * continuous (| for vertical and - for horizontal lines) - * */ public void replaceTypeOnLine() { int width = getWidth(); @@ -373,7 +424,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 +462,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 +477,29 @@ && 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) { + StringBuilder ret = new StringBuilder(in.length()); + Iterator i = createTextSplitter(COLORCODELIKE_REGEX, 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 +725,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 +755,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 +783,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 +864,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 +896,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 +911,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 +932,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 +949,7 @@ public boolean isPointCell(Cell cell) { isCorner(cell) || isIntersection(cell) || isStub(cell) - || isLinesEnd(cell)); + || isLinesEnd(cell)) && isInPlainMode(cell); } @@ -909,7 +977,6 @@ public boolean exactlyOneNeighbourIsBoundary(Cell cell) { } /** - * * A stub looks like that: * *

@@ -964,20 +1031,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 +1068,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;
   }
@@ -1112,7 +1181,7 @@ public CellSet followIntersection(Cell cell, Cell blocked) {
 
   /**
    * Returns the neighbours of a line-cell that are boundaries
-   *  (0 to 2 cells are returned)
+   * (0 to 2 cells are returned)
    *
    * @param cell
    * @return null if the cell is not a line
@@ -1448,7 +1517,6 @@ public void fillCellsWith(Iterable cells, char c) {
   }
 
   /**
-   *
    * Fills the continuous area with if c1 characters with c2,
    * flooding from cell x, y
    *
@@ -1550,7 +1618,6 @@ private CellSet seedFillOld(Cell seed, char newChar) {
 
 
   /**
-   *
    * Locates and returns the '*' boundaries that we would
    * encounter if we did a flood-fill at seed.
    *
@@ -1716,68 +1783,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 +1892,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/latex/_brokenmath/expected.png b/test-resources/latex/_brokenmath/expected.png
new file mode 100644
index 0000000..78c1c00
Binary files /dev/null and b/test-resources/latex/_brokenmath/expected.png differ
diff --git a/test-resources/latex/_brokenmath/in.txt b/test-resources/latex/_brokenmath/in.txt
new file mode 100644
index 0000000..a2c97ce
--- /dev/null
+++ b/test-resources/latex/_brokenmath/in.txt
@@ -0,0 +1,3 @@
++------------------+
+|$\unknownFunction$|
++------------------+
\ No newline at end of file
diff --git a/test-resources/latex/_brokenmath/options.txt b/test-resources/latex/_brokenmath/options.txt
new file mode 100644
index 0000000..2db6cb4
--- /dev/null
+++ b/test-resources/latex/_brokenmath/options.txt
@@ -0,0 +1 @@
+--latex
diff --git a/test-resources/latex/_example/expected.png b/test-resources/latex/_example/expected.png
new file mode 100644
index 0000000..a0548cb
Binary files /dev/null and b/test-resources/latex/_example/expected.png differ
diff --git a/test-resources/latex/_example/in.txt b/test-resources/latex/_example/in.txt
new file mode 100644
index 0000000..f1f2a58
--- /dev/null
+++ b/test-resources/latex/_example/in.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-resources/latex/_example/options.txt b/test-resources/latex/_example/options.txt
new file mode 100644
index 0000000..2db6cb4
--- /dev/null
+++ b/test-resources/latex/_example/options.txt
@@ -0,0 +1 @@
+--latex
diff --git a/test-resources/latex/all-default/expected.png b/test-resources/latex/all-default/expected.png
new file mode 100644
index 0000000..a0548cb
Binary files /dev/null and b/test-resources/latex/all-default/expected.png differ
diff --git a/test-resources/latex/all-default/in.txt b/test-resources/latex/all-default/in.txt
new file mode 100644
index 0000000..f1f2a58
--- /dev/null
+++ b/test-resources/latex/all-default/in.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-resources/latex/all-default/options.txt b/test-resources/latex/all-default/options.txt
new file mode 100644
index 0000000..e69de29
diff --git a/test-resources/latex/all/expected.png b/test-resources/latex/all/expected.png
new file mode 100644
index 0000000..924f098
Binary files /dev/null and b/test-resources/latex/all/expected.png differ
diff --git a/test-resources/latex/all/in.txt b/test-resources/latex/all/in.txt
new file mode 100644
index 0000000..f1f2a58
--- /dev/null
+++ b/test-resources/latex/all/in.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-resources/latex/all/options.txt b/test-resources/latex/all/options.txt
new file mode 100644
index 0000000..58c8e0e
--- /dev/null
+++ b/test-resources/latex/all/options.txt
@@ -0,0 +1 @@
+--latex
\ No newline at end of file
diff --git a/test-resources/latex/box/expected.png b/test-resources/latex/box/expected.png
new file mode 100644
index 0000000..258dfd6
Binary files /dev/null and b/test-resources/latex/box/expected.png differ
diff --git a/test-resources/latex/box/in.txt b/test-resources/latex/box/in.txt
new file mode 100644
index 0000000..1e25027
--- /dev/null
+++ b/test-resources/latex/box/in.txt
@@ -0,0 +1,13 @@
+   +------------------------------------------+
+   |                                          |
+  $|Vertical bars inside math are not confused|$
+   |                                          |
+   +------------------------------------------+
+
+Horizontal bers are not confused.
+ +-----+
+ |     |
+$+-----+$
+
+
+
diff --git a/test-resources/latex/box/options.txt b/test-resources/latex/box/options.txt
new file mode 100644
index 0000000..58c8e0e
--- /dev/null
+++ b/test-resources/latex/box/options.txt
@@ -0,0 +1 @@
+--latex
\ No newline at end of file
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/org/stathissideris/ascii2image/test/latex/LaTeXModeNegativeTest.java b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeNegativeTest.java
new file mode 100644
index 0000000..0646beb
--- /dev/null
+++ b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeNegativeTest.java
@@ -0,0 +1,31 @@
+package org.stathissideris.ascii2image.test.latex;
+
+import org.junit.Test;
+import org.scilab.forge.jlatexmath.ParseException;
+
+import java.io.IOException;
+
+import static com.github.dakusui.crest.Crest.asString;
+import static com.github.dakusui.crest.Crest.assertThat;
+import static com.github.dakusui.crest.Crest.substringAfterRegex;
+
+public class LaTeXModeNegativeTest extends LaTeXModeTestBase {
+  @Test(expected = ExpectedException.class)
+  public void givenBrokenLaTeXmathExpression$whenDitaaIsRunLaTeXModeEnabled$thenAppropriateExceptionIsThrown() throws IOException {
+    try {
+      execute("_brokenmath", 0.98);
+    } catch (ParseException e) {
+      assertThat(
+          e.getMessage(),
+          asString(substringAfterRegex("Unknown symbol or command").after("unknownFunction").$()).isNotNull().$()
+      );
+      throw new ExpectedException(e);
+    }
+  }
+
+  private static class ExpectedException extends RuntimeException {
+    private ExpectedException(ParseException e) {
+      super(e);
+    }
+  }
+}
diff --git a/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTest.java b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTest.java
new file mode 100644
index 0000000..d60edf0
--- /dev/null
+++ b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTest.java
@@ -0,0 +1,37 @@
+package org.stathissideris.ascii2image.test.latex;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static java.util.Objects.requireNonNull;
+
+@RunWith(Parameterized.class)
+public class LaTeXModeTest extends LaTeXModeTestBase {
+  private final String testName;
+  private final double threshold;
+
+  @Parameters
+  public static Object[] parameters() {
+    return Arrays.stream(requireNonNull(new File("tests/latex").listFiles()))
+        .filter(File::isDirectory)
+        .filter(d -> !d.getName().startsWith("_"))
+        .map(d -> new Object[] { d.getName(), 0.98 })
+        .toArray();
+  }
+
+  public LaTeXModeTest(String testName, double threshold) {
+    this.testName = testName;
+    this.threshold = threshold;
+  }
+
+  @Test
+  public void executeTest() throws IOException {
+    execute(testName, threshold);
+  }
+}
\ No newline at end of file
diff --git a/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestBase.java b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestBase.java
new file mode 100644
index 0000000..96ce391
--- /dev/null
+++ b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestBase.java
@@ -0,0 +1,232 @@
+package org.stathissideris.ascii2image.test.latex;
+
+import org.stathissideris.ascii2image.core.CommandLineConverter;
+
+import javax.imageio.ImageIO;
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import static com.github.dakusui.crest.Crest.asDouble;
+import static com.github.dakusui.crest.Crest.assertThat;
+import static com.github.dakusui.crest.Crest.call;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+
+abstract class LaTeXModeTestBase extends TestBase {
+  void execute(String testName, double threshold) throws IOException {
+    System.out.println("START:" + testName);
+    boolean started = false;
+    try {
+      exercise(testName, this.options(testName));
+      started = true;
+      boolean succeeded = false;
+      try {
+        verify(testName, threshold);
+        succeeded = true;
+      } finally {
+        if (succeeded)
+          System.out.println("PASSED:" + testName);
+        else
+          System.out.println("FAILED:" + testName);
+      }
+    } finally {
+      if (!started)
+        System.err.println("ERROR:" + testName);
+    }
+  }
+
+  private void exercise(String testName, String[] options) {
+    String[] args = Stream.concat(
+        Stream.of(inputFilePath(testName), actualImagePath(testName), "-o"),
+        Arrays.stream(options))
+        .toArray(String[]::new);
+    System.out.println("  ditaa " + String.join(" ", args));
+    CommandLineConverter.main(args);
+  }
+
+  private void verify(String testName, double threshold) {
+    ImageFile expected = expectedImage(testName);
+    assertThat(
+        actualImage(testName),
+        asDouble(
+            call("diff", expected, diffImagePath(testName))
+                .andThen("similarity", expected).$())
+            .gt(threshold).$()
+    );
+  }
+
+  private String inputFilePath(String s) {
+    return String.format("tests/latex/%s/in.txt", s);
+  }
+
+  private ImageFile expectedImage(String s) {
+    return new ImageFile(String.format("tests/latex/%s/expected.png", s));
+  }
+
+  private ImageFile actualImage(String s) {
+    return new ImageFile(actualImagePath(s));
+  }
+
+  private String actualImagePath(String s) {
+    return String.format("tests/latex/%s/actual.png", s);
+  }
+
+
+  private String diffImagePath(String s) {
+    return String.format("tests/latex/%s/diff.png", s);
+  }
+
+  String[] options(String s) throws IOException {
+    return Files.lines(Paths.get(String.format("tests/latex/%s/options.txt", s)))
+        .collect(toList())
+        .toArray(new String[0]);
+  }
+
+  public static class ImageFile extends File {
+    private final BufferedImage image;
+
+    ImageFile(String pathname) {
+      super(pathname);
+      try {
+        this.image = ImageIO.read(this);
+      } catch (IOException e) {
+        throw new RuntimeException(pathname, e);
+      }
+    }
+
+    private ImageFile(String pathname, BufferedImage image) {
+      super(pathname);
+      this.image = requireNonNull(image);
+    }
+
+    /**
+     * Creates and returns a new {@code ImageFile} object that contains "diff" between
+     * this and {@code another} image.
+     * The image is saved in a file specified by {@code outPathname}.
+     *
+     * @param another     An image with which returned diff image is created.
+     * @param outPathname A pathname that stores the created image.
+     * @return This object
+     */
+    @SuppressWarnings("unused")
+    public ImageFile diff(ImageFile another, String outPathname) {
+      return new ImageFile(outPathname, diff(this.image, another.image)).write();
+    }
+
+    /**
+     * Returns similarity between this image and {@code another} image.
+     * If they are identical, {@code 1.0} will be returned. If completely different,
+     * {@code 0.0} will be returned.
+     *
+     * This method is reflectively invoked by test methods in the enclosing class.
+     *
+     * @param another An image to be compared to this image
+     * @return The similarity.
+     */
+    @SuppressWarnings("unused")
+    public double similarity(ImageFile another) {
+      return similarity(this.image, another.image);
+    }
+
+    private ImageFile write() {
+      try {
+        ImageIO.write(image, "png", this.getAbsoluteFile());
+        return this;
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+
+    /**
+     * 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 biA input image A
+     * @param biB input image B
+     * @return The similarity.
+     */
+    private static double similarity(BufferedImage biA, BufferedImage biB) {
+      double percentage = 0;
+      try {
+        // take buffer data from both image files //
+        DataBuffer dbA = extendIfNecessary(biA, biB).getData().getDataBuffer();
+        int sizeA = dbA.getSize();
+        DataBuffer dbB = extendIfNecessary(biB, biA).getData().getDataBuffer();
+        int count = 0;
+        for (int i = 0; i < sizeA; i++) {
+
+          if (dbA.getElem(i) == dbB.getElem(i)) {
+            count = count + 1;
+          }
+
+        }
+        percentage = ((double) count) / sizeA;
+      } catch (RuntimeException e) {
+        throw e;
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+      return percentage;
+    }
+
+    /**
+     * Generates an image that highlights diff between given 2 images ({@code img1}
+     * and {@code img2}) and returns it.
+     *
+     * This method is based on a discussion found in stackoverflow.com,
+     * "Highlight differences between images".
+     *
+     * @param img1 An image to be diffed.
+     * @param img2 The other image to be diffed with {@code img2}
+     * @return An image that highlights diff between {@code img1} and {@code img2}.
+     * @see "https://stackoverflow.com/questions/25022578/highlight-differences-between-images"
+     */
+    private static BufferedImage diff(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 = extendIfNecessary(img2, img1).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, img1.getType());
+      out.setRGB(0, 0, w, h, p1, 0, w);
+      return out;
+    }
+
+    private static BufferedImage extendIfNecessary(BufferedImage image, BufferedImage another) {
+      if (another.getWidth() > image.getWidth() || another.getHeight() > image.getHeight()) {
+        BufferedImage ret = new BufferedImage(
+            Math.max(image.getWidth(), another.getWidth()),
+            Math.max(image.getHeight(), another.getHeight()),
+            image.getType()
+        );
+        final int p1[] = image.getRGB(
+            0, 0, image.getWidth(), image.getHeight(),
+            null,
+            0, image.getWidth());
+        ret.setRGB(
+            0, 0, image.getWidth(), image.getHeight(),
+            p1,
+            0, image.getWidth());
+        return ret;
+      }
+      return image;
+    }
+  }
+}
diff --git a/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestExample.java b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestExample.java
new file mode 100644
index 0000000..2d2cb8e
--- /dev/null
+++ b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestExample.java
@@ -0,0 +1,46 @@
+package org.stathissideris.ascii2image.test.latex;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * This is a test class that illustrates how a test in {@code LaTeXModeTest}
+ * failure is displayed in console.
+ */
+@Ignore
+public class LaTeXModeTestExample extends LaTeXModeTestBase {
+  /**
+   * This test fails always with following message. (Irrelevant information is shortened
+   * to fit screen width).
+   *
+   * 
+   * 1:x.diff(...expected.png,...diff.png).similarity(.../expected.png) >[0.98] was not met because x.diff(...expected.png,...diff.png).similarity(...expected.png,...)=<0.95...>:Double
+   * 2:x=<...actual.png>:ImageFile
+   * 3:x.diff(...expected.png,...diff.png).similarity(.../expected.png) >[0.98]
+   * 4:                                  |                            |
+   * 5:                                  |                            +-<0.9538961912448595>:Double
+   * 6:                                  |
+   * 7:                                  +----------------------------------------------------<...diff.png>:ImageFile
+   * 
+ * + * Given x is a {@code ImageFile} object contains imaged created by running ditaa + * this time from test input ({@code actual.png}). + * {@code x.diff(..expected.png,...diff.png)} creates a new {@code ImageFile} object + * and returns it (l.7) and the image of it ({@code diff.png}) highlights pixels + * which differ in {@code expected.png} and {@code actual.png}. + * + * Then, "similarity" between returned {@code ImageFile} for {@code diff.png} + * object and {@code expected.png} is computed to {@code 0.9538...} (l.5). + * However we expect the number is greater than {@code 0.98}, and therefore + * this test must fail. + * + * @throws IOException Failed to access resources + * @see LaTeXModeTestBase.ImageFile + */ + @Test + public void exampleTest() throws IOException { + execute("_example", 0.98); + } +} diff --git a/test/java/org/stathissideris/ascii2image/test/latex/LaTeXUtilsTest.java b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXUtilsTest.java new file mode 100644 index 0000000..67b3f58 --- /dev/null +++ b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXUtilsTest.java @@ -0,0 +1,98 @@ +package org.stathissideris.ascii2image.test.latex; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.stathissideris.ascii2image.graphics.DiagramText; +import org.stathissideris.ascii2image.text.StringUtils; +import org.stathissideris.ascii2image.text.TextGrid; + +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.github.dakusui.crest.Crest.asListOf; +import static com.github.dakusui.crest.Crest.asString; +import static com.github.dakusui.crest.Crest.assertThat; +import static com.github.dakusui.crest.Crest.sublistAfterElement; + +public class LaTeXUtilsTest extends TestBase { + @Rule + public TestName name = new TestName(); + + + @Test + public void givenStringContainingLaTeXFormula$whenTransform$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 givenMathContainingText$whenTokenizeWithRegexUsedInDiagramText$thenTokenizedAsExpected() { + assertThat( + tokenizeTextUsingTextSplitter("hello$HELLO$ world$$", DiagramText.TEXT_SPLITTING_REGEX), + asListOf(String.class, + sublistAfterElement("hello") + .afterElement("$HELLO$") + .afterElement(" world") + .afterElement("$$") + .$()) + .isEmpty().$()); + } + + @Test + public void givenEmptyText$whenTokenizeWithRegexUsedInDiagramText$thenNoTokenReturned() { + assertThat( + tokenizeTextUsingTextSplitter("", DiagramText.TEXT_SPLITTING_REGEX), + asListOf(String.class).isEmpty().$() + ); + } + + @Test + public void givenTextContainingColorCodeLikeSubstring$whenTokenizeWithCOLORCODELIKE_REGEX$thenTokenizedAsExpected() { + assertThat( + tokenizeTextUsingTextSplitter("hellocXYZworld", TextGrid.COLORCODELIKE_REGEX), + asListOf(String.class, + sublistAfterElement("hello") + .afterElement("cXYZ") + .afterElement("world") + .$()) + .isEmpty() + .$()); + } + + @Test + public void givenEmptyText$whenTokenizeWithCOLORCODELIKE_REGEX$thenNoTokenReturned() { + assertThat( + tokenizeTextUsingTextSplitter("", TextGrid.COLORCODELIKE_REGEX), + asListOf(String.class).isEmpty().$()); + } + + private static List tokenizeTextUsingTextSplitter(String s, Pattern textSplittingRegex) { + return StreamSupport.stream(((Iterable) () -> StringUtils.createTextSplitter(textSplittingRegex, s)).spliterator(), false).collect(Collectors.toList()); + } +} diff --git a/test/java/org/stathissideris/ascii2image/test/latex/TestBase.java b/test/java/org/stathissideris/ascii2image/test/latex/TestBase.java new file mode 100644 index 0000000..b3d9c63 --- /dev/null +++ b/test/java/org/stathissideris/ascii2image/test/latex/TestBase.java @@ -0,0 +1,56 @@ +package org.stathissideris.ascii2image.test.latex; + +import org.junit.After; +import org.junit.Before; + +import java.io.OutputStream; +import java.io.PrintStream; + +/** + * A base class for JUnit tests. This class suppresses stdout and stderr only when + * it is run under surefire but not otherwise. + * This is useful if you want to see outputs to stdout and stderr but you don't + * want them in normal build environment. + */ +public class TestBase { + private static final PrintStream STDOUT = System.out; + private static final PrintStream STDERR = System.err; + + @Before + public void before() { + suppressStdOutErrIfRunUnderSurefire(); + } + + @After + public void after() { + restoreStdOutErr(); + } + + private static final PrintStream NOP = new PrintStream(new OutputStream() { + @Override + public void write(int b) { + } + }); + + /** + * Typically called from a method annotated with {@literal @}{@code Before} method. + */ + private static void suppressStdOutErrIfRunUnderSurefire() { + if (isRunUnderSurefire()) { + System.setOut(NOP); + System.setErr(NOP); + } + } + + /** + * Typically called from a method annotated with {@literal @}{@code After} method. + */ + private static void restoreStdOutErr() { + System.setOut(STDOUT); + System.setErr(STDERR); + } + + private static boolean isRunUnderSurefire() { + return System.getProperty("surefire.real.class.path") != null; + } +}