From 3fcbb55b8fee05fefcbd92ec6b8b6963b80d50fe Mon Sep 17 00:00:00 2001 From: Sebastian Klemp Date: Sun, 8 Feb 2026 13:32:16 +0100 Subject: [PATCH] feat: dix all pdf problems. Validator now green --- build.sh | 3 +- debug.log | 163 ++++++++++++++++++++++++ lib/append.go | 7 +- lib/attachment.go | 317 ++++++++++++++++++++++++++++++++++++++++++++++ lib/xmp.go | 109 ++++++++++++++-- main.go | 2 +- 6 files changed, 588 insertions(+), 13 deletions(-) create mode 100644 lib/attachment.go diff --git a/build.sh b/build.sh index c090270..4ff11a3 100755 --- a/build.sh +++ b/build.sh @@ -1 +1,2 @@ -GOARCH=amd64 GOOS=windows go build -ldflags "-s -w" -o dist/pdf_xml_helper.exe main.go \ No newline at end of file +GOARCH=amd64 GOOS=windows go build -ldflags "-s -w" -o dist/pdf_xml_helper.exe main.go +go build -ldflags "-s -w" -o dist/pdf_xml_helper main.go diff --git a/debug.log b/debug.log index e69de29..d0d62a4 100755 --- a/debug.log +++ b/debug.log @@ -0,0 +1,163 @@ +2026-02-07 16:24:43 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 16:24:43 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 16:27:07 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 16:27:07 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 16:31:28 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 16:31:28 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 16:31:28 leave appendToPdf +2026-02-07 16:46:56 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 16:46:56 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 16:46:56 leave appendToPdf +2026-02-07 17:00:03 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:00:03 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:02:58 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:02:58 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:03:39 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:03:39 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:05:24 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:05:24 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:05:59 leave appendToPdf +2026-02-07 17:08:43 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:08:43 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:11:09 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:11:09 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:12:31 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:12:31 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:13:34 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:13:34 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:13:34 leave appendToPdf +2026-02-07 17:13:38 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:13:38 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:15:13 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:15:13 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:16:18 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:16:18 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:19:00 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:19:00 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:21:06 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:21:06 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:23:32 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:23:32 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-07 17:31:00 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-07 17:31:00 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:14:12 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:14:12 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:14:12 leave appendToPdf +2026-02-08 10:15:48 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:15:48 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:17:46 leave appendToPdf +2026-02-08 10:17:47 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:17:47 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:17:47 leave appendToPdf +2026-02-08 10:18:38 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:18:38 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:18:38 leave appendToPdf +2026-02-08 10:19:18 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:19:18 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:19:18 leave appendToPdf +2026-02-08 10:22:04 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:22:04 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:22:07 leave appendToPdf +2026-02-08 10:22:16 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:22:16 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:24:22 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:24:22 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:27:57 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:27:57 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:34:06 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:34:06 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:37:06 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:37:06 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:39:06 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:39:06 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:41:13 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:41:13 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:45:58 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:45:58 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:45:58 leave appendToPdf +2026-02-08 10:46:10 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:46:10 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:46:10 leave appendToPdf +2026-02-08 10:46:45 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:46:45 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:46:45 leave appendToPdf +2026-02-08 10:48:01 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:48:01 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:53:16 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:53:16 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:53:16 leave appendToPdf +2026-02-08 10:59:53 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 10:59:53 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 10:59:59 leave appendToPdf +2026-02-08 11:03:20 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:03:20 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:03:20 leave appendToPdf +2026-02-08 11:04:16 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:04:16 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:06:12 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:06:12 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:06:12 leave appendToPdf +2026-02-08 11:06:43 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:06:43 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:06:43 leave appendToPdf +2026-02-08 11:09:57 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:09:57 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:12:28 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:12:28 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:12:36 leave appendToPdf +2026-02-08 11:14:15 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:14:15 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:14:15 leave appendToPdf +2026-02-08 11:17:27 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:17:27 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:17:27 leave appendToPdf +2026-02-08 11:18:57 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:18:57 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:18:57 leave appendToPdf +2026-02-08 11:19:25 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:19:25 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:19:25 leave appendToPdf +2026-02-08 11:23:48 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:23:48 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:23:48 leave appendToPdf +2026-02-08 11:24:14 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:24:14 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:24:14 leave appendToPdf +2026-02-08 11:26:19 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 11:26:19 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 11:30:08 leave appendToPdf +2026-02-08 12:30:30 enter ExtractFromPdf(./testdata/EN16931_Physiotherapeut_fx.pdf, ) +2026-02-08 12:30:30 pdfInput: './testdata/EN16931_Physiotherapeut_fx.pdf' output: './testdata/EN16931_Physiotherapeut_fx.xml' +2026-02-08 12:30:30 {factur-x.xml Factur-X Rechnung text/xml 13768 4151 0001-01-01 00:00:00 +0000 UTC 2025-12-03 21:24:56 +0000 +0000 [48 97 48 53 57 56 101 49 102 48 49 52 101 48 51 51 51 54 52 53 50 98 101 100 57 53 97 50 51 55 97 101] Alternative false 29 0 R 28 0 R} +2026-02-08 12:31:13 leave extractFromPdf +2026-02-08 12:31:31 enter ExtractFromPdf(./tmp/test.pdf, ) +2026-02-08 12:31:31 pdfInput: './tmp/test.pdf' output: './tmp/test.xml' +2026-02-08 12:31:31 {factur-x.xml Rechnung text/xml 15891 2371 0001-01-01 00:00:00 +0000 UTC 2026-02-08 11:26:25 +0100 +0100 [104 87 4 121 245 4 124 6 192 221 18 52 191 180 47 37] Alternative false 32 0 R 31 0 R} +2026-02-08 12:35:42 leave extractFromPdf +2026-02-08 12:35:54 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 12:35:54 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 13:00:39 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 13:00:39 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 13:00:39 leave appendToPdf +2026-02-08 13:03:25 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 13:03:25 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 13:03:25 leave appendToPdf +2026-02-08 13:04:09 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 13:04:09 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 13:04:26 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 13:04:26 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 13:06:12 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 13:06:12 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 13:06:12 leave appendToPdf +2026-02-08 13:11:18 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test.pdf) +2026-02-08 13:11:18 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test.pdf' +2026-02-08 13:11:18 leave appendToPdf +2026-02-08 13:14:54 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test_1.pdf) +2026-02-08 13:14:54 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test_1.pdf' +2026-02-08 13:14:54 leave appendToPdf +2026-02-08 13:26:55 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test_1.pdf) +2026-02-08 13:26:55 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test_1.pdf' +2026-02-08 13:26:55 leave appendToPdf +2026-02-08 13:29:56 enter appendToPdf(./testdata/180274_20250915_Rechnung-32096.PDF, ./testdata/180274_20250915_Rechnung-32096.xml, tmp/test_1.pdf) +2026-02-08 13:29:56 pdfInput: './testdata/180274_20250915_Rechnung-32096.PDF' xmlInput: './testdata/180274_20250915_Rechnung-32096.xml' output: 'tmp/test_1.pdf' +2026-02-08 13:29:56 leave appendToPdf diff --git a/lib/append.go b/lib/append.go index b5d58b0..685856a 100755 --- a/lib/append.go +++ b/lib/append.go @@ -70,12 +70,13 @@ func AppendToPdf( defer inputFile.Close() var result io.Reader - result, err = addAttachment(inputFile, inputXmlFilePath, embedName, description) + + result, err = addXmpData(inputFile) if err != nil { return err } - result, err = addXmpData(result) + result, err = addAttachment(result, inputXmlFilePath, embedName, description) if err != nil { return err } @@ -120,7 +121,7 @@ func addAttachment( return nil, err } - err = doc.AddAttachment(attachment.Attachment{ + err = AddAttachment(doc, attachment.Attachment{ Name: embedName, Description: description, MIMEType: "text/xml", diff --git a/lib/attachment.go b/lib/attachment.go new file mode 100644 index 0000000..f4fd87a --- /dev/null +++ b/lib/attachment.go @@ -0,0 +1,317 @@ +package lib + +import ( + "crypto/md5" + "errors" + "fmt" + "path/filepath" + "sort" + "time" + + "gitlab.com/romalor/pdflib/pkg/components/attachment" + "gitlab.com/romalor/pdflib/pkg/core" + "gitlab.com/romalor/pdflib/pkg/pdf" +) + +// AddAttachment embeds data as a file attachment. +// The att.Name field specifies the attachment name. +func AddAttachment(d *pdf.Document, att attachment.Attachment, data []byte) error { + if !d.IsWritable() || d.Writer() == nil { + return pdf.ErrNotWritable + } + + if att.Name == "" { + return errors.New("attachment name cannot be empty") + } + + // Apply defaults + modDate := att.ModDate + if modDate.IsZero() { + modDate = time.Now() + } + relationship := att.Relationship + if relationship == "" { + relationship = "Unspecified" + } + + // Guess MIME type if not provided + mimeType := att.MIMEType + if mimeType == "" { + mimeType = guessMIMEType(att.Name) + } + + // Calculate checksum + checksum := md5.Sum(data) + + // Create embedded file stream + efStreamDict := core.NewDictionary() + efStreamDict.Set(core.Name("Type"), core.Name("EmbeddedFile")) + if mimeType != "" { + efStreamDict.Set(core.Name("Subtype"), core.Name(mimeType)) + } + + // Create Params dictionary + params := core.NewDictionary() + params.Set(core.Name("Size"), core.Integer(len(data))) + params.Set(core.Name("ModDate"), core.String(Format(modDate))) + params.Set(core.Name("CheckSum"), core.HexString(checksum[:])) + efStreamDict.Set(core.Name("Params"), params) + + // Create the stream (compression is handled by the writer) + var efStream *core.Stream + if !att.NoCompress { + efStreamDict.Set(core.Name("Filter"), core.Name("FlateDecode")) + } + efStream = core.NewStreamFromBytes(efStreamDict, data) + efStreamRef := d.Writer().Add(efStream) + + // Create EF (embedded files) dictionary + efDict := core.NewDictionary() + efDict.Set(core.Name("F"), efStreamRef) + efDict.Set(core.Name("UF"), efStreamRef) // Unicode-compatible version + + // Create file specification dictionary + fileSpec := core.NewDictionary() + fileSpec.Set(core.Name("Type"), core.Name("Filespec")) + fileSpec.Set(core.Name("F"), core.String(att.Name)) + fileSpec.Set(core.Name("UF"), core.String(att.Name)) // Unicode filename + fileSpec.Set(core.Name("EF"), efDict) + + if att.Description != "" { + fileSpec.Set(core.Name("Desc"), core.String(att.Description)) + } + + if relationship != "Unspecified" { + fileSpec.Set(core.Name("AFRelationship"), core.Name(relationship)) + } + + fileSpecRef := d.Writer().Add(fileSpec) + + // Update the EmbeddedFiles name tree + if err := addToEmbeddedFilesNameTree(d, att.Name, fileSpecRef); err != nil { + return fmt.Errorf("adding to embedded files tree: %w", err) + } + + return nil +} + +// addToEmbeddedFilesNameTree adds an entry to the EmbeddedFiles name tree. +func addToEmbeddedFilesNameTree(d *pdf.Document, name string, fileSpecRef core.Reference) error { + catalog, catalogRef, err := d.Resolver().Catalog() + if err != nil { + return err + } + + // Try to get existing Names dictionary + var namesDict *core.Dictionary + var namesRef core.Reference + var namesIsRef bool + + if namesObj, ok := catalog.Get(core.Name("Names")); ok { + if ref, isRef := namesObj.(core.Reference); isRef { + namesRef = ref + namesIsRef = true + } + namesDict, err = core.ResolveDict(d.Resolver(), namesObj) + if err != nil { + return fmt.Errorf("resolving Names dictionary: %w", err) + } + // Clone the dictionary + namesDict = namesDict.CloneDict() + } else { + // Create new Names dictionary + namesDict = core.NewDictionary() + } + + // Try to get existing EmbeddedFiles name tree + var efDict *core.Dictionary + var efRef core.Reference + var efIsRef bool + + if efObj, ok := namesDict.Get(core.Name("EmbeddedFiles")); ok { + if ref, isRef := efObj.(core.Reference); isRef { + efRef = ref + efIsRef = true + } + efDict, err = core.ResolveDict(d.Resolver(), efObj) + if err != nil { + return fmt.Errorf("resolving EmbeddedFiles dictionary: %w", err) + } + } + + // Build new name tree with the added entry + var newNamesArray core.Array + + if efDict != nil { + // Get existing entries + entries, err := collectNameTreeEntries(d, efDict) + if err != nil { + return fmt.Errorf("collecting existing entries: %w", err) + } + + // Add new entry + entries = append(entries, nameTreeEntry{name: name, value: fileSpecRef}) + + // Sort by name + sort.Slice(entries, func(i, j int) bool { + return entries[i].name < entries[j].name + }) + + // Build names array + newNamesArray = make(core.Array, 0, len(entries)*2) + for _, entry := range entries { + newNamesArray = append(newNamesArray, core.String(entry.name), entry.value) + } + } else { + // Create new names array with single entry + newNamesArray = core.Array{core.String(name), fileSpecRef} + } + + // Create new EmbeddedFiles dictionary + newEFDict := core.NewDictionary() + newEFDict.Set(core.Name("Names"), newNamesArray) + + // Update or create EmbeddedFiles entry + if efIsRef { + if err := d.Writer().Modify(efRef, newEFDict); err != nil { + return fmt.Errorf("modifying EmbeddedFiles: %w", err) + } + } else { + namesDict.Set(core.Name("EmbeddedFiles"), newEFDict) + } + + // Update or create Names dictionary + if namesIsRef { + if !efIsRef { + // We added a new EmbeddedFiles entry, need to update Names + if err := d.Writer().Modify(namesRef, namesDict); err != nil { + return fmt.Errorf("modifying Names: %w", err) + } + } + } else { + // Update catalog + newCatalog := catalog.CloneDict() + newCatalog.Set(core.Name("Names"), namesDict) + + // Extend AF array + afArr := core.Array{fileSpecRef} + newCatalog.Set(core.Name("AF"), afArr) + + if err := d.Writer().Modify(catalogRef, newCatalog); err != nil { + return fmt.Errorf("modifying catalog: %w", err) + } + } + + return nil +} + +// nameTreeEntry represents a name-value pair from a name tree. +type nameTreeEntry struct { + name string + value core.Object +} + +// collectNameTreeEntries collects all entries from a name tree. +func collectNameTreeEntries(d *pdf.Document, dict *core.Dictionary) ([]nameTreeEntry, error) { + var entries []nameTreeEntry + + // Check for Names array (leaf node) + if namesObj, ok := dict.Get(core.Name("Names")); ok { + namesArray, err := core.ResolveArray(d.Resolver(), namesObj) + if err != nil { + return nil, err + } + // Names array has pairs: [name1, value1, name2, value2, ...] + for i := 0; i+1 < len(namesArray); i += 2 { + var name string + switch n := namesArray[i].(type) { + case core.String: + name = string(n) + case core.HexString: + name = string(n) + default: + continue + } + entries = append(entries, nameTreeEntry{name: name, value: namesArray[i+1]}) + } + } + + // Check for Kids array (intermediate node) + if kidsObj, ok := dict.Get(core.Name("Kids")); ok { + kidsArray, err := core.ResolveArray(d.Resolver(), kidsObj) + if err != nil { + return nil, err + } + for _, kid := range kidsArray { + kidDict, err := core.ResolveDict(d.Resolver(), kid) + if err != nil { + continue + } + kidEntries, err := collectNameTreeEntries(d, kidDict) + if err != nil { + continue + } + entries = append(entries, kidEntries...) + } + } + + return entries, nil +} + +// guessMIMEType guesses the MIME type from a filename. +func guessMIMEType(filename string) string { + ext := filepath.Ext(filename) + if ext == "" { + return "application/octet-stream" + } + + // Common MIME types + mimeTypes := map[string]string{ + ".pdf": "application/pdf", + ".txt": "text/plain", + ".html": "text/html", + ".htm": "text/html", + ".xml": "application/xml", + ".json": "application/json", + ".csv": "text/csv", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".mp3": "audio/mpeg", + ".mp4": "video/mp4", + ".zip": "application/zip", + ".doc": "application/msword", + ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".xls": "application/vnd.ms-excel", + ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + } + + if mime, ok := mimeTypes[ext]; ok { + return mime + } + return "application/octet-stream" +} + +// FormatPDFDate formats a time.Time as a PDF date string. +// The format is: D:YYYYMMDDHHmmSSOHH'mm' +// where O is the timezone indicator (+, or -) and HH'mm' is the offset. +func Format(t time.Time) string { + // Get timezone offset + _, offset := t.Zone() + offsetHours := offset / 3600 + offsetMins := (offset % 3600) / 60 + + var sign byte = '+' + if offsetHours < 0 { + sign = '-' + offsetHours = -offsetHours + offsetMins = -offsetMins + } + + return fmt.Sprintf("D:%04d%02d%02d%02d%02d%02d%c%02d'%02d'", + t.Year(), t.Month(), t.Day(), + t.Hour(), t.Minute(), t.Second(), + sign, offsetHours, offsetMins) +} diff --git a/lib/xmp.go b/lib/xmp.go index 73b4829..645e587 100644 --- a/lib/xmp.go +++ b/lib/xmp.go @@ -11,27 +11,120 @@ const ( FacturXPrefix = "fx:" ) +type PDFAProperty struct { + Name string `xmp:"pdfaProperty:name"` + ValueType string `xmp:"pdfaProperty:valueType"` + Category string `xmp:"pdfaProperty:category"` + Description string `xmp:"pdfaProperty:description"` +} + +type PDFASchema struct { + Schema string `xmp:"pdfaSchema:schema"` + NamespaceURI string `xmp:"pdfaSchema:namespaceURI"` + Prefix string `xmp:"pdfaSchema:prefix"` + Property []PDFAProperty `xmp:"pdfaSchema:property"` +} + +type PDFAExtensionData struct { + Schemas [1]PDFASchema `xmp:"pdfaExtension:schemas,bag"` +} + +type PDFAExtensionModel struct { + PDFAExtensionData + + xmpbase.XmpBase +} + +func (x PDFAExtensionModel) Can(nsName string) bool { + return nsName == "pdfaExtension" +} + +func (x PDFAExtensionModel) Namespaces() xmp.NamespaceList { + return xmp.NamespaceList{ + { + Name: "pdfaSchema", + URI: "http://www.aiim.org/pdfa/ns/schema#", + Factory: defaultFactory, + }, + { + Name: "pdfaExtension", + URI: "http://www.aiim.org/pdfa/ns/extension/", + Factory: defaultFactory, + }, + { + Name: "pdfaProperty", + URI: "http://www.aiim.org/pdfa/ns/property#", + Factory: defaultFactory, + }, + } +} + +func defaultFactory(name string) xmp.Model { + return nil +} + func addFacturXXmpData(data []byte) ([]byte, error) { + // --- Register Factur-X namespace --- + xmp.Register(&xmp.Namespace{ + Name: "fx", + URI: FacturXUri, + Factory: defaultFactory, + }) + doc := &xmp.Document{} err := xmp.Unmarshal(data, doc) if err != nil { return nil, err } - // --- Register Factur-X namespace --- - ns := &xmp.Namespace{ - Name: "fx", - URI: FacturXUri, - Factory: func(name string) xmp.Model { - return &xmpbase.XmpBase{} + model := &PDFAExtensionModel{ + PDFAExtensionData: PDFAExtensionData{ + Schemas: [1]PDFASchema{ + { + Schema: "Factur-X PDFA Extension Schema", + NamespaceURI: "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#", + Prefix: "fx", + Property: []PDFAProperty{ + { + Name: "DocumentType", + ValueType: "Text", + Category: "external", + Description: "Factur-X document type", + }, + { + Name: "DocumentFileName", + ValueType: "Text", + Category: "external", + Description: "Attached XML file name", + }, + { + Name: "Version", + ValueType: "Text", + Category: "external", + Description: "Factur-X version", + }, + { + Name: "ConformanceLevel", + ValueType: "Text", + Category: "external", + Description: "Factur-X profile", + }, + }, + }, + }, }, } - xmp.Register(ns) - _, err = doc.MakeModel(ns) + _, err = doc.AddModel(model) if err != nil { return nil, err } + doc.FindModel(&xmp.Namespace{ + Name: "", + URI: "", + Factory: nil, + }) + // --- Set Factur-X properties --- err = doc.SetPath(xmp.PathValue{ Path: FacturXPrefix + "ConformanceLevel", diff --git a/main.go b/main.go index 54f8274..086df49 100755 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ func main() { Name: path.Base(exeName), Usage: "Pdf xml invoice helper cli", UseShortOptionHandling: true, - Version: "1.0.0", + Version: "1.0.1", Commands: []*cli.Command{ { Name: "extract",