Files
BadNote/lib/services/pdfrx_page_text_source.dart

112 lines
3.4 KiB
Dart
Raw Permalink Normal View History

// 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();
}
}
}