// // Swiss QR Bill Generator // Copyright (c) 2017 Manuel Bleichenbacher // Licensed under MIT License // https://opensource.org/licenses/MIT // package net.codecrete.qrbill.canvas; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.util.Matrix; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; /** * Canvas for generating PDF files. * <p> * The PDF generator currently only supports the Helvetica font. * </p> */ public class PDFCanvas extends AbstractCanvas implements ByteArrayResult { /** * Add the QR bill on the last page of the PDF document. */ public static final int LAST_PAGE = -1; /** * Add the QR bill on a new page at the end of the PDF document. */ public static final int NEW_PAGE_AT_END = -2; private PDDocument document; private PDPageContentStream contentStream; private int lastStrokingColor = 0; private int lastNonStrokingColor = 0; private double lastLineWidth = 1; private LineStyle lastLineStyle = LineStyle.Solid; private boolean hasSavedGraphicsState = false; /** * Creates a new instance using the specified page size. * @param width page width, in mm * @param height page height, in mm * @throws IOException thrown if the creation fails */ public PDFCanvas(double width, double height) throws IOException { setupFontMetrics("Helvetica"); document = new PDDocument(); document.getDocumentInformation().setTitle("Swiss QR Bill"); PDPage page = new PDPage(new PDRectangle((float) (width * MM_TO_PT), (float) (height * MM_TO_PT))); document.addPage(page); contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.OVERWRITE, true); } /** * Creates a new instance for adding the QR bill to an exiting PDF document. * <p> * The QR bill can either be added to an existing page by specifying the page number * of an existing page (or {@link #LAST_PAGE}), or it can be added to a new page * at the end of the document (see {@link #NEW_PAGE_AT_END}). * </p> * <p> * The created instance assumes that the page for the QR bill has A4 format and * will add the QR bill at the bottom of the page. * </p> * @param path path to exiting document * @param pageNo the zero-based number of the page the QR bill should be added to * @throws IOException thrown if the creation fails */ public PDFCanvas(Path path, int pageNo) throws IOException { setupFontMetrics("Helvetica"); document = PDDocument.load(path.toFile()); if (pageNo == NEW_PAGE_AT_END) { PDPage page = new PDPage(new PDRectangle((float) (210 * MM_TO_PT), (float) (297 * MM_TO_PT))); document.addPage(page); contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.OVERWRITE, true, true); } else { if (pageNo == LAST_PAGE) pageNo = document.getNumberOfPages() - 1; PDPage page = document.getPage(pageNo); PDRectangle rect = page.getBBox(); contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true); } } @Override public void setTransformation(double translateX, double translateY, double rotate, double scaleX, double scaleY) throws IOException { translateX *= MM_TO_PT; translateY *= MM_TO_PT; if (hasSavedGraphicsState) { contentStream.restoreGraphicsState(); lastStrokingColor = 0; lastNonStrokingColor = 0; lastLineWidth = 1; } contentStream.saveGraphicsState(); hasSavedGraphicsState = true; Matrix matrix = new Matrix(); matrix.translate((float) translateX, (float) translateY); if (rotate != 0) matrix.rotate(rotate); if (scaleX != 1 || scaleY != 1) matrix.scale((float) scaleX, (float) scaleY); contentStream.transform(matrix); } @Override public void putText(String text, double x, double y, int fontSize, boolean isBold) throws IOException { x *= MM_TO_PT; y *= MM_TO_PT; contentStream.setFont(isBold ? PDType1Font.HELVETICA_BOLD : PDType1Font.HELVETICA, fontSize); contentStream.beginText(); contentStream.newLineAtOffset((float) x, (float) y); contentStream.showText(text); contentStream.endText(); } @Override public void putTextLines(String[] lines, double x, double y, int fontSize, double leading) throws IOException { x *= MM_TO_PT; y *= MM_TO_PT; float lineHeight = (float) ((fontMetrics.getLineHeight(fontSize) + leading) * MM_TO_PT); contentStream.setFont(PDType1Font.HELVETICA, fontSize); contentStream.beginText(); contentStream.newLineAtOffset((float) x, (float) y); boolean isFirstLine = true; for (String line : lines) { if (isFirstLine) { isFirstLine = false; } else { contentStream.newLineAtOffset(0, -lineHeight); } contentStream.showText(line); } contentStream.endText(); } @Override public void startPath() { // path is start implicitly } @Override public void moveTo(double x, double y) throws IOException { x *= MM_TO_PT; y *= MM_TO_PT; contentStream.moveTo((float) x, (float) y); } @Override public void lineTo(double x, double y) throws IOException { x *= MM_TO_PT; y *= MM_TO_PT; contentStream.lineTo((float) x, (float) y); } @Override public void cubicCurveTo(double x1, double y1, double x2, double y2, double x, double y) throws IOException { x1 *= MM_TO_PT; y1 *= MM_TO_PT; x2 *= MM_TO_PT; y2 *= MM_TO_PT; x *= MM_TO_PT; y *= MM_TO_PT; contentStream.curveTo((float) x1, (float) y1, (float) x2, (float) y2, (float) x, (float) y); } @Override public void addRectangle(double x, double y, double width, double height) throws IOException { x *= MM_TO_PT; y *= MM_TO_PT; width *= MM_TO_PT; height *= MM_TO_PT; contentStream.addRect((float) x, (float) y, (float) width, (float) height); } @Override public void closeSubpath() throws IOException { contentStream.closePath(); } @Override public void fillPath(int color) throws IOException { if (color != lastNonStrokingColor) { lastNonStrokingColor = color; int r = (color >> 16) & 0xff; int g = (color >> 8) & 0xff; int b = (color >> 8) & 0xff; contentStream.setNonStrokingColor(r / 255f, g / 255f, b / 255f); } contentStream.fill(); } @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) throws IOException { if (color != lastStrokingColor) { lastStrokingColor = color; int r = (color >> 16) & 0xff; int g = (color >> 8) & 0xff; int b = (color >> 8) & 0xff; contentStream.setStrokingColor(r / 255f, g / 255f, b / 255f); } if (lineStyle != lastLineStyle || (lineStyle != LineStyle.Solid && strokeWidth != lastLineWidth)) { lastLineStyle = lineStyle; float[] pattern; switch (lineStyle) { case Dashed: pattern = new float[] { 4 * (float)strokeWidth }; break; case Dotted: pattern = new float[] { 0, 3 * (float)strokeWidth }; break; default: pattern = new float[] { }; } contentStream.setLineCapStyle(lineStyle == LineStyle.Dotted ? 1 : 0); contentStream.setLineDashPattern(pattern, 0); } if (strokeWidth != lastLineWidth) { lastLineWidth = strokeWidth; contentStream.setLineWidth((float) (strokeWidth)); } contentStream.stroke(); } @Override public byte[] toByteArray() throws IOException { if (contentStream != null) { contentStream.close(); contentStream = null; } try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { document.save(os); return os.toByteArray(); } } /** * Writes the resulting PDF document 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 { if (contentStream != null) { contentStream.close(); contentStream = null; } document.save(os); } /** * Saves the resulting PDF document 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 { if (contentStream != null) { contentStream.close(); contentStream = null; } try (OutputStream os = Files.newOutputStream(path)) { document.save(os); } } @Override public void close() throws IOException { if (contentStream != null) { contentStream.close(); contentStream = null; } if (document != null) { document.close(); document = null; } } }