AB-PDF: Textleser für PDF-Tests (ToUnicode, nur Node-Bordmittel)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f71e3e8345
commit
01406c82e5
2 files changed
+98
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { inflateSync } from 'node:zlib'
|
||||
|
||||
/** Beispiel-Auftrag (V3-Form) für die PDF-Tests; `extra` überschreibt einzelne Felder. */
|
||||
export function beispielAuftrag(extra = {}) {
|
||||
return {
|
||||
@@ -34,3 +36,67 @@ export function alleTexte(wert, aus = []) {
|
||||
}
|
||||
return aus
|
||||
}
|
||||
|
||||
/** Anzahl Seiten eines PDFs (Seitenobjekte, nicht der Seitenbaum). */
|
||||
export const seitenZahl = (pdf) => (pdf.toString('latin1').match(/\/Type \/Page(?![s\w])/g) || []).length
|
||||
|
||||
/**
|
||||
* Text aus einem mit pdfkit erzeugten PDF (eingebettete Schriften, Identity-H, ToUnicode), je Seite eine Liste
|
||||
* von Textzeilen in Zeichenreihenfolge. Nur für Tests: kennt genau das, was pdfkit schreibt.
|
||||
*/
|
||||
export function leseSeiten(pdf) {
|
||||
const s = pdf.toString('latin1')
|
||||
const objekte = new Map()
|
||||
const re = /(\d+) 0 obj\n/g
|
||||
let m
|
||||
while ((m = re.exec(s))) {
|
||||
const start = m.index + m[0].length
|
||||
const ende = s.indexOf('\nendobj', start)
|
||||
const koerper = s.slice(start, ende)
|
||||
const strom = koerper.indexOf('\nstream\n')
|
||||
let dict = koerper
|
||||
let daten = null
|
||||
if (strom >= 0) {
|
||||
dict = koerper.slice(0, strom)
|
||||
const laenge = Number(/\/Length (\d+)/.exec(dict)[1])
|
||||
daten = pdf.subarray(start + strom + 8, start + strom + 8 + laenge)
|
||||
if (/\/Filter \/FlateDecode/.test(dict)) daten = inflateSync(daten)
|
||||
}
|
||||
objekte.set(Number(m[1]), { dict, daten })
|
||||
re.lastIndex = ende
|
||||
}
|
||||
const tabellen = new Map()
|
||||
for (const [id, o] of objekte) {
|
||||
const tu = /\/ToUnicode (\d+) 0 R/.exec(o.dict)
|
||||
if (!/\/Subtype \/Type0/.test(o.dict) || !tu) continue
|
||||
const tabelle = []
|
||||
for (const r of objekte.get(Number(tu[1])).daten.toString('latin1').matchAll(/<([0-9a-f]{4})> <([0-9a-f]{4})> \[([^\]]*)\]/g)) {
|
||||
let i = parseInt(r[1], 16)
|
||||
for (const e of r[3].matchAll(/<([0-9a-f ]+)>/g)) tabelle[i++] = e[1].split(' ').map((h) => String.fromCharCode(parseInt(h, 16))).join('')
|
||||
}
|
||||
tabellen.set(id, tabelle)
|
||||
}
|
||||
const schriftNachName = new Map()
|
||||
for (const o of objekte.values()) {
|
||||
if (!/\/Font <</.test(o.dict)) continue
|
||||
for (const f of o.dict.matchAll(/\/(F\d+) (\d+) 0 R/g)) schriftNachName.set(f[1], tabellen.get(Number(f[2])))
|
||||
}
|
||||
const seiten = []
|
||||
for (const o of objekte.values()) {
|
||||
if (!/\/Type \/Page(?![s\w])/.test(o.dict)) continue
|
||||
const inhalt = objekte.get(Number(/\/Contents (\d+) 0 R/.exec(o.dict)[1])).daten.toString('latin1')
|
||||
const zeilen = []
|
||||
for (const bt of inhalt.matchAll(/BT\n([\s\S]*?)\nET/g)) {
|
||||
const tabelle = schriftNachName.get(/\/(F\d+) [\d.]+ Tf/.exec(bt[1])?.[1])
|
||||
let text = ''
|
||||
for (const tj of bt[1].matchAll(/\[([^\]]*)\] TJ/g)) {
|
||||
for (const h of tj[1].matchAll(/<([0-9a-f]+)>/g)) {
|
||||
for (let i = 0; i < h[1].length; i += 4) text += tabelle[parseInt(h[1].slice(i, i + 4), 16)] ?? '\u{FFFD}'
|
||||
}
|
||||
}
|
||||
zeilen.push(text)
|
||||
}
|
||||
seiten.push(zeilen)
|
||||
}
|
||||
return seiten
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import PDFDocument from 'pdfkit'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { leseSeiten, seitenZahl } from './auftraegePdfHilfe.js'
|
||||
|
||||
useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' })
|
||||
const { ladeSchriften } = await import('../server/services/auftraege/pdf.js')
|
||||
|
||||
// Prüft die Testhilfe selbst an einem kleinen, direkt mit pdfkit gebauten PDF (unabhängig vom AB-Code)
|
||||
function kleinesPdf(komprimieren) {
|
||||
const { regular, semibold } = ladeSchriften()
|
||||
const doc = new PDFDocument({ size: 'A4', compress: komprimieren, info: { CreationDate: new Date('2026-10-06T10:00:00.000Z') } })
|
||||
doc.registerFont('Figtree', regular)
|
||||
doc.registerFont('Figtree-SemiBold', semibold)
|
||||
const teile = []
|
||||
doc.on('data', (teil) => teile.push(teil))
|
||||
const fertig = new Promise((resolve) => doc.on('end', () => resolve(Buffer.concat(teile))))
|
||||
doc.font('Figtree').fontSize(12).text(`Müller & Söhne – ß € ${String.fromCodePoint(0x2212)}5`, 50, 50, { lineBreak: false })
|
||||
doc.font('Figtree-SemiBold').fontSize(12).text('Zweite Zeile „fett“', 50, 80, { lineBreak: false })
|
||||
doc.addPage().font('Figtree').fontSize(12).text('Seite zwei', 50, 50, { lineBreak: false })
|
||||
doc.end()
|
||||
return fertig
|
||||
}
|
||||
|
||||
test('leseSeiten und seitenZahl: Text je Seite aus einem pdfkit-PDF, komprimiert und unkomprimiert', async () => {
|
||||
for (const komprimieren of [true, false]) {
|
||||
const pdf = await kleinesPdf(komprimieren)
|
||||
assert.equal(seitenZahl(pdf), 2)
|
||||
assert.deepEqual(leseSeiten(pdf), [[`Müller & Söhne – ß € ${String.fromCodePoint(0x2212)}5`, 'Zweite Zeile „fett“'], ['Seite zwei']])
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user