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.
112 lines
3.4 KiB
Dart
112 lines
3.4 KiB
Dart
// lib/services/pdfrx_page_text_source.dart
|
||
//
|
||
// Production wiring for [PdfTextIndexer]'s two injected text sources, backed by
|
||
// pdfrx (the same engine the editor renders with). Kept SEPARATE from
|
||
// PdfTextIndexer so the indexer's logic (the threshold decision, idempotency,
|
||
// sidecar persistence) is unit-testable without the native pdfium/OCR stack —
|
||
// only this file touches pdfrx, dart:ui, and the OCR engine, and it is exercised
|
||
// on-device, not in CI.
|
||
|
||
import 'dart:async';
|
||
import 'dart:typed_data';
|
||
import 'dart:ui' as ui;
|
||
|
||
import 'package:pdfrx/pdfrx.dart';
|
||
|
||
import 'ocr_engine.dart';
|
||
|
||
/// pdfrx-backed loaders for [PdfTextIndexer].
|
||
class PdfrxPageTextSource {
|
||
const PdfrxPageTextSource._();
|
||
|
||
/// Load the embedded text layer of every page (page order). Each entry is a
|
||
/// page's raw text (possibly empty). Returns an empty list on any failure, so
|
||
/// the indexer treats the PDF as having no text layer (→ OCR fallback).
|
||
static Future<List<String>> loadEmbeddedText(String pdfPath) async {
|
||
PdfDocument? doc;
|
||
try {
|
||
doc = await PdfDocument.openFile(pdfPath);
|
||
final out = <String>[];
|
||
for (final page in doc.pages) {
|
||
final raw = await page.loadText();
|
||
out.add(raw?.fullText ?? '');
|
||
}
|
||
return out;
|
||
} catch (_) {
|
||
return const [];
|
||
} finally {
|
||
await doc?.dispose();
|
||
}
|
||
}
|
||
|
||
/// Render each page and OCR it (page order). Returns one entry per page
|
||
/// (empty where nothing was recognized), or an empty list when the PDF can't
|
||
/// be opened. Honours the OCR engine's own graceful no-op: when no backend is
|
||
/// available every page comes back empty.
|
||
///
|
||
/// Rendering is done at [renderScale]× the page's native 72-dpi size to give
|
||
/// the recognizer enough resolution on scanned scans without exploding memory.
|
||
static Future<List<String>> ocrPages(
|
||
String pdfPath, {
|
||
double renderScale = 2.0,
|
||
}) async {
|
||
PdfDocument? doc;
|
||
try {
|
||
doc = await PdfDocument.openFile(pdfPath);
|
||
final out = <String>[];
|
||
for (final page in doc.pages) {
|
||
final text = await _ocrOnePage(page, renderScale);
|
||
out.add(text ?? '');
|
||
}
|
||
return out;
|
||
} catch (_) {
|
||
return const [];
|
||
} finally {
|
||
await doc?.dispose();
|
||
}
|
||
}
|
||
|
||
static Future<String?> _ocrOnePage(PdfPage page, double renderScale) async {
|
||
PdfImage? image;
|
||
try {
|
||
final fullWidth = page.width * renderScale;
|
||
final fullHeight = page.height * renderScale;
|
||
image = await page.render(
|
||
fullWidth: fullWidth,
|
||
fullHeight: fullHeight,
|
||
);
|
||
if (image == null) return null;
|
||
final png = await _bgraToPng(image.pixels, image.width, image.height);
|
||
if (png == null) return null;
|
||
return OcrEngine.recognizeImage(png);
|
||
} catch (_) {
|
||
return null;
|
||
} finally {
|
||
image?.dispose();
|
||
}
|
||
}
|
||
|
||
/// Encode pdfrx's BGRA8888 raw pixels as PNG (the format [OcrEngine] expects).
|
||
static Future<Uint8List?> _bgraToPng(
|
||
Uint8List bgra,
|
||
int width,
|
||
int height,
|
||
) async {
|
||
final completer = Completer<ui.Image>();
|
||
ui.decodeImageFromPixels(
|
||
bgra,
|
||
width,
|
||
height,
|
||
ui.PixelFormat.bgra8888,
|
||
completer.complete,
|
||
);
|
||
final image = await completer.future;
|
||
try {
|
||
final data = await image.toByteData(format: ui.ImageByteFormat.png);
|
||
return data?.buffer.asUint8List();
|
||
} finally {
|
||
image.dispose();
|
||
}
|
||
}
|
||
}
|