From fba469d6a9346396abc60c7f9830c1ba6ee1d16a Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:21 +0800 Subject: [PATCH 01/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- 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 1461e33..8eb3073 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 2bd8538c4cf77102e6303db45bd5627170bf34a3 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:22 +0800 Subject: [PATCH 02/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- 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 a4aa806..e2195be 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 e83882de79e3288ed1eecba3286684b8ed5b6f57 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:24 +0800 Subject: [PATCH 03/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed4cb9e..a73400c 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 a88a24519b966a09fa3ff527e2b9cbeb99a43b22 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:27 +0800 Subject: [PATCH 04/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- tests/snapshots.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/snapshots.rs b/tests/snapshots.rs index 477a1ad..ebf2b23 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 d721bef044e427cb385010ae53d4567442beea53 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:28 +0800 Subject: [PATCH 05/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- tests/snapshots/snapshots__docx__handmade-rich.docx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/snapshots__docx__handmade-rich.docx.snap b/tests/snapshots/snapshots__docx__handmade-rich.docx.snap index 8010336..38580ca 100644 --- a/tests/snapshots/snapshots__docx__handmade-rich.docx.snap +++ b/tests/snapshots/snapshots__docx__handmade-rich.docx.snap @@ -15,7 +15,7 @@ Rich objects follow. - Build - Ship -tiny dot image +![tiny dot image](asset:0) Embedded object: Excel.Sheet.12 From ab65cd3788e702bdc1c631eaa91fbbedf32a763d Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:30 +0800 Subject: [PATCH 06/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- 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 655b643..f30ec86 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 de91c4484b6f68bb510617c1d3bf4d31f315cd2b Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:31 +0800 Subject: [PATCH 07/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- ...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 d6669ef..bf51ac0 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 09fcc2123dba365932604b87fd18cd62766907f5 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:33 +0800 Subject: [PATCH 08/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- 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 b6c0ef1..73c4911 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 439699d308eca15ee21150fd1b5cf689f761ae99 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:35 +0800 Subject: [PATCH 09/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- 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 5e66bd3..c0dd0de 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](asset:0) From d8082ec4d7f51b37eeddeabc65b5a9bbb5d62a68 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:37 +0800 Subject: [PATCH 10/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- .../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 750148a..ac25bc1 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 5cc55ef44e36abb5a03250d5cfc7956614e82224 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:39 +0800 Subject: [PATCH 11/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- .../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 750148a..ac25bc1 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 845093a1421ebdad67020e3e2c7a2b63e5cea314 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:41 +0800 Subject: [PATCH 12/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- 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 885706e..820ce77 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 98064f9b89cc25cfd87cd14d54fa4afa82250d19 Mon Sep 17 00:00:00 2001 From: f1vwpbleiia7ie Date: Sat, 8 Aug 2026 22:55:43 +0800 Subject: [PATCH 13/13] feat(markdown): emit asset:N hrefs for embedded images Signed-off-by: f1vwpbleiia7ie --- 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 f18ce8e..8b3a8cc 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.