feat(search): index PDF text, OCR scanned PDFs on import
All checks were successful
CI / Windows build (push) Successful in 15m50s

Search now covers handwriting, the PDF text layer, AND scanned
(rasterized) PDFs.

- PdfTextIndexer runs at import: sums the embedded text layer across
  pages; if present it stores that as the document body, otherwise the
  PDF is rasterized and its rendered pages are OCR'd in the background.
  The result lands in the sidecar `pageText` field (distinct from
  `ocrText`, the handwriting OCR). Idempotent (skips a sidecar that
  already has pageText); degrades gracefully with no OCR engine.
- pdfrx_page_text_source abstracts text/render so it's testable.
- VaultSearchIndex now harvests title + typed text + handwriting OCR +
  PDF pageText, so search finds notes, typed PDFs and scanned PDFs.

analyze clean, 409 tests green.
This commit is contained in:
2026-06-25 00:23:19 +08:00
parent 20add27a30
commit e939759458
10 changed files with 668 additions and 16 deletions

View File

@@ -379,6 +379,46 @@ void main() {
expect(reparsed.background, isNull);
});
test('pageText (PDF body / scanned-OCR text) round-trips through sidecar', () {
final original = BadnoteSidecar(
docType: 'pdf',
sourceFile: 'scan.pdf',
pageText: 'page one body\fpage two 第二页',
);
final reparsed = _roundTrip(original);
expect(reparsed.pageText, 'page one body\fpage two 第二页');
// ocrText (handwriting) and pageText (document body) are independent fields.
expect(reparsed.ocrText, isNull);
// Omitted when null/empty (no key bloat for legacy/annotation-only sidecars).
expect(BadnoteSidecar().toJson().containsKey('pageText'), isFalse);
});
test('missing pageText decodes to null (back-compat)', () {
// A sidecar authored before the import-OCR feature simply omits pageText.
final json = <String, dynamic>{
'badnoteSidecarVersion': 1,
'docType': 'pdf',
'sourceFile': 'old.pdf',
'strokes': <String, dynamic>{},
'highlights': <String, dynamic>{},
'bookmarks': <dynamic>[],
'scratchLinks': <dynamic>[],
};
final reparsed = BadnoteSidecar.fromJson(json);
expect(reparsed.pageText, isNull);
});
test('pageText and ocrText coexist independently', () {
final original = BadnoteSidecar(
docType: 'pdf',
ocrText: 'handwritten note',
pageText: 'printed document body',
);
final reparsed = _roundTrip(original);
expect(reparsed.ocrText, 'handwritten note');
expect(reparsed.pageText, 'printed document body');
});
test('missing scratchpad defaults to 4000x4000 empty pad', () {
final json = SidecarScratchLink(
link: const ScratchLink(