diff --git a/README.md b/README.md index f22b504..de56317 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..f6d7815 100644 --- a/project.clj +++ b/project.clj @@ -9,8 +9,14 @@ [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"] + [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"]] + :plugins [[lein-junit "1.1.9"]] + :junit ["test/java"] + :junit-formatter :plain + :junit-results-dir "target/test-results" + :profiles {:dev {:dependencies [[junit/junit "4.12"] [com.github.dakusui/thincrest "3.6.0"]] :java-source-paths ["test/java"]}}) 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..c24a36a 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){
@@ -59,7 +71,7 @@ public void centerHorizontallyBetween(int minX, int maxX){
 		int width = FontMeasurer.instance().getWidthFor(text, font);
 		int center = Math.abs(maxX - minX) / 2;
 		xPos += Math.abs(center - width / 2);
-		
+
 	}
 
 	public void centerVerticallyBetween(int minY, int maxY){
@@ -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,
+						String.valueOf(xPos),
+						String.valueOf(yPos),
+						options.getFontFamily(),
+						String.valueOf(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 358f3ee..1f9a266 100644
--- a/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java
+++ b/src/java/org/stathissideris/ascii2image/graphics/SVGBuilder.java
@@ -284,56 +284,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,
-                    String.valueOf(xPos),
-                    String.valueOf(yPos),
-                    options.getFontFamily(),
-                    String.valueOf(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-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..1fad900
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..a73f2e6
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..3790330
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..44220f3
--- /dev/null
+++ b/test-resources/latex/box/in.txt
@@ -0,0 +1,13 @@
+   +------------------------------------------+
+   |                                          |
+  $|Vertical bars inside math are not confused|$
+   |                                          |
+   +------------------------------------------+
+
+Horizontal bars 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/TextGridTest.java b/test/java/org/stathissideris/ascii2image/test/TextGridTest.java
index d1425c2..0f6632b 100644
--- a/test/java/org/stathissideris/ascii2image/test/TextGridTest.java
+++ b/test/java/org/stathissideris/ascii2image/test/TextGridTest.java
@@ -19,17 +19,17 @@
  */
 package org.stathissideris.ascii2image.test;
 
-import static org.junit.Assert.*;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.stathissideris.ascii2image.text.AbstractionGrid;
 import org.stathissideris.ascii2image.text.CellSet;
 import org.stathissideris.ascii2image.text.TextGrid;
 
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
 public class TextGridTest {
 	
 	@Before public void setUp() {
@@ -38,7 +38,7 @@ public class TextGridTest {
 	@Test public void testFillContinuousAreaSquareOutside() throws FileNotFoundException, IOException {
 		TextGrid squareGrid;
 		squareGrid = new TextGrid();
-		squareGrid.loadFrom("tests/text/simple_square01.txt");
+		squareGrid.loadFrom("test-resources/text/simple_square01.txt");
 
 		CellSet filledArea = squareGrid.fillContinuousArea(0, 0, '*');
 		int size = filledArea.size();
@@ -55,7 +55,7 @@ public class TextGridTest {
 	@Test public void testFillContinuousAreaSquareInside() throws FileNotFoundException, IOException {
 		TextGrid squareGrid;
 		squareGrid = new TextGrid();
-		squareGrid.loadFrom("tests/text/simple_square01.txt");
+		squareGrid.loadFrom("test-resources/text/simple_square01.txt");
 		
 		CellSet filledArea = squareGrid.fillContinuousArea(3, 3, '*');
 		int size = filledArea.size();
@@ -69,7 +69,7 @@ public class TextGridTest {
 	@Test public void testFillContinuousAreaUInside() throws FileNotFoundException, IOException {
 		TextGrid uGrid;
 		uGrid = new TextGrid();
-		uGrid.loadFrom("tests/text/simple_U01.txt");
+		uGrid.loadFrom("test-resources/text/simple_U01.txt");
 		
 		CellSet filledArea = uGrid.fillContinuousArea(3, 3, '*');
 		int size = filledArea.size();
@@ -86,7 +86,7 @@ public class TextGridTest {
 	@Test public void testFillContinuousAreaUOutside() throws FileNotFoundException, IOException {
 		TextGrid uGrid;
 		uGrid = new TextGrid();
-		uGrid.loadFrom("tests/text/simple_U01.txt");
+		uGrid.loadFrom("test-resources/text/simple_U01.txt");
 		
 		CellSet filledArea = uGrid.fillContinuousArea(0, 0, '*');
 		int size = filledArea.size();
@@ -108,7 +108,7 @@ public class TextGridTest {
 	@Test public void testFillContinuousAreaSOutside() throws FileNotFoundException, IOException {
 		TextGrid uGrid;
 		uGrid = new TextGrid();
-		uGrid.loadFrom("tests/text/simple_S01.txt");
+		uGrid.loadFrom("test-resources/text/simple_S01.txt");
 		
 		CellSet filledArea = uGrid.fillContinuousArea(0, 0, '*');
 		int size = filledArea.size();
@@ -134,7 +134,7 @@ public class TextGridTest {
 	@Test public void testFillContinuousAreaSInside1() throws FileNotFoundException, IOException {
 		TextGrid uGrid;
 		uGrid = new TextGrid();
-		uGrid.loadFrom("tests/text/simple_S01.txt");
+		uGrid.loadFrom("test-resources/text/simple_S01.txt");
 		
 		CellSet filledArea = uGrid.fillContinuousArea(3, 3, '*');
 		int size = filledArea.size();
@@ -149,7 +149,7 @@ public class TextGridTest {
 	@Test public void testFillContinuousAreaSInside2() throws FileNotFoundException, IOException {
 		TextGrid uGrid;
 		uGrid = new TextGrid();
-		uGrid.loadFrom("tests/text/simple_S01.txt");
+		uGrid.loadFrom("test-resources/text/simple_S01.txt");
 		
 		CellSet filledArea = uGrid.fillContinuousArea(17, 3, '*');
 		int size = filledArea.size();
@@ -165,7 +165,7 @@ public class TextGridTest {
 	@Test public void testFindBoundariesExpandingFromSquare() throws FileNotFoundException, IOException {
 		TextGrid grid;
 		grid = new TextGrid();
-		grid.loadFrom("tests/text/simple_square01.txt");
+		grid.loadFrom("test-resources/text/simple_square01.txt");
 
 		CellSet wholeGridSet = new CellSet();
 		addSquareToCellSet(grid, wholeGridSet, 0,0, grid.getWidth(),grid.getHeight());
@@ -191,7 +191,7 @@ public class TextGridTest {
 	@Test public void testFindBoundariesExpandingFromUInside() throws FileNotFoundException, IOException {
 		TextGrid grid;
 		grid = new TextGrid();
-		grid.loadFrom("tests/text/simple_U01.txt");
+		grid.loadFrom("test-resources/text/simple_U01.txt");
 
 		CellSet wholeGridSet = new CellSet();
 		addSquareToCellSet(grid, wholeGridSet, 0,0, grid.getWidth(),grid.getHeight());
@@ -227,7 +227,7 @@ public class TextGridTest {
 	@Test public void testFindBoundariesExpandingFromUOutside() throws FileNotFoundException, IOException {
 		TextGrid grid;
 		grid = new TextGrid();
-		grid.loadFrom("tests/text/simple_U01.txt");
+		grid.loadFrom("test-resources/text/simple_U01.txt");
 
 		CellSet wholeGridSet = new CellSet();
 		addSquareToCellSet(grid, wholeGridSet, 0,0, grid.getWidth(),grid.getHeight());
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..6fec22d
--- /dev/null
+++ b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTest.java
@@ -0,0 +1,39 @@
+package org.stathissideris.ascii2image.test.latex;
+
+import org.junit.Ignore;
+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("test-resources/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;
+  }
+
+  @Ignore
+  @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..1c335ab
--- /dev/null
+++ b/test/java/org/stathissideris/ascii2image/test/latex/LaTeXModeTestBase.java
@@ -0,0 +1,234 @@
+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 actualOutputImagePath = actualOutpuImagePath(testName);
+    System.out.println("Create a directory for:" + actualOutputImagePath + ":"  + new File(actualOutputImagePath).getParentFile().mkdirs());
+    String[] args = Stream.concat(
+        Stream.of(inputFilePath(testName), actualOutputImagePath, "-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("test-resources/latex/%s/in.txt", s);
+  }
+
+  private ImageFile expectedImage(String s) {
+    return new ImageFile(String.format("test-resources/latex/%s/expected.png", s));
+  }
+
+  private ImageFile actualImage(String s) {
+    return new ImageFile(actualOutpuImagePath(s));
+  }
+
+  private String actualOutpuImagePath(String s) {
+    return String.format("target/test-output/latex/%s/actual.png", s);
+  }
+
+
+  private String diffImagePath(String s) {
+    return String.format("target/test-output/latex/%s/diff.png", s);
+  }
+
+  String[] options(String s) throws IOException {
+    return Files.lines(Paths.get(String.format("test-resources/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