
























@@ -0,0 +1,216 @@
1+import type {
2+DocumentExtractedImage,
3+DocumentExtractionRequest,
4+DocumentExtractionResult,
5+DocumentExtractorPlugin,
6+} from "openclaw/plugin-sdk/document-extractor";
7+8+type CanvasLike = {
9+toBuffer(type: "image/png"): Buffer;
10+};
11+12+type CanvasModule = {
13+createCanvas(width: number, height: number): CanvasLike;
14+};
15+16+type PdfTextItem = {
17+str: string;
18+};
19+20+type PdfTextContent = {
21+items: Array<PdfTextItem | object>;
22+};
23+24+type PdfViewport = {
25+width: number;
26+height: number;
27+};
28+29+type PdfPage = {
30+getTextContent(): Promise<PdfTextContent>;
31+getViewport(params: { scale: number }): PdfViewport;
32+render(params: { canvas: unknown; viewport: PdfViewport }): { promise: Promise<void> };
33+};
34+35+type PdfDocument = {
36+numPages: number;
37+getPage(pageNumber: number): Promise<PdfPage>;
38+};
39+40+type PdfJsModule = {
41+getDocument(params: { data: Uint8Array; disableWorker?: boolean }): {
42+promise: Promise<PdfDocument>;
43+};
44+};
45+46+const CANVAS_MODULE = "@napi-rs/canvas";
47+const PDFJS_MODULE = "pdfjs-dist/legacy/build/pdf.mjs";
48+const MAX_EXTRACTED_TEXT_CHARS = 200_000;
49+const MAX_RENDER_DIMENSION = 10_000;
50+51+let canvasModulePromise: Promise<CanvasModule> | null = null;
52+let pdfJsModulePromise: Promise<PdfJsModule> | null = null;
53+54+async function loadCanvasModule(): Promise<CanvasModule> {
55+if (!canvasModulePromise) {
56+canvasModulePromise = (import(CANVAS_MODULE) as Promise<CanvasModule>).catch((err) => {
57+canvasModulePromise = null;
58+throw new Error("Optional dependency @napi-rs/canvas is required for PDF image extraction", {
59+cause: err,
60+});
61+});
62+}
63+return canvasModulePromise;
64+}
65+66+async function loadPdfJsModule(): Promise<PdfJsModule> {
67+if (!pdfJsModulePromise) {
68+pdfJsModulePromise = (import(PDFJS_MODULE) as Promise<PdfJsModule>).catch((err) => {
69+pdfJsModulePromise = null;
70+throw new Error("Optional dependency pdfjs-dist is required for PDF extraction", {
71+cause: err,
72+});
73+});
74+}
75+return pdfJsModulePromise;
76+}
77+78+function appendTextWithinLimit(parts: string[], pageText: string, currentLength: number): number {
79+if (!pageText) {
80+return currentLength;
81+}
82+const remaining = MAX_EXTRACTED_TEXT_CHARS - currentLength;
83+if (remaining <= 0) {
84+return currentLength;
85+}
86+const nextText = pageText.length > remaining ? pageText.slice(0, remaining) : pageText;
87+parts.push(nextText);
88+return currentLength + nextText.length;
89+}
90+91+function resolveRenderPlan(
92+viewport: PdfViewport,
93+remainingPixels: number,
94+): { scale: number; width: number; height: number; pixels: number } | null {
95+if (
96+remainingPixels <= 0 ||
97+!Number.isFinite(viewport.width) ||
98+!Number.isFinite(viewport.height) ||
99+viewport.width <= 0 ||
100+viewport.height <= 0
101+) {
102+return null;
103+}
104+105+const pagePixels = Math.max(1, viewport.width * viewport.height);
106+const maxScale = Math.min(
107+1,
108+Math.sqrt(remainingPixels / pagePixels),
109+MAX_RENDER_DIMENSION / viewport.width,
110+MAX_RENDER_DIMENSION / viewport.height,
111+);
112+if (!Number.isFinite(maxScale) || maxScale <= 0) {
113+return null;
114+}
115+116+let best: { scale: number; width: number; height: number; pixels: number } | null = null;
117+let low = 0;
118+let high = maxScale;
119+for (let i = 0; i < 32; i += 1) {
120+const scale = (low + high) / 2;
121+const width = Math.max(1, Math.ceil(viewport.width * scale));
122+const height = Math.max(1, Math.ceil(viewport.height * scale));
123+const pixels = width * height;
124+if (
125+width <= MAX_RENDER_DIMENSION &&
126+height <= MAX_RENDER_DIMENSION &&
127+pixels <= remainingPixels
128+) {
129+best = { scale, width, height, pixels };
130+low = scale;
131+} else {
132+high = scale;
133+}
134+}
135+return best;
136+}
137+138+async function extractPdfContent(
139+request: DocumentExtractionRequest,
140+): Promise<DocumentExtractionResult> {
141+const pdfJsModule = await loadPdfJsModule();
142+const pdf = await pdfJsModule.getDocument({
143+data: new Uint8Array(request.buffer),
144+disableWorker: true,
145+}).promise;
146+147+const effectivePages: number[] = request.pageNumbers
148+ ? request.pageNumbers.filter((p) => p >= 1 && p <= pdf.numPages).slice(0, request.maxPages)
149+ : Array.from({ length: Math.min(pdf.numPages, request.maxPages) }, (_, i) => i + 1);
150+151+const textParts: string[] = [];
152+let extractedTextLength = 0;
153+for (const pageNum of effectivePages) {
154+const page = await pdf.getPage(pageNum);
155+const textContent = await page.getTextContent();
156+const pageText = textContent.items
157+.map((item) => ("str" in item ? item.str : ""))
158+.filter(Boolean)
159+.join(" ");
160+if (pageText) {
161+extractedTextLength = appendTextWithinLimit(textParts, pageText, extractedTextLength);
162+if (extractedTextLength >= MAX_EXTRACTED_TEXT_CHARS) {
163+break;
164+}
165+}
166+}
167+168+const text = textParts.join("\n\n");
169+if (text.trim().length >= request.minTextChars) {
170+return { text, images: [] };
171+}
172+173+let canvasModule: CanvasModule;
174+try {
175+canvasModule = await loadCanvasModule();
176+} catch (err) {
177+request.onImageExtractionError?.(err);
178+return { text, images: [] };
179+}
180+181+const images: DocumentExtractedImage[] = [];
182+let remainingPixels = Math.max(1, Math.floor(request.maxPixels));
183+184+for (const pageNum of effectivePages) {
185+if (remainingPixels <= 0) {
186+break;
187+}
188+const page = await pdf.getPage(pageNum);
189+const viewport = page.getViewport({ scale: 1 });
190+const plan = resolveRenderPlan(viewport, remainingPixels);
191+if (!plan) {
192+break;
193+}
194+const scaled = page.getViewport({ scale: plan.scale });
195+const canvas = canvasModule.createCanvas(plan.width, plan.height);
196+await page.render({
197+canvas: canvas as unknown as HTMLCanvasElement,
198+viewport: scaled,
199+}).promise;
200+const png = canvas.toBuffer("image/png");
201+images.push({ type: "image", data: png.toString("base64"), mimeType: "image/png" });
202+remainingPixels -= plan.pixels;
203+}
204+205+return { text, images };
206+}
207+208+export function createPdfDocumentExtractor(): DocumentExtractorPlugin {
209+return {
210+id: "pdf",
211+label: "PDF",
212+mimeTypes: ["application/pdf"],
213+autoDetectOrder: 10,
214+extract: extractPdfContent,
215+};
216+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。