Skip to content
Closed
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,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: rdocx | 4,105,912 compressed bytes, 54,118,469 member bytes, 37 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |
| Crates.io archive: rdocx | 4,107,757 compressed bytes, 54,123,669 member bytes, 37 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |
| 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
2 changes: 1 addition & 1 deletion crates/oxml-opc/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-opc | 117,614 compressed bytes, 477,731 member bytes, 12 members | 0.14.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `oxml-opc` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |
| Crates.io archive: oxml-opc | 117,674 compressed bytes, 478,067 member bytes, 12 members | 0.14.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `oxml-opc` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |

## Use it when

Expand Down
7 changes: 7 additions & 0 deletions crates/oxml-opc/src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,13 @@ impl OpcPackage {
self.parts.get(key).map(|data| data.as_slice())
}

/// The shared bytes of the part [`Self::get_part`] finds. A clone of the
/// package shares them, so `Arc::ptr_eq` proves the bytes are unchanged.
pub fn get_part_shared(&self, part_name: &str) -> Option<&Arc<Vec<u8>>> {
let key = deterministic_part_key(&self.parts, part_name)?;
self.parts.get(key)
}

/// Return whether a case-equivalent normalized part name exists.
pub fn contains_part(&self, part_name: &str) -> bool {
deterministic_part_key(&self.parts, part_name).is_some()
Expand Down
2 changes: 1 addition & 1 deletion crates/rdocx-oxml/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ schema order, and retains unmodelled XML alongside typed edits.

| Measurement | Value | Version | Platform | Build mode | Input | Command | Statistic | Measured on |
|---|---|---|---|---|---|---|---|---|
| Crates.io archive: rdocx-oxml | 565,559 compressed bytes, 3,484,893 member bytes, 32 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx-oxml` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |
| Crates.io archive: rdocx-oxml | 565,622 compressed bytes, 3,485,061 member bytes, 32 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx-oxml` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |

## Use it when

Expand Down
9 changes: 5 additions & 4 deletions crates/rdocx-oxml/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3669,15 +3669,16 @@ impl CT_Body {

/// Get an iterator over only the tables.
pub fn tables(&self) -> impl Iterator<Item = &CT_Tbl> {
let mut tables = Vec::new();
for content in &self.content {
// Lazy over the body children, so `nth` stops at the table it finds.
self.content.iter().flat_map(|content| {
let mut tables = Vec::new();
match content {
BodyContent::Table(table) => tables.push(table.as_ref()),
BodyContent::ContentControl(sdt) => sdt.collect_tables(SdtOwner::Body, &mut tables),
BodyContent::Paragraph(_) | BodyContent::RawXml(_) => {}
}
}
tables.into_iter()
tables
})
}

/// Get a mutable iterator over only the tables.
Expand Down
3 changes: 2 additions & 1 deletion crates/rdocx-oxml/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2012,7 +2012,8 @@ impl CT_TcPr {
Ok(())
}

fn is_empty(&self) -> bool {
/// Whether nothing would be written, so the cell has no `w:tcPr` element.
pub fn is_empty(&self) -> bool {
self.width.is_none()
&& self.grid_span.is_none()
&& self.h_merge.is_none()
Expand Down
72 changes: 72 additions & 0 deletions crates/rdocx-py/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4576,6 +4576,22 @@ impl WordOwnerIndexes {
self.by_route.retain(|_, bucket| !bucket.is_empty());
}

/// Move owner `index` from the route bucket `from` to the bucket `to`,
/// after its route moved in place. A bucket keeps its owners in order.
fn move_route(&mut self, index: usize, from: u64, to: u64) {
if from == to {
return;
}
if let Some(bucket) = self.by_route.get_mut(&from) {
bucket.retain(|owner| *owner != index);
if bucket.is_empty() {
self.by_route.remove(&from);
}
}
let bucket = self.by_route.entry(to).or_default();
bucket.insert(bucket.partition_point(|owner| *owner < index), index);
}

/// Index every owner, and return the first body table owner of each
/// direct body slot.
fn index_owners(
Expand Down Expand Up @@ -10283,6 +10299,62 @@ impl PyDocument {
Some(self.owners[index].location.clone())
}

/// The physical story of a direct body table cell, from the owner index.
pub(crate) fn indexed_body_cell_story(
&self,
table: usize,
row: usize,
cell: usize,
) -> Option<rdocx::StoryId> {
let table = self.indexed_table_location(table, &None, None)?;
match self.indexed_cell_location(&table, row, cell)? {
WordOwnerLocation::Cell { story, .. } => Some(story),
_ => None,
}
}

/// Move every owner route that starts in the cell story `story` by
/// `delta` items, after an in-place cell property edit wrote its first
/// `w:tcPr` or removed it. That element comes before the cell content,
/// so nothing else moves, and only the route indexes change.
pub(crate) fn move_cell_story_items(&mut self, story: &rdocx::StoryId, delta: isize) {
let in_story = |owner: &rdocx::StoryId| {
owner.kind() == story.kind()
&& owner.part_name() == story.part_name()
&& owner.owner_index() == story.owner_index()
};
let moved = |location: &mut rdocx::ContentLocation| {
if in_story(location.story())
&& let Some(&head) = location.index_path().first()
{
location.set_index_path_head(head.saturating_add_signed(delta));
}
};
for (index, owner) in self.owners.iter_mut().enumerate() {
if !word_owner_story(&owner.location).is_some_and(in_story) {
continue;
}
let from = owner.location.route_hash();
match &mut owner.location {
WordOwnerLocation::Paragraph(location)
| WordOwnerLocation::Run(location, _)
| WordOwnerLocation::Table(location)
| WordOwnerLocation::Hyperlink(location, _)
| WordOwnerLocation::Cell {
table: location, ..
} => moved(location),
WordOwnerLocation::Section(_) => {}
}
self.indexes
.move_route(index, from, owner.location.route_hash());
}
for locations in self.indexes.cell_paragraphs.values_mut() {
locations.iter_mut().for_each(&moved);
}
#[cfg(debug_assertions)]
self.assert_word_inventory_rebuilds();
}

pub(crate) fn capture_current_view(
&self,
state: &RevisionState,
Expand Down
24 changes: 21 additions & 3 deletions crates/rdocx-py/src/paragraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,25 @@ impl ParagraphLocation {
paragraph,
} => {
// The mutable ordinal accessor excludes controlled paragraphs.
// Resolve through the same checked physical route as the reader.
// When its last index reaches the reader's last paragraph, the
// cell has none, both ordinals name the same paragraph, and it
// is edited in place, as a body paragraph is.
{
let mut table = document.table_mut(*table).ok_or_else(|| {
rdocx::Error::Other("table index out of range".to_owned())
})?;
let mut cell = table
.cell(*row, *cell)
.ok_or_else(|| rdocx::Error::Other("cell index out of range".to_owned()))?;
let count = cell.paragraph_count();
if count > 0 && cell.paragraph_mut(count - 1).is_some() {
return edit(&mut cell.paragraph_mut(*paragraph).ok_or_else(|| {
rdocx::Error::Other("cell paragraph index out of range".to_owned())
})?);
}
}
// Otherwise resolve through the same checked physical route as
// the reader.
let location =
document.cell_paragraph_story_location(*table, *row, *cell, *paragraph)?;
document.edit_story_paragraph(&location, edit)
Expand Down Expand Up @@ -984,8 +1002,8 @@ impl PyParagraph {
py,
&candidate,
"Paragraph.text",
location
.native(&document.inner)
document
.paragraph_route(&location, None)
.map(crate::document::WordPublication::ParagraphText)
.unwrap_or(crate::document::WordPublication::Conservative),
)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/rdocx-py/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl PyRun {
)?;
{
let document = self.document.borrow(py);
match paragraph.native(&document.inner) {
match document.paragraph_route(&paragraph, None) {
Ok(location) => {
let (path, owner) = document.capture_owner(
&crate::document::WordOwnerLocation::Run(location, self.run_path.clone()),
Expand Down
72 changes: 51 additions & 21 deletions crates/rdocx-py/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2085,14 +2085,41 @@ impl PyCell {
let (table, row, cell) = self.validate(py)?;
let current_related = self.current_related(py)?;
let mut document = self.document.borrow_mut(py);
let edit_cell = |table: &mut rdocx::Table<'_>| {
let mut cell = table
.cell(row, cell)
.ok_or_else(|| rdocx::Error::Other("cell index out of range".to_owned()))?;
edit(&mut cell)
};
if current_related.is_none()
&& let Some(story) = document.indexed_body_cell_story(table, row, cell)
{
// Each edit validates before it changes the cell properties, so a
// body table cell is edited in place, as a body paragraph is. Only
// writing or removing its `w:tcPr` moves the cell's items.
let location = TableLocation::Body(table);
let has_properties = |document: &rdocx::Document| {
location.read(document, |table| {
table
.cell(row, cell)
.is_some_and(|cell| cell.has_properties())
})
};
let before =
has_properties(&document.inner).map_err(|error| rdocx_to_pyerr(py, error))?;
location
.edit(&mut document.inner, edit_cell)
.map_err(|error| rdocx_to_pyerr(py, error))?;
let after =
has_properties(&document.inner).map_err(|error| rdocx_to_pyerr(py, error))?;
if before != after {
document.move_cell_story_items(&story, if after { 1 } else { -1 });
}
return Ok(());
}
let mut candidate = document.inner.clone_for_staging();
TableLocation::new(table, &current_related.clone())
.edit(&mut candidate, |table| {
let mut cell = table
.cell(row, cell)
.ok_or_else(|| rdocx::Error::Other("cell index out of range".to_owned()))?;
edit(&mut cell)
})
TableLocation::new(table, &current_related)
.edit(&mut candidate, edit_cell)
.map_err(|error| rdocx_to_pyerr(py, error))?;
document.adopt_property_edit(py, candidate, "Cell property edit")?;
Ok(())
Expand Down Expand Up @@ -2516,24 +2543,27 @@ impl PyCell {
let current_related = self.current_related(py)?;
let mut document = self.document.borrow_mut(py);
let mut candidate = document.inner.clone_for_staging();
match &current_related.clone() {
Some(_) => {
TableLocation::new(table, &current_related.clone()).edit(&mut candidate, |table| {
table
.cell(row, cell)
.ok_or_else(|| rdocx::Error::Other("cell index out of range".to_owned()))?
.try_set_text(value)
})
}
None => candidate.try_set_cell_text(table, row, cell, value),
}
.map_err(|error| rdocx_to_pyerr(py, error))?;
// The first paragraph's text is replaced as Paragraph.text replaces
// it, every anchor kept, so the publication below checks the edit
// without a staged reopen of the package.
TableLocation::new(table, &current_related)
.edit(&mut candidate, |table| {
table
.cell(row, cell)
.ok_or_else(|| rdocx::Error::Other("cell index out of range".to_owned()))?
.try_set_text(value)
})
.map_err(|error| rdocx_to_pyerr(py, error))?;
let prepared = document.prepare_word_revision(
py,
&candidate,
"Cell.text",
TableLocation::new(table, &current_related)
.native(&document.inner)
document
.indexed_table_location(table, &current_related, None)
.map(Ok)
.unwrap_or_else(|| {
TableLocation::new(table, &current_related).native(&document.inner)
})
.map(|location| crate::document::WordPublication::CellText(location, row, cell))
.unwrap_or(crate::document::WordPublication::Conservative),
)?;
Expand Down
Loading
Loading