Skip to content

HTML import: images (data: URIs, a caller resolver, sized placeholders) (#1880) - #1897

Merged
yeroo merged 8 commits into
mainfrom
issue-1880-html-import-images-data-uris-cid
Nov 5, 2026
Merged

yeroo merged 8 commits into
mainfrom
issue-1880-html-import-images-data-uris-cid

Conversation

@yeroo

@yeroo yeroo commented Nov 5, 2026

Copy link
Copy Markdown
Owner

[linux]

Closes #1880.

HTML import now converts <img>. docxcore::import::import_html_with(bytes, &mut resolver) returns HtmlImport { document, media }, and HtmlImport::package() puts each image into a Markdown package as word/media/imageN.<ext>, with its rids renamed (cells and nested tables included). import_html is unchanged and converts no images.

What an <img> becomes

  • Source. A data: src is decoded here: percent escapes, then base64, with white space and mail's CRLF wrapping skipped. Any other src goes to the caller's resolver, once per distinct src.
  • Limits. The resolver is called at most 1000 times. It is told the most bytes it may return: the budget's room, 64 MiB per image, and what remains of 128 MiB in all. Only PNG, JPEG, GIF and BMP (by image::sniff) are embedded, and equal bytes share one part.
  • Size.
    • CSS lengths beat the attributes. auto clears a side; a CSS percentage leaves the attribute's size, so e-mail's width=600 style="width:100%" gives 600 px.
    • With one side given, the other follows the image's aspect; with none, its pixel size.
    • A picture wider than the 6.5" body is scaled down to it.
  • Unresolved.
    • Sized: a picture over one shared 1×1 #F2F2F2 PNG with a 1 px A6A6A6 outline and the alt as Alt Text (wp:docPr descr). The colours are by estimate, with no Word oracle.
    • Unsized: the alt text as a run.
    • With neither size nor alt: nothing.
  • Nothing made. A hidden image (display:none, mso-hide:all), a zero-sized one, and a list-marker image (<![if !supportLists]>) make nothing and are never resolved.
  • Word's markup. Word's <![if !vml]> <img> is read; its VML twin sits in a comment.
  • Fixes on the way. A picture is paragraph content: it is not cleared as white space, and it ends its paragraph before a block. take_leading_marker no longer drains a zero-width inline, and never takes a marker past a picture.

The suite

The converting child imports Web Pages through import_html_with and writes the package with its media, so an opened .htm shows its pictures.

Its resolver reads only a relative src under the page's own folder:

  • no URI scheme, file: included;
  • no absolute, rooted, drive, UNC or device path;
  • no : ? * < > | " or NUL after decoding escapes;
  • no .. out of the folder, and no name ending in a dot or a space;
  • no symlink or Windows reparse point on the way;
  • a regular file within the limit, its size checked before it is read.

cid:, http(s): and the rest are placeholders, so opening a page never reaches the network.

This is a deliberate tightening of the agreed plan, which allowed absolute local paths. Review round 1 showed two ways an absolute path reaches the network: macOS's default /net/<host> automounter, and Windows \??\UNC\…. Word, Outlook and browsers' Save Page all write relative page_files/… srcs.

Tests

The implementer ran:

  • cargo fmt --all;
  • cargo clippy --workspace --all-targets -D warnings and the same for the suite;
  • cargo test -p docxcore;
  • cargo test --manifest-path suite/Cargo.toml -p docxy-suite (2802 passed).

The cfg(windows) resolver test was not run here; it is for the Windows CI job.

Review

revmux (claude-only profile) ran two rounds.

  • Round 1: 2 Major (the resolver reaching the network: autofs /net, and Windows \??\ on a relative page folder) and 2 minor (docs; a budget test that failed before any image code ran). All fixed in ef3f1c6 and 628d48e.
  • Round 2: 0 Major, 1 minor (each declared side clamped at 2112 px on its own, distorting wide pictures) and 1 immaterial (a document clone). Both fixed in 21f13ac.
  • The planner's own check of 21f13ac then found a u64 overflow in the box arithmetic with uncapped sides, fixed in 826f06d.

The review stopped at round 2: no Major, all findings fixed.

Out of scope (follow-ups)

  • The suite drawing a placeholder's alt text.
  • Percentage sizes, max-width and floating images.
  • <base href> and MHTML.
  • SVG/WebP, background-image and <picture>/srcset.
  • <a> around images and links go to HTML import: <a href> links become hyperlinks #1879, and cid mapping to the mail host.

🤖 Generated with Claude Code

yeroo and others added 8 commits November 5, 2026 08:08
inline_picture_run_described adds wp:docPr descr (escaped, line breaks
and tabs as references) and an optional 1 px outline, for the HTML
import's pictures and placeholders. inline_picture_run delegates to it
and writes the same bytes as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
import_html_with(bytes, resolver) converts images: a data: src is
decoded, any other is asked of the caller's resolver (told how many
bytes it may return); bytes that are not PNG/JPEG/GIF/BMP, a refusal or
no room make a grey placeholder of the declared size with the alt as Alt
Text, or the alt text when unsized. Sizes: CSS lengths over the
attributes, auto clears a side, a percentage leaves the attribute's;
the other side by the aspect; capped at the body width. Word's
<![if !vml]> image is read; hidden, zero-sized and list-marker images
make nothing. HtmlImport::package puts the media in a Markdown package.

import_html is unchanged. A picture is content of its paragraph (no
whitespace clear, ends it before the next block), and a typed list
marker never takes a picture with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repo stores LF, which turned the mail fixture's CRLF-wrapped data:
URI into an LF one; mark the fixture -text, as the Word corpus is, and
check the wrap is there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1880)

The converting child imports HTML through import_html_with and writes
the package with its media. Its resolver reads only a local file beside
the page (local_image_path): no scheme but file:, no UNC, device or
file://host path, escaped or not, no drive-relative path; a regular file
within the import's limit, its size checked before it is read. cid:,
http(s): and the rest are placeholders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Looking at an absolute path can reach the network: an automounter's
/net/<host> (macOS's default, Linux autofs -hosts) or /afs contacts the
host it names, and on Windows an escaped \%3F%3F\UNC\... became \??\UNC
on a relative page folder. So image_src_parts takes only a relative src
(no scheme, file: included; no rooted, drive, UNC or device path; no
: ? * NUL once escapes are decoded; no .. out of the folder; no name
ending in a dot or space), the page folder is made absolute, and
read_local_image walks each step below it with symlink_metadata,
refusing a symlink or Windows reparse point. A deliberate tightening of
plan v2's absolute and file: paths; Word, Outlook and Save Page write
relative page_files/ srcs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the import (#1880 r1)

The old assertion ran out in the up-front passes, before any image code.
The new test's budget covers those and half of what the images spend: it
fails with images on and fits with them off, so decoding is charged and
running out is not swallowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out (#1880 r2)

An <img> side was clamped to a page's 31 680 twips (the width attribute's
and CSS length's table cap) before the box was scaled to the body, so
width=3000 height=1000 came out 9360 x 4431 twips. Image sides now
saturate at u32 instead (tables keep their cap), and the box scales as a
whole: 624 x 208 px.

The converting child moves the document out of the package instead of
cloning it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With image sides no longer capped (r2), a width of u32::MAX twips over a
1 x 1 000 000 pixel header made h = w*ph/pw about 4e15 and h*body passed
u64: a panic in debug, a wrap in release. picture_twips now computes in
u128 and saturates each side to u64; twips_emu clamps to MAX_EMU as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 98.96907% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docxcore/src/import/html_image.rs 98.69% 5 Missing ⚠️
docxcore/src/import/html.rs 99.04% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yeroo
yeroo merged commit 3526878 into main Nov 5, 2026
27 of 28 checks passed
@yeroo

yeroo commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

[linux] Merged as 3526878 after merge-check returned ok for head 826f06d.

  • Tests: the suite crate ran 2802 tests and the workspace tests ran on 826f06d, all green. CI is 14/14. ui sweep (linux) failed once on style-gallery.uit (dark/light tiles). The PR touches no ribbon, gallery or harness code, and the rerun passed.
  • Review: revmux ran two rounds.
    • Round 1: 2 Major, both the suite resolver reaching the network: autofs /net/<host>, and Windows \??\UNC on a relative page folder. Also 2 minor. All fixed; the resolver now takes only a relative src under the page's folder.
    • Round 2: 0 Major, 1 minor and 1 immaterial, both fixed.
    • The planner's own check found a u64 overflow, fixed in 826f06d.
    • No finding was deferred or disputed.
  • Follow-ups: the leftovers issue Leftovers from #1880: HTML import: images (data: URIs, cid:/http via a caller resolver, sized placeholders) #1898 holds four plan items (all minor, out of scope in the plan):
    • plan-alt: the suite drawing a placeholder's alt text;
    • plan-size: percentage sizes, max-width and floating images;
    • plan-base: <base href> and MHTML;
    • plan-fmt: SVG/WebP, background-image and <picture>/srcset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTML import: images (data: URIs, cid:/http via a caller resolver, sized placeholders)

2 participants