libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit fa03b8e97ebe07dd5dd3fc6dfbeefd3952c6a297
parent 32a6a7c603aeae577619f1177cc35627ba1118ad
Author: Antoine A <>
Date:   Tue, 23 Jun 2026 11:46:51 +0200

nexus: improve pdf generation

Diffstat:
MCargo.lock | 108++++++++++++++-----------------------------------------------------------------
Mlibeufin-ebics/Cargo.toml | 2+-
Mlibeufin-ebics/src/pdf.rs | 148++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtestbench/src/main.rs | 4++--
4 files changed, 132 insertions(+), 130 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -333,17 +333,6 @@ dependencies = [ ] [[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1025,7 +1014,6 @@ version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ - "crc32fast", "miniz_oxide", "zlib-rs", ] @@ -1348,7 +1336,7 @@ dependencies = [ [[package]] name = "http-client" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#5701a8fd93a4833e3cc5ab7b128648d03122d111" +source = "git+git://git.taler.net/taler-rust.git/#e8a8977ada44fe6015d5a50eb8db63e3dfe693f2" dependencies = [ "compact_str", "futures-util", @@ -1809,8 +1797,8 @@ dependencies = [ "flate2", "futures-util", "jiff", + "pdf-writer", "pretty_assertions", - "printpdf", "rand 0.10.1", "rcgen", "reqwest", @@ -1927,12 +1915,6 @@ dependencies = [ ] [[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1977,23 +1959,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] -name = "lopdf" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" -dependencies = [ - "encoding_rs", - "flate2", - "itoa", - "linked-hash-map", - "log", - "md5", - "pom", - "time", - "weezl", -] - -[[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2016,12 +1981,6 @@ dependencies = [ ] [[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - -[[package]] name = "memchr" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2218,15 +2177,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "owned_ttf_parser" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" -dependencies = [ - "ttf-parser", -] - -[[package]] name = "owo-colors" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2262,6 +2212,18 @@ dependencies = [ ] [[package]] +name = "pdf-writer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e456864a7a304047bff84977dc6fb162bd956475d40ba50b2dcecaada7f753" +dependencies = [ + "bitflags 2.13.0", + "itoa", + "memchr", + "ryu", +] + +[[package]] name = "pem" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2326,15 +2288,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] -name = "pom" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b" -dependencies = [ - "bstr", -] - -[[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2384,17 +2337,6 @@ dependencies = [ ] [[package]] -name = "printpdf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d" -dependencies = [ - "lopdf", - "owned_ttf_parser", - "time", -] - -[[package]] name = "proc-macro-error-attr2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3498,7 +3440,7 @@ dependencies = [ [[package]] name = "taler-api" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#5701a8fd93a4833e3cc5ab7b128648d03122d111" +source = "git+git://git.taler.net/taler-rust.git/#e8a8977ada44fe6015d5a50eb8db63e3dfe693f2" dependencies = [ "aws-lc-rs", "axum", @@ -3525,12 +3467,12 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#5701a8fd93a4833e3cc5ab7b128648d03122d111" +source = "git+git://git.taler.net/taler-rust.git/#e8a8977ada44fe6015d5a50eb8db63e3dfe693f2" [[package]] name = "taler-common" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#5701a8fd93a4833e3cc5ab7b128648d03122d111" +source = "git+git://git.taler.net/taler-rust.git/#e8a8977ada44fe6015d5a50eb8db63e3dfe693f2" dependencies = [ "anyhow", "aws-lc-rs", @@ -3559,7 +3501,7 @@ dependencies = [ [[package]] name = "taler-macros" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#5701a8fd93a4833e3cc5ab7b128648d03122d111" +source = "git+git://git.taler.net/taler-rust.git/#e8a8977ada44fe6015d5a50eb8db63e3dfe693f2" dependencies = [ "proc-macro2", "quote", @@ -3569,7 +3511,7 @@ dependencies = [ [[package]] name = "taler-test-utils" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#5701a8fd93a4833e3cc5ab7b128648d03122d111" +source = "git+git://git.taler.net/taler-rust.git/#e8a8977ada44fe6015d5a50eb8db63e3dfe693f2" dependencies = [ "aws-lc-rs", "axum", @@ -3925,12 +3867,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "ttf-parser" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" - -[[package]] name = "tungstenite" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4252,12 +4188,6 @@ dependencies = [ ] [[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] name = "whoami" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/libeufin-ebics/Cargo.toml b/libeufin-ebics/Cargo.toml @@ -38,4 +38,4 @@ roxmltree = "0.21.1" rcgen = { version = "0.14.7", features = ["aws_lc_rs", "pem"], default-features = false } x509-parser = { version = "0.18.1", features = ["verify-aws"] } calamine = { version = "0.35.0" } -printpdf = { version = "0.7", default-features = false } +pdf-writer = "0.15.0" diff --git a/libeufin-ebics/src/pdf.rs b/libeufin-ebics/src/pdf.rs @@ -19,18 +19,43 @@ use aws_lc_rs::{encoding::AsDer as _, rsa::PublicKey, signature::KeyPair}; use jiff::Zoned; -use printpdf::{BuiltinFont, Mm, PdfDocument}; +use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str}; use crate::{ config::EbicsHostCfg, crypto::ebics_pub_key_hash, keys::ClientKeys, utils::hex_chunk_by_two, }; - pub fn generate_keys_pdf(keys: &ClientKeys, cfg: &EbicsHostCfg) -> anyhow::Result<Vec<u8>> { let date = Zoned::now().date().to_string(); - let (doc, p, l) = PdfDocument::new("EBICS Client Keys", Mm(210.0), Mm(297.0), "Content"); - let reg = doc.add_builtin_font(BuiltinFont::Helvetica)?; - let bld = doc.add_builtin_font(BuiltinFont::HelveticaBold)?; + // Assign structural IDs directly + let catalog_id = Ref::new(1); + let page_tree_id = Ref::new(2); + let font_reg_id = Ref::new(3); + let font_bld_id = Ref::new(4); + + // Static mapping for the 3 EBICS pages + let page_ids = [Ref::new(5), Ref::new(6), Ref::new(7)]; + let content_ids = [Ref::new(8), Ref::new(9), Ref::new(10)]; + + let mut pdf = Pdf::new(); + + // Catalog & Page Tree + pdf.catalog(catalog_id).pages(page_tree_id); + pdf.pages(page_tree_id) + .count(3) + .kids(page_ids.iter().copied()); + + // Register built-in fonts + pdf.type1_font(font_reg_id) + .base_font(Name(b"Helvetica")) + .encoding_predefined(Name(b"WinAnsiEncoding")); + + pdf.type1_font(font_bld_id) + .base_font(Name(b"Helvetica-Bold")) + .encoding_predefined(Name(b"WinAnsiEncoding")); + + let f_reg = Name(b"F1"); + let f_bld = Name(b"F2"); let pages_data = [ ( @@ -50,37 +75,82 @@ pub fn generate_keys_pdf(keys: &ClientKeys, cfg: &EbicsHostCfg) -> anyhow::Resul ), ]; + // Conversion factor for Millimeters to Points + const fn mm(v: f32) -> f32 { + v * 2.834_65 + } + + struct Page { + y: f32, + content: Content, + } + + impl Page { + pub fn new() -> Self { + Self { + y: 270.0, + content: Content::new(), + } + } + + pub fn line(&mut self, font: Name, size: f32, text: &str) { + let encoded: Vec<u8> = text + .chars() + .map(|c| match c { + 'Ö' => 0xD6, + 'ü' => 0xFC, + 'ä' => 0xE4, + 'ß' => 0xDF, + _ => c as u8, + }) + .collect(); + + self.content.begin_text(); + self.content.set_font(font, size); + self.content + .set_text_matrix([1.0, 0.0, 0.0, 1.0, mm(15.0), mm(self.y)]); + self.content.show(Str(&encoded)); + self.content.end_text(); + + self.y -= size * 0.45; + } + } + for (i, (title, label, pub_key)) in pages_data.into_iter().enumerate() { - let layer = if i == 0 { - doc.get_page(p).get_layer(l) - } else { - let (p, l) = doc.add_page(Mm(210.0), Mm(297.0), "Content"); - doc.get_page(p).get_layer(l) - }; - - let mut y = 270.0; - let line = |font, y: &mut f32, size, text: &str| { - layer.use_text(text, size, Mm(15.0), Mm(*y), font); - *y -= size * 0.45; - }; + let mut page = pdf.page(page_ids[i]); + page.parent(page_tree_id); + page.media_box(Rect::new(0.0, 0.0, mm(210.0), mm(297.0))); + page.contents(content_ids[i]); + + // Map Fonts to Page Resources + let mut res = page.resources(); + let mut fonts = res.fonts(); + fonts.insert(f_reg).primitive(font_reg_id); + fonts.insert(f_bld).primitive(font_bld_id); + fonts.finish(); + res.finish(); + page.finish(); + + let mut p = Page::new(); // Write Header - line(&bld, &mut y, 24.0, title); - y -= 2.2; - line(&reg, &mut y, 13.0, &format!("Datum: {date}")); - line(&reg, &mut y, 13.0, &format!("Host-ID: {}", cfg.host_id)); - line(&reg, &mut y, 13.0, &format!("User-ID: {}", cfg.user_id)); - line( - &reg, - &mut y, - 11.0, + p.line(f_bld, 24.0, title); + p.y -= 2.2; + for header in [ + format!("Datum: {date}").as_str(), + &format!("Host-ID: {}", cfg.host_id), + &format!("User-ID: {}", cfg.user_id), &format!("Partner-ID: {}", cfg.partner_id), - ); - line(&reg, &mut y, 13.0, "ES version: A006"); - y -= 5.0; - line(&bld, &mut y, 13.0, label); - y -= 5.0; + "ES version: A006", + ] { + p.line(f_reg, 13.0, header); + } + + p.y -= 5.0; + p.line(f_bld, 13.0, label); + p.y -= 5.0; + // Write Public Key Fields for (hdr, bytes) in [ ( "Exponent:", @@ -92,20 +162,22 @@ pub fn generate_keys_pdf(keys: &ClientKeys, cfg: &EbicsHostCfg) -> anyhow::Resul ), ("SHA-256 hash:", ebics_pub_key_hash(pub_key).as_ref()), ] { - line(&bld, &mut y, 13.0, hdr); + p.line(f_bld, 13.0, hdr); for chunk in bytes.chunks(16) { - line(&reg, &mut y, 11.0, &hex_chunk_by_two(chunk).to_string()); + p.line(f_reg, 11.0, &hex_chunk_by_two(chunk).to_string()); } - y -= 5.0; + p.y -= 5.0; } // Write Footer - line(&reg, &mut y, 13.0, "Ort / Datum: ________________"); - line(&reg, &mut y, 13.0, "Firma / Name: ________________"); - line(&reg, &mut y, 13.0, "Unterschrift: ________________"); + p.line(f_reg, 13.0, "Ort / Datum: ________________"); + p.line(f_reg, 13.0, "Firma / Name: ________________"); + p.line(f_reg, 13.0, "Unterschrift: ________________"); + + pdf.stream(content_ids[i], &p.content.finish()); } - Ok(doc.save_to_bytes()?) + Ok(pdf.finish()) } #[test] diff --git a/testbench/src/main.rs b/testbench/src/main.rs @@ -77,7 +77,7 @@ pub struct TestbenchCmd { } #[derive(Parser)] -#[command(name = "shell", no_binary_name = true)] +#[command(no_binary_name = true)] /// Run integration tests on banks provider pub enum NexusCmd { #[clap(alias = "ebics-setup")] @@ -103,7 +103,7 @@ pub enum NexusCmd { } #[derive(Parser)] -#[command(name = "shell", no_binary_name = true)] +#[command(no_binary_name = true)] /// Run integration tests on banks provider pub enum SyncCmd { #[clap(alias = "ebics-setup")]