src / extractors / pdfExtractor.ts
src / extractors / pdfExtractor.ts
import pdfParse from "pdf-parse";
import { ExtractedWebpage } from "../cache/WebpageCache";
import { normalizeWhitespace } from "./htmlExtractor";
export interface PdfExtractResult {
text: string;
pageCount: number;
info: {
title?: string;
author?: string;
creationDate?: string;
};
}
/**
* Evaluates whether a buffer starts with the PDF magic byte signature (%PDF-).
*/
export function isPdfBuffer(buffer: Buffer): boolean {
if (!buffer || buffer.length < 5) return false;
const header = buffer.subarray(0, 1024).toString("latin1");
return header.includes("%PDF-");
}
/**
* Battle-tested digital PDF text extractor powered by pdf-parse / Mozilla pdfjs-dist.
* Handles embedded fonts, CMap/ToUnicode tables, object streams, XRef tables, and all compression filters.
*/
export async function parsePdfBuffer(buffer: Buffer): Promise<PdfExtractResult> {
if (!buffer || buffer.length === 0) {
throw new Error("Empty PDF buffer");
}
if (!isPdfBuffer(buffer)) {
throw new Error("Invalid PDF file (missing %PDF- header signature)");
}
try {
const data = await pdfParse({ data: new Uint8Array(buffer) } as any, {
pagerender: (pageData: any) => {
return pageData.getTextContent().then((textContent: any) => {
let lastY: number | null = null;
let text = "";
for (const item of textContent.items) {
if (lastY !== null && Math.abs(item.transform[5] - lastY) > 5) {
text += "\n";
} else if (text.length > 0 && !text.endsWith(" ") && !text.endsWith("\n")) {
text += " ";
}
text += item.str;
lastY = item.transform[5];
}
return text;
});
},
});
const info: { title?: string; author?: string; creationDate?: string } = {};
if (data.info) {
if (data.info.Title) info.title = String(data.info.Title);
if (data.info.Author) info.author = String(data.info.Author);
if (data.info.CreationDate) info.creationDate = String(data.info.CreationDate);
}
const cleanText = data.text ? data.text.trim() : "";
if (!cleanText) {
throw new Error("No readable text content streams found in digital PDF");
}
return {
text: cleanText,
pageCount: data.numpages || 1,
info,
};
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`PDF parsing failed: ${msg}`);
}
}
/**
* Standard PDF Extractor integration into Browse ExtractedWebpage schema.
*/
export async function extractPdfContent(buffer: Buffer, url: string): Promise<ExtractedWebpage> {
const rawSizeBytes = buffer.length;
const pdfRes = await parsePdfBuffer(buffer);
const cleanBody = normalizeWhitespace(pdfRes.text);
const title = pdfRes.info.title || "PDF Document";
const headerLines: string[] = [`# ${title}`];
const metaDetails: string[] = [];
if (pdfRes.info.author) {
metaDetails.push(`**Author:** ${pdfRes.info.author}`);
}
metaDetails.push(`**Pages:** ${pdfRes.pageCount}`);
if (metaDetails.length > 0) {
headerLines.push(metaDetails.join(" | "));
}
const fullMarkdown = normalizeWhitespace(`${headerLines.join("\n\n")}\n\n${cleanBody}`);
return {
url,
metadata: {
title: pdfRes.info.title || "PDF Document",
author: pdfRes.info.author,
publishedDate: pdfRes.info.creationDate,
},
fullMarkdown,
totalChars: fullMarkdown.length,
isSparse: false,
rawHtmlLength: rawSizeBytes,
readabilityLength: cleanBody.length,
usedJsonLd: false,
usedOg: false,
tablesDetected: fullMarkdown.includes("| ---") || fullMarkdown.includes("| --- |"),
pdfPageCount: pdfRes.pageCount,
imageMarkers: [],
};
}
import pdfParse from "pdf-parse";
import { ExtractedWebpage } from "../cache/WebpageCache";
import { normalizeWhitespace } from "./htmlExtractor";
export interface PdfExtractResult {
text: string;
pageCount: number;
info: {
title?: string;
author?: string;
creationDate?: string;
};
}
/**
* Evaluates whether a buffer starts with the PDF magic byte signature (%PDF-).
*/
export function isPdfBuffer(buffer: Buffer): boolean {
if (!buffer || buffer.length < 5) return false;
const header = buffer.subarray(0, 1024).toString("latin1");
return header.includes("%PDF-");
}
/**
* Battle-tested digital PDF text extractor powered by pdf-parse / Mozilla pdfjs-dist.
* Handles embedded fonts, CMap/ToUnicode tables, object streams, XRef tables, and all compression filters.
*/
export async function parsePdfBuffer(buffer: Buffer): Promise<PdfExtractResult> {
if (!buffer || buffer.length === 0) {
throw new Error("Empty PDF buffer");
}
if (!isPdfBuffer(buffer)) {
throw new Error("Invalid PDF file (missing %PDF- header signature)");
}
try {
const data = await pdfParse({ data: new Uint8Array(buffer) } as any, {
pagerender: (pageData: any) => {
return pageData.getTextContent().then((textContent: any) => {
let lastY: number | null = null;
let text = "";
for (const item of textContent.items) {
if (lastY !== null && Math.abs(item.transform[5] - lastY) > 5) {
text += "\n";
} else if (text.length > 0 && !text.endsWith(" ") && !text.endsWith("\n")) {
text += " ";
}
text += item.str;
lastY = item.transform[5];
}
return text;
});
},
});
const info: { title?: string; author?: string; creationDate?: string } = {};
if (data.info) {
if (data.info.Title) info.title = String(data.info.Title);
if (data.info.Author) info.author = String(data.info.Author);
if (data.info.CreationDate) info.creationDate = String(data.info.CreationDate);
}
const cleanText = data.text ? data.text.trim() : "";
if (!cleanText) {
throw new Error("No readable text content streams found in digital PDF");
}
return {
text: cleanText,
pageCount: data.numpages || 1,
info,
};
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`PDF parsing failed: ${msg}`);
}
}
/**
* Standard PDF Extractor integration into Browse ExtractedWebpage schema.
*/
export async function extractPdfContent(buffer: Buffer, url: string): Promise<ExtractedWebpage> {
const rawSizeBytes = buffer.length;
const pdfRes = await parsePdfBuffer(buffer);
const cleanBody = normalizeWhitespace(pdfRes.text);
const title = pdfRes.info.title || "PDF Document";
const headerLines: string[] = [`# ${title}`];
const metaDetails: string[] = [];
if (pdfRes.info.author) {
metaDetails.push(`**Author:** ${pdfRes.info.author}`);
}
metaDetails.push(`**Pages:** ${pdfRes.pageCount}`);
if (metaDetails.length > 0) {
headerLines.push(metaDetails.join(" | "));
}
const fullMarkdown = normalizeWhitespace(`${headerLines.join("\n\n")}\n\n${cleanBody}`);
return {
url,
metadata: {
title: pdfRes.info.title || "PDF Document",
author: pdfRes.info.author,
publishedDate: pdfRes.info.creationDate,
},
fullMarkdown,
totalChars: fullMarkdown.length,
isSparse: false,
rawHtmlLength: rawSizeBytes,
readabilityLength: cleanBody.length,
usedJsonLd: false,
usedOg: false,
tablesDetected: fullMarkdown.includes("| ---") || fullMarkdown.includes("| --- |"),
pdfPageCount: pdfRes.pageCount,
imageMarkers: [],
};
}