From 0c2359e79f705dd9ed5577daac8470d2841364c0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ukai Date: Sat, 24 Aug 2019 08:08:30 +0900 Subject: [PATCH 1/6] Initial commit for Issue-#39 LaTeX math mode support. --- .gitignore | 7 + README.md | 41 +++ project.clj | 5 +- .../core/CommandLineConverter.java | 18 +- .../ascii2image/core/ConversionOptions.java | 13 +- .../ascii2image/core/ProcessingOptions.java | 14 +- .../ascii2image/graphics/BitmapRenderer.java | 25 +- .../ascii2image/graphics/Diagram.java | 19 +- .../ascii2image/graphics/DiagramText.java | 138 ++++++++- .../ascii2image/graphics/SVGBuilder.java | 50 +--- .../ascii2image/text/StringUtils.java | 28 ++ .../ascii2image/text/TextGrid.java | 267 ++++++++++++------ .../test/latex/LaTeXModeNegativeTest.java | 31 ++ .../ascii2image/test/latex/LaTeXModeTest.java | 37 +++ .../test/latex/LaTeXModeTestBase.java | 232 +++++++++++++++ .../test/latex/LaTeXModeTestExample.java | 46 +++ .../test/latex/LaTeXUtilsTest.java | 98 +++++++ .../ascii2image/test/latex/TestBase.java | 56 ++++ tests/latex/_brokenmath/expected.png | Bin 0 -> 109 bytes tests/latex/_brokenmath/in.txt | 3 + tests/latex/_brokenmath/options.txt | 1 + tests/latex/_example/expected.png | Bin 0 -> 30245 bytes tests/latex/_example/in.txt | 38 +++ tests/latex/_example/options.txt | 1 + tests/latex/all-default/expected.png | Bin 0 -> 30245 bytes tests/latex/all-default/in.txt | 38 +++ tests/latex/all-default/options.txt | 0 tests/latex/all/expected.png | Bin 0 -> 26278 bytes tests/latex/all/in.txt | 38 +++ tests/latex/all/options.txt | 1 + tests/latex/box/expected.png | Bin 0 -> 4292 bytes tests/latex/box/in.txt | 13 + tests/latex/box/options.txt | 1 + tests/text/art-latexmath-1.txt | 38 +++ 34 files changed, 1102 insertions(+), 195 deletions(-) create mode 100644 test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeNegativeTest.java 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/LaTeXUtilsTest.java create mode 100644 test/java/org/stathissideris/ascii2image/test/latex/TestBase.java create mode 100644 tests/latex/_brokenmath/expected.png create mode 100644 tests/latex/_brokenmath/in.txt create mode 100644 tests/latex/_brokenmath/options.txt create mode 100644 tests/latex/_example/expected.png create mode 100644 tests/latex/_example/in.txt create mode 100644 tests/latex/_example/options.txt create mode 100644 tests/latex/all-default/expected.png create mode 100644 tests/latex/all-default/in.txt create mode 100644 tests/latex/all-default/options.txt create mode 100644 tests/latex/all/expected.png create mode 100644 tests/latex/all/in.txt create mode 100644 tests/latex/all/options.txt create mode 100644 tests/latex/box/expected.png create mode 100644 tests/latex/box/in.txt create mode 100644 tests/latex/box/options.txt create mode 100644 tests/text/art-latexmath-1.txt diff --git a/.gitignore b/.gitignore index 65bb389..53fd313 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,10 @@ releases/ tests/images/ tests/images-expected/ target/ + +# 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 \ No newline at end of file diff --git a/README.md b/README.md index 711b77d..57361e9 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,46 @@ 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..36baa7a 100644 --- a/project.clj +++ b/project.clj @@ -9,7 +9,10 @@ [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"] + [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 937953a..1054041 100644 --- a/src/java/org/stathissideris/ascii2image/core/CommandLineConverter.java +++ b/src/java/org/stathissideris/ascii2image/core/CommandLineConverter.java @@ -19,23 +19,16 @@ */ package org.stathissideris.ascii2image.core; -import java.awt.image.RenderedImage; -import java.io.*; - -import javax.imageio.ImageIO; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.OptionBuilder; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.PosixParser; +import org.apache.commons.cli.*; import org.stathissideris.ascii2image.graphics.BitmapRenderer; import org.stathissideris.ascii2image.graphics.Diagram; import org.stathissideris.ascii2image.graphics.SVGRenderer; import org.stathissideris.ascii2image.text.TextGrid; +import javax.imageio.ImageIO; +import java.awt.image.RenderedImage; +import java.io.*; + /** * * @author Efstathios Sideris @@ -63,6 +56,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 5599f65..8d14063 100644
--- a/src/java/org/stathissideris/ascii2image/core/ConversionOptions.java
+++ b/src/java/org/stathissideris/ascii2image/core/ConversionOptions.java
@@ -25,11 +25,12 @@
 import org.xml.sax.SAXException;
 
 import javax.xml.parsers.ParserConfigurationException;
-import java.awt.*;
+import java.awt.Color;
 import java.io.File;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.HashMap;
+import java.util.Objects;
 
 /**
  * 
@@ -80,6 +81,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"));
 
@@ -101,11 +103,16 @@ public ConversionOptions(CommandLine cmdLine) throws UnsupportedEncodingExceptio
 		}
 
 		String encoding = (String) cmdLine.getOptionValue("encoding");
-		if(encoding != null){
+		if(encoding != null) {
 			new String(new byte[2], encoding);
 			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 1833e9f..f76e0e2 100644
--- a/src/java/org/stathissideris/ascii2image/core/ProcessingOptions.java
+++ b/src/java/org/stathissideris/ascii2image/core/ProcessingOptions.java
@@ -19,10 +19,10 @@
  */
 package org.stathissideris.ascii2image.core;
 
-import java.util.HashMap;
-
 import org.stathissideris.ascii2image.graphics.CustomShapeDefinition;
 
+import java.util.HashMap;
+
 /**
  * @author Efstathios Sideris
  *
@@ -36,6 +36,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,7 +238,12 @@ public void putAllInCustomShapes(HashMap customSh
 	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 40d1bd3..71ed72e 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/BitmapRenderer.java
@@ -342,16 +342,7 @@ public RenderedImage render(Diagram diagram, BufferedImage image,  RenderingOpti
 		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){
@@ -392,20 +383,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 ef3d66b..bf45b8e 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/Diagram.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/Diagram.java
@@ -19,12 +19,6 @@
  */
 package org.stathissideris.ascii2image.graphics;
 
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.geom.Rectangle2D;
-import java.util.ArrayList;
-import java.util.Iterator;
-
 import org.stathissideris.ascii2image.core.ConversionOptions;
 import org.stathissideris.ascii2image.core.Pair;
 import org.stathissideris.ascii2image.text.AbstractionGrid;
@@ -35,6 +29,12 @@
 import org.stathissideris.ascii2image.text.TextGrid.CellStringPair;
 import org.stathissideris.ascii2image.text.TextGrid.CellTagPair;
 
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.Iterator;
+
 /**
  * 
  * @author Efstathios Sideris
@@ -550,11 +550,12 @@ public Diagram(TextGrid grid, ConversionOptions options) {
 				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);
-				} else textObject = new DiagramText(minX, y, string, font);
-			
+					textObject = new DiagramText(minX, y, string, lessWideFont, laTeXmathEnabled);
+				} else  textObject = new DiagramText(minX, y, string, font, laTeXmathEnabled);
+
 				textObject.centerVerticallyBetween(getCellMinY(cell), getCellMaxY(cell));
 			
 				//TODO: if the strings start with bullets they should be aligned to the left
diff --git a/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java b/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java
index ef411ca..dd76c4c 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/DiagramText.java
@@ -19,19 +19,30 @@
  */
 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.Rectangle;
+import java.awt.Graphics2D;
 import java.awt.geom.Rectangle2D;
+import java.util.Iterator;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
 /**
  * 
  * @author Efstathios Sideris
  */
 public class DiagramText extends DiagramComponent {
+	public static final Color   DEFAULT_COLOR        = Color.black;
+	public static final Pattern TEXT_SPLITTING_REGEX = Pattern.compile("([^$]+|\\$[^$]*\\$)");
+	private final       boolean latexMathEnabled;
 
-	public static final Color DEFAULT_COLOR = Color.black;
-	
 	private String text;
 	private Font font;
 	private int xPos, yPos;
@@ -40,7 +51,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) throw new IllegalArgumentException("DiagramText cannot be initialised with a null font");
 
@@ -48,6 +59,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 +107,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 +270,51 @@ 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(),
+						SVGBuilder.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 7be7cd5..1b4ab49 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java
@@ -285,56 +285,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) {
+    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 e753245..bb7b071 100644
--- a/src/java/org/stathissideris/ascii2image/text/StringUtils.java
+++ b/src/java/org/stathissideris/ascii2image/text/StringUtils.java
@@ -19,6 +19,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
  *
@@ -166,4 +171,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 1ead278..0ecf1c5 100644
--- a/src/java/org/stathissideris/ascii2image/text/TextGrid.java
+++ b/src/java/org/stathissideris/ascii2image/text/TextGrid.java
@@ -19,15 +19,18 @@
  */
 package org.stathissideris.ascii2image.text;
 
+import org.stathissideris.ascii2image.core.FileUtils;
+import org.stathissideris.ascii2image.core.ProcessingOptions;
+
 import java.awt.Color;
 import java.io.*;
 import java.util.*;
+import java.util.function.IntUnaryOperator;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.stathissideris.ascii2image.core.FileUtils;
-import org.stathissideris.ascii2image.core.ProcessingOptions;
-import org.stathissideris.ascii2image.graphics.CustomShapeDefinition;
+import static java.util.stream.Collectors.toList;
+import static org.stathissideris.ascii2image.text.StringUtils.createTextSplitter;
 
 
 /**
@@ -35,10 +38,13 @@
  * @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 = {'|', '-', '*', '=', ':'};
@@ -84,6 +90,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);
 	}
@@ -106,6 +154,7 @@ public static void main(String[] args) throws Exception {
 
 	public TextGrid(){
 		rows = new ArrayList();
+		this.updateModeRows();
 	}
 	
 	public TextGrid(int width, int height){
@@ -113,6 +162,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){
@@ -124,7 +174,8 @@ public TextGrid(TextGrid otherGrid){
 		rows = new ArrayList();
 		for(StringBuilder row : otherGrid.getRows()) {
 			rows.add(new StringBuilder(row));
-		}		
+		}
+		this.updateModeRows();
 	}
 
 	public void clear(){
@@ -331,8 +382,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))
 						isOnHorizontalLine = true;
@@ -366,6 +416,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))){
@@ -379,23 +431,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) {
+		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");
+	}
 
 	/**
 	 * Replace all occurrences of c1 with c2
@@ -609,7 +668,8 @@ public void removeBoundaries(){
 		Iterator it = toBeRemoved.iterator();
 		while(it.hasNext()){
 			Cell cell = (Cell) it.next();
-			set(cell, ' ');
+			if (isInPlainMode(cell))
+				set(cell, ' ');
 		}
 	}
 
@@ -636,6 +696,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()){
@@ -661,6 +723,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 == '{'){
@@ -727,6 +791,8 @@ public static boolean isBoundary(char c){
 	}
 	public boolean isBoundary(int x, int y){ return isBoundary(new Cell(x, y)); }
 	public boolean isBoundary(Cell cell){
+		if (!isInPlainMode(cell))
+			return false;
 		char c = get(cell.x, cell.y);
 		if(0 == c) return false;
 		if('+' == c || '\\' == c || '/' == c){
@@ -753,17 +819,17 @@ public boolean isLine(Cell cell){
 	public static boolean isHorizontalLine(char c){
 		return StringUtils.isOneOf(c, horizontalLines);
 	}
-	public boolean isHorizontalLine(Cell cell){ return isHorizontalLine(cell.x, cell.y); }
+	public boolean isHorizontalLine(Cell cell){ 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){
 		return StringUtils.isOneOf(c, verticalLines);
 	}
-	public boolean isVerticalLine(Cell cell){ return isVerticalLine(cell.x, cell.y); }
+	public boolean isVerticalLine(Cell cell){ return isVerticalLine(cell.x, cell.y) && isInPlainMode(cell); }
 	public boolean isVerticalLine(int x, int y){
 		char c = get(x, y);
 		if(0 == c) return false;
@@ -781,15 +847,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);
 	}
 
 
@@ -798,7 +864,7 @@ public boolean isPointCell(Cell cell){
 			isCorner(cell)
 			|| isIntersection(cell)
 			|| isStub(cell)
-			|| isLinesEnd(cell));
+			|| isLinesEnd(cell)) && isInPlainMode(cell);
 	}
 
 
@@ -876,20 +942,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);
 	}
 	
 	
@@ -913,7 +980,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;
 	}
@@ -1539,68 +1607,77 @@ public boolean initialiseWithLines(ArrayList lines, ProcessingOpt
 		}
 		rows = new ArrayList(lines.subList(0, i + 2));
 
-		if(options != null) fixTabs(options.getTabSize());
-		else fixTabs(options.DEFAULT_TAB_SIZE);
+		this.updateModeRows();
+		try {
 
+			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)
-		
-		int blankBorderSize = 2;
-		
-		int maxLength = 0;
-		int index = 0;
-		
-		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);
+			// 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 maxLength = 0;
+			int index = 0;
+
+			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);
+				}
+				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
-		
-		StringBuilder topBottomRow =
-			new StringBuilder(StringUtils.repeatString(" ", maxLength + blankBorderSize * 2));
-		
-		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();
-				
-				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("  "));
+			it = rows.iterator();
+			ArrayList newRows = new ArrayList();
+			//TODO: make the following depend on blankBorderSize
+
+			StringBuilder topBottomRow =
+					new StringBuilder(StringUtils.repeatString(" ", maxLength + blankBorderSize * 2));
+
+			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();
+
+					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("  "));
+				}
 			}
+			//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();
-		
+
 		return true;
 	}
 	
@@ -1638,7 +1715,15 @@ private void fixTabs(int tabSize){
 	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/java/org/stathissideris/ascii2image/test/latex/LaTeXModeNegativeTest.java b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeNegativeTest.java
new file mode 100644
index 0000000..f762dcd
--- /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);
+    }
+  }
+}
\ No newline at end of file
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..5f92aa3
--- /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;
+    }
+  }
+}
\ No newline at end of file
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..6195d5d
--- /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); + } +} \ No newline at end of file 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..303b789 --- /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()); + } +} \ No newline at end of file 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..7371093 --- /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; + } +} \ No newline at end of file diff --git a/tests/latex/_brokenmath/expected.png b/tests/latex/_brokenmath/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..78c1c00af1ddb1889d98c99caf06649c51ae24c3 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryoCO|{#S9GG!XV7ZFl&wkP>{XE z)7O>#5jP97y6lzwYh6Gg9#0p?5RU7~KmPx>XJGxu^zY7@$mc+D22WQ%mvv4FO#o}F B8uS1F literal 0 HcmV?d00001 diff --git a/tests/latex/_brokenmath/in.txt b/tests/latex/_brokenmath/in.txt new file mode 100644 index 0000000..a2c97ce --- /dev/null +++ b/tests/latex/_brokenmath/in.txt @@ -0,0 +1,3 @@ ++------------------+ +|$\unknownFunction$| ++------------------+ \ No newline at end of file diff --git a/tests/latex/_brokenmath/options.txt b/tests/latex/_brokenmath/options.txt new file mode 100644 index 0000000..2db6cb4 --- /dev/null +++ b/tests/latex/_brokenmath/options.txt @@ -0,0 +1 @@ +--latex diff --git a/tests/latex/_example/expected.png b/tests/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/tests/latex/_example/options.txt b/tests/latex/_example/options.txt new file mode 100644 index 0000000..2db6cb4 --- /dev/null +++ b/tests/latex/_example/options.txt @@ -0,0 +1 @@ +--latex diff --git a/tests/latex/all-default/expected.png b/tests/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/tests/latex/all-default/options.txt b/tests/latex/all-default/options.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/latex/all/expected.png b/tests/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