{"id":44975749,"url":"https://github.com/developer0hye/anytomd-rs","last_synced_at":"2026-05-31T07:04:52.095Z","repository":{"id":339005298,"uuid":"1159988594","full_name":"developer0hye/anytomd-rs","owner":"developer0hye","description":"Pure Rust document-to-Markdown converter for LLM workflows (DOCX, PPTX, XLSX, HTML, CSV, JSON, XML, images).","archived":false,"fork":false,"pushed_at":"2026-04-28T11:35:40.000Z","size":662,"stargazers_count":38,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-23T00:37:31.008Z","etag":null,"topics":["anytomd","content-extraction","converter","csv","docx","html","image-extraction","json","llm","markdown","pptx","rust","text-processing","xlsx","xml"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/developer0hye.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-17T12:09:29.000Z","updated_at":"2026-05-22T15:13:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/developer0hye/anytomd-rs","commit_stats":null,"previous_names":["developer0hye/anytomd-rs"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/developer0hye/anytomd-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer0hye%2Fanytomd-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer0hye%2Fanytomd-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer0hye%2Fanytomd-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer0hye%2Fanytomd-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/developer0hye","download_url":"https://codeload.github.com/developer0hye/anytomd-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/developer0hye%2Fanytomd-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33720900,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anytomd","content-extraction","converter","csv","docx","html","image-extraction","json","llm","markdown","pptx","rust","text-processing","xlsx","xml"],"created_at":"2026-02-18T17:00:43.694Z","updated_at":"2026-05-31T07:04:52.089Z","avatar_url":"https://github.com/developer0hye.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# anytomd\n\nA pure Rust tool and library that converts various document formats into Markdown — designed for LLM consumption.\n\n[![CI](https://github.com/developer0hye/anytomd-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/developer0hye/anytomd-rs/actions/workflows/ci.yml)\n[![Crates.io](https://img.shields.io/crates/v/anytomd.svg)](https://crates.io/crates/anytomd)\n[![License](https://img.shields.io/crates/l/anytomd.svg)](LICENSE)\n\n## Why?\n\n[MarkItDown](https://github.com/microsoft/markitdown) is a great Python library for converting documents to Markdown. But integrating Python into Rust applications means bundling a Python runtime (~50 MB), dealing with cross-platform compatibility issues, and managing dependency hell.\n\n**anytomd** solves this with a single `cargo add anytomd` — zero external runtime, no C bindings, no subprocess calls. Just pure Rust.\n\n## Supported Formats\n\n| Format | Extensions | Notes |\n|--------|-----------|-------|\n| DOCX | `.docx` | Headings, tables, lists, bold/italic, hyperlinks, images, text boxes |\n| PPTX | `.pptx` | Slides, tables, speaker notes, images, group shapes |\n| XLSX | `.xlsx` | Multi-sheet, date/time handling, images |\n| XLS | `.xls` | Legacy Excel (via calamine) |\n| HTML | `.html`, `.htm` | Full DOM: headings, tables, lists, links, blockquotes, code blocks |\n| CSV | `.csv` | Converted to Markdown tables |\n| Jupyter Notebook | `.ipynb` | Markdown cells preserved, code cells in fenced blocks with language detection |\n| JSON | `.json` | Pretty-printed in fenced code blocks |\n| XML | `.xml` | Pretty-printed in fenced code blocks |\n| Images | `.png`, `.jpg`, `.gif`, `.webp`, `.bmp`, `.tiff`, `.svg`, `.heic`, `.avif` | Optional LLM-based alt text via `ImageDescriber` |\n| Code | `.py`, `.rs`, `.js`, `.ts`, `.c`, `.cpp`, `.go`, `.java`, `.rb`, `.swift`, `.sh`, ... | Fenced code blocks with language identifier |\n| Plain Text | `.txt`, `.md`, `.rst`, `.log`, `.toml`, `.yaml`, `.ini`, etc. | Passthrough with encoding detection (UTF-8, UTF-16, Windows-1252) |\n\n**Note on PDF:** PDF conversion is intentionally out of scope. Gemini, ChatGPT, and Claude already provide native PDF support (with plan/model-specific limits), so anytomd focuses on formats that still benefit from dedicated Markdown conversion. Attempting to convert a PDF will return a descriptive `FormatNotSupported` error.\n\nFormat is auto-detected from magic bytes and file extension. ZIP-based formats (DOCX/PPTX/XLSX) are distinguished by inspecting internal archive structure.\n\n## Conversion Examples\n\n### CSV\n\nA CSV file with multilingual data:\n\n```\nName,Age,City\nAlice,30,Seoul\nBob,25,東京\nCharlie,35,New York\n다영,28,서울\n```\n\n**Output:**\n\n```markdown\n| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seoul |\n| Bob | 25 | 東京 |\n| Charlie | 35 | New York |\n| 다영 | 28 | 서울 |\n```\n\n### DOCX\n\nA Word document with headings, links, Korean text, and emoji:\n\n**Output:**\n\n```markdown\n# Sample Document\n\nThis is a simple paragraph.\n\n## Section One\n\nVisit [Example](https://example.com) for more info.\n\nKorean: 한국어 테스트\n\nEmoji: 🚀✨🌍\n\n### Subsection\n\nFinal paragraph with mixed content.\n```\n\n### PPTX\n\nA PowerPoint presentation with slides, tables, speaker notes, and multilingual content:\n\n**Output:**\n\n```markdown\n## Slide 1: Sample Presentation\n\nWelcome to the presentation.\n\n---\n\n## Slide 2\n\nData Overview\n\n| Name | Value | Status |\n|---|---|---|\n| Alpha | 100 | Active |\n| Beta | 200 | Inactive |\n| Gamma | 300 | Active |\n\n\u003e Note: Remember to explain the data table.\n\n---\n\n## Slide 3: Multilingual\n\n한국어 테스트\n🚀✨🌍\n\n\u003e Note: Test multilingual rendering.\n```\n\n## Installation\n\n### Rust (Cargo)\n\n```sh\ncargo add anytomd\n```\n\n### npm (WASM)\n\n```sh\nnpm install anytomd\n```\n\n```js\nimport init, { convertBytes } from 'anytomd';\n\nawait init();\n\nconst response = await fetch('document.docx');\nconst bytes = new Uint8Array(await response.arrayBuffer());\n\nconst result = convertBytes(bytes, 'docx');\nconsole.log(result.markdown);\n```\n\n### Feature Flags\n\n| Feature | Dependencies | Description |\n|---------|-------------|-------------|\n| *(default)* | `async-gemini` | Async API + `AsyncGeminiDescriber` — all async features enabled out of the box |\n| `async` | `futures-util` | Async API (`convert_file_async`, `convert_bytes_async`, `AsyncImageDescriber` trait) |\n| `async-gemini` | `async` + `reqwest` | `AsyncGeminiDescriber` for concurrent image descriptions via Gemini |\n| `wasm` | `wasm-bindgen`, `js-sys`, `wasm-bindgen-futures` | WebAssembly bindings (`convertBytes`, `convertBytesWithOptions`) for browser/edge use |\n| `wasm` + `async-gemini` | *(combined)* | Adds `convertBytesWithGemini` for async Gemini-powered conversion in WASM |\n\nAsync features are included by default. To opt out:\n\n```toml\nanytomd = { version = \"1\", default-features = false }\n```\n\n## WebAssembly (WASM)\n\nanytomd compiles to `wasm32-unknown-unknown`, enabling client-side document conversion in browsers, Cloudflare Workers, Deno Deploy, and other edge runtimes. Documents never leave the user's device.\n\n### Build\n\n```sh\n# Basic WASM build (sync conversion only)\nwasm-pack build --target web --no-default-features --features wasm\n\n# With Gemini async image descriptions\nwasm-pack build --target web --no-default-features --features wasm,async-gemini\n```\n\n### Usage from JavaScript\n\n```js\nimport init, { convertBytes } from './pkg/anytomd.js';\n\nawait init();\n\nconst response = await fetch('document.docx');\nconst bytes = new Uint8Array(await response.arrayBuffer());\n\nconst result = convertBytes(bytes, 'docx');\nconsole.log(result.markdown);\nconsole.log(result.plainText);\nconsole.log(result.title);       // string or null\nconsole.log(result.warnings);    // string[]\n```\n\n#### With Gemini Image Descriptions (requires `wasm` + `async-gemini` features)\n\n```js\nimport init, { convertBytesWithGemini } from './pkg/anytomd.js';\n\nawait init();\n\nconst response = await fetch('presentation.pptx');\nconst bytes = new Uint8Array(await response.arrayBuffer());\n\n// Images are described concurrently via the Gemini API\nconst result = await convertBytesWithGemini(bytes, 'pptx', 'your-gemini-api-key');\nconsole.log(result.markdown);  // images have LLM-generated alt text\n```\n\n### WASM API Availability\n\n| API | Native | WASM |\n|-----|--------|------|\n| `convert_bytes` / `convertBytes` | Yes | Yes |\n| `convert_bytes_async` | Yes | Yes |\n| `convert_file` / `convert_file_async` | Yes | No (no filesystem) |\n| `GeminiDescriber` (sync) | Yes | No (uses `ureq`) |\n| `AsyncGeminiDescriber` / `convertBytesWithGemini` | Yes | Yes (`wasm` + `async-gemini`) |\n\nAll 12 format converters work on WASM via `convert_bytes`.\n\n## CLI\n\n### Install\n\n```sh\ncargo install anytomd\n```\n\n### Usage\n\n```sh\n# Convert a single file\nanytomd document.docx \u003e output.md\n\n# Convert multiple files (separated by \u003c!-- source: path --\u003e comments)\nanytomd report.docx data.csv slides.pptx \u003e combined.md\n\n# Write output to a file\nanytomd document.docx -o output.md\n\n# Read from stdin (--format is required)\ncat data.csv | anytomd --format csv\n\n# Override format detection\nanytomd --format html page.dat\n\n# Strict mode: treat recoverable errors as hard errors\nanytomd --strict document.docx\n\n# Plain text output (Markdown formatting stripped)\nanytomd --plain-text document.docx\n\n# Plain text from stdin\necho \"Name,Age\" | anytomd --format csv --plain-text\n\n# Extract comments (DOCX/PPTX) into an appended Comments section\nanytomd --extract-comments document.docx\n\n# Image descriptions via Gemini (requires GEMINI_API_KEY env var)\nexport GEMINI_API_KEY=your-key\nanytomd --gemini presentation.pptx\n\n# Use a specific Gemini model\nanytomd --gemini --gemini-model gemini-2.5-flash-lite presentation.pptx\n\n# Resource limits (defaults: 8GiB input, 4GiB images, 16GiB zip)\nanytomd --max-input-size 500MB document.docx\nanytomd --max-zip-size 2GiB archive.xlsx\n```\n\n### Exit Codes\n\n| Code | Meaning |\n|------|---------|\n| 0 | Success |\n| 1 | Conversion failure |\n| 2 | Invalid arguments |\n\n## Quick Start (Library)\n\n```rust\nuse anytomd::{convert_file, convert_bytes, ConversionOptions};\n\n// Convert a file (format auto-detected from extension and magic bytes)\nlet options = ConversionOptions::default();\nlet result = convert_file(\"document.docx\", \u0026options).unwrap();\nprintln!(\"{}\", result.markdown);\n\n// Convert raw bytes with an explicit format\nlet csv_data = b\"Name,Age\\nAlice,30\\nBob,25\";\nlet result = convert_bytes(csv_data, \"csv\", \u0026options).unwrap();\nprintln!(\"{}\", result.markdown);\n```\n\n### Plain Text Output\n\nEvery conversion produces both Markdown and plain text output. The plain text is extracted directly from the source document — no post-processing or markdown stripping — so source characters like `**kwargs` or `# comment` are preserved exactly.\n\n```rust\nuse anytomd::{convert_file, ConversionOptions};\n\nlet result = convert_file(\"document.docx\", \u0026ConversionOptions::default()).unwrap();\n\n// Markdown output\nprintln!(\"{}\", result.markdown);\n\n// Plain text output (no headings, bold, tables, code fences, etc.)\nprintln!(\"{}\", result.plain_text);\n```\n\n### Extracting Embedded Images\n\n```rust\nuse anytomd::{convert_file, ConversionOptions};\n\nlet options = ConversionOptions {\n    extract_images: true,\n    ..Default::default()\n};\nlet result = convert_file(\"presentation.pptx\", \u0026options).unwrap();\n\nfor (filename, bytes) in \u0026result.images {\n    std::fs::write(filename, bytes).unwrap();\n}\n```\n\n### Extracting Comments (DOCX / PPTX)\n\nSetting `extract_comments` appends a `# Comments` section to the end of the\noutput (both Markdown and plain text). Each comment records the commenter, the\ncomment body, and the source — the commented-on text for DOCX, or the slide\nlabel for PPTX (whose comments are anchored to a point, not a text span).\nReplies are flattened and marked `(reply)`; the flag is a no-op for other\nformats.\n\n```rust\nuse anytomd::{convert_file, ConversionOptions};\n\nlet options = ConversionOptions {\n    extract_comments: true,\n    ..Default::default()\n};\nlet result = convert_file(\"document.docx\", \u0026options).unwrap();\nprintln!(\"{}\", result.markdown);\n```\n\nExample appended section:\n\n```markdown\n# Comments\n\n## 1\n- **author**: Jane Smith (2024-01-15T09:30:00Z)\n- **comment**: Please revise this paragraph.\n- **source**: the quick brown fox\n```\n\nNotes:\n\n- **DOCX:** commenter identity comes from the comment author and date; the\n  source is the commented-on text (collapsed to one line, capped at 200\n  characters). Ranges in the body, headers, footers, footnotes, and endnotes\n  are all scanned. Threaded replies are detected via `commentsExtended.xml`.\n- **PPTX:** both the legacy (`commentAuthors.xml`) and modern\n  (`authors.xml` / threaded) comment schemes are supported. The source is the\n  slide label (e.g. `Slide 2: Quarterly Results`).\n\n### LLM-Based Image Descriptions\n\nanytomd can generate alt text for images using any LLM backend via the `ImageDescriber` trait. A built-in Google Gemini implementation is included.\n\n```rust\nuse std::sync::Arc;\nuse anytomd::{convert_file, ConversionOptions, ImageDescriber, ConvertError};\nuse anytomd::gemini::GeminiDescriber;\n\n// Option 1: Use the built-in Gemini describer\nlet describer = GeminiDescriber::from_env()  // reads GEMINI_API_KEY\n    .unwrap()\n    .with_model(\"gemini-3-flash-preview\".to_string());\n\nlet options = ConversionOptions {\n    image_describer: Some(Arc::new(describer)),\n    ..Default::default()\n};\nlet result = convert_file(\"document.docx\", \u0026options).unwrap();\n// Images now have LLM-generated alt text: ![A chart showing quarterly revenue](chart.png)\n\n// Option 2: Implement your own describer for any backend\nstruct MyDescriber;\n\nimpl ImageDescriber for MyDescriber {\n    fn describe(\n        \u0026self,\n        image_bytes: \u0026[u8],\n        mime_type: \u0026str,\n        prompt: \u0026str,\n    ) -\u003e Result\u003cString, ConvertError\u003e {\n        // Call your preferred LLM API here\n        Ok(\"description of the image\".to_string())\n    }\n}\n```\n\n### Async Image Descriptions\n\nFor documents with many images, the async API resolves all descriptions concurrently. Included by default since v0.11.0.\n\n```rust\nuse std::sync::Arc;\nuse anytomd::{convert_file_async, AsyncConversionOptions, AsyncImageDescriber, ConvertError};\nuse anytomd::gemini::AsyncGeminiDescriber;\n\n#[tokio::main]\nasync fn main() {\n    let describer = AsyncGeminiDescriber::from_env().unwrap();\n\n    let options = AsyncConversionOptions {\n        async_image_describer: Some(Arc::new(describer)),\n        ..Default::default()\n    };\n\n    let result = convert_file_async(\"presentation.pptx\", \u0026options).await.unwrap();\n    println!(\"{}\", result.markdown);\n    // All images described concurrently — significant speedup for multi-image documents\n}\n```\n\nThe library has no `tokio` dependency — the caller provides the async runtime. Any runtime (`tokio`, `async-std`, etc.) works.\n\n## API\n\n### `convert_file`\n\n```rust\n/// Convert a file at the given path to Markdown.\n/// Format is auto-detected from magic bytes and file extension.\npub fn convert_file(\n    path: impl AsRef\u003cPath\u003e,\n    options: \u0026ConversionOptions,\n) -\u003e Result\u003cConversionResult, ConvertError\u003e\n```\n\n### `convert_bytes`\n\n```rust\n/// Convert raw bytes to Markdown with an explicit format extension.\npub fn convert_bytes(\n    data: \u0026[u8],\n    extension: \u0026str,\n    options: \u0026ConversionOptions,\n) -\u003e Result\u003cConversionResult, ConvertError\u003e\n```\n\n### `convert_file_async`\n\nIncluded by default (requires the `async` feature if default features are disabled).\n\n```rust\n/// Convert a file at the given path to Markdown with async image description.\n/// If an async_image_describer is set, all image descriptions are resolved concurrently.\npub async fn convert_file_async(\n    path: impl AsRef\u003cPath\u003e,\n    options: \u0026AsyncConversionOptions,\n) -\u003e Result\u003cConversionResult, ConvertError\u003e\n```\n\n### `convert_bytes_async`\n\nIncluded by default (requires the `async` feature if default features are disabled).\n\n```rust\n/// Convert raw bytes to Markdown with async image description.\npub async fn convert_bytes_async(\n    data: \u0026[u8],\n    extension: \u0026str,\n    options: \u0026AsyncConversionOptions,\n) -\u003e Result\u003cConversionResult, ConvertError\u003e\n```\n\n### `ConversionOptions`\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `extract_images` | `bool` | `false` | Extract embedded images into `result.images` |\n| `extract_comments` | `bool` | `false` | Append a `# Comments` section (DOCX/PPTX only) |\n| `max_total_image_bytes` | `usize` | 50 MB | Hard cap for total extracted image bytes |\n| `max_input_bytes` | `usize` | 100 MB | Maximum input file size |\n| `max_uncompressed_zip_bytes` | `usize` | 500 MB | ZIP bomb guard |\n| `strict` | `bool` | `false` | Error on recoverable failures instead of warnings |\n| `image_describer` | `Option\u003cArc\u003cdyn ImageDescriber\u003e\u003e` | `None` | LLM backend for image alt text generation |\n\n### `ConversionResult`\n\n```rust\npub struct ConversionResult {\n    pub markdown: String,                  // The converted Markdown\n    pub plain_text: String,                // Plain text (extracted directly, no markdown syntax)\n    pub title: Option\u003cString\u003e,             // Document title, if detected\n    pub images: Vec\u003c(String, Vec\u003cu8\u003e)\u003e,    // Extracted images (filename, bytes)\n    pub warnings: Vec\u003cConversionWarning\u003e,  // Recoverable issues encountered\n}\n```\n\n### Error Handling\n\nConversion is **best-effort** by default. If a single element fails to parse (e.g., a corrupted table), it is skipped and a warning is added to `result.warnings`. The rest of the document is still converted.\n\nSet `strict: true` in `ConversionOptions` to turn recoverable failures into errors instead.\n\nWarning codes: `SkippedElement`, `UnsupportedFeature`, `ResourceLimitReached`, `MalformedSegment`.\n\n## Development\n\n### Build and Test\n\n```sh\ncargo build \u0026\u0026 cargo test \u0026\u0026 cargo clippy -- -D warnings\n```\n\n### Docker\n\nA Docker environment is available for reproducible Linux builds:\n\n```sh\ndocker compose run --rm verify    # Full loop: fmt + clippy + test + release build\ndocker compose run --rm test      # Run all tests\ndocker compose run --rm lint      # clippy + fmt check\ndocker compose run --rm shell     # Interactive bash\n```\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeveloper0hye%2Fanytomd-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeveloper0hye%2Fanytomd-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeveloper0hye%2Fanytomd-rs/lists"}