AB-PDF: Zeichen ohne Glyph werden ersetzt statt als Kästchen gedruckt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae6d84b2a4
commit
b097fedf5a
2 files changed
+76
No files matched your search
@@ -36,3 +36,43 @@ export function hatGlyph(codePoint) {
|
||||
const { regularFont, semiboldFont } = ladeSchriften()
|
||||
return regularFont.hasGlyphForCodePoint(codePoint) && semiboldFont.hasGlyphForCodePoint(codePoint)
|
||||
}
|
||||
|
||||
const ERSATZ = new Map([
|
||||
[0x2010, '-'], [0x2011, '-'], [0x2012, '-'], [0x2013, '-'], [0x2014, '-'], [0x2015, '-'], [0x2212, '-'],
|
||||
[0x2018, "'"], [0x2019, "'"], [0x201a, "'"], [0x2032, "'"],
|
||||
[0x201c, '"'], [0x201d, '"'], [0x201e, '"'], [0x201f, '"'], [0x2033, '"'],
|
||||
[0x2026, '...'], [0x20ac, 'EUR'], [0x2022, '-'], [0x00b7, '-'], [0x00d7, 'x'], [0x2192, '->'],
|
||||
])
|
||||
// Steuerzeichen (außer \n), weiche Trennung, Zero-Width-Zeichen, Variantenselektoren, BOM
|
||||
const ENTFERNEN = /[\u{0}-\u{8}\u{B}-\u{1F}\u{7F}-\u{9F}\u{AD}\u{200B}-\u{200D}\u{2060}\u{FE00}-\u{FE0F}\u{FEFF}]/gu
|
||||
|
||||
/** Ersetzt Zeichen ohne Glyph, damit nie ein leeres Kästchen im PDF steht. */
|
||||
export function bereinigeText(text, hatGlyphFn = hatGlyph) {
|
||||
const roh = String(text ?? '')
|
||||
.normalize('NFC')
|
||||
.replace(/\r\n?|[\u{2028}\u{2029}]/gu, '\n')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(ENTFERNEN, '')
|
||||
let aus = ''
|
||||
for (const zeichen of roh) {
|
||||
const cp = zeichen.codePointAt(0)
|
||||
if (zeichen === '\n' || hatGlyphFn(cp)) aus += zeichen
|
||||
else if (ERSATZ.has(cp)) aus += ERSATZ.get(cp)
|
||||
else if (/\p{Zs}/u.test(zeichen)) aus += ' '
|
||||
else if (/\p{M}/u.test(zeichen)) continue
|
||||
else {
|
||||
const basis = zeichen.normalize('NFD').codePointAt(0)
|
||||
aus += basis !== cp && hatGlyphFn(basis) ? String.fromCodePoint(basis) : '?'
|
||||
}
|
||||
}
|
||||
return aus
|
||||
}
|
||||
|
||||
/** bereinigeText für jede Zeichenkette in beliebig verschachtelten Objekten und Listen. */
|
||||
export function bereinigeTexte(wert, hatGlyphFn = hatGlyph) {
|
||||
if (typeof wert === 'string') return bereinigeText(wert, hatGlyphFn)
|
||||
if (Array.isArray(wert)) return wert.map((e) => bereinigeTexte(e, hatGlyphFn))
|
||||
if (wert && typeof wert === 'object') return Object.fromEntries(Object.entries(wert).map(([k, v]) => [k, bereinigeTexte(v, hatGlyphFn)]))
|
||||
return wert
|
||||
}
|
||||
|
||||
@@ -39,3 +39,39 @@ test('hatGlyph: deutsche Buchstaben, €, Minuszeichen, geschütztes Leerzeichen
|
||||
for (const zeichen of noetig) assert.ok(hatGlyph(zeichen.codePointAt(0)), `Glyph fehlt: U+${zeichen.codePointAt(0).toString(16)}`)
|
||||
for (const code of [0x202f, 0x2009, 0x200b, 0x416, 0x1f600, 0x3042]) assert.equal(hatGlyph(code), false, `U+${code.toString(16)} sollte fehlen`)
|
||||
})
|
||||
test('bereinigeText: vorhandene Zeichen bleiben unverändert, getrennte Akzente werden zusammengesetzt', () => {
|
||||
const text = `Müller & Söhne – „Größe“ 1.234,56${NBSP}€ ${cp(0x2212)} 20,00${NBSP}€ ÄÖÜ äöü ß … Łukasz Yıldız Çelik`
|
||||
assert.equal(bereinigeText(text), text)
|
||||
assert.equal(bereinigeText('a' + cp(0x301)), 'á'.normalize('NFC'))
|
||||
assert.equal(bereinigeText(null), '')
|
||||
assert.equal(bereinigeText(undefined), '')
|
||||
assert.equal(bereinigeText(42), '42')
|
||||
})
|
||||
|
||||
test('bereinigeText: Steuerzeichen, Zero-Width, weiche Trennung und BOM entfallen; Zeilenenden werden Zeilenumbrüche, Tab wird Leerzeichen', () => {
|
||||
const roh = `a${cp(0x200b)}b${cp(0xad)}c${cp(0xfeff)}d${cp(0)}e\tf\r\ng\rh${cp(0x2028)}i${cp(0x200d, 0xfe0f)}j`
|
||||
assert.equal(bereinigeText(roh), 'abcde f\ng\nh\nij')
|
||||
})
|
||||
|
||||
test('bereinigeText mit Figtree: schmale Leerzeichen werden Leerzeichen, kyrillische und japanische Zeichen sowie Emoji je ein Fragezeichen', () => {
|
||||
assert.equal(bereinigeText(`a${cp(0x202f)}b${cp(0x2009)}c`), 'a b c')
|
||||
assert.equal(bereinigeText(`${cp(0x416)}${cp(0x3042)}${cp(0x1f600)}`), '???')
|
||||
})
|
||||
|
||||
test('bereinigeText: Ersatzregeln bei fehlendem Glyph (Schrift ohne Zeichen über ASCII)', () => {
|
||||
const nurAscii = (code) => code < 0x80
|
||||
const fall = (eingabe) => bereinigeText(eingabe, nurAscii)
|
||||
assert.equal(fall('9,00' + NBSP + '€'), '9,00 EUR')
|
||||
assert.equal(fall(`${cp(0x201e)}x${cp(0x201c)} ${cp(0x201a)}y${cp(0x2019)}`), `"x" 'y'`)
|
||||
assert.equal(fall(`1 ${cp(0x2013)} 2 ${cp(0x2212)} 3 ${cp(0x2014)} 4`), '1 - 2 - 3 - 4')
|
||||
assert.equal(fall(`wait${cp(0x2026)} 3${cp(0xd7)}4 a${cp(0x2192)}b`), 'wait... 3x4 a->b')
|
||||
assert.equal(fall('Müller Çelik Ångström'), 'Muller Celik Angstrom')
|
||||
assert.equal(fall('Łukasz Ğ'), '?ukasz G')
|
||||
assert.equal(fall('e' + cp(0x301) + cp(0x301)), 'e')
|
||||
assert.equal(fall(cp(0x301)), '')
|
||||
})
|
||||
|
||||
test('bereinigeTexte: bearbeitet jede Zeichenkette in verschachtelten Objekten und Listen, andere Werte bleiben', () => {
|
||||
const roh = { a: ['x' + cp(0x200b) + 'y', { b: 'ż' + cp(0x1f600) }], n: 5, f: null, t: true }
|
||||
assert.deepEqual(bereinigeTexte(roh), { a: ['xy', { b: 'ż?' }], n: 5, f: null, t: true })
|
||||
})
|
||||
Reference in new issue
Block a user