diff --git a/crates/doc/src/swatch_refs.rs b/crates/doc/src/swatch_refs.rs index d15519fcf..eb5cfbd12 100644 --- a/crates/doc/src/swatch_refs.rs +++ b/crates/doc/src/swatch_refs.rs @@ -17,6 +17,53 @@ struct Names<'a> { } impl Names<'_> { + /// Rename the swatch references in every paragraph, character, object, cell and table style. + fn styles(&self, styles: &mut crate::styles::Styles) { + let n = self; + for p in &mut styles.paragraph { + n.para(&mut p.para); + n.chars(&mut p.chars); + } + for c in &mut styles.character { + n.chars(&mut c.chars); + } + for o in &mut styles.object { + if let Some(f) = &mut o.fill { + n.fill(f); + } + if let Some(s) = &mut o.stroke { + n.stroke(s); + } + if let Some(tf) = &mut o.text_frame { + n.frame(tf); + } + } + for c in &mut styles.cell { + n.o(&mut c.fill); + if let Some(s) = &mut c.stroke { + n.cell_stroke(s); + } + for s in &mut c.strokes { + n.stroke_attrs(s); + } + } + for t in &mut styles.table { + if let Some(b) = &mut t.border { + n.cell_stroke(b); + } + for b in &mut t.borders { + n.stroke_attrs(b); + } + if let Some(f) = &mut t.alt_rows { + n.alt_fills(f); + } + n.alt_fill_attrs(&mut t.row_fills); + n.alt_fill_attrs(&mut t.column_fills); + n.alt_stroke_attrs(&mut t.row_strokes); + n.alt_stroke_attrs(&mut t.column_strokes); + } + } + fn s(&self, v: &mut String) { if v == self.from { *v = self.to.to_string(); @@ -170,49 +217,7 @@ impl Document { n.s(b); } n.s(&mut self.footnote_options.rule.color); - let styles = self.styles_mut(); - for p in &mut styles.paragraph { - n.para(&mut p.para); - n.chars(&mut p.chars); - } - for c in &mut styles.character { - n.chars(&mut c.chars); - } - for o in &mut styles.object { - if let Some(f) = &mut o.fill { - n.fill(f); - } - if let Some(s) = &mut o.stroke { - n.stroke(s); - } - if let Some(tf) = &mut o.text_frame { - n.frame(tf); - } - } - for c in &mut styles.cell { - n.o(&mut c.fill); - if let Some(s) = &mut c.stroke { - n.cell_stroke(s); - } - for s in &mut c.strokes { - n.stroke_attrs(s); - } - } - for t in &mut styles.table { - if let Some(b) = &mut t.border { - n.cell_stroke(b); - } - for b in &mut t.borders { - n.stroke_attrs(b); - } - if let Some(f) = &mut t.alt_rows { - n.alt_fills(f); - } - n.alt_fill_attrs(&mut t.row_fills); - n.alt_fill_attrs(&mut t.column_fills); - n.alt_stroke_attrs(&mut t.row_strokes); - n.alt_stroke_attrs(&mut t.column_strokes); - } + n.styles(self.styles_mut()); for sp in self.spreads.iter_mut().chain(self.parents.iter_mut()) { for it in &mut Arc::make_mut(sp).items { n.item(Arc::make_mut(it), 0); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 16d20d2be..5032e9b5c 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -555,6 +555,48 @@ impl Renderer { self.drawing_items.pop(); } + /// A group: children carry their own transforms relative to the group. + fn draw_group(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, xf: Affine, page: ShownOn) { + // Isolate Blending / Knockout: the group is its own layer (blend modes stop at it). + let layered = it.opacity < 0.999 || it.blend != DcBlend::Normal || it.isolate || it.knockout; + if layered { + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, Some(blend_mode(it.blend)), Some(it.opacity), None, None); + } + // Gradient feather: the group's objects drawn together, then faded as one. + let feather = it.effects.gradient_feather.on && f.opts.quality != DisplayQuality::Fast; + if feather { + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, None, None, None, None); + } + if it.knockout { + // Knockout: each object first clears its area of those behind it, so it composites + // with the group's backdrop rather than with its siblings. + for c in it.shown_children() { + if !c.hidden { + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None); + ctx.set_transform(f.view * xf * c.xf); + ctx.set_paint(peniko::Color::BLACK); + ctx.fill_path(&c.path.to_bezpath()); + ctx.pop_layer(); + } + self.draw_item(ctx, f, c, xf, page); + } + } else { + for c in it.shown_children() { + self.draw_item(ctx, f, c, xf, page); + } + } + if feather { + fx::gradient_feather_mask(ctx, f, it, xf); + ctx.pop_layer(); + } + if layered { + ctx.pop_layer(); + } + } + fn draw_item_inner(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, parent: Affine, page: ShownOn) { if it.hidden || f.opts.hidden.contains(&it.id) || f.opts.printing_only && it.nonprinting { return; @@ -564,45 +606,7 @@ impl Renderer { // A frame with items pasted into it draws like any other object (draw_body: fill, the items // clipped to the frame, stroke), so its effects apply to all of it. if !it.children().is_empty() && !it.has_nested_items() { - // Groups: children carry their own transforms relative to the group. - // Isolate Blending / Knockout: the group is its own layer (blend modes stop at it). - let layered = it.opacity < 0.999 || it.blend != DcBlend::Normal || it.isolate || it.knockout; - if layered { - ctx.set_transform(Affine::IDENTITY); - ctx.push_layer(None, Some(blend_mode(it.blend)), Some(it.opacity), None, None); - } - // Gradient feather: the group's objects drawn together, then faded as one. - let feather = it.effects.gradient_feather.on && f.opts.quality != DisplayQuality::Fast; - if feather { - ctx.set_transform(Affine::IDENTITY); - ctx.push_layer(None, None, None, None, None); - } - if it.knockout { - // Knockout: each object first clears its area of those behind it, so it composites - // with the group's backdrop rather than with its siblings. - for c in it.shown_children() { - if !c.hidden { - ctx.set_transform(Affine::IDENTITY); - ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None); - ctx.set_transform(f.view * xf * c.xf); - ctx.set_paint(peniko::Color::BLACK); - ctx.fill_path(&c.path.to_bezpath()); - ctx.pop_layer(); - } - self.draw_item(ctx, f, c, xf, page); - } - } else { - for c in it.shown_children() { - self.draw_item(ctx, f, c, xf, page); - } - } - if feather { - fx::gradient_feather_mask(ctx, f, it, xf); - ctx.pop_layer(); - } - if layered { - ctx.pop_layer(); - } + self.draw_group(ctx, f, it, xf, page); return; } // Composed text stays inside its frame apart from glyph overhang (italics, swashes, big diff --git a/crates/textimport/src/xlsx.rs b/crates/textimport/src/xlsx.rs index 314dc9810..a26665ecc 100644 --- a/crates/textimport/src/xlsx.rs +++ b/crates/textimport/src/xlsx.rs @@ -91,6 +91,34 @@ fn general(f: f64) -> String { } } +/// The first and last column used on the header row, if that row has any cells. +fn header_span(cells: &[(usize, usize, String)], header_row: usize) -> Option<(usize, usize)> { + cells.iter().filter(|(r, _, _)| *r == header_row).fold(None, |span, (_, c, _)| match span { + None => Some((*c, *c)), + Some((lo, hi)) => Some((lo.min(*c), hi.max(*c))), + }) +} + +/// One row's values laid into `width` columns starting at column `min_c`; values outside that +/// span come back separately (only the non-empty ones), so the caller can warn. +fn place_row(cols: &[(usize, String)], min_c: usize, width: usize) -> (Vec, Vec) { + let mut row = vec![String::new(); width]; + let mut extra: Vec = Vec::new(); + for (c, v) in cols { + let at = c.checked_sub(min_c).filter(|at| *at < width); + match at { + Some(at) => { + if let Some(cell) = row.get_mut(at) { + *cell = v.clone(); + } + } + None if !v.is_empty() => extra.push(v.clone()), + None => {} + } + } + (row, extra) +} + /// One worksheet as a grid of cached cell text, empty cells kept, for data merge. /// The first used row is the header. Later rows that are entirely empty are left out. /// Cells past the header are kept so the caller can warn. Over 200 columns or 100,000 @@ -117,19 +145,9 @@ pub fn records(bytes: &[u8], sheet: Option<&str>) -> Result>, Im return Err(ImportError::Corrupt("the worksheet is empty".into())); } let header_row = cells.iter().map(|c| c.0).min().unwrap_or(0); - let mut min_c = usize::MAX; - let mut max_c = 0usize; - let mut header_used = false; - for (r, c, _) in &cells { - if *r == header_row { - header_used = true; - min_c = min_c.min(*c); - max_c = max_c.max(*c); - } - } - if !header_used || min_c == usize::MAX { + let Some((min_c, max_c)) = header_span(&cells, header_row) else { return Err(ImportError::Corrupt("the worksheet has no header".into())); - } + }; let width = max_c.saturating_sub(min_c).saturating_add(1); if width > crate::DATA_MERGE_MAX_COLS { return Err(ImportError::Corrupt(format!("the sheet has {width} columns; the limit is {}", crate::DATA_MERGE_MAX_COLS))); @@ -154,25 +172,7 @@ pub fn records(bytes: &[u8], sheet: Option<&str>) -> Result>, Im } let mut out = Vec::with_capacity(data_rows.saturating_add(1)); for (r, cols) in &by_row { - let mut row = vec![String::new(); width]; - let mut extra: Vec = Vec::new(); - for (c, v) in cols { - if *c < min_c { - // Outside the header span. Keep one non-empty value so the caller can warn. - if !v.is_empty() { - extra.push(v.clone()); - } - continue; - } - let at = c - min_c; - if at < width { - if let Some(cell) = row.get_mut(at) { - *cell = v.clone(); - } - } else if !v.is_empty() { - extra.push(v.clone()); - } - } + let (mut row, extra) = place_row(cols, min_c, width); if *r != header_row && row.iter().all(String::is_empty) && extra.is_empty() { continue; }