org.stathissideris.ascii2image.graphics.BitmapRenderer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of plantuml-gplv2 Show documentation
Show all versions of plantuml-gplv2 Show documentation
PlantUML is a component that allows to quickly write diagrams from text.
// THIS FILE HAS BEEN GENERATED BY A PREPROCESSOR.
/* +=======================================================================
* |
* | 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/liberapay (only 1€ per month!)
* https://plantuml.com/paypal
*
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License V2.
*
* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
* LICENSE ("AGREEMENT"). [GNU General Public License V2]
*
* ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES
* RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
*
* You may obtain a copy of the License at
*
* https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* PlantUML can occasionally display sponsored or advertising messages. Those
* messages are usually generated on welcome or error images and never on
* functional diagrams.
* See https://plantuml.com/professional if you want to remove them
*
* Images (whatever their format : PNG, SVG, EPS...) generated by running PlantUML
* are owned by the author of their corresponding sources code (that is, their
* textual description in PlantUML language). Those images are not covered by
* this GPL v2 license.
*
* The generated images can then be used without any reference to the GPL v2 license.
* It is not even necessary to stipulate that they have been generated with PlantUML,
* although this will be appreciated by the PlantUML team.
*
* There is an exception : if the textual description in PlantUML language is also covered
* by any license, then the generated images are logically covered
* by the very same license.
*
* This is the IGY distribution (Install GraphViz by Yourself).
* You have to install GraphViz and to setup the GRAPHVIZ_DOT environment variable
* (see https://plantuml.com/graphviz-dot )
*
* Icons provided by OpenIconic : https://useiconic.com/open
* Archimate sprites provided by Archi : http://www.archimatetool.com
* Stdlib AWS provided by https://github.com/milo-minderbinder/AWS-PlantUML
* Stdlib Icons provided https://github.com/tupadr3/plantuml-icon-font-sprites
* ASCIIMathML (c) Peter Jipsen http://www.chapman.edu/~jipsen
* ASCIIMathML (c) David Lippman http://www.pierce.ctc.edu/dlippman
* CafeUndZopfli ported by Eugene Klyuchnikov https://github.com/eustas/CafeUndZopfli
* Brotli (c) by the Brotli Authors https://github.com/google/brotli
* Themes (c) by Brett Schwarz https://github.com/bschwarz/puml-themes
* Twemoji (c) by Twitter at https://twemoji.twitter.com/
*
*/
package org.stathissideris.ascii2image.graphics;
import java.awt.BasicStroke;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.RenderedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import org.stathissideris.ascii2image.core.RenderingOptions;
import org.stathissideris.ascii2image.core.Shape3DOrderingComparator;
/**
*
* @author Efstathios Sideris
*/
public class BitmapRenderer {
// ::remove folder when __CORE__
private static final boolean DEBUG = false;
private static final String IDREGEX = "^.+_vfill$";
Stroke normalStroke;
Stroke dashStroke;
public RenderedImage renderToImage(Diagram diagram, RenderingOptions options){
BufferedImage image;
if(options.needsTransparency()) {
image = new BufferedImage(
diagram.getWidth(),
diagram.getHeight(),
BufferedImage.TYPE_INT_ARGB);
} else {
image = new BufferedImage(
diagram.getWidth(),
diagram.getHeight(),
BufferedImage.TYPE_INT_RGB);
}
return render(diagram, image, options);
}
public RenderedImage render(Diagram diagram, BufferedImage image, RenderingOptions options){
RenderedImage renderedImage = image;
Graphics2D g2 = image.createGraphics();
Object antialiasSetting = RenderingHints.VALUE_ANTIALIAS_OFF;
if(options.performAntialias())
antialiasSetting = RenderingHints.VALUE_ANTIALIAS_ON;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasSetting);
g2.setColor(options.getBackgroundColor());
//TODO: find out why the next line does not work
g2.fillRect(0, 0, image.getWidth()+10, image.getHeight()+10);
/*for(int y = 0; y < diagram.getHeight(); y ++)
g2.drawLine(0, y, diagram.getWidth(), y);*/
g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));
ArrayList shapes = diagram.getAllDiagramShapes();
if(DEBUG) System.out.println("Rendering "+shapes.size()+" shapes (groups flattened)");
Iterator shapesIt;
if(options.dropShadows()){
//render shadows
shapesIt = shapes.iterator();
while(shapesIt.hasNext()){
DiagramShape shape = (DiagramShape) shapesIt.next();
if(shape.getPoints().isEmpty()) continue;
//GeneralPath path = shape.makeIntoPath();
GeneralPath path;
path = shape.makeIntoRenderPath(diagram);
float offset = diagram.getMinimumOfCellDimension() / 3.333f;
if(path != null
&& shape.dropsShadow()
&& shape.getType() != DiagramShape.TYPE_CUSTOM){
GeneralPath shadow = new GeneralPath(path);
AffineTransform translate = new AffineTransform();
translate.setToTranslation(offset, offset);
shadow.transform(translate);
g2.setColor(new Color(150,150,150));
g2.fill(shadow);
}
}
//blur shadows
if(true) {
int blurRadius = 6;
int blurRadius2 = blurRadius * blurRadius;
float blurRadius2F = blurRadius2;
float weight = 1.0f / blurRadius2F;
float[] elements = new float[blurRadius2];
for (int k = 0; k < blurRadius2; k++)
elements[k] = weight;
Kernel myKernel = new Kernel(blurRadius, blurRadius, elements);
//if EDGE_NO_OP is not selected, EDGE_ZERO_FILL is the default which creates a black border
ConvolveOp simpleBlur =
new ConvolveOp(myKernel, ConvolveOp.EDGE_NO_OP, null);
BufferedImage destination =
new BufferedImage(
image.getWidth(),
image.getHeight(),
image.getType());
simpleBlur.filter(image, (BufferedImage) destination);
//destination = destination.getSubimage(blurRadius/2, blurRadius/2, image.getWidth(), image.getHeight());
g2 = (Graphics2D) destination.getGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasSetting);
renderedImage = (RenderedImage) destination;
}
}
//fill and stroke
float dashInterval = Math.min(diagram.getCellWidth(), diagram.getCellHeight()) / 2;
//Stroke normalStroke = g2.getStroke();
float strokeWeight = diagram.getMinimumOfCellDimension() / 10;
normalStroke =
new BasicStroke(
strokeWeight,
//10,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
);
dashStroke =
new BasicStroke(
strokeWeight,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_ROUND,
0,
new float[] {dashInterval},
0
);
//TODO: at this stage we should draw the open shapes first in order to make sure they are at the bottom (this is useful for the {mo} shape)
//find storage shapes
ArrayList storageShapes = new ArrayList();
shapesIt = shapes.iterator();
while(shapesIt.hasNext()){
DiagramShape shape = (DiagramShape) shapesIt.next();
if(shape.getType() == DiagramShape.TYPE_STORAGE) {
storageShapes.add(shape);
continue;
}
}
//render storage shapes
//special case since they are '3d' and should be
//rendered bottom to top
//TODO: known bug: if a storage object is within a bigger normal box, it will be overwritten in the main drawing loop
//(BUT this is not possible since tags are applied to all shapes overlaping shapes)
Collections.sort(storageShapes, new Shape3DOrderingComparator());
g2.setStroke(normalStroke);
shapesIt = storageShapes.iterator();
while(shapesIt.hasNext()){
DiagramShape shape = (DiagramShape) shapesIt.next();
GeneralPath path;
path = shape.makeIntoRenderPath(diagram);
if(!shape.isStrokeDashed()) {
if(shape.getFillColor() != null)
g2.setColor(shape.getFillColor());
else
g2.setColor(Color.white);
g2.fill(path);
}
if(shape.isStrokeDashed())
g2.setStroke(dashStroke);
else
g2.setStroke(normalStroke);
g2.setColor(shape.getStrokeColor());
g2.draw(path);
}
//render the rest of the shapes
ArrayList pointMarkers = new ArrayList();
shapesIt = shapes.iterator();
while(shapesIt.hasNext()){
DiagramShape shape = (DiagramShape) shapesIt.next();
if(shape.getType() == DiagramShape.TYPE_POINT_MARKER) {
pointMarkers.add(shape);
continue;
}
if(shape.getType() == DiagramShape.TYPE_STORAGE) {
continue;
}
if(shape.getType() == DiagramShape.TYPE_CUSTOM){
renderCustomShape(shape, g2);
continue;
}
if(shape.getPoints().isEmpty()) continue;
int size = shape.getPoints().size();
GeneralPath path;
path = shape.makeIntoRenderPath(diagram);
//fill
if(path != null && shape.isClosed() && !shape.isStrokeDashed()){
if(shape.getFillColor() != null)
g2.setColor(shape.getFillColor());
else
g2.setColor(Color.white);
g2.fill(path);
}
//draw
if(shape.getType() != DiagramShape.TYPE_ARROWHEAD){
g2.setColor(shape.getStrokeColor());
if(shape.isStrokeDashed())
g2.setStroke(dashStroke);
else
g2.setStroke(normalStroke);
g2.draw(path);
}
}
//render point markers
g2.setStroke(normalStroke);
shapesIt = pointMarkers.iterator();
while(shapesIt.hasNext()){
DiagramShape shape = (DiagramShape) shapesIt.next();
//if(shape.getType() != DiagramShape.TYPE_POINT_MARKER) continue;
GeneralPath path;
path = shape.makeIntoRenderPath(diagram);
g2.setColor(Color.white);
g2.fill(path);
g2.setColor(shape.getStrokeColor());
g2.draw(path);
}
//handle text
//g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//renderTextLayer(diagram.getTextObjects().iterator());
Iterator textIt = diagram.getTextObjects().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());
}
if(options.renderDebugLines() || DEBUG){
Stroke debugStroke =
new BasicStroke(
1,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
);
g2.setStroke(debugStroke);
g2.setColor(new Color(170, 170, 170));
g2.setXORMode(Color.white);
for(int x = 0; x < diagram.getWidth(); x += diagram.getCellWidth())
g2.drawLine(x, 0, x, diagram.getHeight());
for(int y = 0; y < diagram.getHeight(); y += diagram.getCellHeight())
g2.drawLine(0, y, diagram.getWidth(), y);
}
g2.dispose();
return renderedImage;
}
private RenderedImage renderTextLayer(ArrayList textObjects, int width, int height){
TextCanvas canvas = new TextCanvas(textObjects);
Image image = canvas.createImage(width, height);
Graphics g = image.getGraphics();
canvas.paint(g);
return (RenderedImage) image;
}
private class TextCanvas extends Canvas {
ArrayList textObjects;
public TextCanvas(ArrayList textObjects){
this.textObjects = textObjects;
}
public void paint(Graphics g){
Graphics 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());
}
}
}
private void renderCustomShape(DiagramShape shape, Graphics2D g2){
CustomShapeDefinition definition = shape.getDefinition();
Rectangle bounds = shape.getBounds();
if(definition.hasBorder()){
g2.setColor(shape.getStrokeColor());
if(shape.isStrokeDashed())
g2.setStroke(dashStroke);
else
g2.setStroke(normalStroke);
g2.drawLine(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y);
g2.drawLine(bounds.x + bounds.width, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height);
g2.drawLine(bounds.x, bounds.y + bounds.height, bounds.x + bounds.width, bounds.y + bounds.height);
g2.drawLine(bounds.x, bounds.y, bounds.x, bounds.y + bounds.height);
// g2.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); //looks different!
}
//TODO: custom shape distintion relies on filename extension. Make this more intelligent
if(definition.getFilename().endsWith(".png")){
renderCustomPNGShape(shape, g2);
} else if(definition.getFilename().endsWith(".svg")){
// renderCustomSVGShape(shape, g2);
throw new UnsupportedOperationException();
}
}
// private void renderCustomSVGShape(DiagramShape shape, Graphics2D g2){
// CustomShapeDefinition definition = shape.getDefinition();
// Rectangle bounds = shape.getBounds();
// Image graphic;
// try {
// if(shape.getFillColor() == null) {
// graphic = ImageHandler.instance().renderSVG(
// definition.getFilename(), bounds.width, bounds.height, definition.stretches());
// } else {
// graphic = ImageHandler.instance().renderSVG(
// definition.getFilename(), bounds.width, bounds.height, definition.stretches(), IDREGEX, shape.getFillColor());
// }
// g2.drawImage(graphic, bounds.x, bounds.y, null);
// } catch (IOException e) {
// Logme.error(e);
// }
// }
private void renderCustomPNGShape(DiagramShape shape, Graphics2D g2){
CustomShapeDefinition definition = shape.getDefinition();
Rectangle bounds = shape.getBounds();
Image graphic = ImageHandler.instance().loadImage(definition.getFilename());
int xPos, yPos, width, height;
if(definition.stretches()){ //occupy all available space
xPos = bounds.x; yPos = bounds.y;
width = bounds.width; height = bounds.height;
} else { //decide how to fit
int newHeight = bounds.width * graphic.getHeight(null) / graphic.getWidth(null);
if(newHeight < bounds.height){ //expand to fit width
height = newHeight;
width = bounds.width;
xPos = bounds.x;
yPos = bounds.y + bounds.height / 2 - graphic.getHeight(null) / 2;
} else { //expand to fit height
width = graphic.getWidth(null) * bounds.height / graphic.getHeight(null);
height = bounds.height;
xPos = bounds.x + bounds.width / 2 - graphic.getWidth(null) / 2;
yPos = bounds.y;
}
}
g2.drawImage(graphic, xPos, yPos, width, height, null);
}
public static boolean isColorDark(Color color){
int brightness = Math.max(color.getRed(), color.getGreen());
brightness = Math.max(color.getBlue(), brightness);
if(brightness < 200) {
if(DEBUG) System.out.println("Color "+color+" is dark");
return true;
}
if(DEBUG) System.out.println("Color "+color+" is not dark");
return false;
}
}