ch.codeblock.qrinvoice.paymentpartreceipt.IText4PaymentPartReceiptWriter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of qrinvoice-openpdf Show documentation
Show all versions of qrinvoice-openpdf Show documentation
Provides an OpenPDF specific payment part receipt writer
/*-
* #%L
* QR Invoice Solutions
* %%
* Copyright (C) 2017 - 2022 Codeblock GmbH
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* -
* This program is 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 Affero General Public License for more details.
* -
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
* -
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses are available for this software. These replace the above
* AGPLv3 terms and offer support, maintenance and allow the use in commercial /
* proprietary products.
* -
* More information on commercial licenses are available at the following page:
* https://www.qr-invoice.ch/licenses/
* #L%
*/
package ch.codeblock.qrinvoice.paymentpartreceipt;
import ch.codeblock.qrinvoice.NotYetImplementedException;
import ch.codeblock.qrinvoice.OutputFormat;
import ch.codeblock.qrinvoice.TechnicalException;
import ch.codeblock.qrinvoice.config.SystemProperties;
import ch.codeblock.qrinvoice.fonts.FontManager;
import ch.codeblock.qrinvoice.fonts.FontStyle;
import ch.codeblock.qrinvoice.graphics.Scissor;
import ch.codeblock.qrinvoice.layout.LayoutException;
import ch.codeblock.qrinvoice.layout.Point;
import ch.codeblock.qrinvoice.layout.Rect;
import ch.codeblock.qrinvoice.model.*;
import ch.codeblock.qrinvoice.model.util.AddressUtils;
import ch.codeblock.qrinvoice.model.util.AlternativeSchemesUtils;
import ch.codeblock.qrinvoice.output.PaymentPartReceipt;
import ch.codeblock.qrinvoice.qrcode.SwissQrCode;
import ch.codeblock.qrinvoice.util.*;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.Rectangle;
import com.lowagie.text.*;
import com.lowagie.text.pdf.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.function.Function;
import static ch.codeblock.qrinvoice.model.util.AddressUtils.toAddressLines;
import static ch.codeblock.qrinvoice.paymentpartreceipt.LayoutDefinitions.*;
import static ch.codeblock.qrinvoice.paymentpartreceipt.PaymentPartReceiptLayoutHelper.compressedLayout;
import static ch.codeblock.qrinvoice.paymentpartreceipt.PaymentPartReceiptLayoutHelper.uncompressedLayout;
import static com.lowagie.text.Element.ALIGN_LEFT;
import static com.lowagie.text.Element.ALIGN_RIGHT;
import static com.lowagie.text.Utilities.millimetersToPoints;
public class IText4PaymentPartReceiptWriter extends AbstractItextPaymentPartReceiptWriter implements IPaymentPartReceiptWriter {
private final Logger logger = LoggerFactory.getLogger(IText4PaymentPartReceiptWriter.class);
private final Rectangle pageCanvas;
/**
* root lower left coordinates of the payment part on the pageCanvas in order to position the elements
*/
private final Point rootLowerLeft;
private final Point paymentPartLowerLeft;
private final Rectangle receiptTitleSectionRect;
private final Rectangle receiptInformationSectionRect;
private final Rectangle receiptAmountSectionRect;
private final Rectangle receiptAcceptanceSectionRect;
private final Rectangle paymentPartTitleSectionRect;
private final Rectangle paymentPartQrSectionRect;
private final Rectangle paymentPartAmountSectionRect;
private final Rectangle paymentPartInformationSectionRect;
private final Rectangle paymentPartFurtherInfoSectionRect;
private final float additionalPrintMarginPts;
private final float printMarginPts;
// use font with paragraph spacing as font size in order to gerenated paragraph spacing in columntext
private Font receiptParagraphSpacingFont;
private Font paymentPartParagraphSpacingFont;
private Font separationLabelFont;
private Font receiptHeadingFont;
private Font receiptValueFont;
private Font paymentPartHeadingFont;
private Font paymentPartValueFont;
public IText4PaymentPartReceiptWriter(final PaymentPartReceiptWriterOptions options) {
super(options);
switch (options.getPageSize()) {
case A4:
pageCanvas = PageSize.A4;
break;
case A5:
pageCanvas = PageSize.A5.rotate();
break;
case DIN_LANG_CROPPED:
throw new NotYetImplementedException("Cropped DIN_LANG output is not implemented for PDF output. It is available for raster graphics only.");
case DIN_LANG:
default:
pageCanvas = new RectangleReadOnly(DIN_LANG.getWidth(), DIN_LANG.getHeight());
break;
}
// root lower left is always (0,0)
rootLowerLeft = new Point<>(0.0f, 0.0f);
// determine the lower left position of the payment part on the page canvas
paymentPartLowerLeft = new Point<>(pageCanvas.getWidth() - getPaymentPartWidth(), 0.0f);
additionalPrintMarginPts = options.isAdditionalPrintMargin() ? millimetersToPoints(LayoutDefinitions.ADDITIONAL_PRINT_MARGIN) : 0;
printMarginPts = QUIET_SPACE_PTS + additionalPrintMarginPts;
receiptTitleSectionRect = itextRectangle(RECEIPT_TITLE_SECTION_RECT.move(rootLowerLeft));
receiptInformationSectionRect = itextRectangle(RECEIPT_INFORMATION_SECTION_RECT.move(rootLowerLeft));
receiptAmountSectionRect = itextRectangle(RECEIPT_AMOUNT_SECTION_RECT.move(rootLowerLeft));
receiptAcceptanceSectionRect = itextRectangle(RECEIPT_ACCEPTANCE_SECTION_RECT.move(rootLowerLeft));
paymentPartTitleSectionRect = itextRectangle(PAYMENT_PART_TITLE_SECTION_RECT.move(paymentPartLowerLeft));
paymentPartQrSectionRect = itextRectangle(PAYMENT_PART_QR_SECTION_RECT.move(paymentPartLowerLeft));
paymentPartAmountSectionRect = itextRectangle(PAYMENT_PART_AMOUNT_SECTION_RECT.move(paymentPartLowerLeft));
paymentPartInformationSectionRect = itextRectangle(PAYMENT_PART_INFORMATION_SECTION_RECT.move(paymentPartLowerLeft));
paymentPartFurtherInfoSectionRect = itextRectangle(PAYMENT_PART_FURTHER_INFO_SECTION_RECT.move(paymentPartLowerLeft));
try {
final BaseFont regularFont = getFont(options, FontStyle.REGULAR);
final BaseFont boldFont = getFont(options, FontStyle.BOLD);
paymentPartHeadingFont = new Font(boldFont, PAYMENT_PART_FONT_SIZE_HEADING);
paymentPartValueFont = new Font(regularFont, PAYMENT_PART_FONT_SIZE_VALUE);
paymentPartParagraphSpacingFont = new Font(regularFont, PAYMENT_PART_PARAGRAPH_SPACING);
receiptParagraphSpacingFont = new Font(regularFont, RECEIPT_PARAGRAPH_SPACING);
separationLabelFont = new Font(regularFont, FONT_SIZE_SEPARATION_LABEL);
receiptHeadingFont = new Font(boldFont, RECEIPT_FONT_SIZE_HEADING);
receiptValueFont = new Font(regularFont, RECEIPT_FONT_SIZE_VALUE);
} catch (Exception e) {
throw new TechnicalException("Error while creating fonts", e);
}
}
private BaseFont getFont(final PaymentPartReceiptWriterOptions options, final FontStyle fontStyle) throws DocumentException, IOException {
if (FontManager.isEmbedded(options.getFontFamily())) {
// getEmbeddedTtf uses caching - byte[] is constructed only once
final byte[] boldTtf = FontManager.getEmbeddedTtf(options.getFontFamily(), fontStyle);
// BaseFont itself has a caching mechanism
return BaseFont.createFont(FontManager.getEmbeddedTtfFileName(options.getFontFamily(), fontStyle), BaseFont.CP1252, options.isFontsEmbedded(), true, boldTtf, null);
} else {
return BaseFont.createFont(getFontNameOrFontPath(options.getFontFamily(), fontStyle), PDF_ENCODING, options.isFontsEmbedded());
}
}
@Override
public PaymentPartReceipt write(QrInvoice qrInvoice, final BufferedImage qrCodeImage) {
try {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(25 * 1024);
final Document document = new Document(pageCanvas, 0, 0, 0, 0);
final PdfWriter writer = PdfWriter.getInstance(document, baos);
document.open();
addMetaData(document);
addReceiptTitleSection(writer);
addReceiptInformationSection(writer, qrInvoice);
addReceiptAmount(writer, qrInvoice.getPaymentAmountInformation());
addReceiptAcceptanceSection(writer);
addPaymentPartTitleSection(writer);
addQrCodeImage(writer, qrCodeImage);
addPaymentPartAmount(writer, qrInvoice.getPaymentAmountInformation());
addPaymentPartInformationSection(writer, qrInvoice);
addPaymentPartFurtherInfoSection(writer, qrInvoice);
drawBoundaryLines(writer.getDirectContent());
document.close();
return new PaymentPartReceipt(getOptions().getPageSize(), OutputFormat.PDF, baos.toByteArray(), null, null);
} catch (DocumentException | IOException e) {
throw new TechnicalException("Unexpected exception occurred during the creation of the payment part", e);
}
}
public void addMetaData(final Document document) {
document.addProducer(QR_INVOICE_PRODUCER);
document.addTitle(getLabel("TitleQrInvoice"));
document.addCreationDate();
}
private void addReceiptTitleSection(final PdfWriter writer) {
debugLayout(writer, receiptTitleSectionRect);
final PdfContentByte content = writer.getDirectContent();
content.beginText();
content.setFontAndSize(receiptHeadingFont.getBaseFont(), FONT_SIZE_TITLE);
final String titleReceipt = getLabel("TitleReceipt");
final float ascent = receiptHeadingFont.getBaseFont().getAscentPoint(titleReceipt, FONT_SIZE_TITLE);
content.showTextAligned(ALIGN_LEFT, titleReceipt, receiptTitleSectionRect.getLeft() + additionalPrintMarginPts, receiptTitleSectionRect.getTop() - ascent, 0);
content.endText();
}
private void addReceiptInformationSection(final PdfWriter writer, final QrInvoice qrInvoice) throws DocumentException {
debugLayout(writer, receiptInformationSectionRect);
final PdfContentByte cb = writer.getDirectContent();
final ColumnText ct = new ColumnText(cb);
final float top = receiptInformationSectionRect.getTop();
ct.setSimpleColumn(receiptInformationSectionRect.getLeft() + additionalPrintMarginPts, receiptInformationSectionRect.getBottom(), receiptInformationSectionRect.getRight(), top);
ct.setLeading(RECEIPT_VALUE_LINE_SPACING, MULTIPLIED_LEADING);
// According to the spec 3.6.2 - Information section - v2.0
// "Because of the limited space, it is permitted to omit the street name and building number from the addresses of the creditor (Payable to) and the debtor (Payable by)"
boolean omitStreetNameHouseNumberOrAddressLine1 = compressedLayout(getOptions().getLayout());
// Account
writeReceiptInformationSectionTitle(ct, getLabel("CdtrInf.IBANCreditor"), true, true);
ct.addText(chunk(IbanUtils.formatIban(qrInvoice.getCreditorInformation().getIban()), true, receiptValueFont));
// Creditor
toAddressLines(qrInvoice.getCreditorInformation().getCreditor(), omitStreetNameHouseNumberOrAddressLine1).forEach(addressLine -> ct.addText(chunk(addressLine, true, receiptValueFont)));
final PaymentReference paymentReference = qrInvoice.getPaymentReference();
if (paymentReference != null) {
// Reference number
if (paymentReference.getReferenceType() != null) {
switch (paymentReference.getReferenceType()) {
case QR_REFERENCE:
case CREDITOR_REFERENCE:
addParagraphSpacing(ct, this.receiptParagraphSpacingFont);
writeReceiptInformationSectionTitle(ct, getLabel("RmtInf.Ref"), true, false);
ct.addText(chunk(ReferenceUtils.format(paymentReference.getReferenceType(), paymentReference.getReference()), true, receiptValueFont));
break;
case WITHOUT_REFERENCE:
break;
}
}
}
// Debtor
addParagraphSpacing(ct, this.receiptParagraphSpacingFont);
if (AddressUtils.isEmpty(qrInvoice.getUltimateDebtor())) {
final float lineHeight = RECEIPT_VALUE_LINE_SPACING + (MULTIPLIED_LEADING * RECEIPT_FONT_SIZE_VALUE);
final float halfLineHeight = lineHeight / 2;
writeReceiptInformationSectionTitle(ct, getLabel("UltmtDbtr.Empty"), true, false);
ct.go();
final float leftX = receiptInformationSectionRect.getLeft() + additionalPrintMarginPts;
final float upperY = ct.getYLine() - halfLineHeight; // half of a line height as spacing between title and free text box
final float lowerY = upperY - RECEIPT_DEBTOR_FIELD.getHeight();
final Rect debtorField = RECEIPT_DEBTOR_FIELD.toRectangle(leftX, lowerY);
writeFreeTextBox(ct.getCanvas(), debtorField);
// as we do not have a line-height option in itext4, add variable number of line feeds in order to move the cursor down in the column for the debtor fields height.
ct.addText(new Chunk(neededLinesBreaks(debtorField.getHeight(), lineHeight), receiptValueFont));
} else {
writeReceiptInformationSectionTitle(ct, getLabel("UltmtDbtr"), true, false);
final Iterator addressLines = toAddressLines(qrInvoice.getUltimateDebtor(), omitStreetNameHouseNumberOrAddressLine1).iterator();
while (addressLines.hasNext()) {
ct.addText(chunk(addressLines.next(), addressLines.hasNext(), receiptValueFont));
}
}
final int ret = ct.go(false);
switch (ret) {
case ColumnText.NO_MORE_TEXT:
// ok case
break;
case ColumnText.NO_MORE_COLUMN:
// nok
final String message = "Not all content could be printed to the receipt information section";
if (System.getProperty(SystemProperties.IGNORE_LAYOUT_ERRORS) == null) {
throw new LayoutException(message);
} else {
logger.info(message);
}
break;
default:
throw new IllegalStateException("Got unexpected result: " + ret);
}
}
private void writeReceiptInformationSectionTitle(ColumnText ct, String text, boolean newLine, boolean firstTitle) throws DocumentException {
writeInformationSectionTitle(ct, text, newLine, firstTitle, this.receiptHeadingFont, RECEIPT_HEADING_LINE_SPACING);
}
private void addReceiptAmount(final PdfWriter writer, final PaymentAmountInformation paymentAmountInformation) {
debugLayout(writer, receiptAmountSectionRect);
final float lowerY = receiptAmountSectionRect.getBottom();
final float upperY = receiptAmountSectionRect.getTop();
final PdfContentByte canvas = writer.getDirectContent();
// header
canvas.setFontAndSize(receiptHeadingFont.getBaseFont(), RECEIPT_FONT_SIZE_HEADING);
canvas.beginText();
final float currencyX = rootLowerLeft.getX() + printMarginPts;
final float amountX = currencyX + millimetersToPoints(12);
final String currencyLabel = getLabel("Currency");
final String amountLabel = getLabel("Amount");
final float headerY = upperY - receiptHeadingFont.getBaseFont().getAscentPoint(currencyLabel + amountLabel, RECEIPT_FONT_SIZE_HEADING);
canvas.showTextAligned(ALIGN_LEFT, currencyLabel, currencyX, headerY, 0);
canvas.showTextAligned(ALIGN_LEFT, amountLabel, amountX, headerY, 0);
canvas.endText();
// body
canvas.setFontAndSize(receiptValueFont.getBaseFont(), RECEIPT_FONT_SIZE_VALUE);
canvas.beginText();
final float valuesY = headerY - RECEIPT_FONT_SIZE_VALUE - RECEIPT_AMOUNT_LINE_SPACING;
canvas.showTextAligned(ALIGN_LEFT, paymentAmountInformation.getCurrency().getCurrencyCode(), currencyX, valuesY, 0);
if (paymentAmountInformation.getAmount() != null) {
final String formattedAmount = DecimalFormatFactory.createPrintAmountFormat().format(paymentAmountInformation.getAmount());
canvas.showTextAligned(ALIGN_LEFT, formattedAmount, amountX, valuesY, 0);
}
canvas.endText();
if (paymentAmountInformation.getAmount() == null) {
final float amountFieldX = receiptAmountSectionRect.getRight() - RECEIPT_AMOUNT_FIELD.getWidth() + additionalPrintMarginPts;
final float amountFieldY = upperY - RECEIPT_AMOUNT_FIELD.getHeight() - millimetersToPoints(0.75f);
final Rect rect = RECEIPT_AMOUNT_FIELD.toRectangle(amountFieldX, amountFieldY);
writeFreeTextBox(canvas, rect);
}
}
private void addReceiptAcceptanceSection(final PdfWriter writer) {
debugLayout(writer, receiptAcceptanceSectionRect);
final PdfContentByte content = writer.getDirectContent();
content.beginText();
final String acceptancePointLabel = getLabel("AcceptancePoint");
final float x = receiptAcceptanceSectionRect.getRight();
final float y = receiptAcceptanceSectionRect.getTop() - paymentPartHeadingFont.getBaseFont().getAscentPoint(acceptancePointLabel, RECEIPT_FONT_SIZE_HEADING);
content.setFontAndSize(paymentPartHeadingFont.getBaseFont(), RECEIPT_FONT_SIZE_HEADING);
content.showTextAligned(ALIGN_RIGHT, acceptancePointLabel, x, y, 0);
content.endText();
}
private void addPaymentPartTitleSection(final PdfWriter writer) {
debugLayout(writer, paymentPartTitleSectionRect);
final PdfContentByte content = writer.getDirectContent();
content.beginText();
content.setFontAndSize(paymentPartHeadingFont.getBaseFont(), FONT_SIZE_TITLE);
final String titlePaymentPart = getLabel("TitlePaymentPart");
final float ascent = paymentPartHeadingFont.getBaseFont().getAscentPoint(titlePaymentPart, FONT_SIZE_TITLE);
content.showTextAligned(ALIGN_LEFT, titlePaymentPart, paymentPartTitleSectionRect.getLeft(), paymentPartTitleSectionRect.getTop() - ascent, 0);
content.endText();
}
private void addQrCodeImage(final PdfWriter writer, final BufferedImage qrCodeImage) throws IOException, DocumentException {
debugLayout(writer, paymentPartQrSectionRect);
final float x = paymentPartQrSectionRect.getLeft();
final float y = paymentPartQrSectionRect.getBottom() + QUIET_SPACE_PTS;
final Image image = Image.getInstance(qrCodeImage, null, true);
image.setAbsolutePosition(x, y);
final float size = millimetersToPoints(SwissQrCode.QR_CODE_SIZE.getWidth());
image.scaleToFit(size, size);
final PdfContentByte canvas = writer.getDirectContent();
canvas.addImage(image);
}
private void addPaymentPartInformationSection(PdfWriter writer, final QrInvoice qrInvoice) throws DocumentException {
debugLayout(writer, paymentPartInformationSectionRect);
final PdfContentByte cb = writer.getDirectContent();
final ColumnText ct = new ColumnText(cb);
// column is fit to the payment part information section, but we want the text to start at the top without any leading on the first line, thus we correct the columns top position
final float top = paymentPartInformationSectionRect.getTop() + PAYMENT_PART_VALUE_LINE_SPACING + (MULTIPLIED_LEADING * PAYMENT_PART_FONT_SIZE_HEADING) - PAYMENT_PART_FONT_SIZE_HEADING;
ct.setSimpleColumn(paymentPartInformationSectionRect.getLeft(), paymentPartInformationSectionRect.getBottom(), paymentPartInformationSectionRect.getRight(), top);
ct.setLeading(PAYMENT_PART_VALUE_LINE_SPACING, MULTIPLIED_LEADING);
// Account
writePaymentPartInformationSectionTitle(ct, getLabel("CdtrInf.IBANCreditor"), true);
ct.addText(chunk(IbanUtils.formatIban(qrInvoice.getCreditorInformation().getIban()), true, paymentPartValueFont));
// Creditor
toAddressLines(qrInvoice.getCreditorInformation().getCreditor()).forEach(addressLine -> ct.addText(chunk(addressLine, true, paymentPartValueFont)));
final PaymentReference paymentReference = qrInvoice.getPaymentReference();
if (paymentReference != null) {
// Reference number
if (paymentReference.getReferenceType() != null) {
switch (paymentReference.getReferenceType()) {
case QR_REFERENCE:
case CREDITOR_REFERENCE:
addParagraphSpacing(ct, this.paymentPartParagraphSpacingFont);
writePaymentPartInformationSectionTitle(ct, getLabel("RmtInf.Ref"), false);
ct.addText(chunk(ReferenceUtils.format(paymentReference.getReferenceType(), paymentReference.getReference()), true, paymentPartValueFont));
break;
case WITHOUT_REFERENCE:
break;
}
}
// Additional information
final String unstructuredMessage = DoNotUseForPayment.localize(paymentReference.getAdditionalInformation().getUnstructuredMessage(), getOptions().getLocale());
final String billInformation = paymentReference.getAdditionalInformation().getBillInformation();
if (unstructuredMessage != null || billInformation != null) {
addParagraphSpacing(ct, this.paymentPartParagraphSpacingFont);
writePaymentPartInformationSectionTitle(ct, getLabel("RmtInf.AddInf.Ustrd"), false);
if (compressedLayout(getOptions().getLayout()) && ((StringUtils.length(unstructuredMessage, billInformation)) > (2 * APPROX_MAX_LINE_LENGTH_PAYMENT_PART_INFO_SECTION))) {
// according to the spec version 2.0 - 3.5.4:
// If both elements are filled in, then a line break can be introduced after the information in the first element "Ustrd" (Unstructured message).
// If there is insufficient space, the line break can be omitted (but this makes it more difficult to read).
// If not all the details contained in the QR code can be displayed, the shortened content must be marked with "..." at the end. It must be ensured
// that all personal data is displayed.
final String joined = StringUtils.join(" ", unstructuredMessage, billInformation);
ct.addText(chunk(joined, true, paymentPartValueFont));
} else {
if (unstructuredMessage != null) {
ct.addText(chunk(unstructuredMessage, true, paymentPartValueFont));
}
if (billInformation != null) {
ct.addText(chunk(billInformation, true, paymentPartValueFont));
}
}
}
}
// Debtor
addParagraphSpacing(ct, this.paymentPartParagraphSpacingFont);
if (AddressUtils.isEmpty(qrInvoice.getUltimateDebtor())) {
float lineHeight = PAYMENT_PART_VALUE_LINE_SPACING + (MULTIPLIED_LEADING * PAYMENT_PART_FONT_SIZE_VALUE);
final float halfLineHeight = lineHeight / 2;
writePaymentPartInformationSectionTitle(ct, getLabel("UltmtDbtr.Empty"), false);
ct.go();
final float leftX = paymentPartInformationSectionRect.getLeft();
final float upperY = ct.getYLine() - halfLineHeight; // half of a line height as spacing between title and free text box
final float lowerY = upperY - PAYMENT_PART_DEBTOR_FIELD.getHeight();
final Rect debtorField = PAYMENT_PART_DEBTOR_FIELD.toRectangle(leftX, lowerY);
writeFreeTextBox(ct.getCanvas(), debtorField);
// as we do not have a line-height option in itext4, add variable number of line feeds in order to move the cursor down in the column for the debtor fields height.
ct.addText(new Chunk(neededLinesBreaks(debtorField.getHeight(), lineHeight), paymentPartValueFont));
} else {
writePaymentPartInformationSectionTitle(ct, getLabel("UltmtDbtr"), false);
final Iterator addressLines = toAddressLines(qrInvoice.getUltimateDebtor()).iterator();
while (addressLines.hasNext()) {
ct.addText(chunk(addressLines.next(), addressLines.hasNext(), paymentPartValueFont));
}
}
final int ret = ct.go(false);
switch (ret) {
case ColumnText.NO_MORE_TEXT:
// ok case
break;
case ColumnText.NO_MORE_COLUMN:
// nok
final String message = "Not all content could be printed to the payment part information section";
if (System.getProperty(SystemProperties.IGNORE_LAYOUT_ERRORS) == null) {
throw new LayoutException(message);
} else {
logger.info(message);
}
break;
default:
throw new IllegalStateException("Got unexpected result: " + ret);
}
}
private void addParagraphSpacing(final ColumnText ct, final Font paragraphSpacingFont) throws DocumentException {
if (uncompressedLayout(getOptions().getLayout())) {
ct.go();
final float oldLeading = ct.getLeading();
final float oldMultipliedLeading = ct.getMultipliedLeading();
ct.addText(new Chunk("\n", paragraphSpacingFont));
ct.setLeading(oldLeading, oldMultipliedLeading);
}
}
private void writePaymentPartInformationSectionTitle(final ColumnText ct, final String text, final boolean firstTitle) throws DocumentException {
writeInformationSectionTitle(ct, text, true, firstTitle, this.paymentPartHeadingFont, PAYMENT_PART_HEADING_LINE_SPACING);
}
private void writeInformationSectionTitle(final ColumnText ct, final String text, final boolean newLine, final boolean firstTitle, final Font font, final float headingLineSpacing) throws DocumentException {
final Float fixedLeading = firstTitle ? Float.valueOf(0f) : Float.valueOf(headingLineSpacing);
ct.go();
final float oldLeading = ct.getLeading();
final float oldMultipliedLeading = ct.getMultipliedLeading();
ct.setLeading(fixedLeading, MULTIPLIED_LEADING);
ct.addText(chunk(text, newLine, font));
ct.go();
// reset leading
ct.setLeading(oldLeading, oldMultipliedLeading);
}
private void addPaymentPartAmount(final PdfWriter writer, final PaymentAmountInformation paymentAmountInformation) {
debugLayout(writer, paymentPartAmountSectionRect);
final float lowerY = paymentPartAmountSectionRect.getBottom();
final float upperY = paymentPartAmountSectionRect.getTop();
final PdfContentByte canvas = writer.getDirectContent();
// header
canvas.setFontAndSize(paymentPartHeadingFont.getBaseFont(), PAYMENT_PART_FONT_SIZE_HEADING);
canvas.beginText();
final float currencyX = paymentPartLowerLeft.getX() + QUIET_SPACE_PTS;
final float amountX = paymentPartLowerLeft.getX() + QUIET_SPACE_PTS + millimetersToPoints(14);
final String currencyLabel = getLabel("Currency");
final String amountLabel = getLabel("Amount");
final float headerY = upperY - paymentPartHeadingFont.getBaseFont().getAscentPoint(currencyLabel + amountLabel, PAYMENT_PART_FONT_SIZE_HEADING);
canvas.showTextAligned(ALIGN_LEFT, currencyLabel, currencyX, headerY, 0);
canvas.showTextAligned(ALIGN_LEFT, amountLabel, amountX, headerY, 0);
canvas.endText();
// body
canvas.setFontAndSize(paymentPartValueFont.getBaseFont(), PAYMENT_PART_FONT_SIZE_VALUE);
canvas.beginText();
final float valuesY = headerY - PAYMENT_PART_FONT_SIZE_VALUE - PAYMENT_PART_AMOUNT_LINE_SPACING;
canvas.showTextAligned(ALIGN_LEFT, paymentAmountInformation.getCurrency().getCurrencyCode(), currencyX, valuesY, 0);
if (paymentAmountInformation.getAmount() != null) {
final String formattedAmount = DecimalFormatFactory.createPrintAmountFormat().format(paymentAmountInformation.getAmount());
canvas.showTextAligned(ALIGN_LEFT, formattedAmount, amountX, valuesY, 0);
}
canvas.endText();
if (paymentAmountInformation.getAmount() == null) {
final float leftAmountField = paymentPartAmountSectionRect.getRight() - PAYMENT_PART_AMOUNT_FIELD.getWidth() - millimetersToPoints(1.0f);
final float lowerAmountField = lowerY + millimetersToPoints(2.0f);
final Rect rect = PAYMENT_PART_AMOUNT_FIELD.toRectangle(leftAmountField, lowerAmountField);
writeFreeTextBox(canvas, rect);
}
}
private void addPaymentPartFurtherInfoSection(final PdfWriter writer, final QrInvoice qrInvoice) {
debugLayout(writer, paymentPartFurtherInfoSectionRect);
if (qrInvoice.getAlternativeSchemes() == null || CollectionUtils.isEmpty(qrInvoice.getAlternativeSchemes().getAlternativeSchemeParameters())) {
return;
}
// Alternative Schemes
final float x = paymentPartFurtherInfoSectionRect.getLeft();
float y = paymentPartFurtherInfoSectionRect.getBottom() + additionalPrintMarginPts + millimetersToPoints(0.5f); // 0.5 is used as descent here... otherwise could be calculated on specific text on BaseFont
if (qrInvoice.getAlternativeSchemes().getAlternativeSchemeParameters() != null) {
final List alternativeSchemes = new ArrayList<>(qrInvoice.getAlternativeSchemes().getAlternativeSchemeParameters());
Collections.reverse(alternativeSchemes);
final PdfContentByte canvas = writer.getDirectContent();
canvas.beginText();
for (final String alternativeScheme : alternativeSchemes) {
final AlternativeSchemesUtils.AlternativeSchemePair scheme = AlternativeSchemesUtils.parseForOutput(alternativeScheme);
if (!scheme.isEmpty()) {
if (scheme.hasName()) {
// 3.4 - font size - v2.0: When filling in the "Alternative procedures" element, the font size is 7 pt, with the name of the alternative procedure printed in bold.
canvas.setFontAndSize(paymentPartHeadingFont.getBaseFont(), PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.showTextAligned(ALIGN_LEFT, scheme.getName(), x, y, 0);
if (scheme.hasValue()) {
final float widthName = paymentPartHeadingFont.getBaseFont().getWidthPoint(scheme.getName(), PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
final float remainingSpaceValue = paymentPartFurtherInfoSectionRect.getWidth() - widthName;
String value = trimIfRequired(scheme.getValue(), remainingSpaceValue, paymentPartValueFont, PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.setFontAndSize(paymentPartValueFont.getBaseFont(), PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.showText(value);
}
} else {
String value = trimIfRequired(scheme.getValue(), paymentPartFurtherInfoSectionRect.getWidth(), paymentPartValueFont, PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.setFontAndSize(paymentPartValueFont.getBaseFont(), PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.showTextAligned(ALIGN_LEFT, value, x, y, 0);
}
y += PAYMENT_PART_FONT_SIZE_FURTHER_INFO;
y += PAYMENT_PART_FURTHER_INFO_LINE_SPACING;
}
}
canvas.endText();
}
// Ultimate Creditor / FINAL Creditor - according to the spec 3.5.5 - further information section:
// "This section is where the "Final creditor" field, if available and approved for use, is displayed.
// Instead of the designation "Final creditor", the relevant values in the Swiss QR Code are preceded by the words "In favour of" (bold).
// Just one line is available, so it is possible that not all the information in the QR-bill can be printed there.
// If that is the case, the shortened entry must be marked by "..." at the end. The data is printed in font size 7 pt, in the same order as in the Swiss QR Code."
final UltimateCreditor ultimateCreditor = qrInvoice.getUltimateCreditor();
if (!AddressUtils.isEmpty(ultimateCreditor)) {
final PdfContentByte canvas = writer.getDirectContent();
canvas.beginText();
final String ultmtCdtrLabel = getLabel("UltmtCdtr") + " ";
canvas.setFontAndSize(paymentPartHeadingFont.getBaseFont(), PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.showTextAligned(ALIGN_LEFT, ultmtCdtrLabel, x, y, 0);
final float widthName = paymentPartHeadingFont.getBaseFont().getWidthPoint(ultmtCdtrLabel, PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
final float remainingWidthAddressLine = paymentPartFurtherInfoSectionRect.getWidth() - widthName;
final String addressLine = trimIfRequired(AddressUtils.toSingleLineAddress(ultimateCreditor), remainingWidthAddressLine, paymentPartValueFont, PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.setFontAndSize(paymentPartValueFont.getBaseFont(), PAYMENT_PART_FONT_SIZE_FURTHER_INFO);
canvas.showText(addressLine);
canvas.endText();
}
}
/**
* According to the spec 3.5.5 - Further information section - v2.0:
* "In the Swiss QR Code, there are always 100 alphanumerical characters available for the "Alternative procedures".
* A maximum of approx. 90 characters can be printed on one line, so it is possible that not all the data included in the QR code can be displayed.
* If that is the case, the shortened entry must be marked by "..." at the end. It must be ensured that all personal data is displayed."
*
* @param input the String to trim if required
* @param maxWidth the max width available for printing the string
* @param font the font used to measure the required text width
* @param fontSize the font size used to measure the required text width
* @return the passed input string or a trimmed version of it
*/
private String trimIfRequired(String input, float maxWidth, Font font, int fontSize) {
float widthPoint = font.getBaseFont().getWidthPoint(input, fontSize);
if (widthPoint <= maxWidth) {
return input;
}
String newString = input;
while (widthPoint > maxWidth) {
final int len = newString.length();
final float ratioTooWide = widthPoint / maxWidth;
// make sure string gets shorter in any circumstances
final int newLen = Math.min((int) Math.floor(len / ratioTooWide), newString.length() - 1);
newString = newString.substring(0, newLen);
widthPoint = font.getBaseFont().getWidthPoint(newString + MORE_TEXT_INDICATOR, fontSize);
}
return newString + MORE_TEXT_INDICATOR;
}
private void writeFreeTextBox(final PdfContentByte canvas, final Rect rect) {
final float cornerLength = BOX_CORNER_LINE_LENGTH;
final float leftX = rect.getLowerLeftX();
final float lowerY = rect.getLowerLeftY();
final float rightX = rect.getUpperRightX();
final float upperY = rect.getUpperRightY();
canvas.setColorStroke(Color.BLACK);
canvas.setLineWidth(BOX_CORNER_LINE_WIDTH);
// left bottom
canvas.moveTo(leftX + cornerLength, lowerY);
canvas.lineTo(leftX, lowerY);
canvas.lineTo(leftX, lowerY + cornerLength);
canvas.stroke();
canvas.closePath();
// left top
canvas.moveTo(leftX + cornerLength, upperY);
canvas.lineTo(leftX, upperY);
canvas.lineTo(leftX, upperY - cornerLength);
canvas.stroke();
canvas.closePath();
// right upper
canvas.moveTo(rightX - cornerLength, upperY);
canvas.lineTo(rightX, upperY);
canvas.lineTo(rightX, upperY - cornerLength);
canvas.stroke();
canvas.closePath();
// right bottom
canvas.moveTo(rightX - cornerLength, lowerY);
canvas.lineTo(rightX, lowerY);
canvas.lineTo(rightX, lowerY + cornerLength);
canvas.stroke();
canvas.closePath();
}
private void drawBoundaryLines(final PdfContentByte canvas) {
BoundaryLines boundaryLines = getOptions().getBoundaryLines();
if (boundaryLines == null) {
return;
}
switch (boundaryLines) {
case ENABLED:
case ENABLED_WITH_MARGINS:
writeSeparationLabel(canvas);
drawVerticalBoundaryLine(canvas, boundaryLines);
drawHorizontalBoundaryLine(canvas, boundaryLines);
break;
}
}
private void writeSeparationLabel(final PdfContentByte canvas) {
if (getOptions().isBoundaryLineSeparationText() && getOptions().getPageSize().greaterThan(ch.codeblock.qrinvoice.PageSize.DIN_LANG)) {
canvas.setFontAndSize(separationLabelFont.getBaseFont(), separationLabelFont.getSize());
final float y = rootLowerLeft.getY() + getPaymentPartHeight() + separationLabelFont.getBaseFont().getAscentPoint(getLabel("SeparationLabel"), separationLabelFont.getSize());
final float x = paymentPartTitleSectionRect.getLeft();
canvas.beginText();
canvas.showTextAligned(ALIGN_LEFT, getLabel("SeparationLabel"), x, y, 0);
canvas.endText();
} else if (getOptions().isBoundaryLineSeparationText()) {
logger.warn("Separation label above payment part was not printed as PageSize is too small.");
}
}
private void drawVerticalBoundaryLine(final PdfContentByte canvas, BoundaryLines boundaryLines) {
final float x = paymentPartLowerLeft.getX();
final float lowerY = paymentPartLowerLeft.getY();
final float upperY = lowerY + getPaymentPartHeight();
float lowerYwithMargin = boundaryLines == BoundaryLines.ENABLED_WITH_MARGINS ? lowerY + printMarginPts : lowerY;
canvas.setColorStroke(Color.BLACK);
canvas.setLineWidth(BOUNDARY_LINE_WIDTH);
if (getOptions().isBoundaryLineScissors()) {
float scissorY = upperY - QUIET_SPACE_PTS;
drawScissor(canvas, x, scissorY, true);
canvas.moveTo(x, lowerYwithMargin);
canvas.lineTo(x, scissorY - millimetersToPoints(SCISSOR_LENGTH));
canvas.closePathStroke();
canvas.moveTo(x, scissorY);
canvas.lineTo(x, upperY);
canvas.closePathStroke();
} else {
canvas.moveTo(x, lowerYwithMargin);
canvas.lineTo(x, upperY);
canvas.closePathStroke();
}
}
private void drawHorizontalBoundaryLine(final PdfContentByte canvas, BoundaryLines boundaryLines) {
final float leftX = rootLowerLeft.getX();
final float lineY = paymentPartLowerLeft.getY() + getPaymentPartHeight();
final float rightX = leftX + DIN_LANG.getWidth();
float leftXwithMargin = boundaryLines == BoundaryLines.ENABLED_WITH_MARGINS ? leftX + printMarginPts : leftX;
float rightXwithMargin = boundaryLines == BoundaryLines.ENABLED_WITH_MARGINS ? rightX - printMarginPts : rightX;
canvas.setColorStroke(Color.BLACK);
canvas.setLineWidth(BOUNDARY_LINE_WIDTH);
final boolean printHorizontalScissor = getOptions().isBoundaryLineScissors() && getOptions().getPageSize().greaterThan(ch.codeblock.qrinvoice.PageSize.DIN_LANG);
if (printHorizontalScissor) {
float scissorX = leftXwithMargin + QUIET_SPACE_PTS + additionalPrintMarginPts;
drawScissor(canvas, scissorX, lineY, false);
// lines
canvas.moveTo(leftXwithMargin, lineY);
// two lines - leave out a space for the scissor
canvas.lineTo(scissorX, lineY);
canvas.closePathStroke();
canvas.moveTo(scissorX + millimetersToPoints(SCISSOR_LENGTH), lineY);
canvas.lineTo(rightXwithMargin, lineY);
canvas.closePathStroke();
} else {
if (getOptions().isBoundaryLineScissors()) {
logger.debug("Scissor on horizontal line is only printed when using PageSize A4 or A5");
}
// one line
canvas.moveTo(leftXwithMargin, lineY);
canvas.lineTo(rightXwithMargin, lineY);
canvas.closePathStroke();
}
}
private void drawScissor(final PdfContentByte canvas, final float x, final float y, final boolean rotate90degree) {
final float scissorWidth = millimetersToPoints(SCISSOR_LENGTH);
final float scissorHeight = millimetersToPoints(((float) SCISSOR_LENGTH) * Scissor.getOrigHeight() / Scissor.getOrigWidth());
final double scale = (double) scissorWidth / (double) Scissor.getOrigWidth();
final float scissorRotatedWidth = (rotate90degree ? scissorHeight : scissorWidth);
final float scissorRotatedHeight = (rotate90degree ? scissorWidth : scissorHeight);
final float scissorX = (rotate90degree) ? x - (scissorRotatedWidth / 2f) : x;
final float scissorY = (rotate90degree) ? y - scissorRotatedHeight : y - (scissorRotatedHeight / 2f);
final PdfTemplate template = canvas.createTemplate(scissorRotatedWidth, scissorRotatedHeight);
final Graphics2D g2d = new PdfGraphics2D(template, scissorRotatedWidth, scissorRotatedHeight);
final AffineTransform at = new AffineTransform();
if (rotate90degree) {
at.translate(scissorRotatedWidth, 0);
}
at.rotate(rotate90degree ? Math.toRadians(90) : 0);
at.scale(scale, scale);
g2d.setTransform(at);
Scissor.paint(g2d);
g2d.dispose();
canvas.addTemplate(template, scissorX, scissorY);
}
private void debugLayout(final PdfWriter writer, final Rectangle rectangle) {
if (System.getProperty(SystemProperties.DEBUG_LAYOUT) != null) {
rectangle.setBorder(Rectangle.BOX);
rectangle.setBorderWidth(BOUNDARY_LINE_WIDTH);
rectangle.setBorderColor(Color.BLACK);
writer.getDirectContent().rectangle(rectangle);
}
}
private String neededLinesBreaks(final float boxHeight, final float lineHeight) {
float calculatedNrOfNeededLineBreaks = boxHeight / lineHeight;
int nrOfLineBreaks = (int) Math.round(calculatedNrOfNeededLineBreaks); // round is okay, if overflowed, only half line height, which is okay here
char[] linebreaks = new char[nrOfLineBreaks];
Arrays.fill(linebreaks, '\n');
return String.valueOf(linebreaks);
}
private Chunk chunk(final String text, final boolean newLine, final Font font) {
final String chunkText = newLine ? text + "\n" : text;
final Chunk chunk = new Chunk(chunkText, font);
final Function widthFunction = (str) -> font.getBaseFont().getWidthPoint(str, font.getSize());
applyOptimalLineSplitting(text, paymentPartInformationSectionRect.getWidth(), widthFunction, () -> chunk.setSplitCharacter(SPLIT_ON_ALL_CHARACTERS));
return chunk;
}
private Rectangle itextRectangle(final Rect rect) {
return new Rectangle(rect.getLowerLeftX(), rect.getLowerLeftY(), rect.getUpperRightX(), rect.getUpperRightY());
}
private static final SplitOnAllCharacters SPLIT_ON_ALL_CHARACTERS = new SplitOnAllCharacters();
private static final class SplitOnAllCharacters implements SplitCharacter {
@Override
public boolean isSplitCharacter(final int start, final int current, final int end, final char[] cc, final PdfChunk[] ck) {
return true;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy