mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 13:37:35 -06:00
Rewriting a paragraph in one style split its text at white space and joined the words with one space, so a run of spaces, an indent or a trailing space typed in the editor was saved as one space or none. The text is now kept as typed (other white space becomes a space, and CR or CRLF become line breaks), and the wrapper keeps each run of spaces, dropping only the spaces where a row wraps. A justified (stretched) row still drops its indent and the spaces after its last word, as before, so word spacing reaches only the spaces between its words; keeping an indent on a justified row is a possible follow-up. Centred and right-aligned rows, and the editor's draft box, are measured to the last word, and a paragraph at 0 Tz is not justified. Fixes #4199 Fixes #3797
This commit is contained in:
@@ -6571,3 +6571,158 @@ fn a_flatten_refused_on_a_later_page_changes_nothing() {
|
||||
assert!(matches!(flatten(&mut doc, &[0, 1], true, false), Err(EditError::Invalid(_))));
|
||||
assert_eq!((*doc.get(first)).clone(), before, "the first page keeps its comment and content");
|
||||
}
|
||||
|
||||
/// #4199, #3797: every space typed into a paragraph is saved, not folded into one.
|
||||
#[test]
|
||||
fn an_edited_paragraph_keeps_every_space_typed() {
|
||||
for typed in ["Hello world", "Hello world end", " indented and trailing "] {
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello world) Tj ET");
|
||||
assert_eq!(text::replace_block(&mut doc, 0, 0, typed).unwrap().substituted, None);
|
||||
let doc = reopen(&doc);
|
||||
let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned();
|
||||
assert!(content.contains(&format!("({typed}) Tj")), "{typed:?}: {content}");
|
||||
assert_eq!(text::text_lines(&doc, 0).unwrap()[0].text, typed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spaces after a row's last word are kept but take no part in its alignment: a justified row
|
||||
/// still reaches the margin, and a centred or right-aligned one sits where it would without them.
|
||||
#[test]
|
||||
fn spaces_after_a_rows_last_word_do_not_move_its_alignment() {
|
||||
let shown = |text: &str, align: added::Align| {
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello world) Tj ET");
|
||||
let style =
|
||||
text::BlockStyle { family: Some((added::Family::Helvetica, false, false)), align: Some(align), width: Some(300.0), ..Default::default() };
|
||||
text::rewrite_block(&mut doc, 0, 0, Some(text), &style).unwrap();
|
||||
String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned()
|
||||
};
|
||||
for (plain, trailing, align) in [
|
||||
("aaa bbb ccc\nddd eee", "aaa bbb ccc \nddd eee", added::Align::Justify),
|
||||
("Title", "Title ", added::Align::Center),
|
||||
("Title", "Title ", added::Align::Right),
|
||||
] {
|
||||
let first = trailing.split('\n').next().unwrap();
|
||||
let expected = shown(plain, align);
|
||||
let actual = shown(trailing, align).replace(&format!("({first})"), &format!("({})", first.trim_end()));
|
||||
assert_eq!(actual, expected, "{trailing:?} {align:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A justified row runs from the paragraph's left edge to the margin. Word spacing reaches only
|
||||
/// the spaces between its words: the spaces before its first word and after its last are left
|
||||
/// out of a stretched row (as before #4199), so the reopened rows stay inside the paragraph and
|
||||
/// can be edited again in place.
|
||||
#[test]
|
||||
fn a_justified_row_keeps_its_indent_and_ends_at_the_margin() {
|
||||
let style = text::BlockStyle {
|
||||
family: Some((added::Family::Helvetica, false, false)),
|
||||
align: Some(added::Align::Justify),
|
||||
width: Some(300.0),
|
||||
..Default::default()
|
||||
};
|
||||
for typed in [" Pack my box with five dozen jugs\nof liquor", " Pack my box with five dozen jugs \nof liquor"] {
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello world) Tj ET");
|
||||
text::rewrite_block(&mut doc, 0, 0, Some(typed), &style).unwrap();
|
||||
let mut doc = reopen(&doc);
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
let first = lines.iter().find(|l| l.text.contains("Pack")).unwrap();
|
||||
assert_eq!(first.text, "Pack my box with five dozen jugs", "{typed:?}");
|
||||
assert!((first.rect[0] - 72.0).abs() < 0.5, "{typed:?}: {:?}", first.rect);
|
||||
assert!(lines.iter().all(|l| l.rect[0] >= 72.0 - 0.5 && l.rect[2] <= 372.5), "{typed:?}: {lines:?}");
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
assert_eq!(blocks.len(), 1, "{typed:?}: {blocks:?}");
|
||||
text::replace_block(&mut doc, 0, 0, "Edited again").unwrap();
|
||||
let again = text::text_lines(&reopen(&doc), 0).unwrap();
|
||||
assert!(again.iter().all(|l| l.rect[0] >= 0.0 && l.rect[2] <= 600.0), "{typed:?}: {again:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A justified paragraph whose first row was typed with an indent is edited again through the
|
||||
/// styled path (a draft with a line break): its rows keep their scaling and stay one paragraph
|
||||
/// inside its width.
|
||||
#[test]
|
||||
fn a_justified_paragraph_with_an_indent_edits_again_at_its_own_scale() {
|
||||
let style = text::BlockStyle {
|
||||
family: Some((added::Family::Helvetica, false, false)),
|
||||
align: Some(added::Align::Justify),
|
||||
width: Some(300.0),
|
||||
..Default::default()
|
||||
};
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello world) Tj ET");
|
||||
text::rewrite_block(&mut doc, 0, 0, Some(" aaa bbb ccc \nddd"), &style).unwrap();
|
||||
let mut doc = reopen(&doc);
|
||||
let draft = text::text_blocks(&doc, 0).unwrap()[0].editor_text.replacen("ccc", "cc", 1);
|
||||
assert!(draft.contains('\n'), "{draft:?}");
|
||||
text::replace_block(&mut doc, 0, 0, &draft).unwrap();
|
||||
let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned();
|
||||
let scales: Vec<&str> = content.lines().filter(|l| l.ends_with(" Tz")).collect();
|
||||
assert!(scales.iter().all(|l| *l == "100 Tz"), "{scales:?}: {content}");
|
||||
let doc = reopen(&doc);
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
assert_eq!(blocks.len(), 1, "{blocks:?}");
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
assert!(lines.iter().all(|l| l.rect[0] >= 72.0 - 0.5 && l.rect[2] <= 372.5), "{lines:?}");
|
||||
}
|
||||
|
||||
/// A paragraph drawn at `0 Tz` has no width to justify into: nothing is divided by it. The
|
||||
/// writer writes a number that isn't finite as 0, so the test looks for the word spacing and
|
||||
/// the move a division by zero would add, rather than for `NaN`.
|
||||
#[test]
|
||||
fn justifying_text_at_zero_horizontal_scaling_divides_by_nothing() {
|
||||
let mut doc = text_page("BT /F1 12 Tf 0 Tz 72 700 Td (Hello world) Tj ET");
|
||||
let style = text::BlockStyle { family: Some((added::Family::Helvetica, false, false)), align: Some(added::Align::Justify), ..Default::default() };
|
||||
text::rewrite_block(&mut doc, 0, 0, Some(" aaa bbb\nccc"), &style).unwrap();
|
||||
let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned();
|
||||
let block = content.split("ET").next().unwrap();
|
||||
assert_eq!(block.matches(" Tw").count(), 1, "only the paragraph's own 0 Tw: {content}");
|
||||
let first_row = block.split(" Tm").nth(1).and_then(|rest| rest.split(" Tj").next()).unwrap();
|
||||
assert!(!first_row.contains(" Td"), "no move before the first row: {content}");
|
||||
}
|
||||
|
||||
/// The preview's box is the box of the rows Apply writes, measured to their last glyphs.
|
||||
#[test]
|
||||
fn a_drafts_box_is_the_box_apply_draws() {
|
||||
let doc = text_page("BT /F1 12 Tf 14 TL 72 700 Td [(First) -2000 (row here )] TJ T* (Second row ) Tj ET");
|
||||
let source = text::text_blocks(&doc, 0).unwrap().swap_remove(0);
|
||||
let draft = source.editor_text.replacen("Second", "Third", 1);
|
||||
let layout = text::text_edit_layout(&doc, 0, 0, &draft, &text::BlockStyle::default()).unwrap();
|
||||
let mut applied = doc.clone();
|
||||
text::replace_block(&mut applied, 0, 0, &draft).unwrap();
|
||||
let boxed = text::text_lines(&applied, 0)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|l| {
|
||||
let end = l.ink.iter().map(|r| r[2]).fold(f64::NEG_INFINITY, f64::max);
|
||||
[l.rect[0], l.rect[1], if end.is_finite() { end.min(l.rect[2]) } else { l.rect[2] }, l.rect[3]]
|
||||
})
|
||||
.reduce(|a, r| [a[0].min(r[0]), a[1].min(r[1]), a[2].max(r[2]), a[3].max(r[3])]);
|
||||
let (d, b) = (layout.draft_rect.unwrap(), boxed.unwrap());
|
||||
assert!(d.iter().zip(b).all(|(x, y)| (x - y).abs() < 0.01), "{draft:?}: preview {d:?}, applied {b:?}");
|
||||
}
|
||||
|
||||
/// The draft's box is measured to its last glyph, not over the spaces typed after it.
|
||||
#[test]
|
||||
fn a_drafts_box_leaves_out_spaces_after_the_last_word() {
|
||||
let doc = text_page("BT /F1 12 Tf 72 700 Td (Hello world) Tj ET");
|
||||
let layout = |text: &str| text::text_edit_layout(&doc, 0, 0, text, &text::BlockStyle::default()).unwrap().draft_rect;
|
||||
assert_eq!(layout("Hello there "), layout("Hello there"));
|
||||
}
|
||||
|
||||
/// A lone carriage return (an old Mac line ending) breaks the row, as `\n` and `\r\n` do.
|
||||
#[test]
|
||||
fn a_lone_carriage_return_breaks_an_edited_paragraph() {
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello world) Tj ET");
|
||||
text::replace_block(&mut doc, 0, 0, "one\rtwo").unwrap();
|
||||
let lines: Vec<String> = text::text_lines(&reopen(&doc), 0).unwrap().into_iter().map(|l| l.text).collect();
|
||||
assert_eq!(lines, ["one", "two"]);
|
||||
}
|
||||
|
||||
/// A subset with no space glyph shows each space as a positioning gap one space wide, so a run
|
||||
/// of spaces still takes a run's width (#4199).
|
||||
#[test]
|
||||
fn a_run_of_spaces_in_a_subset_without_a_space_glyph_keeps_its_width() {
|
||||
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj /F1 12 Tf ( ) Tj /F2 12 Tf (ba) Tj ET");
|
||||
assert_eq!(text::replace_block(&mut doc, 0, 0, "ab ba ab").unwrap().substituted, None);
|
||||
let content = String::from_utf8_lossy(&page_content_bytes(&reopen(&doc), 0)).into_owned();
|
||||
assert_eq!(content.matches("[-330] TJ").count(), 5, "{content}");
|
||||
}
|
||||
|
||||
+87
-26
@@ -204,7 +204,17 @@ fn layout_draft(
|
||||
*count -= 1;
|
||||
continue;
|
||||
}
|
||||
rect = Some(rect.map_or(line.rect, |r| [r[0].min(line.rect[0]), r[1].min(line.rect[1]), r[2].max(line.rect[2]), r[3].max(line.rect[3])]));
|
||||
// Spaces typed after a row's last word are drawn but show nothing, so an upright row is
|
||||
// measured to the end of its last word: they don't make a draft overflow (#4199).
|
||||
let mut r = line.rect;
|
||||
if line.upright() && !line.ink.is_empty() {
|
||||
if crate::bidi::rtl_base(&line.text) {
|
||||
r[0] = r[0].max(line.ink.iter().map(|i| i[0]).fold(f64::INFINITY, f64::min));
|
||||
} else {
|
||||
r[2] = r[2].min(line.ink.iter().map(|i| i[2]).fold(f64::NEG_INFINITY, f64::max));
|
||||
}
|
||||
}
|
||||
rect = Some(rect.map_or(r, |a| [a[0].min(r[0]), a[1].min(r[1]), a[2].max(r[2]), a[3].max(r[3])]));
|
||||
}
|
||||
let overflow = rect
|
||||
.is_some_and(|r| r[0] < source.rect[0] - 0.5 || r[1] < source.rect[1] - 0.5 || r[2] > source.rect[2] + 0.5 || r[3] > source.rect[3] + 0.5);
|
||||
@@ -2565,20 +2575,28 @@ fn paragraph_separator(c: char) -> bool {
|
||||
|
||||
/// Greedy word wrapping to `width` with `advance` giving a string's width. Each `\n` starts a
|
||||
/// new line. A row may also end inside a word of Chinese or Japanese text ([`crate::linebreak`]);
|
||||
/// those pieces join without a space.
|
||||
/// those pieces join without a space. Every white-space character is written as one space, so
|
||||
/// a run of spaces typed between, before or after words stays (#4199); only the spaces where a
|
||||
/// row wraps are dropped.
|
||||
fn wrap(text: &str, width: f64, advance: impl Fn(&str) -> f64) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for row in text.split('\n') {
|
||||
let mut line = String::new();
|
||||
let start = out.len();
|
||||
for word in row.split(paragraph_separator).filter(|word| !word.is_empty()) {
|
||||
// The separators read since the last word: each one boundary between `split` pieces.
|
||||
let mut gap = 0usize;
|
||||
for (k, word) in row.split(paragraph_separator).enumerate() {
|
||||
if k > 0 {
|
||||
gap = gap.saturating_add(1);
|
||||
}
|
||||
if word.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let spaces = " ".repeat(std::mem::take(&mut gap));
|
||||
for (i, piece) in crate::linebreak::pieces(word).into_iter().enumerate() {
|
||||
let candidate = match (line.is_empty(), i) {
|
||||
(true, _) => piece.to_string(),
|
||||
(false, 0) => format!("{line} {piece}"),
|
||||
(false, _) => format!("{line}{piece}"),
|
||||
};
|
||||
if !line.is_empty() && advance(&candidate) > width {
|
||||
let candidate = if i == 0 { format!("{line}{spaces}{piece}") } else { format!("{line}{piece}") };
|
||||
// A row holding only the spaces that indent it doesn't wrap before its first word.
|
||||
if line.chars().any(|c| c != ' ') && advance(&candidate) > width {
|
||||
out.push(std::mem::take(&mut line));
|
||||
line = piece.to_string();
|
||||
} else {
|
||||
@@ -2586,6 +2604,7 @@ fn wrap(text: &str, width: f64, advance: impl Fn(&str) -> f64) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
line.push_str(&" ".repeat(gap));
|
||||
if !line.is_empty() || out.len() == start {
|
||||
out.push(line);
|
||||
}
|
||||
@@ -2722,13 +2741,19 @@ fn rewrite_block_from(
|
||||
let streams = content_streams(doc, &p.dict)?;
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let ops = pieces.parse().ops;
|
||||
// Explicit line breaks stay (#1443); within a line, runs of white space become one space.
|
||||
let text = text
|
||||
// Explicit line breaks stay (#1443), and so does each typed U+0020 space: a run of them is
|
||||
// not folded into one (#4199, #3797). `\r\n` and a lone `\r` are breaks too; other white
|
||||
// space is written as one space.
|
||||
let text: String = text
|
||||
.unwrap_or(&b.text)
|
||||
.split('\n')
|
||||
.map(|row| row.split(paragraph_separator).filter(|word| !word.is_empty()).collect::<Vec<_>>().join(" "))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
.replace("\r\n", "\n")
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'\r' => '\n',
|
||||
c if c != '\n' && paragraph_separator(c) => ' ',
|
||||
c => c,
|
||||
})
|
||||
.collect();
|
||||
// The characters to show, for choosing fonts: the breaks are moves, not glyphs.
|
||||
let flat = text.replace('\n', " ");
|
||||
let o = &first.origin;
|
||||
@@ -2926,12 +2951,13 @@ fn rewrite_block_from(
|
||||
block_ops.extend(state.ops());
|
||||
block_ops.push(Op::new("Tm", tm.iter().map(|v| n(*v)).collect()));
|
||||
// Alignment: each line's offset from the left edge, in text space.
|
||||
let offset = |line: &str| -> f64 {
|
||||
let free = (width - advance(line)).max(0.0) / k;
|
||||
// `visible` is the width the line is aligned on (see the loop below).
|
||||
let offset = |visible: f64| -> f64 {
|
||||
let free = (width - visible).max(0.0) / k;
|
||||
match align {
|
||||
Some(crate::added::Align::Center) => free / 2.0,
|
||||
// A right-to-left line anchored at its right edge may start left of the old box.
|
||||
Some(crate::added::Align::Right) if anchored_right => (right_edge - advance(line)) / k,
|
||||
Some(crate::added::Align::Right) if anchored_right => (right_edge - visible) / k,
|
||||
Some(crate::added::Align::Right) => free,
|
||||
// A justified right-to-left paragraph ends its last line at the right edge.
|
||||
Some(crate::added::Align::Justify) if rtl => free,
|
||||
@@ -2951,11 +2977,29 @@ fn rewrite_block_from(
|
||||
let mut tw_set = 0.0;
|
||||
let mut underlines: Vec<(f64, f64, f64)> = Vec::new(); // (x0, x1, y) in text space
|
||||
for (i, line) in wrapped.iter().enumerate() {
|
||||
let spaces = line.matches(' ').count();
|
||||
let tw =
|
||||
if justify && i + 1 < wrapped.len() && spaces > 0 { (width - advance(line)).max(0.0) / k / o_state.scale / spaces as f64 } else { 0.0 };
|
||||
// A stretched line fills the width from the left edge.
|
||||
let dx = if tw > 0.0 { 0.0 } else { offset(line) };
|
||||
// A stretched (justified) line shows only its words, as before #4199: word spacing would
|
||||
// reach the spaces before its first word and after its last, so they are left out, and
|
||||
// the line fills the width from the left edge. Nothing to stretch over, or a paragraph
|
||||
// drawn at `0 Tz`, isn't justified.
|
||||
let body = line.trim_matches(' ');
|
||||
let interior = body.matches(' ').count();
|
||||
let tw = if justify && i + 1 < wrapped.len() && interior > 0 && o_state.scale.abs() > 1e-6 {
|
||||
Some((width - advance(body)).max(0.0) / k / o_state.scale / interior as f64).filter(|tw| tw.is_finite())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.unwrap_or(0.0);
|
||||
// Other lines show every space typed. A left-to-right one is aligned on its width up to
|
||||
// its last word; a right-to-left one on its whole width, since the spaces typed after its
|
||||
// last word are drawn on its left and the ones before its first on its right.
|
||||
let (shown, visible) = if tw > 0.0 {
|
||||
(body, advance(body))
|
||||
} else if crate::bidi::rtl_base(line) {
|
||||
(line.as_str(), advance(line))
|
||||
} else {
|
||||
(line.as_str(), advance(line.trim_end_matches(' ')))
|
||||
};
|
||||
let dx = if tw > 0.0 { 0.0 } else { offset(visible) };
|
||||
if i > 0 || dx != 0.0 {
|
||||
block_ops.push(Op::new("Td", vec![n(dx - x), n(if i > 0 { -lead } else { 0.0 })]));
|
||||
}
|
||||
@@ -2967,7 +3011,7 @@ fn rewrite_block_from(
|
||||
tw_set = tw;
|
||||
}
|
||||
// Each wrapped line in typing order; drawn left to right (right-to-left runs reversed).
|
||||
let visual = crate::bidi::to_visual(line);
|
||||
let visual = crate::bidi::to_visual(shown);
|
||||
// Stretched without word spacing: shown up to and including each space, then moved on.
|
||||
let pieces = if stretch > 0.0 { visual.split_inclusive(' ').collect() } else { vec![visual.as_str()] };
|
||||
for piece in pieces {
|
||||
@@ -2988,7 +3032,8 @@ fn rewrite_block_from(
|
||||
}
|
||||
}
|
||||
let tw = tw + stretch;
|
||||
let w = (advance(line) + tw * spaces as f64 * o_state.scale * k) / k;
|
||||
// Underlined to the line's last word, over its stretched gaps.
|
||||
let w = (visible + tw * interior as f64 * o_state.scale * k) / k;
|
||||
underlines.push((dx, dx + w, -(i as f64) * lead - size * 0.12));
|
||||
}
|
||||
block_ops.push(Op::new("ET", vec![]));
|
||||
@@ -3354,7 +3399,23 @@ mod tests {
|
||||
assert_eq!(wrap("A B C", 4.0, width), ["A B", "C"]);
|
||||
assert_eq!(wrap("A\u{a0}B C", 3.0, width), ["A B", "C"]);
|
||||
assert_eq!(wrap("A\u{2028}B C", 3.0, width), ["A B", "C"]);
|
||||
assert_eq!(wrap(" \tA B\r\nC ", 3.0, width), ["A B", "C"]);
|
||||
}
|
||||
|
||||
/// #4199: a run of spaces stays a run, before, between and after words; the spaces where a
|
||||
/// row wraps go, and each other white-space character is one space.
|
||||
#[test]
|
||||
fn paragraph_wrapping_keeps_every_space_typed() {
|
||||
let width = |s: &str| s.chars().count() as f64;
|
||||
assert_eq!(wrap("a b c", 20.0, width), ["a b c"]);
|
||||
assert_eq!(wrap(" a b ", 20.0, width), [" a b "]);
|
||||
assert_eq!(wrap("a\t\tb", 20.0, width), ["a b"]);
|
||||
assert_eq!(wrap("aa bb cc", 6.0, width), ["aa bb", "cc"]);
|
||||
// An indent alone on a row doesn't wrap before its first word.
|
||||
assert_eq!(wrap(" abc", 3.0, width), [" abc"]);
|
||||
// The rewrite turns `\r\n` and `\r` into `\n` before wrapping.
|
||||
assert_eq!(wrap(" \tA B\nC ", 3.0, width), [" A", "B", "C "]);
|
||||
assert_eq!(wrap(" ", 3.0, width), [" "]);
|
||||
assert_eq!(wrap("", 3.0, width), [""]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6821,6 +6821,18 @@ fn text_blocks_stay_cached_after_an_edit_and_follow_undo_and_redo() {
|
||||
assert!(Arc::ptr_eq(&redone, &blocks(&session)));
|
||||
}
|
||||
|
||||
/// #4199, #3797: the spaces typed into a paragraph are all there after saving and reopening.
|
||||
#[test]
|
||||
fn edited_text_keeps_every_space_typed_through_save_and_reopen() {
|
||||
let (mut s, id) = session_with(1);
|
||||
let typed = " A b c";
|
||||
s.apply(id, Edit::EditTextBlock { page: 0, block: 0, text: typed.into(), style: pdfcraft_edit::BlockStyle::default() }).unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
let reopened = s.open("saved.pdf", None, saved, None).unwrap();
|
||||
let blocks = s.get(reopened).unwrap().editor.as_ref().unwrap().text_blocks(0).unwrap();
|
||||
assert_eq!(blocks[0].editor_text, typed);
|
||||
}
|
||||
|
||||
/// A new document, or one restored from recovery, has never been written where it belongs: undo
|
||||
/// back to the bytes it was opened from must not make it clean, or closing it would discard the
|
||||
/// whole document without asking. Saving makes it clean; Revert of a saved one too.
|
||||
|
||||
Reference in New Issue
Block a user