Loading documents
The loader
UnifiedDocumentLoader holds the configuration that applies to every document:
the OCR provider and its settings, the table style, the caches and the optional judge. Create it
once and call load() for each file (the batch methods
below call it from several threads at once).
from doc2mark import UnifiedDocumentLoader
loader = UnifiedDocumentLoader(
ocr_provider="tesseract", # "openai" (default), "vertex_ai", "tesseract", None
table_style="markdown_grid", # "minimal_html" (default), "markdown_grid", "styled_html"
cache_dir=".doc2mark-cache", # reuse converted documents across runs
)
result = loader.load("report.pdf")
print(result.content[:200])
The most used constructor arguments (the API reference has all of them):
ocr_provider"openai"(default),"vertex_ai"(also"gemini"),"tesseract", anOCRProvider, a provider instance, orNone/"none"for no OCR. The provider is only used when a document is loaded withocr_images=True.api_key,model,base_url,project,location,temperature,max_tokensProvider settings (OCR).
modeldefaults togpt-5.4-minifor OpenAI and togemini-3.1-flash-lite-previewfor Vertex AI.ocr_configAn
OCRConfigfor everything else about OCR (task, language, structured output, concurrency, neighbour-page context, non-content judge).task,structuredanddetailcan also be given directly.table_styleHow tables with merged cells are written (Tables).
cache_dir,ocr_cacheThe converted-document cache and the OCR answer cache (Caching).
judge,legibility_judge,boilerplate_judgeThe optional judge (Optional judge (TypeSafe / Jev)).
Loading one file
from doc2mark import UnifiedDocumentLoader
loader = UnifiedDocumentLoader(ocr_provider=None)
result = loader.load("data.csv", encoding="utf-8")
print(result.metadata.row_count, result.metadata.column_count)
load(file_path, output_format="markdown", extract_images=False, ocr_images=False,
show_progress=False, encoding="utf-8", delimiter=None):
output_format"markdown","json"or"text"(The result: ProcessedDocument).extract_imagesReturn the pictures of PDF, Word, Excel and PowerPoint files: without OCR they are embedded in the Markdown as
data:URI images, areimageitems ofjson_contentand are listed inimages. For an image file, the file itself re-encoded as PNG (Images).ocr_imagesOCR what needs it: scanned or garbled pages and the pictures that carry something to read (When pages and pictures are OCR’d), and image files. When an OCR provider is configured it implies
extract_images=True. It has no effect on text, CSV, JSON, HTML, XML, Markdown and e-mail files.encoding,delimiterencodingis used for text, CSV, JSON and markup files (defaultutf-8, not detected).delimiter(one character) is the delimiter of CSV files; without it the delimiter is detected. A.tsvfile is always tab separated.
Convenience functions
For scripts, the module-level functions create a loader for one call:
from doc2mark import document_to_markdown, load
result = load("report.docx")
markdown = document_to_markdown("report.docx", output_path="out/report.md")
load(), document_to_markdown(),
batch_convert_to_markdown(), batch_process_documents() and
batch_process_files take ocr_provider (default "openai"), api_key and
ocr_cache for the loader, and route every other keyword argument where it belongs: an option
of load() (encoding, delimiter, show_progress) or of the batch method
(encoding, delimiter, max_workers, progress_callback) goes to that call, every
other UnifiedDocumentLoader setting (table_style, model,
cache_dir, judge, …) to the loader. So load("report.pdf",
table_style="markdown_grid") and load("report.pdf", cache_dir=".cache") work, and in a
batch function max_workers is the number of files converted at once, not the OCR
concurrency (set that on a loader of your own). A name that is neither raises TypeError.
Folders and file lists
from doc2mark import UnifiedDocumentLoader
loader = UnifiedDocumentLoader(ocr_provider=None)
results = loader.batch_process(
"documents",
output_dir="converted",
max_workers=4,
progress_callback=lambda done, total, path: print(f"{done}/{total} {path}"),
)
failed = {path: info["error"] for path, info in results.items() if info["status"] == "failed"}
print(len(results), "files,", len(failed), "failed")
batch_process() finds every file under input_dir
(recursive=True by default) that load() accepts,
whatever the case of its extension (.PDF, .htm and .markdown included; folders are
never converted themselves), in path order, and converts it. With save_files=True (the
default) it writes <name>.md (or .json), keeping the folder structure and the dots of the
file name (v1.2.txt gives v1.2.md). With an output_dir of its own, files that would
share an output name (report.txt and report.md) are written as report.txt.md and
report.md.md, as in the CLI, and an output_dir inside input_dir is left out of the
input files. Without one (next to the inputs) an output replaces the one an earlier run wrote, so
the run can be repeated, but it is never written over the file it was converted from (a Markdown
file keeps its front matter), and two files of one folder with the same stem write the same
file. When pictures were extracted it also writes a <name>_images/ folder. With
output_format="text" no file is written. Without output_dir the files are written next to
the inputs. One file failing does not stop the batch.
The returned dict maps each input path (a string, in input order) to a result:
{"status": "success", "format": "pdf", "content_length": 8120, "duration": 0.41,
"output_files": ["converted/report.md"],
"metadata": {"images_extracted": 0, "tables_found": 0, "pages": 2}}
{"status": "failed", "error": "Processing failed: ...", "format": ".pdf"}
tables_found is the number of tables of the document (ProcessedDocument.tables, see
The result: ProcessedDocument); it is 0 for files without content items (text, data, markup and e-mail files).
batch_process_files() takes a list of paths instead. It
writes only when output_dir is given, as output_dir/<file stem>.md (inputs with the same
stem, such as q1.pdf of two folders, are written as q1.pdf.md and q1.pdf-2.md), and
its results have no pages.
max_workers above 1 converts that many files at once in threads (None, the default,
converts them one after the other); results keep the input order. It is separate from the OCR
concurrency inside one document (OCRConfig.max_concurrency). progress_callback(done, total,
path) is called after each file, failed or not.
load_directory() returns the
ProcessedDocument objects of a folder instead of writing files; files it cannot
convert are skipped with a warning:
from doc2mark import UnifiedDocumentLoader
loader = UnifiedDocumentLoader(ocr_provider=None)
documents = loader.load_directory("documents", pattern="*.pdf")
print([d.metadata.filename for d in documents])