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

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -8,6 +10,7 @@ import '../models/note.dart';
import '../providers/document_provider.dart';
import '../providers/note_provider.dart';
import '../providers/ocr_provider.dart';
import '../providers/search_provider.dart';
import '../editor/canvas/pen_editor_screen.dart';
import '../services/pptx_service.dart';
import '../services/vault_service.dart';
@@ -250,6 +253,10 @@ class HomeScreen extends ConsumerWidget {
final vaultPath = await vault.createNotebook(pickedPath);
// Refresh the documents list so the new notebook shows on return.
await ref.read(documentListProvider.notifier).loadDocuments();
// For a PDF, index its document body (embedded text layer, or background
// OCR of a rasterized/scanned PDF) into the sidecar so search covers it.
// Fire-and-forget: import returns and opens the editor immediately.
_indexPdfInBackground(ref, vaultPath);
if (!context.mounted) return;
await _openVaultFile(context, ref, vaultPath);
} catch (e) {
@@ -257,6 +264,25 @@ class HomeScreen extends ConsumerWidget {
}
}
/// Kick off background document-body indexing for an in-vault PDF (no-op for
/// other types). Runs detached from the import await chain so the editor opens
/// immediately; on completion it bumps the search-index epoch so the newly
/// indexed text is searchable. Idempotency and graceful OCR degradation live in
/// [PdfTextIndexer]; failures here are swallowed (search just misses the body).
void _indexPdfInBackground(WidgetRef ref, String vaultPath) {
final ext = p.extension(vaultPath).replaceFirst('.', '').toLowerCase();
if (ext != 'pdf') return;
final indexer = ref.read(pdfTextIndexerProvider);
unawaited(() async {
final indexed = await indexer.indexPdf(vaultPath);
if (indexed != null && indexed.isNotEmpty) {
// Force the next search to re-scan the vault (picks up the new pageText).
final epoch = ref.read(searchIndexEpochProvider.notifier);
epoch.state = epoch.state + 1;
}
}());
}
/// Route an in-vault [filePath] to the correct editor by extension:
/// pdf → [PenEditorScreen]; pptx/ppt → [PenSlideScreen]; docx → convert to
/// PDF (best-effort, LibreOffice) then open as PDF. Unsupported / failed