diff --git a/README.md b/README.md index 660621f9..0bd6aedd 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,13 @@ Open-source Java library to generate and decode Swiss QR bills (jointly developed with the [.NET version](https://github.com/manuelbl/SwissQRBill.NET)). -Try it yourself and [create a QR bill](https://www.codecrete.net/qrbill). The code for this demonstration (Angular UI and RESTful service) can be found on [GitHub](https://github.com/manuelbl/SwissQRBillDemo) as well. +Try it yourself and [create a QR bill](https://www.codecrete.net/qrbill). The code for this demonstration (Rect UI and RESTful service) can be found on [GitHub](https://github.com/manuelbl/SwissQRBillDemo) as well. -This library implements version 2.1 of the *Swiss Implementation Guidelines QR-bill* valid since September 30, 2019, the *Style guide* released in January 2019 and *Swico Syntax Definition (S1)* from November 11, 2018. +This library implements version 2.2 and 2.3 of the *Swiss Implementation Guidelines QR-bill* from November 20, 2023, and *Swico Syntax Definition (S1)* from November 23, 2018. ## Introduction -The Swiss QR bill is the new QR code based payment format that started on 30 June, 2020. The old payment slip is no longer accepted. - -The new payment slip will be sent electronically in most cases. But it can still be printed at the bottom of an invoice or added to the invoice on a separate sheet. The payer scans the QR code with his/her mobile banking app to initiate the payment. The payment just needs to be confirmed. +The Swiss QR bill is the QR code based payment format that started on 30 June, 2020. The payment slip is sent electronically in most cases. But it can still be printed at the bottom of an invoice or added to the invoice on a separate sheet. The payer scans the QR code with his/her mobile banking app to initiate the payment. The payment just needs to be confirmed. If the invoicing party adds structured bill information (VAT rates, payment conditions etc.) to the QR bill, the payer can automate booking in accounts payable. The invoicing party can also automate the accounts receivable processing as the payment includes all relevant data including a reference number. The Swiss QR bill is convenient for the payer and payee. @@ -46,12 +44,12 @@ If you are using *Maven*, add the below dependency to your `pom.xml`: net.codecrete.qrbill qrbill-generator - [3.0.0,3.999999] + [3.3.0,3.999999] If you are using *Gradle*, add the below dependency to your *build.gradle* file: - compile group: 'net.codecrete.qrbill', name: 'qrbill-generator', version: '3.0.0+' + compile group: 'net.codecrete.qrbill', name: 'qrbill-generator', version: '3.3.0+' To generate a QR bill, you first fill in the `Bill` data structure and then call `QRBill.generate`: @@ -117,6 +115,9 @@ To generate a QR bill, you first fill in the `Bill` data structure and then call } } +More code examples can be found in the [examples](examples) directory. + + ## API Documention [Javadoc API documentation](https://javadoc.io/doc/net.codecrete.qrbill/qrbill-generator): @@ -132,24 +133,15 @@ To generate a QR bill, you first fill in the `Bill` data structure and then call More information can be found in the [Wiki](https://github.com/manuelbl/SwissQRBill/wiki). It's the joint Wiki for the .NET and the Java version. -## PDF generation with Apache PDFBox - -For generating QR bills as PDF, [Apache PDFBox](https://pdfbox.apache.org/) is used. This library is built and tested with version 2 of PDFBox. While this is still the most popular version in use, version 3.0 has been released. It is a major version and includes breaking changes. +## Font license -This library is compatible with both versions. It uses reflection to deal with incompatible differences. +Starting on November 21, 2025, QR bills may use an extended character set (*Extended Latin* instead of a subset of Latin-1). This library is ready for it. The extended character set can be enabled by setting the `characterSet` property of `BillFormat` to `EXTENDED_LATIN`. The current default is `LATIN_1_SUBSET`. -If you want to use version 3 instead of the default version 2, you need to add the following explicit dependency to your project (for Maven): +If the extended character set is used, it is no longer possible to use the PDF standard font *Helvectica* for the text as it is restricted to the smaller *WinANSI* character set. This library will automatically switch to the *Liberation Sans* font and embed the font subset actually used. SVG and PNG ouput can continue to use other fonts. -```xml - - org.apache.pdfbox - pdfbox - 3.0.0 - -``` +The [*Liberation Sans*](https://github.com/liberationfonts/liberation-fonts) font is included in the library. It is made available free of charge by Goolge and Red Hat under the [SIL Open Font License, Version 1.1](https://github.com/liberationfonts/liberation-fonts/blob/main/LICENSE). You will likely need to add their copyright and license information to your product. See the [license](https://github.com/liberationfonts/liberation-fonts/blob/main/LICENSE) for details. -Also see the [PDFBox 3.0 example](https://github.com/manuelbl/SwissQRBill/examples/pdfbox3). ## QR Code diff --git a/examples/pdfbox3/.gitignore b/examples/pdfbox3/.gitignore deleted file mode 100644 index 7ce45552..00000000 --- a/examples/pdfbox3/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/invoice-0.pdf -/invoice-1.pdf -/invoice-2.pdf diff --git a/examples/pdfbox3/README.md b/examples/pdfbox3/README.md deleted file mode 100644 index 560c4356..00000000 --- a/examples/pdfbox3/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Example using PDFBox 3.0 - -The Swiss QR Bill library is built with PDFBox version 2.0 (for PDF generation). PDFBox 3.0 is new -major version. It is not fully backward compatible. The Swiss QR Bill library will detect the -newer version and will still work. - -In order to use PDFBox 3.0, you need to add an explicit dependency to PDFBox version 3.0 to your project -(`pom.xml` in case you are using Maven): - -```xml - - org.apache.pdfbox - pdfbox - 3.0.0 - -``` diff --git a/examples/pdfbox3/build_and_run.sh b/examples/pdfbox3/build_and_run.sh deleted file mode 100755 index 599567af..00000000 --- a/examples/pdfbox3/build_and_run.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -mvn package -mvn exec:java -Dexec.mainClass="net.codecrete.qrbill.examples.QRBillExample" \ No newline at end of file diff --git a/examples/pdfbox3/maven-example.iml b/examples/pdfbox3/maven-example.iml deleted file mode 100644 index 37cd4c3f..00000000 --- a/examples/pdfbox3/maven-example.iml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/examples/pdfbox3/pom.xml b/examples/pdfbox3/pom.xml deleted file mode 100644 index adf2212d..00000000 --- a/examples/pdfbox3/pom.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - 4.0.0 - net.codecrete.qrbill - maven-example - jar - 3.2.0 - Example using PDFBox 3.0 - - - UTF-8 - - - - - net.codecrete.qrbill - qrbill-generator - [3.2.0,3.999999] - - - org.apache.pdfbox - pdfbox - 3.0.0 - - - - - - - maven-compiler-plugin - 3.8.0 - - 1.8 - 1.8 - - - - - - diff --git a/examples/pdfbox3/src/main/java/net/codecrete/qrbill/examples/QRBillExample.java b/examples/pdfbox3/src/main/java/net/codecrete/qrbill/examples/QRBillExample.java deleted file mode 100644 index 3557091a..00000000 --- a/examples/pdfbox3/src/main/java/net/codecrete/qrbill/examples/QRBillExample.java +++ /dev/null @@ -1,89 +0,0 @@ -// -// Swiss QR Bill Generator -// Copyright (c) 2017 Manuel Bleichenbacher -// Licensed under MIT License -// https://opensource.org/licenses/MIT -// -package net.codecrete.qrbill.examples; - -import net.codecrete.qrbill.canvas.PDFCanvas; -import net.codecrete.qrbill.generator.*; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -/** - * Console application for generating a QR bill. - *

- * The QR bill is saved as an SVG file in the working directory. - * The path of the working directory is printed to stdout. - *

- */ -public class QRBillExample { - - public static void main(String[] args) throws IOException, URISyntaxException { - - // Setup bill - Bill bill = new Bill(); - bill.setAccount("CH4431999123000889012"); - bill.setAmountFromDouble(199.95); - bill.setCurrency("CHF"); - - // Set creditor - Address creditor = new Address(); - creditor.setName("Robert Schneider AG"); - creditor.setAddressLine1("Rue du Lac 1268/2/22"); - creditor.setAddressLine2("2501 Biel"); - creditor.setCountryCode("CH"); - bill.setCreditor(creditor); - - // more bill data - bill.setReference("210000000003139471430009017"); - bill.setUnstructuredMessage("Abonnement für 2020"); - - // Set debtor - Address debtor = new Address(); - debtor.setName("Pia-Maria Rutschmann-Schnyder"); - debtor.setAddressLine1("Grosse Marktgasse 28"); - debtor.setAddressLine2("9400 Rorschach"); - debtor.setCountryCode("CH"); - bill.setDebtor(debtor); - - // Set output format - BillFormat format = new BillFormat(); - format.setGraphicsFormat(GraphicsFormat.PDF); - format.setOutputSize(OutputSize.A4_PORTRAIT_SHEET); - format.setLanguage(Language.DE); - bill.setFormat(format); - - // Generate new PDF with QR bill - byte[] svg = QRBill.generate(bill); - Path path0 = Paths.get("invoice-0.pdf"); - Files.write(path0, svg); - System.out.println("QR bill saved at " + path0.toAbsolutePath()); - - // Load existing PDF - Path invoiceWithoutQRBill = Paths.get(QRBillExample.class.getResource("/invoice.pdf").toURI()); - - // Add QR bill to last page - try (PDFCanvas canvas = new PDFCanvas(invoiceWithoutQRBill, PDFCanvas.LAST_PAGE)) { - QRBill.draw(bill, canvas); - Path path = Paths.get("invoice-1.pdf"); - canvas.saveAs(path); - System.out.println("Invoice 1 saved at " + path.toAbsolutePath()); - } - - // Append QR bill to a new page - try (PDFCanvas canvas = new PDFCanvas(invoiceWithoutQRBill, PDFCanvas.NEW_PAGE_AT_END)) { - QRBill.draw(bill, canvas); - Path path = Paths.get("invoice-2.pdf"); - canvas.saveAs(path); - System.out.println("Invoice 2 saved at " + path.toAbsolutePath()); - } - - System.out.println("Generated with version " + QRBill.getLibraryVersion()); - } -} \ No newline at end of file diff --git a/examples/pdfbox3/src/main/resources/invoice.pdf b/examples/pdfbox3/src/main/resources/invoice.pdf deleted file mode 100644 index 437c1f46..00000000 Binary files a/examples/pdfbox3/src/main/resources/invoice.pdf and /dev/null differ diff --git a/generator/build.gradle b/generator/build.gradle index 865c4938..ef202024 100644 --- a/generator/build.gradle +++ b/generator/build.gradle @@ -5,11 +5,11 @@ buildscript { } plugins { - id 'org.sonarqube' version '4.4.1.3373' + id 'jacoco' + id 'org.sonarqube' version '5.0.0.4638' id 'java-library' id 'maven-publish' id 'signing' - id 'jacoco' } repositories { @@ -17,12 +17,13 @@ repositories { } group = 'net.codecrete.qrbill' -version = '3.2.0-SNAPSHOT' +version = '3.3.0-SNAPSHOT' archivesBaseName = 'qrbill-generator' -sourceCompatibility = 1.8 - java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + withJavadocJar() withSourcesJar() } @@ -38,13 +39,6 @@ jar { } } -test { - useJUnitPlatform() - finalizedBy jacocoTestReport - reports.junitXml.required = false - reports.html.required = true -} - javadoc { include 'net/codecrete/qrbill/canvas/*' include 'net/codecrete/qrbill/generator/*' @@ -112,26 +106,21 @@ signing { sonar { properties { property 'sonar.projectName', 'Swiss QR Bill Generator (Java)' + property 'sonar.organization', 'manuelbl-github' + property 'sonar.host', 'https://sonarcloud.io' } } jacocoTestReport { - dependsOn test - reports.xml.required = true - reports.csv.required = false - reports.html.required = false -} - -project.tasks["sonarqube"].dependsOn "jacocoTestReport" - -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' + reports { + xml.required = true + } } dependencies { - api 'org.apache.pdfbox:pdfbox:2.0.29' + api 'org.apache.pdfbox:pdfbox:3.0.2' implementation 'io.nayuki:qrcodegen:1.8.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.3' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' } diff --git a/generator/gradle.properties b/generator/gradle.properties index 52eb54da..977d1b08 100644 --- a/generator/gradle.properties +++ b/generator/gradle.properties @@ -1,2 +1,2 @@ systemProp.sonar.host.url=https://sonarcloud.io -systemProp.sonar.organization=manuelbl-github +org.gradle.jvmargs='-Dfile.encoding=UTF-8' diff --git a/generator/gradle/wrapper/gradle-wrapper.jar b/generator/gradle/wrapper/gradle-wrapper.jar index 7454180f..e6441136 100644 Binary files a/generator/gradle/wrapper/gradle-wrapper.jar and b/generator/gradle/wrapper/gradle-wrapper.jar differ diff --git a/generator/gradle/wrapper/gradle-wrapper.properties b/generator/gradle/wrapper/gradle-wrapper.properties index e750102e..b82aa23a 100644 --- a/generator/gradle/wrapper/gradle-wrapper.properties +++ b/generator/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/generator/gradlew b/generator/gradlew index 1b6c7873..1aa94a42 100755 --- a/generator/gradlew +++ b/generator/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/generator/gradlew.bat b/generator/gradlew.bat index 107acd32..25da30db 100644 --- a/generator/gradlew.bat +++ b/generator/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/generator/src/main/java/net/codecrete/qrbill/canvas/AbstractCanvas.java b/generator/src/main/java/net/codecrete/qrbill/canvas/AbstractCanvas.java index 21c6c4c8..ae0f6030 100644 --- a/generator/src/main/java/net/codecrete/qrbill/canvas/AbstractCanvas.java +++ b/generator/src/main/java/net/codecrete/qrbill/canvas/AbstractCanvas.java @@ -29,6 +29,12 @@ public abstract class AbstractCanvas implements Canvas { */ protected FontMetrics fontMetrics; + /** + * Creates a new instance. + */ + protected AbstractCanvas() { + } + /** * Initializes the font metrics information for the specified font. *

diff --git a/generator/src/main/java/net/codecrete/qrbill/canvas/CharWidthData.java b/generator/src/main/java/net/codecrete/qrbill/canvas/CharWidthData.java index 5dbb4545..930e584a 100644 --- a/generator/src/main/java/net/codecrete/qrbill/canvas/CharWidthData.java +++ b/generator/src/main/java/net/codecrete/qrbill/canvas/CharWidthData.java @@ -14,6 +14,7 @@ * range allowed for QR bill text is covered. *

*/ +@SuppressWarnings("java:S125") class CharWidthData { private CharWidthData() { @@ -31,9 +32,14 @@ private CharWidthData() { static final char HELVETICA_NORMAL_NDASH_WIDTH = 556; /** - * Character widths for Helvetica Normal (range 0x20 to 0x7f) + * Width of euro sign for Helvetica Normal */ - static final char[] HELVETICA_NORMAL_20_7F = { + static final char HELVETICA_NORMAL_EURO_WIDTH = 744; + + /** + * Character widths for Helvetica Normal (range 0x20 to 0x7e) + */ + static final char[] HELVETICA_NORMAL_20_7E = { 278, // 0x20 278, // 0x21 ! 355, // 0x22 " @@ -128,14 +134,13 @@ private CharWidthData() { 334, // 0x7B { 260, // 0x7C | 334, // 0x7D } - 584, // 0x7E ~ - 0 // unused + 584 // 0x7E ~ }; /** - * Character widths for Helvetica Normal (range 0xa0 to 0xff) + * Character widths for Helvetica Normal (range 0xa0 to 0x17f) */ - static final char[] HELVETICA_NORMAL_A0_FF = { + static final char[] HELVETICA_NORMAL_A0_17F = { 278, // 0xA0 non-breaking space 333, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -231,7 +236,145 @@ private CharWidthData() { 556, // 0xFC ü 500, // 0xFD ý 556, // 0xFE þ - 500 // 0xFF ÿ + 500, // 0xFF ÿ + 667, // 0x100 Ā + 556, // 0x101 ā + 667, // 0x102 Ă + 556, // 0x103 ă + 667, // 0x104 Ą + 556, // 0x105 ą + 722, // 0x106 Ć + 500, // 0x107 ć + 722, // 0x108 Ĉ + 500, // 0x109 ĉ + 722, // 0x10A Ċ + 500, // 0x10B ċ + 722, // 0x10C Č + 500, // 0x10D č + 722, // 0x10E Ď + 660, // 0x10F ď + 722, // 0x110 Đ + 556, // 0x111 đ + 667, // 0x112 Ē + 556, // 0x113 ē + 667, // 0x114 Ĕ + 556, // 0x115 ĕ + 667, // 0x116 Ė + 556, // 0x117 ė + 667, // 0x118 Ę + 556, // 0x119 ę + 667, // 0x11A Ě + 556, // 0x11B ě + 778, // 0x11C Ĝ + 556, // 0x11D ĝ + 778, // 0x11E Ğ + 556, // 0x11F ğ + 778, // 0x120 Ġ + 556, // 0x121 ġ + 778, // 0x122 Ģ + 556, // 0x123 ģ + 722, // 0x124 Ĥ + 556, // 0x125 ĥ + 722, // 0x126 Ħ + 556, // 0x127 ħ + 445, // 0x128 Ĩ + 430, // 0x129 ĩ + 400, // 0x12A Ī + 390, // 0x12B ī + 278, // 0x12C Ĭ + 278, // 0x12D ĭ + 278, // 0x12E Į + 222, // 0x12F į + 278, // 0x130 İ + 278, // 0x131 ı + 778, // 0x132 IJ + 444, // 0x133 ij + 500, // 0x134 Ĵ + 222, // 0x135 ĵ + 667, // 0x136 Ķ + 500, // 0x137 ķ + 500, // 0x138 ĸ + 556, // 0x139 Ĺ + 265, // 0x13A ĺ + 556, // 0x13B Ļ + 222, // 0x13C ļ + 556, // 0x13D Ľ + 263, // 0x13E ľ + 556, // 0x13F Ŀ + 310, // 0x140 ŀ + 556, // 0x141 Ł + 222, // 0x142 ł + 722, // 0x143 Ń + 556, // 0x144 ń + 722, // 0x145 Ņ + 556, // 0x146 ņ + 722, // 0x147 Ň + 556, // 0x148 ň + 556, // 0x149 ʼn + 722, // 0x14A Ŋ + 556, // 0x14B ŋ + 778, // 0x14C Ō + 556, // 0x14D ō + 778, // 0x14E Ŏ + 556, // 0x14F ŏ + 778, // 0x150 Ő + 556, // 0x151 ő + 1000, // 0x152 Œ + 944, // 0x153 œ + 722, // 0x154 Ŕ + 333, // 0x155 ŕ + 722, // 0x156 Ŗ + 333, // 0x157 ŗ + 722, // 0x158 Ř + 333, // 0x159 ř + 667, // 0x15A Ś + 500, // 0x15B ś + 667, // 0x15C Ŝ + 500, // 0x15D ŝ + 667, // 0x15E Ş + 500, // 0x15F ş + 667, // 0x160 Š + 500, // 0x161 š + 611, // 0x162 Ţ + 278, // 0x163 ţ + 611, // 0x164 Ť + 360, // 0x165 ť + 611, // 0x166 Ŧ + 320, // 0x167 ŧ + 722, // 0x168 Ũ + 556, // 0x169 ũ + 722, // 0x16A Ū + 556, // 0x16B ū + 722, // 0x16C Ŭ + 556, // 0x16D ŭ + 722, // 0x16E Ů + 556, // 0x16F ů + 722, // 0x170 Ű + 556, // 0x171 ű + 722, // 0x172 Ų + 556, // 0x173 ų + 944, // 0x174 Ŵ + 722, // 0x175 ŵ + 667, // 0x176 Ŷ + 500, // 0x177 ŷ + 667, // 0x178 Ÿ + 611, // 0x179 Ź + 500, // 0x17A ź + 611, // 0x17B Ż + 500, // 0x17C ż + 611, // 0x17D Ž + 500, // 0x17E ž + 278 // 0x17F ſ + }; + + /** + * Character widths for Helvetica Normal (range 0x218 to 0x21b) + */ + static final char[] HELVETICA_NORMAL_218_21B = { + 667, // 0x218 Ș + 500, // 0x219 ș + 611, // 0x21A Ț + 278 // 0x21B ț }; /** @@ -245,9 +388,14 @@ private CharWidthData() { static final char HELVETICA_BOLD_NDASH_WIDTH = 556; /** - * Character widths for Helvetica Bold (range 0x20 to 0x7f) + * Width of euro sign for Helvetica Bold */ - static final char[] HELVETICA_BOLD_20_7F = { + static final char HELVETICA_BOLD_EURO_WIDTH = 744; + + /** + * Character widths for Helvetica Bold (range 0x20 to 0x7e) + */ + static final char[] HELVETICA_BOLD_20_7E = { 278, // 0x20 333, // 0x21 ! 474, // 0x22 " @@ -342,14 +490,13 @@ private CharWidthData() { 389, // 0x7B { 280, // 0x7C | 389, // 0x7D } - 584, // 0x7E ~ - 0 // unused + 584 // 0x7E ~ }; /** - * Character widths for Helvetica Bold (range 0xa0 to 0xff) + * Character widths for Helvetica Bold (range 0xa0 to 0x17f) */ - static final char[] HELVETICA_BOLD_A0_FF = { + static final char[] HELVETICA_BOLD_A0_17F = { 278, // 0xA0 non-breaking space 333, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -445,7 +592,145 @@ private CharWidthData() { 611, // 0xFC ü 556, // 0xFD ý 611, // 0xFE þ - 556 // 0xFF ÿ + 556, // 0xFF ÿ + 722, // 0x100 Ā + 556, // 0x101 ā + 722, // 0x102 Ă + 556, // 0x103 ă + 722, // 0x104 Ą + 556, // 0x105 ą + 722, // 0x106 Ć + 556, // 0x107 ć + 722, // 0x108 Ĉ + 556, // 0x109 ĉ + 722, // 0x10A Ċ + 556, // 0x10B ċ + 722, // 0x10C Č + 556, // 0x10D č + 722, // 0x10E Ď + 750, // 0x10F ď + 722, // 0x110 Đ + 611, // 0x111 đ + 667, // 0x112 Ē + 556, // 0x113 ē + 667, // 0x114 Ĕ + 556, // 0x115 ĕ + 667, // 0x116 Ė + 556, // 0x117 ė + 667, // 0x118 Ę + 556, // 0x119 ę + 667, // 0x11A Ě + 556, // 0x11B ě + 778, // 0x11C Ĝ + 611, // 0x11D ĝ + 778, // 0x11E Ğ + 611, // 0x11F ğ + 778, // 0x120 Ġ + 611, // 0x121 ġ + 778, // 0x122 Ģ + 611, // 0x123 ģ + 722, // 0x124 Ĥ + 611, // 0x125 ĥ + 722, // 0x126 Ħ + 611, // 0x127 ħ + 420, // 0x128 Ĩ + 430, // 0x129 ĩ + 425, // 0x12A Ī + 420, // 0x12B ī + 278, // 0x12C Ĭ + 278, // 0x12D ĭ + 278, // 0x12E Į + 278, // 0x12F į + 278, // 0x130 İ + 278, // 0x131 ı + 834, // 0x132 IJ + 556, // 0x133 ij + 556, // 0x134 Ĵ + 310, // 0x135 ĵ + 722, // 0x136 Ķ + 556, // 0x137 ķ + 556, // 0x138 ĸ + 611, // 0x139 Ĺ + 305, // 0x13A ĺ + 611, // 0x13B Ļ + 278, // 0x13C ļ + 611, // 0x13D Ľ + 417, // 0x13E ľ + 611, // 0x13F Ŀ + 395, // 0x140 ŀ + 611, // 0x141 Ł + 278, // 0x142 ł + 722, // 0x143 Ń + 611, // 0x144 ń + 722, // 0x145 Ņ + 611, // 0x146 ņ + 722, // 0x147 Ň + 611, // 0x148 ň + 611, // 0x149 ʼn + 722, // 0x14A Ŋ + 611, // 0x14B ŋ + 778, // 0x14C Ō + 611, // 0x14D ō + 778, // 0x14E Ŏ + 611, // 0x14F ŏ + 778, // 0x150 Ő + 611, // 0x151 ő + 1000, // 0x152 Œ + 944, // 0x153 œ + 722, // 0x154 Ŕ + 389, // 0x155 ŕ + 722, // 0x156 Ŗ + 389, // 0x157 ŗ + 722, // 0x158 Ř + 389, // 0x159 ř + 667, // 0x15A Ś + 556, // 0x15B ś + 667, // 0x15C Ŝ + 556, // 0x15D ŝ + 667, // 0x15E Ş + 556, // 0x15F ş + 667, // 0x160 Š + 556, // 0x161 š + 611, // 0x162 Ţ + 333, // 0x163 ţ + 611, // 0x164 Ť + 465, // 0x165 ť + 611, // 0x166 Ŧ + 333, // 0x167 ŧ + 722, // 0x168 Ũ + 611, // 0x169 ũ + 722, // 0x16A Ū + 611, // 0x16B ū + 722, // 0x16C Ŭ + 611, // 0x16D ŭ + 722, // 0x16E Ů + 611, // 0x16F ů + 722, // 0x170 Ű + 611, // 0x171 ű + 722, // 0x172 Ų + 611, // 0x173 ų + 944, // 0x174 Ŵ + 778, // 0x175 ŵ + 667, // 0x176 Ŷ + 556, // 0x177 ŷ + 667, // 0x178 Ÿ + 611, // 0x179 Ź + 500, // 0x17A ź + 611, // 0x17B Ż + 500, // 0x17C ż + 611, // 0x17D Ž + 500, // 0x17E ž + 333 // 0x17F ſ + }; + + /** + * Character widths for Helvetica Bold (range 0x218 to 0x21b) + */ + static final char[] HELVETICA_BOLD_218_21B = { + 667, // 0x218 Ș + 556, // 0x219 ș + 611, // 0x21A Ț + 333 // 0x21B ț }; /** @@ -459,9 +744,14 @@ private CharWidthData() { static final char ARIAL_NORMAL_NDASH_WIDTH = 556; /** - * Character widths for Arial Normal (range 0x20 to 0x7f) + * Width of euro sign for Arial Normal */ - static final char[] ARIAL_NORMAL_20_7F = { + static final char ARIAL_NORMAL_EURO_WIDTH = 556; + + /** + * Character widths for Arial Normal (range 0x20 to 0x7e) + */ + static final char[] ARIAL_NORMAL_20_7E = { 278, // 0x20 278, // 0x21 ! 355, // 0x22 " @@ -556,14 +846,13 @@ private CharWidthData() { 334, // 0x7B { 260, // 0x7C | 334, // 0x7D } - 584, // 0x7E ~ - 0 // unused + 584 // 0x7E ~ }; /** - * Character widths for Arial Normal (range 0xa0 to 0xff) + * Character widths for Arial Normal (range 0xa0 to 0x17f) */ - static final char[] ARIAL_NORMAL_A0_FF = { + static final char[] ARIAL_NORMAL_A0_17F = { 278, // 0xA0 non-breaking space 333, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -659,7 +948,145 @@ private CharWidthData() { 556, // 0xFC ü 500, // 0xFD ý 556, // 0xFE þ - 500 // 0xFF ÿ + 500, // 0xFF ÿ + 667, // 0x100 Ā + 556, // 0x101 ā + 667, // 0x102 Ă + 556, // 0x103 ă + 667, // 0x104 Ą + 556, // 0x105 ą + 722, // 0x106 Ć + 500, // 0x107 ć + 722, // 0x108 Ĉ + 500, // 0x109 ĉ + 722, // 0x10A Ċ + 500, // 0x10B ċ + 722, // 0x10C Č + 500, // 0x10D č + 722, // 0x10E Ď + 615, // 0x10F ď + 722, // 0x110 Đ + 556, // 0x111 đ + 667, // 0x112 Ē + 556, // 0x113 ē + 667, // 0x114 Ĕ + 556, // 0x115 ĕ + 667, // 0x116 Ė + 556, // 0x117 ė + 667, // 0x118 Ę + 556, // 0x119 ę + 667, // 0x11A Ě + 556, // 0x11B ě + 778, // 0x11C Ĝ + 556, // 0x11D ĝ + 778, // 0x11E Ğ + 556, // 0x11F ğ + 778, // 0x120 Ġ + 556, // 0x121 ġ + 778, // 0x122 Ģ + 556, // 0x123 ģ + 722, // 0x124 Ĥ + 556, // 0x125 ĥ + 722, // 0x126 Ħ + 556, // 0x127 ħ + 278, // 0x128 Ĩ + 278, // 0x129 ĩ + 278, // 0x12A Ī + 278, // 0x12B ī + 278, // 0x12C Ĭ + 278, // 0x12D ĭ + 278, // 0x12E Į + 222, // 0x12F į + 278, // 0x130 İ + 278, // 0x131 ı + 735, // 0x132 IJ + 444, // 0x133 ij + 500, // 0x134 Ĵ + 222, // 0x135 ĵ + 667, // 0x136 Ķ + 500, // 0x137 ķ + 500, // 0x138 ĸ + 556, // 0x139 Ĺ + 222, // 0x13A ĺ + 556, // 0x13B Ļ + 222, // 0x13C ļ + 556, // 0x13D Ľ + 292, // 0x13E ľ + 556, // 0x13F Ŀ + 334, // 0x140 ŀ + 556, // 0x141 Ł + 222, // 0x142 ł + 722, // 0x143 Ń + 556, // 0x144 ń + 722, // 0x145 Ņ + 556, // 0x146 ņ + 722, // 0x147 Ň + 556, // 0x148 ň + 604, // 0x149 ʼn + 723, // 0x14A Ŋ + 556, // 0x14B ŋ + 778, // 0x14C Ō + 556, // 0x14D ō + 778, // 0x14E Ŏ + 556, // 0x14F ŏ + 778, // 0x150 Ő + 556, // 0x151 ő + 1000, // 0x152 Œ + 944, // 0x153 œ + 722, // 0x154 Ŕ + 333, // 0x155 ŕ + 722, // 0x156 Ŗ + 333, // 0x157 ŗ + 722, // 0x158 Ř + 333, // 0x159 ř + 667, // 0x15A Ś + 500, // 0x15B ś + 667, // 0x15C Ŝ + 500, // 0x15D ŝ + 667, // 0x15E Ş + 500, // 0x15F ş + 667, // 0x160 Š + 500, // 0x161 š + 611, // 0x162 Ţ + 278, // 0x163 ţ + 611, // 0x164 Ť + 375, // 0x165 ť + 611, // 0x166 Ŧ + 278, // 0x167 ŧ + 722, // 0x168 Ũ + 556, // 0x169 ũ + 722, // 0x16A Ū + 556, // 0x16B ū + 722, // 0x16C Ŭ + 556, // 0x16D ŭ + 722, // 0x16E Ů + 556, // 0x16F ů + 722, // 0x170 Ű + 556, // 0x171 ű + 722, // 0x172 Ų + 556, // 0x173 ų + 944, // 0x174 Ŵ + 722, // 0x175 ŵ + 667, // 0x176 Ŷ + 500, // 0x177 ŷ + 667, // 0x178 Ÿ + 611, // 0x179 Ź + 500, // 0x17A ź + 611, // 0x17B Ż + 500, // 0x17C ż + 611, // 0x17D Ž + 500, // 0x17E ž + 222 // 0x17F ſ + }; + + /** + * Character widths for Arial Normal (range 0x218 to 0x21b) + */ + static final char[] ARIAL_NORMAL_218_21B = { + 667, // 0x218 Ș + 500, // 0x219 ș + 611, // 0x21A Ț + 278 // 0x21B ț }; /** @@ -673,9 +1100,14 @@ private CharWidthData() { static final char ARIAL_BOLD_NDASH_WIDTH = 556; /** - * Character widths for Arial Bold (range 0x20 to 0x7f) + * Width of euro sign for Arial Bold */ - static final char[] ARIAL_BOLD_20_7F = { + static final char ARIAL_BOLD_EURO_WIDTH = 556; + + /** + * Character widths for Arial Bold (range 0x20 to 0x7e) + */ + static final char[] ARIAL_BOLD_20_7E = { 278, // 0x20 333, // 0x21 ! 474, // 0x22 " @@ -770,14 +1202,13 @@ private CharWidthData() { 389, // 0x7B { 280, // 0x7C | 389, // 0x7D } - 584, // 0x7E ~ - 0 // unused + 584 // 0x7E ~ }; /** - * Character widths for Arial Bold (range 0xa0 to 0xff) + * Character widths for Arial Bold (range 0xa0 to 0x17f) */ - static final char[] ARIAL_BOLD_A0_FF = { + static final char[] ARIAL_BOLD_A0_17F = { 278, // 0xA0 non-breaking space 333, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -873,7 +1304,145 @@ private CharWidthData() { 611, // 0xFC ü 556, // 0xFD ý 611, // 0xFE þ - 556 // 0xFF ÿ + 556, // 0xFF ÿ + 722, // 0x100 Ā + 556, // 0x101 ā + 722, // 0x102 Ă + 556, // 0x103 ă + 722, // 0x104 Ą + 556, // 0x105 ą + 722, // 0x106 Ć + 556, // 0x107 ć + 722, // 0x108 Ĉ + 556, // 0x109 ĉ + 722, // 0x10A Ċ + 556, // 0x10B ċ + 722, // 0x10C Č + 556, // 0x10D č + 722, // 0x10E Ď + 719, // 0x10F ď + 722, // 0x110 Đ + 611, // 0x111 đ + 667, // 0x112 Ē + 556, // 0x113 ē + 667, // 0x114 Ĕ + 556, // 0x115 ĕ + 667, // 0x116 Ė + 556, // 0x117 ė + 667, // 0x118 Ę + 556, // 0x119 ę + 667, // 0x11A Ě + 556, // 0x11B ě + 778, // 0x11C Ĝ + 611, // 0x11D ĝ + 778, // 0x11E Ğ + 611, // 0x11F ğ + 778, // 0x120 Ġ + 611, // 0x121 ġ + 778, // 0x122 Ģ + 611, // 0x123 ģ + 722, // 0x124 Ĥ + 611, // 0x125 ĥ + 722, // 0x126 Ħ + 611, // 0x127 ħ + 278, // 0x128 Ĩ + 278, // 0x129 ĩ + 278, // 0x12A Ī + 278, // 0x12B ī + 278, // 0x12C Ĭ + 278, // 0x12D ĭ + 278, // 0x12E Į + 278, // 0x12F į + 278, // 0x130 İ + 278, // 0x131 ı + 785, // 0x132 IJ + 556, // 0x133 ij + 556, // 0x134 Ĵ + 278, // 0x135 ĵ + 722, // 0x136 Ķ + 556, // 0x137 ķ + 556, // 0x138 ĸ + 611, // 0x139 Ĺ + 278, // 0x13A ĺ + 611, // 0x13B Ļ + 278, // 0x13C ļ + 611, // 0x13D Ľ + 385, // 0x13E ľ + 611, // 0x13F Ŀ + 479, // 0x140 ŀ + 611, // 0x141 Ł + 278, // 0x142 ł + 722, // 0x143 Ń + 611, // 0x144 ń + 722, // 0x145 Ņ + 611, // 0x146 ņ + 722, // 0x147 Ň + 611, // 0x148 ň + 708, // 0x149 ʼn + 723, // 0x14A Ŋ + 611, // 0x14B ŋ + 778, // 0x14C Ō + 611, // 0x14D ō + 778, // 0x14E Ŏ + 611, // 0x14F ŏ + 778, // 0x150 Ő + 611, // 0x151 ő + 1000, // 0x152 Œ + 944, // 0x153 œ + 722, // 0x154 Ŕ + 389, // 0x155 ŕ + 722, // 0x156 Ŗ + 389, // 0x157 ŗ + 722, // 0x158 Ř + 389, // 0x159 ř + 667, // 0x15A Ś + 556, // 0x15B ś + 667, // 0x15C Ŝ + 556, // 0x15D ŝ + 667, // 0x15E Ş + 556, // 0x15F ş + 667, // 0x160 Š + 556, // 0x161 š + 611, // 0x162 Ţ + 333, // 0x163 ţ + 611, // 0x164 Ť + 479, // 0x165 ť + 611, // 0x166 Ŧ + 333, // 0x167 ŧ + 722, // 0x168 Ũ + 611, // 0x169 ũ + 722, // 0x16A Ū + 611, // 0x16B ū + 722, // 0x16C Ŭ + 611, // 0x16D ŭ + 722, // 0x16E Ů + 611, // 0x16F ů + 722, // 0x170 Ű + 611, // 0x171 ű + 722, // 0x172 Ų + 611, // 0x173 ų + 944, // 0x174 Ŵ + 778, // 0x175 ŵ + 667, // 0x176 Ŷ + 556, // 0x177 ŷ + 667, // 0x178 Ÿ + 611, // 0x179 Ź + 500, // 0x17A ź + 611, // 0x17B Ż + 500, // 0x17C ż + 611, // 0x17D Ž + 500, // 0x17E ž + 278 // 0x17F ſ + }; + + /** + * Character widths for Arial Bold (range 0x218 to 0x21b) + */ + static final char[] ARIAL_BOLD_218_21B = { + 667, // 0x218 Ș + 556, // 0x219 ș + 611, // 0x21A Ț + 333 // 0x21B ț }; /** @@ -887,9 +1456,14 @@ private CharWidthData() { static final char LIBERATION_SANS_NORMAL_NDASH_WIDTH = 556; /** - * Character widths for Liberation Sans Normal (range 0x20 to 0x7f) + * Width of euro sign for Liberation Sans Normal */ - static final char[] LIBERATION_SANS_NORMAL_20_7F = { + static final char LIBERATION_SANS_NORMAL_EURO_WIDTH = 556; + + /** + * Character widths for Liberation Sans Normal (range 0x20 to 0x7e) + */ + static final char[] LIBERATION_SANS_NORMAL_20_7E = { 278, // 0x20 278, // 0x21 ! 355, // 0x22 " @@ -984,14 +1558,13 @@ private CharWidthData() { 334, // 0x7B { 260, // 0x7C | 334, // 0x7D } - 584, // 0x7E ~ - 0 // unused + 584 // 0x7E ~ }; /** - * Character widths for Liberation Sans Normal (range 0xa0 to 0xff) + * Character widths for Liberation Sans Normal (range 0xa0 to 0x17f) */ - static final char[] LIBERATION_SANS_NORMAL_A0_FF = { + static final char[] LIBERATION_SANS_NORMAL_A0_17F = { 278, // 0xA0 non-breaking space 333, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -1087,7 +1660,145 @@ private CharWidthData() { 556, // 0xFC ü 500, // 0xFD ý 556, // 0xFE þ - 500 // 0xFF ÿ + 500, // 0xFF ÿ + 667, // 0x100 Ā + 556, // 0x101 ā + 667, // 0x102 Ă + 556, // 0x103 ă + 667, // 0x104 Ą + 556, // 0x105 ą + 722, // 0x106 Ć + 500, // 0x107 ć + 722, // 0x108 Ĉ + 500, // 0x109 ĉ + 722, // 0x10A Ċ + 500, // 0x10B ċ + 722, // 0x10C Č + 500, // 0x10D č + 722, // 0x10E Ď + 615, // 0x10F ď + 722, // 0x110 Đ + 556, // 0x111 đ + 667, // 0x112 Ē + 556, // 0x113 ē + 667, // 0x114 Ĕ + 556, // 0x115 ĕ + 667, // 0x116 Ė + 556, // 0x117 ė + 667, // 0x118 Ę + 556, // 0x119 ę + 667, // 0x11A Ě + 556, // 0x11B ě + 778, // 0x11C Ĝ + 556, // 0x11D ĝ + 778, // 0x11E Ğ + 556, // 0x11F ğ + 778, // 0x120 Ġ + 556, // 0x121 ġ + 778, // 0x122 Ģ + 556, // 0x123 ģ + 722, // 0x124 Ĥ + 556, // 0x125 ĥ + 722, // 0x126 Ħ + 556, // 0x127 ħ + 278, // 0x128 Ĩ + 278, // 0x129 ĩ + 278, // 0x12A Ī + 278, // 0x12B ī + 278, // 0x12C Ĭ + 278, // 0x12D ĭ + 278, // 0x12E Į + 222, // 0x12F į + 278, // 0x130 İ + 278, // 0x131 ı + 735, // 0x132 IJ + 444, // 0x133 ij + 500, // 0x134 Ĵ + 222, // 0x135 ĵ + 667, // 0x136 Ķ + 500, // 0x137 ķ + 500, // 0x138 ĸ + 556, // 0x139 Ĺ + 222, // 0x13A ĺ + 556, // 0x13B Ļ + 222, // 0x13C ļ + 556, // 0x13D Ľ + 292, // 0x13E ľ + 556, // 0x13F Ŀ + 334, // 0x140 ŀ + 556, // 0x141 Ł + 222, // 0x142 ł + 722, // 0x143 Ń + 556, // 0x144 ń + 722, // 0x145 Ņ + 556, // 0x146 ņ + 722, // 0x147 Ň + 556, // 0x148 ň + 604, // 0x149 ʼn + 723, // 0x14A Ŋ + 556, // 0x14B ŋ + 778, // 0x14C Ō + 556, // 0x14D ō + 778, // 0x14E Ŏ + 556, // 0x14F ŏ + 778, // 0x150 Ő + 556, // 0x151 ő + 1000, // 0x152 Œ + 944, // 0x153 œ + 722, // 0x154 Ŕ + 333, // 0x155 ŕ + 722, // 0x156 Ŗ + 333, // 0x157 ŗ + 722, // 0x158 Ř + 333, // 0x159 ř + 667, // 0x15A Ś + 500, // 0x15B ś + 667, // 0x15C Ŝ + 500, // 0x15D ŝ + 667, // 0x15E Ş + 500, // 0x15F ş + 667, // 0x160 Š + 500, // 0x161 š + 611, // 0x162 Ţ + 278, // 0x163 ţ + 611, // 0x164 Ť + 375, // 0x165 ť + 611, // 0x166 Ŧ + 278, // 0x167 ŧ + 722, // 0x168 Ũ + 556, // 0x169 ũ + 722, // 0x16A Ū + 556, // 0x16B ū + 722, // 0x16C Ŭ + 556, // 0x16D ŭ + 722, // 0x16E Ů + 556, // 0x16F ů + 722, // 0x170 Ű + 556, // 0x171 ű + 722, // 0x172 Ų + 556, // 0x173 ų + 944, // 0x174 Ŵ + 722, // 0x175 ŵ + 667, // 0x176 Ŷ + 500, // 0x177 ŷ + 667, // 0x178 Ÿ + 611, // 0x179 Ź + 500, // 0x17A ź + 611, // 0x17B Ż + 500, // 0x17C ż + 611, // 0x17D Ž + 500, // 0x17E ž + 222 // 0x17F ſ + }; + + /** + * Character widths for Liberation Sans Normal (range 0x218 to 0x21b) + */ + static final char[] LIBERATION_SANS_NORMAL_218_21B = { + 667, // 0x218 Ș + 500, // 0x219 ș + 611, // 0x21A Ț + 278 // 0x21B ț }; /** @@ -1101,9 +1812,14 @@ private CharWidthData() { static final char LIBERATION_SANS_BOLD_NDASH_WIDTH = 556; /** - * Character widths for Liberation Sans Bold (range 0x20 to 0x7f) + * Width of euro sign for Liberation Sans Bold */ - static final char[] LIBERATION_SANS_BOLD_20_7F = { + static final char LIBERATION_SANS_BOLD_EURO_WIDTH = 556; + + /** + * Character widths for Liberation Sans Bold (range 0x20 to 0x7e) + */ + static final char[] LIBERATION_SANS_BOLD_20_7E = { 278, // 0x20 333, // 0x21 ! 474, // 0x22 " @@ -1198,14 +1914,13 @@ private CharWidthData() { 389, // 0x7B { 280, // 0x7C | 389, // 0x7D } - 584, // 0x7E ~ - 0 // unused + 584 // 0x7E ~ }; /** - * Character widths for Liberation Sans Bold (range 0xa0 to 0xff) + * Character widths for Liberation Sans Bold (range 0xa0 to 0x17f) */ - static final char[] LIBERATION_SANS_BOLD_A0_FF = { + static final char[] LIBERATION_SANS_BOLD_A0_17F = { 278, // 0xA0 non-breaking space 333, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -1301,7 +2016,145 @@ private CharWidthData() { 611, // 0xFC ü 556, // 0xFD ý 611, // 0xFE þ - 556 // 0xFF ÿ + 556, // 0xFF ÿ + 722, // 0x100 Ā + 556, // 0x101 ā + 722, // 0x102 Ă + 556, // 0x103 ă + 722, // 0x104 Ą + 556, // 0x105 ą + 722, // 0x106 Ć + 556, // 0x107 ć + 722, // 0x108 Ĉ + 556, // 0x109 ĉ + 722, // 0x10A Ċ + 556, // 0x10B ċ + 722, // 0x10C Č + 556, // 0x10D č + 722, // 0x10E Ď + 719, // 0x10F ď + 722, // 0x110 Đ + 611, // 0x111 đ + 667, // 0x112 Ē + 556, // 0x113 ē + 667, // 0x114 Ĕ + 556, // 0x115 ĕ + 667, // 0x116 Ė + 556, // 0x117 ė + 667, // 0x118 Ę + 556, // 0x119 ę + 667, // 0x11A Ě + 556, // 0x11B ě + 778, // 0x11C Ĝ + 611, // 0x11D ĝ + 778, // 0x11E Ğ + 611, // 0x11F ğ + 778, // 0x120 Ġ + 611, // 0x121 ġ + 778, // 0x122 Ģ + 611, // 0x123 ģ + 722, // 0x124 Ĥ + 611, // 0x125 ĥ + 722, // 0x126 Ħ + 611, // 0x127 ħ + 278, // 0x128 Ĩ + 278, // 0x129 ĩ + 278, // 0x12A Ī + 278, // 0x12B ī + 278, // 0x12C Ĭ + 278, // 0x12D ĭ + 278, // 0x12E Į + 278, // 0x12F į + 278, // 0x130 İ + 278, // 0x131 ı + 785, // 0x132 IJ + 556, // 0x133 ij + 556, // 0x134 Ĵ + 278, // 0x135 ĵ + 722, // 0x136 Ķ + 556, // 0x137 ķ + 556, // 0x138 ĸ + 611, // 0x139 Ĺ + 278, // 0x13A ĺ + 611, // 0x13B Ļ + 278, // 0x13C ļ + 611, // 0x13D Ľ + 385, // 0x13E ľ + 611, // 0x13F Ŀ + 479, // 0x140 ŀ + 611, // 0x141 Ł + 278, // 0x142 ł + 722, // 0x143 Ń + 611, // 0x144 ń + 722, // 0x145 Ņ + 611, // 0x146 ņ + 722, // 0x147 Ň + 611, // 0x148 ň + 708, // 0x149 ʼn + 723, // 0x14A Ŋ + 611, // 0x14B ŋ + 778, // 0x14C Ō + 611, // 0x14D ō + 778, // 0x14E Ŏ + 611, // 0x14F ŏ + 778, // 0x150 Ő + 611, // 0x151 ő + 1000, // 0x152 Œ + 944, // 0x153 œ + 722, // 0x154 Ŕ + 389, // 0x155 ŕ + 722, // 0x156 Ŗ + 389, // 0x157 ŗ + 722, // 0x158 Ř + 389, // 0x159 ř + 667, // 0x15A Ś + 556, // 0x15B ś + 667, // 0x15C Ŝ + 556, // 0x15D ŝ + 667, // 0x15E Ş + 556, // 0x15F ş + 667, // 0x160 Š + 556, // 0x161 š + 611, // 0x162 Ţ + 333, // 0x163 ţ + 611, // 0x164 Ť + 479, // 0x165 ť + 611, // 0x166 Ŧ + 333, // 0x167 ŧ + 722, // 0x168 Ũ + 611, // 0x169 ũ + 722, // 0x16A Ū + 611, // 0x16B ū + 722, // 0x16C Ŭ + 611, // 0x16D ŭ + 722, // 0x16E Ů + 611, // 0x16F ů + 722, // 0x170 Ű + 611, // 0x171 ű + 722, // 0x172 Ų + 611, // 0x173 ų + 944, // 0x174 Ŵ + 778, // 0x175 ŵ + 667, // 0x176 Ŷ + 556, // 0x177 ŷ + 667, // 0x178 Ÿ + 611, // 0x179 Ź + 500, // 0x17A ź + 611, // 0x17B Ż + 500, // 0x17C ż + 611, // 0x17D Ž + 500, // 0x17E ž + 278 // 0x17F ſ + }; + + /** + * Character widths for Liberation Sans Bold (range 0x218 to 0x21b) + */ + static final char[] LIBERATION_SANS_BOLD_218_21B = { + 667, // 0x218 Ș + 556, // 0x219 ș + 611, // 0x21A Ț + 333 // 0x21B ț }; /** @@ -1315,9 +2168,14 @@ private CharWidthData() { static final char FRUTIGER_NORMAL_NDASH_WIDTH = 500; /** - * Character widths for Frutiger Normal (range 0x20 to 0x7f) + * Width of euro sign for Frutiger Normal */ - static final char[] FRUTIGER_NORMAL_20_7F = { + static final char FRUTIGER_NORMAL_EURO_WIDTH = 556; + + /** + * Character widths for Frutiger Normal (range 0x20 to 0x7e) + */ + static final char[] FRUTIGER_NORMAL_20_7E = { 278, // 0x20 389, // 0x21 ! 556, // 0x22 " @@ -1412,14 +2270,13 @@ private CharWidthData() { 278, // 0x7B { 222, // 0x7C | 278, // 0x7D } - 600, // 0x7E ~ - 0 // unused + 600 // 0x7E ~ }; /** - * Character widths for Frutiger Normal (range 0xa0 to 0xff) + * Character widths for Frutiger Normal (range 0xa0 to 0x17f) */ - static final char[] FRUTIGER_NORMAL_A0_FF = { + static final char[] FRUTIGER_NORMAL_A0_17F = { 278, // 0xA0 non-breaking space 389, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -1515,7 +2372,145 @@ private CharWidthData() { 556, // 0xFC ü 444, // 0xFD ý 556, // 0xFE þ - 444 // 0xFF ÿ + 444, // 0xFF ÿ + 667, // 0x100 Ā + 500, // 0x101 ā + 667, // 0x102 Ă + 500, // 0x103 ă + 667, // 0x104 Ą + 500, // 0x105 ą + 667, // 0x106 Ć + 444, // 0x107 ć + 667, // 0x108 Ĉ + 444, // 0x109 ĉ + 667, // 0x10A Ċ + 444, // 0x10B ċ + 667, // 0x10C Č + 444, // 0x10D č + 667, // 0x10E Ď + 604, // 0x10F ď + 667, // 0x110 Đ + 564, // 0x111 đ + 500, // 0x112 Ē + 500, // 0x113 ē + 500, // 0x114 Ĕ + 500, // 0x115 ĕ + 500, // 0x116 Ė + 500, // 0x117 ė + 500, // 0x118 Ę + 500, // 0x119 ę + 500, // 0x11A Ě + 500, // 0x11B ě + 722, // 0x11C Ĝ + 556, // 0x11D ĝ + 722, // 0x11E Ğ + 556, // 0x11F ğ + 722, // 0x120 Ġ + 556, // 0x121 ġ + 722, // 0x122 Ģ + 556, // 0x123 ģ + 667, // 0x124 Ĥ + 556, // 0x125 ĥ + 667, // 0x126 Ħ + 556, // 0x127 ħ + 222, // 0x128 Ĩ + 222, // 0x129 ĩ + 222, // 0x12A Ī + 222, // 0x12B ī + 512, // 0x12C Ĭ + 512, // 0x12D ĭ + 222, // 0x12E Į + 222, // 0x12F į + 222, // 0x130 İ + 222, // 0x131 ı + 530, // 0x132 IJ + 433, // 0x133 ij + 333, // 0x134 Ĵ + 222, // 0x135 ĵ + 611, // 0x136 Ķ + 500, // 0x137 ķ + 512, // 0x138 ĸ + 444, // 0x139 Ĺ + 222, // 0x13A ĺ + 444, // 0x13B Ļ + 222, // 0x13C ļ + 444, // 0x13D Ľ + 270, // 0x13E ľ + 444, // 0x13F Ŀ + 342, // 0x140 ŀ + 444, // 0x141 Ł + 222, // 0x142 ł + 667, // 0x143 Ń + 556, // 0x144 ń + 667, // 0x145 Ņ + 556, // 0x146 ņ + 667, // 0x147 Ň + 556, // 0x148 ň + 616, // 0x149 ʼn + 512, // 0x14A Ŋ + 512, // 0x14B ŋ + 722, // 0x14C Ō + 556, // 0x14D ō + 722, // 0x14E Ŏ + 556, // 0x14F ŏ + 722, // 0x150 Ő + 556, // 0x151 ő + 889, // 0x152 Œ + 889, // 0x153 œ + 556, // 0x154 Ŕ + 333, // 0x155 ŕ + 556, // 0x156 Ŗ + 333, // 0x157 ŗ + 556, // 0x158 Ř + 333, // 0x159 ř + 500, // 0x15A Ś + 389, // 0x15B ś + 500, // 0x15C Ŝ + 389, // 0x15D ŝ + 500, // 0x15E Ş + 389, // 0x15F ş + 500, // 0x160 Š + 389, // 0x161 š + 500, // 0x162 Ţ + 333, // 0x163 ţ + 500, // 0x164 Ť + 339, // 0x165 ť + 500, // 0x166 Ŧ + 333, // 0x167 ŧ + 667, // 0x168 Ũ + 556, // 0x169 ũ + 667, // 0x16A Ū + 556, // 0x16B ū + 667, // 0x16C Ŭ + 556, // 0x16D ŭ + 667, // 0x16E Ů + 556, // 0x16F ů + 667, // 0x170 Ű + 556, // 0x171 ű + 667, // 0x172 Ų + 556, // 0x173 ų + 944, // 0x174 Ŵ + 778, // 0x175 ŵ + 611, // 0x176 Ŷ + 444, // 0x177 ŷ + 611, // 0x178 Ÿ + 500, // 0x179 Ź + 444, // 0x17A ź + 500, // 0x17B Ż + 444, // 0x17C ż + 500, // 0x17D Ž + 444, // 0x17E ž + 512 // 0x17F ſ + }; + + /** + * Character widths for Frutiger Normal (range 0x218 to 0x21b) + */ + static final char[] FRUTIGER_NORMAL_218_21B = { + 500, // 0x218 Ș + 389, // 0x219 ș + 500, // 0x21A Ț + 333 // 0x21B ț }; /** @@ -1529,9 +2524,14 @@ private CharWidthData() { static final char FRUTIGER_BOLD_NDASH_WIDTH = 500; /** - * Character widths for Frutiger Bold (range 0x20 to 0x7f) + * Width of euro sign for Frutiger Bold */ - static final char[] FRUTIGER_BOLD_20_7F = { + static final char FRUTIGER_BOLD_EURO_WIDTH = 556; + + /** + * Character widths for Frutiger Bold (range 0x20 to 0x7e) + */ + static final char[] FRUTIGER_BOLD_20_7E = { 278, // 0x20 389, // 0x21 ! 481, // 0x22 " @@ -1626,14 +2626,13 @@ private CharWidthData() { 333, // 0x7B { 222, // 0x7C | 333, // 0x7D } - 600, // 0x7E ~ - 0 // unused + 600 // 0x7E ~ }; /** - * Character widths for Frutiger Bold (range 0xa0 to 0xff) + * Character widths for Frutiger Bold (range 0xa0 to 0x17f) */ - static final char[] FRUTIGER_BOLD_A0_FF = { + static final char[] FRUTIGER_BOLD_A0_17F = { 278, // 0xA0 non-breaking space 389, // 0xA1 ¡ 556, // 0xA2 ¢ @@ -1729,6 +2728,144 @@ private CharWidthData() { 611, // 0xFC ü 556, // 0xFD ý 611, // 0xFE þ - 556 // 0xFF ÿ + 556, // 0xFF ÿ + 722, // 0x100 Ā + 556, // 0x101 ā + 722, // 0x102 Ă + 556, // 0x103 ă + 722, // 0x104 Ą + 556, // 0x105 ą + 611, // 0x106 Ć + 444, // 0x107 ć + 611, // 0x108 Ĉ + 444, // 0x109 ĉ + 611, // 0x10A Ċ + 444, // 0x10B ċ + 611, // 0x10C Č + 444, // 0x10D č + 722, // 0x10E Ď + 688, // 0x10F ď + 722, // 0x110 Đ + 611, // 0x111 đ + 556, // 0x112 Ē + 556, // 0x113 ē + 556, // 0x114 Ĕ + 556, // 0x115 ĕ + 556, // 0x116 Ė + 556, // 0x117 ė + 556, // 0x118 Ę + 556, // 0x119 ę + 556, // 0x11A Ě + 556, // 0x11B ě + 722, // 0x11C Ĝ + 611, // 0x11D ĝ + 722, // 0x11E Ğ + 611, // 0x11F ğ + 722, // 0x120 Ġ + 611, // 0x121 ġ + 722, // 0x122 Ģ + 611, // 0x123 ģ + 722, // 0x124 Ĥ + 611, // 0x125 ĥ + 722, // 0x126 Ħ + 611, // 0x127 ħ + 278, // 0x128 Ĩ + 278, // 0x129 ĩ + 278, // 0x12A Ī + 278, // 0x12B ī + 512, // 0x12C Ĭ + 512, // 0x12D ĭ + 278, // 0x12E Į + 278, // 0x12F į + 278, // 0x130 İ + 278, // 0x131 ı + 641, // 0x132 IJ + 538, // 0x133 ij + 389, // 0x134 Ĵ + 278, // 0x135 ĵ + 667, // 0x136 Ķ + 556, // 0x137 ķ + 512, // 0x138 ĸ + 500, // 0x139 Ĺ + 278, // 0x13A ĺ + 500, // 0x13B Ļ + 278, // 0x13C ļ + 500, // 0x13D Ľ + 353, // 0x13E ľ + 500, // 0x13F Ŀ + 434, // 0x140 ŀ + 500, // 0x141 Ł + 278, // 0x142 ł + 722, // 0x143 Ń + 611, // 0x144 ń + 722, // 0x145 Ņ + 611, // 0x146 ņ + 722, // 0x147 Ň + 611, // 0x148 ň + 731, // 0x149 ʼn + 512, // 0x14A Ŋ + 512, // 0x14B ŋ + 778, // 0x14C Ō + 611, // 0x14D ō + 778, // 0x14E Ŏ + 611, // 0x14F ŏ + 778, // 0x150 Ő + 611, // 0x151 ő + 944, // 0x152 Œ + 944, // 0x153 œ + 611, // 0x154 Ŕ + 389, // 0x155 ŕ + 611, // 0x156 Ŗ + 389, // 0x157 ŗ + 611, // 0x158 Ř + 389, // 0x159 ř + 556, // 0x15A Ś + 444, // 0x15B ś + 556, // 0x15C Ŝ + 444, // 0x15D ŝ + 556, // 0x15E Ş + 444, // 0x15F ş + 556, // 0x160 Š + 444, // 0x161 š + 556, // 0x162 Ţ + 389, // 0x163 ţ + 556, // 0x164 Ť + 398, // 0x165 ť + 556, // 0x166 Ŧ + 389, // 0x167 ŧ + 722, // 0x168 Ũ + 611, // 0x169 ũ + 722, // 0x16A Ū + 611, // 0x16B ū + 722, // 0x16C Ŭ + 611, // 0x16D ŭ + 722, // 0x16E Ů + 611, // 0x16F ů + 722, // 0x170 Ű + 611, // 0x171 ű + 722, // 0x172 Ų + 611, // 0x173 ų + 1000, // 0x174 Ŵ + 889, // 0x175 ŵ + 667, // 0x176 Ŷ + 556, // 0x177 ŷ + 667, // 0x178 Ÿ + 556, // 0x179 Ź + 500, // 0x17A ź + 556, // 0x17B Ż + 500, // 0x17C ż + 556, // 0x17D Ž + 500, // 0x17E ž + 512 // 0x17F ſ + }; + + /** + * Character widths for Frutiger Bold (range 0x218 to 0x21b) + */ + static final char[] FRUTIGER_BOLD_218_21B = { + 556, // 0x218 Ș + 444, // 0x219 ș + 556, // 0x21A Ț + 389 // 0x21B ț }; } diff --git a/generator/src/main/java/net/codecrete/qrbill/canvas/FontMetrics.java b/generator/src/main/java/net/codecrete/qrbill/canvas/FontMetrics.java index e8157148..005f967a 100644 --- a/generator/src/main/java/net/codecrete/qrbill/canvas/FontMetrics.java +++ b/generator/src/main/java/net/codecrete/qrbill/canvas/FontMetrics.java @@ -23,10 +23,12 @@ public class FontMetrics { private final String fontFamilyList; private final String firstFontFamily; - private final char[] charWidthx20x7F; - private final char[] charWidthxA0xFF; + private final char[] charWidthx20x7E; + private final char[] charWidthxA0x17F; + private final char[] charWidthx218x21B; private final char charDefaultWidth; private final char charNDashWidth; + private final char charEuroWidth; private final FontMetrics boldMetrics; /** @@ -40,58 +42,78 @@ public FontMetrics(String fontFamilyList) { String family = firstFontFamily.toLowerCase(Locale.US); final char[] boldCharWidthx20x7F; - final char[] boldCharWidthxA0xFF; + final char[] boldCharWidthxA0x17F; + final char[] boldCharWidthx218x21B; final char boldCharDefaultWidth; final char boldCharNDashWidth; + final char boldCharEuroWidth; if (family.contains("arial")) { - charWidthx20x7F = CharWidthData.ARIAL_NORMAL_20_7F; - charWidthxA0xFF = CharWidthData.ARIAL_NORMAL_A0_FF; + charWidthx20x7E = CharWidthData.ARIAL_NORMAL_20_7E; + charWidthxA0x17F = CharWidthData.ARIAL_NORMAL_A0_17F; + charWidthx218x21B = CharWidthData.ARIAL_NORMAL_218_21B; charDefaultWidth = CharWidthData.ARIAL_NORMAL_DEFAULT_WIDTH; charNDashWidth = CharWidthData.ARIAL_NORMAL_NDASH_WIDTH; - boldCharWidthx20x7F = CharWidthData.ARIAL_BOLD_20_7F; - boldCharWidthxA0xFF = CharWidthData.ARIAL_BOLD_A0_FF; + charEuroWidth = CharWidthData.ARIAL_NORMAL_EURO_WIDTH; + boldCharWidthx20x7F = CharWidthData.ARIAL_BOLD_20_7E; + boldCharWidthxA0x17F = CharWidthData.ARIAL_BOLD_A0_17F; + boldCharWidthx218x21B = CharWidthData.ARIAL_BOLD_218_21B; boldCharDefaultWidth = CharWidthData.ARIAL_BOLD_DEFAULT_WIDTH; boldCharNDashWidth = CharWidthData.ARIAL_BOLD_NDASH_WIDTH; + boldCharEuroWidth = CharWidthData.ARIAL_BOLD_EURO_WIDTH; } else if (family.contains("liberation") && family.contains("sans")) { - charWidthx20x7F = CharWidthData.LIBERATION_SANS_NORMAL_20_7F; - charWidthxA0xFF = CharWidthData.LIBERATION_SANS_NORMAL_A0_FF; + charWidthx20x7E = CharWidthData.LIBERATION_SANS_NORMAL_20_7E; + charWidthxA0x17F = CharWidthData.LIBERATION_SANS_NORMAL_A0_17F; + charWidthx218x21B = CharWidthData.LIBERATION_SANS_NORMAL_218_21B; charDefaultWidth = CharWidthData.LIBERATION_SANS_NORMAL_DEFAULT_WIDTH; charNDashWidth = CharWidthData.LIBERATION_SANS_NORMAL_NDASH_WIDTH; - boldCharWidthx20x7F = CharWidthData.LIBERATION_SANS_BOLD_20_7F; - boldCharWidthxA0xFF = CharWidthData.LIBERATION_SANS_BOLD_A0_FF; + charEuroWidth = CharWidthData.LIBERATION_SANS_NORMAL_EURO_WIDTH; + boldCharWidthx20x7F = CharWidthData.LIBERATION_SANS_BOLD_20_7E; + boldCharWidthxA0x17F = CharWidthData.LIBERATION_SANS_BOLD_A0_17F; + boldCharWidthx218x21B = CharWidthData.LIBERATION_SANS_BOLD_218_21B; boldCharDefaultWidth = CharWidthData.LIBERATION_SANS_BOLD_DEFAULT_WIDTH; boldCharNDashWidth = CharWidthData.LIBERATION_SANS_BOLD_NDASH_WIDTH; + boldCharEuroWidth = CharWidthData.LIBERATION_SANS_BOLD_EURO_WIDTH; } else if (family.contains("frutiger")) { - charWidthx20x7F = CharWidthData.FRUTIGER_NORMAL_20_7F; - charWidthxA0xFF = CharWidthData.FRUTIGER_NORMAL_A0_FF; + charWidthx20x7E = CharWidthData.FRUTIGER_NORMAL_20_7E; + charWidthxA0x17F = CharWidthData.FRUTIGER_NORMAL_A0_17F; + charWidthx218x21B = CharWidthData.FRUTIGER_NORMAL_218_21B; charDefaultWidth = CharWidthData.FRUTIGER_NORMAL_DEFAULT_WIDTH; charNDashWidth = CharWidthData.FRUTIGER_NORMAL_NDASH_WIDTH; - boldCharWidthx20x7F = CharWidthData.FRUTIGER_BOLD_20_7F; - boldCharWidthxA0xFF = CharWidthData.FRUTIGER_BOLD_A0_FF; + charEuroWidth = CharWidthData.FRUTIGER_NORMAL_EURO_WIDTH; + boldCharWidthx20x7F = CharWidthData.FRUTIGER_BOLD_20_7E; + boldCharWidthxA0x17F = CharWidthData.FRUTIGER_BOLD_A0_17F; + boldCharWidthx218x21B = CharWidthData.FRUTIGER_BOLD_218_21B; boldCharDefaultWidth = CharWidthData.FRUTIGER_BOLD_DEFAULT_WIDTH; boldCharNDashWidth = CharWidthData.FRUTIGER_BOLD_NDASH_WIDTH; + boldCharEuroWidth = CharWidthData.FRUTIGER_BOLD_EURO_WIDTH; } else { - charWidthx20x7F = CharWidthData.HELVETICA_NORMAL_20_7F; - charWidthxA0xFF = CharWidthData.HELVETICA_NORMAL_A0_FF; + charWidthx20x7E = CharWidthData.HELVETICA_NORMAL_20_7E; + charWidthxA0x17F = CharWidthData.HELVETICA_NORMAL_A0_17F; + charWidthx218x21B = CharWidthData.HELVETICA_NORMAL_218_21B; charDefaultWidth = CharWidthData.HELVETICA_NORMAL_DEFAULT_WIDTH; charNDashWidth = CharWidthData.HELVETICA_NORMAL_NDASH_WIDTH; - boldCharWidthx20x7F = CharWidthData.HELVETICA_BOLD_20_7F; - boldCharWidthxA0xFF = CharWidthData.HELVETICA_BOLD_A0_FF; + charEuroWidth = CharWidthData.HELVETICA_NORMAL_EURO_WIDTH; + boldCharWidthx20x7F = CharWidthData.HELVETICA_BOLD_20_7E; + boldCharWidthxA0x17F = CharWidthData.HELVETICA_BOLD_A0_17F; + boldCharWidthx218x21B = CharWidthData.HELVETICA_BOLD_218_21B; boldCharDefaultWidth = CharWidthData.HELVETICA_BOLD_DEFAULT_WIDTH; boldCharNDashWidth = CharWidthData.HELVETICA_BOLD_NDASH_WIDTH; + boldCharEuroWidth = CharWidthData.HELVETICA_BOLD_EURO_WIDTH; } - boldMetrics = new FontMetrics(boldCharWidthx20x7F, boldCharWidthxA0xFF, boldCharDefaultWidth, boldCharNDashWidth); + boldMetrics = new FontMetrics(boldCharWidthx20x7F, boldCharWidthxA0x17F, boldCharWidthx218x21B, boldCharDefaultWidth, boldCharNDashWidth, boldCharEuroWidth); } - private FontMetrics(char[] charWidthx20x7F, char[] charWidthxA0xFF, char charDefaultWidth, char charNDashWidth) { + private FontMetrics(char[] charWidthx20x7E, char[] charWidthxA0x17F, char[] charWidthx218x21B, char charDefaultWidth, char charNDashWidth, char charEuroWidth) { fontFamilyList = null; firstFontFamily = null; - this.charWidthx20x7F = charWidthx20x7F; - this.charWidthxA0xFF = charWidthxA0xFF; + this.charWidthx20x7E = charWidthx20x7E; + this.charWidthxA0x17F = charWidthxA0x17F; + this.charWidthx218x21B = charWidthx218x21B; this.charDefaultWidth = charDefaultWidth; this.charNDashWidth = charNDashWidth; + this.charEuroWidth = charEuroWidth; this.boldMetrics = null; } @@ -156,6 +178,7 @@ public double getLineHeight(int fontSize) { * @param fontSize the font size (in pt) * @return an array of text lines */ + @SuppressWarnings("java:S3776") public String[] splitLines(String text, double maxLength, int fontSize) { /* Yes, this code has a cognitive complexity of 37. Deal with it. */ @@ -297,15 +320,20 @@ public double getTextWidth(CharSequence text, int fontSize, boolean isBold) { * @param ch the character * @return the width of the character */ - private double getCharWidth(char ch) { + private char getCharWidth(char ch) { char width = 0; - if (ch >= 0x20 && ch <= 0x7f) - width = charWidthx20x7F[ch - 0x20]; - else if (ch >= 0xa0 && ch <= 0xff) { - width = charWidthxA0xFF[ch - 0xa0]; + if (ch >= 0x20 && ch <= 0x7e) + width = charWidthx20x7E[ch - 0x20]; + else if (ch >= 0xa0 && ch <= 0x017f) { + width = charWidthxA0x17F[ch - 0xa0]; + } else if (ch >= 0x0218 && ch <= 0x021b) { + width = charWidthx218x21B[ch - 0xa0]; } else if (ch == 0x2013) { width = charNDashWidth; + } else if (ch == 0x20AC) { + width = charEuroWidth; } + if (width == 0 && ch != '\n' && ch != '\r') width = charDefaultWidth; return width; diff --git a/generator/src/main/java/net/codecrete/qrbill/canvas/PDFCanvas.java b/generator/src/main/java/net/codecrete/qrbill/canvas/PDFCanvas.java index 3014a628..5266831f 100644 --- a/generator/src/main/java/net/codecrete/qrbill/canvas/PDFCanvas.java +++ b/generator/src/main/java/net/codecrete/qrbill/canvas/PDFCanvas.java @@ -7,20 +7,20 @@ package net.codecrete.qrbill.canvas; import net.codecrete.qrbill.generator.Bill; +import org.apache.pdfbox.Loader; 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.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.util.Matrix; import java.io.*; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; -import java.util.function.Function; /** * Canvas for generating PDF files. @@ -40,15 +40,9 @@ public class PDFCanvas extends AbstractCanvas implements ByteArrayResult { */ public static final int NEW_PAGE_AT_END = -2; - private static final String PDF_FONT = "Helvetica"; - - private static PDType1Font regularFont; - private static PDType1Font boldFont; - private static Function createDocumentFromPath; - private static Function createDocumentFromBytes; - - private PDDocument document; + private PDFont regularFont; + private PDFont boldFont; private PDPageContentStream contentStream; private final boolean isContentStreamOwned; private int lastStrokingColor = 0; @@ -56,10 +50,6 @@ public class PDFCanvas extends AbstractCanvas implements ByteArrayResult { private double lastLineWidth = 1; private LineStyle lastLineStyle = LineStyle.Solid; - static { - initPdfBox(); - } - /** * Creates a new instance using the specified page size. *

@@ -72,15 +62,44 @@ public class PDFCanvas extends AbstractCanvas implements ByteArrayResult { * the QR bill to this canvas. It will be drawn at the origin of the page, * i.e. the bottom left corner of the bill will be in the bottom left corner of the page. *

+ *

+ * For text, the PDF standard font Helvetica will be used. It does not need to be embedded into + * the file and is available on all PDF viewers. But it is restricted to the WinANSI character set. + *

* * @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(PDF_FONT); + this(width, height, PDFFontSettings.standardHelvetica()); + } + + /** + * Creates a new instance using the specified page size and font. + *

+ * A new PDF file with a single page will be created. + * It can later be retrieved as a byte array (see {@link #toByteArray()}) + * or written to an output stream (see {@link #writeTo(OutputStream)}). + *

+ *

+ * Call {@link net.codecrete.qrbill.generator.QRBill#draw(Bill, Canvas)} to draw + * the QR bill to this canvas. It will be drawn at the origin of the page, + * i.e. the bottom left corner of the bill will be in the bottom left corner of the page. + *

+ *

+ * Font settings specify what font to use and whether to embed the font in the PDF file. + *

+ * + * @param width page width, in mm + * @param height page height, in mm + * @param fontSettings font settings + * @throws IOException thrown if the creation fails + */ + public PDFCanvas(double width, double height, PDFFontSettings fontSettings) throws IOException { document = new PDDocument(); document.getDocumentInformation().setTitle("Swiss QR Bill"); + configureFonts(document, fontSettings); 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); @@ -105,14 +124,48 @@ public PDFCanvas(double width, double height) throws IOException { * The new PDF file can later be retrieved as a byte array (see {@link #toByteArray()}) * or written to an output stream (see {@link #writeTo(OutputStream)}). *

+ *

+ * For text, the PDF standard font Helvetica will be used. It does not need to be embedded into + * the file and is available on all PDF viewers. But it is restricted to the WinANSI character set. + *

* * @param path path to existing PDF 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(PDF_FONT); - document = createDocumentFromPath.apply(path); + this(path, pageNo, PDFFontSettings.standardHelvetica()); + } + + /** + * Creates a new instance for adding the QR bill to an existing PDF document. + *

+ * 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}). If a new page is added, + * it will have A4 portrait format. + *

+ *

+ * Call {@link net.codecrete.qrbill.generator.QRBill#draw(Bill, Canvas)} to draw + * the QR bill to this canvas. It will be drawn at the origin of the page, + * i.e. the bottom left corner of the bill will be in the bottom left corner of the page. + *

+ *

+ * The new PDF file can later be retrieved as a byte array (see {@link #toByteArray()}) + * or written to an output stream (see {@link #writeTo(OutputStream)}). + *

+ *

+ * Font settings specify what font to use and whether to embed the font in the PDF file. + *

+ * + * @param path path to existing PDF document + * @param pageNo the zero-based number of the page the QR bill should be added to + * @param fontSettings font settings + * @throws IOException thrown if the creation fails + */ + public PDFCanvas(Path path, int pageNo, PDFFontSettings fontSettings) throws IOException { + document = Loader.loadPDF(path.toFile()); + configureFonts(document, fontSettings); preparePage(document, pageNo); isContentStreamOwned = true; initGraphicsState(); @@ -135,14 +188,48 @@ public PDFCanvas(Path path, int pageNo) throws IOException { * The new PDF file can later be retrieved as a byte array (see {@link #toByteArray()}) * or written to an output stream (see {@link #writeTo(OutputStream)}). *

+ *

+ * For text, the PDF standard font Helvetica will be used. It does not need to be embedded into + * the file and is available on all PDF viewers. But it is restricted to the WinANSI character set. + *

* * @param pdfDocument binary array containing PDF 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(byte[] pdfDocument, int pageNo) throws IOException { - setupFontMetrics(PDF_FONT); - document = createDocumentFromBytes.apply(pdfDocument); + this(pdfDocument, pageNo, PDFFontSettings.standardHelvetica()); + } + + /** + * Creates a new instance for adding the QR bill to an existing PDF document. + *

+ * 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}). If a new page is added, + * it will have A4 portrait format. + *

+ *

+ * Call {@link net.codecrete.qrbill.generator.QRBill#draw(Bill, Canvas)} to draw + * the QR bill to this canvas. It will be drawn at the origin of the page, + * i.e. the bottom left corner of the bill will be in the bottom left corner of the page. + *

+ *

+ * The new PDF file can later be retrieved as a byte array (see {@link #toByteArray()}) + * or written to an output stream (see {@link #writeTo(OutputStream)}). + *

+ *

+ * Font settings specify what font to use and whether to embed the font in the PDF file. + *

+ * + * @param pdfDocument binary array containing PDF document + * @param pageNo the zero-based number of the page the QR bill should be added to + * @param fontSettings font settings + * @throws IOException thrown if the creation fails + */ + public PDFCanvas(byte[] pdfDocument, int pageNo, PDFFontSettings fontSettings) throws IOException { + document = Loader.loadPDF(pdfDocument); + configureFonts(document, fontSettings); preparePage(document, pageNo); isContentStreamOwned = true; initGraphicsState(); @@ -167,13 +254,49 @@ public PDFCanvas(byte[] pdfDocument, int pageNo) throws IOException { * be closed (see {@link #close()}). The instance methods {@link #toByteArray()} * and {@link #writeTo(OutputStream)} may not be used and will throw an exception. *

+ *

+ * For text, the PDF standard font Helvetica will be used. It does not need to be embedded into + * the file and is available on all PDF viewers. But it is restricted to the WinANSI character set. + *

* * @param pdfDocument PDF 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(PDDocument pdfDocument, int pageNo) throws IOException { - setupFontMetrics(PDF_FONT); + this(pdfDocument, pageNo, PDFFontSettings.standardHelvetica()); + } + + /** + * Creates a new instance for adding the QR bill to the specified PDF document. + *

+ * 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}). If a new page is added, + * it will have A4 portrait format. + *

+ *

+ * Call {@link net.codecrete.qrbill.generator.QRBill#draw(Bill, Canvas)} to draw + * the QR bill to this canvas. It will be drawn at the origin of the page, + * i.e. the bottom left corner of the bill will be in the bottom left corner of the page. + *

+ *

+ * The PDF document must have been opened with the appropriate PDFBox method, + * and it must be saved with a PDFBox method. Before saving it, this instance must + * be closed (see {@link #close()}). The instance methods {@link #toByteArray()} + * and {@link #writeTo(OutputStream)} may not be used and will throw an exception. + *

+ *

+ * Font settings specify what font to use and whether to embed the font in the PDF file. + *

+ * + * @param pdfDocument PDF document + * @param pageNo the zero-based number of the page the QR bill should be added to + * @param fontSettings font settings + * @throws IOException thrown if the creation fails + */ + public PDFCanvas(PDDocument pdfDocument, int pageNo, PDFFontSettings fontSettings) throws IOException { + configureFonts(pdfDocument, fontSettings); preparePage(pdfDocument, pageNo); isContentStreamOwned = true; initGraphicsState(); @@ -193,14 +316,18 @@ public PDFCanvas(PDDocument pdfDocument, int pageNo) throws IOException { * (see {@link #close()}). Closing it will also reset the graphics state to the state before * creating this instance. *

+ *

+ * For text, the PDF standard font Helvetica will be used. It does not need to be embedded into + * the file and is available on all PDF viewers. But it is restricted to the WinANSI character set. + *

* * @param contentStream PDF page content stream */ public PDFCanvas(PDPageContentStream contentStream) { - setupFontMetrics(PDF_FONT); this.contentStream = contentStream; isContentStreamOwned = false; try { + configureFonts(null, PDFFontSettings.standardHelvetica()); initGraphicsState(); } catch (IOException e) { // should add throws IOException to constructor in next major release @@ -208,6 +335,26 @@ public PDFCanvas(PDPageContentStream contentStream) { } } + @SuppressWarnings("DataFlowIssue") + private void configureFonts(PDDocument doc, PDFFontSettings fontSettings) throws IOException { + setupFontMetrics(fontSettings.getFontFamily()); + + switch (fontSettings.getFontEmbedding()) { + case STANDARD_HELVETICA: + regularFont = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + boldFont = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + break; + case EMBEDDED_LIBERATION_SANS: + regularFont = PDType0Font.load(doc, PDFCanvas.class.getResource("/fonts/LiberationSans-Regular.ttf").openStream()); + boldFont = PDType0Font.load(doc, PDFCanvas.class.getResource("/fonts/LiberationSans-Bold.ttf").openStream()); + break; + case EMBEDDED_CUSTOM: + regularFont = PDType0Font.load(doc, Files.newInputStream(fontSettings.getRegularFontPath())); + boldFont = PDType0Font.load(doc, Files.newInputStream(fontSettings.getBoldFontPath())); + break; + } + } + private void preparePage(PDDocument doc, int pageNo) throws IOException { if (pageNo == NEW_PAGE_AT_END) { PDPage page = new PDPage(new PDRectangle((float) (210 * MM_TO_PT), (float) (297 * MM_TO_PT))); @@ -239,99 +386,6 @@ private void initGraphicsState() throws IOException { contentStream.saveGraphicsState(); } - private static void initPdfBox() { - // use reflection to load PDFBox elements differing between 2.0 and 3.0 - - // try PDFBox 3.0 first, then PDFBox 2.0 - String pdfBox3ErrorMessage = loadPdfBox3Functions(); - String pdfBox2ErrorMessage = null; - if (pdfBox3ErrorMessage != null) - pdfBox2ErrorMessage = loadPdfBox2Fonts(); - - if (pdfBox3ErrorMessage != null && pdfBox2ErrorMessage != null) { - throw new IllegalStateException(String.format("Unable to load PDFBox 3.0 or 2.0: %s, %s", - pdfBox2ErrorMessage, pdfBox3ErrorMessage)); - } - } - - @SuppressWarnings({"java:S1872", "java:S1192", "JavaReflectionMemberAccess"}) - private static String loadPdfBox3Functions() { - try { - Class standard14FontsClass = Class.forName("org.apache.pdfbox.pdmodel.font.Standard14Fonts"); - Class fontNameClass = null; - for (Class innerClass : standard14FontsClass.getDeclaredClasses()) { - if ("FontName".equals(innerClass.getSimpleName())) { - fontNameClass = innerClass; - break; - } - } - if (fontNameClass == null) - return "org.apache.pdfbox.pdmodel.font.Standard14Fonts$FontName not found"; - - Constructor pdType1FontClassConstructor = PDType1Font.class.getConstructor(fontNameClass); - PDType1Font helveticaFont = null; - PDType1Font helveticaBoldFont = null; - for (Object enumValue : fontNameClass.getEnumConstants()) { - if ("Helvetica".equals(enumValue.toString())) { - helveticaFont = (PDType1Font) pdType1FontClassConstructor.newInstance(enumValue); - } else if ("Helvetica-Bold".equals(enumValue.toString())) { - helveticaBoldFont = (PDType1Font) pdType1FontClassConstructor.newInstance(enumValue); - } - } - - if (helveticaFont == null) - return "org.apache.pdfbox.pdmodel.font.Standard14Fonts$FontName.HELVETICA not found"; - if (helveticaBoldFont == null) - return "org.apache.pdfbox.pdmodel.font.Standard14Fonts$FontName.HELVETICA_BOLD not found"; - - Class loaderClass = Class.forName("org.apache.pdfbox.Loader"); - Method loadPdfFromFile = loaderClass.getMethod("loadPDF", File.class); - Method loadPdfFromBytes = loaderClass.getMethod("loadPDF", byte[].class); - - regularFont = helveticaFont; - boldFont = helveticaBoldFont; - setupDocumentCreationLambdas(loadPdfFromFile, loadPdfFromBytes); - return null; - - } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | - InvocationTargetException e) { - return e.getMessage(); - } - } - - private static synchronized String loadPdfBox2Fonts() { - try { - regularFont = (PDType1Font) PDType1Font.class.getField("HELVETICA").get(null); - boldFont = (PDType1Font) PDType1Font.class.getField("HELVETICA_BOLD").get(null); - - Method loadPdfFromFile = PDDocument.class.getMethod("load", File.class); - Method loadPdfFromBytes = PDDocument.class.getMethod("load", byte[].class); - setupDocumentCreationLambdas(loadPdfFromFile, loadPdfFromBytes); - return null; - - } catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException e) { - return e.getMessage(); - } - } - - private static void setupDocumentCreationLambdas(Method loadPdfFromFile, Method loadPdfFromBytes) { - createDocumentFromPath = path -> { - try { - return (PDDocument) loadPdfFromFile.invoke(null, path.toFile()); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException("Failed to invoke org.apache.pdfbox.Loader.loadPDF(java.io.File)", e); - } - }; - createDocumentFromBytes = bytes -> { - try { - //noinspection PrimitiveArrayArgumentToVarargsMethod - return (PDDocument) loadPdfFromBytes.invoke(null, bytes); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException("Failed to invoke org.apache.pdfbox.Loader.loadPDF(byte[])", e); - } - }; - } - @Override public void setTransformation(double translateX, double translateY, double rotate, double scaleX, double scaleY) throws IOException { translateX *= MM_TO_PT; diff --git a/generator/src/main/java/net/codecrete/qrbill/canvas/PDFFontSettings.java b/generator/src/main/java/net/codecrete/qrbill/canvas/PDFFontSettings.java new file mode 100644 index 00000000..3a2ff22a --- /dev/null +++ b/generator/src/main/java/net/codecrete/qrbill/canvas/PDFFontSettings.java @@ -0,0 +1,126 @@ +package net.codecrete.qrbill.canvas; + +import java.nio.file.Path; + +/** + * Sets the font to use for a PDF canvas. + * + *

+ * To render a QR bill to a PDF document, a regular and a bold font face are needed. According to the Swiss + * QR bill specification, only the non-serif fonts Helvetica, Arial, Liberation Sans and Frutiger are allowed. + *

+ *

+ * There are three options for fonts: + *

+ * + */ +public class PDFFontSettings { + private final FontEmbedding fontEmbedding; + private final String fontFamily; + private final Path regularFontPath; + private final Path boldFontPath; + + private PDFFontSettings(FontEmbedding fontEmbedding, String fontFamily, Path regularFontPath, Path boldFontPath) { + this.fontEmbedding = fontEmbedding; + this.fontFamily = fontFamily; + this.regularFontPath = regularFontPath; + this.boldFontPath = boldFontPath; + } + + /** + * Creates a font settings instance for the standard Helvetica font. + * @return font settings instance + */ + public static PDFFontSettings standardHelvetica() { + return new PDFFontSettings(FontEmbedding.STANDARD_HELVETICA, "Helvetica", null, null); + } + + /** + * Creates a font settings instance for the Liberation Sans font. + * @return font settings instance + */ + public static PDFFontSettings embeddedLiberationSans() { + return new PDFFontSettings(FontEmbedding.EMBEDDED_LIBERATION_SANS, "Liberation Sans", null, null); + } + + /** + * Creates a font settings instance for a custom font. + *

+ * The font family name is used to determine what font information to use for calculating line breaks. + *

+ * @param fontFamily font family name + * @param regularFontPath path to the regular font face in TrueType format + * @param boldFontPath path to the bold font face in TrueType format + * @return font settings instance + */ + public static PDFFontSettings embeddedCustomFont(String fontFamily, Path regularFontPath, Path boldFontPath) { + return new PDFFontSettings(FontEmbedding.EMBEDDED_CUSTOM, fontFamily, regularFontPath, boldFontPath); + } + + /** + * Gets the font embedding. + * @return font embedding option + */ + public FontEmbedding getFontEmbedding() { + return fontEmbedding; + } + + /** + * Gets the font family name relevant for calculating line breaks. + * @return font family name + */ + public String getFontFamily() { + return fontFamily; + } + + /** + * Gets the path to the regular font face in TrueType format. + * @return font path + */ + public Path getRegularFontPath() { + return regularFontPath; + } + + /** + * Gets the path to the bold font face in TrueType format. + * @return font path + */ + public Path getBoldFontPath() { + return boldFontPath; + } + + /** + * Font embedding options. + */ + public enum FontEmbedding { + /** + * Standard Helvetica font, without embedding. + */ + STANDARD_HELVETICA, + /** + * Liberation Sans font included in this library, with embedding. + */ + EMBEDDED_LIBERATION_SANS, + /** + * Custom font provided by caller, with embedding. + */ + EMBEDDED_CUSTOM + } +} diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/Address.java b/generator/src/main/java/net/codecrete/qrbill/generator/Address.java index 9f233170..fb522b0a 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/Address.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/Address.java @@ -64,6 +64,13 @@ public enum Type { /** ISO country code */ private String countryCode; + /** + * Creates an empty address. + */ + public Address() { + // default constructor, for JavaDoc documentation + } + /** * Gets the address type. *

diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/Bill.java b/generator/src/main/java/net/codecrete/qrbill/generator/Bill.java index 1361efbb..2bc1fb53 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/Bill.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/Bill.java @@ -67,6 +67,13 @@ public enum Version { /** Bill format */ private BillFormat format = new BillFormat(); + /** + * Creates a new instance with default values for the format. + */ + public Bill() { + // default constructor, for JavaDoc documentation + } + /** * Gets the version of the QR bill standard. * diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/BillFormat.java b/generator/src/main/java/net/codecrete/qrbill/generator/BillFormat.java index 246cd02f..f03fc045 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/BillFormat.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/BillFormat.java @@ -44,6 +44,9 @@ public class BillFormat implements Serializable { /** Data separator for QR code data */ private QrDataSeparator qrDataSeparator = QrDataSeparator.LF; + /** Character set used for the QR bill data */ + private SPSCharacterSet characterSet = SPSCharacterSet.LATIN_1_SUBSET; + /** * Creates a new instance with default values */ @@ -66,6 +69,8 @@ public BillFormat(BillFormat format) { marginLeft = format.marginLeft; marginRight = format.marginRight; localCountryCode = format.localCountryCode; + qrDataSeparator = format.qrDataSeparator; + characterSet = format.characterSet; } /** @@ -343,6 +348,38 @@ public void setQrDataSeparator(QrDataSeparator qrDataSeparator) { this.qrDataSeparator = qrDataSeparator; } + /** + * Gets the character set used for the QR bill data. + *

+ * Defaults to {@link SPSCharacterSet#LATIN_1_SUBSET}. + *

+ *

+ * Until November 21, 2025, {@link SPSCharacterSet#LATIN_1_SUBSET} is the only value that will generate + * QR bills accepted by all banks. This will change by November 21, 2025. A release after that date + * wil change the default to {@link SPSCharacterSet#EXTENDED_LATIN}. + *

+ * @return the character set used for the QR bill data. + */ + public SPSCharacterSet getCharacterSet() { + return characterSet; + } + + /** + * Sets the character set used for the QR bill data. + *

+ * Defaults to {@link SPSCharacterSet#LATIN_1_SUBSET}. + *

+ *

+ * Until November 21, 2025, {@link SPSCharacterSet#LATIN_1_SUBSET} is the only value that will generate + * QR bills accepted by all banks. This will change by November 21, 2025. A release after that date + * wil change the default to {@link SPSCharacterSet#EXTENDED_LATIN}. + *

+ * @param characterSet the character set used for the QR bill data. + */ + public void setCharacterSet(SPSCharacterSet characterSet) { + this.characterSet = characterSet; + } + /** * {@inheritDoc} */ @@ -360,7 +397,8 @@ public boolean equals(Object o) { marginLeft == that.marginLeft && marginRight == that.marginRight && Objects.equals(localCountryCode, that.localCountryCode) && - qrDataSeparator == that.qrDataSeparator; + qrDataSeparator == that.qrDataSeparator && + characterSet == that.characterSet; } /** @@ -369,7 +407,7 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(outputSize, language, separatorType, fontFamily, graphicsFormat, resolution, marginLeft, - marginLeft, localCountryCode, qrDataSeparator); + marginLeft, localCountryCode, qrDataSeparator, characterSet); } /** @@ -388,6 +426,7 @@ public String toString() { ", marginRight=" + marginRight + ", localCountryCode='" + localCountryCode + '\'' + ", qrDataSeparator=" + qrDataSeparator + + ", characterSet=" + characterSet + '}'; } } diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/Payments.java b/generator/src/main/java/net/codecrete/qrbill/generator/Payments.java index d8c684f5..7752da45 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/Payments.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/Payments.java @@ -7,12 +7,8 @@ package net.codecrete.qrbill.generator; -import java.text.Normalizer; -import java.util.Arrays; import java.util.Locale; -import static java.lang.Character.UnicodeBlock.COMBINING_DIACRITICAL_MARKS; - /** * Field validations related to Swiss Payment standards */ @@ -23,12 +19,11 @@ private Payments() { } /** - * Returns a cleaned text valid for the Swiss Payment Standards 2018. + * Returns a cleaned text valid according to the specified character set. *

- * Unsupported characters (according to Swiss Payment Standards 2018, ch. 2.4.1 and appendix D) are - * replaced with supported characters, either with the same character without accent (e.g. A instead of Ă), - * with characters of similar meaning (e.g. TM instead of ™, ij instead of ij), with a space - * (for unsupported whitespace characters) or with a dot. + * Unsupported characters are replaced with supported characters, either with the same character without accent + * (e.g. A instead of Ă), with characters of similar meaning (e.g. TM instead of ™, ij instead of ij), + * with a space (for unsupported whitespace characters) or with a dot. *

*

* Some valid letters can be represented either with a single Unicode code point or with two code points, @@ -41,21 +36,19 @@ private Payments() { *

* * @param text string to clean + * @param characterSet character set specifying valid characters * @return valid text for Swiss payments */ - public static String cleanedText(String text) { - CleaningResult result = new CleaningResult(); - cleanText(text, false, result); - return result.cleanedString; + static String cleanedText(String text, SPSCharacterSet characterSet) { + return StringCleanup.cleanedText(text, characterSet); } /** - * Returns a cleaned and trimmed text valid for the Swiss Payment Standards 2018. + * Returns a cleaned and trimmed text valid according to the specified character set. *

- * Unsupported characters (according to Swiss Payment Standards 2018, ch. 2.4.1 and appendix D) are - * replaced with supported characters, either with the same character without accent (e.g. A instead of Ă), - * with characters of similar meaning (e.g. TM instead of ™, ij instead of ij), with a space - * (for unsupported whitespace characters) or with a dot. + * Unsupported characters are replaced with supported characters, either with the same character without accent + * (e.g. A instead of Ă), with characters of similar meaning (e.g. TM instead of ™, ij instead of ij), + * with a space (for unsupported whitespace characters) or with a dot. *

*

* Leading and trailing whitespace is removed. Multiple consecutive spaces are replaced with a single whitespace. @@ -71,207 +64,25 @@ public static String cleanedText(String text) { *

* * @param text string to clean + * @param characterSet character set specifying valid characters * @return valid text for Swiss payments */ - public static String cleanedAndTrimmedText(String text) { - CleaningResult result = new CleaningResult(); - cleanText(text, true, result); - return result.cleanedString; + static String cleanedAndTrimmedText(String text, SPSCharacterSet characterSet) { + return StringCleanup.cleanedAndTrimmedText(text, characterSet); } /** - * Indicates if the text consists only of characters allowed in Swiss payments. - *

- * The valid character set is defined in Swiss Payment Standards 2018, ch. 2.4.1 and appendix D - *

+ * Indicates if the text consists only of characters allowed in the specified character set. *

* This method does not attempt to deal with accents and umlauts built from two code points. It will * return {@code false} if the text contains such characters. *

- * @param text text to check + * @param text text to check, possibly {@code null} + * @param characterSet character set specifying valid characters * @return {@code true} if the text is valid, {@code false} otherwise */ - public static boolean isValidText(String text) { - int len = text.length(); - for (int i = 0; i < len; i++) { - if (!isValidCharacter(text.charAt(i))) - return false; - } - return true; - } - - /** - * Returns if the character is a valid for a Swiss payment. - *

- * Valid characters are defined in Swiss Payment Standards 2022, - * Customer Credit Transfer Initiation (pain.001), ch. 3.1 and appendix C. - *

- * - * @param ch character to test - * @return {@code true} if it is valid, {@code false} otherwise - */ - @SuppressWarnings({"java:S1126", "RedundantIfStatement"}) - public static boolean isValidCharacter(char ch) { - // Basic Latin - if (ch >= 0x0020 && ch <= 0x007E) - return true; - - // Latin-1 Supplement and Latin Extended-A - if (ch >= 0x00A0 && ch <= 0x017F) - return true; - - // Additional characters - if (ch >= 0x0218 && ch <= 0x021B) - return true; - - if (ch == 0x20AC) - return true; - - return false; - } - - /** - * Returns if the code point is a valid for a Swiss payment. - *

- * Valid characters are defined in Swiss Payment Standards 2022, - * Customer Credit Transfer Initiation (pain.001), ch. 3.1 and appendix C. - *

- * - * @param codePoint code point to test - * @return {@code true} if it is valid, {@code false} otherwise - */ - public static boolean isValidCodePoint(int codePoint) { - return codePoint <= 0xFFFF && isValidCharacter((char) codePoint); - } - - static void cleanText(String text, boolean trimWhitespace, CleaningResult result) { - result.cleanedString = null; - result.replacedUnsupportedChars = false; - - if (text == null) - return; - - // step 1: quick test for valid text - boolean isValidString = isValidText(text); - - if (!isValidString) { - // step 2: normalize string (to deal with accents built from two code points) and test again - if (!Normalizer.isNormalized(text, Normalizer.Form.NFC)) { - text = Normalizer.normalize(text, Normalizer.Form.NFC); - isValidString = isValidText(text); - } - - // step 3: replace invalid characters - if (!isValidString) { - text = replaceInvalidCharacters(text); - result.replacedUnsupportedChars = true; - } - } - - if (trimWhitespace) - text = Strings.spacesCleaned(text); - if (text.isEmpty()) - text = null; - - result.cleanedString = text; - } - - private static String replaceInvalidCharacters(String text) { - StringBuilder sb = new StringBuilder(); - int len = text.length(); - int offset = 0; - boolean inFallback = false; - while (offset < len) { - final int codePoint = text.codePointAt(offset); - - if (isValidCodePoint(codePoint)) { - // valid code point - sb.append((char) codePoint); - inFallback = false; - } else if (replaceCodePoint(codePoint, sb)) { - // good replacement - inFallback = false; - } else if (!inFallback) { - // no replacement found and not consecutive fallback - sb.append('.'); - inFallback = true; - } - - offset += Character.charCount(codePoint); - } - return sb.toString(); - } - - // sorted by code point (BMP only, no surrogates) - private static final char[] ACCENTED_CHARS = - "ƠơƯưǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǞǟǠǡǢǣǦǧǨǩǪǫǬǭǰǴǵǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȞȟȦȧȨȩȪȫȬȭȮȯȰȱȲȳ΅ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿẀẁẂẃẄẅẆẇẈẉẊẋẌẍẎẏẐẑẒẓẔẕẖẗẘẙẛẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹ῁῭΅Å≠≮≯" - .toCharArray(); - - private static final char[] REPLACEMENT_CHARS = - "OoUuAaIiOoUuUuUuUuUuAaAaÆæGgKkOoOojGgNnAaÆæØøAaAaEeEeIiIiOoOoRrRrUuUuHhAaEeOoOoOoOoYy¨AaBbBbBbCcDdDdDdDdDdEeEeEeEeEeFfGgHhHhHhHhHhIiIiKkKkKkLlLlLlLlMmMmMmNnNnNnNnOoOoOoOoPpPpRrRrRrRrSsSsSsSsSsTtTtTtTtUuUuUuUuUuVvVvWwWwWwWwWwXxXxYyZzZzZzhtwyſAaAaAaAaAaAaAaAaAaAaAaAaEeEeEeEeEeEeEeEeIiIiOoOoOoOoOoOoOoOoOoOoOoOoUuUuUuUuUuUuUuYyYyYyYy¨¨¨A=<>" - .toCharArray(); - - private static boolean replaceCodePoint(int codePoint, StringBuilder sb) { - // whitespace is replaced with a space - if (Character.isWhitespace(codePoint)) { - sb.append(' '); - return true; - } - - // check if code point is a valid character without accent (precomputed case) - if (codePoint <= 0xFFFF) { - int pos = Arrays.binarySearch(ACCENTED_CHARS, (char) codePoint); - if (pos >= 0) { - sb.append(REPLACEMENT_CHARS[pos]); - return true; - } - } - - // check if code point is a valid character with accent (using canonical decomposition) - String codePointString = new String(new int[] { codePoint }, 0, 1); - String decomposed1 = Normalizer.normalize(codePointString, Normalizer.Form.NFD); - int firstCodePoint = decomposed1.codePointAt(0); - if (decomposed1.length() > 1 && isValidCodePoint(firstCodePoint)) { - int secondCodePoint = decomposed1.codePointAt(1); - if (Character.UnicodeBlock.of(secondCodePoint) != COMBINING_DIACRITICAL_MARKS) { - sb.append((char) firstCodePoint); - return true; - } - } - - // check if compatibility decomposition results in valid substring - String decomposed2 = decomposedString(codePointString); - if (decomposed2 != null) { - sb.append(decomposed2); - return true; - } - - // no good replacement - return false; - } - - private static String decomposedString(String codePointString) { - String decomposedString = Normalizer.normalize(codePointString, Normalizer.Form.NFKD); - int len = decomposedString.length(); - for (int i = 0; i < len; i += 1) { - if (!isValidCharacter(decomposedString.charAt(i))) - return null; - } - return decomposedString; - } - - /** - * Result of cleaning a string value - */ - static class CleaningResult { - /** - * Cleaned string - */ - String cleanedString; - /** - * Flag indicating that unsupported characters have been replaced - */ - boolean replacedUnsupportedChars; + public static boolean isValidText(String text, SPSCharacterSet characterSet) { + return StringCleanup.isValidText(text, characterSet); } /** diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/QRBill.java b/generator/src/main/java/net/codecrete/qrbill/generator/QRBill.java index 04050996..82b6b261 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/QRBill.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/QRBill.java @@ -261,7 +261,7 @@ public static String encodeQrCodeText(Bill bill) { * Decodes the text embedded in the QR code and fills it into a {@link Bill} * data structure. *

- * A subset of the validations related to embedded QR code text is run. It the + * A subset of the validations related to embedded QR code text is run. If the * validation fails, a {@link QRBillValidationError} is thrown, which contains * the validation result. See the error messages marked with a dagger in * + * The character set defines the allowed characters in the various payment fields. + *

+ */ +public enum SPSCharacterSet { + /** + * Restrictive character set from the original Swiss Payment Standard and original QR bill specification. + *

+ * Valid characters consist of a subset of the printable Latin-1 characters in the Unicode blocks Basic Latin + * and Latin-1 Supplement. + *

+ */ + LATIN_1_SUBSET(SPSCharacterSet::isInLatin1Subset), + + /** + * Extended Latin character set. + *

+ * Valid characters are all printable characters from the Unicode blocks Basic Latin (Unicode codePoints + * U+0020 to U+007E), Latin-1 Supplement (Unicode codePoints U+00A0 to U+00FF) and Latin Extended A + * (Unicode codePoints U+0100 to U+017F) plus a few additional characters (such as the Euro sign). + *

+ *

+ * This character set has been introduced with SPS 2022 (November 18, 2022) but may not be used in QR bills + * until November 21, 2025 when all banks are ready to accept messages with this character set. + *

+ */ + EXTENDED_LATIN(SPSCharacterSet::isInExtendedLatin), + + /** + * Full Unicode character set. + *

+ * This character set may be used when decoding the QR code text. It is not suitable for generating QR bills + * or payment messages in general, and it is not covered by the Swiss Payment Standard. + *

+ */ + FULL_UNICODE(SPSCharacterSet::isInUnicode); + + private final Predicate containsCharacter; + + SPSCharacterSet(Predicate containsCharacter) { + this.containsCharacter = containsCharacter; + } + + /** + * Returns if this characters set contains the specified character. + * @param ch character + * @return {@code true} if the character is in this character set, {@code false} otherwise + */ + public boolean contains(char ch) { + return containsCharacter.test((int)ch); + } + + /** + * Returns if this characters set contains the specified Unicode code point. + * @param codePoint Unicode code point + * @return {@code true} if the code point is in this character set, {@code false} otherwise + */ + public boolean contains(int codePoint) { + return containsCharacter.test(codePoint); + } + + @SuppressWarnings("java:S3776") + private static boolean isInLatin1Subset(int codePoint) { + if (codePoint < 0x20) + return false; + if (codePoint == 0x5e) + return false; + if (codePoint <= 0x7e) + return true; + if (codePoint == 0xa3 || codePoint == 0xb4) + return true; + if (codePoint < 0xc0 || codePoint > 0xfd) + return false; + if (codePoint == 0xc3 || codePoint == 0xc5 || codePoint == 0xc6) + return false; + if (codePoint == 0xd0 || codePoint == 0xd5 || codePoint == 0xd7 || codePoint == 0xd8) + return false; + if (codePoint == 0xdd || codePoint == 0xde) + return false; + if (codePoint == 0xe3 || codePoint == 0xe5 || codePoint == 0xe6) + return false; + return codePoint != 0xf0 && codePoint != 0xf5 && codePoint != 0xf8; + } + + @SuppressWarnings({"java:S1126", "RedundantIfStatement"}) + private static boolean isInExtendedLatin(int codePoint) { + // Basic Latin + if (codePoint >= 0x0020 && codePoint <= 0x007E) + return true; + + // Latin-1 Supplement and Latin Extended-A + if (codePoint >= 0x00A0 && codePoint <= 0x017F) + return true; + + // Additional characters + if (codePoint >= 0x0218 && codePoint <= 0x021B) + return true; + + if (codePoint == 0x20AC) + return true; + + return false; + } + + @SuppressWarnings("java:S3400") + private static boolean isInUnicode(int codePoint) { + return true; + } +} diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/StringCleanup.java b/generator/src/main/java/net/codecrete/qrbill/generator/StringCleanup.java new file mode 100644 index 00000000..3794bce8 --- /dev/null +++ b/generator/src/main/java/net/codecrete/qrbill/generator/StringCleanup.java @@ -0,0 +1,321 @@ +// +// Swiss QR Bill Generator +// Copyright (c) 2024 Manuel Bleichenbacher +// Licensed under MIT License +// https://opensource.org/licenses/MIT +// + +package net.codecrete.qrbill.generator; + +import java.text.Normalizer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.Character.UnicodeBlock.COMBINING_DIACRITICAL_MARKS; + +class StringCleanup { + private StringCleanup() { + } + + // sorted by code point (BMP only, no surrogates, resulting in a single character valid for Latin-1 subset) + private static final char[] QUICK_REPLACEMENTS_FROM = + "¨¯¸ÃÅÕÝãåõÿĀāĂ㥹ĆćĈĉĊċČčĎďĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĨĩĪīĬĭĮįİĴĵĶķĹĺĻļĽľŃńŅņŇňŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžƠơƯưǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǞǟǠǡǦǧǨǩǪǫǬǭǰǴǵǸǹǺǻȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȞȟȦȧȨȩȪȫȬȭȮȯȰȱȲȳ˘˙˚˛˜˝ͺ΄΅ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿẀẁẂẃẄẅẆẇẈẉẊẋẌẍẎẏẐẑẒẓẔẕẖẗẘẙẛẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹ᾽᾿῀῁῍῎῏῝῞῟῭΅´῾‗‾Å≠≮≯﹉﹊﹋﹌ ̄" + .toCharArray(); + private static final char[] QUICK_REPLACEMENTS_TO = + " AAOYaaoyAaAaAaCcCcCcCcDdEeEeEeEeEeGgGgGgGgHhIiIiIiIiIJjKkLlLlLlNnNnNnOoOoOoRrRrRrSsSsSsSsTtTtUuUuUuUuUuUuWwYyYZzZzZzOoUuAaIiOoUuUuUuUuUuAaAaGgKkOoOojGgNnAaAaAaEeEeIiIiOoOoRrRrUuUuSsTtHhAaEeOoOoOoOoYy AaBbBbBbCcDdDdDdDdDdEeEeEeEeEeFfGgHhHhHhHhHhIiIiKkKkKkLlLlLlLlMmMmMmNnNnNnNnOoOoOoOoPpPpRrRrRrRrSsSsSsSsSsTtTtTtTtUuUuUuUuUuVvVvWwWwWwWwWwXxXxYyZzZzZzhtwysAaAaAaAaAaAaAaAaAaAaAaAaEeEeEeEeEeEeEeEeIiIiOoOoOoOoOoOoOoOoOoOoOoOoUuUuUuUuUuUuUuYyYyYyYy A=<> " + .toCharArray(); + + // additional replacements, not covered by Unicode decomposition + // (from a single character to possibly multiple characters) + private static final String[] ADDITIONAL_REPLACEMENT_PAIRS = new String[] { + "Œ", "OE", + "œ", "oe", + "Æ", "AE", + "æ", "ae", + "Ǣ", "AE", + "ǣ", "ae", + "Ǽ", "AE", + "ǽ", "ae", + "Ǿ", "O", + "ǿ", "o", + "ȸ", "db", + "ȹ", "qp", + "Ø", "O", + "ø", "o", + "€", "E", + "^", ".", + "¡", "! ", + "¢", "c", + "¤", " ", + "¥", "Y", + "¦", "/", + "§", "S", + "©", "(c)", + "«", "<<", + "¬", "-", + "\u00AD", "", // soft hyphen + "®", "(r)", + "°", "o", + "±", "+-", + "µ", "u", + "¶", "P", + "·", "-", + "»", ">>", + "¿", "? ", + "Ð", "D", + "×", "x", + "Þ", "TH", + "ð", "d", + "þ", "th", + "Đ", "D", + "đ", "d", + "Ħ", "H", + "ħ", "h", + "ı", "i", + "ĸ", "k", + "Ŀ", "L", + "ŀ", "l", + "Ł", "L", + "ł", "l", + "ʼn", "n", + "Ŋ", "N", + "ŋ", "n", + "Ŧ", "T", + "ŧ", "t", + "⁄", "/", // fraction slash + }; + + // additional replacements, not covered by Unicode decomposition + private static final Map additionalReplacements = new HashMap<>(); + + static { + for (int i = 0; i < ADDITIONAL_REPLACEMENT_PAIRS.length; i += 2) { + additionalReplacements.put(ADDITIONAL_REPLACEMENT_PAIRS[i].codePointAt(0), ADDITIONAL_REPLACEMENT_PAIRS[i + 1]); + } + } + + /** + * Returns a cleaned text valid according to the specified character set. + *

+ * Unsupported characters are replaced with supported characters, either with the same character without accent + * (e.g. A instead of Ă), with characters of similar meaning (e.g. TM instead of ™, ij instead of ij), + * with a space (for unsupported whitespace characters) or with a dot. + *

+ *

+ * Some valid letters can be represented either with a single Unicode code point or with two code points, + * e.g. the letter A with umlaut can be represented either with the single code point U+00C4 (latin capital + * letter A with diaeresis) or with the two code points U+0041 U+0308 (latin capital letter A, + * combining diaeresis). This will be recognized and converted to the valid single code point form. + *

+ *

+ * If {@code text} is {@code null} or the resulting string would be empty, {@code null} is returned. + *

+ * + * @param text string to clean + * @param characterSet character set specifying valid characters + * @return valid text for Swiss payments + */ + static String cleanedText(String text, SPSCharacterSet characterSet) { + CleaningResult result = new CleaningResult(); + cleanText(text, characterSet, false, result); + return result.cleanedString; + } + + /** + * Returns a cleaned and trimmed text valid according to the specified character set. + *

+ * Unsupported characters are replaced with supported characters, either with the same character without accent + * (e.g. A instead of Ă), with characters of similar meaning (e.g. TM instead of ™, ij instead of ij), + * with a space (for unsupported whitespace characters) or with a dot. + *

+ *

+ * Leading and trailing whitespace is removed. Multiple consecutive spaces are replaced with a single whitespace. + *

+ *

+ * Some valid letters can be represented either with a single Unicode code point or with two code points, + * e.g. the letter A with umlaut can be represented either with the single code point U+00C4 (latin capital + * letter A with diaeresis) or with the two code points U+0041 U+0308 (latin capital letter A, + * combining diaeresis). This will be recognized and converted to the valid single code point form. + *

+ *

+ * If {@code text} is {@code null} or the resulting string would be empty, {@code null} is returned. + *

+ * + * @param text string to clean + * @param characterSet character set specifying valid characters + * @return valid text for Swiss payments + */ + static String cleanedAndTrimmedText(String text, SPSCharacterSet characterSet) { + CleaningResult result = new CleaningResult(); + cleanText(text, characterSet, true, result); + return result.cleanedString; + } + + /** + * Indicates if the text consists only of characters allowed in the specified character set. + *

+ * This method does not attempt to deal with accents and umlauts built from two code points. It will + * return {@code false} if the text contains such characters. + *

+ * @param text text to check, possibly {@code null} + * @param characterSet character set specifying valid characters + * @return {@code true} if the text is valid, {@code false} otherwise + */ + static boolean isValidText(String text, SPSCharacterSet characterSet) { + if (text == null) + return true; + + int len = text.length(); + for (int i = 0; i < len; i++) { + if (!characterSet.contains(text.charAt(i))) + return false; + } + return true; + } + + static void cleanText(String text, SPSCharacterSet characterSet, boolean trimWhitespace, CleaningResult result) { + result.cleanedString = null; + result.replacedUnsupportedChars = false; + + if (text == null) + return; + + // step 1: quick test for valid text + boolean isValidString = isValidText(text, characterSet); + + if (!isValidString) { + // step 2: normalize string (to deal with accents built from two code points) and test again + if (!Normalizer.isNormalized(text, Normalizer.Form.NFC)) { + text = Normalizer.normalize(text, Normalizer.Form.NFC); + isValidString = isValidText(text, characterSet); + } + + // step 3: replace characters + if (!isValidString) { + text = replaceCharacters(text, characterSet); + result.replacedUnsupportedChars = true; + } + } + + if (trimWhitespace) + text = Strings.spacesCleaned(text); + if (text.isEmpty()) + text = null; + + result.cleanedString = text; + } + + private static String replaceCharacters(String text, SPSCharacterSet characterSet) { + StringBuilder sb = new StringBuilder(); + int len = text.length(); + int offset = 0; + boolean inFallback = false; + while (offset < len) { + final int codePoint = text.codePointAt(offset); + + if (characterSet.contains(codePoint)) { + // valid code point + sb.appendCodePoint(codePoint); + inFallback = false; + } else if (replaceCodePoint(codePoint, characterSet, sb)) { + // good replacement + inFallback = false; + } else if (!inFallback) { + // no replacement found and not consecutive fallback + sb.append('.'); + inFallback = true; + } + + offset += Character.charCount(codePoint); + } + return sb.toString(); + } + + private static boolean replaceCodePoint(int codePoint, SPSCharacterSet characterSet, StringBuilder sb) { + // whitespace is replaced with a space + if (Character.isWhitespace(codePoint)) { + sb.append(' '); + return true; + } + + // check if there is a quick replacement (precomputed case) + if (codePoint <= 0xFFFF) { + int pos = Arrays.binarySearch(QUICK_REPLACEMENTS_FROM, (char) codePoint); + if (pos >= 0) { + sb.append(QUICK_REPLACEMENTS_TO[pos]); + return true; + } + } + + String codePointString = new String(new int[] { codePoint }, 0, 1); + + // check if canonical decomposition yields a valid string + String canoncial = decomposedString(codePointString, characterSet, Normalizer.Form.NFD); + if (canoncial != null) { + sb.append(canoncial); + return true; + } + + // check if compatibility decomposition yields a valid string + String compatibility = decomposedString(codePointString, characterSet, Normalizer.Form.NFKD); + if (compatibility != null) { + sb.append(compatibility); + return true; + } + + // check for additional replacements + String replacement = additionalReplacements.get(codePoint); + if (replacement != null) { + sb.append(replacement); + return true; + } + + // no good replacement + return false; + } + + private static String decomposedString(String codePointString, SPSCharacterSet characterSet, Normalizer.Form form) { + // decompose string + String decomposedString = Normalizer.normalize(codePointString, form); + + boolean hasFractionSlash = false; + + // check for valid characters + int len = decomposedString.length(); + for (int i = 0; i < len; i += 1) { + if (!characterSet.contains(decomposedString.charAt(i))) { + + // check if decomposition consists one or more valid characters + // and combining diacritical mark at the end + if (i == len - 1 && Character.UnicodeBlock.of(decomposedString.charAt(i)) == COMBINING_DIACRITICAL_MARKS) { + return decomposedString.substring(0, i); + } + + // check for fraction slash (U+2044) + if (decomposedString.charAt(i) == '⁄') { + hasFractionSlash = true; + } else { + // return null if no valid decomposition is available + return null; + } + } + } + + return hasFractionSlash ? decomposedString.replace('⁄', '/') : decomposedString; + } + + /** + * Result of cleaning a string value + */ + static class CleaningResult { + /** + * Cleaned string + */ + String cleanedString; + /** + * Flag indicating that unsupported characters have been replaced + */ + boolean replacedUnsupportedChars; + } +} diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/SwicoBillInformation.java b/generator/src/main/java/net/codecrete/qrbill/generator/SwicoBillInformation.java index e75a0123..80836972 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/SwicoBillInformation.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/SwicoBillInformation.java @@ -37,6 +37,13 @@ public class SwicoBillInformation { private List vatImportTaxes; private List paymentConditions; + /** + * Creates a new instance with {@code null} values. + */ + public SwicoBillInformation() { + // default constructor, for JavaDoc documentation + } + /** * Gets the invoice number. * diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/ValidationResult.java b/generator/src/main/java/net/codecrete/qrbill/generator/ValidationResult.java index f10cc7bc..fccd7361 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/ValidationResult.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/ValidationResult.java @@ -24,6 +24,13 @@ public class ValidationResult implements Serializable { /** Cleaned bill data */ private Bill cleanedBill; + /** + * Creates a new validation result instance + */ + public ValidationResult() { + // default constructor, for JavaDoc documentation + } + /** * Gets the list of validation messages * diff --git a/generator/src/main/java/net/codecrete/qrbill/generator/Validator.java b/generator/src/main/java/net/codecrete/qrbill/generator/Validator.java index 4694aa4f..d2370d30 100644 --- a/generator/src/main/java/net/codecrete/qrbill/generator/Validator.java +++ b/generator/src/main/java/net/codecrete/qrbill/generator/Validator.java @@ -6,7 +6,7 @@ // package net.codecrete.qrbill.generator; -import net.codecrete.qrbill.generator.Payments.CleaningResult; +import net.codecrete.qrbill.generator.StringCleanup.CleaningResult; import net.codecrete.qrbill.generator.ValidationMessage.Type; import java.math.BigDecimal; @@ -403,7 +403,7 @@ private String clippedValue(String value, int maxLength, String fieldRoot, Strin private String cleanedValue(String value, String fieldRoot, String subfield) { CleaningResult result = new CleaningResult(); - Payments.cleanText(value, true, result); + StringCleanup.cleanText(value, billOut.getFormat().getCharacterSet(), true, result); if (result.replacedUnsupportedChars) validationResult.addMessage(Type.WARNING, fieldRoot + subfield, ValidationConstants.KEY_REPLACED_UNSUPPORTED_CHARACTERS); return result.cleanedString; @@ -411,7 +411,7 @@ private String cleanedValue(String value, String fieldRoot, String subfield) { private String cleanedValue(String value, String fieldName) { CleaningResult result = new CleaningResult(); - Payments.cleanText(value, true, result); + StringCleanup.cleanText(value, billOut.getFormat().getCharacterSet(), true, result); if (result.replacedUnsupportedChars) validationResult.addMessage(Type.WARNING, fieldName, ValidationConstants.KEY_REPLACED_UNSUPPORTED_CHARACTERS); return result.cleanedString; diff --git a/generator/src/main/resources/fonts/LICENSE b/generator/src/main/resources/fonts/LICENSE new file mode 100644 index 00000000..aba73e8a --- /dev/null +++ b/generator/src/main/resources/fonts/LICENSE @@ -0,0 +1,102 @@ +Digitized data copyright (c) 2010 Google Corporation + with Reserved Font Arimo, Tinos and Cousine. +Copyright (c) 2012 Red Hat, Inc. + with Reserved Font Name Liberation. + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +PREAMBLE The goals of the Open Font License (OFL) are to stimulate +worldwide development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to provide +a free and open framework in which fonts may be shared and improved in +partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. +The fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + + + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. +This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components +as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting ? in part or in whole ? +any of the components of the Original Version, by changing formats or +by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer +or other person who contributed to the Font Software. + + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components,in + Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the + corresponding Copyright Holder. This restriction only applies to the + primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + +5) The Font Software, modified or unmodified, in part or in whole, must + be distributed entirely under this license, and must not be distributed + under any other license. The requirement for fonts to remain under + this license does not apply to any document created using the Font + Software. + + + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + + + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. + diff --git a/generator/src/main/resources/fonts/LiberationSans-Bold.ttf b/generator/src/main/resources/fonts/LiberationSans-Bold.ttf new file mode 100644 index 00000000..dc5d57f1 Binary files /dev/null and b/generator/src/main/resources/fonts/LiberationSans-Bold.ttf differ diff --git a/generator/src/main/resources/fonts/LiberationSans-Regular.ttf b/generator/src/main/resources/fonts/LiberationSans-Regular.ttf new file mode 100644 index 00000000..e6339859 Binary files /dev/null and b/generator/src/main/resources/fonts/LiberationSans-Regular.ttf differ diff --git a/generator/src/test/java/net/codecrete/qrbill/canvas/PdfCanvasTest.java b/generator/src/test/java/net/codecrete/qrbill/canvas/PdfCanvasTest.java index dea3a91a..3992d644 100644 --- a/generator/src/test/java/net/codecrete/qrbill/canvas/PdfCanvasTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/canvas/PdfCanvasTest.java @@ -16,6 +16,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.util.Matrix; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -64,7 +65,7 @@ void addPageToOpenPdfDocument() throws IOException { PDPage page = new PDPage(new PDRectangle(210 * MM_TO_PT, 297 * MM_TO_PT)); document.addPage(page); try (PDPageContentStream stream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.OVERWRITE, true)) { - stream.setFont(PDType1Font.HELVETICA_BOLD, 18); + stream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 18); stream.beginText(); stream.newLineAtOffset(20 * MM_TO_PT, 220 * MM_TO_PT); stream.showText("Swiss QR Bill"); @@ -100,7 +101,7 @@ void addPageToOpenContentStream() throws IOException { stream.restoreGraphicsState(); - stream.setFont(PDType1Font.HELVETICA_BOLD, 18); + stream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 18); stream.beginText(); stream.newLineAtOffset(20 * MM_TO_PT, 220 * MM_TO_PT); stream.showText("Swiss QR Bill"); diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/A4BillTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/A4BillTest.java index c66286e5..80f8035c 100644 --- a/generator/src/test/java/net/codecrete/qrbill/generator/A4BillTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/generator/A4BillTest.java @@ -86,6 +86,18 @@ void createA4PDFBill6() { generateAndCompareBill(SampleData.getExample6(), GraphicsFormat.PDF, "a4bill_ex6.pdf"); } + @Test + void createA4PDFBill8a() { + generateAndCompareBill(SampleData.getExample8(), GraphicsFormat.PDF, "a4bill_ex8a.pdf"); + } + + @Test + void createA4PDFBill8b() { + Bill bill = SampleData.getExample8(); + bill.getFormat().setCharacterSet(SPSCharacterSet.EXTENDED_LATIN); + generateAndCompareBill(bill, GraphicsFormat.PDF, "a4bill_ex8b.pdf"); + } + private void generateAndCompareBill(Bill bill, GraphicsFormat graphicsFormat, String expectedFileName) { bill.getFormat().setOutputSize(OutputSize.A4_PORTRAIT_SHEET); diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/BillFormatTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/BillFormatTest.java index 6ad5fdf6..d15daf75 100644 --- a/generator/src/test/java/net/codecrete/qrbill/generator/BillFormatTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/generator/BillFormatTest.java @@ -25,6 +25,8 @@ void defaultValueTest() { assertEquals(144, format.getResolution()); assertEquals(5.0, format.getMarginLeft()); assertEquals(5.0, format.getMarginRight()); + assertEquals(QrDataSeparator.LF, format.getQrDataSeparator()); + assertEquals(SPSCharacterSet.LATIN_1_SUBSET, format.getCharacterSet()); } @Test @@ -32,13 +34,15 @@ void hashCodeTest() { BillFormat format1 = new BillFormat(); BillFormat format2 = new BillFormat(); assertEquals(format1.hashCode(), format2.hashCode()); + format1.setCharacterSet(SPSCharacterSet.FULL_UNICODE); + assertNotEquals(format1.hashCode(), format2.hashCode()); } @Test void toStringTest() { BillFormat format = new BillFormat(); String text = format.toString(); - assertEquals("BillFormat{outputSize=QR_BILL_ONLY, language=EN, separatorType=DASHED_LINE_WITH_SCISSORS, fontFamily='Helvetica,Arial,\"Liberation Sans\"', graphicsFormat=SVG, resolution=144, marginLeft=5.0, marginRight=5.0, localCountryCode='CH', qrDataSeparator=LF}", text); + assertEquals("BillFormat{outputSize=QR_BILL_ONLY, language=EN, separatorType=DASHED_LINE_WITH_SCISSORS, fontFamily='Helvetica,Arial,\"Liberation Sans\"', graphicsFormat=SVG, resolution=144, marginLeft=5.0, marginRight=5.0, localCountryCode='CH', qrDataSeparator=LF, characterSet=LATIN_1_SUBSET}", text); } @Test diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/BillTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/BillTest.java index d0eca4a8..f2c10ae1 100644 --- a/generator/src/test/java/net/codecrete/qrbill/generator/BillTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/generator/BillTest.java @@ -183,7 +183,7 @@ void testHashCode() { void testToString() { Bill bill = createBill(); String text = bill.toString(); - assertEquals("Bill{version=V2_0, amount=100.30, currency='CHF', account='CH12343345345', creditor=Address{type=STRUCTURED, name='Vision Consult GmbH', addressLine1='null', addressLine2='null', street='Hintergasse', houseNo='7b', postalCode='8400', town='Winterthur', countryCode='CH'}, referenceType='NON', reference='null', debtor=Address{type=STRUCTURED, name='Vision Consult GmbH', addressLine1='null', addressLine2='null', street='Hintergasse', houseNo='7b', postalCode='8400', town='Winterthur', countryCode='CH'}, unstructuredMessage='null', billInformation='null', alternativeSchemes=null, format=BillFormat{outputSize=QR_BILL_ONLY, language=EN, separatorType=DASHED_LINE_WITH_SCISSORS, fontFamily='Helvetica,Arial,\"Liberation Sans\"', graphicsFormat=SVG, resolution=144, marginLeft=5.0, marginRight=5.0, localCountryCode='CH', qrDataSeparator=LF}}", text); + assertEquals("Bill{version=V2_0, amount=100.30, currency='CHF', account='CH12343345345', creditor=Address{type=STRUCTURED, name='Vision Consult GmbH', addressLine1='null', addressLine2='null', street='Hintergasse', houseNo='7b', postalCode='8400', town='Winterthur', countryCode='CH'}, referenceType='NON', reference='null', debtor=Address{type=STRUCTURED, name='Vision Consult GmbH', addressLine1='null', addressLine2='null', street='Hintergasse', houseNo='7b', postalCode='8400', town='Winterthur', countryCode='CH'}, unstructuredMessage='null', billInformation='null', alternativeSchemes=null, format=BillFormat{outputSize=QR_BILL_ONLY, language=EN, separatorType=DASHED_LINE_WITH_SCISSORS, fontFamily='Helvetica,Arial,\"Liberation Sans\"', graphicsFormat=SVG, resolution=144, marginLeft=5.0, marginRight=5.0, localCountryCode='CH', qrDataSeparator=LF, characterSet=LATIN_1_SUBSET}}", text); } private Address createAddress() { diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/CharacterSetTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/CharacterSetTest.java index 7bd3d013..1fb164d4 100644 --- a/generator/src/test/java/net/codecrete/qrbill/generator/CharacterSetTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/generator/CharacterSetTest.java @@ -39,6 +39,32 @@ void unstructuredMessageReplacement() { assertSingleWarningMessage(ValidationConstants.FIELD_UNSTRUCTURED_MESSAGE, ValidationConstants.KEY_REPLACED_UNSUPPORTED_CHARACTERS); } + @Test + void unstructuredMessageReplacementLatin1Subset() { + bill = SampleData.getExample8(); + validate(); + + assertFalse(result.hasErrors()); + assertTrue(result.hasWarnings()); + assertTrue(result.hasMessages()); + assertEquals(3, result.getValidationMessages().size()); + for (ValidationMessage message : result.getValidationMessages()) { + assertEquals(ValidationConstants.KEY_REPLACED_UNSUPPORTED_CHARACTERS, message.getMessageKey()); + } + + assertEquals("Facture 48390, E10 de réduction", validatedBill.getUnstructuredMessage()); + assertEquals("Bugra Çavdarli", validatedBill.getCreditor().getName()); + assertEquals("L'OEil de Boeuf", validatedBill.getDebtor().getName()); + } + + @Test + void unstructuredMessageReplacementExtendedLatin() { + bill = SampleData.getExample8(); + bill.getFormat().setCharacterSet(SPSCharacterSet.EXTENDED_LATIN); + validate(); + assertNoMessages(); + } + @Test void billInfoReplacement() { bill = SampleData.getExample1(); diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/EncodedTextTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/EncodedTextTest.java index dcccba46..57a9e097 100644 --- a/generator/src/test/java/net/codecrete/qrbill/generator/EncodedTextTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/generator/EncodedTextTest.java @@ -29,7 +29,10 @@ class EncodedTextTest { void createText(int sample, QrDataSeparator separator) { Bill bill = SampleQrCodeText.getBillData(sample); bill.getFormat().setQrDataSeparator(separator); - assertEquals(SampleQrCodeText.getQrCodeText(sample), QRBill.encodeQrCodeText(bill)); + assertEquals( + SampleQrCodeText.getQrCodeText(sample, separator == QrDataSeparator.CR_LF ? "\r\n" : "\n"), + QRBill.encodeQrCodeText(bill) + ); } @Test diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/PaymentsCharacterSetTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/PaymentsCharacterSetTest.java index cfd428f3..41e6c01a 100644 --- a/generator/src/test/java/net/codecrete/qrbill/generator/PaymentsCharacterSetTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/generator/PaymentsCharacterSetTest.java @@ -3,8 +3,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import java.util.stream.Stream; + +import static net.codecrete.qrbill.generator.SPSCharacterSet.EXTENDED_LATIN; +import static net.codecrete.qrbill.generator.SPSCharacterSet.LATIN_1_SUBSET; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,84 +19,102 @@ class PaymentsCharacterSetTest { private static final String TEXT_WITHOUT_COMBINING_ACCENTS = "àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ"; private static final String TEXT_WITH_COMBINING_ACCENTS = "àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ"; - @ParameterizedTest - @ValueSource(chars = { 'A', 'b', '3', '%', '{', '®', 'Ò', 'æ', 'Ă', 'Ķ', 'Ŕ', 'ț', '€' }) - void validCharacters_returnsTrue(char validChar) { - assertTrue(Payments.isValidCharacter(validChar)); - } - - @ParameterizedTest - @ValueSource(chars = { 'A', 'b', '3', '%', '{', '®', 'Ò', 'æ', 'Ă', 'Ķ', 'Ŕ', 'ț', '€' }) - void validCodePoints_returnsTrue(char validChar) { - assertTrue(Payments.isValidCodePoint(validChar)); - } - - @ParameterizedTest - @ValueSource(chars = { '\n', '\r', '\u007f', '\u0083', 'Ɖ', 'Ǒ', 'Ȑ', 'Ȟ' }) - void invalidCharacters_returnsFalse(char invalidChar) { - assertFalse(Payments.isValidCharacter(invalidChar)); - } - - @ParameterizedTest - @ValueSource(chars = { '\n', '\r', '\u007f', '\u0083', 'Ɖ', 'Ǒ', 'Ȑ', 'Ȟ' }) - void invalidCodePoints_returnsFalse(char invalidChar) { - assertFalse(Payments.isValidCodePoint(invalidChar)); - } - @ParameterizedTest @ValueSource(strings = { "abc", "ABC", "123", "äöüÄÖÜ", "àáâäçèéìíîïñôöùúûüýÿÀÁÂÄÇÊËÌÍÎÏÓÔÖÛÜÝ", "€", "£", "¥", " ", "" }) void validText_returnsTrue(String validText) { - assertTrue(Payments.isValidText(validText)); + assertTrue(Payments.isValidText(validText, EXTENDED_LATIN)); } @ParameterizedTest @ValueSource(strings = { "a\nc", "ABǑC", "12\uD83D\uDE003", "äöü\uD83C\uDDEE\uD83C\uDDF9ÄÖÜ" }) void invalidText_returnsFalse(String invalidText) { - assertFalse(Payments.isValidText(invalidText)); + assertFalse(Payments.isValidText(invalidText, EXTENDED_LATIN)); + } + + @Test + void nullText_isValid() { + assertTrue(Payments.isValidText(null, LATIN_1_SUBSET)); + assertTrue(Payments.isValidText(null, EXTENDED_LATIN)); } @ParameterizedTest @ValueSource(strings = { "abc", " a b c ", "àáâäçè" }) void cleanText_isNotChanged(String text) { - assertEquals(text, Payments.cleanedText(text)); + assertEquals(text, Payments.cleanedText(text, EXTENDED_LATIN)); } @Test void decomposedAccents_areComposed() { assertEquals(46, TEXT_WITHOUT_COMBINING_ACCENTS.length()); assertEquals(59, TEXT_WITH_COMBINING_ACCENTS.length()); - assertEquals(TEXT_WITHOUT_COMBINING_ACCENTS, Payments.cleanedText(TEXT_WITH_COMBINING_ACCENTS)); + assertEquals(TEXT_WITHOUT_COMBINING_ACCENTS, Payments.cleanedText(TEXT_WITH_COMBINING_ACCENTS, EXTENDED_LATIN)); } @ParameterizedTest @ValueSource(strings = { "ab\nc|ab c", "ȆȇȈȉ|EeIi", "Ǒ|O", "ljnjffi|ljnjffi", "Ǽ|Æ", "ʷ|w", "⁵|5", "℁|a/s", - "Ⅶ|VII", "③|3", "xƉx|x.x", "x\uD83C\uDDE8\uD83C\uDDEDx|x.x" }) + "Ⅶ|VII", "③|3", "xƉx|x.x", "x\uD83C\uDDE8\uD83C\uDDEDx|x.x", "DŽ|DZ" }) void invalidCharacters_areReplaced(String combinedText) { String[] textAndExpected = combinedText.split("\\|"); - assertEquals(textAndExpected[1], Payments.cleanedAndTrimmedText(textAndExpected[0])); + assertEquals(textAndExpected[1], Payments.cleanedAndTrimmedText(textAndExpected[0], EXTENDED_LATIN)); } @Test void cleanedNull_returnsNull() { - assertNull(Payments.cleanedAndTrimmedText(null)); + assertNull(Payments.cleanedAndTrimmedText(null, EXTENDED_LATIN)); } @ParameterizedTest @ValueSource(strings = { "", " "}) void blankText_returnsNull(String text) { - assertNull(Payments.cleanedAndTrimmedText(text)); + assertNull(Payments.cleanedAndTrimmedText(text, EXTENDED_LATIN)); } @ParameterizedTest @ValueSource(strings = { "a b c|a b c", " a b c|a b c"}) void multipleSpaces_becomeSingleSpace(String combinedText) { String[] textAndExpected = combinedText.split("\\|"); - assertEquals(textAndExpected[1], Payments.cleanedAndTrimmedText(textAndExpected[0])); + assertEquals(textAndExpected[1], Payments.cleanedAndTrimmedText(textAndExpected[0], EXTENDED_LATIN)); } @ParameterizedTest @ValueSource(strings = { " a ", " a b", " "}) void spaces_areNotTrimmed(String text) { - assertEquals(text, Payments.cleanedText(text)); + assertEquals(text, Payments.cleanedText(text, EXTENDED_LATIN)); + } + + @ParameterizedTest + @MethodSource("provideExtendedLatinChars") + void allChars_haveGoodReplacement(char ch, String unicodeName) { + // unicode code name is displayed if test fails + + String cleaned = Payments.cleanedText(String.valueOf(ch), LATIN_1_SUBSET); + assertNotEquals(".", cleaned); + assertTrue(Payments.isValidText(cleaned, LATIN_1_SUBSET)); + + + cleaned = Payments.cleanedText(String.valueOf(ch), EXTENDED_LATIN); + assertNotEquals(".", cleaned); + assertTrue(Payments.isValidText(cleaned, EXTENDED_LATIN)); + } + + private static Stream provideExtendedLatinChars() { + Stream.Builder builder = Stream.builder(); + for (char ch = 0x0020; ch <= 0x007E; ch++) { + if (ch == '.' || ch == '^') + continue; + builder.add(Arguments.of(ch, String.format("U+%04X", (int) ch))); + } + for (char ch = 0x00A0; ch <= 0x00FF; ch++) { + builder.add(Arguments.of(ch, String.format("U+%04X", (int) ch))); + } + for (char ch = 0x0100; ch <= 0x017F; ch++) { + builder.add(Arguments.of(ch, String.format("U+%04X", (int) ch))); + } + builder.add(Arguments.of((char) 0x0218, "U+0218")); + builder.add(Arguments.of((char) 0x0219, "U+0219")); + builder.add(Arguments.of((char) 0x021A, "U+021A")); + builder.add(Arguments.of((char) 0x021B, "U+021B")); + builder.add(Arguments.of((char) 0x20AC, "U+20AC")); + return builder.build(); } } diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/QRBillTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/QRBillTest.java index 4f69bcf8..df235eae 100644 --- a/generator/src/test/java/net/codecrete/qrbill/generator/QRBillTest.java +++ b/generator/src/test/java/net/codecrete/qrbill/generator/QRBillTest.java @@ -11,6 +11,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * Unit tests for generation of A6 bills (PDF and SVG) *

diff --git a/generator/src/test/java/net/codecrete/qrbill/generator/SPSCharacterSetTest.java b/generator/src/test/java/net/codecrete/qrbill/generator/SPSCharacterSetTest.java new file mode 100644 index 00000000..a50da566 --- /dev/null +++ b/generator/src/test/java/net/codecrete/qrbill/generator/SPSCharacterSetTest.java @@ -0,0 +1,40 @@ +package net.codecrete.qrbill.generator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for SPS character set + */ +@DisplayName("SPS character set") +class SPSCharacterSetTest { + + @ParameterizedTest + @ValueSource(chars = { 'A', 'b', '3', '%', '{', '®', 'Ò', 'æ', 'Ă', 'Ķ', 'Ŕ', 'ț', '€' }) + void extendedLatin_containsValidCharacters(char validChar) { + assertTrue(SPSCharacterSet.EXTENDED_LATIN.contains(validChar)); + } + + @ParameterizedTest + @ValueSource(chars = { 'A', 'b', '3', '%', '{', '®', 'Ò', 'æ', 'Ă', 'Ķ', 'Ŕ', 'ț', '€' }) + void extendedLatin_containsValidCodePoints(char validChar) { + assertTrue(SPSCharacterSet.EXTENDED_LATIN.contains((int)validChar)); + } + + @ParameterizedTest + @ValueSource(chars = { '\n', '\r', '\u007f', '\u0083', 'Ɖ', 'Ǒ', 'Ȑ', 'Ȟ' }) + void extendedLatin_doesNotContainInvalidCharacters(char invalidChar) { + assertFalse(SPSCharacterSet.EXTENDED_LATIN.contains(invalidChar)); + } + + @ParameterizedTest + @ValueSource(chars = { '\n', '\r', '\u007f', '\u0083', 'Ɖ', 'Ǒ', 'Ȑ', 'Ȟ' }) + void extendedLatin_doesNotContainInvalidCodePoints(char invalidChar) { + assertFalse(SPSCharacterSet.EXTENDED_LATIN.contains((int)invalidChar)); + } +} diff --git a/generator/src/test/java/net/codecrete/qrbill/testhelper/SampleData.java b/generator/src/test/java/net/codecrete/qrbill/testhelper/SampleData.java index 80387c9b..def77e4a 100644 --- a/generator/src/test/java/net/codecrete/qrbill/testhelper/SampleData.java +++ b/generator/src/test/java/net/codecrete/qrbill/testhelper/SampleData.java @@ -179,4 +179,31 @@ public static Bill getExample7() { bill.setUnstructuredMessage("Auftrag 2830188 / Rechnung 2021007834"); return bill; } + + public static Bill getExample8() { + Bill bill = new Bill(); + bill.getFormat().setLanguage(Language.FR); + bill.setAccount("CH14 8914 4587 8681 9314 7"); + Address creditor = new Address(); + creditor.setName("Buğra Çavdarli"); + creditor.setStreet("Rue du Lièvre"); + creditor.setHouseNo("13"); + creditor.setPostalCode("1219"); + creditor.setTown("Aïre"); + creditor.setCountryCode("CH"); + bill.setCreditor(creditor); + bill.setAmount(BigDecimal.valueOf(17900, 2)); + bill.setCurrency("CHF"); + Address debtor = new Address(); + debtor.setName("L'Œil de Bœuf"); + debtor.setStreet("Route d'Outre Vièze"); + debtor.setHouseNo("44"); + debtor.setPostalCode("1871"); + debtor.setTown("Choëx"); + debtor.setCountryCode("CH"); + bill.setDebtor(debtor); + bill.setReference("RF35RF23452352345"); + bill.setUnstructuredMessage("Facture 48390, €10 de réduction"); + return bill; + } } diff --git a/generator/src/test/resources/a4bill_ex1.pdf b/generator/src/test/resources/a4bill_ex1.pdf index 485e5c9e..5457dbb4 100644 Binary files a/generator/src/test/resources/a4bill_ex1.pdf and b/generator/src/test/resources/a4bill_ex1.pdf differ diff --git a/generator/src/test/resources/a4bill_ex2.pdf b/generator/src/test/resources/a4bill_ex2.pdf index fdeb8a7c..4b3ae8f2 100644 Binary files a/generator/src/test/resources/a4bill_ex2.pdf and b/generator/src/test/resources/a4bill_ex2.pdf differ diff --git a/generator/src/test/resources/a4bill_ex3.pdf b/generator/src/test/resources/a4bill_ex3.pdf index bc48dfdc..248c1406 100644 Binary files a/generator/src/test/resources/a4bill_ex3.pdf and b/generator/src/test/resources/a4bill_ex3.pdf differ diff --git a/generator/src/test/resources/a4bill_ex4.pdf b/generator/src/test/resources/a4bill_ex4.pdf index 809fb2da..d60dcbad 100644 Binary files a/generator/src/test/resources/a4bill_ex4.pdf and b/generator/src/test/resources/a4bill_ex4.pdf differ diff --git a/generator/src/test/resources/a4bill_ex5.pdf b/generator/src/test/resources/a4bill_ex5.pdf index e956e237..f21838b0 100644 Binary files a/generator/src/test/resources/a4bill_ex5.pdf and b/generator/src/test/resources/a4bill_ex5.pdf differ diff --git a/generator/src/test/resources/a4bill_ex6.pdf b/generator/src/test/resources/a4bill_ex6.pdf index 0fe9316a..6947eb21 100644 Binary files a/generator/src/test/resources/a4bill_ex6.pdf and b/generator/src/test/resources/a4bill_ex6.pdf differ diff --git a/generator/src/test/resources/a4bill_ex8a.pdf b/generator/src/test/resources/a4bill_ex8a.pdf new file mode 100644 index 00000000..4ec25070 Binary files /dev/null and b/generator/src/test/resources/a4bill_ex8a.pdf differ diff --git a/generator/src/test/resources/a4bill_ex8b.pdf b/generator/src/test/resources/a4bill_ex8b.pdf new file mode 100644 index 00000000..0d458bbe Binary files /dev/null and b/generator/src/test/resources/a4bill_ex8b.pdf differ diff --git a/generator/src/test/resources/a4bill_postproc1.pdf b/generator/src/test/resources/a4bill_postproc1.pdf index 96e79d41..04d4573e 100644 Binary files a/generator/src/test/resources/a4bill_postproc1.pdf and b/generator/src/test/resources/a4bill_postproc1.pdf differ diff --git a/generator/src/test/resources/a4bill_postproc2.pdf b/generator/src/test/resources/a4bill_postproc2.pdf index 41cba4ab..3b33b121 100644 Binary files a/generator/src/test/resources/a4bill_postproc2.pdf and b/generator/src/test/resources/a4bill_postproc2.pdf differ diff --git a/generator/src/test/resources/invoice-01.pdf b/generator/src/test/resources/invoice-01.pdf index d928283b..a926a348 100644 Binary files a/generator/src/test/resources/invoice-01.pdf and b/generator/src/test/resources/invoice-01.pdf differ diff --git a/generator/src/test/resources/invoice-02.pdf b/generator/src/test/resources/invoice-02.pdf index d928283b..a926a348 100644 Binary files a/generator/src/test/resources/invoice-02.pdf and b/generator/src/test/resources/invoice-02.pdf differ diff --git a/generator/src/test/resources/invoice-03.pdf b/generator/src/test/resources/invoice-03.pdf index 989b208c..793b509a 100644 Binary files a/generator/src/test/resources/invoice-03.pdf and b/generator/src/test/resources/invoice-03.pdf differ diff --git a/generator/src/test/resources/invoice-04.pdf b/generator/src/test/resources/invoice-04.pdf index 989b208c..793b509a 100644 Binary files a/generator/src/test/resources/invoice-04.pdf and b/generator/src/test/resources/invoice-04.pdf differ diff --git a/generator/src/test/resources/linestyle_1.pdf b/generator/src/test/resources/linestyle_1.pdf index 485e5c9e..5457dbb4 100644 Binary files a/generator/src/test/resources/linestyle_1.pdf and b/generator/src/test/resources/linestyle_1.pdf differ diff --git a/generator/src/test/resources/linestyle_2.pdf b/generator/src/test/resources/linestyle_2.pdf index 6256bc60..d8fdce08 100644 Binary files a/generator/src/test/resources/linestyle_2.pdf and b/generator/src/test/resources/linestyle_2.pdf differ diff --git a/generator/src/test/resources/pdfcanvas-opendoc.pdf b/generator/src/test/resources/pdfcanvas-opendoc.pdf index 334dc1cb..677dd869 100644 Binary files a/generator/src/test/resources/pdfcanvas-opendoc.pdf and b/generator/src/test/resources/pdfcanvas-opendoc.pdf differ diff --git a/generator/src/test/resources/pdfcanvas-openstream.pdf b/generator/src/test/resources/pdfcanvas-openstream.pdf index affa1121..9e4411b1 100644 Binary files a/generator/src/test/resources/pdfcanvas-openstream.pdf and b/generator/src/test/resources/pdfcanvas-openstream.pdf differ diff --git a/generator/src/test/resources/pdfcanvas-saveas.pdf b/generator/src/test/resources/pdfcanvas-saveas.pdf index 0912ae44..cf4e3920 100644 Binary files a/generator/src/test/resources/pdfcanvas-saveas.pdf and b/generator/src/test/resources/pdfcanvas-saveas.pdf differ diff --git a/generator/src/test/resources/pdfcanvas-writeto.pdf b/generator/src/test/resources/pdfcanvas-writeto.pdf index c2d4087b..f3e7bbbc 100644 Binary files a/generator/src/test/resources/pdfcanvas-writeto.pdf and b/generator/src/test/resources/pdfcanvas-writeto.pdf differ diff --git a/generator/src/test/resources/qrcode_quiet_zone.pdf b/generator/src/test/resources/qrcode_quiet_zone.pdf index 5260bfc5..d7e7fcee 100644 Binary files a/generator/src/test/resources/qrcode_quiet_zone.pdf and b/generator/src/test/resources/qrcode_quiet_zone.pdf differ