
























1-import { existsSync } from "node:fs";
2-import { createRequire } from "node:module";
3-import path from "node:path";
41import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
526-const { canvasSizes, getDocumentMock, pdfDocument } = vi.hoisted(() => ({
7-canvasSizes: [] as Array<{ width: number; height: number }>,
8-getDocumentMock: vi.fn(),
3+const { createEngineMock, openPdfMock, pdfDocument } = vi.hoisted(() => ({
4+createEngineMock: vi.fn(),
5+openPdfMock: vi.fn(),
96pdfDocument: {
10-numPages: 2,
11-getPage: vi.fn(async () => ({
12-getTextContent: vi.fn(async () => ({ items: [] })),
13-getViewport: vi.fn(({ scale }: { scale: number }) => ({
14-width: 1000 * scale,
15-height: 1000 * scale,
16-})),
17-render: vi.fn(() => ({ promise: Promise.resolve() })),
18-})),
7+pageCount: 2,
8+extract: vi.fn(),
9+destroy: vi.fn(),
1910},
2011}));
211222-vi.mock("pdfjs-dist/legacy/build/pdf.mjs", () => ({
23-getDocument: getDocumentMock,
24-}));
25-26-vi.mock("@napi-rs/canvas", () => ({
27-createCanvas: vi.fn((width: number, height: number) => {
28-canvasSizes.push({ width, height });
29-return {
30-toBuffer: vi.fn(() => Buffer.from("png")),
31-};
32-}),
13+vi.mock("clawpdf", () => ({
14+createEngine: createEngineMock,
3315}));
34163517import { createPdfDocumentExtractor } from "./document-extractor.js";
361837-const require = createRequire(import.meta.url);
38-39-function requireFirstMockArg(mock: ReturnType<typeof vi.fn>, label: string) {
40-const [call] = mock.mock.calls;
41-if (!call) {
42-throw new Error(`Expected ${label}`);
43-}
44-return call[0];
19+function request(overrides = {}) {
20+return {
21+buffer: Buffer.from("%PDF-1.4"),
22+mimeType: "application/pdf",
23+maxPages: 2,
24+maxPixels: 100,
25+minTextChars: 10,
26+ ...overrides,
27+};
4528}
46294730describe("PDF document extractor", () => {
4831afterAll(() => {
49-vi.doUnmock("pdfjs-dist/legacy/build/pdf.mjs");
50-vi.doUnmock("@napi-rs/canvas");
32+vi.doUnmock("clawpdf");
5133vi.resetModules();
5234});
53355436beforeEach(() => {
55-canvasSizes.length = 0;
56-getDocumentMock.mockReset();
57-getDocumentMock.mockReturnValue({ promise: Promise.resolve(pdfDocument) });
58-pdfDocument.getPage.mockClear();
37+createEngineMock.mockResolvedValue({ open: openPdfMock });
38+openPdfMock.mockReset();
39+openPdfMock.mockResolvedValue(pdfDocument);
40+pdfDocument.pageCount = 2;
41+pdfDocument.extract.mockReset();
42+pdfDocument.destroy.mockReset();
5943});
60446145it("declares PDF support", () => {
@@ -70,55 +54,90 @@ describe("PDF document extractor", () => {
7054});
7155});
725673-it("treats maxPixels as a hard total image rendering budget", async () => {
57+it("extracts text first and renders fallback images through clawpdf", async () => {
58+pdfDocument.extract.mockResolvedValueOnce({ text: "", images: [] }).mockResolvedValueOnce({
59+text: "",
60+images: [
61+{
62+type: "image",
63+bytes: Uint8Array.from(Buffer.from("png")),
64+mimeType: "image/png",
65+page: 1,
66+width: 10,
67+height: 10,
68+},
69+],
70+});
7471const extractor = createPdfDocumentExtractor();
757276-const result = await extractor.extract({
77-buffer: Buffer.from("%PDF-1.4"),
78-mimeType: "application/pdf",
79-maxPages: 2,
80-maxPixels: 100,
81-minTextChars: 10,
82-});
73+const result = await extractor.extract(request());
83748475if (!result) {
8576throw new Error("Expected PDF extraction result");
8677}
87-expect(result.images).toHaveLength(1);
88-expect(canvasSizes).toEqual([{ width: 10, height: 10 }]);
78+expect(openPdfMock).toHaveBeenCalledWith(expect.any(Uint8Array));
79+expect(pdfDocument.extract).toHaveBeenNthCalledWith(1, {
80+mode: "text",
81+maxPages: 2,
82+maxTextChars: 200_000,
83+});
84+expect(pdfDocument.extract).toHaveBeenNthCalledWith(2, {
85+mode: "images",
86+maxPages: 2,
87+image: {
88+maxDimension: 10_000,
89+maxPixels: 100,
90+forms: true,
91+},
92+});
93+expect(result).toEqual({
94+text: "",
95+images: [{ type: "image", data: "cG5n", mimeType: "image/png" }],
96+});
97+expect(pdfDocument.destroy).toHaveBeenCalledTimes(1);
8998});
909991-it("passes standardFontDataUrl to pdfjs getDocument as a package-root filesystem path", async () => {
100+it("skips image fallback when enough text is extracted", async () => {
101+pdfDocument.extract.mockResolvedValueOnce({ text: "enough text", images: [] });
92102const extractor = createPdfDocumentExtractor();
9310394-await extractor.extract({
95-buffer: Buffer.from("%PDF-1.4"),
96-mimeType: "application/pdf",
97-maxPages: 1,
98-maxPixels: 4_000_000,
99-minTextChars: 200,
100-});
104+const result = await extractor.extract(request({ minTextChars: 5 }));
101105102-expect(getDocumentMock).toHaveBeenCalledTimes(1);
103-const params = requireFirstMockArg(getDocumentMock, "pdfjs getDocument call");
104-const { data, standardFontDataUrl, ...stableParams } = params as {
105-data: Uint8Array;
106-disableWorker: boolean;
107-standardFontDataUrl: string;
108-};
109-expect(stableParams).toEqual({
110-disableWorker: true,
111-});
112-expect(data).toBeInstanceOf(Uint8Array);
113-expect(typeof standardFontDataUrl).toBe("string");
114-115-const expectedStandardFontDataUrl =
116-path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "standard_fonts") + "/";
117-expect(standardFontDataUrl).toBe(expectedStandardFontDataUrl);
118-expect(path.isAbsolute(standardFontDataUrl)).toBe(true);
119-expect(standardFontDataUrl.endsWith("/")).toBe(true);
120-expect(standardFontDataUrl.startsWith("file://")).toBe(false);
121-expect(existsSync(standardFontDataUrl)).toBe(true);
122-expect(existsSync(path.join(standardFontDataUrl, "LiberationSans-Regular.ttf"))).toBe(true);
106+expect(result).toEqual({ text: "enough text", images: [] });
107+expect(pdfDocument.extract).toHaveBeenCalledTimes(1);
108+expect(pdfDocument.destroy).toHaveBeenCalledTimes(1);
109+});
110+111+it("filters selected pages before passing them to clawpdf", async () => {
112+pdfDocument.extract
113+.mockResolvedValueOnce({ text: "", images: [] })
114+.mockResolvedValueOnce({ text: "", images: [] });
115+const extractor = createPdfDocumentExtractor();
116+117+await extractor.extract(request({ pageNumbers: [3, 2, 0, 1], maxPages: 2 }));
118+119+expect(pdfDocument.extract).toHaveBeenNthCalledWith(
120+1,
121+expect.objectContaining({ pages: [2, 1] }),
122+);
123+expect(pdfDocument.extract).toHaveBeenNthCalledWith(
124+2,
125+expect.objectContaining({ pages: [2, 1] }),
126+);
127+});
128+129+it("reports image fallback failures and returns extracted text", async () => {
130+const onImageExtractionError = vi.fn();
131+const failure = new Error("render failed");
132+pdfDocument.extract
133+.mockResolvedValueOnce({ text: "short", images: [] })
134+.mockRejectedValueOnce(failure);
135+const extractor = createPdfDocumentExtractor();
136+137+const result = await extractor.extract(request({ onImageExtractionError }));
138+139+expect(result).toEqual({ text: "short", images: [] });
140+expect(onImageExtractionError).toHaveBeenCalledWith(failure);
141+expect(pdfDocument.destroy).toHaveBeenCalledTimes(1);
123142});
124143});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。