mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 18:59:48 +00:00
Edit text: do not group or join by an empty font name (#3661)
Review follow-up. `Metrics::from_dict` leaves `base_font` empty when a font has no `/BaseFont`, and a bare subset tag such as `/ABCDEF+` left an empty name after stripping. Two consequences: - `same_style` split one paragraph into separate reading blocks for the exporters when both names were empty, though they grouped before subset joining; - `/ABCDEF+` and `/GHIJKL+` compared equal after stripping, so unrelated malformed fonts could join into one line. `same_base_font` now strips both tags first and requires a non-empty, equal name, so a missing name or a bare tag matches nothing. `same_style` keeps grouping lines whose fonts both lack a name, since the resource or fallback face `same_font` already matched is their whole identity. Tests: a page in a font without `/BaseFont` still reads as one block (it split into two before this change), and a bare tag matches no other name.
This commit is contained in:
@@ -883,7 +883,7 @@ fn text_page(content: &str) -> Document {
|
||||
let objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents 4 0 R /Resources << /Font << /F1 5 0 R /F2 6 0 R /F3 7 0 R /F4 8 0 R >> >> >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents 4 0 R /Resources << /Font << /F1 5 0 R /F2 6 0 R /F3 7 0 R /F4 8 0 R /F5 9 0 R >> >> >>".into(),
|
||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
// Subset: glyphs for a (97) and b (98) only.
|
||||
@@ -894,6 +894,8 @@ fn text_page(content: &str) -> Document {
|
||||
.into(),
|
||||
// Helvetica again, in a second resource.
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
// No `/BaseFont`: `Metrics::from_dict` leaves its base-font name empty.
|
||||
"<< /Type /Font /Subtype /Type1 /Encoding /WinAnsiEncoding >>".into(),
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offs = Vec::new();
|
||||
@@ -1760,6 +1762,22 @@ fn different_fonts_start_a_new_line() {
|
||||
assert_eq!(text::text_lines(&doc, 0).unwrap().len(), 2);
|
||||
}
|
||||
|
||||
/// A font without `/BaseFont` has no name to compare. Its lines still group by resource, as they
|
||||
/// did before subset joining: an empty name must not split one paragraph into two blocks.
|
||||
#[test]
|
||||
fn lines_in_a_font_without_a_base_font_stay_one_block() {
|
||||
let doc = text_page("BT /F5 12 Tf 72 700 Td (First) Tj 0 -15 Td (second) Tj ET");
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert!(lines.iter().all(|l| l.base_font.is_empty()), "{:?}", lines.iter().map(|l| &l.base_font).collect::<Vec<_>>());
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
assert_eq!(blocks.len(), 1);
|
||||
assert_eq!(blocks[0].text, "First second");
|
||||
let reading = text::reading_blocks(&doc, 0).unwrap();
|
||||
assert_eq!(reading.len(), 1, "{:?}", reading.iter().map(|b| &b.text).collect::<Vec<_>>());
|
||||
assert_eq!(reading[0].text, "First second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_lines_are_found_and_replaced_in_place() {
|
||||
let mut doc =
|
||||
|
||||
+15
-1
@@ -1466,8 +1466,10 @@ fn same_font(line: &TextLine, font: &[u8], base_font: &str, face: Option<&str>)
|
||||
|
||||
/// Whether two `/BaseFont` names name the same font: a subset's six-letter tag and `+` prefix do
|
||||
/// not change the font (ISO 32000-2 §9.6.4), so `ABCDEF+Arial` and `GHIJKL+Arial` are one font.
|
||||
/// A name that is missing, or only a tag (`ABCDEF+`), names no font and matches nothing.
|
||||
fn same_base_font(a: &str, b: &str) -> bool {
|
||||
!a.is_empty() && (a == b || strip_subset_tag(a) == strip_subset_tag(b))
|
||||
let (a, b) = (strip_subset_tag(a), strip_subset_tag(b));
|
||||
!a.is_empty() && a == b
|
||||
}
|
||||
|
||||
/// `ABCDEF+Arial` → `Arial`. A name without a subset tag is returned unchanged.
|
||||
@@ -2804,6 +2806,18 @@ pub(crate) fn translate_blocks(doc: &mut Document, page: usize, indexes: &[usize
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_subset_tag_is_the_only_difference_between_subsets_of_one_font() {
|
||||
assert!(same_base_font("ABCDEF+Arial", "GHIJKL+Arial"));
|
||||
assert!(same_base_font("Arial", "ABCDEF+Arial"));
|
||||
assert!(same_base_font("Arial", "Arial"));
|
||||
assert!(!same_base_font("ABCDEF+Arial", "GHIJKL+Helvetica"));
|
||||
// A tag with no name after it, or no name at all, names no font.
|
||||
assert!(!same_base_font("ABCDEF+", "GHIJKL+"));
|
||||
assert!(!same_base_font("", ""));
|
||||
assert!(!same_base_font("Arial", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_wrapping_keeps_ideographic_spaces() {
|
||||
let width = |s: &str| s.chars().count() as f64;
|
||||
|
||||
@@ -52,7 +52,10 @@ fn invisible(line: &TextLine) -> bool {
|
||||
|
||||
fn same_style(a: &TextLine, b: &TextLine) -> bool {
|
||||
same_font(a, b.font.as_bytes(), &b.base_font, b.face.as_deref())
|
||||
&& same_base_font(&a.base_font, &b.base_font)
|
||||
// With no `/BaseFont` (or only a subset tag) there is no name to compare: the resource or
|
||||
// fallback face `same_font` matched is the whole identity, so keep the lines together.
|
||||
&& (same_base_font(&a.base_font, &b.base_font)
|
||||
|| (strip_subset_tag(&a.base_font).is_empty() && strip_subset_tag(&b.base_font).is_empty()))
|
||||
&& a.bold == b.bold
|
||||
&& a.italic == b.italic
|
||||
&& a.color == b.color
|
||||
|
||||
Reference in New Issue
Block a user