OCR

The doc2mark.ocr package turns page images into text. It is built around a single ergonomic facade — OCR — that wraps an interchangeable provider (OpenAI, Vertex/Gemini, or Tesseract) behind one small surface: construct OCR(provider, **config) and call read() (a batch of images) or read_one() (a single image).

By default OCR is structured: each image is parsed into a OCRPage with a hard boundary between what is literally on the page (document.raw) and the model’s analysis of it (document.interpretation). The flat OCRResult.text is still always populated — rendered from the raw transcription — so older code keeps working.

This page documents the facade, the configuration and result types, the provider registry, and the three bundled providers. The structured result model itself (OCRPage and friends) is documented on the schema page.

The OCR facade

OCR is the one class users are expected to touch. Everything else in this package is either a type it returns, a configuration object it accepts, or a provider it delegates to.

from doc2mark import OCR

ocr = OCR("openai")                 # credentials from OPENAI_API_KEY
results = ocr.read(list_of_png_bytes)   # List[bytes] -> List[OCRResult]

first = results[0]
first.text                          # rendered markdown (back-compat)
first.document.raw.text             # verbatim transcription
first.document.interpretation       # structured analysis (may be None)

Constructor

OCR(provider="openai", *, api_key=None, **config_kwargs)
provider

Provider name ("openai", "vertex_ai", "gemini", "tesseract") or an OCRProvider enum member. Defaults to "openai".

api_key

Keyword-only API key forwarded to the provider. When omitted, providers fall back to their environment variable (e.g. OPENAI_API_KEY); Vertex/Gemini uses Application Default Credentials and Tesseract needs no key at all.

**config_kwargs

Any field of OCRConfig (model, task, language, detail, structured, max_concurrency, …). Two fields are coerced/validated eagerly for ergonomics: a string task is mapped to the matching Task member, and detail must be "raw" or "full". An unknown value raises ValueError immediately, listing the valid options.

Note

OCR("openai", task="receipt") is shorthand — the string "receipt" is coerced to Task.RECEIPT before the config is built.

read and read_one

read(images, *, task=None, tasks=None, language=None,
     structured=None, detail=None) -> List[OCRResult]
read_one(image, **kw) -> OCRResult

Both methods accept the same per-call overrides; None preserves whatever was configured on the facade. read_one is a thin convenience wrapper that calls read([image], **kw)[0].

task

A single OCR intent applied to every image, overriding config.task. Accepts a Task or its string name.

tasks

A per-image list of intents for mixed batches. Its length must equal len(images) or a ValueError is raised. When given, tasks wins over the single task.

language

Output-language hint appended to the prompt (overrides config.language).

structured

Override of config.structured for this call. False selects the legacy free-form path: OCRResult.text is filled and OCRResult.document is None.

detail

"full" (default) returns raw and interpretation; "raw" tells the model to skip the interpretation subtree, saving output tokens.

from doc2mark import OCR, Task

ocr = OCR("openai", detail="full")

# One intent for the whole batch:
pages = ocr.read(images, task="document")

# Mixed batch — one intent per image:
pages = ocr.read(images, tasks=[Task.RECEIPT, Task.TABLE, "handwriting"])

# Cheap, transcription-only pass (no interpretation):
page = ocr.read_one(image, detail="raw")
class doc2mark.ocr.OCR(provider='openai', *, api_key=None, **config_kwargs)[source]

Bases: object

The single user-facing entry point for structured OCR.

Wraps an OCR provider behind an ergonomic facade:

from doc2mark.ocr import OCR
ocr = OCR("openai")                 # creds from env
results = ocr.read(images)          # List[bytes] -> List[OCRResult]
results[0].text                     # rendered markdown (back-compat)
results[0].document.raw.text        # structured verbatim transcription
results[0].document.interpretation  # structured analysis (may be None)

Ergonomic kwargs are coerced: OCR("openai", task="receipt") maps the string to Task.RECEIPT, and an unknown task/detail raises a clear ValueError.

Parameters:
read(images, *, task=None, tasks=None, language=None, structured=None, detail=None)[source]

Run OCR over a batch of images, returning one OCRResult each.

All keyword arguments are per-call overrides of the config; None preserves the configured behavior. tasks (per-image) wins over the single task.

Parameters:
  • images (List[bytes])

  • task (str | Task | None)

  • tasks (List[str | Task] | None)

  • language (str | None)

  • structured (bool | None)

  • detail (str | None)

Return type:

List[OCRResult]

read_one(image, **kw)[source]

Convenience wrapper around read() for a single image.

Parameters:

image (bytes)

Return type:

OCRResult

Tasks

Task is a small str enum naming the intent of an image. The intent selects a short, schema-aligned instruction that steers the model toward the right raw fields (tables, label/value pairs, handwriting flag, …). Because Task subclasses str, members compare equal to their string value, which is why task="receipt" and Task.RECEIPT are interchangeable.

Member

.value / intent

AUTO

"auto" — general-purpose default

TABLE

"table" — image dominated by tabular data

DOCUMENT

"document" — prose with headings, lists, reading order

FORM

"form" — labelled fields and filled values

RECEIPT

"receipt" — receipts/invoices (merchant, totals, items)

HANDWRITING

"handwriting" — handwritten content

CODE

"code" — source code or terminal output

language is intentionally not a task — it is a separate config field, so there is no “multilingual” task.

from doc2mark import Task

list(Task)                       # all members
Task.RECEIPT.value               # 'receipt'
Task("table") is Task.TABLE      # True
class doc2mark.ocr.Task(*values)[source]

Bases: str, Enum

OCR intent. Replaces the free-form PromptTemplate variants with a small set of intent names that select a schema-aligned instruction (see TASK_PROMPTS). language is a config field, not a task, so the old MULTILINGUAL template is dropped.

Configuration

OCRConfig is the dataclass that holds every knob. You rarely build it directly — the OCR facade constructs it from your keyword arguments — but it documents exactly what is tunable.

Live LLM knobs

Field

Default

Meaning

model

None

Provider model id; None uses the provider default

task

Task.AUTO

Default intent for every image

language

None

Output-language hint

temperature

None

Sampling temperature

max_tokens

None

Max response tokens

base_url

None

OpenAI-compatible endpoint override

max_concurrency

None

Cap on concurrent image calls (see below)

structured

True

Structured output is the default

detail

"full"

"raw" skips interpretation

response_model

None

BYO pydantic schema; NoneOCRPage

on_parse_error

"raw_text"

"raw_text" degrades gracefully; "raise" errors

Tesseract-only fields

enhance_image (default True) and detect_layout (default True) are read only by the Tesseract provider. detect_tables exists but is not consumed by the current Tesseract path.

Warning

Deprecated, inert fields. enhance_image, detect_tables, detect_layout, timeout, max_retries, and extra do nothing for the LLM providers (OpenAI / Vertex / Gemini). Setting any of them to a non-default value makes the provider emit a single DeprecationWarning at construction. OCRConfig.deprecated_llm_overrides() returns the list of such fields a caller has touched. Prefer the live knobs above.

max_concurrency and image downscaling

max_concurrency caps how many image OCR calls the LLM providers run at once inside LangChain’s batch_as_completed. None (the default) means “use the LangChain default” — a CPU-tied thread pool, typically around 12. Precedence is explicit config value → ``OCR_MAX_CONCURRENCY`` env var → ``None``. Raise it to keep large scanned documents within an SLA (e.g. 32 for a several-thousand page job).

A second environment variable, OCR_MAX_IMAGE_DIM, controls downscaling. When set to a positive integer, every image is resized so its longest side is at most that many pixels before it is sent to the model. This trims token cost and upload size for high-resolution scans; an unset or invalid value leaves images untouched.

import os
from doc2mark import OCR

os.environ["OCR_MAX_IMAGE_DIM"] = "2048"   # downscale big scans
ocr = OCR("openai", max_concurrency=32)     # 32 images in flight at once
pages = ocr.read(images)
class doc2mark.ocr.OCRConfig(model=None, task=Task.AUTO, language=None, temperature=None, max_tokens=None, base_url=None, max_concurrency=None, structured=True, detail='full', response_model=None, on_parse_error='raw_text', context_pages=0, enhance_image=True, detect_tables=True, detect_layout=True, max_retries=3, timeout=30, extra=None)[source]

Bases: object

Configuration for OCR processing.

The live knobs for LLM providers are model, task, language, max_concurrency, and the structured-output controls (structured/detail/response_model/on_parse_error). The remaining fields are either Tesseract-only (enhance_image, detect_layout) or deprecated no-ops kept for backward compatibility (see _DEPRECATED_LLM_FIELDS).

Parameters:
  • model (str | None)

  • task (Task)

  • language (str | None)

  • temperature (float | None)

  • max_tokens (int | None)

  • base_url (str | None)

  • max_concurrency (int | None)

  • structured (bool)

  • detail (Literal['raw', 'full'])

  • response_model (Type[BaseModel] | None)

  • on_parse_error (Literal['raw_text', 'raise'])

  • context_pages (int)

  • enhance_image (bool)

  • detect_tables (bool)

  • detect_layout (bool)

  • max_retries (int)

  • timeout (int)

  • extra (Dict[str, Any] | None)

deprecated_llm_overrides()[source]

Return the names of deprecated/inert fields the user set to a non-default value. LLM providers use this to emit a single DeprecationWarning at construction time.

Return type:

List[str]

Results

Every OCR call returns one OCRResult per input image, in input order. text is always present; document carries the structured OCRPage for the LLM providers (and for Tesseract, with an empty interpretation), or None for the legacy free-form path.

Attribute

Type

Meaning

text

str

Rendered markdown — always populated

confidence

Optional[float]

Self-confidence (LLM) or avg score (Tesseract)

language

Optional[str]

Detected/configured language

metadata

Optional[dict]

Model, token usage, batch index, …

document

Optional[OCRPage]

Structured page, or None for free-form

from doc2mark import OCR

ocr = OCR("openai", detail="full")
r = ocr.read_one(receipt_png, task="receipt")

# Flat view (back-compat):
print(r.text)

# Structured RAW — what is literally on the page:
print(r.document.raw.text)              # verbatim transcription
for kv in r.document.raw.fields:        # label/value pairs
    print(kv.label, "=", kv.value)
for table in r.document.raw.tables:     # transcribed tables
    print(table.headers, table.rows)

# Structured INTERPRETATION — the model's reading (None when detail="raw"):
interp = r.document.interpretation
if interp is not None:
    print(interp.document_type)         # e.g. "receipt"
    print(interp.summary)
    print(interp.self_confidence)       # 0.0 .. 1.0
class doc2mark.ocr.OCRResult(text, confidence=None, language=None, metadata=None, document=None)[source]

Bases: object

Result from OCR processing.

text is always populated (rendered from document.raw when structured output is used) for backward compatibility. document carries the structured OCRPage when available, or None for legacy/free-form results and non-LLM providers.

Parameters:
  • text (str)

  • confidence (float | None)

  • language (str | None)

  • metadata (Dict[str, Any] | None)

  • document (OCRPage | None)

Provider registry

Providers are selected by name through a tiny registry. OCRProvider is the enum of known providers, and OCRFactory resolves a name to a concrete BaseOCR subclass.

OCRProvider

Member

.value

Implementation

OPENAI

"openai"

OpenAIOCR

VERTEX_AI

"vertex_ai"

VertexAIOCR

GEMINI

"gemini"

alias → VertexAIOCR

TESSERACT

"tesseract"

TesseractOCR

GEMINI is an alias: it is registered against the same VertexAIOCR implementation, so OCR("gemini") and OCR("vertex_ai") behave identically.

class doc2mark.ocr.OCRProvider(*values)[source]

Bases: Enum

Available OCR providers.

OCRFactory

OCRFactory is a class-level registry. Providers register themselves at import time via register_provider(); the facade calls create() for you. Provider lookup is case-insensitive and accepts either an OCRProvider or a string. An unknown or unregistered name raises ValueError.

from doc2mark import OCRFactory, OCRConfig

OCRFactory.list_providers()              # e.g. ['openai', 'vertex_ai', 'gemini', 'tesseract']
provider = OCRFactory.create("tesseract", config=OCRConfig(language="english"))
pages = provider.batch_process_images(list_of_png_bytes)
class doc2mark.ocr.OCRFactory[source]

Bases: object

Factory for creating OCR providers.

classmethod create(provider, api_key=None, config=None)[source]

Create an OCR provider instance.

Parameters:
  • provider (OCRProvider | str) – Provider type or string name

  • api_key (str | None) – API key for the provider

  • config (OCRConfig | None) – OCR configuration

Returns:

OCR provider instance

Raises:

ValueError – If provider is not registered

Return type:

BaseOCR

classmethod list_providers()[source]

List available OCR providers.

Returns:

List of provider names

Return type:

List[str]

classmethod register_provider(provider, provider_class)[source]

Register an OCR provider.

Parameters:
  • provider (OCRProvider) – Provider enum value

  • provider_class (type) – Provider class type

BaseOCR

BaseOCR is the abstract base every provider implements. The only required method is batch_process_images(), which all built-in providers implement on top of efficient batch processing. Subclass it to add a custom provider, then register it with OCRFactory.

class doc2mark.ocr.BaseOCR(api_key=None, config=None)[source]

Bases: ABC

Abstract base class for OCR providers.

Parameters:
  • api_key (str | None)

  • config (OCRConfig | None)

abstractmethod batch_process_images(images, **kwargs)[source]

Process multiple images in batch using LangChain.

This is the primary method for OCR processing. All implementations must use LangChain for efficient batch processing.

Parameters:
  • images (List[bytes]) – List of image data as bytes

  • **kwargs – Additional provider-specific options

Returns:

List of OCRResult objects in the same order as input

Return type:

List[OCRResult]

preprocess_image(image_data)[source]

Preprocess image before OCR (optional).

Parameters:

image_data (bytes) – Raw image data

Returns:

Preprocessed image data

Return type:

bytes

property provider_name: str

Get the provider name.

property requires_api_key: bool

Check if this provider requires an API key.

validate_api_key()[source]

Validate that the API key is set if required.

Returns:

True if valid, False otherwise

Return type:

bool

Providers

OpenAIOCR

GPT-4V-class OCR via LangChain. Structured output is produced with with_structured_output(method="json_schema"), so the default result carries a full OCRPage. Requires an API key (api_key= or OPENAI_API_KEY) and the doc2mark[ocr] extra. The default model is gpt-5.4-mini; base_url (or OPENAI_BASE_URL) targets OpenAI-compatible endpoints. Model knobs follow the precedence explicit constructor argument → ``OCRConfig`` field → built-in default.

from doc2mark import OCR

ocr = OCR("openai", model="gpt-5.4-mini", detail="full")
pages = ocr.read(images, task="document")
print(pages[0].document.raw.text)
class doc2mark.ocr.openai.OpenAIOCR(api_key=None, config=None, model=<object object>, temperature=<object object>, max_tokens=<object object>, max_workers=5, default_prompt=None, prompt_template=None, timeout=30, max_retries=3, base_url=<object object>, **kwargs)[source]

Bases: BaseOCR

OpenAI GPT-4V based OCR implementation with comprehensive configuration options.

Parameters:
  • api_key (str | None)

  • config (OCRConfig | None)

  • model (Any)

  • temperature (Any)

  • max_tokens (Any)

  • max_workers (int)

  • default_prompt (str | None)

  • prompt_template (str | PromptTemplate | None)

  • timeout (int)

  • max_retries (int)

  • base_url (Any)

batch_process_images(images, *, task=None, tasks=None, language=None, structured=None, detail=None, **kwargs)[source]

Process multiple images using LangChain for optimal performance.

Parameters:
  • images (List[bytes]) – List of image data

  • task (str | Task | None) – Single OCR intent applied to every image (overrides config.task).

  • tasks (List[str | Task] | None) – Per-image OCR intents for mixed batches; must match len(images).

  • language (str | None) – Output language hint (overrides config.language).

  • structured (bool | None) – Override config.structured for this call. When False the legacy free-form path runs (returns OCRResult.text, document=None).

  • detail (str | None) – “full” or “raw”; “raw” skips interpretation to save tokens.

  • **kwargs – Additional options (e.g. save_locally, content_type, instructions, prompt_template for the legacy path).

Returns:

List of OCR results in the same order as input

Return type:

List[OCRResult]

get_available_prompts()[source]

Get available prompt templates.

Returns:

Dictionary of prompt template names and descriptions

Return type:

Dict[str, str]

get_configuration_summary()[source]

Get current configuration summary.

Returns:

Dictionary with current configuration

Return type:

Dict[str, any]

property requires_api_key: bool

OpenAI requires an API key.

update_model_config(model=None, temperature=None, max_tokens=None, **kwargs)[source]

Update model configuration.

Parameters:
  • model (str | None) – New model name

  • temperature (float | None) – New temperature value

  • max_tokens (int | None) – New max tokens value

  • **kwargs – Additional model parameters

update_prompt_template(template_name)[source]

Update the prompt template.

Parameters:

template_name (str | PromptTemplate) – Name of the prompt template to use (string or PromptTemplate enum)

Raises:

ValueError – If template name is not available

validate_api_key()[source]

Validate OpenAI API key.

Return type:

bool

VertexAIOCR

Google Gemini OCR via langchain-google-genai (Vertex AI backend). Like OpenAI it is structured by default. It authenticates with Application Default Credentials rather than an API key — set GOOGLE_CLOUD_PROJECT (or pass project=) and configure ADC — and needs the doc2mark[vertex_ai] extra. The default model is gemini-3.1-flash-lite-preview and the default location is "global". Reachable as either OCR("vertex_ai") or OCR("gemini").

from doc2mark import OCR

ocr = OCR("gemini", project="my-gcp-project")   # alias for vertex_ai
pages = ocr.read(images, tasks=["receipt", "table"])
print(pages[0].document.interpretation.summary)
class doc2mark.ocr.vertex_ai.VertexAIOCR(api_key=None, config=None, project=None, location='global', model='gemini-3.1-flash-lite-preview', temperature=0, max_tokens=8192, default_prompt=None, prompt_template=None, timeout=30, max_retries=3, **kwargs)[source]

Bases: BaseOCR

Google Gemini based OCR implementation via langchain-google-genai.

Parameters:
  • api_key (str | None)

  • config (OCRConfig | None)

  • project (str | None)

  • location (str)

  • model (str)

  • temperature (float)

  • max_tokens (int)

  • default_prompt (str | None)

  • prompt_template (str | PromptTemplate | None)

  • timeout (int)

  • max_retries (int)

batch_process_images(images, max_workers=None, **kwargs)[source]

Process multiple images using Gemini via LangChain.

Parameters:
  • images (List[bytes]) – List of image data

  • max_workers (int | None) – Not used - kept for compatibility

  • **kwargs – Additional options

Returns:

List of OCR results in the same order as input

Return type:

List[OCRResult]

get_available_prompts()[source]

Get available prompt templates.

Return type:

Dict[str, str]

get_configuration_summary()[source]

Get current configuration summary.

Return type:

Dict

property requires_api_key: bool

Vertex AI uses ADC, not an API key.

update_prompt_template(template_name)[source]

Update the prompt template.

Parameters:

template_name (str | PromptTemplate)

validate_api_key()[source]

Vertex AI uses Google Cloud ADC, not an API key.

Return type:

bool

TesseractOCR

Offline, local OCR via pytesseract + Pillow. It is raw-only: it produces an OCRPage whose raw.text holds the transcription and whose interpretation is always None (a non-LLM engine cannot infer document type, summaries, or confidence in the same way). It needs no API key and honours the Tesseract-only config fields enhance_image and detect_layout. language is mapped to a Tesseract language code (e.g. "chinese"chi_sim+chi_tra), defaulting to English.

from doc2mark import OCR

ocr = OCR("tesseract", language="english", enhance_image=True)
r = ocr.read_one(scanned_png)
print(r.text)                       # transcription
print(r.document.interpretation)    # always None for Tesseract
class doc2mark.ocr.tesseract.TesseractOCR(api_key=None, config=None)[source]

Bases: BaseOCR

Tesseract-based OCR implementation.

Parameters:
  • api_key (str | None)

  • config (OCRConfig | None)

batch_process_images(images, max_workers=4, **kwargs)[source]

Process multiple images concurrently for better performance.

Parameters:
  • images (List[bytes]) – List of image data

  • max_workers (int) – Maximum number of concurrent workers (CPU-bound, so fewer workers)

  • **kwargs – Additional options

Returns:

List of OCR results in the same order as input

Return type:

List[OCRResult]

property pil

Lazy load PIL.

preprocess_image(image_data)[source]

Preprocess image for better OCR results.

Parameters:

image_data (bytes) – Raw image data

Returns:

Preprocessed image data

Return type:

bytes

property pytesseract

Lazy load pytesseract.

property requires_api_key: bool

Tesseract doesn’t require an API key.

validate_api_key()[source]

Tesseract doesn’t require an API key.

Return type:

bool