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)
providerProvider name (
"openai","vertex_ai","gemini","tesseract") or anOCRProviderenum member. Defaults to"openai".api_keyKeyword-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_kwargsAny field of
OCRConfig(model,task,language,detail,structured,max_concurrency, …). Two fields are coerced/validated eagerly for ergonomics: a stringtaskis mapped to the matchingTaskmember, anddetailmust be"raw"or"full". An unknown value raisesValueErrorimmediately, 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].
taskA single OCR intent applied to every image, overriding
config.task. Accepts aTaskor its string name.tasksA per-image list of intents for mixed batches. Its length must equal
len(images)or aValueErroris raised. When given,taskswins over the singletask.languageOutput-language hint appended to the prompt (overrides
config.language).structuredOverride of
config.structuredfor this call.Falseselects the legacy free-form path:OCRResult.textis filled andOCRResult.documentisNone.detail"full"(default) returnsrawandinterpretation;"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:
objectThe 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 toTask.RECEIPT, and an unknowntask/detailraises a clearValueError.- Parameters:
provider (str | OCRProvider)
api_key (str | None)
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 |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
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 |
|---|---|---|
|
|
Provider model id; |
|
|
Default intent for every image |
|
|
Output-language hint |
|
|
Sampling temperature |
|
|
Max response tokens |
|
|
OpenAI-compatible endpoint override |
|
|
Cap on concurrent image calls (see below) |
|
|
Structured output is the default |
|
|
|
|
|
BYO pydantic schema; |
|
|
|
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:
objectConfiguration 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)
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 |
|---|---|---|
|
|
Rendered markdown — always populated |
|
|
Self-confidence (LLM) or avg score (Tesseract) |
|
|
Detected/configured language |
|
|
Model, token usage, batch index, … |
|
|
Structured page, or |
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:
objectResult from OCR processing.
textis always populated (rendered fromdocument.rawwhen structured output is used) for backward compatibility.documentcarries the structuredOCRPagewhen available, orNonefor 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 |
|
Implementation |
|---|---|---|
|
|
|
|
|
|
|
|
alias → |
|
|
GEMINI is an alias: it is registered against the same
VertexAIOCR implementation, so OCR("gemini") and OCR("vertex_ai")
behave identically.
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:
objectFactory 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:
- 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:
ABCAbstract 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.
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:
BaseOCROpenAI 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
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:
BaseOCRGoogle 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]
- property requires_api_key: bool
Vertex AI uses ADC, not an API key.
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:
BaseOCRTesseract-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.