mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 11:50:20 -06:00
ODT: default text colour; covered cells' content stays out of the body
The default paragraph style's text colour (fo:color) is read into the document's default and written back: Google Docs' grey résumé came back black. What a covered table cell holds (not shown, §9.1.5) is skipped; it went before the table as empty paragraphs, pushing ONLYOFFICE's table down the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This commit is contained in:
@@ -919,6 +919,8 @@ impl Body<'_> {
|
||||
t.row.push(Cell { covered: true, ..Default::default() });
|
||||
}
|
||||
}
|
||||
// What a covered cell holds isn't shown (§9.1.5): skipped (it went before the table).
|
||||
self.skip = 1;
|
||||
}
|
||||
"frame" => {
|
||||
let w = get(a, "svg:width").and_then(length);
|
||||
@@ -1276,6 +1278,10 @@ impl Body<'_> {
|
||||
if self.skip > 0 {
|
||||
return;
|
||||
}
|
||||
// Not a custom shape's geometry (a covered cell, say): nothing more.
|
||||
if self.geometry.is_none() && self.geometry_xml.is_none() {
|
||||
return;
|
||||
}
|
||||
let xml = self.geometry_xml.take();
|
||||
let keep = |x: Option<String>| x.filter(|x| x.len() < MAX_GEOMETRY_XML).map(|x| std::sync::Arc::new(format!("{x}</draw:enhanced-geometry>")));
|
||||
if let Some(g) = self.geometry.take()
|
||||
|
||||
@@ -441,6 +441,8 @@ struct Parsed {
|
||||
font: Option<String>,
|
||||
/// The document's default font size, points.
|
||||
size: Option<f32>,
|
||||
/// The document's default text colour.
|
||||
color: Option<wordcraft_doc::props::Rgb>,
|
||||
/// The custom paragraph styles the paragraphs use, parents included, parents first.
|
||||
named: Vec<super::styles::NamedStyle>,
|
||||
/// Their ODF names, alongside.
|
||||
@@ -618,6 +620,7 @@ fn parse_tracked(bytes: &[u8], paper: (f32, f32), profile: &OdfProfile) -> Resul
|
||||
track,
|
||||
font: styles.doc_font(),
|
||||
size: styles.doc_size(),
|
||||
color: styles.doc_color(),
|
||||
spacing: styles.doc_spacing(),
|
||||
looks: styles.kind_looks(),
|
||||
named,
|
||||
@@ -665,6 +668,7 @@ pub fn import_with(bytes: &[u8], page: &SectionProps, profile: &OdfProfile) -> R
|
||||
track,
|
||||
font,
|
||||
size,
|
||||
color,
|
||||
spacing,
|
||||
looks,
|
||||
named,
|
||||
@@ -705,6 +709,9 @@ pub fn import_with(bytes: &[u8], page: &SectionProps, profile: &OdfProfile) -> R
|
||||
if size.is_some() {
|
||||
doc.styles.default_chr.size = size;
|
||||
}
|
||||
if let Some(c) = color {
|
||||
doc.styles.default_chr.color = Some(wordcraft_doc::props::TextColor::Rgb(c));
|
||||
}
|
||||
doc.styles.default_chr.kern = kern.then_some(1.0);
|
||||
doc.styles.default_chr.overlay(&scripts);
|
||||
if let Some(t) = default_tab {
|
||||
|
||||
@@ -341,13 +341,18 @@ fn styles_xml(
|
||||
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{}{widows} style:tab-stop-distance=\"{tab}\"/><style:text-properties fo:font-family=\"'{}'\" fo:font-size=\"{size}\" style:letter-kerning=\"{kern}\"{scripts}/></style:default-style>",
|
||||
"<style:default-style style:family=\"paragraph\"><style:paragraph-properties{}{widows} style:tab-stop-distance=\"{tab}\"/><style:text-properties fo:font-family=\"'{}'\" fo:font-size=\"{size}\" style:letter-kerning=\"{kern}\"{color}{scripts}/></style:default-style>",
|
||||
spacing_props(defaults),
|
||||
x(&body_font),
|
||||
// Widow and orphan control as Normal has it, said either way (the standard leaves it open
|
||||
// when unsaid: see `OdfProfile::widow_control`).
|
||||
widows = if normal.widow_control { " fo:widows=\"2\" fo:orphans=\"2\"" } else { " fo:widows=\"0\" fo:orphans=\"0\"" },
|
||||
size = pt(size),
|
||||
// The document's text colour (a colour, not automatic).
|
||||
color = match doc.styles.default_chr.color {
|
||||
Some(wordcraft_doc::props::TextColor::Rgb(c)) => format!(" fo:color=\"#{}\"", c.hex().to_ascii_lowercase()),
|
||||
_ => String::new(),
|
||||
},
|
||||
tab = pt(tab),
|
||||
scripts = super::writer::script_attrs(&doc.styles.default_chr)
|
||||
));
|
||||
|
||||
@@ -1042,6 +1042,8 @@ pub(super) struct Styles {
|
||||
pub(super) default_kern: bool,
|
||||
/// Widow and orphan control `style:default-style` for paragraphs states, if it does.
|
||||
default_widows: Option<bool>,
|
||||
/// The text colour of `style:default-style` for paragraphs (`fo:color`), if it gives one.
|
||||
default_color: Option<wordcraft_doc::props::Rgb>,
|
||||
/// `style:default-style` for graphics: what every drawing shape's style falls back on.
|
||||
default_graphic: super::frames::Graphic,
|
||||
/// The named dashes and gradients graphic styles use.
|
||||
@@ -1092,6 +1094,7 @@ impl Styles {
|
||||
self.default_size = Some(sz.min(1638.0));
|
||||
}
|
||||
self.default_kern = get(&a, "style:letter-kerning").is_some_and(|k| k.trim() == "true");
|
||||
self.default_color = get(&a, "fo:color").and_then(model::parse_color);
|
||||
script_properties(&a, &mut self.default_scripts, &self.faces);
|
||||
}
|
||||
"paragraph-properties" if in_default && cur.is_none() => {
|
||||
@@ -1173,6 +1176,12 @@ impl Styles {
|
||||
|
||||
/// 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.
|
||||
/// The document's default text colour: the Standard paragraph style's, else the default
|
||||
/// style's.
|
||||
pub(super) fn doc_color(&self) -> Option<wordcraft_doc::props::Rgb> {
|
||||
self.map.get("Standard").filter(|s| !s.auto).and_then(|s| s.fmt.color).or(self.default_color)
|
||||
}
|
||||
|
||||
pub(super) fn doc_size(&self) -> Option<f32> {
|
||||
self.map
|
||||
.get("Standard")
|
||||
|
||||
@@ -44,6 +44,28 @@ fn default_paragraph_style_font_becomes_document_font() {
|
||||
assert_eq!(doc.styles.default_chr.font.as_deref(), Some("DejaVu Serif"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_default_text_colour_round_trips() {
|
||||
// Google Docs' résumé template greys all its text in the default style; it came back black.
|
||||
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 styles = format!(
|
||||
r##"<office:document-styles {NS_T}><office:styles><style:default-style style:family="paragraph"><style:text-properties fo:color="#666666"/></style:default-style></office:styles></office:document-styles>"##
|
||||
);
|
||||
let grey = Some(wordcraft_doc::props::TextColor::Rgb(wordcraft_doc::props::Rgb(0x66, 0x66, 0x66)));
|
||||
let doc = import(&package(&content, Some(&styles))).unwrap();
|
||||
assert_eq!(doc.styles.default_chr.color, grey);
|
||||
let back = import(&export(&doc).unwrap()).unwrap();
|
||||
assert_eq!(back.styles.default_chr.color, grey);
|
||||
// None stated: none written (automatic).
|
||||
let plain = import(&package(&content, None)).unwrap();
|
||||
let styles = entry(&export(&plain).unwrap(), "styles.xml");
|
||||
let default =
|
||||
styles.split("<style:default-style style:family=\"paragraph\">").nth(1).and_then(|s| s.split("</style:default-style>").next()).unwrap();
|
||||
assert!(!default.contains("fo:color"), "{default}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_garbage() {
|
||||
assert!(parse(b"not a zip").is_err());
|
||||
|
||||
@@ -176,3 +176,18 @@ fn a_cells_background_picture_is_read_drawn_and_written() {
|
||||
let again = cells(&back)[0].picture.clone().expect("kept");
|
||||
assert_eq!((again.repeat, again.align, again.size), (pic.repeat, pic.align, pic.size));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_a_covered_cell_holds_stays_out_of_the_document() {
|
||||
// ONLYOFFICE writes paragraphs into covered cells (§9.1.5: not shown); they went before the
|
||||
// table as empty paragraphs, pushing it down the page.
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:body><office:text><table:table><table:table-column table:number-columns-repeated="2"/><table:table-row><table:table-cell table:number-columns-spanned="2"><text:p>wide</text:p></table:table-cell><table:covered-table-cell><text:p>hidden</text:p><text:list><text:list-item><text:p/></text:list-item></text:list></table:covered-table-cell></table:table-row><table:table-row><table:table-cell><text:p>a</text:p></table:table-cell><table:table-cell><text:p>b</text:p></table:table-cell></table:table-row></table:table><text:p>after</text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let d = import(&package(&content, None)).unwrap();
|
||||
assert!(matches!(&*d.body[0], Block::Table(_)), "the table comes first");
|
||||
assert_eq!(d.body.len(), 2);
|
||||
assert_eq!(d.body[1].as_para().unwrap().text, "after");
|
||||
let Block::Table(t) = &*d.body[0] else { panic!() };
|
||||
assert_eq!(t.rows[0].cells[0].props.span, 2);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user