dm2xcod
DOCX to Markdown converter in Rust with Python bindings.
Table of Contents
- Why dm2xcod
- Requirements
- Installation
- Quick Start
- API Reference
- CLI Reference
- Architecture Overview
- Development
- License
Why dm2xcod
- Rust-based converter focused on predictable performance.
- Covers common DOCX structures: headings, lists, tables, notes, links, images.
- Supports image handling strategies: inline base64, save to directory, or skip.
- Exposes both CLI and Python (
PyO3) entry points. - Includes strict reference validation for footnote/comment/endnote integrity.
Requirements
- Rust
1.75+(building from source) - Python
3.12+(ABI3 wheel compatibility)
Installation
Python package
pip install dm2xcod
CLI (cargo)
cargo install dm2xcod
Rust library
[dependencies]
dm2xcod = "0.3"
Quick Start
CLI
# write to file
dm2xcod input.docx output.md
# print markdown to stdout
dm2xcod input.docx
Python
import dm2xcod
# path input
markdown = dm2xcod.convert_docx("document.docx")
print(markdown)
# bytes input
with open("document.docx", "rb") as f:
markdown = dm2xcod.convert_docx(f.read())
Rust
use dm2xcod::{ConvertOptions, DocxToMarkdown};
fn main() -> anyhow::Result<()> {
let converter = DocxToMarkdown::new(ConvertOptions::default());
let markdown = converter.convert("document.docx")?;
println!("{}", markdown);
Ok(())
}
API Reference
ConvertOptions
| Field | Type | Default | Description |
|---|---|---|---|
image_handling |
ImageHandling |
Inline |
Image output strategy |
preserve_whitespace |
bool |
false |
Preserve original spacing more strictly |
html_underline |
bool |
true |
Use HTML tags for underline output |
html_strikethrough |
bool |
false |
Use HTML tags for strikethrough output |
strict_reference_validation |
bool |
false |
Fail on unresolved note/comment references |
ImageHandling variants:
ImageHandling::InlineImageHandling::SaveToDir(PathBuf)ImageHandling::Skip
Example with non-default options:
use dm2xcod::{ConvertOptions, DocxToMarkdown, ImageHandling};
fn main() -> Result<(), dm2xcod::Error> {
let options = ConvertOptions {
image_handling: ImageHandling::SaveToDir("./images".into()),
preserve_whitespace: true,
html_underline: true,
html_strikethrough: true,
strict_reference_validation: true,
};
let converter = DocxToMarkdown::new(options);
let markdown = converter.convert("document.docx")?;
println!("{}", markdown);
Ok(())
}
Advanced: Custom extractor/renderer injection
DocxToMarkdown::with_components(options, extractor, renderer) lets you replace the default pipeline.
use dm2xcod::adapters::docx::AstExtractor;
use dm2xcod::converter::ConversionContext;
use dm2xcod::core::ast::{BlockNode, DocumentAst};
use dm2xcod::render::Renderer;
use dm2xcod::{ConvertOptions, DocxToMarkdown, Result};
use rs_docx::document::BodyContent;
#[derive(Debug, Default, Clone, Copy)]
struct MyExtractor;
impl AstExtractor for MyExtractor {
fn extract<'a>(
&self,
_body: &[BodyContent<'a>],
_context: &mut ConversionContext<'a>,
) -> Result<DocumentAst> {
Ok(DocumentAst {
blocks: vec![BlockNode::Paragraph("custom pipeline".to_string())],
references: Default::default(),
})
}
}
#[derive(Debug, Default, Clone, Copy)]
struct MyRenderer;
impl Renderer for MyRenderer {
fn render(&self, document: &DocumentAst) -> Result<String> {
Ok(format!("blocks={}", document.blocks.len()))
}
}
fn main() -> Result<()> {
let converter = DocxToMarkdown::with_components(
ConvertOptions::default(),
MyExtractor,
MyRenderer,
);
let output = converter.convert("document.docx")?;
println!("{}", output);
Ok(())
}
Python API
dm2xcod.convert_docx(input: str | bytes) -> str- Current Python entry point uses default conversion options.
CLI Reference
dm2xcod <INPUT> [OUTPUT] [--images-dir <DIR>] [--skip-images]
| Argument/Option | Description |
|---|---|
<INPUT> |
Input DOCX path (required) |
[OUTPUT] |
Output Markdown path (optional, otherwise stdout) |
--images-dir <DIR> |
Save extracted images to a directory |
--skip-images |
Skip image extraction/output |
Architecture Overview
Conversion pipeline:
- Parse DOCX (
rs_docx) - Build conversion context (relationships, numbering, styles, references, image strategy)
- Extract AST via adapter (
AstExtractor) - Validate references (optional strict mode)
- Render final markdown via renderer (
Renderer)
Project layout:
src/
adapters/ # Input adapters (DOCX -> AST extraction boundary)
core/ # Shared AST/model types
converter/ # Orchestration and conversion context
render/ # Markdown rendering + escaping
lib.rs # Public API (Rust + Python bindings)
main.rs # CLI entrypoint
Development
Build from source
# Rust library/CLI
cargo build --release
# Python extension in local env
pip install maturin
maturin develop --features python
Test and lint
cargo test --all-features
cargo clippy --all-features --tests -- -D warnings
Performance benchmark
# default: tests/aaa, 3 iterations, max 5 files
./scripts/run_perf_benchmark.sh
# custom: input_dir iterations max_files
./scripts/run_perf_benchmark.sh ./samples 5 10
Latest benchmark record (2026-02-14):
- Command:
./scripts/run_perf_benchmark.sh ./tests/aaa 10 10 - Threshold gate:
./scripts/check_perf_threshold.sh ./output_tests/perf/latest.json 15.0(pass) - Environment:
macOS 26.2 (Darwin arm64),rustc 1.92.0 (ded5c06cf 2025-12-08) - Result file:
output_tests/perf/latest.json
{"input_dir":"./tests/aaa","iterations":10,"files":2,"samples":20,"avg_ms":1.651,"min_ms":0.434,"max_ms":6.081,"total_ms":33.029,"overall_ms":33.034}
Performance threshold gate
# fails if avg_ms exceeds threshold
./scripts/check_perf_threshold.sh ./output_tests/perf/latest.json 15.0
Release notes
# auto-detect previous tag to HEAD
./scripts/generate_release_notes.sh
# explicit range and output file
./scripts/generate_release_notes.sh v0.3.9 v0.3.10 ./output_tests/release_notes.md
API stability policy
See docs/API_POLICY.md.
License
MIT
Metadata
Release files for dm2xcod 0.3.14
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| dm2xcod-0.3.14.tar.gz | 887.5 kB | Without provenance | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dm2xcod-0.3.14-cp312-abi3-win_amd64.whl | CPython 3.12 | abi3 | Windows x86-64 | Details |
| dm2xcod-0.3.14-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.12 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| dm2xcod-0.3.14-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl | CPython 3.12 | abi3 | macOS 10.12+ x86-64, macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64) | Details |
Total release size: 5.7 MB
Release files / dm2xcod-0.3.14.tar.gz
| Download URL | dm2xcod-0.3.14.tar.gz |
|---|---|
| Size | 887.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fd8d4015966caf6424bbc4afecdf819f0677f327addd8e579aeb2c150b31c729
|
|
BLAKE2b-256 checksum How to use checksums |
ec20c07e2e322f55000c0b0354bc5019e327195f300dff6b37947dc52f4b9ca8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.12.4
|
Release files / dm2xcod-0.3.14-cp312-abi3-win_amd64.whl
| Download URL | dm2xcod-0.3.14-cp312-abi3-win_amd64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | CPython 3.12 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1b9c757203a5962d08af0f6f19eb3b0ded1b1ec4b8eef768e8a0ae2cffb840b5
|
|
BLAKE2b-256 checksum How to use checksums |
1d833843614b9b154c9689a629108452b5feaec9e72fa10d9a7ddd1dc95f6f47
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.12.4
|
Release files / dm2xcod-0.3.14-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | dm2xcod-0.3.14-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | CPython 3.12 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
143a24d954d43f2d812393cb8e7d6ab20356c848f6167e77da8fa9b4f6a070d2
|
|
BLAKE2b-256 checksum How to use checksums |
8d6b44f1e300c52c8e0bb65df038012c252628a63f7a93650719adafd7430c9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.12.4
|
Release files / dm2xcod-0.3.14-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | dm2xcod-0.3.14-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.12 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
a9abd4d67c4f0265812165d256a31245fd3c28f5da68ba1ab34002ff9e8f01f5
|
|
BLAKE2b-256 checksum How to use checksums |
4aa4e72814bf40e7aaead6ef2d8113c3fc29cde8ef0a4a6afac6a49deb4e0ca4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.12.4
|
Security for dm2xcod 0.3.14
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.