mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-06 18:51:26 +00:00
Gap 19: ODT letters keep one page: default size, page frames, box styles, empty lines
Saving LibreOffice's letter and fax templates added a page and moved
their logos. Fixes:
- Default font size: the writer always wrote the default style's size as
12 pt. It now writes Normal's size (11 pt in these letters).
- Page-anchored frames: a frame placed "from the paragraph" was moved by
the page margin. A page has no paragraph, so it is now placed from the
page, and "from the paragraph's content" from the page's text area, as
LibreOffice places it.
- Text box styles: a custom style used only by paragraphs in text boxes
(the fax's centred "Frame contents") was dropped. The style pass now
walks text boxes and groups.
- Empty paragraphs: an empty paragraph's own font size (its height) was
dropped. It is now kept as the paragraph mark's formatting, a new
field in the shared flow model that the ODT reader and writer use.
The marine, fax and bottle letters keep one page after a save. Over the
125-document corpus the round-trip pixel difference falls from 408k to
333k, and the business letters' differences roughly halve.
Also moved a doc comment that 48c60a5 left on the wrong function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This commit is contained in:
13 files changed
+138
-14
No files matched your search
@@ -421,6 +421,7 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, opts: FlowOpts) -> Vec<Para> {
|
||||
mark_del: None,
|
||||
fmt_change: None,
|
||||
extra: None,
|
||||
mark: None,
|
||||
};
|
||||
if opts.paras {
|
||||
out.extra = para_extra(doc, &p.props);
|
||||
@@ -458,6 +459,7 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, opts: FlowOpts) -> Vec<Para> {
|
||||
mark_del: None,
|
||||
fmt_change: None,
|
||||
extra: None,
|
||||
mark: None,
|
||||
};
|
||||
let prev = std::mem::replace(&mut out, next);
|
||||
if prev.inlines.is_empty() {
|
||||
@@ -581,10 +583,18 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, opts: FlowOpts) -> Vec<Para> {
|
||||
}
|
||||
// The paragraph that ends a section carries its properties (the last piece of it does).
|
||||
out.section.clone_from(&p.section);
|
||||
out.mark = empty_mark(&out, p, opts);
|
||||
done.push(out);
|
||||
done
|
||||
}
|
||||
|
||||
/// The mark formatting of `out`, the last piece of `p`, when it holds no text (its height), for
|
||||
/// formats that keep it (flows asked for paragraph formatting).
|
||||
fn empty_mark(out: &Para, p: &Paragraph, opts: FlowOpts) -> Option<Box<CharProps>> {
|
||||
let m = CharProps { ins: None, del: None, fmt_change: None, ..p.mark.clone() };
|
||||
(opts.paras && out.inlines.is_empty() && !m.is_empty()).then(|| Box::new(m))
|
||||
}
|
||||
|
||||
/// The drawings from `from` on (just added) raised by their run's position `raise` (points): a
|
||||
/// drawing in line keeps it as its float's `y` (see [`crate::model::Img::float`]).
|
||||
fn raise_drawings(inlines: &mut [Inline], from: usize, raise: Option<f32>) {
|
||||
|
||||
@@ -597,6 +597,9 @@ impl<'a> Builder<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(m) = p.mark.as_deref().filter(|_| out.text.is_empty()) {
|
||||
out.mark = self.budget(CharProps { ins: None, del: None, fmt_change: None, ..m.clone() });
|
||||
}
|
||||
out.mark.ins = p.mark_ins.as_deref().map(|m| self.revision(RevisionKind::Insert, m));
|
||||
out.mark.del = p.mark_del.as_deref().map(|m| self.revision(RevisionKind::Delete, m));
|
||||
out.normalize();
|
||||
|
||||
@@ -652,6 +652,10 @@ pub struct Para {
|
||||
/// Paragraph formatting beyond the fields above, for formats that keep it (ODT): its own
|
||||
/// tab stops. Only flows asked for it ([`FlowOpts::paras`]) hold it.
|
||||
pub extra: Option<Box<wordcraft_doc::props::ParaProps>>,
|
||||
/// The paragraph mark's direct formatting (its size sets an empty paragraph's height), for
|
||||
/// formats that keep it (ODT): only flows asked for paragraph formatting
|
||||
/// ([`FlowOpts::paras`]) hold it, and only for paragraphs without text.
|
||||
pub mark: Option<Box<wordcraft_doc::props::CharProps>>,
|
||||
}
|
||||
|
||||
impl Para {
|
||||
|
||||
@@ -546,7 +546,13 @@ impl Body<'_> {
|
||||
self.index_marks.clear();
|
||||
let Some(mut p) = self.para.take() else { return };
|
||||
self.glue_connectors(&mut p);
|
||||
self.fmt.pop();
|
||||
// An empty paragraph's own text formatting is its mark's: its size is its height.
|
||||
if let Some(base) = self.fmt.pop()
|
||||
&& p.inlines.is_empty()
|
||||
&& base != model::Fmt::default()
|
||||
{
|
||||
p.mark = Some(Box::new(crate::model::char_props(&base)));
|
||||
}
|
||||
// A paragraph ending inside an open insertion has an inserted mark. (Paragraphs that
|
||||
// `place_deletion` splits off keep only the marks read inside their region.)
|
||||
if p.mark_ins.is_none() {
|
||||
|
||||
@@ -305,18 +305,24 @@ pub(super) fn float_of(anchor: Option<&str>, x: Option<&str>, y: Option<&str>, g
|
||||
Some("page-start-margin") => Anchor::LeftMargin,
|
||||
Some("page-end-margin") => Anchor::RightMargin,
|
||||
Some("page-content") => Anchor::Margin,
|
||||
// A page has no paragraph: LibreOffice measures a page-anchored frame's paragraph
|
||||
// relations from the page, its content relations from the page's text area.
|
||||
Some("paragraph-content") if page => Anchor::Margin,
|
||||
Some("paragraph-start-margin") if page => Anchor::LeftMargin,
|
||||
Some("paragraph-end-margin") if page => Anchor::RightMargin,
|
||||
Some("paragraph-content") => Anchor::Column,
|
||||
Some("char") => Anchor::Character,
|
||||
Some("char") if !page => Anchor::Character,
|
||||
Some(_) | None if page => Anchor::Page,
|
||||
Some(_) => Anchor::Paragraph,
|
||||
None if page => Anchor::Page,
|
||||
None => Anchor::Paragraph,
|
||||
};
|
||||
let v_rel = match g.vrel.as_deref() {
|
||||
Some("page") => Anchor::Page,
|
||||
Some("page-content") => Anchor::Margin,
|
||||
Some("line") | Some("char") | Some("text") | Some("baseline") => Anchor::Line,
|
||||
Some("paragraph-content") if page => Anchor::Margin,
|
||||
Some("line") | Some("char") | Some("text") | Some("baseline") if !page => Anchor::Line,
|
||||
Some(_) | None if page => Anchor::Page,
|
||||
Some(_) => Anchor::Paragraph,
|
||||
None if page => Anchor::Page,
|
||||
None => Anchor::Paragraph,
|
||||
};
|
||||
let h_align = match g.hpos.as_deref() {
|
||||
|
||||
@@ -113,11 +113,7 @@ fn each_para(blocks: &mut [FBlock], depth: usize, f: &mut dyn FnMut(&mut Para))
|
||||
FBlock::Para(p) => {
|
||||
f(p);
|
||||
for i in &mut p.inlines {
|
||||
match i {
|
||||
model::Inline::Note { blocks, .. } => each_para(blocks, depth + 1, f),
|
||||
model::Inline::Comment { comment, .. } => each_para(&mut comment.blocks, depth + 1, f),
|
||||
_ => {}
|
||||
}
|
||||
each_inline_para(i, depth + 1, f);
|
||||
}
|
||||
}
|
||||
FBlock::Table(t) => {
|
||||
@@ -129,6 +125,24 @@ fn each_para(blocks: &mut [FBlock], depth: usize, f: &mut dyn FnMut(&mut Para))
|
||||
}
|
||||
}
|
||||
|
||||
/// [`each_para`] in the paragraphs `i` holds: a note's, a comment's, a text box's, or those of
|
||||
/// the text boxes in a group.
|
||||
fn each_inline_para(i: &mut model::Inline, depth: usize, f: &mut dyn FnMut(&mut Para)) {
|
||||
if depth > model::MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
match i {
|
||||
model::Inline::Note { blocks, .. } | model::Inline::Shape { blocks, .. } => each_para(blocks, depth, f),
|
||||
model::Inline::Comment { comment, .. } => each_para(&mut comment.blocks, depth, f),
|
||||
model::Inline::Group { members, .. } => {
|
||||
for m in members {
|
||||
each_inline_para(&mut m.inline, depth + 1, f);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The custom character styles the text of `blocks` names (by ODF name, in [`model::Fmt::extra`])
|
||||
/// with their custom parents, parents first, each with its document style id, added to `found`;
|
||||
/// the text then names them by id. `para_ids` are the paragraph styles' ids, which a character
|
||||
|
||||
@@ -280,12 +280,16 @@ fn styles_xml(
|
||||
first: Some(normal.indent_first),
|
||||
};
|
||||
let kern = doc.styles.default_chr.kern.is_some_and(|k| k > 0.0);
|
||||
// Normal's font size (Standard, written empty, takes the default style's).
|
||||
let size = doc.styles.resolve_char(None, &Default::default()).size;
|
||||
let size = if size.is_finite() { size.clamp(1.0, 1638.0) } else { 12.0 };
|
||||
let tab = doc.settings.default_tab.clamp(1.0, 1584.0);
|
||||
let mut s = format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<office:document-styles {NS}><office:styles>");
|
||||
s.push_str(&format!(
|
||||
"<style:default-style style:family=\"paragraph\"><style:paragraph-properties{} style:tab-stop-distance=\"{tab}\"/><style:text-properties fo:font-family=\"'{}'\" fo:font-size=\"12pt\" style:letter-kerning=\"{kern}\"{scripts}/></style:default-style>",
|
||||
"<style:default-style style:family=\"paragraph\"><style:paragraph-properties{} style:tab-stop-distance=\"{tab}\"/><style:text-properties fo:font-family=\"'{}'\" fo:font-size=\"{size}\" style:letter-kerning=\"{kern}\"{scripts}/></style:default-style>",
|
||||
spacing_props(defaults),
|
||||
x(&body_font),
|
||||
size = pt(size),
|
||||
tab = pt(tab),
|
||||
scripts = super::writer::script_attrs(&doc.styles.default_chr)
|
||||
));
|
||||
|
||||
@@ -1068,7 +1068,6 @@ 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> {
|
||||
@@ -1081,6 +1080,7 @@ impl Styles {
|
||||
.map(|v| v.min(1638.0))
|
||||
}
|
||||
|
||||
/// The document's default font: the Standard paragraph style's, else the default style's.
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -1185,6 +1185,16 @@ fn the_default_styles_font_size_is_the_documents() {
|
||||
// Written back as the default style's.
|
||||
let s = entry(&export(&doc).unwrap(), "styles.xml");
|
||||
assert!(s.contains(r#"fo:font-size="10pt""#), "{s}");
|
||||
// Standard's 11 pt too: LibreOffice's letters grew a page at 12 pt.
|
||||
let s = entry(&export(&d).unwrap(), "styles.xml");
|
||||
assert!(
|
||||
s.contains(r#"<style:text-properties fo:font-family="'Liberation Serif'" fo:font-size="11pt""#)
|
||||
|| s.contains(r#"fo:font-size="11pt" style:letter-kerning"#),
|
||||
"{s}"
|
||||
);
|
||||
let back = import(&export(&d).unwrap()).unwrap();
|
||||
let p = back.body[0].as_para().unwrap();
|
||||
assert_eq!(back.styles.resolve_char(p.props.style.as_deref(), &p.runs[0].props).size, 11.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1202,3 +1212,22 @@ fn a_style_with_no_parent_takes_the_default_styles_size() {
|
||||
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]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_paragraph_keeps_its_marks_size() {
|
||||
// LibreOffice's fax template: an empty 6 pt line above "Important!". Its size is the mark's,
|
||||
// the paragraph's height, and is written back on the paragraph's style.
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:automatic-styles><style:style style:name="P1" style:family="paragraph"><style:text-properties fo:font-size="6pt"/></style:style></office:automatic-styles><office:body><office:text><text:p text:style-name="P1"/><text:p text:style-name="P1">Text</text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let doc = import(&package(&content, None)).unwrap();
|
||||
let marks = |d: &Document| d.body.iter().filter_map(|b| b.as_para()).map(|p| p.mark.size).collect::<Vec<_>>();
|
||||
// Only the empty paragraph's: the other's text has the size itself.
|
||||
assert_eq!(marks(&doc), [Some(6.0), None]);
|
||||
let s = entry(&export(&doc).unwrap(), "content.xml");
|
||||
assert!(s.contains(r#"fo:font-size="6pt""#), "{s}");
|
||||
let back = import(&export(&doc).unwrap()).unwrap();
|
||||
assert_eq!(marks(&back), [Some(6.0), None]);
|
||||
let p = back.body[1].as_para().unwrap();
|
||||
assert_eq!(p.runs[0].props.size, Some(6.0));
|
||||
}
|
||||
@@ -56,6 +56,25 @@ fn wrap_modes_and_offsets() {
|
||||
assert_eq!(float(&open("", r#"text:anchor-type="as-char""#)).wrap, Wrap::Inline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_anchored_frames_paragraph_relations_are_the_pages() {
|
||||
// LibreOffice's letter templates: a page-anchored frame "from the paragraph" is placed from
|
||||
// the page's edge, "from the paragraph's content" from its text area.
|
||||
let f = float(&open(
|
||||
r#"style:wrap="run-through" style:horizontal-pos="from-left" style:horizontal-rel="paragraph" style:vertical-pos="from-top" style:vertical-rel="paragraph""#,
|
||||
r#"text:anchor-type="page" svg:x="2cm" svg:y="2cm""#,
|
||||
));
|
||||
assert_eq!((f.h_rel, f.v_rel), (Anchor::Page, Anchor::Page));
|
||||
let f = float(&open(
|
||||
r#"style:wrap="run-through" style:horizontal-pos="from-left" style:horizontal-rel="paragraph-content" style:vertical-rel="char""#,
|
||||
r#"text:anchor-type="page" svg:x="2cm""#,
|
||||
));
|
||||
assert_eq!((f.h_rel, f.v_rel), (Anchor::Margin, Anchor::Page));
|
||||
// Anchored to a paragraph, they stay the paragraph's.
|
||||
let f = float(&open(r#"style:horizontal-rel="paragraph-content" style:vertical-rel="char""#, r#"text:anchor-type="char""#));
|
||||
assert_eq!((f.h_rel, f.v_rel), (Anchor::Column, Anchor::Line));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floating_pictures_round_trip() {
|
||||
for (graphic, frame) in [
|
||||
|
||||
@@ -283,3 +283,25 @@ fn boxes_of(d: &Document) -> usize {
|
||||
.filter(|o| matches!(o, InlineObject::Shape { kind: ShapeKind::TextBox, .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_custom_style_only_in_a_text_box_is_kept() {
|
||||
// LibreOffice's fax template: "Frame contents" (centred) names only paragraphs in a text box.
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T} {NS_D}><office:body><office:text><text:p>Before<draw:frame svg:width="2in" svg:height="1in" text:anchor-type="paragraph"><draw:text-box><text:p text:style-name="Frame_20_contents">Important!</text:p></draw:text-box></draw:frame></text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:style style:name="Standard" style:family="paragraph"/><style:style style:name="Frame_20_contents" style:display-name="Frame contents" style:family="paragraph" style:parent-style-name="Standard"><style:paragraph-properties fo:text-align="center"/></style:style></office:styles></office:document-styles>"#
|
||||
);
|
||||
let centred = |d: &Document| {
|
||||
let part = d.parts.values().find(|p| p.kind == PartKind::TextBox).expect("the box's story");
|
||||
let p = part.blocks[0].as_para().unwrap();
|
||||
let style = p.props.style.clone().expect("a style");
|
||||
assert!(d.styles.get(&style).is_some(), "{style} is defined");
|
||||
d.styles.resolve_para(&p.props).align
|
||||
};
|
||||
let d = import(&package(&content, Some(&styles))).unwrap();
|
||||
assert_eq!(centred(&d), wordcraft_doc::props::Align::Center);
|
||||
let back = import(&export(&d).unwrap()).unwrap();
|
||||
assert_eq!(centred(&back), wordcraft_doc::props::Align::Center);
|
||||
}
|
||||
@@ -299,8 +299,14 @@ impl Writer {
|
||||
props.push('\u{1}');
|
||||
props.push_str(&children);
|
||||
}
|
||||
if p.extra.as_ref().and_then(|x| x.suppress_hyphens) == Some(false) {
|
||||
// An empty paragraph's mark formatting, its size giving its height.
|
||||
let mark = p.mark.as_deref().filter(|_| p.inlines.is_empty()).map(super::writer::props::props_text).unwrap_or_default();
|
||||
let hyphenate = p.extra.as_ref().and_then(|x| x.suppress_hyphens) == Some(false);
|
||||
if hyphenate || !mark.is_empty() {
|
||||
props.push('\u{2}');
|
||||
props.push_str(&mark);
|
||||
}
|
||||
if hyphenate {
|
||||
props.push_str(" fo:hyphenate=\"true\"");
|
||||
}
|
||||
let master = start.map(|(m, _)| m.clone()).unwrap_or_default();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity with Microsoft Word
|
||||
|
||||
> **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
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-02 · **Change:** trivial (ODT default font size saved; page-anchored frames placed from the page; text box paragraph styles kept; empty paragraphs keep their size); previously 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); previouLine 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;
|
||||
@@ -280,6 +280,7 @@ Custom document properties retain their bookmark link targets through DOCX saves
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | trivial | ODT: saving wrote the default font size as 12 pt whatever the document's was, so LibreOffice's 11 pt letters gained a page after a save; Normal's size is written. A page-anchored frame placed "from the paragraph" (LibreOffice's letter logos) was moved by the page margin; it's placed from the page, and "from the paragraph's content" from the page's text area. A custom style naming only paragraphs in text boxes (the fax template's centred "Frame contents") was dropped, leaving them Normal; it's kept. An empty paragraph's own font size (its height: the fax's 6 pt line) was dropped; it's the paragraph mark's, read and written (`odt::tests::the_default_styles_font_size_is_the_documents`, `odt::tests::an_empty_paragraph_keeps_its_marks_size`, `floats::a_page_anchored_frames_paragraph_relations_are_the_pages`, `text_boxes::a_custom_style_only_in_a_text_box_is_kept`) |
|
||||
| 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`) |
|
||||
|
||||
Reference in new issue
Block a user