diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index c623a8c..bba9ddc 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -29,6 +29,10 @@ jobs:
with:
repository: AdaWorldAPI/ndarray
path: ndarray
+ - uses: actions/checkout@v4
+ with:
+ repository: AdaWorldAPI/OGAR
+ path: OGAR
# ndarray declares rust-version = 1.95; bump the toolchain to satisfy it.
- name: Rust 1.95 toolchain
run: rustup toolchain install 1.95 --profile minimal --component clippy --component rustfmt && rustup default 1.95
diff --git a/Cargo.toml b/Cargo.toml
index 86c9c76..b6bbb31 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,5 +9,5 @@
# (source: AdaWorldAPI/Tesseract) before it lands. The transcoded primitives
# ride the OGAR Core (`lance-graph-contract`) per the Core-First doctrine.
[workspace]
-members = ["crates/tesseract-core", "crates/tesseract-ocr", "crates/tesseract-recognizer", "crates/tesseract-ocr-pdf"]
+members = ["crates/tesseract-core", "crates/tesseract-ocr", "crates/tesseract-recognizer", "crates/tesseract-ocr-pdf", "crates/tesseract-ogar"]
resolver = "2"
diff --git a/crates/tesseract-ogar/Cargo.toml b/crates/tesseract-ogar/Cargo.toml
new file mode 100644
index 0000000..cb8910c
--- /dev/null
+++ b/crates/tesseract-ogar/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "tesseract-ogar"
+version = "0.0.1"
+edition = "2021"
+license = "MIT"
+description = "In-binary executor for the OGAR-authoritative tesseract-rs OCR action table (ogar_vocab::ocr_actions): OGAR declares the eight capabilities, this crate executes them. No serde, no wire format — a typed OcrRequest/OcrResponse dispatch, monomorphized, in the same binary as the declaration it is proven against."
+
+[dependencies]
+# THE authoritative OCR capability table (Phase 1) — sibling checkout, path dep,
+# no pin, per the workspace's local-repos-are-canonical convention.
+ogar-vocab = { path = "../../../OGAR/crates/ogar-vocab" }
+# The two OCR foundations + the PDF front-end (path deps, fork-only policy).
+tesseract-ocr = { path = "../tesseract-ocr", features = ["seg-approx"] }
+tesseract-core = { path = "../tesseract-core" }
+tesseract-ocr-pdf = { path = "../tesseract-ocr-pdf" }
diff --git a/crates/tesseract-ogar/src/lib.rs b/crates/tesseract-ogar/src/lib.rs
new file mode 100644
index 0000000..6da2b0e
--- /dev/null
+++ b/crates/tesseract-ogar/src/lib.rs
@@ -0,0 +1,600 @@
+//! # tesseract-ogar — the in-binary executor for the OGAR OCR action table
+//!
+//! [`ogar_vocab::ocr_actions`] is the **authoritative** declaration of the
+//! eight OCR capabilities `tesseract-rs` exposes (`recognize_line` /
+//! `recognize_page` / `extract_text_layer` / `extract_page_image` /
+//! `render_text` / `render_tsv` / `render_hocr` / `render_searchable_pdf`).
+//! This crate is that table's executor: OGAR declares, this crate runs.
+//!
+//! ## Typed, not serialized
+//!
+//! Every consumer of this crate lives in the SAME binary as OGAR and the two
+//! OCR foundations (`tesseract-core`, `tesseract-recognizer`) — there is no
+//! process boundary here, so [`OcrRequest`]/[`OcrResponse`] are plain Rust
+//! enums, not a wire DTO. No `serde`, no JSON, no schema round-trip: the
+//! "OpenAPI-shaped" surface (one request type per declared capability, a
+//! matching typed response) exists so a caller gets the SAME shape an
+//! external API would advertise, but every call is a monomorphized function
+//! call in-process — the operator's framing: "wie OpenAPI aussieht, aber in
+//! der gleichen Binary ohne serde auskommt."
+//!
+//! ## The exhaustiveness fuse
+//!
+//! [`OCR_ACTION_NAMES`](ogar_vocab::ocr_actions::OCR_ACTION_NAMES) is OGAR's
+//! `const`-evaluable fingerprint of the declared capability names.
+//! [`COVERED_CAPABILITIES`] is this crate's own fingerprint of what
+//! [`OcrExecutor::execute`] handles. A `const` assertion below pins their
+//! *lengths* equal at compile time (a cheap, allocation-free tripwire); the
+//! `every_declared_capability_is_covered_and_vice_versa` test in this
+//! crate's test module pins the actual *names* equal, in both directions —
+//! so a capability added to OGAR without a matching `OcrRequest` arm here
+//! fails the test, and a capability removed from OGAR without pruning this
+//! crate's coverage also fails it.
+//!
+//! ## Drift = build/test failure, not a runtime surprise
+//!
+//! This crate never re-implements OCR logic — every [`OcrExecutor::execute`]
+//! arm is a thin dispatch onto the proven [`tesseract_ocr`]/
+//! [`tesseract_ocr_pdf`] public API. The value this crate adds is the
+//! *join*: proving, at compile time and test time, that the declared
+//! capability table and the actual executable surface never diverge.
+
+use std::path::Path;
+
+use tesseract_core::dawg::DawgError;
+use tesseract_core::DictLite;
+use tesseract_ocr::{LineWords, LstmRecognizer};
+use tesseract_ocr_pdf::{GreyImage, PageOcr, PdfError, RenderReport, SearchablePdfError};
+
+/// Every OCR capability this crate's [`OcrExecutor::execute`] handles, in the
+/// same order as [`ogar_vocab::ocr_actions::OCR_ACTION_NAMES`] — this
+/// crate's half of the exhaustiveness fuse (see the module docs).
+pub const COVERED_CAPABILITIES: &[&str] = &[
+ "recognize_line",
+ "recognize_page",
+ "extract_text_layer",
+ "extract_page_image",
+ "render_text",
+ "render_tsv",
+ "render_hocr",
+ "render_searchable_pdf",
+];
+
+/// This crate's self-registration with the AUTHORITATIVE OGAR table — the
+/// consumer's half of the confirmation loop (operator, 2026-07-07): the
+/// authority names its expected executor
+/// (`ogar_vocab::ocr_actions::OCR_EXPECTED_EXECUTORS`), and THIS const is how
+/// the consumer "trägt sich ein" — covered capabilities + the subject
+/// classids it activates. `ogar_vocab::ocr_actions::verify_ocr_registration`
+/// asserts the full roundtrip (expected consumer, coverage both directions,
+/// activated classid set == declared subjects, every id minted) in this
+/// crate's test suite — drift bangs once, in this binary, no serialization.
+pub const REGISTRATION: ogar_vocab::capability_registry::CapabilityRegistration =
+ ogar_vocab::capability_registry::CapabilityRegistration {
+ consumer: "tesseract-ogar",
+ covered: COVERED_CAPABILITIES,
+ subject_classids: ogar_vocab::ocr_actions::OCR_SUBJECT_CLASSIDS,
+ };
+
+// The cheap, allocation-free half of the fuse: OGAR's `OCR_ACTION_NAMES` and
+// this crate's `COVERED_CAPABILITIES` must have the same length at compile
+// time. This does NOT check the actual names (that needs `ActionDef`, which
+// isn't `const`-constructible — see `ogar_vocab::ocr_actions`'s module doc,
+// "why a `fn`, not a `const`") — the name-level check is the
+// `every_declared_capability_is_covered_and_vice_versa` test below.
+const _: () = assert!(
+ ogar_vocab::ocr_actions::OCR_ACTION_NAMES.len() == COVERED_CAPABILITIES.len(),
+ "tesseract-ogar::COVERED_CAPABILITIES has drifted from ogar_vocab::ocr_actions::OCR_ACTION_NAMES's length"
+);
+
+/// One typed request per declared OGAR OCR capability. Plain Rust types,
+/// zero serialization — see the module docs.
+#[derive(Debug, Clone, Copy)]
+pub enum OcrRequest<'a> {
+ /// `recognize_line` — a single pre-cropped grey text-line strip.
+ /// `grey` is row-major 8-bit, `width`×`height` pixels. `with_dict`
+ /// selects the dictionary-beam decode when this executor was assembled
+ /// with a dictionary (see [`OcrExecutor::from_data_paths`]); it is
+ /// silently equivalent to `false` when no dictionary was loaded.
+ RecognizeLine {
+ /// Row-major 8-bit grey line strip.
+ grey: &'a [u8],
+ /// Width in pixels.
+ width: usize,
+ /// Height in pixels.
+ height: usize,
+ /// Use the loaded dictionary beam, if any.
+ with_dict: bool,
+ },
+ /// `recognize_page` — a full grey page, segmented into line bands via
+ /// the `seg-approx` projection-profile finder (see
+ /// [`tesseract_ocr::LstmRecognizer::recognize_page`] for the
+ /// approximation-vs-transcode scope).
+ RecognizePage {
+ /// Row-major 8-bit grey page.
+ grey: &'a [u8],
+ /// Width in pixels.
+ width: usize,
+ /// Height in pixels.
+ height: usize,
+ /// Use the loaded dictionary beam, if any.
+ with_dict: bool,
+ },
+ /// `extract_text_layer` — the D5.1 fast path: per-page `Some(text)`/
+ /// `None` classification of a digital PDF's existing text layer.
+ ExtractTextLayer {
+ /// The PDF file's raw bytes.
+ pdf_bytes: &'a [u8],
+ },
+ /// `extract_page_image` — the D5.2 pragmatic scanned-page image
+ /// extraction (largest image XObject on the page, decoded to grey).
+ ExtractPageImage {
+ /// The PDF file's raw bytes.
+ pdf_bytes: &'a [u8],
+ /// 1-based page number (matches [`tesseract_ocr_pdf::extract_page_image`]).
+ page: u32,
+ },
+ /// `render_text` — plain-text join of already-recognized line/word
+ /// output (`ResultIterator::IterateAndAppendUTF8TextlineText` transcode).
+ RenderText {
+ /// Recognized lines, in reading order.
+ lines: &'a [LineWords],
+ },
+ /// `render_tsv` — Tesseract TSV rendering of already-recognized
+ /// line/word output.
+ RenderTsv {
+ /// Recognized lines, in reading order.
+ lines: &'a [LineWords],
+ /// Page width in pixels.
+ page_w: u32,
+ /// Page height in pixels.
+ page_h: u32,
+ },
+ /// `render_hocr` — hOCR rendering of already-recognized line/word
+ /// output.
+ RenderHocr {
+ /// Recognized lines, in reading order.
+ lines: &'a [LineWords],
+ /// Page width in pixels.
+ page_w: u32,
+ /// Page height in pixels.
+ page_h: u32,
+ /// The `
`/`ocr_page` image file name to embed.
+ image_name: &'a str,
+ },
+ /// `render_searchable_pdf` — the D4.5 invisible-text-layer searchable
+ /// PDF assembly, one or more OCR'd pages.
+ RenderSearchablePdf {
+ /// One entry per output page.
+ pages: &'a [PageOcr],
+ /// The embedded image resolution, in DPI.
+ dpi: u32,
+ },
+}
+
+/// One typed response per declared OGAR OCR capability — see
+/// [`OcrRequest`] for the matching request shape and
+/// [`ogar_vocab::ocr_actions::OcrActionSpec::produces`] for the declared
+/// output names each variant below corresponds to.
+#[derive(Debug, Clone, PartialEq)]
+pub enum OcrResponse {
+ /// `recognize_line`'s `text, unichar_ids` outputs.
+ Recognized {
+ /// Recognized unichar ids, in reading order.
+ unichar_ids: Vec,
+ /// Recognized text.
+ text: String,
+ },
+ /// `recognize_page`'s `textlines, text` outputs. `textlines` is derived
+ /// from `text` by splitting on `'\n'` and dropping empty entries — a
+ /// lossless recovery, since [`tesseract_ocr::LstmRecognizer::recognize_page`]
+ /// itself builds `text` by `'\n'`-joining exactly the non-empty per-line
+ /// results (see that method's doc comment), and no single recognized
+ /// line ever contains an internal `'\n'`.
+ PageText {
+ /// The page's text, split back into per-line strings.
+ textlines: Vec,
+ /// The whole page's text (lines joined by `'\n'`).
+ text: String,
+ },
+ /// `extract_text_layer`'s `page_texts` output — one entry per page,
+ /// `None` for an image-only page.
+ PageTexts(Vec