mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
This commit is contained in:
1 parent
5ee4a85876
commit
2ef2e2b822
3 files changed
+185
-1
No files matched your search
@@ -10868,6 +10868,116 @@ fn a_run_of_spaces_in_a_subset_without_a_space_glyph_keeps_its_width() {
|
||||
assert_eq!(content.matches("[-330] TJ").count(), 5, "{content}");
|
||||
}
|
||||
|
||||
/// #6131: a subset that writes width 0 for its unused space (pdfTeX does) has no space glyph. A word added to
|
||||
/// the line takes a positioning gap for its spaces, not code 32 at width 0, which would run the words together.
|
||||
#[test]
|
||||
fn a_space_in_a_subset_that_gives_it_no_width_is_a_gap_not_code_32() {
|
||||
for subtype in ["TrueType", "Type1"] {
|
||||
// Codes 32..=98: the space and everything else 0 wide, but a and b (97, 98).
|
||||
let widths: Vec<String> = (32..=98).map(|c| if c == 97 || c == 98 { "500" } else { "0" }.to_string()).collect();
|
||||
let font = format!(
|
||||
"<< /Type /Font /Subtype /{subtype} /BaseFont /ABCDEF+Sample /FirstChar 32 /LastChar 98 /Widths [{}] /Encoding /WinAnsiEncoding >>",
|
||||
widths.join(" ")
|
||||
);
|
||||
let content = "BT /F1 12 Tf 72 700 Td (ab) Tj ET";
|
||||
let objs = [
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_string(),
|
||||
format!(
|
||||
"<< /Length {} >>
|
||||
stream
|
||||
{content}
|
||||
endstream",
|
||||
content.len()
|
||||
),
|
||||
font,
|
||||
];
|
||||
let mut out = b"%PDF-1.7
|
||||
"
|
||||
.to_vec();
|
||||
let mut offs = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offs.push(out.len());
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"{} 0 obj
|
||||
{o}
|
||||
endobj
|
||||
",
|
||||
i + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
}
|
||||
let x = out.len();
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"xref
|
||||
0 {}
|
||||
0000000000 65535 f
|
||||
",
|
||||
objs.len() + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
for o in offs {
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"{o:010} 00000 n
|
||||
"
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
}
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"trailer
|
||||
<< /Size {} /Root 1 0 R >>
|
||||
startxref
|
||||
{x}
|
||||
%%EOF
|
||||
",
|
||||
objs.len() + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
let mut doc = Document::open(Arc::new(out)).unwrap();
|
||||
assert_eq!(text::replace_block(&mut doc, 0, 0, "ab ba").unwrap().substituted, None, "{subtype}");
|
||||
// Every byte the text-showing operators draw (strings inside `TJ` arrays too) and every
|
||||
// `TJ` adjustment, read from the saved content rather than matched as text.
|
||||
let ops = pdfcraft_content::parse(&page_content_bytes(&reopen(&doc), 0)).ops;
|
||||
let mut shown: Vec<u8> = Vec::new();
|
||||
let mut adjustments: Vec<f64> = Vec::new();
|
||||
// How many bytes had been drawn when each adjustment came: the gap must sit between the words.
|
||||
let mut positions: Vec<usize> = Vec::new();
|
||||
for op in ops.iter().filter(|o| matches!(o.op.as_slice(), b"Tj" | b"TJ" | b"'" | b"\"")) {
|
||||
for operand in &op.operands {
|
||||
match operand {
|
||||
Object::String(s) => shown.extend_from_slice(&s.bytes),
|
||||
Object::Array(items) => {
|
||||
for item in items {
|
||||
match item {
|
||||
Object::String(s) => shown.extend_from_slice(&s.bytes),
|
||||
number => {
|
||||
if let Some(adjustment) = number.as_f64() {
|
||||
adjustments.push(adjustment);
|
||||
positions.push(shown.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(shown, b"abba", "{subtype}: the words are drawn and no code 32 at width 0 between them");
|
||||
assert!(matches!(adjustments.as_slice(), [gap] if *gap < 0.0), "{subtype}: one adjustment moves the second word right: {adjustments:?}");
|
||||
assert_eq!(positions, [2], "{subtype}: the gap is between the words, after \"ab\"");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_exclusive_scope_resets_path_on_scope_exit() {
|
||||
// An earlier scope has a path operation closed by Q (with 'h' closepath).
|
||||
|
||||
@@ -951,7 +951,11 @@ impl Metrics {
|
||||
|| (self.outlined_cids.is_empty() && self.explicit_default);
|
||||
}
|
||||
match code.checked_sub(self.first).and_then(|i| self.widths.get(i as usize)) {
|
||||
Some(w) => *w > 0.0 || code == 32,
|
||||
// A full font keeps its space whatever `/Widths` says; a subset that gives its space no width has no
|
||||
// space glyph (pdfTeX writes 0 for the codes it never uses), and a typed space drawn as code 32 would
|
||||
// glue the words together (#6131): the edit sets it as a positioning gap instead. Only when code 32
|
||||
// is a space: another glyph a subset puts there keeps its code.
|
||||
Some(w) => *w > 0.0 || (code == 32 && !(self.subset && self.text_of(code).is_some_and(|t| t.chars().all(char::is_whitespace)))),
|
||||
None => self.widths.is_empty() && !self.subset,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +334,76 @@ fn subset_fonts_only_encode_the_glyphs_they_have() {
|
||||
assert_eq!(m.encode("abc"), None);
|
||||
}
|
||||
|
||||
/// #6131: pdfTeX writes `/FirstChar` below 32 and `/Widths` of 0 for the codes it never uses, the space
|
||||
/// among them: the subset has no space glyph, and the words are apart by `TJ` gaps. Writing a typed space as
|
||||
/// code 32 would glue the words together at width 0, so the font can't encode one and the edit sets the
|
||||
/// space as a gap instead (as for a subset whose widths stop short of 32).
|
||||
#[test]
|
||||
fn a_subset_that_gives_the_space_no_width_cannot_encode_one() {
|
||||
// pdfTeX writes Type 1 fonts; the same rule holds for a TrueType subset.
|
||||
for subtype in ["TrueType", "Type1"] {
|
||||
a_zero_width_space_in_a_subset_of(subtype);
|
||||
}
|
||||
}
|
||||
|
||||
fn a_zero_width_space_in_a_subset_of(subtype: &str) {
|
||||
let metrics = |base: &str, space: i64| {
|
||||
let mut doc = Document::new_empty();
|
||||
// Codes 32..=98: the space, then a and b (97, 98) with a width, everything else 0.
|
||||
let widths: Vec<Object> = (32..=98)
|
||||
.map(|code| {
|
||||
Object::Int(match code {
|
||||
32 => space,
|
||||
97 | 98 => 500,
|
||||
_ => 0,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let f = font(
|
||||
&mut doc,
|
||||
vec![
|
||||
("Subtype", Object::name(subtype)),
|
||||
("BaseFont", Object::name(base)),
|
||||
("FirstChar", Object::Int(32)),
|
||||
("Widths", Object::Array(widths)),
|
||||
("Encoding", Object::name("WinAnsiEncoding")),
|
||||
],
|
||||
);
|
||||
Metrics::from_dict(&doc, &f)
|
||||
};
|
||||
let subset = metrics("ABCDEF+Sample", 0);
|
||||
assert!(subset.subset);
|
||||
assert_eq!(subset.encode("ab").as_deref(), Some(&b"ab"[..]));
|
||||
assert_eq!(subset.encode("a b"), None, "no space glyph: a gap sets it");
|
||||
assert_eq!(subset.encode(" "), None);
|
||||
assert_eq!(metrics("ABCDEF+Sample", 250).encode("a b").as_deref(), Some(&b"a b"[..]), "a subset that does draw the space");
|
||||
assert_eq!(metrics("Sample", 0).encode("a b").as_deref(), Some(&b"a b"[..]), "a full font keeps its space whatever the width says");
|
||||
}
|
||||
|
||||
/// Only a *space* at code 32 is given up for a width of 0: a subset that puts another glyph there (a symbol
|
||||
/// font, say) keeps encoding it.
|
||||
#[test]
|
||||
fn a_subset_keeps_a_zero_width_glyph_that_is_not_a_space_at_code_32() {
|
||||
let mut doc = Document::new_empty();
|
||||
let widths: Vec<Object> = (32..=98).map(|code| Object::Int(if code == 97 { 500 } else { 0 })).collect();
|
||||
let mut enc = Dict::new();
|
||||
enc.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(32), Object::name("Z")]));
|
||||
let f = font(
|
||||
&mut doc,
|
||||
vec![
|
||||
("Subtype", Object::name("TrueType")),
|
||||
("BaseFont", Object::name("ABCDEF+Sample")),
|
||||
("FirstChar", Object::Int(32)),
|
||||
("Widths", Object::Array(widths)),
|
||||
("Encoding", Object::Dict(enc)),
|
||||
],
|
||||
);
|
||||
let m = Metrics::from_dict(&doc, &f);
|
||||
assert!(m.subset);
|
||||
assert_eq!(m.text_of(32).as_deref(), Some("Z"));
|
||||
assert_eq!(m.encode("Z").as_deref(), Some(&[32][..]), "code 32 is a glyph here, not a space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmap_streams_decode_within_a_size_cap() {
|
||||
// A valid CMap padded past the cap (a small Flate stream of whitespace) is not decoded whole.
|
||||
|
||||
Reference in new issue
Block a user