net.codecrete.qrbill.canvas.PNGCanvas Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of qrbill-generator Show documentation
Show all versions of qrbill-generator Show documentation
Java library for generating Swiss QR bills
//
// Swiss QR Bill Generator
// Copyright (c) 2018 Manuel Bleichenbacher
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
package net.codecrete.qrbill.canvas;
import net.codecrete.qrbill.generator.QRBillGenerationException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
/**
* Canvas for generating PNG files.
*
* PNGs are not an optimal file format for QR bills. Vector formats such a SVG
* or PDF are of better quality and use far less processing power to generate.
*
*/
public class PNGCanvas extends AbstractCanvas implements ByteArrayResult {
private static final String METADATA_KEY_VALUE = "value";
private static final String METADATA_KEY_KEYWORD = "keyword";
private final int resolution;
private final float coordinateScale;
private final float fontScale;
private BufferedImage image;
private Graphics2D graphics;
private Path2D.Double currentPath;
/**
* Creates a new instance with the specified image size, resolution and font family.
*
* It is recommended to use at least 144 dpi for a readable result.
*
*
* The first font family in the list is used.
*
*
* @param width image width, in mm
* @param height image height, in mm
* @param resolution resolution of the result (in dpi)
* @param fontFamilyList list of font families (comma separated, CSS syntax)
*/
public PNGCanvas(double width, double height, int resolution, String fontFamilyList) {
this.resolution = resolution;
coordinateScale = (float) (resolution / 25.4);
fontScale = (float) (resolution / 72.0);
setupFontMetrics(fontFamilyList);
// create image
int w = (int) (width * coordinateScale + 0.5);
int h = (int) (height * coordinateScale + 0.5);
image = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
// create graphics context
graphics = image.createGraphics();
// clear background
graphics.setColor(new Color(0xffffff));
graphics.fillRect(0, 0, w, h);
// enable high quality output
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
// initialize transformation
setTransformation(0, 0, 0, 1, 1);
}
@Override
public void setTransformation(double translateX, double translateY, double rotate, double scaleX, double scaleY) {
// Our coordinate system extends from the bottom up. Java Graphics2D's system
// extends from the top down. So Y coordinates need to be treated specially.
translateX *= coordinateScale;
translateY *= coordinateScale;
AffineTransform at = new AffineTransform();
at.translate(translateX, image.getHeight() - translateY);
if (rotate != 0)
at.rotate(-rotate);
if (scaleX != 1 || scaleY != 1)
at.scale(scaleX, scaleY);
graphics.setTransform(at);
}
@Override
public void putText(String text, double x, double y, int fontSize, boolean isBold) {
x *= coordinateScale;
y *= -coordinateScale;
graphics.setColor(new Color(0));
Font font = new Font(fontMetrics.getFirstFontFamily(), isBold ? Font.BOLD : Font.PLAIN, (int) (fontSize * fontScale + 0.5));
graphics.setFont(font);
graphics.drawString(text, (float) x, (float) y);
}
@Override
public void startPath() {
currentPath = new Path2D.Double(Path2D.WIND_NON_ZERO);
}
@Override
public void moveTo(double x, double y) {
x *= coordinateScale;
y *= -coordinateScale;
currentPath.moveTo(x, y);
}
@Override
public void lineTo(double x, double y) {
x *= coordinateScale;
y *= -coordinateScale;
currentPath.lineTo(x, y);
}
@Override
public void cubicCurveTo(double x1, double y1, double x2, double y2, double x, double y) {
x1 *= coordinateScale;
y1 *= -coordinateScale;
x2 *= coordinateScale;
y2 *= -coordinateScale;
x *= coordinateScale;
y *= -coordinateScale;
currentPath.curveTo(x1, y1, x2, y2, x, y);
}
@Override
public void addRectangle(double x, double y, double width, double height) {
x *= coordinateScale;
y *= -coordinateScale;
width *= coordinateScale;
height *= -coordinateScale;
currentPath.moveTo(x, y);
currentPath.lineTo(x, y + height);
currentPath.lineTo(x + width, y + height);
currentPath.lineTo(x + width, y);
currentPath.closePath();
}
@Override
public void closeSubpath() {
currentPath.closePath();
}
@Override
public void fillPath(int color) {
graphics.setColor(new Color(color));
graphics.fill(currentPath);
}
@Override
public void strokePath(double strokeWidth, int color) throws IOException {
strokePath(strokeWidth, color, LineStyle.Solid);
}
@Override
public void strokePath(double strokeWidth, int color, LineStyle lineStyle) {
graphics.setColor(new Color(color));
BasicStroke stroke;
switch (lineStyle) {
case Dashed:
stroke = new BasicStroke((float) (strokeWidth * fontScale), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
10, new float[] { 4 * (float)strokeWidth * fontScale }, 0);
break;
case Dotted:
stroke = new BasicStroke((float) (strokeWidth * fontScale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER,
10, new float[] { 0, 3 * (float)strokeWidth * fontScale }, 0);
break;
default:
stroke = new BasicStroke((float) (strokeWidth * fontScale), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
}
graphics.setStroke(stroke);
graphics.draw(currentPath);
}
@Override
public byte[] toByteArray() throws IOException {
graphics.dispose();
graphics = null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
// Instead of ImageIO.write(image, "png", os)
createPNG(image, os, resolution);
return os.toByteArray();
}
/**
* Writes the resulting PNG image to the specified output stream.
* @param os the output stream
* @throws IOException thrown if the image cannot be written
*/
public void writeTo(OutputStream os) throws IOException {
graphics.dispose();
graphics = null;
// Instead of ImageIO.write(image, "png", os)
createPNG(image, os, resolution);
}
/**
* Saves the resulting PNG image to the specified path.
* @param path the path to write to
* @throws IOException thrown if the image cannot be written
*/
public void saveAs(Path path) throws IOException {
graphics.dispose();
graphics = null;
try (OutputStream os = Files.newOutputStream(path)) {
// Instead of ImageIO.write(image, "png", os)
createPNG(image, os, resolution);
}
}
@Override
public void close() {
if (graphics != null) {
graphics.dispose();
graphics = null;
}
image = null;
}
/**
* Saves image as PDF and stores meta data to indicate the resolution.
*/
private static void createPNG(BufferedImage image, OutputStream os, int resolution) throws IOException {
ImageWriter writer = null;
ImageWriteParam writeParam = null;
IIOMetadata metadata = null;
for (Iterator iw = ImageIO.getImageWritersByFormatName("png"); iw.hasNext(); ) {
writer = iw.next();
writeParam = writer.getDefaultWriteParam();
ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(image.getType());
metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
if (!metadata.isReadOnly() && metadata.isStandardMetadataFormatSupported())
break;
}
if (writer == null || writeParam == null)
throw new QRBillGenerationException("No valid PNG writer found");
addDpiMetadata(metadata, resolution);
addTextMetadata(metadata);
try (ImageOutputStream stream = ImageIO.createImageOutputStream(os)) {
writer.setOutput(stream);
writer.write(metadata, new IIOImage(image, null, metadata), writeParam);
}
}
private static final String PNG_STANDARD_METADATA_FORMAT = "javax_imageio_1.0";
/**
* Add meta data to specify the resolution
*/
private static void addDpiMetadata(IIOMetadata metadata, int dpi) throws IIOInvalidTreeException {
// native metadata format ("pHYs")
double pixelsPerMeter = dpi / 25.4 * 1000;
String pixelsPerMeterString = Integer.toString((int) (pixelsPerMeter + 0.5));
IIOMetadataNode physNode = new IIOMetadataNode("pHYs");
physNode.setAttribute("pixelsPerUnitXAxis", pixelsPerMeterString);
physNode.setAttribute("pixelsPerUnitYAxis", pixelsPerMeterString);
physNode.setAttribute("unitSpecifier", "meter");
IIOMetadataNode root = new IIOMetadataNode(metadata.getNativeMetadataFormatName());
root.appendChild(physNode);
metadata.mergeTree(metadata.getNativeMetadataFormatName(), root);
// standard metadata format
double pixelsPerMM = dpi / 25.4;
String pixelsPerMMString = Double.toString(pixelsPerMM);
IIOMetadataNode horizontalPixelSize = new IIOMetadataNode("HorizontalPixelSize");
horizontalPixelSize.setAttribute(METADATA_KEY_VALUE, pixelsPerMMString);
IIOMetadataNode verticalPixelSize = new IIOMetadataNode("VerticalPixelSize");
verticalPixelSize.setAttribute(METADATA_KEY_VALUE, pixelsPerMMString);
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
dimension.appendChild(horizontalPixelSize);
dimension.appendChild(verticalPixelSize);
root = new IIOMetadataNode(PNG_STANDARD_METADATA_FORMAT);
root.appendChild(dimension);
metadata.mergeTree(PNG_STANDARD_METADATA_FORMAT, root);
}
private static void addTextMetadata(IIOMetadata metadata) throws IIOInvalidTreeException {
IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry");
textEntry.setAttribute(METADATA_KEY_KEYWORD, "Title");
textEntry.setAttribute(METADATA_KEY_VALUE, "Swiss QR Bill");
IIOMetadataNode text = new IIOMetadataNode("tEXt");
text.appendChild(textEntry);
IIOMetadataNode commentMetadata = new IIOMetadataNode(metadata.getNativeMetadataFormatName());
commentMetadata.appendChild(text);
metadata.mergeTree(metadata.getNativeMetadataFormatName(), commentMetadata);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy