mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 19:19:10 -06:00
This commit is contained in:
@@ -1405,6 +1405,74 @@ fn a_subset_without_a_space_glyph_keeps_its_font_when_editing_words() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Chrome (Skia) style subset: Type0/Identity-H over a TrueType program, CID = glyph id.
|
||||
/// `/W` lists most used glyphs, writes 0 for some it dropped, and leaves `a` and `y` to `/DW`;
|
||||
/// `z` has a ToUnicode entry but no glyph in the program (its CID is past the last glyph).
|
||||
fn skia_subset_page() -> Document {
|
||||
// Inter's glyph ids: P 375, o 790, l 736, a 507, n 773, d 597, G 141, e 614, r 852, m 766, y 998, space 1777.
|
||||
let mut doc = text_page(
|
||||
"BT /F3 12 Tf 72 700 Td <0177031602E001FB03050255> Tj ET \
|
||||
BT /F3 12 Tf 72 650 Td <008D0266035402FE01FB030503E606F101FB0305025506F102FE031603540266> Tj ET",
|
||||
);
|
||||
let program = doc.add(Object::Stream(Stream::flate(Dict::new(), include_bytes!("../../../assets/fonts/Inter-Regular.ttf"))));
|
||||
let unicode = doc.add(Object::Stream(Stream::flate(
|
||||
Dict::new(),
|
||||
b"1 begincodespacerange <0000> <FFFF> endcodespacerange 13 beginbfchar <0177> <0050> <0316> <006F> <02E0> <006C> \
|
||||
<01FB> <0061> <0305> <006E> <0255> <0064> <008D> <0047> <0266> <0065> <0354> <0072> <02FE> <006D> <03E6> <0079> \
|
||||
<06F1> <0020> <1000> <007A> endbfchar",
|
||||
)));
|
||||
let widths: Vec<Object> = [(0x8D, 746), (0x177, 639), (0x255, 612), (0x266, 583), (0x2E0, 242), (0x2FE, 876), (0x305, 591), (0x316, 600)]
|
||||
.into_iter()
|
||||
.chain([(0x354, 376), (0x355, 0), (0x356, 0), (0x6F1, 281)])
|
||||
.flat_map(|(cid, width)| [Object::Int(cid), Object::Array(vec![Object::Int(width)])])
|
||||
.collect();
|
||||
let descriptor = Dict::from_iter([
|
||||
(b"Type".to_vec(), Object::name("FontDescriptor")),
|
||||
(b"FontName".to_vec(), Object::name("BAAAAA+SampleSans")),
|
||||
(b"Flags".to_vec(), Object::Int(32)),
|
||||
(b"FontFile2".to_vec(), Object::Ref(program)),
|
||||
]);
|
||||
let cid_font = Dict::from_iter([
|
||||
(b"Type".to_vec(), Object::name("Font")),
|
||||
(b"Subtype".to_vec(), Object::name("CIDFontType2")),
|
||||
(b"BaseFont".to_vec(), Object::name("BAAAAA+SampleSans")),
|
||||
(b"CIDToGIDMap".to_vec(), Object::name("Identity")),
|
||||
(b"DW".to_vec(), Object::Int(562)),
|
||||
(b"W".to_vec(), Object::Array(widths)),
|
||||
(b"FontDescriptor".to_vec(), Object::Dict(descriptor)),
|
||||
]);
|
||||
let font = Dict::from_iter([
|
||||
(b"Type".to_vec(), Object::name("Font")),
|
||||
(b"Subtype".to_vec(), Object::name("Type0")),
|
||||
(b"BaseFont".to_vec(), Object::name("BAAAAA+SampleSans")),
|
||||
(b"Encoding".to_vec(), Object::name("Identity-H")),
|
||||
(b"DescendantFonts".to_vec(), Object::Array(vec![Object::Dict(cid_font)])),
|
||||
(b"ToUnicode".to_vec(), Object::Ref(unicode)),
|
||||
]);
|
||||
let p = pdfcraft_model::pages(&doc)[0].obj;
|
||||
let fonts = Dict::from_iter([(b"F3".to_vec(), Object::Dict(font))]);
|
||||
doc.update_dict(p, |d| d.set(b"Resources".to_vec(), Object::Dict(Dict::from_iter([(b"Font".to_vec(), Object::Dict(fonts))])))).unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_subset_keeps_its_font_for_glyphs_left_to_the_default_width() {
|
||||
let doc = skia_subset_page();
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
assert_eq!(blocks.iter().map(|b| b.text.as_str()).collect::<Vec<_>>(), ["Poland", "Germany and more"]);
|
||||
// Deleting words only, and replacing a word with one drawn elsewhere on the page (`y` and `a`
|
||||
// have no /W entry): every glyph is in the subset, so the font stays.
|
||||
for (block, text) in [(1, "Germany"), (0, "Germany")] {
|
||||
let mut doc = doc.clone();
|
||||
assert_eq!(text::replace_block(&mut doc, 0, block, text).unwrap().substituted, None, "{text}");
|
||||
let lines = text::text_lines(&reopen(&doc), 0).unwrap();
|
||||
assert_eq!(lines.iter().find(|l| l.text == text).unwrap().font, "F3");
|
||||
}
|
||||
// `z` maps to a CID the program has no glyph for: that still needs another font.
|
||||
let mut doc = doc.clone();
|
||||
assert!(text::replace_block(&mut doc, 0, 0, "Graz").unwrap().substituted.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaces_in_another_subset_do_not_split_a_line() {
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello) Tj /F2 12 Tf ( ) Tj /F1 12 Tf (world) Tj ET");
|
||||
|
||||
+45
-1
@@ -61,6 +61,9 @@ pub struct Metrics {
|
||||
code_len: usize,
|
||||
/// Valid single-character explicit mappings, preindexed once for large full CJK fonts.
|
||||
explicit_reverse: HashMap<char, u32>,
|
||||
/// Subset CIDFontType2 only: one bit per CID (below 65 536) whose glyph has an outline in the
|
||||
/// embedded program. See [`cids_with_outlines`].
|
||||
outlined_cids: Vec<u64>,
|
||||
}
|
||||
|
||||
fn nums(doc: &Document, o: Option<&Object>) -> Vec<f64> {
|
||||
@@ -129,6 +132,38 @@ pub fn embedded_sfnt(doc: &Document, font: &Dict) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The CIDs (below 65 536) of a Type 0 font over an embedded TrueType program whose glyph has
|
||||
/// outline data, through `/CIDToGIDMap`; empty when the program or the map can't be read.
|
||||
/// Subsetters keep glyph ids and empty the glyphs they drop, so an empty glyph is a missing one.
|
||||
fn cids_with_outlines(doc: &Document, font: &Dict) -> Vec<u64> {
|
||||
use skrifa::raw::TableProvider as _;
|
||||
let Some(descendant) = dict(doc, font.get(b"DescendantFonts").and_then(|o| doc.resolve(o).as_array().and_then(|a| a.first()).cloned()).as_ref())
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(program) = dict(doc, descendant.get(b"FontDescriptor")).and_then(|d| d.get(b"FontFile2").cloned()) else { return Vec::new() };
|
||||
let program = doc.resolve(&program);
|
||||
let Object::Stream(stream) = program.as_ref() else { return Vec::new() };
|
||||
let Ok(bytes) = stream.decoded_within(16 << 20) else { return Vec::new() };
|
||||
let Some(loca) = skrifa::FontRef::from_index(&bytes, 0).ok().and_then(|face| face.loca(None).ok()) else { return Vec::new() };
|
||||
let outlined = |gid: u32| loca.get_raw(gid as usize).zip(loca.get_raw(gid as usize + 1)).is_some_and(|(start, end)| end > start);
|
||||
match descendant.get(b"CIDToGIDMap").map(|o| doc.resolve(o)).as_deref() {
|
||||
None | Some(Object::Name(_)) => bit_set_below_64k((0..loca.len().min(0x1_0000) as u32).filter(|&gid| outlined(gid))),
|
||||
Some(Object::Stream(map)) => {
|
||||
let Ok(map) = map.decoded_within(1 << 17) else { return Vec::new() };
|
||||
bit_set_below_64k(
|
||||
map.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, gid)| outlined(u32::from(u16::from_be_bytes(**gid))))
|
||||
.map(|(cid, _)| cid as u32),
|
||||
)
|
||||
}
|
||||
Some(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace an existing cmap directory entry with a bounded Unicode format-12 table.
|
||||
/// Checked ranges prevent damaged sfnt offsets from indexing outside the source program.
|
||||
fn append_editor_cmap(bytes: &mut Vec<u8>, mappings: &std::collections::BTreeMap<u32, u32>) -> Option<()> {
|
||||
@@ -538,6 +573,7 @@ impl Metrics {
|
||||
visual_order: false,
|
||||
code_len: 1,
|
||||
explicit_reverse: (32..127u8).map(|c| (char::from(c), u32::from(c))).collect(),
|
||||
outlined_cids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +594,9 @@ impl Metrics {
|
||||
m.bold |= bold;
|
||||
m.italic |= italic;
|
||||
m.base_font = base;
|
||||
if m.composite && m.subset {
|
||||
m.outlined_cids = cids_with_outlines(doc, font);
|
||||
}
|
||||
m.code_len = match &m.codes {
|
||||
Codes::One => 1,
|
||||
Codes::Two => 2,
|
||||
@@ -752,7 +791,12 @@ impl Metrics {
|
||||
fn has_glyph(&self, code: u32) -> bool {
|
||||
if self.composite {
|
||||
let cid = self.cid(code);
|
||||
return self.cid_widths.contains_key(&cid) || self.cid_ranges.iter().any(|(a, b, _)| cid >= *a && cid <= *b) || !self.subset;
|
||||
// A CID /W leaves out takes /DW, and Skia (Chrome) leaves out every CID of that width: for
|
||||
// those, only the subset's program says whether the glyph is there.
|
||||
return self.cid_widths.contains_key(&cid)
|
||||
|| self.cid_ranges.iter().any(|(a, b, _)| cid >= *a && cid <= *b)
|
||||
|| !self.subset
|
||||
|| self.outlined_cids.get(cid as usize / 64).is_some_and(|w| (*w >> (cid % 64)) & 1 == 1);
|
||||
}
|
||||
match code.checked_sub(self.first).and_then(|i| self.widths.get(i as usize)) {
|
||||
Some(w) => *w > 0.0 || code == 32,
|
||||
|
||||
Reference in New Issue
Block a user