Export: pin merged RTF writer and preserve empty-page breaks

Use the merged craft-libs #1467 commit for craft-rtf and its codepage dependency, leaving other shared pins unchanged. Flush pending breaks before a second break and at end of input so blank and omitted-image pages retain every boundary. Add semantic readback coverage for those cases.
This commit is contained in:
Dylan M. Taylor committed 2026-11-15 06:11:34 -05:00
1 parent a65719a622
commit e94fa524ca
4 files changed
+42 -6

No files matched your search

Generated
+2 -2
View File
@@ -1202,7 +1202,7 @@ dependencies = [
[[package]]
name = "craft-codepage"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=14b2ebb7c2dda180e026037395abbefe48109eb5#14b2ebb7c2dda180e026037395abbefe48109eb5"
source = "git+https://github.com/storytold/craft-libs?rev=f29e82f1a7a009e68312bd2e84adab72be23344d#f29e82f1a7a009e68312bd2e84adab72be23344d"
[[package]]
name = "craft-csv"
@@ -1364,7 +1364,7 @@ dependencies = [
[[package]]
name = "craft-rtf"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=14b2ebb7c2dda180e026037395abbefe48109eb5#14b2ebb7c2dda180e026037395abbefe48109eb5"
source = "git+https://github.com/storytold/craft-libs?rev=f29e82f1a7a009e68312bd2e84adab72be23344d#f29e82f1a7a009e68312bd2e84adab72be23344d"
dependencies = [
"craft-codepage",
"encoding_rs",
+1 -1
View File
@@ -37,7 +37,7 @@ craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.
craft-pdf = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "e2dc681a3", features = ["cos", "crypt", "filters"] }
# The writer the RTF export maps onto (storytold/craft-libs#793), at storytold/craft-libs#1467:
# its font and colour lookups take constant time, as PdfCraft's own writer did.
craft-rtf = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "14b2ebb7c2dda180e026037395abbefe48109eb5" }
craft-rtf = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "f29e82f1a7a009e68312bd2e84adab72be23344d" }
craft-csv = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "e2dc681a3" }
craft-xml = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "e2dc681a3" }
craft-image-codecs = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "e2dc681a3" }
+16 -3
View File
@@ -42,6 +42,12 @@ fn table(t: &Table) -> craft_rtf::Table {
craft_rtf::Table { rows }
}
/// Keep a break outside tables, including when no text follows it on its page.
fn standalone_page_break(blocks: &mut Vec<Block>) {
let run = Run { text: PAGE_BREAK.to_string(), fmt: CharFmt::default() };
blocks.push(Block::Para(Para { runs: vec![run], ..Para::default() }));
}
/// Rich Text Format: paragraphs with their sizes, bold, italic and colours, real table rows, page
/// breaks between pages (images are left out).
pub fn rtf(pages: &[Page]) -> String {
@@ -64,14 +70,21 @@ pub fn rtf(pages: &[Page]) -> String {
}
Item::Table(t) => {
if std::mem::take(&mut page_break) {
let run = Run { text: PAGE_BREAK.to_string(), fmt: CharFmt::default() };
blocks.push(Block::Para(Para { runs: vec![run], ..Para::default() }));
standalone_page_break(&mut blocks);
}
blocks.push(Block::Table(table(&t)));
}
Item::Img(_) => {}
Item::PageBreak => page_break = true,
Item::PageBreak => {
if page_break {
standalone_page_break(&mut blocks);
}
page_break = true;
}
}
}
if page_break {
standalone_page_break(&mut blocks);
}
craft_rtf::write(&Document { blocks, pictures_left_out: 0 }, "Helvetica")
}
+23
View File
@@ -413,6 +413,29 @@ fn rtf_page_breaks_stay_out_of_tables() {
assert_eq!(texts.iter().filter(|t| t.starts_with('\u{000C}')).count(), 2, "one break before each later page: {texts:?}");
}
/// Empty and image-only pages still occupy a page when pictures are omitted from RTF.
#[test]
fn rtf_preserves_breaks_for_pages_without_text() {
let mut blank = page();
blank.blocks.clear();
blank.images.clear();
blank.rules.clear();
let mut image_only = page();
image_only.blocks.clear();
for pages in [
vec![blank.clone(), blank.clone(), page()],
vec![page(), blank.clone(), blank.clone()],
vec![page(), blank.clone(), blank.clone(), table_page()],
vec![blank.clone(), blank.clone(), blank],
vec![page(), image_only.clone(), image_only.clone(), page()],
vec![page(), image_only],
] {
let r = rtf(&pages);
let breaks = rtf_paras(&r).iter().flatten().map(|run| run.0.matches(craft_rtf::PAGE_BREAK).count()).sum::<usize>();
assert_eq!(breaks, pages.len() - 1, "{r}");
}
}
/// A 3-column table with a spanning header row and a hole in the last row.
fn table_page() -> Page {
let cell = |t: &str, x: f64, y: f64, w: f64| Block {