mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-19 02:10:15 -05:00
A floating picture or VML shape in a header table cell is placed on the page behind the text. Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
9 files changed
+244
-17
No files matched your search
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (progress log: a header-cell picture watermark sits behind the text, #1775; previously trivial: symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: progress log: pasted blockquotes are indented paragraphs, #1270; previously trivial: progress log: print-area strings translated in every catalog; previously trivial: progress log: comments readable in the dark theme, #2045; previously trivial: progress log: ODT sections, #1379; previously trivial: progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls, #1329; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
|
||||
|
||||
@@ -176,6 +176,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
|
||||
| 2026-10-15 | Spelling as you type no longer flags the word being typed; the squiggle comes when the word ends or the caret leaves it, as in Word (#1552) |
|
||||
| 2026-10-15 | Text Highlight Color: No Color becomes the Highlight button's choice, as in Word, so the next click on the button removes highlighting instead of applying the colour picked before; the ribbon and the mini toolbar share one menu (#2105) |
|
||||
| 2026-10-16 | Footnote and endnote editing keeps the body and active note text at normal opacity (#2545), without changing document colours or print/PDF output. |
|
||||
| 2026-10-16 | A picture watermark in a header that starts with a table is drawn centred on the page behind the body text, on every page, instead of in the header's cell at the top of the page (#1775); `layoutInCell="0"` and VML `o:allowincell="f"` survive save and reopen |
|
||||
|---|---|
|
||||
| 2026-10-16 | Save Changes gives assistive technology a named alert dialog with the loss warning and any save failure; visual layout and save policy unchanged (#1015). Native screen-reader verification remains pending. |
|
||||
| 2026-10-15 | The font family caches are bounded (4096 families for text, 1024 for math, names over 256 bytes aren't remembered), so a document naming thousands of missing fonts can't grow memory; equation fonts are looked up again after a font is added or removed instead of keeping the old choice (#1354) |
|
||||
@@ -500,6 +501,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
|
||||
| 2026-10-15 | trivial | Progress log: image size limits checked before allocation (#1321) |
|
||||
| 2026-10-17 | trivial | Progress log: Read Aloud speaks each sentence in its own language, #1843 |
|
||||
| 2026-10-18 | trivial | Progress log: `wordcraft-cli run --script` (#1963) |
|
||||
| 2026-10-18 | trivial | Progress log: header-cell picture watermarks drawn behind the text (#1775); readiness estimates unchanged |
|
||||
| 2026-10-17 | trivial | Language dialog keeps inherited proofing language and no-proof choices (#2717); readiness estimates unchanged |
|
||||
| 2026-10-14 | trivial | Progress log: ASK and SET fields show nothing (#1765) |
|
||||
| 2026-10-15 | trivial | Progress log: at-least table rows start the next page whole (#1742) |
|
||||
|
||||
@@ -216,6 +216,10 @@ pub struct Float {
|
||||
/// laid out, as Word for Windows does (Word for Mac draws hidden VML shapes).
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub hidden: bool,
|
||||
/// Anchored in a table cell but placed on the page, not in the cell (`wp:anchor/@layoutInCell`
|
||||
/// off, any VML shape in a header or footer): a picture watermark in a header that starts with a table.
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub outside_cell: bool,
|
||||
}
|
||||
|
||||
/// Longest alternative text kept (characters): hostile files and commands are capped.
|
||||
|
||||
@@ -1510,7 +1510,7 @@ impl Reader<'_> {
|
||||
let style = grp.attr("style").unwrap_or("");
|
||||
let len = |k: &str| vml_style(style, k).and_then(|v| measure(v, 1.0)).unwrap_or(0.0).clamp(0.0, crate::units::MAX_LEN_PT);
|
||||
let (w, h) = (len("width"), len("height"));
|
||||
let float = vml_float(grp, style);
|
||||
let float = vml_float(grp, style, sc.in_hf);
|
||||
// The members' space; without a usable `coordsize` it is the group's own box.
|
||||
let (ch_w, ch_h) = vml_coordpair(grp, "coordsize").filter(|(cw, ch)| *cw > 0.0 && *ch > 0.0).unwrap_or((w.max(1.0), h.max(1.0)));
|
||||
let (cx, cy) = vml_coordpair(grp, "coordorigin").unwrap_or((0.0, 0.0));
|
||||
@@ -1590,7 +1590,7 @@ impl Reader<'_> {
|
||||
f.set_spin(vml_spin(style));
|
||||
f
|
||||
}
|
||||
None => vml_float(shape, style),
|
||||
None => vml_float(shape, style, sc.in_hf),
|
||||
};
|
||||
if let Some(img) = shape.find("v:imagedata") {
|
||||
let id = img.attr("r:id").or_else(|| img.attr("r:pict"))?;
|
||||
@@ -1930,6 +1930,8 @@ fn anchor_float(c: &El) -> Float {
|
||||
f.dist = dist("distL").max(dist("distR"));
|
||||
f.dist_top = dist("distT");
|
||||
f.dist_bottom = dist("distB");
|
||||
// Required by the schema; a file leaving it out lays out in the cell, as before.
|
||||
f.outside_cell = !on_off_attr(c, "layoutInCell") && c.attr("layoutInCell").is_some();
|
||||
f
|
||||
}
|
||||
|
||||
@@ -2000,7 +2002,7 @@ fn vml_style<'a>(style: &'a str, key: &str) -> Option<&'a str> {
|
||||
/// `mso-position-horizontal` / `mso-position-vertical`), wrapped as its `w10:wrap` says. Without
|
||||
/// wrapping (`none`, or no `w10:wrap`) it's in front of the text, or behind it at a negative
|
||||
/// `z-index`. Anything else stays inline.
|
||||
fn vml_float(shape: &El, style: &str) -> Float {
|
||||
fn vml_float(shape: &El, style: &str, in_hf: bool) -> Float {
|
||||
let get = |k: &str| vml_style(style, k);
|
||||
let hidden = get("visibility").is_some_and(|v| v.eq_ignore_ascii_case("hidden"));
|
||||
if !get("position").is_some_and(|p| p.eq_ignore_ascii_case("absolute")) {
|
||||
@@ -2053,6 +2055,9 @@ fn vml_float(shape: &El, style: &str) -> Float {
|
||||
dist_top: dist("mso-wrap-distance-top"),
|
||||
dist_bottom: dist("mso-wrap-distance-bottom"),
|
||||
hidden,
|
||||
// Word for Mac draws a VML shape anchored in a header table cell on the page whatever
|
||||
// `o:allowincell` says (user-checked), as for Word's picture and text watermarks.
|
||||
outside_cell: in_hf,
|
||||
..Float::default()
|
||||
};
|
||||
f.set_spin(vml_spin(style));
|
||||
|
||||
@@ -1191,7 +1191,7 @@ impl Writer<'_> {
|
||||
("relativeHeight", &self.z.to_string()),
|
||||
("behindDoc", behind),
|
||||
("locked", "0"),
|
||||
("layoutInCell", "1"),
|
||||
("layoutInCell", if float.outside_cell { "0" } else { "1" }),
|
||||
("allowOverlap", "1"),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1169,6 +1169,79 @@ fn word_watermark_survives_save() {
|
||||
assert_eq!(d2.settings.watermark, None);
|
||||
}
|
||||
|
||||
/// Issue #1775: a picture watermark in a header that starts with a table is anchored in a cell
|
||||
/// but placed on the page: any VML shape in a header (Word's picture watermark, `o:allowincell="f"`; user-checked
|
||||
/// in Word for Mac, the attribute makes no difference) or DrawingML `layoutInCell="0"`. That survives save and reopen; a shape laid out in its cell stays so.
|
||||
#[test]
|
||||
fn header_cell_picture_placed_on_the_page() {
|
||||
let vml = |allow: &str| {
|
||||
format!(
|
||||
r##"<w:r><w:pict><v:shape id="WordPictureWatermark1" o:spid="_x0000_s2049" type="#_x0000_t75" style="position:absolute;margin-left:0;margin-top:0;width:300pt;height:200pt;z-index:-251657216;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin" {allow}><v:imagedata r:id="rIdP" o:title="" gain="19661f" blacklevel="22938f"/></v:shape></w:pict></w:r>"##
|
||||
)
|
||||
};
|
||||
let anchor = |in_cell: &str| {
|
||||
format!(
|
||||
r#"<w:r><w:drawing><wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0" relativeHeight="1" behindDoc="1" locked="0" layoutInCell="{in_cell}" allowOverlap="1"><wp:simplePos x="0" y="0"/><wp:positionH relativeFrom="margin"><wp:align>center</wp:align></wp:positionH><wp:positionV relativeFrom="margin"><wp:align>center</wp:align></wp:positionV><wp:extent cx="3810000" cy="2540000"/><wp:wrapNone/><wp:docPr id="1" name="Picture 1"/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="1" name="p.png"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rIdP"/></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="3810000" cy="2540000"/></a:xfrm><a:prstGeom prst="rect"/></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:anchor></w:drawing></w:r>"#
|
||||
)
|
||||
};
|
||||
let read = |run: &str| {
|
||||
let hdr = format!(
|
||||
r#"<w:hdr {W_NS}><w:tbl><w:tblGrid><w:gridCol w:w="4680"/><w:gridCol w:w="4680"/></w:tblGrid><w:tr><w:tc><w:p><w:r><w:t>Left</w:t></w:r></w:p></w:tc><w:tc><w:p>{run}<w:r><w:t>Right</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:p/></w:hdr>"#
|
||||
);
|
||||
let hrels = rels(&[("rIdP", "image", "media/p.png")]);
|
||||
let body = r#"<w:p><w:r><w:t>Body</w:t></w:r></w:p><w:sectPr><w:headerReference w:type="default" r:id="rIdH"/></w:sectPr>"#;
|
||||
let parts = [("word/header1.xml", hdr.as_str()), ("word/_rels/header1.xml.rels", hrels.as_str()), ("word/media/p.png", "\u{89}PNG stand-in")];
|
||||
wordcraft_docx::read(&docx(body, &[("rIdH", "header", "header1.xml")], &parts)).unwrap()
|
||||
};
|
||||
let floats = |d: &Document| -> Vec<(bool, Wrap)> {
|
||||
let hid = d.last_section.headers.default.unwrap();
|
||||
let mut out = Vec::new();
|
||||
for b in &d.parts[&hid].blocks {
|
||||
if let Block::Table(t) = &**b {
|
||||
for c in &t.rows[0].cells {
|
||||
for p in c.blocks.iter().filter_map(|b| b.as_para()) {
|
||||
out.extend(p.objects.iter().filter_map(|o| o.frame()).map(|(_, _, f)| (f.outside_cell, f.wrap)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
for (run, outside) in [
|
||||
(vml(r#"o:allowincell="f""#), true),
|
||||
(vml(r#"o:allowincell="false""#), true),
|
||||
// Word for Mac draws every VML picture in a header cell on the page, ticked or not.
|
||||
(vml(""), true),
|
||||
(vml(r#"o:allowincell="t""#), true),
|
||||
(anchor("0"), true),
|
||||
(anchor("false"), true),
|
||||
(anchor("1"), false),
|
||||
] {
|
||||
let d = read(&run);
|
||||
assert_eq!(floats(&d), vec![(outside, Wrap::BehindText)], "{run}");
|
||||
let d2 = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap();
|
||||
assert_eq!(floats(&d2), vec![(outside, Wrap::BehindText)], "saved: {run}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue #1775: only header and footer shapes are placed on the page from a cell (what was checked
|
||||
/// in Word); a VML picture in a body table cell stays laid out in its cell.
|
||||
#[test]
|
||||
fn body_cell_vml_picture_stays_in_its_cell() {
|
||||
let pic = r##"<w:r><w:pict><v:shape type="#_x0000_t75" style="position:absolute;margin-left:0;margin-top:0;width:100pt;height:80pt;z-index:-1;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin"><v:imagedata r:id="rIdP"/></v:shape></w:pict></w:r>"##;
|
||||
let body = format!(r#"<w:tbl><w:tblGrid><w:gridCol w:w="4680"/></w:tblGrid><w:tr><w:tc><w:p>{pic}</w:p></w:tc></w:tr></w:tbl><w:p/>"#);
|
||||
let parts = [("word/media/p.png", "\u{89}PNG stand-in")];
|
||||
let d = wordcraft_docx::read(&docx(&body, &[("rIdP", "image", "media/p.png")], &parts)).unwrap();
|
||||
let Block::Table(t) = &*d.body[0] else { panic!("table first") };
|
||||
let floats: Vec<bool> = t.rows[0].cells[0]
|
||||
.blocks
|
||||
.iter()
|
||||
.filter_map(|b| b.as_para())
|
||||
.flat_map(|p| p.objects.iter().filter_map(|o| o.frame()).map(|(_, _, f)| f.outside_cell))
|
||||
.collect();
|
||||
assert_eq!(floats, vec![false]);
|
||||
}
|
||||
|
||||
/// Issue #149: an absolutely positioned VML text box in a header floats where its style puts it
|
||||
/// (relative to the page here, behind the text at a negative z-index, no wrapping) instead of
|
||||
/// sitting inline in the header's flow, and it keeps that placement when saved and read back.
|
||||
|
||||
@@ -699,6 +699,57 @@ struct Ctx<'a> {
|
||||
/// Cache-key hashes of the table-style text formatting cells were laid out with
|
||||
/// ([`Ctx::table_key`]).
|
||||
table_keys: Vec<(para::CellText, u64)>,
|
||||
/// While a header or footer with tables is laid out: its page, for floats in its cells placed
|
||||
/// on the page (see [`with_pin`]).
|
||||
pin: Option<Pin>,
|
||||
}
|
||||
|
||||
/// A header or footer (`story`) laid out for page `number` of a section (`sect`, `mirror`): its
|
||||
/// table cells' floats placed on the page rather than in their cell ([`Float::outside_cell`],
|
||||
/// Word's picture watermark in a header that starts with a table), drawn behind the header's
|
||||
/// text (`behind`) or in front, in page coordinates. Once each (`seen`): a cell may be laid out
|
||||
/// more than once.
|
||||
struct Pin {
|
||||
story: StoryRef,
|
||||
sect: SectionProps,
|
||||
number: u32,
|
||||
mirror: bool,
|
||||
behind: Vec<Placed>,
|
||||
front: Vec<Placed>,
|
||||
seen: HashSet<(Vec<u32>, usize)>,
|
||||
}
|
||||
|
||||
/// Whether a float's position on both axes is measured on the page (from its edges or margins),
|
||||
/// not from its column, paragraph, character or line: one that can be placed on the page from
|
||||
/// anywhere.
|
||||
fn page_anchored(f: &Float) -> bool {
|
||||
use Anchor::*;
|
||||
matches!(f.h_rel, Page | Margin | LeftMargin | RightMargin | InsideMargin | OutsideMargin)
|
||||
&& matches!(f.v_rel, Page | Margin | TopMargin | BottomMargin | InsideMargin | OutsideMargin)
|
||||
}
|
||||
|
||||
/// Run `f`, a layout of header or footer `id` for page `number` of `sect`, with the floats in its
|
||||
/// table cells that are placed on the page put there (see [`Pin`]). Returns `f`'s result and those
|
||||
/// floats' items, in page coordinates: behind the header's text, and in front.
|
||||
fn with_pin<T>(ctx: &mut Ctx, id: u32, sect: &SectionProps, number: u32, f: impl FnOnce(&mut Ctx) -> T) -> (T, Vec<Placed>, Vec<Placed>) {
|
||||
let tables = ctx.doc.parts.get(&id).is_some_and(|p| p.blocks.iter().any(|b| matches!(**b, Block::Table(_))));
|
||||
if !tables {
|
||||
return (f(ctx), Vec::new(), Vec::new());
|
||||
}
|
||||
let pin = Pin {
|
||||
story: StoryRef::Part(id),
|
||||
sect: sect.clone(),
|
||||
number,
|
||||
mirror: ctx.mirror,
|
||||
behind: Vec::new(),
|
||||
front: Vec::new(),
|
||||
seen: HashSet::new(),
|
||||
};
|
||||
let outer = ctx.pin.replace(pin);
|
||||
let out = f(ctx);
|
||||
let pin = std::mem::replace(&mut ctx.pin, outer);
|
||||
let (behind, front) = pin.map(|p| (p.behind, p.front)).unwrap_or_default();
|
||||
(out, behind, front)
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
@@ -1144,12 +1195,19 @@ fn layout_box(
|
||||
// Floating objects anchored here: place them, then wrap the text around them.
|
||||
// One left out of the layout (hidden, or deleted in the final text) takes no room.
|
||||
let mut floats = HashMap::new();
|
||||
// A header or footer cell's floats placed on the page: no room taken in the cell.
|
||||
let mut pinned = HashMap::new();
|
||||
let left_out = left_out_objects(ctx.doc, p, table.map(|t| &t.chr), ctx.opts.show_hidden, ctx.hide_deleted);
|
||||
for (oi, o) in p.objects.iter().enumerate() {
|
||||
let Some((w, h, float)) = floating(o) else { continue };
|
||||
if left_out(oi) {
|
||||
continue;
|
||||
}
|
||||
if let Some(pin) = ctx.pin.as_ref().filter(|pin| in_cell && pin.story == story && float.outside_cell && page_anchored(float)) {
|
||||
let page = PageFrame { sect: &pin.sect, origin: (0.0, 0.0), number: pin.number, mirror: pin.mirror };
|
||||
pinned.insert(oi, float_rect(Some(page), (0.0, width), y, w, h, float));
|
||||
continue;
|
||||
}
|
||||
let r = float_rect(frame, (0.0, width), y, w, h, float);
|
||||
let outline = ctx.wrap_outline(o);
|
||||
excl.extend(wrap_area(r, float, outline.as_deref().map(|v| v.as_slice())));
|
||||
@@ -1163,7 +1221,7 @@ fn layout_box(
|
||||
push_para(&mut items, story, &path, &pl, 0, pl.lines.len(), 0.0, y + lead, width);
|
||||
// Pictures, shapes and text boxes: drawn, their areas and text boxes' text.
|
||||
if !p.objects.is_empty() {
|
||||
let at = ObjFrame { page: frame, col: (0.0, width), para_y: y, table_chr: table.map(|t| &t.chr) };
|
||||
let at = ObjFrame { page: frame, col: (0.0, width), para_y: y, table_chr: table.map(|t| &t.chr), pinned: Some(&pinned) };
|
||||
let (back, front) = place_objects(ctx, story, &path, p, &pl, (0, pl.lines.len()), (0.0, y + lead), &at, &floats, depth);
|
||||
let at = behind.min(items.len());
|
||||
behind += back.len();
|
||||
@@ -2078,6 +2136,7 @@ fn layout_pass(
|
||||
quiet_marks: Vec::new(),
|
||||
mirror: false,
|
||||
table_keys: Vec::new(),
|
||||
pin: None,
|
||||
};
|
||||
let sections = doc.sections();
|
||||
let web = matches!(opts.view, ViewMode::Web | ViewMode::Draft | ViewMode::Outline);
|
||||
@@ -2680,7 +2739,9 @@ fn body_top_for(ctx: &mut Ctx, sect: &SectionProps, header: Option<u32>) -> f32
|
||||
// Placed as `headers_footers` draws it, so page-relative floats wrap the same way.
|
||||
let frame = PageFrame { sect, origin: (sect.margin_left + sect.gutter, sect.header), number: 1, mirror: ctx.mirror };
|
||||
let track = std::mem::replace(&mut ctx.track_totals, false);
|
||||
let h = layout_box(ctx, StoryRef::Part(id), &blocks, &[], sect.text_width(), BoxContext::Other, 0, Some(frame)).height;
|
||||
let (h, ..) = with_pin(ctx, id, sect, 1, |ctx| {
|
||||
layout_box(ctx, StoryRef::Part(id), &blocks, &[], sect.text_width(), BoxContext::Other, 0, Some(frame)).height
|
||||
});
|
||||
ctx.track_totals = track;
|
||||
// Word starts the body right below a header that reaches past the top margin, no gap.
|
||||
sect.margin_top.max(sect.header + h)
|
||||
@@ -2693,7 +2754,8 @@ fn foot_top_for(ctx: &mut Ctx, sect: &SectionProps, footer: Option<u32>) -> f32
|
||||
// A page total in the footer is filled in after pagination, like the header's: it doesn't
|
||||
// make the body lay out again.
|
||||
let track = std::mem::replace(&mut ctx.track_totals, false);
|
||||
let laid = layout_box(ctx, StoryRef::Part(id), &blocks, &[], sect.text_width(), BoxContext::Other, 0, None);
|
||||
let (laid, ..) =
|
||||
with_pin(ctx, id, sect, 1, |ctx| layout_box(ctx, StoryRef::Part(id), &blocks, &[], sect.text_width(), BoxContext::Other, 0, None));
|
||||
ctx.track_totals = track;
|
||||
let top = sect.page_h - sect.footer - laid.height;
|
||||
// Flow-table strokes extend above their cell boxes. Floating decorations keep their wrap rules.
|
||||
@@ -2987,6 +3049,9 @@ struct ObjFrame<'a> {
|
||||
para_y: f32,
|
||||
/// A table's character formatting under the paragraph's (see `left_out_objects`).
|
||||
table_chr: Option<&'a CharProps>,
|
||||
/// Floats placed on the page from a header or footer cell (by object index, in page
|
||||
/// coordinates): their items go to [`Ctx::pin`].
|
||||
pinned: Option<&'a HashMap<usize, Rect>>,
|
||||
}
|
||||
|
||||
/// The pictures, shapes and text boxes in lines `l0..l1` of paragraph `p` (story `story`, at
|
||||
@@ -3034,7 +3099,11 @@ fn place_objects(
|
||||
if if floating { !first } else { !in_piece } {
|
||||
continue;
|
||||
}
|
||||
let rect = if floating {
|
||||
let pinned = at.pinned.and_then(|m| m.get(&oi)).copied().filter(|_| floating);
|
||||
let marks = (behind.len(), front.len());
|
||||
let rect = if let Some(r) = pinned {
|
||||
r
|
||||
} else if floating {
|
||||
floats.get(&oi).copied().unwrap_or_else(|| float_rect(at.page, at.col, at.para_y, w, h, float))
|
||||
} else {
|
||||
let cx = x + line.cl_left(k).unwrap_or(0.0);
|
||||
@@ -3083,6 +3152,16 @@ fn place_objects(
|
||||
let layer = if float.wrap == Wrap::BehindText { &mut behind } else { &mut front };
|
||||
layer.extend(items);
|
||||
}
|
||||
// Placed on the page from a header or footer cell: drawn with the header, once.
|
||||
if pinned.is_some() {
|
||||
let (b, f) = (behind.split_off(marks.0.min(behind.len())), front.split_off(marks.1.min(front.len())));
|
||||
if let Some(pin) = ctx.pin.as_mut()
|
||||
&& pin.seen.insert((path.to_vec(), c.start))
|
||||
{
|
||||
pin.behind.extend(b);
|
||||
pin.front.extend(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(behind, front)
|
||||
@@ -3490,6 +3569,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
col: (x, width),
|
||||
para_y: if pb.web { y } else { anchor_y },
|
||||
table_chr: None,
|
||||
pinned: None,
|
||||
};
|
||||
let (behind, front) = place_objects(ctx, StoryRef::Body, &[block as u32], p, &pl, (l0, l1), (x, y), &at, &float_rects, 0);
|
||||
items.extend(front);
|
||||
@@ -3631,6 +3711,7 @@ fn place_outline_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
col: (x, width),
|
||||
para_y: y,
|
||||
table_chr: None,
|
||||
pinned: None,
|
||||
};
|
||||
let (behind, front) = place_objects(ctx, StoryRef::Body, &[block as u32], &q, &pl, (0, l1), (x, y), &at, &HashMap::new(), 0);
|
||||
items.extend(front);
|
||||
@@ -4220,11 +4301,13 @@ fn headers_footers(ctx: &mut Ctx, pages: &mut [Page], sections: &[(usize, &Secti
|
||||
let blocks = part.blocks.clone();
|
||||
let frame = PageFrame { sect, origin: (x, sect.header), number: page.number, mirror: ctx.mirror };
|
||||
let base = defining_width(sections, page.section, |s| s.headers, hid);
|
||||
let mut items = with_pct_base(ctx, base, |c| layout_box(c, StoryRef::Part(id), &blocks, &[], w, BoxContext::Other, 0, Some(frame)).items);
|
||||
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, |ctx| {
|
||||
with_pct_base(ctx, base, |c| layout_box(c, StoryRef::Part(id), &blocks, &[], w, BoxContext::Other, 0, Some(frame)).items)
|
||||
});
|
||||
for it in &mut items {
|
||||
it.translate(x, sect.header);
|
||||
}
|
||||
page.header = items;
|
||||
page.header = behind.into_iter().chain(items).chain(front).collect();
|
||||
page.header_story = Some(id);
|
||||
}
|
||||
if let Some(id) = fid
|
||||
@@ -4233,13 +4316,17 @@ fn headers_footers(ctx: &mut Ctx, pages: &mut [Page], sections: &[(usize, &Secti
|
||||
let blocks = part.blocks.clone();
|
||||
let base = defining_width(sections, page.section, |s| s.footers, fid);
|
||||
// The footer's height fixes where it sits, which page-relative floats need.
|
||||
let h = with_pct_base(ctx, base, |c| layout_box(c, StoryRef::Part(id), &blocks, &[], w, BoxContext::Other, 0, None).height);
|
||||
let (h, ..) = with_pin(ctx, id, sect, page.number, |ctx| {
|
||||
with_pct_base(ctx, base, |c| layout_box(c, StoryRef::Part(id), &blocks, &[], w, BoxContext::Other, 0, None).height)
|
||||
});
|
||||
let frame = PageFrame { sect, origin: (x, sect.page_h - sect.footer - h), number: page.number, mirror: ctx.mirror };
|
||||
let mut items = with_pct_base(ctx, base, |c| layout_box(c, StoryRef::Part(id), &blocks, &[], w, BoxContext::Other, 0, Some(frame)).items);
|
||||
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, |ctx| {
|
||||
with_pct_base(ctx, base, |c| layout_box(c, StoryRef::Part(id), &blocks, &[], w, BoxContext::Other, 0, Some(frame)).items)
|
||||
});
|
||||
for it in &mut items {
|
||||
it.translate(x, sect.page_h - sect.footer - h);
|
||||
}
|
||||
page.footer = items;
|
||||
page.footer = behind.into_iter().chain(items).chain(front).collect();
|
||||
page.footer_story = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3938,6 +3938,56 @@ fn automatic_text_in_a_styled_text_box_takes_the_box_s_colour() {
|
||||
assert_eq!(glyph_colors(None), (Some(Rgb::BLACK), Some(Rgb::BLACK), Some(Rgb(0, 0x80, 0))));
|
||||
}
|
||||
|
||||
/// Issue #1775: a picture watermark anchored in a header that starts with a table sits in a cell;
|
||||
/// placed on the page (`o:allowincell="f"`, `layoutInCell="0"`), it's centred on the margins
|
||||
/// behind the body text on every page, not drawn in the header's cell. One laid out in the cell
|
||||
/// stays there.
|
||||
#[test]
|
||||
fn header_cell_float_placed_on_the_page() {
|
||||
use wordcraft_doc::para::{Anchor, Float, FloatAlign, Wrap};
|
||||
let mut d = Document::from_text(&"Body text of the contract. ".repeat(400));
|
||||
let wm = |outside_cell: bool| Float {
|
||||
wrap: Wrap::BehindText,
|
||||
h_rel: Anchor::Margin,
|
||||
v_rel: Anchor::Margin,
|
||||
h_align: Some(FloatAlign::Center),
|
||||
v_align: Some(FloatAlign::Center),
|
||||
outside_cell,
|
||||
..Default::default()
|
||||
};
|
||||
let header = |d: &mut Document, outside_cell: bool| {
|
||||
let mut t = Table::new(1, 2, 468.0);
|
||||
let mut cp = wordcraft_doc::Paragraph::with_text("Right cell", Default::default());
|
||||
cp.insert_object(0, rect_shape(360.0, 288.0, wm(outside_cell)), &Default::default()).unwrap();
|
||||
t.rows[0].cells[1].blocks = vec![wordcraft_doc::para_block(cp)];
|
||||
let id = d.add_part(
|
||||
wordcraft_doc::PartKind::Header,
|
||||
vec![std::sync::Arc::new(wordcraft_doc::Block::Table(t)), wordcraft_doc::para_block(Default::default())],
|
||||
);
|
||||
d.last_section.headers.default = Some(id);
|
||||
};
|
||||
header(&mut d, true);
|
||||
let l = lay(&d);
|
||||
assert!(l.pages.len() > 1);
|
||||
// Letter, 1" margins: the margins' box is 468 x 648 from (72, 72).
|
||||
let (x, y) = (72.0 + (468.0 - 360.0) / 2.0, 72.0 + (648.0 - 288.0) / 2.0);
|
||||
for (n, page) in l.pages.iter().enumerate() {
|
||||
let s = shapes(&page.header);
|
||||
assert_eq!(s.len(), 1, "page {n}: {s:?}");
|
||||
assert!((s[0].x - x).abs() < 0.01 && (s[0].y - y).abs() < 0.01, "page {n}: centred on the page, not in the cell: {:?}", s[0]);
|
||||
assert!(matches!(page.header.first(), Some(Placed::Shape { .. })), "page {n}: behind the header text");
|
||||
let o = page.header.iter().find_map(|i| if let Placed::Object { rect, .. } = i { Some(*rect) } else { None }).unwrap();
|
||||
assert!((o.x - x).abs() < 0.01 && (o.y - y).abs() < 0.01, "page {n}: its area for selection: {o:?}");
|
||||
assert!(first_line_top(&page.items) < 80.0, "page {n}: the body isn't pushed down");
|
||||
}
|
||||
// Laid out in the cell: it stays centred on the right cell (centre x 72 + 234 + 117), at the
|
||||
// top of the page.
|
||||
header(&mut d, false);
|
||||
let s = shapes(&lay(&d).pages[0].header);
|
||||
assert_eq!(s.len(), 1);
|
||||
assert!((s[0].x + 180.0 - 423.0).abs() < 1.0 && s[0].y < 72.0, "in the cell: {:?}", s[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_area_uses_each_distance() {
|
||||
use wordcraft_doc::para::{Float, Wrap};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-18 · **Change:** trivial (`w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `<blockquote>` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-18 · **Change:** trivial (a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `<blockquote>` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
|
||||
|
||||
Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the
|
||||
share of a typical real-world file's content and formatting that survives (read: shows correctly;
|
||||
@@ -86,6 +86,8 @@ Measured by grepping `crates/docx/src/read` and `src/write` for the OOXML elemen
|
||||
| Restrict Editing (`w:documentProtection` edit/formatting/enforcement and password hash, `w:locked` styles, `w:permStart`/`w:permEnd` exceptions) | ✅ | ✅ | #412: SHA-512 hash with salt and spin count written; Word's legacy `w:cryptAlgorithmSid`/`w:hash` form (SHA-1/-256/-384/-512, ECMA-376 Part 4 §14.8.1) is kept and verified; MD2/MD4/MD5, `w:algIdExt` and other unsupported forms are refused, never unlocked; `w:colFirst`/`w:colLast` table-column ranges read as plain ranges; forms mode (`w:edit="forms"`) lets edits inside fillable content controls (`w:lock` content-editable, innermost wins) run, everything else rolls back; row-wrapped controls and control deletion remain refused; `review.restrict` reports `formFields`; Find Next Region covers form fields (the pane button shows for forms documents); under active protection, `developer.properties` refuses `lockEdit`/`lockDelete` changes — as Word blocks property edits until Stop Protection — while other properties (title, tag, placeholder…) stay editable |
|
||||
| Legacy form fields (`w:ffData`), check boxes, drop-downs | 🟡 | ✅ | The field definition is kept raw and saved back. Check boxes (`FORMCHECKBOX`) are drawn as Word draws them: a square as tall as the text's line, inset 1.16 pt, 0.72 pt border, an X when checked (`w:checked`, else `w:default`), `w:size` honoured; drop-downs (`FORMDROPDOWN`) show their selected entry; text fields (`FORMTEXT`) show their result. Not yet: filling them in, Protect Document's forms mode for legacy fields |
|
||||
| DrawingML pictures inline and anchored (`wp:anchor`), wrap square/tight/through/top-bottom | ✅ | ✅ | Tight/through read and written with their `wp:wrapPolygon` (and `edited`); without one, a shape's outline is written (a picture's rectangle). `wrapText` (both sides, left, right, largest) read and written for square, tight and through |
|
||||
| Legacy form fields (`w:ffData`), check boxes, drop-downs | ❌ | ❌ | |
|
||||
| DrawingML pictures inline and anchored (`wp:anchor`), wrap square/tight/through/top-bottom | ✅ | ✅ | Tight/through read and written with their `wp:wrapPolygon` (and `edited`); without one, a shape's outline is written (a picture's rectangle). `wrapText` (both sides, left, right, largest) read and written for square, tight and through. DrawingML `layoutInCell="0"` is kept and written back; a VML shape in a header or footer table cell is always treated the same way (user-checked in Word for Mac, whatever `o:allowincell` says; saved as DrawingML `layoutInCell="0"`). In a header or footer table cell, such a picture placed relative to the page or margins is placed on the page, so a picture watermark in a header that starts with a table is centred behind the text instead of sitting in the cell (#1775) |
|
||||
| WordArt text effects (`w14:textFill`, `w14:textOutline`, `w14:shadow`, `w14:glow`, `w14:reflection` in `w:rPr`) | ✅ | ✅ | Solid, gradient (linear) and no fill, outline colour/width, shadow, glow, reflection (#416); `w14:scene3d`/`w14:props3d` dropped |
|
||||
| Legacy font effects (`w:outline`, `w:shadow`, `w:emboss`, `w:imprint` in `w:rPr`) | ✅ | ✅ | Drawn on screen and in PDFs as Word's PDFs show them (#2534): hollow outlined letters; a sharp grey copy down-right (shadow, emboss) or up-left (imprint); WordArt effects on the same run win; tracked insertions and deletions keep them. Not drawn on colour emoji, or on top-to-bottom text with upright East Asian characters. Emboss and imprint are approximations (no highlight edge) |
|
||||
| OpenType ligatures (`w14:ligatures` in `w:rPr`: runs, styles, document defaults) | ✅ | ✅ | All 16 values; an unknown value is ignored; nothing is written when the document has none |
|
||||
@@ -255,6 +257,8 @@ Imported text fields use the first nonempty cached result run's formatting on th
|
||||
| 2026-10-16 | trivial | `w:eastAsianLayout` (two lines in one, horizontal in vertical) is read and written; not laid out yet |
|
||||
| 2026-10-15 | trivial | A group holding Word ink can't be ungrouped (its ink would be lost on save) |
|
||||
| 2026-10-15 | trivial | Native ink nested in groups and drawing canvases kept on save, in its place, drawn from its fallback picture |
|
||||
| 2026-10-18 | trivial | DOCX: every VML shape anchored in a header table cell is placed on the page whatever `o:allowincell` says (user-checked in Word for Mac, #1775) |
|
||||
| 2026-10-16 | trivial | DOCX: `wp:anchor/@layoutInCell="0"` and VML `o:allowincell="f"` are read and written back (`layoutInCell="0"`), so a header-cell picture watermark is placed on the page (#1775) |
|
||||
| 2026-10-13 | trivial | A cell's explicit top vertical alignment (`w:vAlign w:val="top"`) is kept on read and write and beats a table style that centres or bottom-aligns (#1288) |
|
||||
| 2026-10-14 | trivial | Row table border exceptions survive DOCX saves |
|
||||
| 2026-10-12 | trivial | Fields keep `w:dirty` through DOCX round trips — on buffered fields and range-field begin markers alike — and computing a result (Update Field, Update Table) clears it like Word's F9, so a generator's placeholder TOC still refreshes when Word opens the file until WordCraft replaces the placeholder (#877) |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Layout and pagination parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** trivial (an at-least row with no room for its minimum starts the next page whole, #1742; previously trivial: AutoFit donors lose equal widths down to their content minima; previously trivial: automatic space between Asian text and Latin text or digits, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: positional tabs go to the margins or indents, #2367; previously trivial: field results with right-to-left text read right to left, #1670; previously trivial: a list bullet no installed font can draw shows the level's default bullet, #1683; previously trivial: a missing Thai font is drawn in one Thai face for the whole run, #1549; previously trivial: the caret after spaces hanging past the margin stays at the margin, #2146; previously trivial: covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: TOCs include table-cell headings and retain full targets; previously trivial: kept table rows follow the next row; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** trivial (VML header-cell floats always on the page, user-checked in Word for Mac, #1775; previously trivial: an at-least row with no room for its minimum starts the next page whole, #1742; previously trivial: AutoFit donors lose equal widths down to their content minima; previously trivial: automatic space between Asian text and Latin text or digits, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: positional tabs go to the margins or indents, #2367; previously trivial: field results with right-to-left text read right to left, #1670; previously trivial: a list bullet no installed font can draw shows the level's default bullet, #1683; previously trivial: a missing Thai font is drawn in one Thai face for the whole run, #1549; previously trivial: the caret after spaces hanging past the margin stays at the margin, #2146; previously trivial: covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: TOCs include table-cell headings and retain full targets; previously trivial: kept table rows follow the next row; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
A document only "opens correctly" if its pages break where Word breaks them: the same lines, the
|
||||
same page count, footnotes and pictures on the same pages. This checklist covers line breaking,
|
||||
@@ -60,7 +60,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic
|
||||
| Tables: autofit to contents/window, fixed widths, Word 2013 edge | 🟡 | #137; #44: AutoFit Contents measures each column's narrowest and widest text and shares the width like Word, once (not live as you type); consistent a percentage width past 100% is kept, as Word does; cell widths override stale grid proportions when their totals agree (`cell_widths_override_grid`); conflicting widths and spans still differ | 3–5 |
|
||||
| Floating tables (`w:tblpPr`) | 🟡 | #137; one placed on the page or margin that would cover visible text before it and does not fit below it moves to the next page (#534); empty line-break lines do not count as text; other overlap rules untested | 2–3 |
|
||||
| Nested tables | 🟡 | render; polish missing | 2–4 |
|
||||
| Floating pictures/shapes placement (relative to page/margin/column/paragraph/line/character) | ✅ | #136 | — |
|
||||
| Floating pictures/shapes placement (relative to page/margin/column/paragraph/line/character) | ✅ | #136; in a header or footer table cell, a float not laid out in its cell (`layoutInCell="0"`, VML `o:allowincell="f"`, or any VML shape in a header or footer: user-checked in Word for Mac) and placed relative to the page or margins is placed on the page, drawn with the header behind or in front of its text, on every page: Word's picture watermark in a header that starts with a table is centred behind the body text (#1775, `layout::tests::header_cell_float_placed_on_the_page`). Other cell floats are still placed relative to their cell | — |
|
||||
| Text wrap: square, top-and-bottom, behind, in front | ✅ | 2026-10-06; beside an object, a line's first word that doesn't fit the strip goes on whole to the row's next strip or the next row, instead of being split (#1376, `layout::tests::a_word_too_wide_for_the_strip_beside_a_float_moves_on_whole`; inferred from a reporter's comparison, not yet measured in Word) a paragraph running on into the next column or page is broken again there around that column's shapes, its own floating objects left where they are (#1374). Text in an earlier column doesn't yet wrap around a shape anchored in a later one (#1158) | #884: an object dragged on a multi-column page anchors to a paragraph in the column it lands in, so that column's text wraps around it. Text laid out before the anchor (an earlier column) doesn't yet wrap around an object anchored later on the page, nor do lines a paragraph continues into the next column (broken once, in the column it starts in) |
|
||||
| Text wrap: tight, through (contour, wrap polygon) | 🟡 | #354: lines follow the wrap polygon, or the outline worked out from the object (shape geometry; a picture's opaque pixels as a convex hull; otherwise the rectangle), turned and flipped with it, kept the left/right distance from text; Through also uses gaps inside the outline. Wrap side (`wrapText`: both sides, left only, right only, largest side per line) applies to square, tight and through wraps, turned objects included; picture outlines are convex; not compared page-for-page with Word | 1–2 |
|
||||
| Text boxes, overflow, linked text boxes | ✅ | #46; linked text boxes (#369): the text runs on through each box in turn at that box's line length, overflow past the last box hidden; Align Text top / middle / bottom; the box's own internal margins, and a box set to Resize shape to fit text shows all its text (#743) | — |
|
||||
@@ -229,6 +229,8 @@ Footer flow-table strokes reserve their drawn top edge, including the half-width
|
||||
| 2026-10-16 | trivial | Explicit cell page breaks take no line height; original native Word controls in modes 14 and 15 and synthetic cluster/cache regressions. |
|
||||
| 2026-10-16 | trivial | Flow-table top border strokes in footers reserve body clearance; original mode-15 Word controls and a synthetic layout regression. |
|
||||
| 2026-10-16 | trivial | Subpoint grid units retain their declared widths without shrinking adjacent cells; tiny totals normalize without scale-factor overflow |
|
||||
| 2026-10-18 | trivial | Any VML float in a header or footer cell is placed on the page, as Word for Mac does (user-checked, #1775) |
|
||||
| 2026-10-16 | trivial | A header or footer cell's float placed relative to the page and not laid out in its cell (a picture watermark in a header that starts with a table) is placed on the page, behind the text (#1775) |
|
||||
| 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows |
|
||||
| 2026-10-14 | trivial | Consistent cell widths override a stale table grid |
|
||||
| 2026-10-14 | trivial | Paragraphs inside complex-field instructions take no flow height; empty result paragraphs keep their line. |
|
||||
|
||||
Reference in new issue
Block a user