PngCanvas.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2025, 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.teavm.c;
import net.sourceforge.plantuml.klimt.drawing.raster.ArcsToCubics;
import net.sourceforge.plantuml.klimt.drawing.raster.RasterCanvas;
import net.sourceforge.plantuml.klimt.drawing.raster.RasterPath;
import net.sourceforge.plantuml.klimt.font.UFont;
import net.sourceforge.plantuml.klimt.font.UFontFace;
/**
* The {@link RasterCanvas} of the TeaVM C build: drawn by plutovg (see
* {@link NativeCanvas}), text with the fonts embedded in the executable
* (Liberation Sans, whatever the family: the other families are not embedded
* yet), never the fonts of the machine. The PNG is encoded in C, as PngIO does
* on the JVM (src/teavm-c/native/plantuml_png.h).
*
* A canvas holds native memory: {@link #dispose} releases it.
*/
public final class PngCanvas implements RasterCanvas {
// ::remove file when JAVA8
private long handle;
private final boolean opaque;
private double scale = 1;
// The clip set on the native canvas, which only changes it when it differs
// (setting a clip saves and restores the native state)
private boolean clipped;
private double clipX;
private double clipY;
private double clipWidth;
private double clipHeight;
private final ArcsToCubics replay = new ArcsToCubics() {
@Override
protected void doMoveTo(double x, double y) {
NativeCanvas.moveTo(handle, x, y);
}
@Override
protected void doLineTo(double x, double y) {
NativeCanvas.lineTo(handle, x, y);
}
@Override
protected void doCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) {
NativeCanvas.cubicTo(handle, x1, y1, x2, y2, x3, y3);
}
@Override
protected void doClosePath() {
NativeCanvas.closePath(handle);
}
@Override
public void rectangle(double x, double y, double width, double height, double arcWidth, double arcHeight) {
// Arcs are diameters, the native rectangle takes radii
NativeCanvas.rect(handle, x, y, width, height, arcWidth / 2, arcHeight / 2);
}
@Override
public void ellipse(double x, double y, double width, double height) {
NativeCanvas.ellipse(handle, x + width / 2, y + height / 2, width / 2, height / 2);
}
};
/**
* @param opaque the image will get an opaque background, see
* {@link RasterCanvas.Factory}
*/
public PngCanvas(int width, int height, boolean opaque) {
this.opaque = opaque;
if (width <= 0 || height <= 0)
throw new IllegalArgumentException("Bad size " + width + "x" + height);
this.handle = NativeCanvas.create(width, height);
if (handle == 0)
throw new IllegalStateException("Cannot create a " + width + "x" + height + " canvas");
}
private long handle() {
if (handle == 0)
throw new IllegalStateException("Canvas disposed");
return handle;
}
@Override
public void scale(double factor) {
NativeCanvas.scale(handle(), factor);
scale *= factor;
}
@Override
public void setColor(int argb) {
NativeCanvas.setColor(handle(), argb);
}
@Override
public void setLinearGradient(double x1, double y1, double x2, double y2, double[] offsets, int[] colors,
int spread) {
NativeCanvas.setLinearGradient(handle(), x1, y1, x2, y2, offsets, colors, Math.min(offsets.length, 16),
spread);
}
@Override
public void setStroke(double thickness, double dashVisible, double dashSpace) {
// Java2D draws a zero width as the thinnest line: one device pixel
if (thickness <= 0)
thickness = 1 / scale;
NativeCanvas.setStroke(handle(), thickness, dashVisible, dashSpace);
}
@Override
public void setClip(double x, double y, double width, double height) {
if (clipped && x == clipX && y == clipY && width == clipWidth && height == clipHeight)
return;
NativeCanvas.setClip(handle(), x, y, width, height);
clipped = true;
clipX = x;
clipY = y;
clipWidth = width;
clipHeight = height;
}
@Override
public void resetClip() {
if (clipped == false)
return;
NativeCanvas.resetClip(handle());
clipped = false;
}
@Override
public void fill(RasterPath path) {
handle();
path.visit(replay);
NativeCanvas.fill(handle);
}
@Override
public void stroke(RasterPath path) {
handle();
path.visit(replay);
NativeCanvas.stroke(handle);
}
@Override
public void drawString(String text, UFont font, double x, double y, double angle) {
setFont(font);
final char[] chars = text.toCharArray();
NativeCanvas.fillText(handle, chars, chars.length, x, y, angle);
}
/**
* Advance width of the text, as the native canvas draws it.
*/
public double getTextWidth(String text, UFont font) {
setFont(font);
final char[] chars = text.toCharArray();
return NativeCanvas.textWidth(handle, chars, chars.length);
}
/**
* @return ascent, descent (both positive) and line gap of the font
*/
public double[] getFontMetrics(UFont font) {
setFont(font);
final double[] metrics = new double[3];
NativeCanvas.fontMetrics(handle, metrics);
return metrics;
}
private void setFont(UFont font) {
final UFontFace face = font.getFontFace();
final int id;
if (face.isBold())
id = face.isItalic() ? NativeCanvas.FONT_SANS_BOLD_ITALIC : NativeCanvas.FONT_SANS_BOLD;
else
id = face.isItalic() ? NativeCanvas.FONT_SANS_ITALIC : NativeCanvas.FONT_SANS;
if (NativeCanvas.setFont(handle(), id, font.getSize2D()) != 0)
throw new IllegalStateException("No embedded font");
}
@Override
public byte[] toPng() {
final int size = NativeCanvas.encodePng(handle(), opaque ? 1 : 0);
if (size < 0)
throw new IllegalStateException("PNG encoding failed");
final byte[] result = new byte[size];
NativeCanvas.copyPng(handle, result);
return result;
}
@Override
public void dispose() {
if (handle != 0) {
NativeCanvas.destroy(handle);
handle = 0;
}
}
}