spb/autoscholar
Public
1import { PDFDict, PDFDocument, PDFName } from "pdf-lib";23export interface PdfInspection {4 ok: boolean;5 pages: number;6 text: string;7 textChars: number;8 fonts: { total: number; embedded: number; names: string[]; unembedded: string[] };9 errors: string[];10 title?: string;11}1213export async function inspectPdf(buf: Buffer): Promise<PdfInspection> {14 const errors: string[] = [];15 let pages = 0;16 const fonts = { total: 0, embedded: 0, names: [] as string[], unembedded: [] as string[] };17 let title: string | undefined;18 try {19 const doc = await PDFDocument.load(buf, { ignoreEncryption: true, updateMetadata: false });20 pages = doc.getPageCount();21 title = doc.getTitle();22 const seen = new Set<string>();23 for (const page of doc.getPages()) {24 const res = page.node.Resources();25 const fontDict = res?.lookup(PDFName.of("Font"));26 if (!(fontDict instanceof PDFDict)) continue;27 for (const [, ref] of fontDict.entries()) {28 const font = doc.context.lookup(ref);29 if (!(font instanceof PDFDict)) continue;30 const base = font.lookup(PDFName.of("BaseFont"))?.toString() ?? "?";31 if (seen.has(base)) continue;32 seen.add(base);33 let desc = font.lookup(PDFName.of("FontDescriptor"));34 if (!(desc instanceof PDFDict)) {35 const descendants = font.lookup(PDFName.of("DescendantFonts")) as unknown as36 | { get?: (i: number) => unknown }37 | undefined;38 const first = descendants && typeof descendants.get === "function" ? descendants.get(0) : undefined;39 const df = first ? doc.context.lookup(first as never) : undefined;40 if (df instanceof PDFDict) desc = df.lookup(PDFName.of("FontDescriptor"));41 }42 const d = desc instanceof PDFDict ? desc : undefined;43 const embedded = !!(44 d &&45 (d.has(PDFName.of("FontFile")) || d.has(PDFName.of("FontFile2")) || d.has(PDFName.of("FontFile3")))46 );47 const subtype = font.lookup(PDFName.of("Subtype"))?.toString();48 const isType3 = subtype === "/Type3";49 fonts.total++;50 fonts.names.push(base);51 if (embedded || isType3) fonts.embedded++;52 else fonts.unembedded.push(base);53 }54 }55 } catch (e) {56 errors.push(`pdf-lib parse failed: ${(e as Error).message}`);57 }58 let text = "";59 try {60 const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");61 const task = pdfjs.getDocument({62 data: new Uint8Array(buf),63 useSystemFonts: true,64 disableFontFace: true,65 verbosity: 0,66 });67 const pdf = await task.promise;68 pages = pages || pdf.numPages;69 const parts: string[] = [];70 for (let i = 1; i <= pdf.numPages; i++) {71 const page = await pdf.getPage(i);72 const content = await page.getTextContent();73 parts.push(content.items.map((it) => ("str" in it ? it.str : "")).join(" "));74 }75 text = parts.join("\n");76 await task.destroy();77 } catch (e) {78 errors.push(`pdfjs text extraction failed: ${(e as Error).message}`);79 }80 const textChars = text.replace(/\s+/g, "").length;81 if (pages === 0) errors.push("no pages");82 if (textChars < 500) errors.push(`too little selectable text (${textChars} chars)`);83 return { ok: errors.length === 0, pages, text, textChars, fonts, errors, title };84}85