mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-06 18:51:26 +00:00
Gap 19: ODT default font size; dates in the locale's order
- The document's default font size (`fo:font-size` in `style:default-style`, or the Standard style's: the document's defaults) was ignored, leaving 12 pt; LibreOffice's letters are 10 pt. It is now the document's default. A named style with no parent keeps the default style's size, as ODF inherits it. - A date style ordered by the locale (`number:automatic-order`) showed its parts as listed: "2026November2". It now shows in US English order: "November 2, 2026", or "11/2/2026" with a numeric month, as LibreOffice shows it here. WordCraft's own rendering of LibreOffice's letter templates comes much closer: one letter goes from 11,377 to 6,237 differing pixels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This commit is contained in:
6 files changed
+137
-3
No files matched your search
@@ -182,3 +182,43 @@ pub(super) fn picture_of(instr: &str) -> Option<String> {
|
||||
};
|
||||
(!pic.is_empty()).then(|| pic.chars().take(MAX_PICTURE).collect())
|
||||
}
|
||||
|
||||
/// A date's parts in the order and with the separators the locale gives them, for a date style
|
||||
/// with `number:automatic-order` (§19.341): its weekday, month, day and year as US English
|
||||
/// orders them (LibreOffice's "November 2, 2026", or "11/2/2026" with a numeric month). Its own
|
||||
/// text between them is left out; a time's parts stay as they are.
|
||||
pub(super) fn auto_ordered(parts: &[Part]) -> Vec<Part> {
|
||||
let find = |f: fn(&Part) -> bool| parts.iter().find(|p| f(p)).cloned();
|
||||
let weekday = find(|p| matches!(p, Part::Weekday(_)));
|
||||
let month = find(|p| matches!(p, Part::Month(..)));
|
||||
let day = find(|p| matches!(p, Part::Day(_)));
|
||||
let year = find(|p| matches!(p, Part::Year(_)));
|
||||
let Some(month) = month else { return parts.to_vec() };
|
||||
let textual = matches!(month, Part::Month(_, true));
|
||||
let text = |s: &str| Part::Text(s.to_string());
|
||||
let mut out = Vec::new();
|
||||
if let Some(w) = weekday {
|
||||
out.extend([w, text(", ")]);
|
||||
}
|
||||
out.push(month);
|
||||
if let Some(d) = day {
|
||||
out.extend([text(if textual { " " } else { "/" }), d]);
|
||||
}
|
||||
if let Some(y) = year {
|
||||
out.extend([text(if textual { ", " } else { "/" }), y]);
|
||||
}
|
||||
// A time with it keeps its own parts, after.
|
||||
let time: Vec<Part> = parts.iter().filter(|p| matches!(p, Part::Hours(_) | Part::Minutes(_) | Part::Seconds(_) | Part::AmPm)).cloned().collect();
|
||||
if !time.is_empty() {
|
||||
out.push(text(" "));
|
||||
for (i, p) in time.into_iter().enumerate() {
|
||||
if i > 0 && !matches!(p, Part::AmPm) {
|
||||
out.push(text(":"));
|
||||
} else if matches!(p, Part::AmPm) {
|
||||
out.push(text(" "));
|
||||
}
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -395,6 +395,8 @@ struct Parsed {
|
||||
track: Option<bool>,
|
||||
/// The document's default font.
|
||||
font: Option<String>,
|
||||
/// The document's default font size, points.
|
||||
size: Option<f32>,
|
||||
/// The custom paragraph styles the paragraphs use, parents included, parents first.
|
||||
named: Vec<super::styles::NamedStyle>,
|
||||
/// Their ODF names, alongside.
|
||||
@@ -552,6 +554,7 @@ fn parse_tracked(bytes: &[u8], paper: (f32, f32)) -> Result<Parsed, String> {
|
||||
last,
|
||||
track,
|
||||
font: styles.doc_font(),
|
||||
size: styles.doc_size(),
|
||||
spacing: styles.doc_spacing(),
|
||||
looks: styles.kind_looks(),
|
||||
named,
|
||||
@@ -589,6 +592,7 @@ pub fn import_on(bytes: &[u8], page: &SectionProps) -> Result<Document, String>
|
||||
last,
|
||||
track,
|
||||
font,
|
||||
size,
|
||||
spacing,
|
||||
looks,
|
||||
named,
|
||||
@@ -622,6 +626,9 @@ pub fn import_on(bytes: &[u8], page: &SectionProps) -> Result<Document, String>
|
||||
if font.is_some() {
|
||||
doc.styles.default_chr.font = font;
|
||||
}
|
||||
if size.is_some() {
|
||||
doc.styles.default_chr.size = size;
|
||||
}
|
||||
doc.styles.default_chr.kern = kern.then_some(1.0);
|
||||
doc.styles.default_chr.overlay(&scripts);
|
||||
if let Some(t) = default_tab {
|
||||
|
||||
@@ -443,6 +443,8 @@ pub(super) struct NamedStyle {
|
||||
struct DateStyles {
|
||||
/// The style being read: its name and parts.
|
||||
data: Option<(String, Vec<super::datastyle::Part>)>,
|
||||
/// It orders its parts as the locale does (§19.341 `number:automatic-order`).
|
||||
auto_order: bool,
|
||||
/// In its `number:text`.
|
||||
in_text: bool,
|
||||
}
|
||||
@@ -454,7 +456,10 @@ impl DateStyles {
|
||||
use super::datastyle::{Part, part, picture};
|
||||
match ev {
|
||||
Ev::Start(q, a) => match local(q) {
|
||||
"date-style" | "time-style" => self.data = get(a, "style:name").map(|n| (n.to_string(), Vec::new())),
|
||||
"date-style" | "time-style" => {
|
||||
self.data = get(a, "style:name").map(|n| (n.to_string(), Vec::new()));
|
||||
self.auto_order = local(q) == "date-style" && get(a, "number:automatic-order").is_some_and(|v| v.trim() == "true");
|
||||
}
|
||||
"text" => self.in_text = self.data.is_some(),
|
||||
name => {
|
||||
let on = |k: &str, v: &str| get(a, k).is_some_and(|x| x.trim() == v);
|
||||
@@ -471,6 +476,7 @@ impl DateStyles {
|
||||
if let Some((name, parts)) = self.data.take()
|
||||
&& out.len() < MAX_DATA_STYLES
|
||||
{
|
||||
let parts = if std::mem::take(&mut self.auto_order) { super::datastyle::auto_ordered(&parts) } else { parts };
|
||||
out.insert(name, picture(&parts));
|
||||
}
|
||||
}
|
||||
@@ -920,6 +926,8 @@ pub(super) struct Styles {
|
||||
pub(super) lists: HashMap<String, Vec<ListLevel>>,
|
||||
/// Font of `style:default-style` for paragraphs (the document default, ODF 1.3 §16.4).
|
||||
default_font: Option<String>,
|
||||
/// The default paragraph style's font size (`fo:font-size` in `style:default-style`), points.
|
||||
default_size: Option<f32>,
|
||||
/// Spacing of `style:default-style` for paragraphs.
|
||||
default_spacing: Spacing,
|
||||
/// Tab stops are measured from the paragraph's indent (LibreOffice's `TabsRelativeToIndent`
|
||||
@@ -978,6 +986,9 @@ impl Styles {
|
||||
"graphic-properties" if in_graphic_default && cur.is_none() => self.default_graphic = super::frames::Graphic::read(&a),
|
||||
"text-properties" if in_default && cur.is_none() => {
|
||||
self.default_font = default_font(&a, &self.faces);
|
||||
if let Some(sz) = get(&a, "fo:font-size").and_then(length).filter(|v| v.is_finite() && *v >= 1.0) {
|
||||
self.default_size = Some(sz.min(1638.0));
|
||||
}
|
||||
self.default_kern = get(&a, "style:letter-kerning").is_some_and(|k| k.trim() == "true");
|
||||
script_properties(&a, &mut self.default_scripts, &self.faces);
|
||||
}
|
||||
@@ -1058,6 +1069,18 @@ impl Styles {
|
||||
}
|
||||
|
||||
/// The document's default font: the Standard paragraph style's, else the default style's.
|
||||
/// The document's default font size: the Standard paragraph style's (the document's
|
||||
/// defaults, as for [`Styles::doc_font`]), else the default style's.
|
||||
pub(super) fn doc_size(&self) -> Option<f32> {
|
||||
self.map
|
||||
.get("Standard")
|
||||
.filter(|s| !s.auto)
|
||||
.and_then(|s| s.fmt.size)
|
||||
.or(self.default_size)
|
||||
.filter(|v| v.is_finite() && *v >= 1.0)
|
||||
.map(|v| v.min(1638.0))
|
||||
}
|
||||
|
||||
pub(super) fn doc_font(&self) -> Option<String> {
|
||||
self.map.get("Standard").filter(|s| !s.auto).and_then(|s| s.fmt.font.as_deref().map(str::to_string)).or_else(|| self.default_font.clone())
|
||||
}
|
||||
@@ -1302,6 +1325,11 @@ impl Styles {
|
||||
};
|
||||
let mut fmt = Fmt::default();
|
||||
s.fmt.apply(&mut fmt);
|
||||
// A style with no parent inherits the default style, not Standard (the document's
|
||||
// defaults here): its size, when Standard's differs.
|
||||
if s.parent.is_none() && fmt.size.is_none() && self.doc_size() != self.default_size {
|
||||
fmt.size = self.default_size;
|
||||
}
|
||||
let look = ParaLook {
|
||||
align: s.align,
|
||||
keep_next: s.keep_next == Some(true),
|
||||
|
||||
@@ -1159,3 +1159,46 @@ pub(super) fn styles_of(bytes: &[u8]) -> String {
|
||||
let mut z = archive(bytes);
|
||||
String::from_utf8(read_entry(&mut z, "styles.xml").unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_default_styles_font_size_is_the_documents() {
|
||||
// LibreOffice's letter templates: 10 pt in `style:default-style`; the Standard style's own
|
||||
// size (the document's defaults) wins over it.
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:body><office:text><text:p>Hi</text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let default_only = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:default-style style:family="paragraph"><style:text-properties fo:font-size="10pt"/></style:default-style></office:styles></office:document-styles>"#
|
||||
);
|
||||
let doc = import(&package(&content, Some(&default_only))).unwrap();
|
||||
assert_eq!(doc.styles.default_chr.size, Some(10.0));
|
||||
let p = doc.body[0].as_para().unwrap();
|
||||
assert_eq!(doc.styles.resolve_char(p.props.style.as_deref(), &p.runs[0].props).size, 10.0);
|
||||
let standard = default_only.replace(
|
||||
"</office:styles>",
|
||||
r#"<style:style style:name="Standard" style:family="paragraph"><style:text-properties fo:font-size="11pt"/></style:style></office:styles>"#,
|
||||
);
|
||||
let d = import(&package(&content, Some(&standard))).unwrap();
|
||||
assert_eq!(d.styles.default_chr.size, Some(11.0));
|
||||
let p = d.body[0].as_para().unwrap();
|
||||
assert_eq!(d.styles.resolve_char(p.props.style.as_deref(), &p.runs[0].props).size, 11.0);
|
||||
// Written back as the default style's.
|
||||
let s = entry(&export(&doc).unwrap(), "styles.xml");
|
||||
assert!(s.contains(r#"fo:font-size="10pt""#), "{s}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_style_with_no_parent_takes_the_default_styles_size() {
|
||||
// The default style says 12 pt, Standard 11 pt (the document's defaults); a style with no
|
||||
// parent inherits the default style's 12 pt, as LibreOffice shows it.
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:body><office:text><text:p text:style-name="Loose">Hi</text:p><text:p>Plain</text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:default-style style:family="paragraph"><style:text-properties fo:font-size="12pt"/></style:default-style><style:style style:name="Standard" style:family="paragraph"><style:text-properties fo:font-size="11pt"/></style:style><style:style style:name="Loose" style:family="paragraph"/></office:styles></office:document-styles>"#
|
||||
);
|
||||
let d = import(&package(&content, Some(&styles))).unwrap();
|
||||
let sizes: Vec<f32> =
|
||||
d.body.iter().filter_map(|b| b.as_para()).map(|p| d.styles.resolve_char(p.props.style.as_deref(), &p.runs[0].props).size).collect();
|
||||
assert_eq!(sizes, [12.0, 11.0]);
|
||||
}
|
||||
@@ -284,3 +284,18 @@ fn hostile_variables_never_panic() {
|
||||
let _ = export(&d).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_date_in_the_locales_order_shows_so() {
|
||||
// LibreOffice's letters: year, month by name and day, `number:automatic-order`: shown in
|
||||
// the locale's order, "November 2, 2026" in US English.
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T} xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"><office:styles><number:date-style style:name="N112" number:automatic-order="true"><number:year number:style="long"/><number:month number:style="long" number:textual="true"/><number:day/></number:date-style><number:date-style style:name="N2" number:automatic-order="true"><number:day/><number:month/><number:year number:style="long"/></number:date-style></office:styles></office:document-styles>"#
|
||||
);
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:body><office:text><text:p><text:date style:data-style-name="N112" text:date-value="2026-11-02">2026November2</text:date> <text:date style:data-style-name="N2" text:date-value="2026-11-02">2112026</text:date></text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let d = import(&package(&content, Some(&styles))).unwrap();
|
||||
let codes: Vec<String> = fields(&d).into_iter().map(|(c, _)| c).collect();
|
||||
assert_eq!(codes, [r#"DATE \@ "MMMM d, yyyy""#, r#"DATE \@ "M/d/yyyy""#]);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-02 · **Change:** trivial (ODT page background pictures); previously trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODT macros and unknown package parts kept); previously trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously trivial (ODT picture crops and contour wrapping); previously trivial (ODT fidelity re-estimated at ~85%); previously trivial (ODT custom document properties and user fields as DOCPROPERTY); previously trivial (ODT connectors read and written glued); previously trivial (ODT borders around text read and written); previously trivial (ODT index marks read and written as XE fields; an index held around its entries updates); previously trivial (ODT ruby read and written); previously trivial (ODT page breaks after a paragraph, kept lines, widow control, hyphenation, table alignment and width and row heights); previously trivial (in-line drawings raised or lowered by their run's position; ODT in-line frames keep their place on the baseline; leading's space after the last line; text box borders shown by LibreOffice); previously trivial (ODT form controls opened and saved as content controls); previously trivial (password-protected ODT opened and saved); previously trivial (ODT headers and footers keep their shapes, text boxes, charts and embedded objects on save); previously trivial (ODT embedded spreadsheets and OLE objects kept whole instead of lost; flat ODT sub-documents valid `office:document`s); previously trivial (ODT charts in drawing groups drawn on open); previously trivial (ODT charts opened and saved as editable charts; flat ODT sub-documents no longer end the body); previously trivial (ODT leading read as an at-least spacing); previously trivial (ODT tracked formatting changes; changed regions named by `xml:id`); previously trivial (ODT drop caps; tab stops no longer drop a paragraph's background); previously trivial (flat ODT `.fodt` opened and saved; ODT fidelity re-estimated at ~80%); previously trivial (ODT page settings; a footnote no longer takes a footer's text); previously trivial (ODT dates in their formats, sequences, chapters and references); previously trivial (ODT bookmarks over text); previously trivial (ODT tables of contents and indexes; ODT fidelity re-estimated); previously trivial (ODT polygons, polylines and paths); previously trivial (ODT arrowheads, dashes and gradients); previously trivial (ODT groups of shapes and pictures); previously trivial (ODT rotated and mirrored shapes and pictures); previously trivial (ODT drawing shapes read and written); previously trivial (ODT text box frames keep their line, fill, padding and fit); previously trivial (ODT Asian and complex-script fonts); previously trivial (ODT right-to-left paragraphs); previously trivial (ODT text boxes kept); previously trivial (ODT unnumbered headings detached from the outline); previously trivial (ODT contextual spacing and layout settings); previously trivial (ODT custom character styles kept); previously trivial (ODT list level positions); previously trivial (ODT custom paragraph styles kept); previously trivial (ODT equations written as formula objects); previously trivial (ODT numbered headings written as headings); previously trivial (ODT floating pictures); previously trivial (ODT paragraph borders and background); previously trivial (ODT heading numbering); previously trivial (ODT font-face names); previously trivial (ODT table cell borders and padding); previously trivial (ODT heading, title, quote and preformatted styles keep their look); previously trivial (ODT tab stops read and written); previously trivial (ODT character formatting read and written); previously trivial (ODT lists keep their number formats, bullets and identity); previously trivial (ODT comments reLine truncated
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-02 · **Change:** trivial (ODT default font size; locale-ordered dates); previously trivial (ODT page background pictures); previously trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODT macros and unknown package parts kept); previously trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously trivial (ODT picture crops and contour wrapping); previously trivial (ODT fidelity re-estimated at ~85%); previously trivial (ODT custom document properties and user fields as DOCPROPERTY); previously trivial (ODT connectors read and written glued); previously trivial (ODT borders around text read and written); previously trivial (ODT index marks read and written as XE fields; an index held around its entries updates); previously trivial (ODT ruby read and written); previously trivial (ODT page breaks after a paragraph, kept lines, widow control, hyphenation, table alignment and width and row heights); previously trivial (in-line drawings raised or lowered by their run's position; ODT in-line frames keep their place on the baseline; leading's space after the last line; text box borders shown by LibreOffice); previously trivial (ODT form controls opened and saved as content controls); previously trivial (password-protected ODT opened and saved); previously trivial (ODT headers and footers keep their shapes, text boxes, charts and embedded objects on save); previously trivial (ODT embedded spreadsheets and OLE objects kept whole instead of lost; flat ODT sub-documents valid `office:document`s); previously trivial (ODT charts in drawing groups drawn on open); previously trivial (ODT charts opened and saved as editable charts; flat ODT sub-documents no longer end the body); previously trivial (ODT leading read as an at-least spacing); previously trivial (ODT tracked formatting changes; changed regions named by `xml:id`); previously trivial (ODT drop caps; tab stops no longer drop a paragraph's background); previously trivial (flat ODT `.fodt` opened and saved; ODT fidelity re-estimated at ~80%); previously trivial (ODT page settings; a footnote no longer takes a footer's text); previously trivial (ODT dates in their formats, sequences, chapters and references); previously trivial (ODT bookmarks over text); previously trivial (ODT tables of contents and indexes; ODT fidelity re-estimated); previously trivial (ODT polygons, polylines and paths); previously trivial (ODT arrowheads, dashes and gradients); previously trivial (ODT groups of shapes and pictures); previously trivial (ODT rotated and mirrored shapes and pictures); previously trivial (ODT drawing shapes read and written); previously trivial (ODT text box frames keep their line, fill, padding and fit); previously trivial (ODT Asian and complex-script fonts); previously trivial (ODT right-to-left paragraphs); previously trivial (ODT text boxes kept); previously trivial (ODT unnumbered headings detached from the outline); previously trivial (ODT contextual spacing and layout settings); previously trivial (ODT custom character styles kept); previously trivial (ODT list level positions); previously trivial (ODT custom paragraph styles kept); previously trivial (ODT equations written as formula objects); previously trivial (ODT numbered headings written as headings); previously trivial (ODT floating pictures); previously trivial (ODT paragraph borders and background); previously trivial (ODT heading numbering); previously trivial (ODT font-face names); previously trivial (ODT table cell borders and padding); previously trivial (ODT heading, title, quote and preformatted styles keep their look); previously trivial (ODT tab stops read and written); previously trivial (ODT character formatting read and written); previously trivial (ODT lists keep their number fLine truncated
|
||||
|
||||
Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the
|
||||
share of a typical real-world file's content and formatting that survives (read: shows correctly;
|
||||
@@ -57,7 +57,7 @@ Weights are the share of a Word user's file traffic (estimate). Read/write: ✅
|
||||
| PDF export | 8% | W | — | ✅ | ~80% | 6–10 | `crates/pdf/src/tests.rs` | krilla: embedded subsets, selectable text incl. field results (#133); Hebrew and Arabic copy and search as whole words (right-to-left glyphs written in visual order, #1383, also with fonts that draw lam-alef as two glyphs at one x, #2310); All Caps and Small Caps text extracts as the capitals shown, as from Word's PDFs, and never changes the case of other text (#1473), links, outline, tagging. Outline destinations follow leading page breaks to the heading text. Fonts it can't subset become outlines (#132). Print Markup (#589): comments in balloons beside each page (scaled to its paper) and tracked changes; HTML/Markdown/text copies end with a comment list. Horizontal and vertical lines inside groups are drawn (#1474). No PDF/A option, no "best for printing/online" choice |
|
||||
| PDF open (PDF Reflow, Word for Windows) | 1% | R | ❌ | — | 0% | 30–50 | — | Converting a PDF into an editable document. A sibling app (PdfCraft) may supply the parser |
|
||||
| `.rtf` Rich Text Format | 3% | R/W | 🟡 | 🟡 | ~50% | 10–15 | `crates/formats/src/tests.rs` | ~1,500 lines: text, character/paragraph formatting, styles, lists, tables, pictures (`\shppict`, #154; Windows and Enhanced Metafiles, `\wmetafile` and `\emfblip`, read and written, #4259), fields, bookmarks. 8-bit text in the document's code page (`\ansicpg`) or the current font's (`\fcharset`, `\cpg`): Cyrillic, Central European, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese, Thai (was Windows-1252 only), Mac OS Roman (`\mac`, `\fcharset77`) and DOS code pages (`\pc` 437, `\pca` 850, `\cpg` 437/737/850/852/857/866, `\fcharset254`) (#1653). East Asian multibyte code pages decoded as runs: 932 Shift-JIS (`\fcharset128`, Japanese), 936 GBK (134, Simplified Chinese), 949 UHC/windows-949 (129, Korean) and 950 Big5 (136, Traditional Chinese) through `\ansicpg`, `\cpg` or the font's charset, escaped or raw, a lead byte and its trail byte together; invalid sequences read as U+FFFD, and a run that starts `EF BB BF` is text, not a BOM (#1653). Pasted from the system clipboard where the platform offers it (Windows, Wayland) when there is no HTML (#687). Tracked insertions and deletions read and written (`\revtbl`, `\revised`/`\deleted`, authors, `\revdttm` dates), so Accept/Reject All work after opening or a round trip; tracked paragraph marks (`\par` inside `\revised`/`\deleted` groups, including a `\sect` inside a deletion) and character and paragraph formatting revisions (`\crauth`/`\crdate` with `\oldcprops`, `\prauth`/`\prdate` with `\oldpprops`) read and written; section and table formatting revisions (`\oldsprops`, `\oldtprops`) not yet. Sections on import (#1378): each `\sect` ends a section with its own page size and orientation, margins, header/footer distance, columns, break type, page-number restart, start and format, title page and headers/footers (those it leaves out link to the previous); export writes every section too (break type, page size and orientation, margins, header/footer distance, columns, page numbering, title page; not headers/footers). Thin: comments, notes, RTL |
|
||||
| `.odt` OpenDocument Text (and flat `.fodt`, templates `.ott`) | 2% | R/W | 🟡 | 🟡 | ~90% | 20–35 | `crates/formats/src/tests.rs`, `crates/formats/src/odt/tests.rs` and one file per feature in `crates/formats/src/odt/tests_more/`, `crates/engine/tests/it/odt_sections.rs`, `odt_toc.rs`, `odt_fields.rs`, `odt_charts.rs`, `odt_objects.rs`, `odt_password.rs`, `odt_forms.rs`; checked against LibreOffice's own documents (round trips compared page by page) and the ODF 1.3 schema | Ruby (`text:ruby`, §6.4: a base text with ruby text over it) is read as WordCraft's Phonetic Guide and written back as ODF ruby above its base, centred (one inside a tracked change is written as its base text, which the change marks); its own position and alignment aren't kept. A border around text (LibreOffice's `loext:border` or its sides, with `loext:padding`: ODF 1.3 has no attribute for it) is Word's text border, one line all round, and is written back so (a document with one is then extended ODF, not strict). Emphasis marks (`style:text-emphasize`) and rotated text (`style:text-rotation-angle`) aren't read. How text flows: a page break after a paragraph (`fo:break-after`, a page break before the next paragraph as Word has it), lines kept together (`fo:keep-together`), widow and orphan control (`fo:widows`/`fo:orphans`: on with any line count, off with none; the counts themselves aren't kept) and hyphenation (`fo:hyphenate`, a paragraph's in ODF and the document's in WordCraft: a document with a hyphenated paragraph hyphenates, its other paragraphs opting out) are read and written; a table's alignment and width (`table:align`, `style:rel-width`, or `style:width` for an aligned table) and its rows' heights (`style:row-height` exact, `style:min-row-height` at least) are read and written. Form controls (`office:forms` controls placed by `draw:control`, ODF 1.3 §13) open as content controls — text fields and text areas as plain text controls, check boxes as check boxes, radio buttons as check boxes with round symbols, list boxes as drop-down lists, combo boxes as combo boxes, date fields as date controls, each with its value, entries and selection, its name as the control's tag and a label as its title — and content controls of those kinds in the body are written back as form controls LibreOffice shows with their values (in headers and footers, as their text); a control keeps its size (`draw:control` `svg:width`/`svg:height`); buttons, image buttons, labels and the other controls WordCraft has no kind for show their label, locked, and are kept verbatim and written back as they were (one naming a namespace WordCraft doesn't declare is left out); hidden controls (`form:hidden`) are kept and written back with their form; a control's look (colours, font) isn't kept. Placeholders (`text:placeholder`, §7.7.2, as LibreOffice's templates use: "<Recipient's name>") open as plain text content controls showing them, so a click selects one and typing replaces it, and are written back as placeholders of their type (their description as the control's title) while they show it. A master page followed by another (`style:next-style-name`: LibreOffice's "First Page" before "Default") is a section with a different first page, its own headers and footers on the first page and the next one's after it. A page background picture (`style:background-image` in the page layout, §17.3) is WordCraft's page picture (Design › Page Color › Fill Effects): repeated as tiled, stretched as stretched, not repeated as centred at its own size (a fill WordCraft draws so; DOCX writes it stretched), and is written back with its picture. Tables are fixed layout (Word's `w:tblLayout`), as LibreOffice's columns never fit their content, and a table starting a page (`fo:break-before` on the table) starts it: its first paragraph has a page break before it, which moves the table, and is written back on the table. The columns of a table filling the margins are written with their share of the table (`style:rel-column-width`) too, so on a narrower page they keep their proportions in LibreOffice (an aligned table's aren't: LibreOffice lays one out oddly with shares). A fixed date or time with neither a value nor text (LibreOffice's templates) is a date field showing the day; `text:author-name` is `USERNAME`, which WordCraft now fills with the user's name. A page layout that states no size is the default page's (the user's region's, as LibreOffice uses its default paper), and a margin it leaves unsaid is LibreOffice's 2 cm (a layout with no properties at all included); Emphasis marks (`style:text-emphasize`, §20.370) open as Word's (dot, accent as comma, circle, disc as dot; below as dot below) and are written back. Variables (`text:variable-set`, `text:variable-get`, §7.4) open as Word's SET and REF fields (a displayed set as SET then REF), which Update Field works out as LibreOffice shows them, and are written back declared (`text:variable-decls`), sets hidden withLine truncated
|
||||
| `.odt` OpenDocument Text (and flat `.fodt`, templates `.ott`) | 2% | R/W | 🟡 | 🟡 | ~90% | 20–35 | `crates/formats/src/tests.rs`, `crates/formats/src/odt/tests.rs` and one file per feature in `crates/formats/src/odt/tests_more/`, `crates/engine/tests/it/odt_sections.rs`, `odt_toc.rs`, `odt_fields.rs`, `odt_charts.rs`, `odt_objects.rs`, `odt_password.rs`, `odt_forms.rs`; checked against LibreOffice's own documents (round trips compared page by page) and the ODF 1.3 schema | Ruby (`text:ruby`, §6.4: a base text with ruby text over it) is read as WordCraft's Phonetic Guide and written back as ODF ruby above its base, centred (one inside a tracked change is written as its base text, which the change marks); its own position and alignment aren't kept. A border around text (LibreOffice's `loext:border` or its sides, with `loext:padding`: ODF 1.3 has no attribute for it) is Word's text border, one line all round, and is written back so (a document with one is then extended ODF, not strict). Emphasis marks (`style:text-emphasize`) and rotated text (`style:text-rotation-angle`) aren't read. How text flows: a page break after a paragraph (`fo:break-after`, a page break before the next paragraph as Word has it), lines kept together (`fo:keep-together`), widow and orphan control (`fo:widows`/`fo:orphans`: on with any line count, off with none; the counts themselves aren't kept) and hyphenation (`fo:hyphenate`, a paragraph's in ODF and the document's in WordCraft: a document with a hyphenated paragraph hyphenates, its other paragraphs opting out) are read and written; a table's alignment and width (`table:align`, `style:rel-width`, or `style:width` for an aligned table) and its rows' heights (`style:row-height` exact, `style:min-row-height` at least) are read and written. Form controls (`office:forms` controls placed by `draw:control`, ODF 1.3 §13) open as content controls — text fields and text areas as plain text controls, check boxes as check boxes, radio buttons as check boxes with round symbols, list boxes as drop-down lists, combo boxes as combo boxes, date fields as date controls, each with its value, entries and selection, its name as the control's tag and a label as its title — and content controls of those kinds in the body are written back as form controls LibreOffice shows with their values (in headers and footers, as their text); a control keeps its size (`draw:control` `svg:width`/`svg:height`); buttons, image buttons, labels and the other controls WordCraft has no kind for show their label, locked, and are kept verbatim and written back as they were (one naming a namespace WordCraft doesn't declare is left out); hidden controls (`form:hidden`) are kept and written back with their form; a control's look (colours, font) isn't kept. Placeholders (`text:placeholder`, §7.7.2, as LibreOffice's templates use: "<Recipient's name>") open as plain text content controls showing them, so a click selects one and typing replaces it, and are written back as placeholders of their type (their description as the control's title) while they show it. A master page followed by another (`style:next-style-name`: LibreOffice's "First Page" before "Default") is a section with a different first page, its own headers and footers on the first page and the next one's after it. The default paragraph style's font size (`fo:font-size` in `style:default-style`, or the Standard style's: the document's defaults) is the document's default size (it stayed 12 pt), a style with no parent keeping the default style's, and a date style ordered by the locale (`number:automatic-order`) shows in US English order ("November 2, 2026", as LibreOffice shows it here). A page background picture (`style:background-image` in the page layout, §17.3) is WordCraft's page picture (Design › Page Color › Fill Effects): repeated as tiled, stretched as stretched, not repeated as centred at its own size (a fill WordCraft draws so; DOCX writes it stretched), and is written back with its picture. Tables are fixed layout (Word's `w:tblLayout`), as LibreOffice's columns never fit their content, and a table starting a page (`fo:break-before` on the table) starts it: its first paragraph has a page break before it, which moves the table, and is written back on the table. The columns of a table filling the margins are written with their share of the table (`style:rel-column-width`) too, so on a narrower page they keep their proportions in LibreOffice (an aligned table's aren't: LibreOffice lays one out oddly with shares). A fixed date or time with neither a value nor text (LibreOffice's templates) is a date field showing the day; `text:author-name` is `USERNAME`, which WordCraft now fills with the user's name. A page layout that states no size is the default page's (the user's region's, as LibreOffice uses its default paper), and a margin it leaves unsaid is LibreOffice's 2 cm (a layout with no properties at all included); Emphasis marks (`style:text-eLine truncated
|
||||
| `.txt` Plain text | 2% | R/W | ✅ | ✅ | ~95% | 1–2 | unit tests | Encoding detection. Files with other extensions (`.m3u`/`.m3u8` playlists, logs, config files, no extension) open as text when they are text, Word and RTF files by their signature; binary files are refused (#569). A Word or RTF extension that contradicts the content also opens by signature, as in Word: RTF saved as `.doc` (hh.ru résumés, accounting exports), a `.docx` renamed `.doc`, a Word 97-2003 file renamed `.docx`; a Word XML Document (Word 2003 XML or Flat OPC) or a web page (`<html>`, `<!DOCTYPE html>`) under a `.doc`, `.docx` or `.rtf` name opens as what it is (#2853, `tests::word_xml_and_web_pages_saved_as_doc_open_by_content`). Word's encoding choice dialog and line-ending options missing; such files save only through Save As |
|
||||
| `.htm/.html` Web Page and Web Page, Filtered | 1% | R/W | 🟡 | 🟡 | ~60% | 6–10 | `crates/formats/src/tests.rs` | Tables (#102), relative pictures embedded (#113), missing `</head>` tolerated (#154); the empty comments `<!-->` and `<!--->` end at once instead of swallowing the rest of the page (#5040). Header cells (`<th>`) bold unless their CSS says otherwise; `text-align` on a cell, or inherited from its row, row group or table, aligns the cell's paragraphs (#1381). HTML gives no page size, so it opens on the region's paper (A4 or Letter), as Markdown and plain text do; `@page` isn't read (#1381). No Word "Web Page" (with `_files` folder) round-trip. The same reader takes HTML from the system clipboard: Paste keeps another program's formatting (a Word document, a web page; Windows' "HTML Format" header understood), Merge Formatting keeps its emphasis (#687). A pasted `<blockquote>` (LibreOffice's paragraph indented on both sides) becomes a Normal paragraph indented by the page's `blockquote` margins, else 0.5 in each side; opening an .html file keeps it as the Quote style (#1270). Equations are written as MathML (with the linear format as a UnicodeMath annotation) and read back from MathML: our annotation, else the presentation elements, else a TeX annotation (#816). A copy of one equation alone also goes in the clipboard's MathML formats (Windows "MathML" and "MathML Presentation", macOS `public.mathml.presentation` and `public.mathml`; W3C MathML 3 §6.2.4), which Microsoft documents Word reading first when it pastes math (#1297; the paste into Word is not yet verified); Ruby (furigana, phonetic guides) is written as `<ruby>base<rt>reading</rt></ruby>` in the base text's formatting and read back from `<ruby>`/`<rb>`/`<rt>` (`<rp>` fallback parentheses dropped; malformed markup keeps its text), so docx/doc → HTML → doc keeps the annotation; HTML states no alignment, size or raise, so the Phonetic Guide's defaults apply (#1681) CRLF and lone CR input newlines normalize before tokenization, preserving preformatted lines and leaving decoded character references separate (#4810). |
|
||||
| `.mht/.mhtml` Single File Web Page | <1% | R/W | ❌ | ❌ | 0% | 3–5 | — | |
|
||||
@@ -280,6 +280,7 @@ Custom document properties retain their bookmark link targets through DOCX saves
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | trivial | ODT: the document's default font size (the default paragraph style's, 10 pt in LibreOffice's letters) was ignored, leaving 12 pt; it is the document's default. A date style ordered by the locale showed "2026November2"; it shows "November 2, 2026" (`odt::tests::the_default_styles_font_size_is_the_documents`, `fields::a_date_in_the_locales_order_shows_so`) |
|
||||
| 2026-11-02 | trivial | ODT: page background pictures (LibreOffice's report templates' world map) were dropped; they are page pictures, centred ones drawn at their own size in the middle of the page (on screen and in PDF), and written back (`page_setup::a_page_background_picture_is_read_and_written`, `render::tests::page_picture_is_painted_behind_the_text`) |
|
||||
| 2026-11-02 | trivial | ODT: a table starting a page (LibreOffice's agenda minutes) didn't; it does, in WordCraft and after a save, and LibreOffice's 6orange agenda round-trips with its text the same. ODT tables are fixed layout, as LibreOffice's columns never fit their content (`flow_breaks::a_table_starting_a_page_starts_it`, `a_tables_columns_keep_their_widths`) |
|
||||
| 2026-11-02 | trivial | Emphasis marks (Font › Emphasis mark) weren't modelled: DOCX `w:em` and ODT `style:text-emphasize` were dropped. They are read, drawn over (or under) each character with the line made taller for them, set by `format.emphasisMark`, and written back in both formats (`docx/tests/it/emphasis_marks.rs`, `odt::tests_more::chars::emphasis_marks_are_read_and_written`, `engine::tests::emphasis_marks_are_set_and_drawn`) |
|
||||
|
||||
Reference in new issue
Block a user