diff --git a/.github/workflows/update-bentopdf-viewer.yml b/.github/workflows/update-bentopdf-viewer.yml index f7bf4206..eafe9f0c 100644 --- a/.github/workflows/update-bentopdf-viewer.yml +++ b/.github/workflows/update-bentopdf-viewer.yml @@ -24,10 +24,10 @@ jobs: fi echo "version=$CUR" >> "$GITHUB_OUTPUT" - - name: Read latest commit from our viewer fork (v2 branch) + - name: Read latest commit from our viewer fork id: upstream-version run: | - LATEST=$(git ls-remote https://github.com/alam00000/bentopdf-pdfium-viewer refs/heads/v2 | cut -c1-12) + LATEST=$(git ls-remote https://github.com/alam00000/bentopdf-pdfium-viewer refs/heads/main | cut -c1-12) echo "version=$LATEST" >> "$GITHUB_OUTPUT" - name: Should update? @@ -53,13 +53,13 @@ jobs: - name: Clone our viewer fork if: steps.gate.outputs.run == 'true' - run: git clone --depth 1 --branch v2 https://github.com/alam00000/bentopdf-pdfium-viewer ./_fork/bentopdf-pdfium-viewer + run: git clone --depth 1 --branch main https://github.com/alam00000/bentopdf-pdfium-viewer ./_fork/bentopdf-pdfium-viewer - name: Setup Node if: steps.gate.outputs.run == 'true' uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm - name: Install upstream deps @@ -127,7 +127,7 @@ jobs: - name: Cleanup upstream clone if: steps.gate.outputs.run == 'true' - run: rm -rf ./_upstream + run: rm -rf ./_fork - name: Create Pull Request if: steps.gate.outputs.run == 'true' @@ -136,7 +136,7 @@ jobs: commit-message: "build(deps): bump bentopdf-viewer from ${{ steps.current-version.outputs.version }} to ${{ steps.upstream-version.outputs.version }}" title: "build(deps): bump bentopdf-viewer from ${{ steps.current-version.outputs.version }} to ${{ steps.upstream-version.outputs.version }}" body: | - - Build snippet from upstream embed-pdf-viewer via `npm run build:snippet` + - Build snippet from the BentoPDF viewer fork via `pnpm run build:snippet` - Pack tarball into `vendor/bentopdf-viewer/` and point dependency to it - Refresh package-lock.json branch: chore/update-bentopdf-viewer diff --git a/package-lock.json b/package-lock.json index 567f45a5..b22a0d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4272,7 +4272,7 @@ "node_modules/bentopdf-viewer": { "version": "2.9.1", "resolved": "file:vendor/bentopdf-viewer/bentopdf-viewer-2.9.1.tgz", - "integrity": "sha512-sfzgEpggMof2srTALARXtDJ0ZCzyEIClPsl8J0O4oHBWfuuT0LWvDMWAHaWuBXXIHa307RfboeK3ORy1cJO9Hw==", + "integrity": "sha512-WCKztFUzEuWpqvLu8UeXN1aEWgUUNHmV0K+d1PPS9DwCXEoc0d8K0qO59fMHxim3Xr2jaD7i8gECW6iiEcmBBA==", "license": "MIT", "dependencies": { "preact": "^10.17.0", diff --git a/package.json b/package.json index ad3a7803..38ec47e9 100644 --- a/package.json +++ b/package.json @@ -90,12 +90,12 @@ "@types/node-forge": "^1.3.14", "@types/papaparse": "^5.5.2", "archiver": "^7.0.1", + "bentopdf-viewer": "file:vendor/bentopdf-viewer/bentopdf-viewer-2.9.1.tgz", "blob-stream": "^0.1.3", "bwip-js": "^4.8.0", "cropperjs": "^1.6.2", "diff": "^8.0.3", "dompurify": "^3.4.12", - "bentopdf-viewer": "file:vendor/bentopdf-viewer/bentopdf-viewer-2.9.1.tgz", "fast-xml-parser": "^5.7.1", "heic2any": "^0.0.4", "highlight.js": "^11.11.1", diff --git a/src/js/config/editor-fonts.ts b/src/js/config/editor-fonts.ts new file mode 100644 index 00000000..6e45ba54 --- /dev/null +++ b/src/js/config/editor-fonts.ts @@ -0,0 +1,53 @@ +const FONT_CDN = + import.meta.env.VITE_EMBEDPDF_FONTS_URL || + 'https://cdn.jsdelivr.net/npm/@embedpdf'; + +const CHARSET = { + ANSI: 0, + DEFAULT: 1, + SHIFTJIS: 128, + HANGEUL: 129, + GB2312: 134, + CHINESEBIG5: 136, + GREEK: 161, + TURKISH: 162, + VIETNAMESE: 163, + HEBREW: 177, + ARABIC: 178, + BALTIC: 186, + RUSSIAN: 204, + THAI: 222, + EASTEUROPE: 238, +} as const; + +const PACK = { + jp: 'fonts-jp@1.0.0/fonts/NotoSansJP-Regular.otf', + kr: 'fonts-kr@1.0.0/fonts/NotoSansKR-Regular.otf', + sc: 'fonts-sc@1.0.0/fonts/NotoSansHans-Regular.otf', + tc: 'fonts-tc@1.0.0/fonts/NotoSansHant-Regular.otf', + arabic: 'fonts-arabic@1.0.0/fonts/NotoNaskhArabic-Regular.ttf', + hebrew: 'fonts-hebrew@1.0.0/fonts/NotoSansHebrew-Regular.ttf', + latin: 'fonts-latin@1.0.0/fonts/NotoSans-Regular.ttf', +} as const; + +export const editorFontFallback = { + baseUrl: FONT_CDN, + fonts: { + [CHARSET.SHIFTJIS]: PACK.jp, + [CHARSET.HANGEUL]: PACK.kr, + [CHARSET.GB2312]: PACK.sc, + [CHARSET.CHINESEBIG5]: PACK.tc, + [CHARSET.ARABIC]: PACK.arabic, + [CHARSET.HEBREW]: PACK.hebrew, + [CHARSET.GREEK]: PACK.latin, + [CHARSET.TURKISH]: PACK.latin, + [CHARSET.VIETNAMESE]: PACK.latin, + [CHARSET.BALTIC]: PACK.latin, + [CHARSET.RUSSIAN]: PACK.latin, + [CHARSET.EASTEUROPE]: PACK.latin, + [CHARSET.THAI]: PACK.latin, + [CHARSET.ANSI]: PACK.latin, + [CHARSET.DEFAULT]: PACK.latin, + }, + defaultFont: PACK.latin, +}; diff --git a/src/js/logic/edit-pdf-page.ts b/src/js/logic/edit-pdf-page.ts index d0a62ab0..8d976d75 100644 --- a/src/js/logic/edit-pdf-page.ts +++ b/src/js/logic/edit-pdf-page.ts @@ -5,6 +5,7 @@ import { formatBytes, downloadFile } from '../utils/helpers.js'; import { makeUniqueFileKey } from '../utils/deduplicate-filename.js'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { getEditorDisabledCategories } from '../utils/disabled-tools.js'; +import { editorFontFallback } from '../config/editor-fonts.js'; const embedPdfWasmUrl = new URL( 'bentopdf-viewer/dist/pdfium.wasm', @@ -12,7 +13,47 @@ const embedPdfWasmUrl = new URL( ).href; import type { EmbedPdfContainer } from 'bentopdf-viewer'; -import type { DocManagerPlugin } from '@/types'; +import type { + AnnotationPluginLite, + DocManagerPlugin, + FreeTextSystemFontAnnotation, +} from '@/types'; + +const FREETEXT_SUBTYPE = 3; + +function collectSystemFontFreeTexts( + annotationPlugin: AnnotationPluginLite | null +): FreeTextSystemFontAnnotation[] { + if (!annotationPlugin) return []; + try { + const state = annotationPlugin.getState(); + const out: FreeTextSystemFontAnnotation[] = []; + for (const tracked of Object.values(state.byUid)) { + const obj = tracked.object; + if (obj.type !== FREETEXT_SUBTYPE) continue; + if (obj.intent === 'FreeTextCallout') continue; + if (!obj.fontPostScriptName || !obj.fontPostScriptName.trim()) continue; + if (!obj.id || obj.pageIndex == null || !obj.rect) continue; + if ((obj.rotation ?? 0) !== 0) continue; + out.push({ + id: obj.id, + pageIndex: obj.pageIndex, + contents: obj.contents ?? '', + fontSize: obj.fontSize ?? 12, + fontColor: obj.fontColor ?? '#000000', + textAlign: obj.textAlign ?? 0, + verticalAlign: obj.verticalAlign ?? 0, + opacity: obj.opacity ?? 1, + backgroundColor: obj.color ?? obj.backgroundColor, + rect: obj.rect, + fontPostScriptName: obj.fontPostScriptName, + }); + } + return out; + } catch { + return []; + } +} let viewerInstance: EmbedPdfContainer | null = null; let docManagerPlugin: DocManagerPlugin | null = null; @@ -143,6 +184,7 @@ async function handleFiles(files: FileList) { target: pdfContainer, worker: true, wasmUrl: embedPdfWasmUrl, + fontFallback: editorFontFallback, export: { defaultFileName: firstFile.name, }, @@ -218,7 +260,31 @@ async function handleFiles(files: FileList) { try { const exportPlugin = registry.getPlugin('export').provides(); const arrayBuffer = await exportPlugin.saveAsCopy().toPromise(); - const blob = new Blob([arrayBuffer], { type: 'application/pdf' }); + let outBytes = new Uint8Array(arrayBuffer); + let annotationPlugin: AnnotationPluginLite | null = null; + try { + annotationPlugin = registry + .getPlugin('annotation') + .provides() as unknown as AnnotationPluginLite; + } catch { + annotationPlugin = null; + } + const customFontAnnots = collectSystemFontFreeTexts(annotationPlugin); + if (customFontAnnots.length > 0) { + try { + const { embedFreeTextSystemFonts } = + await import('../utils/freetext-font-embed.js'); + outBytes = await embedFreeTextSystemFonts( + outBytes, + customFontAnnots + ); + } catch (err) { + console.error('Font embed pass failed:', err); + } + } + const blob = new Blob([new Uint8Array(outBytes)], { + type: 'application/pdf', + }); downloadFile(blob, currentFileName); } catch (err) { console.error('Error downloading PDF:', err); diff --git a/src/js/types/edit-pdf-type.ts b/src/js/types/edit-pdf-type.ts index a6d80671..4e529366 100644 --- a/src/js/types/edit-pdf-type.ts +++ b/src/js/types/edit-pdf-type.ts @@ -13,3 +13,48 @@ export interface DocManagerPlugin { closeDocument: (id: string) => void; saveAsCopy: (id: string) => Promise; } + +export interface EditorAnnotationRect { + origin: { x: number; y: number }; + size: { width: number; height: number }; +} + +export interface EditorAnnotationObjectLite { + id?: string; + pageIndex?: number; + type?: number; + intent?: string; + contents?: string; + fontSize?: number; + fontColor?: string; + textAlign?: number; + verticalAlign?: number; + opacity?: number; + color?: string; + backgroundColor?: string; + rect?: EditorAnnotationRect; + rotation?: number; + fontFamilyName?: string; + fontPostScriptName?: string; + fontFaceName?: string; +} + +export interface AnnotationPluginLite { + getState: () => { + byUid: Record; + }; +} + +export interface FreeTextSystemFontAnnotation { + id: string; + pageIndex: number; + contents: string; + fontSize: number; + fontColor: string; + textAlign: number; + verticalAlign: number; + opacity: number; + backgroundColor?: string; + rect: EditorAnnotationRect; + fontPostScriptName: string; +} diff --git a/src/js/utils/freetext-font-embed.ts b/src/js/utils/freetext-font-embed.ts new file mode 100644 index 00000000..e1b72ad3 --- /dev/null +++ b/src/js/utils/freetext-font-embed.ts @@ -0,0 +1,445 @@ +import { + PDFDocument, + PDFDict, + PDFName, + PDFString, + PDFHexString, + PDFFont, + PDFOperator, + beginText, + endText, + moveText, + setFontAndSize, + setFillingRgbColor, + showText, + pushGraphicsState, + popGraphicsState, + rectangle, + fill, +} from 'pdf-lib'; +import fontkit from '@pdf-lib/fontkit'; +import type { FreeTextSystemFontAnnotation } from '@/types'; + +interface LocalFontQueryResult { + postscriptName: string; + blob: () => Promise; +} + +type QueryLocalFonts = (opts?: { + postscriptNames?: string[]; +}) => Promise; + +export function extractTtcFace( + bytes: Uint8Array, + postscriptName: string +): Uint8Array { + const u16 = (o: number) => ((bytes[o] ?? 0) << 8) | (bytes[o + 1] ?? 0); + const u32 = (o: number) => + (((bytes[o] ?? 0) << 24) | + ((bytes[o + 1] ?? 0) << 16) | + ((bytes[o + 2] ?? 0) << 8) | + (bytes[o + 3] ?? 0)) >>> + 0; + if (bytes.length < 16 || u32(0) !== 0x74746366) return bytes; + + const numFonts = u32(8); + if (numFonts === 0 || numFonts > 64) return bytes; + const want = postscriptName.replace(/[\s-]/g, '').toLowerCase(); + + const nameOf = (faceOff: number, nameId: number): string => { + const numTables = u16(faceOff + 4); + for (let i = 0; i < numTables; i++) { + const rec = faceOff + 12 + i * 16; + const tag = String.fromCharCode( + bytes[rec] ?? 0, + bytes[rec + 1] ?? 0, + bytes[rec + 2] ?? 0, + bytes[rec + 3] ?? 0 + ); + if (tag !== 'name') continue; + const nt = u32(rec + 8); + const count = u16(nt + 2); + const strBase = nt + u16(nt + 4); + for (let r = 0; r < count; r++) { + const nr = nt + 6 + r * 12; + if (u16(nr + 6) !== nameId) continue; + const platform = u16(nr); + const len = u16(nr + 8); + const off = strBase + u16(nr + 10); + if (platform === 3 || platform === 0) { + let out = ''; + for (let k = 0; k + 1 < len; k += 2) + out += String.fromCharCode(u16(off + k)); + return out; + } + let out = ''; + for (let k = 0; k < len; k++) + out += String.fromCharCode(bytes[off + k] ?? 0); + return out; + } + } + return ''; + }; + + let best = u32(12); + let bestScore = -1; + for (let f = 0; f < numFonts; f++) { + const off = u32(12 + f * 4); + if (off + 12 > bytes.length) continue; + const ps = nameOf(off, 6).replace(/[\s-]/g, '').toLowerCase(); + const fam = nameOf(off, 1).replace(/[\s-]/g, '').toLowerCase(); + let score = 0; + if (want && ps === want) score = 4; + else if (want && fam && (want.startsWith(fam) || fam.startsWith(want))) + score = 2; + if (score > bestScore) { + bestScore = score; + best = off; + } + } + + const numTables = u16(best + 4); + if (numTables === 0 || numTables > 64) return bytes; + const records: Array<{ tag: string; sum: number; off: number; len: number }> = + []; + for (let i = 0; i < numTables; i++) { + const rec = best + 12 + i * 16; + records.push({ + tag: String.fromCharCode( + bytes[rec] ?? 0, + bytes[rec + 1] ?? 0, + bytes[rec + 2] ?? 0, + bytes[rec + 3] ?? 0 + ), + sum: u32(rec + 4), + off: u32(rec + 8), + len: u32(rec + 12), + }); + } + const headerSize = 12 + numTables * 16; + let total = headerSize; + const placed = new Map(); + for (const r of records) { + const key = r.off + ':' + r.len; + if (!placed.has(key)) { + placed.set(key, total); + total += (r.len + 3) & ~3; + } + } + const out = new Uint8Array(total); + const w16 = (o: number, v: number) => { + out[o] = (v >> 8) & 0xff; + out[o + 1] = v & 0xff; + }; + const w32 = (o: number, v: number) => { + out[o] = (v >>> 24) & 0xff; + out[o + 1] = (v >>> 16) & 0xff; + out[o + 2] = (v >>> 8) & 0xff; + out[o + 3] = v & 0xff; + }; + w32(0, u32(best)); + w16(4, numTables); + let pow2 = 1; + let entSel = 0; + while (pow2 * 2 <= numTables) { + pow2 *= 2; + entSel++; + } + w16(6, pow2 * 16); + w16(8, entSel); + w16(10, (numTables - pow2) * 16); + records.forEach((r, i) => { + const rec = 12 + i * 16; + for (let k = 0; k < 4; k++) out[rec + k] = r.tag.charCodeAt(k); + w32(rec + 4, r.sum); + const newOff = placed.get(r.off + ':' + r.len); + if (newOff === undefined) return; + w32(rec + 8, newOff); + w32(rec + 12, r.len); + out.set(bytes.subarray(r.off, r.off + r.len), newOff); + if (r.tag === 'head' && r.len >= 12) w32(newOff + 8, 0); + }); + return out; +} + +async function fetchFontBytes( + postscriptName: string +): Promise { + const qlf = (window as unknown as { queryLocalFonts?: QueryLocalFonts }) + .queryLocalFonts; + if (typeof qlf !== 'function') return null; + try { + const list = await qlf({ postscriptNames: [postscriptName] }); + if (list.length === 0) return null; + const blob = await list[0].blob(); + return extractTtcFace( + new Uint8Array(await blob.arrayBuffer()), + postscriptName + ); + } catch { + return null; + } +} + +function subsetPrefix(postscriptName: string): string { + let h = 0x811c9dc5; + for (const c of postscriptName) { + h = Math.imul(h ^ c.charCodeAt(0), 0x01000193); + } + let prefix = ''; + for (let i = 0; i < 6; i++) { + prefix += String.fromCharCode(65 + (((h >>> (i * 4)) & 0x0f) % 26)); + h = Math.imul(h, 0x01000193); + } + return prefix; +} + +function hexToRgb(hex: string): { r: number; g: number; b: number } { + const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); + return m + ? { + r: parseInt(m[1], 16) / 255, + g: parseInt(m[2], 16) / 255, + b: parseInt(m[3], 16) / 255, + } + : { r: 0, g: 0, b: 0 }; +} + +interface EmbeddedFontMetrics { + ascent: number; + descent: number; + unitsPerEm: number; +} + +function fontMetrics(font: PDFFont): EmbeddedFontMetrics { + const carrier = font as unknown as { + embedder?: { + font?: { ascent?: number; descent?: number; unitsPerEm?: number }; + }; + }; + const f = carrier.embedder?.font; + const unitsPerEm = + typeof f?.unitsPerEm === 'number' && f.unitsPerEm > 0 ? f.unitsPerEm : 1000; + const ascent = + typeof f?.ascent === 'number' && f.ascent > 0 ? f.ascent : unitsPerEm * 0.8; + const descent = + typeof f?.descent === 'number' && f.descent <= 0 + ? f.descent + : -unitsPerEm * 0.2; + return { ascent, descent, unitsPerEm }; +} + +function breakLongToken( + font: PDFFont, + token: string, + size: number, + maxWidth: number, + out: string[] +): string { + let cur = ''; + for (const ch of token) { + const candidate = cur + ch; + if (cur !== '' && font.widthOfTextAtSize(candidate, size) > maxWidth) { + out.push(cur); + cur = ch; + } else { + cur = candidate; + } + } + return cur; +} + +function wrapLine( + font: PDFFont, + text: string, + size: number, + maxWidth: number +): string[] { + if (!text) return ['']; + const tokens = text.match(/\s+|\S+/g) ?? []; + const out: string[] = []; + let cur = ''; + for (const token of tokens) { + if (/^\s+$/.test(token)) { + cur += token; + continue; + } + const candidate = cur + token; + if (font.widthOfTextAtSize(candidate.trimEnd(), size) <= maxWidth) { + cur = candidate; + continue; + } + if (cur.trim() !== '') { + out.push(cur.trimEnd()); + } + if (font.widthOfTextAtSize(token, size) <= maxWidth) { + cur = token; + } else { + cur = breakLongToken(font, token, size, maxWidth, out); + } + } + out.push(cur.trimEnd()); + return out; +} + +function findFreeTextDict( + doc: PDFDocument, + pageIndex: number, + id: string +): PDFDict | null { + if (pageIndex < 0 || pageIndex >= doc.getPageCount()) return null; + const annots = doc.getPage(pageIndex).node.Annots(); + if (!annots) return null; + for (let i = 0; i < annots.size(); i++) { + const obj = annots.lookup(i); + if (!(obj instanceof PDFDict)) continue; + const subtype = obj.lookup(PDFName.of('Subtype')); + if (!(subtype instanceof PDFName) || subtype.decodeText() !== 'FreeText') { + continue; + } + const nm = obj.lookup(PDFName.of('NM')); + const nmStr = + nm instanceof PDFString + ? nm.asString() + : nm instanceof PDFHexString + ? nm.decodeText() + : ''; + if (nmStr === id) return obj; + } + return null; +} + +function bakeAppearance( + doc: PDFDocument, + dict: PDFDict, + annot: FreeTextSystemFontAnnotation, + font: PDFFont +): void { + const w = annot.rect.size.width; + const h = annot.rect.size.height; + if (!(w > 0) || !(h > 0)) return; + const size = annot.fontSize > 0 ? annot.fontSize : 12; + const lineHeight = size * 1.18; + const metrics = fontMetrics(font); + const scalePerUnit = size / metrics.unitsPerEm; + const maxWidth = Math.max(w, size); + + const lines: string[] = []; + for (const paragraph of (annot.contents ?? '').split(/\r\n?|\n/)) { + lines.push(...wrapLine(font, paragraph, size, maxWidth)); + } + + const blockHeight = lines.length * lineHeight; + const vOffset = + annot.verticalAlign === 1 + ? Math.max(0, (h - blockHeight) / 2) + : annot.verticalAlign === 2 + ? Math.max(0, h - blockHeight) + : 0; + const contentHeight = (metrics.ascent - metrics.descent) * scalePerUnit; + const halfLeading = (lineHeight - contentHeight) / 2; + const firstBaselineFromTop = + vOffset + halfLeading + metrics.ascent * scalePerUnit; + + const color = hexToRgb(annot.fontColor || '#000000'); + const bg = + annot.backgroundColor && + /^#?[a-f\d]{6}$/i.test(annot.backgroundColor.trim()) + ? hexToRgb(annot.backgroundColor.trim()) + : null; + + const ops: PDFOperator[] = [pushGraphicsState()]; + if (bg) { + ops.push( + setFillingRgbColor(bg.r, bg.g, bg.b), + rectangle(0, 0, w, h), + fill() + ); + } + ops.push( + beginText(), + setFontAndSize('F0', size), + setFillingRgbColor(color.r, color.g, color.b) + ); + let prevX = 0; + let prevY = 0; + lines.forEach((line, i) => { + if (!line) return; + const lineWidth = font.widthOfTextAtSize(line, size); + const x = + annot.textAlign === 1 + ? Math.max(0, (w - lineWidth) / 2) + : annot.textAlign === 2 + ? Math.max(0, w - lineWidth) + : 0; + const y = h - (firstBaselineFromTop + i * lineHeight); + ops.push(moveText(x - prevX, y - prevY), showText(font.encodeText(line))); + prevX = x; + prevY = y; + }); + ops.push(endText(), popGraphicsState()); + + const xobj = doc.context.formXObject(ops, { + BBox: [0, 0, w, h], + Resources: { Font: { F0: font.ref } }, + }); + const ref = doc.context.register(xobj); + dict.set(PDFName.of('AP'), doc.context.obj({ N: ref })); +} + +export async function embedFreeTextSystemFonts( + bytes: Uint8Array, + annotations: FreeTextSystemFontAnnotation[] +): Promise { + const candidates = annotations.filter( + (a) => a.fontPostScriptName.trim() !== '' + ); + if (candidates.length === 0) return bytes; + + const doc = await PDFDocument.load(bytes, { + ignoreEncryption: true, + updateMetadata: false, + }); + doc.registerFontkit(fontkit); + + const fontCache = new Map(); + const resolveFont = async ( + postscriptName: string + ): Promise => { + const cached = fontCache.get(postscriptName); + if (cached !== undefined) return cached; + const fontBytes = await fetchFontBytes(postscriptName); + if (!fontBytes) { + fontCache.set(postscriptName, null); + return null; + } + try { + const font = await doc.embedFont(fontBytes, { + subset: true, + customName: `${subsetPrefix(postscriptName)}+${postscriptName}`, + }); + fontCache.set(postscriptName, font); + return font; + } catch { + fontCache.set(postscriptName, null); + return null; + } + }; + + let touched = false; + for (const annot of candidates) { + try { + const font = await resolveFont(annot.fontPostScriptName); + if (!font) continue; + const dict = findFreeTextDict(doc, annot.pageIndex, annot.id); + if (!dict) continue; + bakeAppearance(doc, dict, annot, font); + touched = true; + } catch { + continue; + } + } + + if (!touched) return bytes; + return doc.save(); +} diff --git a/vendor/bentopdf-viewer/.upstream-version b/vendor/bentopdf-viewer/.upstream-version index dedcc7d4..3389253d 100644 --- a/vendor/bentopdf-viewer/.upstream-version +++ b/vendor/bentopdf-viewer/.upstream-version @@ -1 +1 @@ -2.9.1 +66b57252 diff --git a/vendor/bentopdf-viewer/bentopdf-viewer-2.9.1.tgz b/vendor/bentopdf-viewer/bentopdf-viewer-2.9.1.tgz index 8d5ae618..c3937f5b 100644 Binary files a/vendor/bentopdf-viewer/bentopdf-viewer-2.9.1.tgz and b/vendor/bentopdf-viewer/bentopdf-viewer-2.9.1.tgz differ