/* ======================================================================== * PlantUML : a free UML diagram generator * ======================================================================== * * (C) Copyright 2009-2020, Arnaud Roques * * Project Info: http://plantuml.com * * If you like this project or if you find it useful, you can support us at: * * http://plantuml.com/patreon (only 1$ per month!) * http://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.eps; import java.awt.Color; import java.awt.geom.PathIterator; import java.awt.image.BufferedImage; import java.util.Locale; import java.util.StringTokenizer; import net.sourceforge.plantuml.BackSlash; import net.sourceforge.plantuml.Log; import net.sourceforge.plantuml.graphic.HtmlColorGradient; import net.sourceforge.plantuml.ugraphic.ColorMapper; import net.sourceforge.plantuml.ugraphic.ShadowManager; import net.sourceforge.plantuml.ugraphic.UPath; import net.sourceforge.plantuml.ugraphic.USegment; import net.sourceforge.plantuml.ugraphic.USegmentType; import net.sourceforge.plantuml.utils.MathUtils; import net.sourceforge.plantuml.version.Version; public class EpsGraphics { public static final String END_OF_FILE = "%plantuml done"; protected static final long COEF = 100L; // http://www.linuxfocus.org/Francais/May1998/article43.html // http://www.tailrecursive.org/postscript/text.html private final StringBuilder body = new StringBuilder(); private final StringBuilder header = new StringBuilder(); private Color color = Color.BLACK; private Color fillcolor = Color.BLACK; private String strokeWidth = format(1); // private String strokeDasharray = null; private final PostScriptCommandMacro setcolorgradient = new PostScriptCommandMacro("setcolorgradient"); private final PostScriptCommandMacro simplerect = new PostScriptCommandMacro("simplerect"); private final PostScriptCommandMacro roundrect = new PostScriptCommandMacro("roundrect"); private boolean setcolorgradientUsed = false; private boolean simplerectUsed = false; private boolean roundrectUsed = false; public EpsGraphics() { header.append("%!PS-Adobe-3.0 EPSF-3.0\n"); header.append("%%Creator: PlantUML v" + Version.versionString(15) + BackSlash.NEWLINE); header.append("%%Title: noTitle\n"); // header.append("%%CreationDate: " + new Date() + BackSlash.BS_N); setcolorgradient.add(new PostScriptCommandRaw("3 index 7 index sub 1 index mul 7 index add", true)); setcolorgradient.add(new PostScriptCommandRaw("3 index 7 index sub 2 index mul 7 index add", true)); setcolorgradient.add(new PostScriptCommandRaw("3 index 7 index sub 3 index mul 7 index add", true)); setcolorgradient.add(new PostScriptCommandRaw("setrgbcolor", true)); // setcolorgradient.add(new PostScriptCommandRaw("0 7 1 {pop} for")); setcolorgradient.add(new PostScriptCommandRaw("pop pop pop pop pop pop pop ", true)); simplerect.add(new PostScriptCommandRaw("newpath moveto 1 index 0 rlineto", true)); simplerect.add(new PostScriptCommandRaw("0 exch rlineto", true)); simplerect.add(new PostScriptCommandRaw("neg 0 rlineto", true)); roundrect.add(new PostScriptCommandRaw("newpath", true)); roundrect.add(new PostScriptCommandRaw("dup 3 index add 2 index 2 index add 2 index 180 270 arc", true)); roundrect.add(new PostScriptCommandRaw("2 index 5 index add 1 index sub 2 index 2 index add 2 index 270 0 arc", true)); roundrect.add(new PostScriptCommandRaw( "2 index 5 index add 1 index sub 2 index 5 index add 2 index sub 2 index 0 90 arc", true)); roundrect.add(new PostScriptCommandRaw("dup 3 index add 2 index 5 index add 2 index sub 2 index 90 180 arc", true)); roundrect.add(new PostScriptCommandRaw("pop pop pop pop pop ", true)); } private boolean closeDone = false; private int maxX = 10; private int maxY = 10; final protected void ensureVisible(double x, double y) { if (x > maxX) { maxX = (int) (x + 1); } if (y > maxY) { maxY = (int) (y + 1); } if (urlArea != null) { urlArea.ensureVisible((int) Math.round(x), (int) Math.round(y)); } } protected final Color getColor() { return color; } public void close() { checkCloseDone(); header.append("%%BoundingBox: 0 0 " + maxX + " " + maxY + BackSlash.NEWLINE); // header.append("%%DocumentData: Clean7Bit\n"); // header.append("%%DocumentProcessColors: Black\n"); header.append("%%ColorUsage: Color\n"); header.append("%%Origin: 0 0\n"); header.append("%%EndComments\n\n"); header.append("gsave\n"); header.append("0 " + maxY + " translate\n"); header.append(".01 -.01 scale\n"); if (setcolorgradientUsed) { header.append(setcolorgradient.getPostStringDefinition()); } if (simplerectUsed) { header.append(simplerect.getPostStringDefinition()); } if (roundrectUsed) { header.append(roundrect.getPostStringDefinition()); } append("grestore", true); // if(isClipSet()) // writer.write("grestore\n"); append("showpage", true); append(END_OF_FILE, true); append("%%EOF", true); closeDone = true; } private void checkCloseDone() { if (closeDone) { throw new IllegalStateException(); } } public String getEPSCode() { if (closeDone == false) { close(); } return header.toString() + getBodyString(); } protected String getBodyString() { return body.toString(); } public final void setStrokeColor(Color c) { checkCloseDone(); this.color = c; } public void setFillColor(Color c) { checkCloseDone(); this.fillcolor = c; } public final void setStrokeWidth(double strokeWidth, double dashVisible, double dashSpace) { checkCloseDone(); this.strokeWidth = format(strokeWidth); this.dashVisible = (long) (dashVisible * COEF); this.dashSpace = (long) (dashSpace * COEF); } private long dashVisible = 0; private long dashSpace = 0; public void newpathDot() { final boolean dashed = isDashed(); checkCloseDone(); append(strokeWidth + " setlinewidth", true); appendColor(color); if (dashed) { append("[" + dashSpace + " " + dashVisible + "] 0 setdash", true); } append("newpath", true); } private boolean isDashed() { return dashVisible != 0 || dashSpace != 0; } private boolean isDashed2() { return dashVisible == 0 || dashSpace == 0; } private boolean isDashed3() { return dashSpace != 0 && dashVisible != 0; } public void closepathDot() { final boolean dashed = isDashed(); append("stroke", true); if (dashed) { append("[] 0 setdash", true); } } public void epsLine(double x1, double y1, double x2, double y2) { ensureVisible(x1, y1); ensureVisible(x2, y2); checkCloseDone(); append(strokeWidth + " setlinewidth", true); appendColor(color); append("newpath", true); if (isDashed2()) { append(format(x1) + " " + format(y1) + " moveto", true); append(format(x2 - x1) + " " + format(y2 - y1) + " rlineto", true); } else if (x1 == x2) { epsHLine(x1, Math.min(y1, y2), Math.max(y1, y2)); } else if (y1 == y2) { epsVLine(y1, Math.min(x1, x2), Math.max(x1, x2)); } append("stroke", true); ensureVisible(Math.max(x1, x2), Math.max(y1, y2)); } protected void epsHLine(final double x, final double ymin, final double ymax) { append(format(x) + " " + format(ymin) + " moveto", true); for (long y2 = (long) (ymin * COEF); y2 < (long) (ymax * COEF); y2 += (dashVisible + dashSpace)) { final long v; if (y2 + dashVisible > (long) (ymax * COEF)) { v = y2 - (long) (ymax * COEF); } else { v = dashSpace; } append("0 " + v + " rlineto", true); append("0 " + dashSpace + " rmoveto", true); } } protected void epsVLine(final double y, final double xmin, final double xmax) { append(format(xmin) + " " + format(y) + " moveto", true); for (long x2 = (long) (xmin * COEF); x2 < (long) (xmax * COEF); x2 += (dashVisible + dashSpace)) { final long v; if (x2 + dashVisible > (long) (xmax * COEF)) { v = x2 - (long) (xmax * COEF); } else { v = dashSpace; } append("" + v + " 0 rlineto", true); append("" + dashSpace + " 0 rmoveto", true); } } public void epsPath(double x, double y, UPath path) { checkCloseDone(); if (fillcolor != null) { appendColor(fillcolor); append("newpath", true); for (USegment seg : path) { final USegmentType type = seg.getSegmentType(); final double coord[] = seg.getCoord(); if (type == USegmentType.SEG_MOVETO) { movetoNoMacro(coord[0] + x, coord[1] + y); } else if (type == USegmentType.SEG_LINETO) { linetoNoMacro(coord[0] + x, coord[1] + y); } else if (type == USegmentType.SEG_QUADTO) { throw new UnsupportedOperationException(); } else if (type == USegmentType.SEG_CUBICTO) { curvetoNoMacro(coord[0] + x, coord[1] + y, coord[2] + x, coord[3] + y, coord[4] + x, coord[5] + y); } else if (type == USegmentType.SEG_CLOSE) { // Nothing } else { Log.println("unknown1 " + seg); } } append("closepath eofill", true); } if (color != null) { append(strokeWidth + " setlinewidth", true); appendColor(color); append("newpath", true); for (USegment seg : path) { final USegmentType type = seg.getSegmentType(); final double coord[] = seg.getCoord(); if (type == USegmentType.SEG_MOVETO) { movetoNoMacro(coord[0] + x, coord[1] + y); } else if (type == USegmentType.SEG_LINETO) { linetoNoMacro(coord[0] + x, coord[1] + y); } else if (type == USegmentType.SEG_QUADTO) { throw new UnsupportedOperationException(); } else if (type == USegmentType.SEG_CUBICTO) { curvetoNoMacro(coord[0] + x, coord[1] + y, coord[2] + x, coord[3] + y, coord[4] + x, coord[5] + y); } else if (type == USegmentType.SEG_CLOSE) { // Nothing } else { Log.println("unknown2 " + seg); } } append("stroke", true); } } public void epsPolygon(HtmlColorGradient gr, ColorMapper mapper, double... points) { assert points.length % 2 == 0; setFillColor(mapper.getMappedColor(gr.getColor1())); epsPolygon(points); } public void epsPolygon(double... points) { assert points.length % 2 == 0; checkCloseDone(); double lastX = 0; double lastY = 0; if (fillcolor != null) { appendColor(fillcolor); append("newpath", true); for (int i = 0; i < points.length; i += 2) { ensureVisible(points[i], points[i + 1]); if (i == 0) { append(format(points[i]) + " " + format(points[i + 1]) + " moveto", true); } else { append(format(points[i] - lastX) + " " + format(points[i + 1] - lastY) + " rlineto", true); } lastX = points[i]; lastY = points[i + 1]; } append(format(points[0]) + " " + format(points[1]) + " lineto", true); append("closepath eofill", true); } if (color != null) { append(strokeWidth + " setlinewidth", true); appendColor(color); append("newpath", true); for (int i = 0; i < points.length; i += 2) { ensureVisible(points[i], points[i + 1]); if (i == 0) { append(format(points[i]) + " " + format(points[i + 1]) + " moveto", true); } else { append(format(points[i] - lastX) + " " + format(points[i + 1] - lastY) + " rlineto", true); } lastX = points[i]; lastY = points[i + 1]; } append(format(points[0]) + " " + format(points[1]) + " lineto", true); append("closepath stroke", true); } } public void epsRectangle(double x, double y, double width, double height, double rx, double ry) { checkCloseDone(); ensureVisible(x, y); ensureVisible(x + width, y + height); if (fillcolor != null) { appendColor(fillcolor); epsRectangleInternal(x, y, width, height, rx, ry, true); append("closepath eofill", true); if (isDashed3()) { append("[] 0 setdash", true); } } if (color != null) { append(strokeWidth + " setlinewidth", true); appendColor(color); epsRectangleInternal(x, y, width, height, rx, ry, false); append("closepath stroke", true); if (isDashed3()) { append("[] 0 setdash", true); } } } public void epsRectangle(double x, double y, double width, double height, double rx, double ry, HtmlColorGradient gr, ColorMapper mapper) { checkCloseDone(); ensureVisible(x, y); ensureVisible(x + width, y + height); setcolorgradientUsed = true; if (rx == 0 && ry == 0) { simplerectUsed = true; appendColorShort(mapper.getMappedColor(gr.getColor1())); appendColorShort(mapper.getMappedColor(gr.getColor2())); append(format(width) + " " + format(height) + " " + format(x) + " " + format(y), true); append("100 -1 1 {", true); append("100 div", true); append("newpath", true); append("2 index 2 index moveto", true); append("dup 5 index mul 2 mul dup 0 rlineto", true); append("neg 4 index 2 index mul 2 mul rlineto", true); append("closepath eoclip", true); append("10 index 10 index 10 index", true); append("10 index 10 index 10 index", true); append("6 index setcolorgradient", true); append("4 index 4 index 4 index 4 index simplerect", true); append("closepath eofill", true); append("pop", true); append("} for", true); append("pop pop pop pop", true); append("pop pop pop", true); append("pop pop pop", true); append("initclip", true); } else { roundrectUsed = true; appendColorShort(mapper.getMappedColor(gr.getColor1())); appendColorShort(mapper.getMappedColor(gr.getColor2())); append(format(width) + " " + format(height) + " " + format(x) + " " + format(y) + " " + format((rx + ry) / 2), true); append("100 -1 1 {", true); append("100 div", true); append("newpath", true); append("3 index 3 index moveto", true); append("dup 6 index mul 2 mul dup 0 rlineto", true); append("neg 5 index 2 index mul 2 mul rlineto", true); append("closepath eoclip", true); append("11 index 11 index 11 index", true); append("11 index 11 index 11 index", true); append("6 index setcolorgradient", true); append("5 index 5 index 5 index 5 index 5 index roundrect", true); append("closepath eofill", true); append("pop", true); append("} for", true); append("pop pop pop pop pop", true); append("pop pop pop", true); append("pop pop pop", true); append("initclip", true); } } private void epsRectangleInternal(double x, double y, double width, double height, double rx, double ry, boolean fill) { if (rx == 0 && ry == 0) { simpleRectangle(x, y, width, height, fill); } else { roundRectangle(x, y, width, height, rx, ry); } } private void roundRectangle(double x, double y, double width, double height, double rx, double ry) { if (isDashed3()) { append("[" + dashSpace + " " + dashVisible + "] 0 setdash", true); } final double round = MathUtils.min((rx + ry) / 2, width / 2, height / 2); append(format(width) + " " + format(height) + " " + format(x) + " " + format(y) + " " + format(round) + " roundrect", true); roundrectUsed = true; } private void simpleRectangle(double x, double y, double width, double height, boolean fill) { if (isDashed3()) { append("[" + dashSpace + " " + dashVisible + "] 0 setdash", true); } // if (isDashed3() || fill) { append(format(width) + " " + format(height) + " " + format(x) + " " + format(y) + " simplerect", true); simplerectUsed = true; // } } public void epsEllipse(double x, double y, double xRadius, double yRadius, double start, double extend) { checkCloseDone(); ensureVisible(x + xRadius, y + yRadius); double scale = 1; if (xRadius != yRadius) { scale = yRadius / xRadius; append("gsave", true); append("1 " + format(scale) + " scale", true); } // if (fillcolor != null) { // appendColor(fillcolor); // append("newpath", true); // append(format(x) + " " + format(y / scale) + " " + format(xRadius) + " 0 360 arc", true); // append("closepath eofill", true); // } if (color != null) { append(strokeWidth + " setlinewidth", true); appendColor(color); append("newpath", true); final double a1 = -start + 180 + 5; final double a2 = -start - extend + 180 - 5; append(format(x) + " " + format(y / scale) + " " + format(xRadius) + " " + format(a1) + " " + format(a2) + " arc", true); append("stroke", true); } if (scale != 1) { append("grestore", true); } } public void epsEllipse(double x, double y, double xRadius, double yRadius) { checkCloseDone(); ensureVisible(x + xRadius, y + yRadius); double scale = 1; if (xRadius != yRadius) { scale = yRadius / xRadius; append("gsave", true); append("1 " + formatSimple4(scale) + " scale", true); } if (fillcolor != null) { appendColor(fillcolor); append("newpath", true); append(format(x) + " " + format(y / scale) + " " + format(xRadius) + " 0 360 arc", true); append("closepath eofill", true); } if (color != null) { append(strokeWidth + " setlinewidth", true); appendColor(color); append("newpath", true); append(format(x) + " " + format(y / scale) + " " + format(xRadius) + " 0 360 arc", true); append("closepath stroke", true); } if (scale != 1) { append("grestore", true); } } protected void appendColor(Color c) { if (c == null) { return; } final double r = c.getRed() / 255.0; final double g = c.getGreen() / 255.0; final double b = c.getBlue() / 255.0; append(formatSimple2(r) + " " + formatSimple2(g) + " " + formatSimple2(b) + " setrgbcolor", true); } protected void appendColorShort(Color c) { if (c == null) { return; } final double r = c.getRed() / 255.0; final double g = c.getGreen() / 255.0; final double b = c.getBlue() / 255.0; append(formatSimple2(r) + " " + formatSimple2(g) + " " + formatSimple2(b), true); } static String format(double x) { if (x == 0) { return "0"; } return Long.toString((long) (x * COEF)); } public static String formatSimple4(double x) { if (x == 0) { return "0"; } String s = String.format(Locale.US, "%1.4f", x); s = s.replaceAll("(\\.\\d*?)0+$", "$1"); if (s.endsWith(".")) { s = s.substring(0, s.length() - 1); } return s; } private static String formatSimple2(double x) { if (x == 0) { return "0"; } String s = String.format(Locale.US, "%1.2f", x); s = s.replaceAll("(\\.\\d*?)0+$", "$1"); if (s.endsWith(".")) { s = s.substring(0, s.length() - 1); } return s; } protected void append(String s, boolean checkConsistence) { if (checkConsistence && s.indexOf(" ") != -1) { throw new IllegalArgumentException(s); } body.append(s + BackSlash.NEWLINE); } final public void linetoNoMacro(double x1, double y1) { append(format(x1) + " " + format(y1) + " lineto", true); ensureVisible(x1, y1); } final public void movetoNoMacro(double x1, double y1) { append(format(x1) + " " + format(y1) + " moveto", true); ensureVisible(x1, y1); } final public void curvetoNoMacro(double x1, double y1, double x2, double y2, double x3, double y3) { append(format(x1) + " " + format(y1) + " " + format(x2) + " " + format(y2) + " " + format(x3) + " " + format(y3) + " curveto", true); ensureVisible(x1, y1); ensureVisible(x2, y2); ensureVisible(x3, y3); } // FONT public void moveto(double x1, double y1) { append(format(x1) + " " + format(y1) + " moveto", true); ensureVisible(x1, y1); } public void lineto(double x1, double y1) { append(format(x1) + " " + format(y1) + " lineto", true); ensureVisible(x1, y1); } public void curveto(double x1, double y1, double x2, double y2, double x3, double y3) { append(format(x1) + " " + format(y1) + " " + format(x2) + " " + format(y2) + " " + format(x3) + " " + format(y3) + " curveto", true); ensureVisible(x1, y1); ensureVisible(x2, y2); ensureVisible(x3, y3); } public void quadto(double x1, double y1, double x2, double y2) { append(format(x1) + " " + format(y1) + " " + format(x1) + " " + format(y1) + " " + format(x2) + " " + format(y2) + " curveto", true); ensureVisible(x1, y1); ensureVisible(x2, y2); } public void newpath() { append("0 setlinewidth", true); appendColor(color); append("newpath", true); } public void closepath() { append("closepath", true); } public void fill(int windingRule) { append("%fill", true); if (windingRule == PathIterator.WIND_EVEN_ODD) { append("eofill", true); } else if (windingRule == PathIterator.WIND_NON_ZERO) { append("fill", true); } } public void drawImage(BufferedImage image, double x, double y) { final int width = image.getWidth(); final int height = image.getHeight(); append("gsave", true); append(format(x) + " " + format(y) + " translate", true); append(format(width) + " " + format(height) + " scale", true); append("" + width + " " + height + " 8 [" + width + " 0 0 -" + height + " 0 " + height + "]", true); // append("" + width + " " + height + " 8 [0 0 0 0 0 0]"); append("{<", true); final StringBuilder sb = new StringBuilder(); for (int j = height - 1; j >= 0; j--) { for (int i = 0; i < width; i++) { final String hexString = getRgb(image.getRGB(i, j)); assert hexString.length() == 6; sb.append(hexString); } } append(sb.toString(), true); // append(">} image"); append(">} false 3 colorimage", true); ensureVisible(x + width, y + height); append("grestore", true); } static String getRgb(int x) { final String s = "000000" + Integer.toHexString(x); return s.substring(s.length() - 6); } public void drawEps(String eps, double x, double y) { final int idx = eps.indexOf("%%BoundingBox:"); if (idx == -1) { throw new IllegalArgumentException(); } final StringTokenizer st = new StringTokenizer(eps.substring(idx + "%%BoundingBox:".length()), " \n\t\r"); final int x1 = Integer.parseInt(st.nextToken()); final int y1 = Integer.parseInt(st.nextToken()); final int x2 = Integer.parseInt(st.nextToken()); final int y2 = Integer.parseInt(st.nextToken()); assert x2 >= x1; assert y2 >= y1; append("gsave", true); final double dx = x - x1; final double dy = y + y2; append(format(dx) + " " + format(dy) + " translate", true); append("1 -1 scale", true); append(eps, false); ensureVisible(x + (x2 - x1), y + (y2 - y1)); append("grestore", true); } protected final long getDashVisible() { return dashVisible; } protected final long getDashSpace() { return dashSpace; } static class UrlArea { private final String url; private int xmin = Integer.MAX_VALUE; private int xmax = Integer.MIN_VALUE; private int ymin = Integer.MAX_VALUE; private int ymax = Integer.MIN_VALUE; UrlArea(String url) { this.url = url; } void ensureVisible(int x, int y) { if (x < xmin) { xmin = x; } if (x > xmax) { xmax = x; } if (y < ymin) { ymin = y; } if (y > ymax) { ymax = y; } } } private UrlArea urlArea; public void closeLink() { if (urlArea != null && urlArea.xmin != Integer.MAX_VALUE) { final int width = urlArea.xmax - urlArea.xmin; final int height = urlArea.ymax - urlArea.ymin; assert width >= 0 && height >= 0; epsUrlLink(urlArea.xmin, urlArea.ymin, width, height, urlArea.url); } this.urlArea = null; } public void epsUrlLink(int x, int y, int width, int height, String url) { append("[ /Rect [ " + x + " " + y + " " + (x + width) + " " + (y + height) + " ]", true); append("/Border [ 0 0 0 ]", true); append("/Action << /Subtype /URI /URI (" + url + ") >>", true); append("/Subtype /Link", true); append("/ANN pdfmark", true); } public void openLink(String url) { this.urlArea = new UrlArea(url); } // Shadow final private ShadowManager shadowManager = new ShadowManager(50, 200); public void epsRectangleShadow(double x, double y, double width, double height, double rx, double ry, double deltaShadow) { setStrokeColor(null); for (double i = 0; i <= deltaShadow; i += 0.5) { setFillColor(shadowManager.getColor(i, deltaShadow)); final double diff = i; epsRectangle(x + deltaShadow + diff, y + deltaShadow + diff, width - 2 * diff, height - 2 * diff, rx + 1, ry + 1); } } public void epsPolygonShadow(double deltaShadow, double... points) { assert points.length % 2 == 0; setStrokeColor(null); for (double i = 0; i <= deltaShadow; i += 0.5) { setFillColor(shadowManager.getColor(i, deltaShadow)); final double diff = i; epsPolygon(shadowManager.getShadowDeltaPoints(deltaShadow, diff, points)); } } public void epsEllipseShadow(double x, double y, double xRadius, double yRadius, double deltaShadow) { setStrokeColor(null); for (double i = 0; i <= deltaShadow; i += 0.5) { setFillColor(shadowManager.getColor(i, deltaShadow)); final double diff = i; epsEllipse(x + deltaShadow, y + deltaShadow, xRadius - diff, yRadius - diff); } } }