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`:
- * The QR bill is saved as an SVG file in the working directory.
- * The path of the working directory is printed to
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@@ -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: + *
+ *+ * 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+ * 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 Listdiff --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