UGraphicTeXGraphics.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml.math;
import java.util.ArrayList;
import java.util.List;
import org.scilab.forge.jlatexmath.FontInfo;
import org.scilab.forge.jlatexmath.GlyphOutlines;
import org.scilab.forge.jlatexmath.TeXColor;
import org.scilab.forge.jlatexmath.TeXFormula;
import org.scilab.forge.jlatexmath.TeXGraphics;
import net.sourceforge.plantuml.klimt.UPath;
import net.sourceforge.plantuml.klimt.UStroke;
import net.sourceforge.plantuml.klimt.UTranslate;
import net.sourceforge.plantuml.klimt.awt.XColor;
import net.sourceforge.plantuml.klimt.color.HColor;
import net.sourceforge.plantuml.klimt.color.HColors;
import net.sourceforge.plantuml.klimt.drawing.UGraphic;
import net.sourceforge.plantuml.klimt.shape.UText;
import net.sourceforge.plantuml.klimt.font.FontConfiguration;
import net.sourceforge.plantuml.klimt.font.UFontFactory;
import net.sourceforge.plantuml.klimt.geom.USegment;
/**
* The canvas of JLaTeXMath (see PLAN.md, phase 2) drawn on a {@link UGraphic}:
* a formula is only paths, so that every output (SVG, PNG, PDF, EMF, the C
* build...) gets it the same way, with no font and no Batik.
* <p>
* UGraphic only knows translations, so this class keeps its own affine
* transformation (scale, rotate, translate of the boxes) and gives the shapes
* to the UGraphic with their points already transformed. A glyph is a filled
* path made from glyphs.bin, a line is a stroked path.
* <p>
* The origin of the formula is the origin of the {@code UGraphic} given to the
* constructor.
* <p>
* The color of the formula is an {@link HColor}, given as is to the UGraphic
* (which applies its color mapper, as for any shape). A color set by the boxes
* (an AWT color, from \color) replaces it until it is set back.
*/
public class UGraphicTeXGraphics implements TeXGraphics {
// ::remove file when __MIT__ or __EPL__ or __BSD__ or __ASL__ or __LGPL__
/** Control point of a cubic curve that approximates a quarter of circle. */
private static final double KAPPA = 0.5522847498307936;
private final UGraphic ug;
// x' = a x + c y + e ; y' = b x + d y + f
private double a = 1;
private double b = 0;
private double c = 0;
private double d = 1;
private double e = 0;
private double f = 0;
/** Stands for the foreground HColor in the colors that the boxes save and restore. */
private final TeXColor foregroundMark = new TeXColor(0, 0, 0);
private final HColor foreground;
private TeXColor color = foregroundMark;
private float lineWidth = 1;
/**
* The glyphs that follow each other with the same color are gathered in one
* path: the rasterizer then fills their union. Drawn one by one, the parts
* that overlap (the pieces of an extensible arrow or brace) are painted
* twice, and the edge pixels that they share get darker.
*/
private UPath pendingGlyphs;
private HColor pendingColor;
private final List<double[]> savedTransforms = new ArrayList<double[]>();
private final List<Float> savedStrokes = new ArrayList<Float>();
public UGraphicTeXGraphics(UGraphic ug, HColor foreground) {
this.ug = ug;
this.foreground = foreground == null ? HColors.BLACK : foreground;
}
public TeXColor getColor() {
return color;
}
public void setColor(TeXColor color) {
if (color != this.color)
flush();
this.color = color;
}
public void setLineWidth(float width) {
this.lineWidth = width;
}
public void saveStroke() {
savedStrokes.add(Float.valueOf(lineWidth));
}
public void restoreStroke() {
lineWidth = savedStrokes.remove(savedStrokes.size() - 1).floatValue();
}
// ------------------------------------------------------------------ shapes
public void fillRect(float x, float y, float width, float height) {
final UPath path = UPath.none();
rect(path, x, y, width, height);
fill(path);
}
public void drawRect(float x, float y, float width, float height) {
final UPath path = UPath.none();
rect(path, x, y, width, height);
stroke(path);
}
public void drawRoundRect(float x, float y, float width, float height, float arcWidth, float arcHeight) {
// As java.awt.geom.RoundRectangle2D: the arcs are the diameters of the corners
final double rx = Math.min(Math.max(arcWidth, 0) / 2, width / 2);
final double ry = Math.min(Math.max(arcHeight, 0) / 2, height / 2);
final double x2 = x + width;
final double y2 = y + height;
final double kx = rx * KAPPA;
final double ky = ry * KAPPA;
final UPath path = UPath.none();
moveTo(path, x + rx, y);
lineTo(path, x2 - rx, y);
cubicTo(path, x2 - rx + kx, y, x2, y + ry - ky, x2, y + ry);
lineTo(path, x2, y2 - ry);
cubicTo(path, x2, y2 - ry + ky, x2 - rx + kx, y2, x2 - rx, y2);
lineTo(path, x + rx, y2);
cubicTo(path, x + rx - kx, y2, x, y2 - ry + ky, x, y2 - ry);
lineTo(path, x, y + ry);
cubicTo(path, x, y + ry - ky, x + rx - kx, y, x + rx, y);
stroke(path);
}
public void drawLine(float x1, float y1, float x2, float y2) {
final UPath path = UPath.none();
moveTo(path, x1, y1);
lineTo(path, x2, y2);
stroke(path);
}
public void drawChar(int fontId, char ch) {
final FontInfo info = FontInfo.getFontInfo(fontId);
final GlyphOutlines.GlyphFont glyphs = info == null ? null : info.getGlyphs();
if (glyphs == null)
return;
// The AWT font was at PIXELS_PER_POINT * FONT_SCALE_FACTOR, 1 em
final double k = TeXFormula.PIXELS_PER_POINT * TeXFormula.FONT_SCALE_FACTOR / glyphs.getUnitsPerEm();
final UPath path = UPath.none();
glyphs.visit(ch, new GlyphOutlines.Sink() {
private double px;
private double py;
public void moveTo(int x, int y) {
px = x * k;
py = -y * k;
UGraphicTeXGraphics.this.moveTo(path, px, py);
}
public void lineTo(int x, int y) {
final double nx = x * k;
final double ny = -y * k;
// some fonts repeat a point: nothing to draw, and a shorter path
if (nx == px && ny == py)
return;
px = nx;
py = ny;
UGraphicTeXGraphics.this.lineTo(path, px, py);
}
public void quadTo(int cx, int cy, int x, int y) {
final double qx = cx * k;
final double qy = -cy * k;
final double ex = x * k;
final double ey = -y * k;
// quadratic to cubic: control points at 2/3 of the way to the control point
UGraphicTeXGraphics.this.cubicTo(path, px + 2.0 / 3.0 * (qx - px), py + 2.0 / 3.0 * (qy - py),
ex + 2.0 / 3.0 * (qx - ex), ey + 2.0 / 3.0 * (qy - ey), ex, ey);
px = ex;
py = ey;
}
public void close() {
path.closePath();
}
});
if (path.isEmpty())
return;
final HColor fore = current();
if (pendingGlyphs != null && pendingColor.equals(fore) == false)
flush();
if (pendingGlyphs == null) {
pendingGlyphs = UPath.none();
pendingColor = fore;
}
for (USegment segment : path)
pendingGlyphs.add(segment.getCoord(), segment.getSegmentType());
}
/**
* Draws the glyphs that are waiting. Done before anything else is drawn, so
* that the order of the drawing is kept, and at the end of the formula.
*/
public void flush() {
if (pendingGlyphs == null)
return;
final UPath path = pendingGlyphs;
final HColor fore = pendingColor;
pendingGlyphs = null;
pendingColor = null;
ug.apply(HColors.transparent()).apply(fore.bg()).apply(UStroke.withThickness(0)).draw(path);
}
public void drawText(String text, double fontSize) {
// The text is drawn by the font engine of the UGraphic: it can be neither rotated nor
// flipped, and its size is a whole number of pixels
flush();
final double scale = Math.sqrt(Math.abs(a * d - b * c));
final int size = Math.max(1, (int) Math.round(fontSize * scale));
final HColor fore = current();
final FontConfiguration fontConfiguration = FontConfiguration.create(UFontFactory.sansSerif(size), fore, fore,
UStroke.simple(), 8);
ug.apply(new UTranslate(tx(0, 0), ty(0, 0))).draw(UText.build(text, fontConfiguration));
}
// ------------------------------------------------------------------ transformation
public void translate(double tx, double ty) {
e += a * tx + c * ty;
f += b * tx + d * ty;
}
public void scale(double sx, double sy) {
a *= sx;
b *= sx;
c *= sy;
d *= sy;
}
public void rotate(double theta) {
// StrictMath: the same result on every platform, so that the output is deterministic
final double cos = StrictMath.cos(theta);
final double sin = StrictMath.sin(theta);
final double na = a * cos + c * sin;
final double nb = b * cos + d * sin;
final double nc = -a * sin + c * cos;
final double nd = -b * sin + d * cos;
a = na;
b = nb;
c = nc;
d = nd;
}
public void rotate(double theta, double x, double y) {
translate(x, y);
rotate(theta);
translate(-x, -y);
}
public double getScaleX() {
return a;
}
public void saveTransform() {
savedTransforms.add(new double[] { a, b, c, d, e, f });
}
public void restoreTransform() {
final double[] m = savedTransforms.remove(savedTransforms.size() - 1);
a = m[0];
b = m[1];
c = m[2];
d = m[3];
e = m[4];
f = m[5];
}
// ------------------------------------------------------------------ helpers
private void rect(UPath path, double x, double y, double width, double height) {
moveTo(path, x, y);
lineTo(path, x + width, y);
lineTo(path, x + width, y + height);
lineTo(path, x, y + height);
lineTo(path, x, y);
path.closePath();
}
private double tx(double x, double y) {
return a * x + c * y + e;
}
private double ty(double x, double y) {
return b * x + d * y + f;
}
private void moveTo(UPath path, double x, double y) {
path.moveTo(tx(x, y), ty(x, y));
}
private void lineTo(UPath path, double x, double y) {
path.lineTo(tx(x, y), ty(x, y));
}
private void cubicTo(UPath path, double x1, double y1, double x2, double y2, double x, double y) {
path.cubicTo(tx(x1, y1), ty(x1, y1), tx(x2, y2), ty(x2, y2), tx(x, y), ty(x, y));
}
private HColor current() {
if (color == foregroundMark)
return foreground;
return HColors.simple(new XColor(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
}
/** Filled with the current color, no outline. */
private void fill(UPath path) {
flush();
final HColor fore = current();
ug.apply(HColors.transparent()).apply(fore.bg()).apply(UStroke.withThickness(0)).draw(path);
}
/** Outline only, with the current color and line width (scaled as the lines were). */
private void stroke(UPath path) {
flush();
final double scale = Math.sqrt(Math.abs(a * d - b * c));
ug.apply(current()).apply(HColors.transparent().bg()).apply(UStroke.withThickness(lineWidth * scale)).draw(path);
}
}