From 443615e77b7c0c5c40f4ffcf00612ee11c1fa06f Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:05 +0800 Subject: [PATCH 01/16] update README.md Signed-off-by: alexiskowalski --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed4cb9e4..a73400c6 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ let document = anydoc::to_document(&bytes, None)?; - **One output for every format.** Each format parses into a shared document model and renders through a single Markdown serializer, so escaping, tables, heading anchors, and footnotes behave identically whether the input was a `.doc` from 2003 or a `.pptx` from yesterday. - **Full document structure.** Headings with anchors, bold/italic/strikethrough, inline code and code blocks, links and internal cross-references, bulleted/numbered/nested/task lists with the source's own numbering, tables with merged cells and header rows, block quotes, footnotes and endnotes, and speaker notes. -- **Embedded assets.** Images and embedded objects render as their alt text in the Markdown, and the raw bytes stay available on the document model, tagged with their media type. Images with an external URL become ordinary Markdown images. +- **Embedded assets.** Images and embedded objects become Markdown image refs of the form `![alt](asset:N)`, where `N` indexes `Document.assets` (raw bytes + media type). Rewrite those hrefs after writing the files out, or consume the document model directly. Images with an external URL become ordinary Markdown images. - **Content-based format detection.** The format is read from the bytes themselves (PDF header, RTF open group, OLE stream names, ZIP package mimetype), so mislabeled files still convert correctly. - **Fast.** Pure Rust, no ML models, no external services. Median conversion time is under 5ms per document. - **Bindings that stay out of the way.** Node.js conversion runs on the libuv thread pool and never blocks the event loop; Python releases the GIL so other threads keep running. TypeScript types and Python stubs ship with the packages. From 9e14f3539b1111b50cc6682e9e38cc5369851d2c Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:07 +0800 Subject: [PATCH 02/16] update inline.rs Signed-off-by: alexiskowalski --- src/render/markdown/inline.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/render/markdown/inline.rs b/src/render/markdown/inline.rs index 1461e338..8eb3073a 100644 --- a/src/render/markdown/inline.rs +++ b/src/render/markdown/inline.rs @@ -164,10 +164,16 @@ fn render_image( escape_text(alt.trim(), ctx, EscapeOpts { in_label: true, ..Default::default() }); let _ = write!(out, "![{}]({})", alt, format_url(url)); } - // Embedded assets render as their alt text: Markdown cannot embed - // bytes, and the bytes stay available in `Document::assets`. A - // source-less image has only its alt text to offer. - ImageSource::Asset(_) | ImageSource::Unavailable => { + // Embedded assets cannot carry bytes in Markdown, but they keep a + // stable positional marker (`asset:N`) that indexes `Document::assets` + // so callers can rewrite the href after writing files out. + ImageSource::Asset(id) => { + let alt = + escape_text(alt.trim(), ctx, EscapeOpts { in_label: true, ..Default::default() }); + let _ = write!(out, "![{}](asset:{})", alt, id.0); + } + // A source-less image has only its alt text to offer. + ImageSource::Unavailable => { if !alt.trim().is_empty() { out.push_str(&escape_text( alt.trim(), From b9dcd668f4d999288692af1974ebc4a98e317406 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:09 +0800 Subject: [PATCH 03/16] update tests.rs Signed-off-by: alexiskowalski --- src/render/markdown/tests.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/render/markdown/tests.rs b/src/render/markdown/tests.rs index a4aa8067..e2195be7 100644 --- a/src/render/markdown/tests.rs +++ b/src/render/markdown/tests.rs @@ -165,6 +165,26 @@ fn sourceless_image_renders_alt_text() { assert_eq!(md, "chart\n"); } +#[test] +fn embedded_asset_image_emits_asset_href() { + use crate::model::AssetId; + let md = doc(vec![Block::Paragraph(vec![Inline::Image { + alt: "photo".into(), + source: ImageSource::Asset(AssetId(0)), + }])]); + assert_eq!(md, "![photo](asset:0)\n"); +} + +#[test] +fn embedded_asset_empty_alt_keeps_positional_marker() { + use crate::model::AssetId; + let md = doc(vec![Block::Paragraph(vec![Inline::Image { + alt: "".into(), + source: ImageSource::Asset(AssetId(3)), + }])]); + assert_eq!(md, "![](asset:3)\n"); +} + #[test] fn composite_marker_labels_are_escaped() { // M15: source-derived labels must not alter Markdown structure. From 6fa7f87e3f4f061d8b6adc38fd871d68d27261ba Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:12 +0800 Subject: [PATCH 04/16] update snapshots.rs Signed-off-by: alexiskowalski --- tests/snapshots.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/snapshots.rs b/tests/snapshots.rs index 477a1adc..ebf2b231 100644 --- a/tests/snapshots.rs +++ b/tests/snapshots.rs @@ -166,8 +166,8 @@ fn doc_inline_picture_is_retained() { ); } -/// The RTF `\pict` payload is retained as an asset (the Markdown output -/// shows only the alt text, which is empty for pictures without one). +/// The RTF `\pict` payload is retained as an asset (Markdown references it +/// as `asset:N` when the picture appears as an image inline). #[test] fn rtf_inline_picture_is_retained() { let bytes = std::fs::read(fixture_root().join("rtf").join("text.rtf")).unwrap(); From 9e49b672f53a81e74954ba38dd12f97e928cb7c0 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:13 +0800 Subject: [PATCH 05/16] update snapshots__doc__text.doc.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__doc__text.doc.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__doc__text.doc.snap b/tests/snapshots/snapshots__doc__text.doc.snap index b6c0ef11..73c4911b 100644 --- a/tests/snapshots/snapshots__doc__text.doc.snap +++ b/tests/snapshots/snapshots__doc__text.doc.snap @@ -66,7 +66,7 @@ Jump to the bookmarked paragraph. ## Objects -Inline image: done. +Inline image: ![](asset:0) done. Text box: after the box. From e41bf51a4bd17bbba2010f4b216fa0bfb09ce879 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:15 +0800 Subject: [PATCH 06/16] update snapshots__docx__handmade-manyrefs.docx.snap Signed-off-by: alexiskowalski --- ...apshots__docx__handmade-manyrefs.docx.snap | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tests/snapshots/snapshots__docx__handmade-manyrefs.docx.snap b/tests/snapshots/snapshots__docx__handmade-manyrefs.docx.snap index d6669ef6..bf51ac05 100644 --- a/tests/snapshots/snapshots__docx__handmade-manyrefs.docx.snap +++ b/tests/snapshots/snapshots__docx__handmade-manyrefs.docx.snap @@ -3,3 +3,143 @@ source: tests/snapshots.rs expression: output --- Seventy references to one image follow. + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) + +![](asset:0) From d240fa8291673fd04996a59536059fd9bceea498 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:17 +0800 Subject: [PATCH 07/16] update snapshots__docx__handmade-ole.docx.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__docx__handmade-ole.docx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__docx__handmade-ole.docx.snap b/tests/snapshots/snapshots__docx__handmade-ole.docx.snap index c9626e86..b9e155d2 100644 --- a/tests/snapshots/snapshots__docx__handmade-ole.docx.snap +++ b/tests/snapshots/snapshots__docx__handmade-ole.docx.snap @@ -4,4 +4,4 @@ expression: output --- Before the object -Embedded object: Excel.Sheet.12 +![Embedded object: Excel.Sheet.12](asset:0) From 2ce5b0ba03ebd0706d15d42efc5fbed97fbe8ea8 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:19 +0800 Subject: [PATCH 08/16] update snapshots__docx__handmade-rich.docx.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__docx__handmade-rich.docx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/snapshots/snapshots__docx__handmade-rich.docx.snap b/tests/snapshots/snapshots__docx__handmade-rich.docx.snap index 80103366..9114d992 100644 --- a/tests/snapshots/snapshots__docx__handmade-rich.docx.snap +++ b/tests/snapshots/snapshots__docx__handmade-rich.docx.snap @@ -15,8 +15,8 @@ Rich objects follow. - Build - Ship -tiny dot image +![tiny dot image](asset:0) -Embedded object: Excel.Sheet.12 +![Embedded object: Excel.Sheet.12](asset:1) After the objects. From cefa86ae9dd0df57f083bcb0d5759dc3945ee5c3 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:21 +0800 Subject: [PATCH 09/16] update snapshots__docx__text.docx.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__docx__text.docx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__docx__text.docx.snap b/tests/snapshots/snapshots__docx__text.docx.snap index 655b643e..f30ec86f 100644 --- a/tests/snapshots/snapshots__docx__text.docx.snap +++ b/tests/snapshots/snapshots__docx__text.docx.snap @@ -66,7 +66,7 @@ Jump to [the bookmarked paragraph](#plainmark). ## Objects -Inline image: tiny red dot done. +Inline image: ![tiny red dot](asset:0) done. Text box: after the box. From e81f173ed9ac21abf461fdc6e78e4423de155a28 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:23 +0800 Subject: [PATCH 10/16] update snapshots__epub__book.epub.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__epub__book.epub.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__epub__book.epub.snap b/tests/snapshots/snapshots__epub__book.epub.snap index 5e66bd3d..96bbaa9e 100644 --- a/tests/snapshots/snapshots__epub__book.epub.snap +++ b/tests/snapshots/snapshots__epub__book.epub.snap @@ -26,7 +26,7 @@ A list of things: See [Chapter Two](#epub-text-ch002-xhtml-chapter-two) for the table, or jump straight to [the marked paragraph](#epub-text-ch002-xhtml-markpoint). -tiny dot tiny dot +![tiny dot](asset:0) tiny dot From 350552195954d6713024e1b9fa44d075002692c6 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:24 +0800 Subject: [PATCH 11/16] update snapshots__malformed__corrupt-styles--skips.docx.snap Signed-off-by: alexiskowalski --- .../snapshots__malformed__corrupt-styles--skips.docx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__malformed__corrupt-styles--skips.docx.snap b/tests/snapshots/snapshots__malformed__corrupt-styles--skips.docx.snap index 750148a5..ac25bc1e 100644 --- a/tests/snapshots/snapshots__malformed__corrupt-styles--skips.docx.snap +++ b/tests/snapshots/snapshots__malformed__corrupt-styles--skips.docx.snap @@ -66,7 +66,7 @@ Jump to [the bookmarked paragraph](#plainmark). Objects -Inline image: tiny red dot done. +Inline image: ![tiny red dot](asset:0) done. Text box: after the box. From cc3e84bd9e5e6b8c7e2f599e34f8f0338c9a702f Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:26 +0800 Subject: [PATCH 12/16] update snapshots__malformed__missing-styles--skips.docx.snap Signed-off-by: alexiskowalski --- .../snapshots__malformed__missing-styles--skips.docx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__malformed__missing-styles--skips.docx.snap b/tests/snapshots/snapshots__malformed__missing-styles--skips.docx.snap index 750148a5..ac25bc1e 100644 --- a/tests/snapshots/snapshots__malformed__missing-styles--skips.docx.snap +++ b/tests/snapshots/snapshots__malformed__missing-styles--skips.docx.snap @@ -66,7 +66,7 @@ Jump to [the bookmarked paragraph](#plainmark). Objects -Inline image: tiny red dot done. +Inline image: ![tiny red dot](asset:0) done. Text box: after the box. From e3975be732800fcfde9d81315da3f9bf8599f039 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:28 +0800 Subject: [PATCH 13/16] update snapshots__odp__pres.odp.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__odp__pres.odp.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/snapshots/snapshots__odp__pres.odp.snap b/tests/snapshots/snapshots__odp__pres.odp.snap index d46d3477..e70d94bd 100644 --- a/tests/snapshots/snapshots__odp__pres.odp.snap +++ b/tests/snapshots/snapshots__odp__pres.odp.snap @@ -17,6 +17,8 @@ Numbers Slide | --- | --- | | North | 42 | +![](asset:0) + Grouped shapes below. Inside a group shape. From 36a941fad89773dd81c219df59c56d2905d341b6 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:30 +0800 Subject: [PATCH 14/16] update snapshots__odt__text.odt.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__odt__text.odt.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__odt__text.odt.snap b/tests/snapshots/snapshots__odt__text.odt.snap index 885706ee..820ce773 100644 --- a/tests/snapshots/snapshots__odt__text.odt.snap +++ b/tests/snapshots/snapshots__odt__text.odt.snap @@ -65,7 +65,7 @@ Jump to [the bookmarked paragraph](#plainmark). ## Objects -Inline image: tiny red dot done. +Inline image: ![tiny red dot](asset:0) done. Text box: after the box. From 94592d339d29cff3b9a293b1dce0ad79470cf830 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:31 +0800 Subject: [PATCH 15/16] update snapshots__pptx__handmade-order.pptx.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__pptx__handmade-order.pptx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__pptx__handmade-order.pptx.snap b/tests/snapshots/snapshots__pptx__handmade-order.pptx.snap index 782ed778..652da2e4 100644 --- a/tests/snapshots/snapshots__pptx__handmade-order.pptx.snap +++ b/tests/snapshots/snapshots__pptx__handmade-order.pptx.snap @@ -8,4 +8,4 @@ Kicker before the title Body after the title -Quarterly numbers +![Quarterly numbers](asset:0) From a72d760030f42533c05e7ac83297eeb0df4da894 Mon Sep 17 00:00:00 2001 From: alexiskowalski Date: Fri, 28 Aug 2026 20:34:33 +0800 Subject: [PATCH 16/16] update snapshots__rtf__text.rtf.snap Signed-off-by: alexiskowalski --- tests/snapshots/snapshots__rtf__text.rtf.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__rtf__text.rtf.snap b/tests/snapshots/snapshots__rtf__text.rtf.snap index f18ce8ea..8b3a8ccd 100644 --- a/tests/snapshots/snapshots__rtf__text.rtf.snap +++ b/tests/snapshots/snapshots__rtf__text.rtf.snap @@ -65,7 +65,7 @@ Jump to [the bookmarked paragraph](#plainmark). ## Objects -Inline image: done. +Inline image: ![](asset:0) done. Text box: Inside the text box.