Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions apps/pdf-master/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ export function App() {
}),
[orderedDocuments, store.pageOrderByDocument, store.pages],
);
const pageTextSearchIndex = useMemo(() => {
const index = new Map<string, string>();

for (const { pages } of groupedPages) {
for (const page of pages) {
if (page.textContent) {
index.set(page.id, page.textContent.toLowerCase());
}
}
}

return index;
}, [groupedPages]);

const filteredGroups = useMemo(() => {
const query = searchQuery.trim().toLowerCase();
Expand All @@ -132,7 +145,8 @@ export function App() {
: pages.filter(
(page) =>
page.label.toLowerCase().includes(query) ||
String(page.sourcePageIndex + 1).includes(query),
String(page.sourcePageIndex + 1).includes(query) ||
pageTextSearchIndex.get(page.id)?.includes(query),
);

if (!matchingPages.length) {
Expand All @@ -142,7 +156,7 @@ export function App() {
return { document, pages: matchingPages };
})
.filter(Boolean) as typeof groupedPages;
}, [groupedPages, searchQuery]);
}, [groupedPages, pageTextSearchIndex, searchQuery]);

const workspaceRevision = useMemo(
() =>
Expand Down Expand Up @@ -671,7 +685,7 @@ export function App() {
<div className="p-4">
<EmptyState
title="No pages match this search"
description="Try a page number, page label, or clear the search field to return to the full workspace."
description="Try a page number, page label, text content, or clear the search field to return to the full workspace."
/>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion apps/pdf-master/src/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function Toolbar({
value={searchQuery}
onChange={(event) => onSearchChange(event.target.value)}
className="w-full border-0 bg-transparent p-0 text-sm text-[color:var(--pm-text-strong)] outline-none placeholder:text-[color:var(--pm-text-faint)]"
placeholder="Search pages"
placeholder="Search pages by text, label, or number"
/>
</label>

Expand Down
2 changes: 2 additions & 0 deletions apps/pdf-master/src/domain/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function addDocumentToWorkspace(
height: page.height,
rotation: 0,
label: page.label,
textContent: page.textContent,
};
pages[page.id] = pageEntity;
pageIds.push(page.id);
Expand Down Expand Up @@ -118,6 +119,7 @@ export function addDocumentToWorkspaceAtPosition(
height: page.height,
rotation: 0,
label: page.label,
textContent: page.textContent,
};
pages[page.id] = pageEntity;
pageIds.push(page.id);
Expand Down
2 changes: 2 additions & 0 deletions apps/pdf-master/src/domain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface PageEntity {
height: number;
rotation: number;
label: string;
textContent?: string;
}

export interface SelectionState {
Expand Down Expand Up @@ -180,6 +181,7 @@ export interface IngestPagePayload {
width: number;
height: number;
label: string;
textContent?: string;
}

export interface IngestDocumentPayload {
Expand Down
19 changes: 18 additions & 1 deletion apps/pdf-master/src/services/pdfInspection.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { inspectPdfFile } from '@/services/pdfInspection';
import { createFormPdfFile } from '@/test/pdfFixtures';
import { createFormPdfFile, createPdfFile } from '@/test/pdfFixtures';

describe('pdf inspection integration', () => {
it('extracts page inventory and AcroForm metadata from a real PDF', async () => {
Expand All @@ -13,4 +13,21 @@ describe('pdf inspection integration', () => {
expect(payload.hasForms).toBe(true);
expect(payload.formFields.map((field) => field.name)).toEqual(['name', 'approved', 'status']);
});

it('extracts text content from PDF pages', async () => {
const file = await createPdfFile('text-test.pdf', [
[400, 600], // Page 1 with text "Fixture 1"
[400, 600], // Page 2 with text "Fixture 2"
]);

const payload = await inspectPdfFile(file, 'doc-text');

expect(payload.pageCount).toBe(2);
// Text extraction may vary depending on PDF library, but we should get some text
expect(payload.pages[0]?.textContent).toBeDefined();
expect(payload.pages[1]?.textContent).toBeDefined();
// The fixture draws "Fixture 1" and "Fixture 2" on pages
expect(payload.pages[0]?.textContent?.toLowerCase()).toContain('fixture');
expect(payload.pages[1]?.textContent?.toLowerCase()).toContain('fixture');
Comment thread
YurMil marked this conversation as resolved.
});
});
42 changes: 41 additions & 1 deletion apps/pdf-master/src/services/pdfInspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import {
PDFTextField,
} from 'pdf-lib';
import type { PDFField } from 'pdf-lib';
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';
import type { FormFieldModel, IngestDocumentPayload } from '@/domain/types';

export async function inspectPdfFile(file: File, documentId: string): Promise<IngestDocumentPayload> {
const pdf = await PDFDocument.load(await file.arrayBuffer(), { updateMetadata: false });
const bytes = new Uint8Array(await file.arrayBuffer());
const [pdf, pageTextContent] = await Promise.all([
PDFDocument.load(bytes.slice(), { updateMetadata: false }),
extractPageTextContent(bytes.slice()),
]);
const pages = pdf.getPages().map((page, index) => {
const size = page.getSize();
return {
Expand All @@ -19,6 +24,7 @@ export async function inspectPdfFile(file: File, documentId: string): Promise<In
width: size.width,
height: size.height,
label: `Page ${index + 1}`,
textContent: pageTextContent[index],
};
});

Expand Down Expand Up @@ -47,6 +53,40 @@ export async function inspectPdfFile(file: File, documentId: string): Promise<In
};
}

async function extractPageTextContent(bytes: Uint8Array): Promise<string[]> {
const loadingTask = pdfjs.getDocument({
data: bytes,
disableWorker: true,
stopAtErrors: false,
} as Parameters<typeof pdfjs.getDocument>[0] & { disableWorker: boolean });

const pdf = await loadingTask.promise;

try {
const textContent: string[] = [];

for (let index = 1; index <= pdf.numPages; index += 1) {
const page = await pdf.getPage(index);
try {
const content = await page.getTextContent();
textContent.push(
content.items
.map((item) => ('str' in item ? item.str : ''))
.join(' ')
.trim(),
);
} finally {
page.cleanup();
}
}

return textContent;
} finally {
await pdf.destroy();
loadingTask.destroy();
}
}

function readFormField(field: PDFField): FormFieldModel {
if (field instanceof PDFTextField) {
return buildField(field.getName(), 'text', field.getText() ?? '', undefined, field.isReadOnly(), field.isRequired());
Expand Down
1 change: 1 addition & 0 deletions apps/pdf-master/src/store/pdfStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ export const usePdfStore = create<PdfStore>((set, get) => ({
height: page.height,
rotation: 0,
label: page.label,
textContent: page.textContent,
};
newPageIds.push(page.id);
}
Expand Down
2 changes: 1 addition & 1 deletion static/utilities/pdf-master/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>pdf-master</title>
<script type="module" crossorigin src="./assets/app-nlhTV1tO.js"></script>
<script type="module" crossorigin src="./assets/app-CugQOqfU.js"></script>
<link rel="stylesheet" crossorigin href="./assets/app-Cf9cPR_H.css">
</head>
<body>
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

54 changes: 0 additions & 54 deletions static/utilities/pdf-master/assets/ingest.worker-CXqkJFlA.js

This file was deleted.

This file was deleted.

66 changes: 66 additions & 0 deletions static/utilities/pdf-master/assets/ingest.worker-DAyMgYPq.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.