Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/oxml-drawing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The archive row is regenerated from the package that carries this README.

| Measurement | Value | Version | Platform | Build mode | Input | Command | Statistic | Measured on |
|---|---|---|---|---|---|---|---|---|
| Crates.io archive: oxml-drawing | 161,119 compressed bytes, 1,128,372 member bytes, 24 members | 0.12.1 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo package --locked --no-verify` | Tracked `oxml-drawing` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-09-19 |
| Crates.io archive: oxml-drawing | 161,853 compressed bytes, 1,131,956 member bytes, 24 members | 0.12.1 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo package --locked --no-verify` | Tracked `oxml-drawing` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-09-27 |

## Use it when

Expand Down
94 changes: 88 additions & 6 deletions crates/oxml-drawing/src/fill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,12 @@ pub enum GradientGeometry {
/// Linear gradient angle and scaling behavior.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LinearGradient {
/// `@ang`. The schema makes it optional without a default, and a missing
/// angle reads as 0, a left to right axis, as in LibreOffice's import.
pub angle: Angle,
pub scaled: Option<bool>,
/// The source had no `@ang`, so a zero angle is written without one.
angle_omitted: bool,
raw_children: OrderedRawChildren,
}

Expand Down Expand Up @@ -322,8 +326,12 @@ impl PathGradientKind {
/// Path gradient shape and optional focal rectangle.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PathGradient {
/// `@path`. The schema makes it optional without a default, and a missing
/// path reads as `rect`, as in LibreOffice's import.
pub kind: PathGradientKind,
pub fill_to_rect: Option<RelativeRect>,
/// The source had no `@path`, so a `rect` path is written without one.
kind_omitted: bool,
raw_children: OrderedRawChildren,
}

Expand Down Expand Up @@ -1035,9 +1043,11 @@ fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String

fn parse_linear(start: &BytesStart<'_>) -> Result<LinearGradient> {
reject_conflicting_a_prefix(start)?;
let angle = optional_parse::<i32>(start, b"ang")?;
Ok(LinearGradient {
angle: Angle(required_i32(start, b"ang")?),
angle: Angle(angle.unwrap_or(0)),
scaled: optional_bool(start, b"scaled")?,
angle_omitted: angle.is_none(),
raw_children: OrderedRawChildren::default(),
})
}
Expand All @@ -1053,11 +1063,17 @@ fn parse_linear_element(

fn parse_empty_path(start: &BytesStart<'_>) -> Result<PathGradient> {
reject_conflicting_a_prefix(start)?;
let value = required_attr(start, b"path")?;
let kind = PathGradientKind::parse(&value).ok_or_else(|| invalid(start, b"path", value))?;
let (kind, kind_omitted) = match get_attr(start, b"path") {
Some(value) => (
PathGradientKind::parse(&value).ok_or_else(|| invalid(start, b"path", value))?,
false,
),
None => (PathGradientKind::Rectangle, true),
};
Ok(PathGradient {
kind,
fill_to_rect: None,
kind_omitted,
raw_children: OrderedRawChildren::default(),
})
}
Expand Down Expand Up @@ -1099,7 +1115,9 @@ fn write_geometry<W: Write>(writer: &mut Writer<W>, geometry: &GradientGeometry)
GradientGeometry::Linear(linear) => {
let angle = linear.angle.0.to_string();
let mut start = BytesStart::new("a:lin");
start.push_attribute(("ang", angle.as_str()));
if !linear.angle_omitted || linear.angle.0 != 0 {
start.push_attribute(("ang", angle.as_str()));
}
if let Some(scaled) = linear.scaled.map(bool_text) {
start.push_attribute(("scaled", scaled));
}
Expand All @@ -1112,7 +1130,9 @@ fn write_geometry<W: Write>(writer: &mut Writer<W>, geometry: &GradientGeometry)
}
GradientGeometry::Path(path) => {
let mut start = BytesStart::new("a:path");
start.push_attribute(("path", path.kind.as_str()));
if !path.kind_omitted || path.kind != PathGradientKind::Rectangle {
start.push_attribute(("path", path.kind.as_str()));
}
if path.fill_to_rect.is_none() && path.raw_children.is_empty() {
return write_empty(writer, start);
}
Expand Down Expand Up @@ -1486,7 +1506,9 @@ fn write_end<W: Write>(writer: &mut Writer<W>, name: &str) -> Result<()> {

#[cfg(test)]
mod tests {
use super::{Fill, FillError, GradientGeometry};
use super::{
Angle, Fill, FillError, GradientFill, GradientGeometry, LinearGradient, PathGradientKind,
};

#[test]
fn every_fill_form_round_trips_and_gradient_stops_keep_document_order() {
Expand Down Expand Up @@ -1523,6 +1545,66 @@ mod tests {
));
}

#[test]
fn gradient_geometry_without_angle_or_path_round_trips_without_adding_them() {
let cases: &[&[u8]] = &[
br#"<a:gradFill><a:lin scaled="0"/></a:gradFill>"#,
br#"<a:gradFill><a:lin/></a:gradFill>"#,
br#"<a:gradFill><a:path/></a:gradFill>"#,
br#"<a:gradFill><a:path><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill>"#,
];
for xml in cases {
let parsed = Fill::from_xml(xml).unwrap();
assert_eq!(parsed.to_xml().unwrap(), *xml);
}

// A missing angle reads as 0 and a missing path as rect. Another value
// assigned to either is written, since the default no longer holds.
let Fill::Gradient(mut gradient) = Fill::from_xml(cases[0]).unwrap() else {
panic!("expected gradient")
};
let Some(GradientGeometry::Linear(linear)) = &mut gradient.geometry else {
panic!("expected linear geometry")
};
assert_eq!(linear.angle, Angle(0));
assert_eq!(linear.scaled, Some(false));
linear.angle = Angle(5_400_000);
assert_eq!(
Fill::Gradient(gradient).to_xml().unwrap(),
br#"<a:gradFill><a:lin ang="5400000" scaled="0"/></a:gradFill>"#
);

let Fill::Gradient(mut gradient) = Fill::from_xml(cases[3]).unwrap() else {
panic!("expected gradient")
};
let Some(GradientGeometry::Path(path)) = &mut gradient.geometry else {
panic!("expected path geometry")
};
assert_eq!(path.kind, PathGradientKind::Rectangle);
assert!(path.fill_to_rect.is_some());
path.kind = PathGradientKind::Circle;
assert_eq!(
Fill::Gradient(gradient).to_xml().unwrap(),
br#"<a:gradFill><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill>"#
);

// Explicit defaults, and a value built in code, keep their attribute.
for xml in [
br#"<a:gradFill><a:lin ang="0" scaled="0"/></a:gradFill>"#.as_slice(),
br#"<a:gradFill><a:path path="rect"/></a:gradFill>"#,
] {
assert_eq!(Fill::from_xml(xml).unwrap().to_xml().unwrap(), xml);
}
let built = GradientFill {
geometry: Some(GradientGeometry::Linear(LinearGradient::default())),
..GradientFill::default()
};
assert_eq!(
Fill::Gradient(built).to_xml().unwrap(),
br#"<a:gradFill><a:lin ang="0"/></a:gradFill>"#
);
}

#[test]
fn fill_forms_read_any_prefix_and_write_fixed_a_prefix_in_schema_order() {
let fill = Fill::from_xml(br#"<z:blipFill><z:blip r:embed="rId4"/><z:srcRect l="10"/><z:stretch><z:fillRect t="20"/></z:stretch></z:blipFill>"#).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/oxml-pdf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ rows are the enforced release-mode bounds plus one dated observation.

| Measurement | Value | Version | Platform | Build mode | Input | Command | Statistic | Measured on |
|---|---|---|---|---|---|---|---|---|
| Crates.io archive: oxml-pdf | 66,015 compressed bytes, 304,432 member bytes, 14 members | 0.12.1 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo package --locked --no-verify` | Tracked `oxml-pdf` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-09-19 |
| Crates.io archive: oxml-pdf | 67,056 compressed bytes, 310,995 member bytes, 14 members | 0.12.1 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo package --locked --no-verify` | Tracked `oxml-pdf` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-09-27 |
| Large-document layout throughput | minimum 250 pages/s, observed 31,019.1 pages/s | rdocx 0.14.0 | macOS 26.6.2, Apple M5 Max, arm64 | release, one test thread | 1,000 one-page paragraphs with deterministic fonts | `cargo test -p rdocx --test regression_test --release a_thousand_page_document_paginates_and_renders_within_the_declared_limits -- --ignored --exact --nocapture --test-threads=1` | pages per wall-clock second | 2026-09-19 |
| Large-document layout peak allocation | maximum 64 MiB, observed 29.03 MiB | rdocx 0.14.0 | macOS 26.6.2, Apple M5 Max, arm64 | release, one test thread | 1,000 one-page paragraphs with deterministic fonts | `cargo test -p rdocx --test regression_test --release a_thousand_page_document_paginates_and_renders_within_the_declared_limits -- --ignored --exact --nocapture --test-threads=1` | peak live allocation | 2026-09-19 |
| Large-document PDF throughput | minimum 1,000 pages/s, observed 60,058.0 pages/s | rdocx 0.14.0 | macOS 26.6.2, Apple M5 Max, arm64 | release, one test thread | 1,000 deterministic layout pages | `cargo test -p rdocx --test regression_test --release a_thousand_page_document_paginates_and_renders_within_the_declared_limits -- --ignored --exact --nocapture --test-threads=1` | pages per wall-clock second | 2026-09-19 |
Expand Down
Loading
Loading