mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 03:27:21 -06:00
M6: a paste into a full field keeps the longest part that fits, across a change of font
This commit is contained in:
@@ -156,13 +156,41 @@ impl TextBox {
|
||||
if self.fits(after) {
|
||||
return (after.to_string(), p + inserted.len());
|
||||
}
|
||||
// The most of the insertion that fits: text only grows as it gets longer, so halve.
|
||||
let (mut lo, mut hi) = (0, inserted.len());
|
||||
while lo < hi {
|
||||
let mid = lo + (hi - lo).div_ceil(2);
|
||||
if self.fits(&with(mid)) { lo = mid } else { hi = mid - 1 }
|
||||
// The longest of the insertion's first `from..=to` characters that fits, halving: in one
|
||||
// font a longer text is never narrower. `None` when even `from` doesn't fit.
|
||||
let longest = |from: usize, to: usize| -> Option<usize> {
|
||||
if from > to || !self.fits(&with(from)) {
|
||||
return None;
|
||||
}
|
||||
let (mut lo, mut hi) = (from, to);
|
||||
while lo < hi {
|
||||
let mid = lo + (hi - lo).div_ceil(2);
|
||||
if self.fits(&with(mid)) { lo = mid } else { hi = mid - 1 }
|
||||
}
|
||||
Some(lo)
|
||||
};
|
||||
// The font can change once, from the form's own to Helvetica, at the first inserted
|
||||
// character the form's font can't show: each side of that point is searched on its own.
|
||||
let n = inserted.len();
|
||||
let kept = match self.font_switch(prefix, inserted, suffix) {
|
||||
Some(t) => longest(0, t.saturating_sub(1)).into_iter().chain(longest(t, n)).max(),
|
||||
None => longest(0, n),
|
||||
}
|
||||
(with(lo), p + lo)
|
||||
.unwrap_or(0);
|
||||
(with(kept), p + kept)
|
||||
}
|
||||
|
||||
/// Where an insertion changes the font [`Self::fits`] measures in: how much of `inserted`
|
||||
/// (between `prefix` and `suffix`) first includes a character the form's own font can't show,
|
||||
/// when everything around it can be shown in that font. `None` when the font never changes.
|
||||
fn font_switch(&self, prefix: &[char], inserted: &[char], suffix: &[char]) -> Option<usize> {
|
||||
let enc = self.font.as_ref().filter(|_| !self.password)?;
|
||||
// As `fits` decides: a multi-line value is checked line by line, its breaks aside.
|
||||
let shows = |c: &char| (self.multiline && matches!(c, '\n' | '\r')) || enc.can_encode(&c.to_string());
|
||||
if !prefix.iter().chain(suffix).all(shows) {
|
||||
return None;
|
||||
}
|
||||
inserted.iter().position(|c| !shows(c)).map(|i| i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -472,6 +472,34 @@ fn an_edit_to_a_full_field_keeps_what_fits() {
|
||||
assert!(kept.starts_with("SW") && kept.ends_with('t') && tb.fits(&kept), "{kept}");
|
||||
}
|
||||
|
||||
/// When the field's own font can't show a character of a paste, the text from there on is drawn
|
||||
/// in Helvetica, which can be narrower: the longest part that fits may come after that point, and
|
||||
/// is still the one kept.
|
||||
#[test]
|
||||
fn an_edit_keeps_the_longest_part_that_fits_across_a_change_of_font() {
|
||||
// A subset font holding only A, 1 em wide: 12 pt an A at 12 pt, against Helvetica's 8 pt.
|
||||
let objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [6 0 R] >>".into(),
|
||||
"<< /Fields [6 0 R] /DA (/Wide 12 Tf 0 g) /DR << /Font << /Wide 5 0 R >> >> >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /ABCDEF+Wide /FirstChar 65 /LastChar 65 /Widths [1000] /Encoding /WinAnsiEncoding >>".into(),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (one) /DA (/Wide 12 Tf 0 g) /Rect [50 760 150 780] /P 3 0 R >>".into(),
|
||||
];
|
||||
let doc = document(&objs);
|
||||
let all = fields(&doc);
|
||||
let f = field(&all, "one");
|
||||
let tb = appearance::text_box(&doc, f, &f.widgets[0]);
|
||||
// 96 pt inside: 8 A's in the subset font, or 11 characters (8.004 pt each) once a B brings
|
||||
// Helvetica in.
|
||||
assert!(tb.fits(&"A".repeat(8)) && !tb.fits(&"A".repeat(9)));
|
||||
let pasted = format!("{}B{}", "A".repeat(10), "A".repeat(30));
|
||||
let (kept, caret) = tb.fitted_edit("", &pasted);
|
||||
assert_eq!(kept, format!("{}B", "A".repeat(10)), "the 11 characters that fit in Helvetica, not the 8 before the B");
|
||||
assert!(!tb.fits(&format!("{}BA", "A".repeat(10))), "and no more");
|
||||
assert_eq!(caret, 11);
|
||||
}
|
||||
|
||||
/// Issue #1323: a text field without a format keeps a whitespace-only value as typed; only an
|
||||
/// empty value clears it.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user