diff --git a/Cargo.lock b/Cargo.lock index e46eb7356f..0e5395268c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1090,9 +1090,10 @@ dependencies = [ [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" +source = "git+https://github.com/storytold/craft-libs?rev=a3e0b5a9440d18f19422a9dca13516747b7d3181#a3e0b5a9440d18f19422a9dca13516747b7d3181" dependencies = [ "craft-winprint-gdi", + "log", "miniz_oxide 0.9.1", "roxmltree", "tempfile", @@ -6814,7 +6815,9 @@ dependencies = [ "open", "pollster 1.0.1", "rfd", + "serde", "serde_json", + "tempfile", "windows", "winit", "winresource", diff --git a/ROADMAP.md b/ROADMAP.md index 863d5a0de0..e768277d02 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,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-11-05 · **Change:** trivial (Page drawing indexes text-box warp lookups per paragraph (#3614)); previously trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (progress log: Times and Courier take their Windows counterparts' line spacing; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: the status bar's count can be words, characters or characters without spaces, #2797); previously trivial (Linux display server setting in File › Options › Advanced, #2184); previously trivial (progress log: Linux Read Aloud through Speech Dispatcher, #4576); previously trivial (progress log: a click just outside a floating table by turned cell text, #4181; previously minor: progress log: Spanish proofing, #3360, #3553; previously trivial: progress log: Insert › Table's grid previews the hovered table on the page, #3931; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: MHT quoted MIME parameters); previously trivial (progress log: CLI convert format options); previously trivial (MATH construction reuse, #3630); previously trivial (DOCX `w:adjustRightInd` is preserved through style resolution and ordered DOCX saves); previously trivial (per-page ink object lookups, #3614); previously trivial (progress log: ODT display settings removed); previously trivial (progress log: dialogs are modal except the five Word keeps modeless, and a press beside one shakes it, #3859); previously trivial (progress log: picture brightness, contrast and transparency, ODT element audit, ODT checked against 220 files from other applications, ODT display styles); previously trivial (progress log: the dialogs of the mirrored Arabic interface read right to left: label-and-field rows, tables (list columns, the Models table), tile galleries and the Add/Remove arrows follow the reading direction, through one `rtl::Table` helper that draws exactly as before in English; no ranked gap closed, readiness estimates unchanged); previously trivial (progress log: fonts that aren't installed are flagged in the font box and status bar); previously trivial (progress log: ODT sections inside text boxes); previously trivial (progress log: ODT definitions sharing a name, sections around tables, stacking order); previously trivial (progress log: ODT forms and indexes written as read); previously trivial (progress log: ODT table styles saved, WordCraft-origin documents checked in LibreOffice); previously trivial (progress log: ODT interpretation profile); previously trivial (progress log: ODT controls, custom shapes, in-line placement); previously trivial (progress log: ODT charts kept, frame outlines, header height); previously trivial (progress log: ODT header rows); previously trivial (progress log: ODT layout setting, padding, columns); previously trivial (progress log: ODT tables, header spacing, picture paragraphs); previously trivial (progress log: ODT round trips keep their page count); previously trivial (progress log: emphasis marks, SET fields, ODT page backgrounds and tables); previously trivial (progress log: ODT checked against LibreOffice's templates); previously trivial (progress log: ODT at ~90%); previously trivial (progress log: ODT read and written nearly whole); previously trivial (progress log: ODT text boxes); previously trivial (progress log: ODT layout settings); previously trivial (progress log: ODT character styles); previously trivial (progress log: ODT custom styles); previously trivial (progress log: ODT equations saved); previously trivial (progress log: ODT floating pictures); previously trivial (progress log: ODT paragraph borders); previously trivial (progress log: ODT heading numbering); previously trivial (progress log: ODT table borders); previously trivial (progress log: ODT heading styles); previously trivial (progress log: ODT tab stops); previously trivial (progress log: ODT character formatting); previously trivial (progress log: ODT lists); previously trivial (progress log: ODT comments); previously trivial (progress log: ODT footnotes and endnotes); previously trivial (progress log: ODT headers, footers and fields); previously trivial (progress log: ODT paragraph spacing); previously trivial (progress log: Options › Language redesign, CP4.3); previously trivial (progress log: the missing-pack bar on a fresh install, the caret's typing language, and the spelling menu heading in the interface language, CP4.3); previously trivial (progress log: Language pack languages in every language list, CP4.3); previously trivial (progress log: craft-proof pin, French elision and Valencian, CP4.3); previously trivial (progress log: a long comment's card and balloon stop at ten lines, #3382); previously trivial (progress log: a pasted text box story is charged for the formatting it copies); previously trivial (progress log: a .doc table row keeps one override per cell); previously trivial (progress log: a .doc file's sections find their paragraphs by index); previously minor (Arabic interface language with mirrored UI, #1195); previously trivial (progress log: Replace and Remove Table of Contents with Word's title above a section break, #4041); previously trivial (progress log: a password-protected file named on the command line asks for its password after the startup document is replaced, #5299); previously trivial (progress log: language-packs-2 published and pinned, 75 Language packs); previously trivial (progress log: language-packs-1 published and pinned, CP4.4); previously trivial (progress log: Language packs, CP4);; previously trivial (DOCX table rows and cells inside selected alternate-content branches survive open and repeated saves (#5219); broader corpus qualification and readiness estimates unchanged); previously trivial (progress log: a list inside a Markdown block quote stays quoted); previously trivial (progress log: arrowheads keep their kind, width and length and are drawn at Word's sizes); previously trivial (progress log: the line between columns for selected text, #5178); previously trivial (progress log: .doc standard font, #5066); previously trivial (progress log: a table row doesn't split off a picture that doesn't fit, #4272); previously trivial (progress log: a middle or bottom cell of a row split across pages lines up on the first page); previously trivial (progress log: tab leader dots spaced and placed as in Word); previously trivial (progress log: a list level above counts its first number at the first deeper item); previously trivial (progress log: paragraph side borders lie outside the text, #5036); previously trivial (progress log: a paragraph naming an undefined style is formatted as Normal, #5077); previously trivial (progress log: TOC page numbers as PAGEREF fields, #5097); previously trivial (progress log: Backstage keytips, #4960); previously trivial (Progress log: only the printer's PDF leaves out links; Save these pages as PDF keeps them, #4734); previously trivial (progress log: Chinese and Japanese text counts a word per character, #4547; previously trivial: aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (progress log: corporate authors in bibliographies, #4848); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (progress log: Table Properties follows the selection, #4857); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (note conversion walks reference offsets and formatting runs together, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (progress log: RTF pictures stored as Windows or Enhanced Metafiles are imported, #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (progress log: Insert › Shapes icon grid, #4349); previously trivial (progress log: AutoCorrect Options with no document open, #4585); previously trivial (progress log: window fallback geometry and equation symbol tooltips); previously trivial (progress log: window titles omit NUL characters, #3634); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (progress log: Custom document properties dialog, #2982; previously trivial: progress log: View › New Window; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: progress log: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: progress log: recovery copies and .json files store pictures as base64, #586 follow-up; previously trivial: progress log: legacy menu keys, #1697; previously trivial: progress log: shape gradients and transparency, #1500; previously trivial: progress log: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: progress log: text above a float's anchor wraps around it, #2048; previously trivial: progress log: File › Recover follows the app theme, #3243; previously trivial: progress log: formatting toolbar with the text context menu, #2959; previously trivial: progress log: caret inside ligatures, #1974; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously 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; progress docs restructured) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** trivial (print helpers erase loaded plaintext inputs before driver work, #5710); previously trivial (Windows print submissions use isolated helpers so a cancelled driver cannot block a retry, #5710); previously trivial (print cancellation remains available without a document; sending-boundary limitation clarified, #5710); previously trivial (progress log: a print job waiting on its driver can be cancelled, and quitting doesn't wait for it, #5710); previously trivial (progress log: the Print panel's printer list no longer waits on printer drivers, #5710); previously trivial (Page drawing indexes text-box warp lookups per paragraph (#3614)); previously trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (progress log: Times and Courier take their Windows counterparts' line spacing; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: the status bar's count can be words, characters or characters without spaces, #2797); previously trivial (Linux display server setting in File › Options › Advanced, #2184); previously trivial (progress log: Linux Read Aloud through Speech Dispatcher, #4576); previously trivial (progress log: a click just outside a floating table by turned cell text, #4181; previously minor: progress log: Spanish proofing, #3360, #3553; previously trivial: progress log: Insert › Table's grid previews the hovered table on the page, #3931; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: MHT quoted MIME parameters); previously trivial (progress log: CLI convert format options); previously trivial (MATH construction reuse, #3630); previously trivial (DOCX `w:adjustRightInd` is preserved through style resolution and ordered DOCX saves); previously trivial (per-page ink object lookups, #3614); previously trivial (progress log: ODT display settings removed); previously trivial (progress log: dialogs are modal except the five Word keeps modeless, and a press beside one shakes it, #3859); previously trivial (progress log: picture brightness, contrast and transparency, ODT element audit, ODT checked against 220 files from other applications, ODT display styles); previously trivial (progress log: the dialogs of the mirrored Arabic interface read right to left: label-and-field rows, tables (list columns, the Models table), tile galleries and the Add/Remove arrows follow the reading direction, through one `rtl::Table` helper that draws exactly as before in English; no ranked gap closed, readiness estimates unchanged); previously trivial (progress log: fonts that aren't installed are flagged in the font box and status bar); previously trivial (progress log: ODT sections inside text boxes); previously trivial (progress log: ODT definitions sharing a name, sections around tables, stacking order); previously trivial (progress log: ODT forms and indexes written as read); previously trivial (progress log: ODT table styles saved, WordCraft-origin documents checked in LibreOffice); previously trivial (progress log: ODT interpretation profile); previously trivial (progress log: ODT controls, custom shapes, in-line placement); previously trivial (progress log: ODT charts kept, frame outlines, header height); previously trivial (progress log: ODT header rows); previously trivial (progress log: ODT layout setting, padding, columns); previously trivial (progress log: ODT tables, header spacing, picture paragraphs); previously trivial (progress log: ODT round trips keep their page count); previously trivial (progress log: emphasis marks, SET fields, ODT page backgrounds and tables); previously trivial (progress log: ODT checked against LibreOffice's templates); previously trivial (progress log: ODT at ~90%); previously trivial (progress log: ODT read and written nearly whole); previously trivial (progress log: ODT text boxes); previously trivial (progress log: ODT layout settings); previously trivial (progress log: ODT character styles); previously trivial (progress log: ODT custom styles); previously trivial (progress log: ODT equations saved); previously trivial (progress log: ODT floating pictures); previously trivial (progress log: ODT paragraph borders); previously trivial (progress log: ODT heading numbering); previously trivial (progress log: ODT table borders); previously trivial (progress log: ODT heading styles); previously trivial (progress log: ODT tab stops); previously trivial (progress log: ODT character formatting); previously trivial (progress log: ODT lists); previously trivial (progress log: ODT comments); previously trivial (progress log: ODT footnotes and endnotes); previously trivial (progress log: ODT headers, footers and fields); previously trivial (progress log: ODT paragraph spacing); previously trivial (progress log: Options › Language redesign, CP4.3); previously trivial (progress log: the missing-pack bar on a fresh install, the caret's typing language, and the spelling menu heading in the interface language, CP4.3); previously trivial (progress log: Language pack languages in every language list, CP4.3); previously trivial (progress log: craft-proof pin, French elision and Valencian, CP4.3); previously trivial (progress log: a long comment's card and balloon stop at ten lines, #3382); previously trivial (progress log: a pasted text box story is charged for the formatting it copies); previously trivial (progress log: a .doc table row keeps one override per cell); previously trivial (progress log: a .doc file's sections find their paragraphs by index); previously minor (Arabic interface language with mirrored UI, #1195); previously trivial (progress log: Replace and Remove Table of Contents with Word's title above a section break, #4041); previously trivial (progress log: a password-protected file named on the command line asks for its password after the startup document is replaced, #5299); previously trivial (progress log: language-packs-2 published and pinned, 75 Language packs); previously trivial (progress log: language-packs-1 published and pinned, CP4.4); previously trivial (progress log: Language packs, CP4);; previously trivial (DOCX table rows and cells inside selected alternate-content branches survive open and repeated saves (#5219); broader corpus qualification and readiness estimates unchanged); previously trivial (progress log: a list inside a Markdown block quote stays quoted); previously trivial (progress log: arrowheads keep their kind, width and length and are drawn at Word's sizes); previously trivial (progress log: the line between columns for selected text, #5178); previously trivial (progress log: .doc standard font, #5066); previously trivial (progress log: a table row doesn't split off a picture that doesn't fit, #4272); previously trivial (progress log: a middle or bottom cell of a row split across pages lines up on the first page); previously trivial (progress log: tab leader dots spaced and placed as in Word); previously trivial (progress log: a list level above counts its first number at the first deeper item); previously trivial (progress log: paragraph side borders lie outside the text, #5036); previously trivial (progress log: a paragraph naming an undefined style is formatted as Normal, #5077); previously trivial (progress log: TOC page numbers as PAGEREF fields, #5097); previously trivial (progress log: Backstage keytips, #4960); previously trivial (Progress log: only the printer's PDF leaves out links; Save these pages as PDF keeps them, #4734); previously trivial (progress log: Chinese and Japanese text counts a word per character, #4547; previously trivial: aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (progress log: corporate authors in bibliographies, #4848); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (progress log: Table Properties follows the selection, #4857); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (note conversion walks reference offsets and formatting runs together, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (progress log: RTF pictures stored as Windows or Enhanced Metafiles are imported, #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (progress log: Insert › Shapes icon grid, #4349); previously trivial (progress log: AutoCorrect Options with no document open, #4585); previously trivial (progress log: window fallback geometry and equation symbol tooltips); previously trivial (progress log: window titles omit NUL characters, #3634); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (progress log: Custom document properties dialog, #2982; previously trivial: progress log: View › New Window; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: progress log: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: progress log: recovery copies and .json files store pictures as base64, #586 follow-up; previously trivial: progress log: legacy menu keys, #1697; previously trivial: progress log: shape gradients and transparency, #1500; previously trivial: progress log: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: progress log: text above a float's anchor wraps around it, #2048; previously trivial: progress log: File › Recover follows the app theme, #3243; previously trivial: progress log: formatting toolbar with the text context menu, #2959; previously trivial: progress log: caret inside ligatures, #1974; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously 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; progress docs restructured) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. > **Legacy form update:** 2026-10-22 · trivial: numeric XML references decode in field names and labels; readiness estimates unchanged. @@ -140,6 +140,9 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm ## Progress log +- 2026-11-06: Windows print submissions run in separate helper processes (#5710). A helper acknowledges cancellation before it stops, so a driver stalled in a cancelled job cannot hold another job’s provider lock. A private temporary PDF and bounded settings messages cross the process boundary; normal shutdown reaps helpers and removes their inputs. The helper erases its plaintext inputs before driver work; a hard app crash during startup can still leave private temporary files. Jobs already sending still complete. Readiness estimates unchanged. +- 2026-11-06: File › Print: before sending starts, Cancel printing stops a waiting job, frees the Print page and keeps the settings; a successfully cancelled job is never sent. Cancellation remains available after the last document closes. Quitting cancels waiting jobs and joins jobs already sending (#5710). On Windows, winprint repeats the DEVMODE conversion after sending starts. A stall during that repeat prevents cancellation and can keep shutdown waiting, even before a native job exists. Readiness estimates unchanged. +- 2026-11-06: File › Print on Windows no longer waits on printer drivers to list printers (#5710). The list used to wait until each printer's driver said whether it prints on both sides, and a networked printer asleep in power save could keep it empty with Refresh unavailable; now discovery asks no driver (the Windows spooler still supplies the list), and printing still checks the job's options with the driver. The reporter's sleeping printer has not been retested yet. Readiness estimates unchanged. - 2026-11-04: CLI `convert` rejects a missing or repeated `--format` value before opening the input or writing output, matching the existing page/scale option checks. Readiness estimates unchanged. - 2026-11-04: MHT import keeps semicolons and escaped quotes inside quoted MIME parameters, so an unrelated parameter cannot supply a false multipart boundary and hide the document text. Readiness estimates unchanged. - 2026-10-28: Ink replay, eraser picking, lasso selection and arrow-candidate discovery reuse the paragraph prefixes they have already scanned (#3614). Page item order, stroke geometry and saved data retain their behavior. This narrows the stability work; ink import fidelity, native pen input and broader corpus qualification remain open. @@ -788,6 +791,10 @@ MATH glyphs that reference the same construction now share its immutable variant | Date | Change | Summary | |---|---|---| +| 2026-11-06 | trivial | Print helpers remove loaded PDF/settings files before driver work, tolerate prior removal and report cleanup errors; a held-driver regression checks the files are already absent (#5710) | +| 2026-11-06 | trivial | Windows print submissions isolated per helper process; cancelled driver work cannot block a retry (#5710) | +| 2026-11-06 | trivial | Progress log: print cancellation before sending, including without a document, and the Windows repeated-conversion limitation (#5710) | +| 2026-11-06 | trivial | Progress log: the printer list no longer waits on printer drivers (#5710) | | 2026-11-05 | trivial | PR #4545: eligible cell floats retain in-bounds offsets and touch the outer cell edge only on overflow, ignoring wrap distances for object placement. Front-of-text, turned-cell and outside-cell opt-outs remain. Synthetic geometry controls; native front/behind-text parity remains unqualified. | | 2026-11-04 | trivial | Progress log: CLI convert format options; readiness estimates unchanged | | 2026-11-04 | trivial | Progress log: MHT quoted MIME parameters; readiness estimates unchanged | diff --git a/apps/wordcraft/Cargo.toml b/apps/wordcraft/Cargo.toml index f44ae42a57..0fd7a423db 100644 --- a/apps/wordcraft/Cargo.toml +++ b/apps/wordcraft/Cargo.toml @@ -22,6 +22,8 @@ wordcraft-control-key = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "clipboard", "wayland", "x11", "links"] } egui = { workspace = true } serde_json = { workspace = true } +serde = { workspace = true } +tempfile = "3" log = { workspace = true } chrono = { workspace = true } @@ -77,7 +79,9 @@ windows = { version = "0.62", features = ["UI_ViewManagement", "System_UserProfi clipboard-win = { version = "5.4", features = ["std"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -craft-print = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +# Pinned apart from craft-ui, which doesn't depend on it: `printer_queues`, the printer list that +# asks no driver (#5710, storytold/craft-libs#618), and `submit_cancellable` (storytold/craft-libs#622). +craft-print = { git = "https://github.com/storytold/craft-libs", rev = "a3e0b5a9440d18f19422a9dca13516747b7d3181" } rfd = { workspace = true } # Waits for an `rfd::AsyncFileDialog` on a background thread (the dialogs don't block the UI, #94); # rfd itself already depends on it. diff --git a/apps/wordcraft/src/main.rs b/apps/wordcraft/src/main.rs index 879559997b..2c9eb1d679 100644 --- a/apps/wordcraft/src/main.rs +++ b/apps/wordcraft/src/main.rs @@ -337,6 +337,7 @@ fn services(file_dialog: file_dialogs::Hook, ctx: &egui::Context) -> Services { file_dialog: Some(file_dialog), printers: Some(print::discover()), print_native: Some(print::submit()), + cancel_print: Some(print::cancel()), clipboard_picture: Some(Box::new(clipboard::picture)), make_default_app: default_apps::hook(), // Personal templates live in `Templates` in the settings folder (#2852), made on first save. @@ -439,6 +440,10 @@ fn open_non_unicode(app: &mut WordApp, path: &std::path::Path) { /// geometry, opens requested documents or the Start screen, starts the control server when /// asked, and runs the eframe event loop. fn main() -> eframe::Result { + #[cfg(windows)] + if let Some(code) = print::isolated::helper_mode() { + std::process::exit(code); + } // Windows: the update helper is a copy of this program started with `--finish-update`. #[cfg(windows)] if let Some(result) = craft_update::self_update::finish(&wordcraft_ui_egui::UPDATE_APP, &std::env::args_os().skip(1).collect::>()) { diff --git a/apps/wordcraft/src/print.rs b/apps/wordcraft/src/print.rs index 5e8489b71a..9465c4485c 100644 --- a/apps/wordcraft/src/print.rs +++ b/apps/wordcraft/src/print.rs @@ -1,13 +1,21 @@ //! Desktop spooler adapter. Discovery and submission never run on the UI thread. +#[cfg(any(windows, test))] +pub(crate) mod isolated; + use wordcraft_ui_egui::printing::{Discover, Duplex, Printer, Settings, Submit}; /// Returns a discovery callback that enumerates spooler printers on a named worker thread and /// sends the list through a channel, keeping spooler calls off the UI thread. +/// +/// The Print panel needs only names and the default, so the list comes from +/// `craft_print::printer_queues`, which asks no printer driver anything: on Windows a networked +/// printer in power save can hold a driver's capability query for a long time, and the list must +/// not wait on it (#5710). Submission still checks the job's options with the driver. pub fn discover() -> Discover { Box::new(|| { let (sender, receiver) = std::sync::mpsc::channel(); let _ = std::thread::Builder::new().name("print-discovery".into()).spawn(move || { - let printers = craft_print::printers() + let printers = craft_print::printer_queues() .into_iter() .map(|printer| Printer { name: printer.name, label: printer.description, default: printer.default }) .collect(); @@ -17,22 +25,59 @@ pub fn discover() -> Discover { }) } +/// A submission worker and how to stop its job: `true` when it will never be sent, `false` when +/// it was already being sent. +struct Job { + worker: std::thread::JoinHandle<()>, + cancel: Box bool + Send>, + // Acknowledged helper cancellation permits joining it to reap the process and its input PDF. + isolated: bool, +} + // Keep submission workers reachable until normal app shutdown; dropping a JoinHandle would // let process exit end an in-process Windows job or the CUPS hand-off before the spooler has it. -static JOBS: std::sync::Mutex>> = std::sync::Mutex::new(Vec::new()); +static JOBS: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); +fn jobs() -> std::sync::MutexGuard<'static, Vec> { + JOBS.lock().unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// Waits for the jobs already being sent, so the spooler has them before the process exits. A +/// job still waiting (on a sleeping printer's driver, say: #5710) is cancelled instead, so it is +/// never sent and quitting doesn't wait on the driver. pub fn finish_pending_jobs() { - let workers = std::mem::take(&mut *JOBS.lock().unwrap_or_else(std::sync::PoisonError::into_inner)); - for worker in workers { - if worker.join().is_err() { + let pending = std::mem::take(&mut *jobs()); + // Cancel them all first: a job left waiting while another is joined could start sending. + let finishing: Vec<_> = pending + .into_iter() + .filter(|job| { + let cancelled = (job.cancel)(); + job.isolated || !cancelled + }) + .collect(); + for job in finishing { + if job.worker.join().is_err() { log::error!("the print worker stopped unexpectedly during shutdown"); } } } +/// The Print page's Cancel: stops the job being submitted (the Print page submits one at a +/// time, the latest) unless it has started reaching the spooler. `true` when it will never be +/// sent. A job that has finished sending answers `false`, so its receipt is still shown. +pub fn cancel() -> Box bool> { + Box::new(|| jobs().last().is_none_or(|job| (job.cancel)())) +} + +/// Submit off the UI thread, isolating Windows drivers while retaining CUPS token cancellation. pub fn submit() -> Submit { Box::new(|bytes, settings: Settings, collate, options| { let (sender, receiver) = std::sync::mpsc::channel(); + #[cfg(not(windows))] + let cancel = craft_print::Cancel::new(); + #[cfg(windows)] + let cancel = isolated::Submission::new(); + let token = cancel.clone(); let worker = std::thread::Builder::new().name("print-submit".into()).spawn(move || { let job = craft_print::Job { printer: Some(settings.printer), @@ -48,12 +93,16 @@ pub fn submit() -> Submit { title: "WordCraft document".into(), options, }; - let _ = sender.send(craft_print::submit(&bytes, &job).map_err(|error| error.to_string())); + #[cfg(not(windows))] + let sent = craft_print::submit_cancellable(&bytes, &job, &mut |_, _| {}, &token).map_err(|error| error.to_string()); + #[cfg(windows)] + let sent = token.submit(&bytes, &job); + let _ = sender.send(sent); }); if let Ok(worker) = worker { - let mut jobs = JOBS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - jobs.retain(|job| !job.is_finished()); - jobs.push(worker); + let mut jobs = jobs(); + jobs.retain(|job| !job.worker.is_finished()); + jobs.push(Job { worker, cancel: Box::new(move || cancel.cancel()), isolated: cfg!(windows) }); } receiver }) @@ -62,20 +111,67 @@ pub fn submit() -> Submit { #[cfg(test)] mod tests { use super::*; - #[test] - fn shutdown_waits_for_pending_submission_workers() { + /// A worker that waits until released, then reports that it finished. + fn worker() -> (std::thread::JoinHandle<()>, std::sync::mpsc::Sender<()>, std::sync::mpsc::Receiver<()>) { let (release, wait) = std::sync::mpsc::channel(); let (completed, completion) = std::sync::mpsc::channel(); let worker = std::thread::spawn(move || { - wait.recv().unwrap(); - completed.send(()).unwrap(); + let _ = wait.recv(); + let _ = completed.send(()); }); - JOBS.lock().unwrap().push(worker); - let shutdown = std::thread::spawn(finish_pending_jobs); - assert!(completion.try_recv().is_err(), "the print worker is still pending"); - release.send(()).unwrap(); - shutdown.join().unwrap(); - completion.recv().unwrap(); - assert!(JOBS.lock().unwrap().is_empty(), "workers were joined before shutdown returned"); + (worker, release, completion) + } + + /// A cancel action that records when it was asked and answers `stops`. + fn asked(stops: bool) -> (Box bool + Send>, std::sync::Arc) { + let flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let set = flag.clone(); + let cancel = Box::new(move || { + set.store(true, std::sync::atomic::Ordering::SeqCst); + stops + }); + (cancel, flag) + } + + /// Shutdown waits for a job already being sent, so the spooler has it before the process + /// exits, but not for one still waiting on a driver (a sleeping printer, #5710): that one is + /// cancelled, so it is never sent, and quitting doesn't hang on it. Every job is cancelled + /// before any is waited for, so a waiting job can't start sending meanwhile. The Print page's + /// Cancel asks only the latest job. One test, because JOBS is global. + #[test] + fn shutdown_waits_for_jobs_being_sent_and_cancels_the_rest() { + let (sending, release_sending, sent) = worker(); + let (stuck, release_stuck, _) = worker(); + let (stop_sending, sending_asked) = asked(false); + let (stop_stuck, cancelled) = asked(true); + // The job being sent comes first, so a one-at-a-time shutdown would join it before + // cancelling the waiting one. + *JOBS.lock().unwrap() = + vec![Job { worker: sending, cancel: stop_sending, isolated: false }, Job { worker: stuck, cancel: stop_stuck, isolated: false }]; + // Cancel from the Print page asks the latest job only. + assert!(cancel()(), "the latest job was still waiting: never sent"); + assert!(!sending_asked.load(std::sync::atomic::Ordering::SeqCst), "an earlier job isn't the one being submitted"); + cancelled.store(false, std::sync::atomic::Ordering::SeqCst); + let (finished, shutdown) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + finish_pending_jobs(); + let _ = finished.send(()); + }); + let wait = std::time::Duration::from_millis(200); + assert!(shutdown.recv_timeout(wait).is_err(), "shutdown waits for the job being sent"); + assert!(cancelled.load(std::sync::atomic::Ordering::SeqCst), "the waiting job was cancelled before the wait"); + release_sending.send(()).unwrap(); + assert!(shutdown.recv_timeout(std::time::Duration::from_secs(10)).is_ok(), "shutdown doesn't wait for the stuck job"); + sent.recv().unwrap(); + assert!(JOBS.lock().unwrap().is_empty(), "shutdown took every job"); + // A job that has already been sent answers false, so the Print page keeps its receipt. + let (done, release_done, finished) = worker(); + release_done.send(()).unwrap(); + finished.recv().unwrap(); + *JOBS.lock().unwrap() = vec![Job { worker: done, cancel: Box::new(|| false), isolated: false }]; + assert!(!cancel()(), "a sent job isn't reported as cancelled"); + JOBS.lock().unwrap().clear(); + // The stuck worker was left behind without blocking shutdown; let it end. + release_stuck.send(()).unwrap(); } } diff --git a/apps/wordcraft/src/print/isolated.rs b/apps/wordcraft/src/print/isolated.rs new file mode 100644 index 0000000000..b2463b5d92 --- /dev/null +++ b/apps/wordcraft/src/print/isolated.rs @@ -0,0 +1,592 @@ +//! One Windows driver submission per process: a cancelled driver cannot retain another +//! job's provider lock. The child acknowledges its Cancel token before it exits. + +use std::{ + io::{BufRead, BufReader, Read, Write}, + path::Path, + process::{Child, ChildStdin, Command, Stdio}, + sync::{Arc, Condvar, Mutex}, + time::Duration, +}; + +use serde::{Deserialize, Serialize}; + +#[cfg(windows)] +const FLAG: &str = "--wordcraft-print-worker"; +const PREFIX: &str = "WORDCRAFT_PRINT:"; +const MAX_MESSAGE: u64 = 64 * 1024; +const MAX_PDF: u64 = 512 * 1024 * 1024; +const CANCELLED: &str = "the print job was cancelled before anything was sent to the printer"; + +#[derive(Serialize, Deserialize)] +enum Duplex { + Off, + LongEdge, + ShortEdge, +} + +#[derive(Serialize, Deserialize)] +struct Request { + printer: Option, + copies: u32, + collate: bool, + duplex: Option, + grayscale: bool, + shrink: bool, + title: String, + options: Vec<(String, String)>, +} + +impl Request { + /// Preserve all spooler options in a platform-independent wire representation. + fn from_job(job: &craft_print::Job) -> Self { + Self { + printer: job.printer.clone(), + copies: job.copies, + collate: job.collate, + duplex: job.duplex.map(|d| match d { + craft_print::Duplex::Off => Duplex::Off, + craft_print::Duplex::LongEdge => Duplex::LongEdge, + craft_print::Duplex::ShortEdge => Duplex::ShortEdge, + }), + grayscale: job.grayscale, + shrink: job.scaling == craft_print::Scaling::ShrinkToFit, + title: job.title.clone(), + options: job.options.clone(), + } + } + + /// Restore the job without substituting printer defaults or changing copy semantics. + fn job(self) -> craft_print::Job { + craft_print::Job { + printer: self.printer, + copies: self.copies, + collate: self.collate, + duplex: self.duplex.map(|d| match d { + Duplex::Off => craft_print::Duplex::Off, + Duplex::LongEdge => craft_print::Duplex::LongEdge, + Duplex::ShortEdge => craft_print::Duplex::ShortEdge, + }), + grayscale: self.grayscale, + scaling: if self.shrink { craft_print::Scaling::ShrinkToFit } else { craft_print::Scaling::Exact }, + title: self.title, + options: self.options, + } + } +} + +#[derive(Serialize, Deserialize)] +enum Event { + Ready, + Cancelled, + Sending, + Done(Result), +} + +enum State { + Starting, + Waking(Arc>), + Running(ChildStdin), + Sending(ChildStdin), + Cancelled, + Finished, +} + +#[derive(Clone)] +pub(crate) struct Submission(Arc<(Mutex, Condvar)>); + +impl Submission { + /// Create one submission whose driver cannot run until the parent grants startup. + pub(crate) fn new() -> Self { + Self(Arc::new((Mutex::new(State::Starting), Condvar::new()))) + } + + /// Recover the lifecycle state after poisoning without invoking another driver call. + fn state(&self) -> std::sync::MutexGuard<'_, State> { + self.0.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner) + } + + /// Only an acknowledgement of the child's token can cancel a running helper. A timeout + /// is a refusal, never permission to kill a process that may already be sending. + pub(crate) fn cancel(&self) -> bool { + let mut state = self.state(); + match &mut *state { + State::Starting => { + *state = State::Cancelled; + return true; + } + State::Waking(child) => { + let _ = child.lock().unwrap_or_else(std::sync::PoisonError::into_inner).kill(); + *state = State::Cancelled; + return true; + } + State::Cancelled => return true, + State::Finished | State::Sending(_) => return false, + State::Running(input) => { + if input.write_all(b"cancel\n").and_then(|_| input.flush()).is_err() { + return false; + } + } + } + let (state, _) = self + .0 + .1 + .wait_timeout_while(state, Duration::from_secs(1), |s| matches!(s, State::Running(_))) + .unwrap_or_else(std::sync::PoisonError::into_inner); + matches!(*state, State::Cancelled) + } + + #[cfg(windows)] + /// Submit through this executable's private helper mode and await its final receipt. + pub(crate) fn submit(&self, pdf: &[u8], job: &craft_print::Job) -> Result { + self.submit_with(pdf, job, |directory| { + let mut command = Command::new(std::env::current_exe()?); + command.arg(FLAG).arg(directory); + Ok(command) + }) + } + + /// Publish a terminal state on every result, waking a cancellation caller awaiting an ack. + fn submit_with(&self, pdf: &[u8], job: &craft_print::Job, command: impl FnOnce(&Path) -> std::io::Result) -> Result { + let result = self.run(pdf, job, command); + let mut state = self.state(); + if !matches!(*state, State::Cancelled) { + *state = State::Finished; + } + self.0.1.notify_all(); + if matches!(*state, State::Cancelled) { Err(CANCELLED.into()) } else { result } + } + + /// Own the private inputs and child until receipt, acknowledged cancellation or failure. + fn run(&self, pdf: &[u8], job: &craft_print::Job, command: impl FnOnce(&Path) -> std::io::Result) -> Result { + if matches!(*self.state(), State::Cancelled) { + return Err(CANCELLED.into()); + } + if pdf.len() as u64 > MAX_PDF { + return Err("the print PDF exceeds the 512 MiB helper limit".into()); + } + let request = serde_json::to_vec(&Request::from_job(job)).map_err(error)?; + if request.len() as u64 > MAX_MESSAGE { + return Err("the print settings exceed the helper message limit".into()); + } + let directory = tempfile::tempdir().map_err(error)?; + std::fs::write(directory.path().join("job.json"), request).map_err(error)?; + std::fs::write(directory.path().join("input.pdf"), pdf).map_err(error)?; + let child = + command(directory.path()).map_err(error)?.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().map_err(error)?; + let child = Arc::new(Mutex::new(child)); + let outcome = (|| { + let (input, output) = { + let mut process = child.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + (process.stdin.take().ok_or("print helper has no input pipe")?, process.stdout.take().ok_or("print helper has no output pipe")?) + }; + { + let mut state = self.state(); + if matches!(*state, State::Cancelled) { + return Err(CANCELLED.into()); + } + *state = State::Waking(child.clone()); + } + let mut output = BufReader::new(output); + loop { + let message = line(&mut output)?.ok_or("the print helper stopped before it was ready")?; + let Some((_, json)) = message.split_once(PREFIX) else { continue }; + if !matches!(serde_json::from_str::(json).map_err(error)?, Event::Ready) { + return Err("the print helper did not acknowledge startup".into()); + } + break; + } + { + let mut state = self.state(); + if matches!(*state, State::Cancelled) { + return Err(CANCELLED.into()); + } + *state = State::Running(input); + if let State::Running(input) = &mut *state { + input.write_all(b"start\n").and_then(|_| input.flush()).map_err(error)?; + } + } + while let Some(line) = line(&mut output)? { + let Some((_, json)) = line.split_once(PREFIX) else { continue }; + match serde_json::from_str::(json).map_err(error)? { + Event::Ready => return Err("the print helper repeated its startup acknowledgement".into()), + Event::Cancelled => { + *self.state() = State::Cancelled; + self.0.1.notify_all(); + return Err(CANCELLED.into()); + } + Event::Sending => { + let mut state = self.state(); + let previous = std::mem::replace(&mut *state, State::Finished); + *state = match previous { + State::Running(input) | State::Sending(input) => State::Sending(input), + other => other, + }; + self.0.1.notify_all(); + } + Event::Done(result) => return result, + } + } + Err("the print helper stopped without a receipt".into()) + })(); + // A cancelled child has acknowledged that it cannot send. Other outcomes are terminal + // receipts or a broken protocol/pipe; reap the process and its private input files. + if outcome.is_err() { + let _ = child.lock().unwrap_or_else(std::sync::PoisonError::into_inner).kill(); + } + let status = child.lock().unwrap_or_else(std::sync::PoisonError::into_inner).wait().map_err(error)?; + if outcome.is_ok() && !status.success() { + return Err("the print helper failed after its receipt".into()); + } + outcome + } +} + +/// Keep helper transport failures distinct from a printer's own error message. +fn error(error: impl std::fmt::Display) -> String { + format!("print helper: {error}") +} + +/// Read at most one byte beyond the cap so oversized files fail without unbounded allocation. +fn bounded(mut input: impl Read, limit: u64) -> Result, String> { + let mut bytes = Vec::new(); + input.by_ref().take(limit.saturating_add(1)).read_to_end(&mut bytes).map_err(error)?; + if bytes.len() as u64 > limit { Err("print helper input exceeds its limit".into()) } else { Ok(bytes) } +} + +/// Read one bounded UTF-8 protocol record, distinguishing EOF from an oversized message. +fn line(input: &mut impl BufRead) -> Result, String> { + let mut bytes = Vec::new(); + input.take(MAX_MESSAGE + 1).read_until(b'\n', &mut bytes).map_err(error)?; + if bytes.len() as u64 > MAX_MESSAGE { + return Err("print helper message exceeds its limit".into()); + } + if bytes.is_empty() { Ok(None) } else { String::from_utf8(bytes).map(|s| Some(s.trim_end().to_owned())).map_err(error) } +} + +/// Serialize complete events under stdout's lock so the driver and listener cannot interleave. +fn emit(event: &Event) -> Result<(), String> { + let mut output = std::io::stdout().lock(); + write!(output, "{PREFIX}").map_err(error)?; + serde_json::to_writer(&mut output, event).map_err(error)?; + writeln!(output).and_then(|_| output.flush()).map_err(error) +} + +/// Remove both plaintext inputs even when one fails; already-removed inputs count as success. +fn remove_inputs(directory: &Path) -> Result<(), String> { + let mut first_error = None; + for name in ["job.json", "input.pdf"] { + if let Err(failure) = std::fs::remove_file(directory.join(name)) + && failure.kind() != std::io::ErrorKind::NotFound + { + first_error.get_or_insert_with(|| error(format!("could not remove {name}: {failure}"))); + } + } + first_error.map_or(Ok(()), Err) +} + +/// Load and erase the private inputs before driver work, while an independent listener can cancel. +fn helper( + directory: &Path, + cancel_request: impl Fn(&craft_print::Cancel) -> bool + Send + 'static, + submit: impl FnOnce(&[u8], &craft_print::Job, &craft_print::Cancel) -> Result, +) -> Result<(), String> { + let mut input = BufReader::new(std::io::stdin()); + emit(&Event::Ready)?; + if line(&mut input)?.as_deref() != Some("start") { + return Err("print helper requires a start acknowledgement".into()); + } + let cancel = craft_print::Cancel::new(); + let listener = cancel.clone(); + std::thread::Builder::new() + .name("print-cancel".into()) + .spawn(move || { + loop { + match line(&mut input) { + Ok(Some(command)) if command == "cancel" => { + if cancel_request(&listener) { + let _ = emit(&Event::Cancelled); + std::process::exit(0); + } + if emit(&Event::Sending).is_err() { + break; + } + } + _ => break, + } + } + // If the parent disappears, never leave a waiting driver process behind. Sending + // jobs finish normally so the OS gets their pages even if the UI has gone away. + if cancel_request(&listener) { + std::process::exit(0); + } + }) + .map_err(error)?; + let request: Request = + serde_json::from_slice(&bounded(std::fs::File::open(directory.join("job.json")).map_err(error)?, MAX_MESSAGE)?).map_err(error)?; + let pdf = bounded(std::fs::File::open(directory.join("input.pdf")).map_err(error)?, MAX_PDF)?; + remove_inputs(directory)?; + let result = submit(&pdf, &request.job(), &cancel); + emit(&Event::Done(result)) +} + +#[cfg(windows)] +/// Handle only the private first-argument flag, before logging or GUI initialization. +pub(crate) fn helper_mode() -> Option { + let mut args = std::env::args_os().skip(1); + if args.next().as_deref() != Some(std::ffi::OsStr::new(FLAG)) { + return None; + } + let result = args.next().ok_or_else(|| "print helper directory missing".to_owned()).and_then(|dir| { + helper(Path::new(&dir), craft_print::Cancel::cancel, |pdf, job, cancel| { + craft_print::submit_cancellable(pdf, job, &mut |_, _| {}, cancel).map_err(error) + }) + }); + if let Err(message) = &result { + let _ = emit(&Event::Done(Err(message.clone()))); + } + Some(i32::from(result.is_err())) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A real test child exercises production pipes, token handling and process cleanup. + #[test] + #[ignore = "launched by the process protocol tests"] + fn helper_process() { + let Some(directory) = std::env::var_os("WORDCRAFT_TEST_PRINT_DIR") else { return }; + if std::env::var("WORDCRAFT_TEST_PRINT_MODE").as_deref() == Ok("waking") { + std::fs::write(Path::new(&directory).join("waking"), b"starting").unwrap(); + loop { + std::thread::park(); + } + } + let result = helper( + Path::new(&directory), + |token| { + if std::env::var("WORDCRAFT_TEST_PRINT_MODE").as_deref() == Ok("sending") { false } else { token.cancel() } + }, + |pdf, job, _token| { + assert_eq!(pdf, b"synthetic PDF"); + assert_eq!(job.printer.as_deref(), Some("test queue")); + match std::env::var("WORDCRAFT_TEST_PRINT_MODE").as_deref() { + Ok("held") => { + let _provider = Mutex::new(()); + let _held = _provider.lock().unwrap(); + std::fs::write(Path::new(&directory).join("held"), b"driver entered").unwrap(); + loop { + std::thread::park(); + } + } + Ok("crash") => std::process::exit(7), + Ok("sending") => { + std::fs::write(Path::new(&directory).join("sending"), b"sending").unwrap(); + while !Path::new(&directory).join("release").exists() { + std::thread::sleep(Duration::from_millis(5)); + } + Ok("spooled synthetic job".into()) + } + _ => Ok("spooled synthetic job".into()), + } + }, + ); + std::process::exit(i32::from(result.is_err())); + } + + /// Launch just the ignored helper entry, without invoking printer hardware. + fn command(directory: &Path, mode: &str) -> std::io::Result { + let mut command = Command::new(std::env::current_exe()?); + let name = module_path!().split_once("::").map_or(module_path!(), |(_, name)| name); + command.args(["--ignored", "--exact", &format!("{name}::helper_process"), "--nocapture"]); + command.env("WORDCRAFT_TEST_PRINT_DIR", directory).env("WORDCRAFT_TEST_PRINT_MODE", mode); + Ok(command) + } + + /// Give the child a literal printer identity to verify transport. + fn job() -> craft_print::Job { + craft_print::Job { printer: Some("test queue".into()), ..Default::default() } + } + + #[test] + /// Cancelling before submission cannot launch a driver process. + fn cancelled_start_never_launches_a_helper() { + let submission = Submission::new(); + assert!(submission.cancel()); + let result = submission.submit_with(b"synthetic PDF", &job(), |_| Err(std::io::Error::other("must not launch"))); + assert_eq!(result, Err(CANCELLED.into())); + } + + #[test] + /// A cancellation during creation withholds the start grant and removes private inputs. + fn cancellation_during_startup_never_starts_the_driver() { + let submission = Submission::new(); + let worker = submission.clone(); + let (started, starting) = std::sync::mpsc::channel(); + let (release, wait) = std::sync::mpsc::channel(); + let running = std::thread::spawn(move || { + worker.submit_with(b"synthetic PDF", &job(), |directory| { + started.send(directory.to_path_buf()).unwrap(); + wait.recv().unwrap(); + command(directory, "held") + }) + }); + let directory = starting.recv_timeout(Duration::from_secs(10)).unwrap(); + assert!(submission.cancel()); + release.send(()).unwrap(); + assert_eq!(running.join().unwrap(), Err(CANCELLED.into())); + assert!(!directory.exists(), "startup cancellation reaps the child and removes its inputs"); + } + + #[test] + /// A hung startup has no permission to send and can be killed without a token ack. + fn an_unresponsive_starting_process_can_be_stopped_without_an_acknowledgement() { + let submission = Submission::new(); + let worker = submission.clone(); + let (path_tx, path_rx) = std::sync::mpsc::channel(); + let running = std::thread::spawn(move || { + worker.submit_with(b"synthetic PDF", &job(), |dir| { + path_tx.send(dir.to_path_buf()).unwrap(); + command(dir, "waking") + }) + }); + let directory = path_rx.recv_timeout(Duration::from_secs(10)).unwrap(); + let deadline = std::time::Instant::now() + Duration::from_secs(10); + while !directory.join("waking").exists() && std::time::Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(5)); + } + assert!(directory.join("waking").exists()); + assert!(submission.cancel(), "a process without a start grant cannot send anything"); + assert_eq!(running.join().unwrap(), Err(CANCELLED.into())); + assert!(!directory.exists()); + } + + #[test] + /// Job transport preserves every explicit setting, including Unicode and duplex direction. + fn job_options_survive_the_process_boundary() { + let expected = craft_print::Job { + printer: Some("Office λ".into()), + copies: 3, + collate: false, + duplex: Some(craft_print::Duplex::ShortEdge), + grayscale: true, + scaling: craft_print::Scaling::ShrinkToFit, + title: "A document".into(), + options: vec![("media".into(), "iso_a4_210x297mm".into())], + }; + let bytes = serde_json::to_vec(&Request::from_job(&expected)).unwrap(); + let received: Request = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(received.job(), expected); + } + + #[test] + /// Retry succeeds immediately after cancellation, before joining the old worker. + fn cancelled_driver_does_not_block_the_next_process() { + let submission = Submission::new(); + let worker = submission.clone(); + let (path_tx, path_rx) = std::sync::mpsc::channel(); + let running = std::thread::spawn(move || { + worker.submit_with(b"synthetic PDF", &job(), |dir| { + path_tx.send(dir.to_path_buf()).unwrap(); + command(dir, "held") + }) + }); + let directory = path_rx.recv_timeout(Duration::from_secs(10)).unwrap(); + let deadline = std::time::Instant::now() + Duration::from_secs(10); + while !directory.join("held").exists() && std::time::Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(5)); + } + assert!(directory.join("held").exists(), "the child is held inside its driver"); + assert!(!directory.join("input.pdf").exists(), "the stalled driver retains no plaintext PDF"); + assert!(!directory.join("job.json").exists(), "loaded settings were erased before driver work"); + assert!(submission.cancel(), "a stalled driver still acknowledges cancellation"); + let next = Submission::new(); + assert_eq!(next.submit_with(b"synthetic PDF", &job(), |dir| command(dir, "ready")), Ok("spooled synthetic job".into())); + assert_eq!(running.join().unwrap(), Err(CANCELLED.into())); + assert!(!directory.exists(), "the reaped helper's PDF and settings were removed"); + assert!(!next.cancel(), "a completed job keeps its receipt"); + } + + #[test] + /// A refused cancellation keeps the sending process alive until its receipt. + fn cancellation_refusal_preserves_the_sending_job() { + let submission = Submission::new(); + let worker = submission.clone(); + let (path_tx, path_rx) = std::sync::mpsc::channel(); + let running = std::thread::spawn(move || { + worker.submit_with(b"synthetic PDF", &job(), |dir| { + path_tx.send(dir.to_path_buf()).unwrap(); + command(dir, "sending") + }) + }); + let directory = path_rx.recv_timeout(Duration::from_secs(10)).unwrap(); + let deadline = std::time::Instant::now() + Duration::from_secs(10); + while !directory.join("sending").exists() && std::time::Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(5)); + } + assert!(directory.join("sending").exists()); + assert!(!submission.cancel(), "a sending job refuses cancellation"); + assert!(matches!(*submission.state(), State::Sending(_))); + assert!(!running.is_finished(), "the child is retained until it finishes sending"); + std::fs::write(directory.join("release"), b"continue").unwrap(); + assert_eq!(running.join().unwrap(), Ok("spooled synthetic job".into())); + assert!(!directory.exists()); + } + + #[test] + /// Closing the parent's pipe stops a waiting driver and permits parent-side cleanup. + fn parent_disconnect_stops_a_waiting_driver() { + let submission = Submission::new(); + let worker = submission.clone(); + let (path_tx, path_rx) = std::sync::mpsc::channel(); + let running = std::thread::spawn(move || { + worker.submit_with(b"synthetic PDF", &job(), |dir| { + path_tx.send(dir.to_path_buf()).unwrap(); + command(dir, "held") + }) + }); + let directory = path_rx.recv_timeout(Duration::from_secs(10)).unwrap(); + let deadline = std::time::Instant::now() + Duration::from_secs(10); + while !directory.join("held").exists() && std::time::Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(5)); + } + assert!(directory.join("held").exists()); + *submission.state() = State::Finished; // Drops the parent's pipe as on process exit. + assert_eq!(running.join().unwrap(), Err("the print helper stopped without a receipt".into())); + assert!(!directory.exists()); + } + + #[test] + /// Helper failure removes its input directory; input limits reject oversize data. + fn helper_crashes_are_errors_and_inputs_are_bounded() { + let submission = Submission::new(); + let mut inputs = None; + assert_eq!( + submission.submit_with(b"synthetic PDF", &job(), |dir| { + inputs = Some(dir.to_path_buf()); + command(dir, "crash") + }), + Err("the print helper stopped without a receipt".into()) + ); + assert!(!inputs.unwrap().exists(), "a helper crash removes the PDF and settings"); + assert_eq!(bounded(&b"abcd"[..], 3), Err("print helper input exceeds its limit".into())); + assert_eq!(bounded(&b"abc"[..], 3), Ok(b"abc".to_vec())); + } + + /// Cleanup is repeatable and reports real failures while still attempting both inputs. + #[test] + fn input_cleanup_tolerates_missing_files_and_reports_failures() { + let directory = tempfile::tempdir().unwrap(); + std::fs::write(directory.path().join("job.json"), b"settings").unwrap(); + std::fs::write(directory.path().join("input.pdf"), b"private PDF").unwrap(); + assert_eq!(remove_inputs(directory.path()), Ok(())); + assert_eq!(remove_inputs(directory.path()), Ok(())); + assert!(!directory.path().join("input.pdf").exists()); + std::fs::create_dir(directory.path().join("job.json")).unwrap(); + std::fs::write(directory.path().join("input.pdf"), b"private PDF").unwrap(); + let error = remove_inputs(directory.path()).unwrap_err(); + assert!(error.contains("could not remove job.json"), "{error}"); + assert!(!directory.path().join("input.pdf").exists(), "a settings cleanup failure does not retain the PDF"); + } +} diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index 0bfe531cee..91275a2a03 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -746,6 +746,7 @@ impl WordApp { // Native printing submits the selected queue on a worker; the browser owns its dialog. "ui.printSettings" => return Some(printing::settings(&mut self.printing, p, self.services.printers.as_ref())), "ui.print" | "ui.printSubmit" => return Some(self.print_to_system(p)), + "ui.printCancel" => return Some(printing::cancel(self)), // File › Options › General › Make WordCraft the default… changes the user's system // default apps (Linux) or opens system settings (Windows), so only a click on that // button does it ([`WordApp::make_default_app`]); scripts, the control channel and @@ -1054,6 +1055,7 @@ pub(crate) fn works_without_document(id: &str) -> bool { | "ui.language" | "ui.saveChanges" | "ui.openFileDialog" + | "ui.printCancel" | "ui.fileBrowser" | "ui.makeDefaultApp" | "ui.discord" diff --git a/crates/ui-egui/src/print_page.rs b/crates/ui-egui/src/print_page.rs index b74e25f9cd..edb6f87e31 100644 --- a/crates/ui-egui/src/print_page.rs +++ b/crates/ui-egui/src/print_page.rs @@ -208,6 +208,10 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { if let Some(discover) = &app.services.printers { crate::printing::show(&mut app.printing, ui, discover, |label| crate::i18n::t(label).to_owned()); } + // A job can wait long on a sleeping printer's driver; Cancel frees the page. + if app.printing.busy() && app.services.cancel_print.is_some() && ui.button(tl!("Cancel printing")).clicked() { + let _ = app.run("ui.printCancel", serde_json::json!({})); + } app.ui.print.copies = app.printing.settings.copies; } else if hook { ui.label(tl!("Send the document straight to the system print dialog, or save a copy in another format below.")); diff --git a/crates/ui-egui/src/printing.rs b/crates/ui-egui/src/printing.rs index 93aab7de39..619b1c1dc7 100644 --- a/crates/ui-egui/src/printing.rs +++ b/crates/ui-egui/src/printing.rs @@ -16,6 +16,9 @@ pub fn commands(app: &crate::WordApp) -> Vec { "enabled":true}), json!({"id":"ui.printSubmit", "label":"Submit print job", "location":"File › Print", "shortcut":null, "params":submit_params, "enabled":enabled}), json!({"id":"ui.print", "label":"Print (alias of ui.printSubmit)", "location":"File › Print", "shortcut":null, "params":submit_params, "enabled":enabled}), + json!({"id":"ui.printCancel", "label":"Cancel printing", "location":"File › Print", "shortcut":null, + "params":"none. Stops the job being submitted if it hasn't started reaching the spooler (it may wait on a sleeping printer's driver); returns cancelled: true when nothing will be sent, false when the job was already being sent (cancel it from the system's print queue).", + "enabled": app.services.cancel_print.is_some() && app.printing.busy()}), ] } @@ -127,6 +130,28 @@ pub fn feedback(message: &Feedback, translate: impl Fn(&str) -> String) -> Strin } } +/// `ui.printCancel`: stop the job being submitted before it reaches the spooler. A job can wait +/// a long time on a sleeping network printer's driver (#5710) with the print settings locked; +/// once cancelled it is never sent, so the panel is free again at once: printer list and +/// settings kept, the printers listed again. +pub fn cancel(app: &mut crate::WordApp) -> Result { + if !app.printing.busy() { + return Err("no print job is being submitted".into()); + } + let cancel = app.services.cancel_print.as_ref().ok_or("cancelling a print job isn't available here")?; + if !cancel() { + app.status(tl!("Cancellation could not be confirmed here. The printer driver may still be processing the job. If it appears in the system's print queue, cancel it there.")); + return Ok(serde_json::json!({"cancelled": false})); + } + // The shared state keeps the job's receiver private: a fresh one drops it (the worker's + // answer, if it ever comes, goes nowhere) and keeps the user's settings. + let old = std::mem::take(&mut app.printing); + app.printing.settings = old.settings; + app.printing.printers = old.printers; + app.status(tl!("Printing cancelled. Nothing was sent to the printer.")); + Ok(serde_json::json!({"cancelled": true})) +} + /// The IPP/PWG 5101.1 media name of a Layout › Size paper (the spooler's `media` option). pub fn pwg_media(paper: &str) -> Option<&'static str> { Some(match paper { diff --git a/crates/ui-egui/src/services.rs b/crates/ui-egui/src/services.rs index 3ac6cb3806..dcff376d0a 100644 --- a/crates/ui-egui/src/services.rs +++ b/crates/ui-egui/src/services.rs @@ -8,6 +8,9 @@ pub struct Services { pub printers: Option, /// Desktop: submit the PDF with the chosen settings on a worker. pub print_native: Option, + /// Desktop: stop the print job still waiting to be sent (on a sleeping printer's driver, + /// say). `true`: it will never be sent; `false`: it was already being sent. + pub cancel_print: Option bool>>, /// Pick a file to open; the argument says what for (`document`, `picture`). pub pick_open: Option Option>>, /// Pick a path to save to, given a suggested name. diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs index 4208a3132b..9a3e321c6f 100644 --- a/crates/ui-egui/src/tests.rs +++ b/crates/ui-egui/src/tests.rs @@ -3582,6 +3582,65 @@ fn a_successful_native_print_returns_to_the_document() { } } +/// #5710: a job waiting on a sleeping network printer's driver locked the Print page until the +/// driver answered. `ui.printCancel` stops it before it is sent and frees the page at once, +/// keeping the chosen printer; a job already being sent can't be stopped, and the page says so. +#[test] +fn cancelling_a_waiting_print_job_frees_the_print_page() { + use printing::Printer; + for (never_sent, close_document) in [(true, false), (false, false), (true, true), (false, true)] { + let mut a = app(); + a.printing.printers = vec![Printer { name: "Office".into(), label: String::new(), default: true }]; + a.printing.settings.printer = "Office".into(); + a.printing.settings.copies = 3; + // The worker never answers, as when the driver doesn't: keep its sender alive. + let held = std::rc::Rc::new(std::cell::RefCell::new(Vec::new())); + let senders = held.clone(); + a.services.print_native = Some(Box::new(move |_, _, _, _| { + let (tx, rx) = std::sync::mpsc::channel(); + senders.borrow_mut().push(tx); + rx + })); + let asked = std::rc::Rc::new(std::cell::Cell::new(0)); + let cancels = asked.clone(); + a.services.cancel_print = Some(Box::new(move || { + cancels.set(cancels.get() + 1); + never_sent + })); + for junk in [json!({}), json!(null), json!("all"), json!([1, 2]), json!({"cancel": -1, "job": "x"})] { + assert!(a.run("ui.printCancel", junk).is_err(), "nothing to cancel before a job is submitted"); + } + a.run("ui.print", json!({})).unwrap(); + assert!(a.printing.busy(), "the job waits"); + if close_document { + a.execute("file.close", json!({})).unwrap(); + assert!(!a.has_document(), "the last document closed"); + assert!(a.printing.busy(), "closing the document keeps its pending print job"); + } + let enabled = |a: &WordApp| printing::commands(a).iter().any(|c| c["id"] == "ui.printCancel" && c["enabled"] == true); + assert!(enabled(&a), "Cancel is offered while the job waits"); + let reply = a.execute("ui.printCancel", json!({})).unwrap(); + assert_eq!(reply["cancelled"], never_sent); + assert_eq!(asked.get(), 1, "the host was asked to stop the job"); + assert_eq!(a.printing.busy(), !never_sent, "never_sent {never_sent}: whether the page is still locked"); + assert_eq!(a.printing.settings.printer, "Office", "the chosen printer is kept"); + assert_eq!(a.printing.settings.copies, 3, "the settings are kept"); + assert_eq!(a.printing.printers.len(), 1, "the printer list is kept"); + let said = a.status_msg.as_ref().map(|(text, _)| text.as_str()).unwrap_or_default(); + let expected = if never_sent { + "Printing cancelled. Nothing was sent to the printer." + } else { + "Cancellation could not be confirmed here. The printer driver may still be processing the job. If it appears in the system's print queue, cancel it there." + }; + assert_eq!(said, expected, "never_sent {never_sent}: the status bar says what happened"); + assert_eq!(enabled(&a), !never_sent); + if never_sent { + assert_eq!(a.execute("ui.printCancel", json!({})).unwrap_err(), "no print job is being submitted"); + } + assert_eq!(held.borrow().len(), 1); + } +} + /// #1156: the Print page's preview fills the pane beside the settings, zooms from its own /// bar, scrolls both ways once the page outgrows the pane, and One Page fits it again. #[test] diff --git a/docs/gaps.md b/docs/gaps.md index 00ddc72161..635f11ddac 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,8 +1,8 @@ # Where WordCraft falls short of Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** trivial (Page drawing indexes text-box warp lookups per paragraph (#3614)); previously trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (MATH construction reuse, #3630); previously trivial (DOCX `w:adjustRightInd` defaults true and is written in schema order); previously trivial (ink page lookups reuse scanned paragraph prefixes, #3614); previously trivial (#19: ODT settings for what the standard leaves open removed); previously trivial (#19: ODT element audit; files from other applications checked); previously trivial (#11: the dialogs of the mirrored Arabic interface read right to left: label-and-field rows, tables (list columns, the Models table), tile galleries and the Add/Remove arrows follow the reading direction, through one `rtl::Table` helper that draws exactly as before in English; no ranked gap closed, readiness estimates unchanged); previously trivial (#4: fonts that aren't installed are flagged in the font box and status bar); previously trivial (#19: ODT sections inside text boxes kept as read); previously trivial (#19: an ODT saved without edits keeps what WordCraft doesn't model); previously trivial (#19: ODT interpretation profile, WordCraft / LibreOffice / custom); previously trivial (#19: ODT form controls placed and multi-line; custom shapes keep their geometry); previously trivial (#19: ODT in-line frames placed against the text; template instantiation listed as left); previously trivial (#19: ODT charts kept as read; frame outlines; default header height); previously trivial (#19: ODT tables no longer gain repeated header rows); previously trivial (#19: ODT padding without a border, unequal columns, column breaks, picture-only lines); previously trivial (#19: ODT tables' margins and spacing, header spacing, pictures alone in a paragraph); previously trivial (#19: no LibreOffice template changes page count through a save); previously trivial (#19: ODT variables, emphasis marks, page backgrounds, fixed tables, default size); previously trivial (#19: ODT checked against LibreOffice's own templates: placeholders, first-page styles, unsized pages, metafile logos, `.ott`); previously trivial (#19: ODT at ~90%: custom shapes, cell formulas, macros kept, turned text, control sizes and buttons); previously trivial (#19 narrowed: ODT read and written nearly whole; estimate 18–27 h → 10–16 h); previously trivial (#10 narrowed: text in a turned text box is drawn turned with it); previously trivial (gap 19 narrowed: ODT heading numbering read and written); previously trivial (gap 19 narrowed: ODT table borders and padding read and written); previously trivial (gap 19 narrowed: ODT heading style looks read and written); previously trivial (gap 19 narrowed: ODT tab stops read and written); previously trivial (gap 19 narrowed: ODT character formatting read and written); previously trivial (gap 19 narrowed: ODT list formats read and written); previously trivial (gap 19 narrowed: ODT comments read and written); previously trivial (gap 19 narrowed: ODT footnotes and endnotes read and written); previously trivial (gap 19 narrowed: ODT headers and footers written, text fields and paragraph spacing read and written); previously trivial (#8: Options › Language lists Your languages and Add a language… replaces the Language packs table); previously trivial (#8: the missing-pack bar works on a fresh install and counts the caret's language); previously trivial (#8: Language pack languages are in Set Proofing Language, Review › Language and the status bar; built-in proofing is named as such); previously trivial (#20: layout resource caps and linear-time passes (#3867, #3866)); previously trivial (#3 narrowed: a pasted text box story is charged for the formatting it copies); previously trivial (#3 narrowed: a .doc table row keeps one override per cell); previously trivial (#3 narrowed: a .doc file's sections find their paragraphs by index); previously minor (#11: Arabic interface with mirrored shell landed, #1195; hi/id/pt-PT and Hebrew UI remain); previously trivial (#8: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland); no ranked gap changed); previously trivial (#8: language-packs-1 is published and pinned, 61 packs: Lithuanian, Romanian, Catalan, French, Greek, Slovak, Serbian and Latvian install as Language packs; Dutch, German and the other compounding languages wait on craft-proof; no ranked gap changed); previously minor (#8: Language packs: every other language's spelling and hyphenation installs from File › Options › Language or the missing-pack bar, through the font store; Lithuanian and Romanian are available as a Language pack once language-packs-1 is published; Dutch, Catalan, French, Greek, Slovak, Serbian and Latvian packs wait on craft-proof engine support); previously trivial (gap 8: English spelling reads SCOWL 2026, one dictionary per English, #40, #520, #5205; no ranked gap changed);; previously trivial (DOCX table rows and cells inside selected alternate-content branches survive open and repeated saves (#5219); broader corpus qualification and readiness estimates unchanged); previously trivial (#26 narrowed: MHT opens and `w:altChunk` content is brought in on open; saving MHT remains) previously trivial (gap 8 points at craft-proof in craft-libs, its data in craft-i18n, #5202; no ranked gap changed); previously trivial (arrowheads keep their kind, width and length and are drawn at Word's sizes; no ranked gap changed); previously trivial (.doc default font from the stylesheet's standard font, #5066; no ranked gap changed); previously trivial (a middle or bottom cell of a row split across pages lines up on the first page; no ranked gap changed); previously trivial (tab leader dots spaced and placed as in Word; no ranked gap changed); previously trivial (a list level above counts its first number at the first deeper item; no ranked gap changed); previously trivial (only the PDF sent to the printer leaves out link annotations; Save these pages as PDF keeps them, #4734; no ranked gap changed); previously trivial (documents with links print: the print job leaves out link annotations, #4734; no ranked gap changed; previously trivial: aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (note conversion reference-run membership uses one ordered walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (gap 19: RTF reads and writes WMF and EMF pictures, #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (View windows: several open documents and Switch Windows in the engine; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: text mini toolbar available with the right-click menu, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: tracked table/row/cell/section property changes record in WordCraft, narrowing gap 5; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: Format Page Numbers dialog carries Word's number formats and Continue from previous section / Start at, #1113; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: #8: English dialects — en-GB, en-CA, en-AU… flag each other's spellings, #1281; previously trivial: #1: shape gradients and transparency, #1500; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: line breaking: a slash opening a word travels whole to the next line, #2963; previously trivial: recipient list search and merged-only view, #1576; previously trivial: #2 impact restated: only ink rendering and OLE editing remain; previously trivial: #2 narrowed: native ink nested in groups and drawing canvases kept on save; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: automatic Asian/Latin and Asian/number spacing laid out, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: #24: the Spike, Ctrl+F3 and Ctrl+Shift+F3; previously trivial: VML freeforms read and kept on save; previously trivial: 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: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously trivial: sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; 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 Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** trivial (print helpers erase loaded plaintext inputs before driver work, #5710); previously trivial (Windows print submissions use isolated helpers so a cancelled driver cannot block a retry, #5710); previously trivial (print cancellation remains available without a document; sending-boundary limitation clarified, #5710); previously trivial (gap 6: a print job waiting on its driver can be cancelled, #5710; no ranked gap changed); previously trivial (gap 6: the printer list no longer waits on printer drivers, #5710; no ranked gap changed); previously trivial (Page drawing indexes text-box warp lookups per paragraph (#3614)); previously trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (MATH construction reuse, #3630); previously trivial (DOCX `w:adjustRightInd` defaults true and is written in schema order); previously trivial (ink page lookups reuse scanned paragraph prefixes, #3614); previously trivial (#19: ODT settings for what the standard leaves open removed); previously trivial (#19: ODT element audit; files from other applications checked); previously trivial (#11: the dialogs of the mirrored Arabic interface read right to left: label-and-field rows, tables (list columns, the Models table), tile galleries and the Add/Remove arrows follow the reading direction, through one `rtl::Table` helper that draws exactly as before in English; no ranked gap closed, readiness estimates unchanged); previously trivial (#4: fonts that aren't installed are flagged in the font box and status bar); previously trivial (#19: ODT sections inside text boxes kept as read); previously trivial (#19: an ODT saved without edits keeps what WordCraft doesn't model); previously trivial (#19: ODT interpretation profile, WordCraft / LibreOffice / custom); previously trivial (#19: ODT form controls placed and multi-line; custom shapes keep their geometry); previously trivial (#19: ODT in-line frames placed against the text; template instantiation listed as left); previously trivial (#19: ODT charts kept as read; frame outlines; default header height); previously trivial (#19: ODT tables no longer gain repeated header rows); previously trivial (#19: ODT padding without a border, unequal columns, column breaks, picture-only lines); previously trivial (#19: ODT tables' margins and spacing, header spacing, pictures alone in a paragraph); previously trivial (#19: no LibreOffice template changes page count through a save); previously trivial (#19: ODT variables, emphasis marks, page backgrounds, fixed tables, default size); previously trivial (#19: ODT checked against LibreOffice's own templates: placeholders, first-page styles, unsized pages, metafile logos, `.ott`); previously trivial (#19: ODT at ~90%: custom shapes, cell formulas, macros kept, turned text, control sizes and buttons); previously trivial (#19 narrowed: ODT read and written nearly whole; estimate 18–27 h → 10–16 h); previously trivial (#10 narrowed: text in a turned text box is drawn turned with it); previously trivial (gap 19 narrowed: ODT heading numbering read and written); previously trivial (gap 19 narrowed: ODT table borders and padding read and written); previously trivial (gap 19 narrowed: ODT heading style looks read and written); previously trivial (gap 19 narrowed: ODT tab stops read and written); previously trivial (gap 19 narrowed: ODT character formatting read and written); previously trivial (gap 19 narrowed: ODT list formats read and written); previously trivial (gap 19 narrowed: ODT comments read and written); previously trivial (gap 19 narrowed: ODT footnotes and endnotes read and written); previously trivial (gap 19 narrowed: ODT headers and footers written, text fields and paragraph spacing read and written); previously trivial (#8: Options › Language lists Your languages and Add a language… replaces the Language packs table); previously trivial (#8: the missing-pack bar works on a fresh install and counts the caret's language); previously trivial (#8: Language pack languages are in Set Proofing Language, Review › Language and the status bar; built-in proofing is named as such); previously trivial (#20: layout resource caps and linear-time passes (#3867, #3866)); previously trivial (#3 narrowed: a pasted text box story is charged for the formatting it copies); previously trivial (#3 narrowed: a .doc table row keeps one override per cell); previously trivial (#3 narrowed: a .doc file's sections find their paragraphs by index); previously minor (#11: Arabic interface with mirrored shell landed, #1195; hi/id/pt-PT and Hebrew UI remain); previously trivial (#8: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland); no ranked gap changed); previously trivial (#8: language-packs-1 is published and pinned, 61 packs: Lithuanian, Romanian, Catalan, French, Greek, Slovak, Serbian and Latvian install as Language packs; Dutch, German and the other compounding languages wait on craft-proof; no ranked gap changed); previously minor (#8: Language packs: every other language's spelling and hyphenation installs from File › Options › Language or the missing-pack bar, through the font store; Lithuanian and Romanian are available as a Language pack once language-packs-1 is published; Dutch, Catalan, French, Greek, Slovak, Serbian and Latvian packs wait on craft-proof engine support); previously trivial (gap 8: English spelling reads SCOWL 2026, one dictionary per English, #40, #520, #5205; no ranked gap changed);; previously trivial (DOCX table rows and cells inside selected alternate-content branches survive open and repeated saves (#5219); broader corpus qualification and readiness estimates unchanged); previously trivial (#26 narrowed: MHT opens and `w:altChunk` content is brought in on open; saving MHT remains) previously trivial (gap 8 points at craft-proof in craft-libs, its data in craft-i18n, #5202; no ranked gap changed); previously trivial (arrowheads keep their kind, width and length and are drawn at Word's sizes; no ranked gap changed); previously trivial (.doc default font from the stylesheet's standard font, #5066; no ranked gap changed); previously trivial (a middle or bottom cell of a row split across pages lines up on the first page; no ranked gap changed); previously trivial (tab leader dots spaced and placed as in Word; no ranked gap changed); previously trivial (a list level above counts its first number at the first deeper item; no ranked gap changed); previously trivial (only the PDF sent to the printer leaves out link annotations; Save these pages as PDF keeps them, #4734; no ranked gap changed); previously trivial (documents with links print: the print job leaves out link annotations, #4734; no ranked gap changed; previously trivial: aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (note conversion reference-run membership uses one ordered walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (gap 19: RTF reads and writes WMF and EMF pictures, #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (View windows: several open documents and Switch Windows in the engine; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: text mini toolbar available with the right-click menu, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: tracked table/row/cell/section property changes record in WordCraft, narrowing gap 5; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: Format Page Numbers dialog carries Word's number formats and Continue from previous section / Start at, #1113; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: #8: English dialects — en-GB, en-CA, en-AU… flag each other's spellings, #1281; previously trivial: #1: shape gradients and transparency, #1500; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: line breaking: a slash opening a word travels whole to the next line, #2963; previously trivial: recipient list search and merged-only view, #1576; previously trivial: #2 impact restated: only ink rendering and OLE editing remain; previously trivial: #2 narrowed: native ink nested in groups and drawing canvases kept on save; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: automatic Asian/Latin and Asian/number spacing laid out, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: #24: the Spike, Ctrl+F3 and Ctrl+Shift+F3; previously trivial: VML freeforms read and kept on save; previously trivial: 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: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously trivial: sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; 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 Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Highlight update:** 2026-10-25 · Highlight shortcut toggles the last chosen colour off when the whole selection has it; explicit colour commands remain setters (#4085). Readiness estimates unchanged. -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (#15: bibliography sources keep a corporate author, #4848); previously trivial (Table Properties applies to the selected rows, columns and cells, #4857); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (#7: Custom Document Properties dialog, #2982; previously trivial: egui 0.37 migration: the interface catalogs stay shared and pinned by `craft-i18n.rev` at `9205108cfdced5177706264038848b68b2422d2b` (27 languages, English fallback); input-method handling has new regressions but managed CI passed, and native IME acceptance plus native-speaker catalog review remain open; previously trivial: Markdown code spans keep line-broken text literal, #3738; previously trivial: small follow-ups from the field, table and AutoFormat batch, #4232; previously trivial: highlight target includes selected paragraph marks, #4085; previously trivial: full-target highlight check beyond 10,000 runs, #4085; previously trivial: Exact-spacing table contact uses the specified line height rather than paragraph-mark metrics (#3977); previously trivial: cu20we: restrict pre-spacing float origins to paragraph anchors; document cap helpers and regressions; previously trivial: a command that changes nothing is not an edit: no undo step, no unsaved changes, no relayout, and the redo stack survives it; previously minor: #8 narrowed: Spanish proofing (Spain, Mexico, Latin America, Uruguay) from the installed Hunspell dictionaries, #3360, #3553; previously trivial: gap 10: group connector rerouting preserves transformed free and unresolved endpoints under nonuniform scaling; obstacle routing still missing; readiness estimates unchanged; previously trivial: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: the ranked gap list held two rows each for #19, #20 and #21; each gap appears once now; a document with many relationships opens in the time its part takes, #3411; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: text mini toolbar available with the right-click menu, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: tracked table/row/cell/section property changes record in WordCraft, narrowing gap 5; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: Format Page Numbers dialog carries Word's number formats and Continue from previous section / Start at, #1113; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: #8: English dialects — en-GB, en-CA, en-AU… flag each other's spellings, #1281; previously trivial: #1: shape gradients and transparency, #1500; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: line breaking: a slash opening a word travels whole to the next line, #2963; previously trivial: recipient list search and merged-only view, #1576; previously trivial: #2 impact restated: only ink rendering and OLE editing remain; previously trivial: #2 narrowed: native ink nested in groups and drawing canvases kept on save; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: automatic Asian/Latin and Asian/number spacing laid out, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: #24: the Spike, Ctrl+F3 and Ctrl+Shift+F3; previously trivial: VML freeforms read and kept on save; previously trivial: 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: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously trivial: sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; 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 Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** trivial (Windows print submissions use isolated helpers so a cancelled driver cannot block a retry, #5710); previously trivial (print cancellation remains available without a document; sending-boundary limitation clarified, #5710); previously trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (#15: bibliography sources keep a corporate author, #4848); previously trivial (Table Properties applies to the selected rows, columns and cells, #4857); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (#7: Custom Document Properties dialog, #2982; previously trivial: egui 0.37 migration: the interface catalogs stay shared and pinned by `craft-i18n.rev` at `9205108cfdced5177706264038848b68b2422d2b` (27 languages, English fallback); input-method handling has new regressions but managed CI passed, and native IME acceptance plus native-speaker catalog review remain open; previously trivial: Markdown code spans keep line-broken text literal, #3738; previously trivial: small follow-ups from the field, table and AutoFormat batch, #4232; previously trivial: highlight target includes selected paragraph marks, #4085; previously trivial: full-target highlight check beyond 10,000 runs, #4085; previously trivial: Exact-spacing table contact uses the specified line height rather than paragraph-mark metrics (#3977); previously trivial: cu20we: restrict pre-spacing float origins to paragraph anchors; document cap helpers and regressions; previously trivial: a command that changes nothing is not an edit: no undo step, no unsaved changes, no relayout, and the redo stack survives it; previously minor: #8 narrowed: Spanish proofing (Spain, Mexico, Latin America, Uruguay) from the installed Hunspell dictionaries, #3360, #3553; previously trivial: gap 10: group connector rerouting preserves transformed free and unresolved endpoints under nonuniform scaling; obstacle routing still missing; readiness estimates unchanged; previously trivial: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: the ranked gap list held two rows each for #19, #20 and #21; each gap appears once now; a document with many relationships opens in the time its part takes, #3411; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: text mini toolbar available with the right-click menu, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: tracked table/row/cell/section property changes record in WordCraft, narrowing gap 5; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: Format Page Numbers dialog carries Word's number formats and Continue from previous section / Start at, #1113; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: #8: English dialects — en-GB, en-CA, en-AU… flag each other's spellings, #1281; previously trivial: #1: shape gradients and transparency, #1500; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: line breaking: a slash opening a word travels whole to the next line, #2963; previously trivial: recipient list search and merged-only view, #1576; previously trivial: #2 impact restated: only ink rendering and OLE editing remain; previously trivial: #2 narrowed: native ink nested in groups and drawing canvases kept on save; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: automatic Asian/Latin and Asian/number spacing laid out, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: #24: the Spike, Ctrl+F3 and Ctrl+Shift+F3; previously trivial: VML freeforms read and kept on save; previously trivial: 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: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously trivial: sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; 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 Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 Recovery discovery no longer interrupts an explicitly created or opened document. Older copies remain in File › Recover, whose buttons use the interface palette. See [local recovery](recovery.md). @@ -171,6 +171,10 @@ Table rows and cells inside `mc:AlternateContent` use the same selected-branch p | Date | Change | Summary | |---|---|---| +| 2026-11-06 | trivial | Print helpers remove loaded PDF/settings files before driver work, tolerate prior removal and report cleanup errors; a held-driver regression checks the files are already absent (#5710) | +| 2026-11-06 | trivial | Windows print submissions use a separate helper per job, preserving provider serialization inside the helper. An acknowledged cancellation stops that helper, so a stalled cancelled driver cannot block a later job; normal shutdown reaps helpers and removes temporary PDFs. The helper erases its plaintext inputs before driver work; a hard app crash during startup can still leave private temporary files. Startup requires an explicit start grant; after sending begins, jobs complete (#5710) | +| 2026-11-06 | trivial | Gap 6: File › Print: before sending starts, Cancel printing (`ui.printCancel`) stops a waiting job and frees the Print page while keeping its settings, including after the last document closes; a successfully cancelled job is never sent. Quitting cancels waiting jobs and joins jobs already sending (#5710). On Windows, winprint repeats the DEVMODE conversion after sending starts. A stall during that repeat prevents cancellation and can keep shutdown waiting, even before a native job exists. No ranked gap changed; readiness estimates unchanged | +| 2026-11-06 | trivial | Gap 6: On Windows the Print panel lists printers without asking their drivers anything, removing the driver query a networked printer in power save could hold, which kept the list empty with Refresh unavailable (#5710; likely also behind the long "Finding printers" waits of #5645 and #5413): discovery uses craft-print's `printer_queues` (storytold/craft-libs#618), and printing still checks the job's options with the driver; the reported printer is awaiting retest; no ranked gap changed; readiness estimates unchanged | | 2026-11-05 | trivial | PR #4545: eligible cell floats retain in-bounds offsets and touch the outer cell edge only on overflow, ignoring wrap distances for object placement. Front-of-text, turned-cell and outside-cell opt-outs remain. Synthetic geometry controls; native front/behind-text parity remains unqualified. | | 2026-11-05 | trivial | DOCX preserves `w:adjustRightInd` with its true default and writes it in schema order; grid layout remains open | | 2026-10-28 | trivial | Ink page lookups reuse scanned paragraph prefixes, #3614. Drawing order, geometry and saved data retain their behavior; no readiness estimate change | diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index 204df2ef9d..5e741c2f97 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -1,6 +1,6 @@ # Hardware parity with Microsoft Word -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-11-01 · **Change:** trivial (Linux Read Aloud: a sentence a screen reader cuts off is said again); previously trivial (ink page lookup work, #3614); previously trivial (only the PDF sent to the printer has no link annotations; Save these pages as PDF keeps them, #4734); previously trivial (a document with links prints: the print job's PDF has no link annotations, #4734; previously trivial: Pen / stylus row: two-stroke and closed-head arrows; previously trivial: Pen / stylus row: ink, eraser and Ink to Shape incl. arrows landed; previously trivial: a window too big for its screen shrinks to fit when it opens, #1215; previously trivial: 32-bit Windows starts on OpenGL; previously major: first version) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 and Word for Windows +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-11-06 · **Change:** trivial (print helpers erase loaded plaintext inputs before driver work, #5710); previously trivial (Windows print submissions use isolated helpers so a cancelled driver cannot block a retry, #5710); previously trivial (print cancellation remains available without a document; sending-boundary limitation clarified, #5710); previously trivial (a print job waiting on its driver can be cancelled, and quitting doesn't wait for it, #5710); previously trivial (the printer list no longer waits on printer drivers, #5710); previously trivial (Linux Read Aloud: a sentence a screen reader cuts off is said again); previously trivial (ink page lookup work, #3614); previously trivial (only the PDF sent to the printer has no link annotations; Save these pages as PDF keeps them, #4734); previously trivial (a document with links prints: the print job's PDF has no link annotations, #4734; previously trivial: Pen / stylus row: two-stroke and closed-head arrows; previously trivial: Pen / stylus row: ink, eraser and Ink to Shape incl. arrows landed; previously trivial: a window too big for its screen shrinks to fit when it opens, #1215; previously trivial: 32-bit Windows starts on OpenGL; previously major: first version) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 and Word for Windows > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -28,6 +28,10 @@ File > Print offers 2, 4, 6, 9 or 16 logical pages per physical sheet side and l | Date | Change | Summary | |---|---|---| +| 2026-11-06 | trivial | Print helpers remove loaded PDF/settings files before driver work, tolerate prior removal and report cleanup errors; a held-driver regression checks the files are already absent (#5710) | +| 2026-11-06 | trivial | Windows print submissions use a separate helper per job, preserving provider serialization inside the helper. An acknowledged cancellation stops that helper, so a stalled cancelled driver cannot block a later job; normal shutdown reaps helpers and removes temporary PDFs. The helper erases its plaintext inputs before driver work; a hard app crash during startup can still leave private temporary files. Startup requires an explicit start grant; after sending begins, jobs complete (#5710) | +| 2026-11-06 | trivial | File › Print: before sending starts, Cancel printing (`ui.printCancel`) stops a waiting job and frees the Print page while keeping its settings, including after the last document closes; a successfully cancelled job is never sent. Quitting cancels waiting jobs and joins jobs already sending (#5710). On Windows, winprint repeats the DEVMODE conversion after sending starts. A stall during that repeat prevents cancellation and can keep shutdown waiting, even before a native job exists. | +| 2026-11-06 | trivial | On Windows the Print panel lists printers without asking their drivers anything, removing the driver query a networked printer in power save could hold, which kept the list empty with Refresh unavailable (#5710; likely also behind the long "Finding printers" waits of #5645 and #5413): discovery uses craft-print's `printer_queues` (storytold/craft-libs#618), and printing still checks the job's options with the driver; physical sleeping-printer output remains to be confirmed by the reporter | | 2026-10-28 | trivial | Ink page lookup work, #3614. Drawing order, geometry and saved data retain their behavior; no readiness estimate change | | 2026-11-01 | trivial | N-up and booklet print imposition (#641): integrate current print-job link handling (#4734), preserve markup preview and sheet/copy planning; readiness estimates unchanged. | | 2026-10-30 | trivial | Only the PDF sent to the printer leaves out link annotations; Save these pages as PDF (`file.printPdf`) keeps them (#4734) |