SPB Git

spb/autoscholar

Public
3.3 KB · 85 lines typescript
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
Files · main
Menu

Theme