mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
Merge pull request #6663 from storytold/merge/render-interpret-patches
Land render and font patches (#6292, #6287, #6314, #6353, #6359, #6368, #6409, #6491, #6545, #6516, #6521, #6528, #6533, #6529, #6542, #6543, #6569, #6572)
This commit is contained in:
38 files changed
+4155
-229
No files matched your search
@@ -240,6 +240,10 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
## Progress log
|
||||
- **2026-10-13 (M8, redact by regex):** `redact_mark` takes `regex` (caller expression, linear-time engine, length/size/nesting caps, empty-matching refused) with optional `validate` (luhn, mod97, iban). Headless only; issue #1886.
|
||||
|
||||
- **2026-11-07 (M2, #6449):** A non-embedded Times New Roman set as a TrueType CID font (`TimesNewRoman,Bold`, `TimesNewRomanPSMT` and the other faces) draws the glyphs its codes name, as the non-embedded Arial does: its first 258 glyphs follow the same standard Macintosh order, which the embedded Times New Roman subsets of a public French form confirm in 941 of 942 `/ToUnicode` entries (vendored hayro-interpret patch 88). That form drew "Salarié(e) du régime général" as "Salari²*g+f w't²i lo g'ì²p²tcn" and "changement" as "ej angement". Times and Times New Roman MT Std are not read this way.
|
||||
|
||||
- **2026-11-05 (M2, #5889):** A non-embedded Arial set as a TrueType CID font (`CIDFontType2`, `Identity-H`, as Word and PDFMaker write it) draws the glyphs its codes name. Those codes are glyph indexes in the author's Arial, whose first 258 glyphs follow the standard Macintosh order, so the standard substitute now draws each glyph by name (vendored hayro-interpret patch 76). Before, it took each code as one of its own glyph indexes, and "Low Power" drew as "@ck 'Dck Yf". Arial Narrow, Black and Unicode MS, and `CIDFontType0` fonts, are not read this way. Copying or searching those runs still needs a `/ToUnicode`.
|
||||
|
||||
- **2026-11-07 (M7, #2964 / #3185):** Supported Form text can be prepared for direct editing; groups left grouped (unsafe compositing, text nested past eight levels) are reported. Legacy Thai keeps the original font programs, Regular/Bold and text settings, also across same-face subsets; the narrow THSarabunPSK map repair is checked glyph by glyph against the font program; cross-family replacement is explicit. Builds on #3067 and #2811. Unsafe groups and mixed contextual styles remain partial; estimates unchanged.
|
||||
- **2026-10-28 (M10, #4119 partial):** Scan & OCR ▸ Enhance scanned file cleans scanned pages in place: background removal (level 0–100, as the camera clean-up), edge shadow removal (book gutters, copier borders) and despeckle. Each scan image is decoded, cleaned and written back into its own object at the same size and colour space (JPEG stays JPEG, others lossless, 1-bit stays 1-bit), so the OCR text layer, comments and links stay; one undo step, shared images once, unsupported images (CMYK, indexed, JPEG 2000, JBIG2, CCITT, broken data) reported. `page_enhance_scan` drives it headlessly. Not yet an option of Recognize text. Estimates unchanged.
|
||||
- **2026-11-07 (M10, `ocr.ocr-web`, port onto #977):** Web OCR rebuilt on main's engines and preprocessing (#977). `OcrJob::run` is now `OcrJob::steps` a page at a time: the page renders, crops to the region and cleans each page, a Web Worker runs ocrs on the result, and `OcrShot::place` maps the words back through the region and preprocessing. Every strategy reads like this when only one engine is present, and the web build has only ocrs. The downloaded models are installed into the engine cache (`ocr::install`), so the Verify screen and the engine-status checks see them too. The batch keeps main's skipped-files bucket, low-confidence notes and cancel. The translations moved to craft-i18n (storytold/craft-i18n#141). The Verify screen starts the model download itself and runs once the models land; before, on a fresh page it failed with the desktop's "set PDFCRAFT_MODELS" message. Known limits: the cleaning steps, and the Verify screen's Run OCR and re-reads, run on the page. Estimates unchanged.
|
||||
|
||||
@@ -421,6 +421,12 @@ pub(crate) fn render(args: &[String]) -> Result<(), CliError> {
|
||||
let _ =
|
||||
writeln!(std::io::stderr().lock(), "warning: page {page}: a colour that could not be computed was painted in a fallback colour");
|
||||
}
|
||||
pdfcraft_render::RenderWarning::Type3WidthsMissing => {
|
||||
let _ = writeln!(
|
||||
std::io::stderr().lock(),
|
||||
"warning: page {page}: a Type 3 font has no /Widths; its glyphs were spaced by the widths their glyph procedures give"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -410,6 +410,7 @@ fn render_warning_text(warning: pdfcraft_render::RenderWarning) -> &'static str
|
||||
match warning {
|
||||
RenderWarning::ContentTruncated => "content past the page's safety budget was skipped",
|
||||
RenderWarning::ColourFallback => "a colour that could not be computed was painted in a fallback colour",
|
||||
RenderWarning::Type3WidthsMissing => "a Type 3 font has no /Widths; its glyphs were spaced by the widths their glyph procedures give",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+89
-2
@@ -1402,8 +1402,9 @@ fn glyph_component(part: &str) -> Option<String> {
|
||||
if let Ok(i) = crate::encodings::NAMES.binary_search_by(|(n, _)| (*n).cmp(part)) {
|
||||
return char::from_u32(crate::encodings::NAMES[i].1).map(String::from);
|
||||
}
|
||||
// `from_str_radix` accepts a leading `+`, which is not a glyph name, so the digits are checked.
|
||||
let hexish = |h: &str| !h.is_empty() && h.bytes().all(|b| b.is_ascii_hexdigit());
|
||||
// `from_str_radix` accepts a leading `+` and lowercase digits, and the glyph list
|
||||
// specification's digits are uppercase only (`uni20ac` names nothing), so they are checked.
|
||||
let hexish = |h: &str| !h.is_empty() && h.bytes().all(|b| matches!(b, b'0'..=b'9' | b'A'..=b'F'));
|
||||
if let Some(hex) = part.strip_prefix("uni").filter(|h| h.len() >= 4 && h.len() % 4 == 0 && hexish(h)) {
|
||||
let mut s = String::new();
|
||||
for i in (0..hex.len()).step_by(4) {
|
||||
@@ -1464,6 +1465,9 @@ fn unicode_map(doc: &Document, font: &Dict, composite: bool) -> Unicode {
|
||||
out.encoding.insert(code as u32, c.to_string());
|
||||
}
|
||||
}
|
||||
if base_name.is_none() && !symbolic_builtin {
|
||||
out.encoding.extend(symbolic_mac_roman_texts(doc, font));
|
||||
}
|
||||
if let Some(Object::Dict(d)) = enc.as_deref()
|
||||
&& let Some(diffs) = d.get(b"Differences").map(|x| doc.resolve(x)).and_then(|x| x.as_array().cloned())
|
||||
{
|
||||
@@ -1527,6 +1531,74 @@ fn identity_to_unicode() -> Vec<u8> {
|
||||
cmap.into_bytes()
|
||||
}
|
||||
|
||||
/// What each code of an embedded TrueType font that isn't flagged non-symbolic, names no base
|
||||
/// encoding and doesn't name its glyphs shows when two things say the code is Mac OS Roman: its
|
||||
/// Macintosh Roman (1, 0) `cmap` subtable selects the glyph the code draws (ISO 32000-2 §9.6.5.4:
|
||||
/// single bytes "look up the associated glyph descriptions" there, and a (3, 0) subtable maps "the
|
||||
/// single-byte codes in the (1, 0) subtable" prepended with 0xF0), and the glyph sits where the
|
||||
/// standard Macintosh glyph order (the 258 names of a version 1.0 `post` table) puts that Mac OS
|
||||
/// Roman character. So code 0x88 drawn with glyph 106, `agrave`, is `à`, where Standard has
|
||||
/// nothing and WinAnsi `ˆ`, while a Windows subset in the same order whose codes are WinAnsi (0x92
|
||||
/// drawn with glyph 183, `quoteright`) keeps its reading. The glyph drawn is found the way the
|
||||
/// renderer finds it: the Windows (3, 0) and (3, 1) subtables first, with the code and the code at
|
||||
/// 0xF000, 0xF100 and 0xF200, then the Macintosh and Unicode-platform ones. The renderer's text
|
||||
/// layer reads the same codes the same way (vendored hayro-interpret patch (101)).
|
||||
fn symbolic_mac_roman_texts(doc: &Document, font: &Dict) -> Vec<(u32, String)> {
|
||||
use skrifa::GlyphId;
|
||||
use skrifa::raw::{TableProvider as _, tables::cmap::PlatformId, tables::post::DEFAULT_GLYPH_NAMES, types::Tag};
|
||||
let Some(descriptor) = dict(doc, font.get(b"FontDescriptor")) else { return Vec::new() };
|
||||
// Flag bit 6, Nonsymbolic (ISO 32000-2 Table 121).
|
||||
if descriptor_flags(doc, &descriptor) & 32 != 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(object) = descriptor.get(b"FontFile2") else { return Vec::new() };
|
||||
let object = doc.resolve(object);
|
||||
let Object::Stream(stream) = object.as_ref() else { return Vec::new() };
|
||||
let Ok(bytes) = doc.with_direct_parms(stream).decoded_within(MAX_SFNT) else { return Vec::new() };
|
||||
let Ok(face) = skrifa::FontRef::from_index(&bytes, 0) else { return Vec::new() };
|
||||
// A CFF program in `/FontFile2` selects its glyphs by name, not through the `cmap`. A `post`
|
||||
// table of version 1.0 or 2.0 (also with its 16-bit halves swapped, as the renderer reads it)
|
||||
// names the glyphs, and a font that names them needn't keep the order: a Wingdings subset
|
||||
// draws code 0x83 with glyph 101, `Eacute` in the order, which it names `threesans`.
|
||||
let named = face
|
||||
.table_data(Tag::new(b"post"))
|
||||
.is_some_and(|data| matches!(data.as_bytes().get(..4), Some([0, 1, 0, 0] | [0, 2, 0, 0] | [0, 0, 0, 1] | [0, 0, 0, 2])));
|
||||
if face.cff().is_ok() || named {
|
||||
return Vec::new();
|
||||
}
|
||||
let Ok(cmap) = face.cmap() else { return Vec::new() };
|
||||
let records = cmap.encoding_records();
|
||||
let lookup = |code: u32, windows: bool| {
|
||||
records.iter().find_map(|record| {
|
||||
let offsets: &[u32] = match (record.platform_id(), record.encoding_id()) {
|
||||
(PlatformId::Windows, 0 | 1) if windows => &[0, 0xF000, 0xF100, 0xF200],
|
||||
(PlatformId::Macintosh | PlatformId::Unicode, 0) if !windows => &[0],
|
||||
_ => return None,
|
||||
};
|
||||
let subtable = record.subtable(cmap.offset_data()).ok()?;
|
||||
offsets.iter().find_map(|o| subtable.map_codepoint(code + o).filter(|g| *g != GlyphId::NOTDEF))
|
||||
})
|
||||
};
|
||||
(0..=255u32)
|
||||
.filter_map(|code| {
|
||||
let drawn = lookup(code, true).or_else(|| lookup(code, false))?;
|
||||
let mac = records.iter().find_map(|record| {
|
||||
(record.platform_id() == PlatformId::Macintosh && record.encoding_id() == 0)
|
||||
.then(|| record.subtable(cmap.offset_data()).ok()?.map_codepoint(code))
|
||||
.flatten()
|
||||
})?;
|
||||
if mac != drawn {
|
||||
return None;
|
||||
}
|
||||
let c = char::from_u32(*crate::encodings::MAC_ROMAN.get(usize::try_from(code).ok()?)?).filter(|c| *c != '\0')?;
|
||||
let standard = *DEFAULT_GLYPH_NAMES.get(usize::try_from(drawn.to_u32()).ok()?)?;
|
||||
// Mac OS Roman 0xCA reads as a space, for the reason WinAnsi 0xA0 does (`MAC_ROMAN`).
|
||||
let at_its_place = glyph_unicode(standard) == Some(c) || (code == 0xCA && standard == "nonbreakingspace");
|
||||
at_its_place.then(|| (code, c.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -1560,6 +1632,21 @@ mod encoding_tests {
|
||||
Object::Dict(enc)
|
||||
}
|
||||
|
||||
/// The glyph list specification's hex forms: `uni` and groups of exactly four uppercase
|
||||
/// digits, `u` and four to six.
|
||||
#[test]
|
||||
fn glyph_names_take_uppercase_hex_in_whole_groups() {
|
||||
assert_eq!(glyph_text("uni20ac"), None, "the specification's hex digits are uppercase");
|
||||
assert_eq!(glyph_text("u1f600"), None);
|
||||
assert_eq!(glyph_text("uni65"), None, "`uni` takes groups of exactly four digits");
|
||||
assert_eq!(glyph_text("u65"), None, "`u` takes four to six digits");
|
||||
assert_eq!(glyph_text("u0010FFFF"), None);
|
||||
assert_eq!(glyph_text("uni0065").as_deref(), Some("e"));
|
||||
assert_eq!(glyph_text("u0065").as_deref(), Some("e"), "`u` and four digits");
|
||||
assert_eq!(glyph_text("u1F600").as_deref(), Some("\u{1f600}"), "`u` and five");
|
||||
assert_eq!(glyph_text("u10FFFF").as_deref(), Some("\u{10ffff}"), "`u` and six, the last code point");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_names_follow_the_naming_algorithm() {
|
||||
// Annex D names and the two hex forms, as before.
|
||||
|
||||
@@ -1185,6 +1185,145 @@ fn low_codes_resolve_to_the_covering_range() {
|
||||
assert_eq!((u.covering(0xFFFF).is_some(), u.covering(0x1_0001).is_some(), u.covering(0x1_0002).is_none()), (true, true, true));
|
||||
}
|
||||
|
||||
/// [`cmap_only_sfnt_named`] without a `post` table.
|
||||
fn cmap_only_sfnt(mac: Option<&[(u8, u8)]>, windows: &[(u16, u16)]) -> Vec<u8> {
|
||||
cmap_only_sfnt_named(mac, windows, false)
|
||||
}
|
||||
|
||||
/// An sfnt holding a `cmap` table with these subtables: (1, 0) format 0 mapping codes to glyphs,
|
||||
/// and (3, 0) format 4 mapping each code point to its glyph, one segment each; and, if `named`, a
|
||||
/// version 2.0 `post` table.
|
||||
fn cmap_only_sfnt_named(mac: Option<&[(u8, u8)]>, windows: &[(u16, u16)], named: bool) -> Vec<u8> {
|
||||
let be16 = |out: &mut Vec<u8>, n: u16| out.extend_from_slice(&n.to_be_bytes());
|
||||
let mut subtables: Vec<((u16, u16), Vec<u8>)> = Vec::new();
|
||||
if let Some(map) = mac {
|
||||
let mut t = Vec::new();
|
||||
for n in [0u16, 262, 0] {
|
||||
be16(&mut t, n);
|
||||
}
|
||||
let mut ids = [0u8; 256];
|
||||
for &(code, glyph) in map {
|
||||
ids[usize::from(code)] = glyph;
|
||||
}
|
||||
t.extend(ids);
|
||||
subtables.push(((1, 0), t));
|
||||
}
|
||||
let segments: Vec<(u16, u16)> = windows.iter().copied().chain([(0xFFFF, 0)]).collect();
|
||||
let count = segments.len() as u16;
|
||||
let mut t = Vec::new();
|
||||
for n in [4u16, 16 + 8 * count, 0, 2 * count, 2, 0, 0] {
|
||||
be16(&mut t, n);
|
||||
}
|
||||
segments.iter().for_each(|&(c, _)| be16(&mut t, c));
|
||||
be16(&mut t, 0);
|
||||
segments.iter().for_each(|&(c, _)| be16(&mut t, c));
|
||||
segments.iter().for_each(|&(c, g)| be16(&mut t, if c == 0xFFFF { 1 } else { g.wrapping_sub(c) }));
|
||||
segments.iter().for_each(|_| be16(&mut t, 0));
|
||||
subtables.push(((3, 0), t));
|
||||
|
||||
let mut cmap = Vec::new();
|
||||
be16(&mut cmap, 0);
|
||||
be16(&mut cmap, subtables.len() as u16);
|
||||
let mut offset = 4 + 8 * subtables.len() as u32;
|
||||
for ((platform, encoding), bytes) in &subtables {
|
||||
be16(&mut cmap, *platform);
|
||||
be16(&mut cmap, *encoding);
|
||||
cmap.extend_from_slice(&offset.to_be_bytes());
|
||||
offset += bytes.len() as u32;
|
||||
}
|
||||
subtables.iter().for_each(|(_, bytes)| cmap.extend(bytes));
|
||||
let mut tables = vec![(*b"cmap", cmap)];
|
||||
if named {
|
||||
// Version 2.0 with no glyphs to name.
|
||||
let mut post = vec![0, 2, 0, 0];
|
||||
post.resize(34, 0);
|
||||
tables.push((*b"post", post));
|
||||
}
|
||||
let mut out = vec![0, 1, 0, 0];
|
||||
out.extend_from_slice(&(tables.len() as u16).to_be_bytes());
|
||||
out.extend([0; 6]);
|
||||
let mut offset = 12 + 16 * tables.len() as u32;
|
||||
for (tag, table) in &tables {
|
||||
out.extend(tag);
|
||||
out.extend_from_slice(&0u32.to_be_bytes());
|
||||
out.extend_from_slice(&offset.to_be_bytes());
|
||||
out.extend_from_slice(&(table.len() as u32).to_be_bytes());
|
||||
offset += table.len() as u32;
|
||||
}
|
||||
tables.into_iter().for_each(|(_, table)| out.extend(table));
|
||||
out
|
||||
}
|
||||
|
||||
/// A symbolic TrueType font with no `/Encoding` reads a code as Mac OS Roman when its (1, 0) `cmap`
|
||||
/// subtable selects the glyph the code draws and the glyph sits where the standard Macintosh glyph
|
||||
/// order puts that character, as the renderer's text layer reads it (vendored hayro-interpret
|
||||
/// patch (101), ISO 32000-2 §9.6.5.4). Otherwise the codes read as before (Standard): WinAnsi codes
|
||||
/// in that order (0x92 drawn with `quoteright`), no (1, 0) subtable, one selecting other glyphs
|
||||
/// than the (3, 0) one, a font flagged non-symbolic, or one that names its glyphs.
|
||||
#[test]
|
||||
fn a_symbolic_truetype_font_in_the_macintosh_order_reads_its_codes_as_mac_roman() {
|
||||
let read = |program: Vec<u8>, flags: i64, codes: [u32; 2]| {
|
||||
let mut doc = Document::new_empty();
|
||||
let program = doc.add(Object::Stream(Stream::flate(Dict::new(), &program)));
|
||||
let mut descriptor = Dict::new();
|
||||
descriptor.set(b"Flags".to_vec(), Object::Int(flags));
|
||||
descriptor.set(b"FontFile2".to_vec(), Object::Ref(program));
|
||||
let f = font(&mut doc, vec![("Subtype", Object::name("TrueType")), ("FontDescriptor", Object::Dict(descriptor))]);
|
||||
let u = unicode_map(&doc, &f, false);
|
||||
codes.map(|c| u.text(c).unwrap_or_default().into_owned())
|
||||
};
|
||||
// `agrave` and `eacute` are glyphs 106 and 112 of the standard Macintosh order, `endash` and
|
||||
// `quoteright` 178 and 183.
|
||||
let mac_order = cmap_only_sfnt(Some(&[(0x88, 106), (0x8E, 112)]), &[(0xF088, 106), (0xF08E, 112)]);
|
||||
assert_eq!(read(mac_order.clone(), 4, [0x88, 0x8E]), ["à", "é"]);
|
||||
assert_eq!(read(mac_order, 32, [0x88, 0x8E]), ["", ""], "flagged non-symbolic");
|
||||
let winansi = cmap_only_sfnt(Some(&[(0x92, 183), (0x96, 178)]), &[(0xF092, 183), (0xF096, 178)]);
|
||||
assert_eq!(read(winansi, 4, [0x92, 0x96]), ["", ""], "WinAnsi codes in the Macintosh order");
|
||||
let windows_only = cmap_only_sfnt(None, &[(0xF088, 106), (0xF08E, 112)]);
|
||||
assert_eq!(read(windows_only, 4, [0x88, 0x8E]), ["", ""], "no (1, 0) subtable");
|
||||
let disagreeing = cmap_only_sfnt(Some(&[(0x88, 112), (0x8E, 106)]), &[(0xF088, 106), (0xF08E, 112)]);
|
||||
assert_eq!(read(disagreeing, 4, [0x88, 0x8E]), ["", ""], "a (1, 0) subtable selecting other glyphs");
|
||||
// A `post` table of version 2.0 names the glyphs; what it calls them doesn't matter here.
|
||||
let named = cmap_only_sfnt_named(Some(&[(0x88, 106), (0x8E, 112)]), &[(0xF088, 106), (0xF08E, 112)], true);
|
||||
assert_eq!(read(named, 4, [0x88, 0x8E]), ["", ""], "a font that names its glyphs");
|
||||
}
|
||||
|
||||
/// The Mac OS Roman reading applies only to a font that names no base encoding and isn't Symbol or
|
||||
/// ZapfDingbats (ISO 32000-2 §9.6.5.4, §9.6.6.1): a named `/BaseEncoding` or `/Encoding` keeps its
|
||||
/// own reading, a `/Differences` without a base encoding still gets it, and a Symbol font keeps no
|
||||
/// implicit mapping.
|
||||
#[test]
|
||||
fn the_mac_roman_reading_needs_no_base_encoding_and_no_symbolic_builtin() {
|
||||
let read = |entries: Vec<(&str, Object)>| {
|
||||
let mut doc = Document::new_empty();
|
||||
let program = cmap_only_sfnt(Some(&[(0x88, 106), (0x8E, 112)]), &[(0xF088, 106), (0xF08E, 112)]);
|
||||
let program = doc.add(Object::Stream(Stream::flate(Dict::new(), &program)));
|
||||
let mut descriptor = Dict::new();
|
||||
descriptor.set(b"Flags".to_vec(), Object::Int(4));
|
||||
descriptor.set(b"FontFile2".to_vec(), Object::Ref(program));
|
||||
let mut all = vec![("Subtype", Object::name("TrueType")), ("FontDescriptor", Object::Dict(descriptor))];
|
||||
all.extend(entries);
|
||||
let f = font(&mut doc, all);
|
||||
let u = unicode_map(&doc, &f, false);
|
||||
[0x88, 0x8E].map(|c| u.text(c).unwrap_or_default().into_owned())
|
||||
};
|
||||
assert_eq!(read(vec![]), ["à", "é"]);
|
||||
assert_eq!(read(vec![("Encoding", Object::name("WinAnsiEncoding"))]), ["ˆ", "Ž"], "a named base encoding");
|
||||
let mut based = Dict::new();
|
||||
based.set(b"BaseEncoding".to_vec(), Object::name("WinAnsiEncoding"));
|
||||
assert_eq!(read(vec![("Encoding", Object::Dict(based))]), ["ˆ", "Ž"], "a /BaseEncoding");
|
||||
let mut diffs = Dict::new();
|
||||
diffs.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(0x8E), Object::name("ecircumflex")]));
|
||||
assert_eq!(read(vec![("Encoding", Object::Dict(diffs))]), ["à", "ê"], "/Differences without a base encoding");
|
||||
assert_eq!(read(vec![("BaseFont", Object::name("Symbol"))]), ["", ""], "Symbol");
|
||||
assert_eq!(read(vec![("BaseFont", Object::name("ZapfDingbats"))]), ["", ""], "ZapfDingbats");
|
||||
// A name it can't read clears the Mac OS Roman reading for its code, as it clears a base
|
||||
// encoding's.
|
||||
let mut notdef = Dict::new();
|
||||
notdef.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(0x8E), Object::name(".notdef")]));
|
||||
assert_eq!(read(vec![("Encoding", Object::Dict(notdef))]), ["à", ""], "/Differences naming .notdef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_subset_without_cmap_uses_pdf_glyph_ids_in_editor_and_shaper_without_replacing_the_program() {
|
||||
use skrifa::{MetadataProvider as _, raw::TableProvider as _};
|
||||
|
||||
@@ -270,6 +270,9 @@ pub enum RenderWarning {
|
||||
/// A colour could not be computed (a colour space that cannot be read, or a Separation or
|
||||
/// DeviceN tint transform that cannot be evaluated) and was painted in a fallback colour.
|
||||
ColourFallback,
|
||||
/// A Type 3 font without the `/Widths` it requires (ISO 32000-2 Table 110) was used; its
|
||||
/// glyphs were spaced by the widths their glyph procedures give.
|
||||
Type3WidthsMissing,
|
||||
}
|
||||
|
||||
/// Counters for work performed by a renderer or render pool.
|
||||
@@ -478,6 +481,7 @@ fn warning_settings(settings: &InterpreterSettings, warnings: Arc<Mutex<Vec<Rend
|
||||
let warning = match warning {
|
||||
InterpreterWarning::ContentTruncated => RenderWarning::ContentTruncated,
|
||||
InterpreterWarning::ColorFallback => RenderWarning::ColourFallback,
|
||||
InterpreterWarning::Type3WidthsMissing => RenderWarning::Type3WidthsMissing,
|
||||
InterpreterWarning::UnsupportedFont | InterpreterWarning::ImageDecodeFailure => return,
|
||||
};
|
||||
let mut warnings = lock(&target);
|
||||
|
||||
@@ -5570,6 +5570,22 @@ fn a_font_whose_post_version_has_swapped_halves_is_still_used() {
|
||||
assert_eq!(alternate([0, 0, 0, 2]), alternate([0, 2, 0, 0]), "and also with swapped halves");
|
||||
}
|
||||
|
||||
/// A font dictionary whose `/Subtype` is missing or the empty name is read as a Type 1 font,
|
||||
/// so its `/BaseFont` picks the face (vendored hayro-interpret patch 94). `/Subtype` is required
|
||||
/// (ISO 32000-2 Table 109), but such a `/Courier-Bold` was drawn in the Helvetica fallback,
|
||||
/// shifting its columns (PDFBOX-1988).
|
||||
#[test]
|
||||
fn a_font_without_a_subtype_is_drawn_by_its_base_font() {
|
||||
let render = |subtype: &str, base_font: &str| {
|
||||
let resources = format!("<< /Font << /F1 << /Type /Font {subtype} /BaseFont /{base_font} >> >> >>");
|
||||
render_first_page(image_cache_pdf("BT /F1 20 Tf 5 25 Td (iiiiii) Tj ET", &resources, &[])).rgba.to_vec()
|
||||
};
|
||||
let courier = render("/Subtype /Type1", "Courier-Bold");
|
||||
assert_ne!(courier, render("/Subtype /Type1", "Helvetica"), "Courier Bold and Helvetica differ");
|
||||
assert_eq!(render("/Subtype /", "Courier-Bold"), courier, "an empty /Subtype");
|
||||
assert_eq!(render("", "Courier-Bold"), courier, "no /Subtype");
|
||||
}
|
||||
|
||||
/// A page with no `/MediaBox`, its own or inherited, is US Letter (612 × 792 pt), as other
|
||||
/// readers draw it and as the page model and inspector already sized it; it was A4 (vendored
|
||||
/// hayro-syntax patch 44; ISO 32000-2 Table 31 makes the box required and gives no default;
|
||||
@@ -6485,6 +6501,94 @@ trailer << /Root 1 0 R >>
|
||||
assert!(p != [255, 255, 255, 255] && p[3] == 255, "the triangles are drawn: {p:?}");
|
||||
}
|
||||
|
||||
/// A rainbow of tensor-product patches (two quarter rings per band) painted as a pattern
|
||||
/// through a rectangle that is just the rainbow's box. Every patch is 722 triangles, and
|
||||
/// across a narrow curved band they are thin and lie at every angle, so their boxes hold
|
||||
/// many times their area: walking those boxes spent the visit budget (eight per pixel of the
|
||||
/// rectangle, four per triangle) before the last patches, and the inner bands were left white
|
||||
/// (through the page-sized rectangle, or with `sh`, the same mesh drew whole). The sampler
|
||||
/// now walks only the columns of each row a triangle can reach. Vendored hayro-interpret
|
||||
/// patch 99.
|
||||
#[test]
|
||||
fn thin_patches_of_a_mesh_in_a_tight_path_are_all_painted() {
|
||||
const K: f64 = 0.5523; // a quarter circle's Bézier handles, as a fraction of the radius
|
||||
const BANDS: [[u8; 3]; 6] = [[255, 0, 0], [255, 128, 0], [255, 255, 0], [0, 200, 0], [0, 0, 255], [128, 0, 255]];
|
||||
// The stream order of a tensor patch's points (column i, row j), ISO 32000-2 8.7.4.5.8.
|
||||
const ORDER: [(usize, usize); 16] =
|
||||
[(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3), (3, 3), (3, 2), (3, 1), (3, 0), (2, 0), (1, 0), (1, 1), (1, 2), (2, 2), (2, 1)];
|
||||
// Bands 20/6 pt wide from radius 50 inwards, centred on (100, 100) by the pattern matrix;
|
||||
// 16-bit coordinates decoded onto -64..64.
|
||||
let width = 20.0 / BANDS.len() as f64;
|
||||
let mut data = Vec::new();
|
||||
for (band, colour) in BANDS.iter().enumerate() {
|
||||
let (outer, inner) = (50.0 - band as f64 * width, 50.0 - (band + 1) as f64 * width);
|
||||
for side in [1.0, -1.0] {
|
||||
data.push(0u8);
|
||||
for (i, j) in ORDER {
|
||||
let rho = inner + (outer - inner) * j as f64 / 3.0;
|
||||
let (x, y) = [(side * rho, 0.0), (side * rho, K * rho), (side * K * rho, rho), (0.0, rho)][i];
|
||||
for v in [x, y] {
|
||||
data.extend_from_slice(&(((v + 64.0) / 128.0 * 65535.0).round() as u16).to_be_bytes());
|
||||
}
|
||||
}
|
||||
for _ in 0..4 {
|
||||
data.extend_from_slice(colour);
|
||||
}
|
||||
}
|
||||
}
|
||||
let render = |rect: &str| {
|
||||
let content = format!("/Pattern cs /P0 scn {rect} re f");
|
||||
let mut pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R /Resources << /Pattern << /P0 6 0 R >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
6 0 obj << /Type /Pattern /PatternType 2 /Shading 5 0 R /Matrix [1 0 0 1 100 100] >> endobj
|
||||
5 0 obj << /ShadingType 7 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [-64 64 -64 64 0 1 0 1 0 1] /Length {} >> stream
|
||||
",
|
||||
content.len(),
|
||||
data.len()
|
||||
)
|
||||
.into_bytes();
|
||||
pdf.extend_from_slice(&data);
|
||||
pdf.extend_from_slice(
|
||||
b"
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF
|
||||
",
|
||||
);
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.5, tag: 0, ..Default::default() });
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (300, 300));
|
||||
page
|
||||
};
|
||||
// At 45° on either side, the middle of the outer and of the inner band, in device pixels
|
||||
// (scale 1.5, y down).
|
||||
let at = |rho: f64, side: f64| {
|
||||
let d = rho * std::f64::consts::FRAC_1_SQRT_2;
|
||||
(((100.0 + side * d) * 1.5) as usize, ((100.0 - d) * 1.5) as usize)
|
||||
};
|
||||
for (what, rect) in [("the rainbow's box", "50 100 100 50"), ("the page", "0 0 200 200")] {
|
||||
let page = render(rect);
|
||||
for (band, rho) in [(0, 50.0 - width / 2.0), (BANDS.len() - 1, 30.0 + width / 2.0)] {
|
||||
for side in [1.0, -1.0] {
|
||||
let (x, y) = at(rho, side);
|
||||
let got = rgb_at(&page.rgba, 300, x, y);
|
||||
let want = BANDS[band];
|
||||
assert!(
|
||||
got.iter().zip(want).all(|(g, w)| g.abs_diff(w) <= 8),
|
||||
"through {what}, band {band} at ({x}, {y}): got {got:?}, want {want:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Two triangles of a mesh that share an edge cover it together. The sampler tested each
|
||||
/// pixel's corner, and only strictly inside a triangle, so the diagonal two triangles share
|
||||
/// was left white wherever it ran through the samples, and so was a pixel row or column
|
||||
@@ -7844,6 +7948,329 @@ trailer << /Root 1 0 R >>
|
||||
}
|
||||
}
|
||||
|
||||
/// A page of `/ImageMask` stencils painted with a tiling pattern: the 4 × 4 stencil inks a ring
|
||||
/// (its centre 2 × 2 samples are `1`, unpainted under the default `/Decode [0 1]`), filled with
|
||||
/// a red coloured pattern.
|
||||
fn patterned_stencil_page(media: (u32, u32), content: &str) -> RenderedPage {
|
||||
let tile = "1 0 0 rg 0 0 10 10 re f";
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] /Contents 4 0 R /Resources << /XObject << /Msk 5 0 R >> /Pattern << /P 6 0 R >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true /Filter /ASCIIHexDecode /Length 9 >> stream
|
||||
00606000>
|
||||
endstream endobj
|
||||
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
|
||||
{tile}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
media.0,
|
||||
media.1,
|
||||
content.len(),
|
||||
tile.len()
|
||||
);
|
||||
let page = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
scale: 1.0,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
page
|
||||
}
|
||||
|
||||
/// A stencil mask filled with a pattern shows the pattern where the stencil inks and nothing
|
||||
/// elsewhere: not in its unpainted samples, and not around it, also when it is drawn smaller
|
||||
/// than its samples and lands on whole device pixels.
|
||||
#[test]
|
||||
fn a_patterned_stencil_mask_paints_only_where_it_inks() {
|
||||
// Pixel rows run top down: PDF (x, y) is pixel (x, 200 - y).
|
||||
let px = |p: &RenderedPage, x: u32, y: u32| p.rgba[(((200 - y) * p.width + x) * 4) as usize..][..3].to_vec();
|
||||
let page = patterned_stencil_page((200, 200), "/Pattern cs /P scn q 40 0 0 40 20 20 cm /Msk Do Q q 3 0 0 3 100.4 100.4 cm /Msk Do Q");
|
||||
assert_eq!(px(&page, 25, 25), [255, 0, 0], "the ring is red");
|
||||
assert_eq!(px(&page, 40, 40), [255, 255, 255], "the stencil's unpainted centre stays white");
|
||||
for (x, y) in [(15, 40), (65, 40), (40, 15), (40, 65)] {
|
||||
assert_eq!(px(&page, x, y), [255, 255, 255], "outside the stencil at ({x}, {y})");
|
||||
}
|
||||
assert!((100..104).any(|x| px(&page, x, 101) != [255, 255, 255]), "the shrunk stencil is drawn");
|
||||
for edge in [97, 98, 106, 107] {
|
||||
for along in 97..=107 {
|
||||
assert_eq!(px(&page, edge, along), [255, 255, 255], "beside the shrunk stencil at ({edge}, {along})");
|
||||
assert_eq!(px(&page, along, edge), [255, 255, 255], "above or below the shrunk stencil at ({along}, {edge})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A patterned stencil mask under a soft mask is drawn at the soft mask's opacity once, as a
|
||||
/// rectangle filled with the pattern beside it is: the stencil's coverage, which cuts the pattern
|
||||
/// to the stencil's shape, is not masked a second time.
|
||||
#[test]
|
||||
fn a_patterned_stencil_mask_under_a_soft_mask_is_masked_once() {
|
||||
let content = "/S gs /Pattern cs /P scn q 40 0 0 40 20 20 cm /Msk Do Q 120 20 40 40 re f";
|
||||
let tile = "1 0 0 rg 0 0 10 10 re f";
|
||||
let group = "0.5 g 0 0 200 200 re f";
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R /Resources << /XObject << /Msk 5 0 R >> /Pattern << /P 6 0 R >> /ExtGState << /S << /SMask << /S /Luminosity /G 7 0 R >> >> >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true /Filter /ASCIIHexDecode /Length 9 >> stream
|
||||
00606000>
|
||||
endstream endobj
|
||||
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
|
||||
{tile}
|
||||
endstream endobj
|
||||
7 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 200 200] /Group << /S /Transparency /CS /DeviceGray >> /Length {} >> stream
|
||||
{group}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len(),
|
||||
tile.len(),
|
||||
group.len()
|
||||
);
|
||||
let page = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
scale: 1.0,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
// Pixel rows run top down: PDF (x, y) is pixel (x, 200 - y).
|
||||
let px = |x: u32, y: u32| page.rgba[(((200 - y) * page.width + x) * 4) as usize..][..3].to_vec();
|
||||
let (ring, rect) = (px(25, 25), px(140, 40));
|
||||
assert!(rect != [255, 255, 255] && rect != [255, 0, 0], "the rectangle is half masked: {rect:?}");
|
||||
assert!(ring.iter().zip(&rect).all(|(a, b)| a.abs_diff(*b) <= 2), "stencil {ring:?}, rectangle {rect:?}");
|
||||
assert_eq!(px(40, 40), [255, 255, 255], "the stencil's unpainted centre stays white");
|
||||
}
|
||||
|
||||
/// A patterned stencil mask in a Type 3 glyph is shrunk by area averaging, as it is outside
|
||||
/// one (#4800): the glyph's own image rules are for its images, not for the coverage that cuts
|
||||
/// the pattern. Here a one-column stroke of an 8-column stencil, drawn 4 pixels wide, is a
|
||||
/// half-covered red; nearest-neighbour would read columns 1, 3, 5 and 7 and miss it.
|
||||
#[test]
|
||||
fn a_patterned_stencil_mask_in_a_type3_glyph_keeps_thin_strokes() {
|
||||
let content = "BT /Pattern cs /P scn /T3 1 Tf 0 0 Td (a) Tj ET";
|
||||
let glyph = "400 0 0 0 400 100 d1 q 400 0 0 100 0 0 cm /Im0 Do Q";
|
||||
let tile = "1 0 0 rg 0 0 10 10 re f";
|
||||
let pdf = format!(
|
||||
"%PDF-1.4
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 4 1] /Contents 4 0 R /Resources << /Font << /T3 6 0 R >> /Pattern << /P 8 0 R >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /XObject /Subtype /Image /Width 8 /Height 1 /ImageMask true /Filter /ASCIIHexDecode /Length 3 >> stream
|
||||
DF>
|
||||
endstream endobj
|
||||
6 0 obj << /Type /Font /Subtype /Type3 /FontBBox [0 0 400 100] /FontMatrix [0.01 0 0 0.01 0 0] /CharProcs << /a 7 0 R >> /Encoding << /Type /Encoding /Differences [97 /a] >> /FirstChar 97 /LastChar 97 /Widths [400] /Resources << /XObject << /Im0 5 0 R >> >> >> endobj
|
||||
7 0 obj << /Length {} >> stream
|
||||
{glyph}
|
||||
endstream endobj
|
||||
8 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
|
||||
{tile}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len(),
|
||||
glyph.len(),
|
||||
tile.len()
|
||||
);
|
||||
let page = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
scale: 1.0,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (4, 1));
|
||||
let px = page.rgba.as_chunks::<4>().0.to_vec();
|
||||
let [r, g, b, _] = px.get(1).copied().unwrap_or_default();
|
||||
assert!(r >= 254 && g.abs_diff(128) <= 1 && b == g, "half-covered red over white, got {px:?}");
|
||||
assert_eq!(px.get(3), Some(&[255, 255, 255, 255]), "{px:?}");
|
||||
}
|
||||
|
||||
/// pdf.js issue 8078 paints 73,047 small stencil masks with one tiling pattern. Each was drawn
|
||||
/// through a canvas-sized luminance mask (a full-page render and composite per stencil), which
|
||||
/// took minutes for the page; a stencil now costs what its rectangle covers. Here 5,000
|
||||
/// patterned stencils on a 1000 × 2000 page. What the page draws offscreen is counted (hayro
|
||||
/// patch 30) rather than timed, so the check is the same on every machine: the one pattern cell,
|
||||
/// and not a page-sized pixmap per stencil.
|
||||
#[test]
|
||||
fn many_patterned_stencil_masks_draw_no_page_sized_masks() {
|
||||
let mut content = String::from("/Pattern cs /P scn ");
|
||||
for i in 0..5000u32 {
|
||||
content.push_str(&format!("q 16 0 0 16 {} {} cm /Msk Do Q ", (i % 50) * 20, (i / 50) * 20));
|
||||
}
|
||||
let before = hayro::offscreen_draws();
|
||||
let page = patterned_stencil_page((1000, 2000), &content);
|
||||
let after = hayro::offscreen_draws();
|
||||
assert_eq!(after.tiling_cells - before.tiling_cells, 1, "one cell for the pattern's one setting");
|
||||
let pixels = after.pixels - before.pixels;
|
||||
assert!(pixels < 1000 * 2000, "5,000 patterned stencils drew {pixels} pixels offscreen");
|
||||
// The last stencil sits at (980, 1980): its ring is red, its centre white.
|
||||
let px = |x: u32, y: u32| page.rgba[(((2000 - y) * page.width + x) * 4) as usize..][..3].to_vec();
|
||||
assert_eq!(px(981, 1981), [255, 0, 0]);
|
||||
assert_eq!(px(988, 1988), [255, 255, 255]);
|
||||
}
|
||||
|
||||
/// A coloured tiling pattern's cell is drawn once for all the fills of one setting (`scn`), as
|
||||
/// counted by hayro patch 30: here 3,000 small squares filled with one pattern. Drawn again for
|
||||
/// each fill, each cell was kept until the page was composited (pdf.js issue 8078 ran out of
|
||||
/// memory that way, its 3.5 MB cell drawn for each of 73,047 stencils). The squares all show the
|
||||
/// pattern in place. The same holds for a step so large that the fills show one cell, which is
|
||||
/// drawn with a transparent border instead of repeated.
|
||||
#[test]
|
||||
fn fills_of_one_pattern_setting_share_its_cell() {
|
||||
let mut content = String::from("/Pattern cs /P scn ");
|
||||
for i in 0..3000u32 {
|
||||
content.push_str(&format!("{} {} 4 4 re f ", (i % 60) * 5, (i / 60) * 5));
|
||||
}
|
||||
// The cell is blue with a red lower-left quarter; at `/Matrix` scale 10 a cell is 100 units,
|
||||
// so with a step of 10 red shows where both coordinates, modulo 100, are below 50. With a
|
||||
// step of 100000 the one cell shows and nothing past it.
|
||||
let cell = "0 0 1 rg 0 0 10 10 re f 1 0 0 rg 0 0 5 5 re f";
|
||||
for (step, past_the_first_cell) in [("10", [0, 0, 255]), ("100000", [255, 255, 255])] {
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Pattern << /P 5 0 R >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep {step} /YStep {step} /Matrix [10 0 0 10 0 0] /Resources << >> /Length {} >> stream
|
||||
{cell}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len(),
|
||||
cell.len()
|
||||
);
|
||||
let before = hayro::offscreen_draws();
|
||||
let page = render_first_page(pdf.into_bytes());
|
||||
let cells = hayro::offscreen_draws().tiling_cells - before.tiling_cells;
|
||||
assert_eq!(cells, 1, "step {step}: cells drawn for 3,000 fills of one setting");
|
||||
// Pixel rows run top down: PDF (x, y) is pixel (x, 300 - y).
|
||||
let px = |x: u32, y: u32| page.rgba[(((300 - y) * page.width + x) * 4) as usize..][..3].to_vec();
|
||||
// The first square, at (0, 0), and the last, at (295, 245).
|
||||
assert_eq!(px(1, 1), [255, 0, 0], "step {step}");
|
||||
assert_eq!(px(296, 246), past_the_first_cell, "step {step}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills interleaving the settings of two coloured patterns with cells of one size, and of one
|
||||
/// of them at two cell scales, each show their own pattern in place: a cell kept for one setting
|
||||
/// is not taken for another (hayro patch 30). `/P` is blue with a red lower-left quarter and `/Q`
|
||||
/// green with a yellow one, both at `/Matrix` scale 10, so a cell is 100 units and its quarter
|
||||
/// shows where both coordinates, modulo 100, are below 50. The form `/X`, drawn at scale 2, sets
|
||||
/// `/P` in its own space, where a cell is 200 units, red where both page coordinates, modulo
|
||||
/// 200, are below 100. Each of the six settings draws its cell once, for all its fills.
|
||||
#[test]
|
||||
fn fills_of_interleaved_pattern_settings_each_show_their_own_cell() {
|
||||
let content = "/Pattern cs /P scn 60 60 4 4 re f 10 10 4 4 re f q 2 0 0 2 0 0 cm /X Do Q \
|
||||
/Q scn 110 10 4 4 re f 160 160 4 4 re f /P scn 210 210 4 4 re f q 2 0 0 2 0 0 cm /X Do Q \
|
||||
/Q scn 260 260 4 4 re f 110 110 4 4 re f";
|
||||
// In page space, squares at (60, 260) and (260, 60), where `/P` at scale 1 is blue.
|
||||
let form = "/Pattern cs /P scn 30 130 2 2 re f 130 30 2 2 re f";
|
||||
let p = "0 0 1 rg 0 0 10 10 re f 1 0 0 rg 0 0 5 5 re f";
|
||||
let q = "0 1 0 rg 0 0 10 10 re f 1 1 0 rg 0 0 5 5 re f";
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Pattern << /P 5 0 R /Q 6 0 R >> /XObject << /X 7 0 R >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Matrix [10 0 0 10 0 0] /Resources << >> /Length {} >> stream
|
||||
{p}
|
||||
endstream endobj
|
||||
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Matrix [10 0 0 10 0 0] /Resources << >> /Length {} >> stream
|
||||
{q}
|
||||
endstream endobj
|
||||
7 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 150 150] /Resources << /Pattern << /P 5 0 R >> >> /Length {} >> stream
|
||||
{form}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len(),
|
||||
p.len(),
|
||||
q.len(),
|
||||
form.len()
|
||||
);
|
||||
let before = hayro::offscreen_draws();
|
||||
let page = render_first_page(pdf.into_bytes());
|
||||
assert_eq!(hayro::offscreen_draws().tiling_cells - before.tiling_cells, 6, "one cell per setting");
|
||||
// The middle of the 4 × 4 square at PDF (x, y); pixel rows run top down.
|
||||
let px = |x: u32, y: u32| page.rgba[(((300 - 2 - y) * page.width + x + 2) * 4) as usize..][..3].to_vec();
|
||||
let (red, blue, green, yellow) = ([255, 0, 0], [0, 0, 255], [0, 255, 0], [255, 255, 0]);
|
||||
for (x, y, colour, what) in [
|
||||
(60, 60, blue, "/P"),
|
||||
(10, 10, red, "/P"),
|
||||
(60, 260, red, "/P at scale 2"),
|
||||
(260, 60, red, "/P at scale 2"),
|
||||
(110, 10, yellow, "/Q"),
|
||||
(160, 160, green, "/Q"),
|
||||
(210, 210, red, "/P set again"),
|
||||
(260, 260, green, "/Q set again"),
|
||||
(110, 110, yellow, "/Q set again"),
|
||||
] {
|
||||
assert_eq!(px(x, y), colour, "{what} at ({x}, {y})");
|
||||
}
|
||||
}
|
||||
|
||||
/// An uncoloured tiling pattern (`/PaintType 2`) takes the colour of each setting, so fills that
|
||||
/// alternate red and green settings of it show both, each in place. Its cell also takes the
|
||||
/// opacity the setting captured, which a patterned stencil mask's copy of the setting leaves out
|
||||
/// (its transparency group applies it, hayro-interpret patch 46), so the cell drawn for one fill
|
||||
/// of a setting is not the other's (hayro patch 30 keeps only coloured cells): a square and a
|
||||
/// stencil painted with one half-opaque blue setting show the same half blue.
|
||||
#[test]
|
||||
fn an_uncoloured_pattern_shows_each_settings_colour() {
|
||||
let content = "/C cs 1 0 0 /U scn 10 10 20 20 re f 0 1 0 /U scn 40 10 20 20 re f \
|
||||
1 0 0 /U scn 70 10 20 20 re f 0 1 0 /U scn 100 10 20 20 re f \
|
||||
/H gs 0 0 1 /U scn 10 50 20 20 re f q 20 0 0 20 40 50 cm /Msk Do Q";
|
||||
let tile = "0 0 10 10 re f";
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 150 100] /Contents 4 0 R /Resources << /ColorSpace << /C [/Pattern /DeviceRGB] >> /Pattern << /U 6 0 R >> /XObject << /Msk 5 0 R >> /ExtGState << /H << /ca 0.5 >> >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true /Filter /ASCIIHexDecode /Length 9 >> stream
|
||||
00000000>
|
||||
endstream endobj
|
||||
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
|
||||
{tile}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len(),
|
||||
tile.len()
|
||||
);
|
||||
let page = render_first_page(pdf.into_bytes());
|
||||
// Pixel rows run top down: PDF (x, y) is pixel (x, 100 - y).
|
||||
let px = |x: u32, y: u32| page.rgba[(((100 - y) * page.width + x) * 4) as usize..][..3].to_vec();
|
||||
for (x, colour) in [(20, [255, 0, 0]), (50, [0, 255, 0]), (80, [255, 0, 0]), (110, [0, 255, 0])] {
|
||||
assert_eq!(px(x, 20), colour, "the square at x {x}");
|
||||
}
|
||||
let (square, stencil) = (px(20, 60), px(50, 60));
|
||||
assert!(square[2] == 255 && square[0].abs_diff(128) <= 1 && square[1] == square[0], "half blue: {square:?}");
|
||||
assert!(square.iter().zip(&stencil).all(|(a, b)| a.abs_diff(*b) <= 2), "square {square:?}, stencil {stencil:?}");
|
||||
}
|
||||
|
||||
/// #2971: in a knockout transparency group (`/K true`) each element composites with the
|
||||
/// group's initial backdrop, not with the elements drawn before it in the group, over the
|
||||
/// element's shape. A blue square at 50 % opacity over a red one at 50 % shows only the blue
|
||||
@@ -8516,21 +8943,232 @@ fn decoded_images_serve_every_size_they_are_drawn_at() {
|
||||
let mut device = Draws::default();
|
||||
interpret_page(page, &mut ctx, &mut device);
|
||||
assert_eq!(device.0, 5, "every draw of /{name} was handed its pixels");
|
||||
cache.decoded_image_decodes()
|
||||
// (image decodes, mask decodes)
|
||||
let masks = cache.decoded_mask_decodes();
|
||||
(cache.decoded_image_decodes() - masks, masks)
|
||||
};
|
||||
assert_eq!(decodes("A"), 1, "a Flate image decodes the same at every size");
|
||||
assert_eq!(decodes("B"), 5, "a JPX soft mask decodes for the size it is drawn at");
|
||||
assert_eq!(decodes("A"), (1, 0), "a Flate image decodes the same at every size");
|
||||
// The image decodes once for each of the five sizes, its JPX soft mask within it: only a mask
|
||||
// averaged down (hayro-interpret patch 82) has a cache entry of its own.
|
||||
assert_eq!(decodes("B"), (5, 0), "an image with a JPX soft mask decodes for the size it is drawn at");
|
||||
}
|
||||
|
||||
/// A soft mask far larger than where its image is drawn is averaged down to that size and cached
|
||||
/// there (vendored hayro-interpret patch 82): a 2 × 2 image whose mask is 34,862 × 4,332, drawn 40
|
||||
/// times on one slide (pdf.js `issue16263.pdf`), was decoded and drawn at full size each time and
|
||||
/// the page took minutes. Drawn ten times at one size, the mask is now decoded once and handed
|
||||
/// over at that size rounded up to a power of two; a mask no larger than twice its drawn size
|
||||
/// keeps its own.
|
||||
#[test]
|
||||
fn a_soft_mask_far_larger_than_where_it_is_drawn_is_shrunk_once() {
|
||||
use hayro::hayro_interpret::font::Glyph;
|
||||
use hayro::hayro_interpret::hayro_syntax::Pdf;
|
||||
use hayro::hayro_interpret::{
|
||||
BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, Paint, PathDrawMode, SoftMask, TransformExt, interpret_page,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Rect};
|
||||
use std::io::Write;
|
||||
|
||||
/// Asks for every raster image at the size it is drawn at, as the renderer does, and keeps
|
||||
/// the size of each alpha channel it was handed.
|
||||
#[derive(Default)]
|
||||
struct Draws(Vec<(u32, u32)>);
|
||||
impl<'a> Device<'a> for Draws {
|
||||
fn set_soft_mask(&mut self, _: Option<SoftMask<'a>>) {}
|
||||
fn set_blend_mode(&mut self, _: BlendMode) {}
|
||||
fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'a>, _: &PathDrawMode) {}
|
||||
fn push_clip_path(&mut self, _: &ClipPath) {}
|
||||
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'a>>, _: BlendMode) {}
|
||||
fn draw_glyph(&mut self, _: &Glyph<'a>, _: Affine, _: Affine, _: &Paint<'a>, _: &GlyphDrawMode) {}
|
||||
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
|
||||
let [a, b, c, d, ..] = transform.as_coeffs();
|
||||
let target = ((a.hypot(b) * f64::from(image.width())).ceil() as u32, (c.hypot(d) * f64::from(image.height())).ceil() as u32);
|
||||
if let Image::Raster(r) = image {
|
||||
r.with_rgba(|_, alpha| self.0.push(alpha.map_or((0, 0), |a| (a.width, a.height))), Some(target));
|
||||
}
|
||||
}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
fn pop_transparency_group(&mut self) {}
|
||||
}
|
||||
|
||||
let flate = |data: &[u8]| {
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
z.write_all(data).unwrap();
|
||||
z.finish().unwrap()
|
||||
};
|
||||
let rgb = flate(&[0u8; 2 * 2 * 3]);
|
||||
let image = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /SMask 6 0 R /Length {} >>",
|
||||
rgb.len()
|
||||
);
|
||||
// `side` × `side` mask samples, drawn ten times at 20 × 20 points (device pixels at 72 dpi).
|
||||
let draws = |side: u32| {
|
||||
let alpha = flate(&(0..side * side).map(|i| (i % 256) as u8).collect::<Vec<_>>());
|
||||
let mask = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width {side} /Height {side} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>",
|
||||
alpha.len()
|
||||
);
|
||||
let content = (0..10).map(|i| format!("q 20 0 0 20 {} 0 cm /A Do Q", i * 22)).collect::<Vec<_>>().join(" ");
|
||||
let bytes = image_cache_pdf(&content, "<< /XObject << /A 5 0 R >> >>", &[(&image, &rgb), (&mask, &alpha)]);
|
||||
let parsed = Pdf::new(Arc::new(bytes)).expect("parses");
|
||||
let page = &parsed.pages()[0];
|
||||
let cache = InterpreterCache::new();
|
||||
let initial = page.initial_transform(true).to_kurbo();
|
||||
let mut ctx = Context::new(initial, Rect::new(0.0, 0.0, 230.0, 30.0), &cache, page.xref(), InterpreterSettings::default());
|
||||
let mut device = Draws::default();
|
||||
interpret_page(page, &mut ctx, &mut device);
|
||||
(device.0, cache.decoded_image_decodes())
|
||||
};
|
||||
let (alphas, decodes) = draws(1000);
|
||||
// Drawn 20 pixels square, averaged down to that rounded up to a power of two.
|
||||
assert_eq!(alphas, vec![(32, 32); 10], "averaged down to the size it is drawn at");
|
||||
// One decode for the image and one for its mask (here no larger than the intermediate the
|
||||
// averaged-down sizes are made from, so that is the mask itself), plus the averaged-down mask,
|
||||
// all shared by the ten draws.
|
||||
assert_eq!(decodes, 3, "decoded once");
|
||||
let (alphas, _) = draws(40);
|
||||
assert_eq!(alphas, vec![(40, 40); 10], "no more than twice the drawn size: kept as it is");
|
||||
}
|
||||
|
||||
/// The soft masks a small image is handed, drawn at each of `sizes` (width × height points at 72
|
||||
/// dpi): its `mask` (`width`, `height`, 8-bit samples) is larger than the 2 × 1 image, so it may
|
||||
/// be averaged down (vendored hayro-interpret patch 82). Each mask is given as its size and
|
||||
/// samples.
|
||||
fn soft_masks_handed(width: u32, height: u32, mask: &[u8], sizes: &[(u32, u32)]) -> (Vec<(u32, u32, Vec<u8>)>, usize) {
|
||||
soft_masks_handed_keyed(width, height, mask, sizes, ("Width", "Height"))
|
||||
}
|
||||
|
||||
/// `soft_masks_handed`, with the mask's dimensions under `keys` (`/W` and `/H`, say).
|
||||
fn soft_masks_handed_keyed(width: u32, height: u32, mask: &[u8], sizes: &[(u32, u32)], keys: (&str, &str)) -> (Vec<(u32, u32, Vec<u8>)>, usize) {
|
||||
use hayro::hayro_interpret::font::Glyph;
|
||||
use hayro::hayro_interpret::hayro_syntax::Pdf;
|
||||
use hayro::hayro_interpret::{
|
||||
BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, Paint, PathDrawMode, SoftMask, TransformExt, interpret_page,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Rect};
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Draws(Vec<(u32, u32, Vec<u8>)>);
|
||||
impl<'a> Device<'a> for Draws {
|
||||
fn set_soft_mask(&mut self, _: Option<SoftMask<'a>>) {}
|
||||
fn set_blend_mode(&mut self, _: BlendMode) {}
|
||||
fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'a>, _: &PathDrawMode) {}
|
||||
fn push_clip_path(&mut self, _: &ClipPath) {}
|
||||
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'a>>, _: BlendMode) {}
|
||||
fn draw_glyph(&mut self, _: &Glyph<'a>, _: Affine, _: Affine, _: &Paint<'a>, _: &GlyphDrawMode) {}
|
||||
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
|
||||
let [a, b, c, d, ..] = transform.as_coeffs();
|
||||
let target = ((a.hypot(b) * f64::from(image.width())).ceil() as u32, (c.hypot(d) * f64::from(image.height())).ceil() as u32);
|
||||
if let Image::Raster(r) = image {
|
||||
r.with_rgba(|_, alpha| self.0.extend(alpha.map(|a| (a.width, a.height, a.data))), Some(target));
|
||||
}
|
||||
}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
fn pop_transparency_group(&mut self) {}
|
||||
}
|
||||
let flate = |data: &[u8]| {
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
z.write_all(data).unwrap();
|
||||
z.finish().unwrap()
|
||||
};
|
||||
let rgb = flate(&[0u8; 2 * 3]);
|
||||
let image = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /SMask 6 0 R /Length {} >>",
|
||||
rgb.len()
|
||||
);
|
||||
let alpha = flate(mask);
|
||||
let mask_dict = format!(
|
||||
"<< /Type /XObject /Subtype /Image /{} {width} /{} {height} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>",
|
||||
keys.0,
|
||||
keys.1,
|
||||
alpha.len()
|
||||
);
|
||||
let content = sizes.iter().map(|(w, h)| format!("q {w} 0 0 {h} 0 0 cm /A Do Q")).collect::<Vec<_>>().join(" ");
|
||||
let bytes = image_cache_pdf(&content, "<< /XObject << /A 5 0 R >> >>", &[(&image, &rgb), (&mask_dict, &alpha)]);
|
||||
let parsed = Pdf::new(Arc::new(bytes)).expect("parses");
|
||||
let page = &parsed.pages()[0];
|
||||
let cache = InterpreterCache::new();
|
||||
let initial = page.initial_transform(true).to_kurbo();
|
||||
let mut ctx = Context::new(initial, Rect::new(0.0, 0.0, 400.0, 400.0), &cache, page.xref(), InterpreterSettings::default());
|
||||
let mut device = Draws::default();
|
||||
interpret_page(page, &mut ctx, &mut device);
|
||||
(device.0, cache.decoded_image_decodes())
|
||||
}
|
||||
|
||||
/// An averaged-down soft mask (vendored hayro-interpret patch 82):
|
||||
/// - takes each source sample into the target sample its centre falls in: seven columns
|
||||
/// `0 0 0 255 0 0 0`, drawn three wide and so averaged to four, give `0 0 127 0` (by left edges,
|
||||
/// `0 127 0 0`);
|
||||
/// - is never sized past the samples a truncated stream decodes: a mask that says it is 100 rows
|
||||
/// but holds 10, drawn 20 high, is handed no rows that no sample covers;
|
||||
/// - is cached per drawn size, rounded up to a power of two: the same image drawn 20 and then 200
|
||||
/// points wide is handed its mask at 32 and then at its own 100, not the first draw's 32 again,
|
||||
/// and drawn 20 and 21 wide it is handed one mask of 32 both times, whether the mask's
|
||||
/// dimensions are under `/Width` and `/Height` or `/W` and `/H`;
|
||||
/// - is made, at every size, from one intermediate mask at most 4096 on a side, so a mask wider
|
||||
/// than that, drawn at four sizes, is decoded in full once and not four times.
|
||||
#[test]
|
||||
fn an_averaged_down_soft_mask_follows_its_samples_and_its_drawn_size() {
|
||||
let (handed, _) = soft_masks_handed(7, 1, &[0, 0, 0, 255, 0, 0, 0], &[(3, 1)]);
|
||||
assert_eq!(handed, vec![(4, 1, vec![0, 0, 127, 0])], "centres, not left edges");
|
||||
let (handed, _) = soft_masks_handed(100, 100, &[255; 100 * 10], &[(20, 20)]);
|
||||
// 32 wide (20 rounded up to a power of two), and 10 rows, the ones decoded, not 32: all opaque.
|
||||
assert_eq!(handed, vec![(32, 10, vec![255; 32 * 10])], "no uncovered rows");
|
||||
let sizes = |draws: &[(u32, u32)]| -> Vec<(u32, u32)> {
|
||||
soft_masks_handed(100, 100, &[255; 100 * 100], draws).0.iter().map(|(w, h, _)| (*w, *h)).collect()
|
||||
};
|
||||
assert_eq!(sizes(&[(20, 20), (200, 200)]), vec![(32, 32), (100, 100)], "each draw at its own size");
|
||||
assert_eq!(sizes(&[(20, 20), (21, 21)]), vec![(32, 32), (32, 32)], "draws a pixel apart share one");
|
||||
// The same with the mask's dimensions under the abbreviated `/W` and `/H`, which an image is
|
||||
// read with too.
|
||||
let (handed, _) = soft_masks_handed_keyed(100, 100, &[255; 100 * 100], &[(20, 20), (200, 200)], ("W", "H"));
|
||||
let abbreviated: Vec<(u32, u32)> = handed.iter().map(|(w, h, _)| (*w, *h)).collect();
|
||||
assert_eq!(abbreviated, vec![(32, 32), (100, 100)], "each draw at its own size, under /W and /H");
|
||||
// Columns alternating 0 and 255, so every averaged-down sample is about half: within 8, as a
|
||||
// sample covering 19 or 20 columns, say, holds 9 or 10 opaque ones.
|
||||
let wide: Vec<u8> = (0..5000 * 2).map(|i| if i % 2 == 0 { 0 } else { 255 }).collect();
|
||||
let (handed, decodes) = soft_masks_handed(5000, 2, &wide, &[(20, 1), (40, 1), (80, 1), (160, 1)]);
|
||||
let sizes: Vec<(u32, u32)> = handed.iter().map(|(w, h, _)| (*w, *h)).collect();
|
||||
assert_eq!(sizes, vec![(32, 1), (64, 1), (128, 1), (256, 1)]);
|
||||
assert!(handed.iter().all(|(_, _, data)| data.iter().all(|&a| a.abs_diff(127) <= 8)), "{handed:?}");
|
||||
// The image at each of the four sizes, the 4096-wide intermediate once, and the mask at each
|
||||
// of the four sizes made from it. Without the intermediate there are 8, each mask size a full
|
||||
// decode.
|
||||
assert_eq!(decodes, 9, "the full mask decoded once");
|
||||
}
|
||||
|
||||
// ICC profiles whose stream dictionaries are the same (vendored hayro-interpret patch 48).
|
||||
mod icc_profile_cache;
|
||||
|
||||
// A Type 3 glyph shown again decodes its procedure once (vendored hayro-interpret patch 87).
|
||||
mod type3_proc_cache;
|
||||
|
||||
// A picture that doesn't decode is decoded once however often it is drawn (vendored
|
||||
// hayro-interpret patch 91).
|
||||
mod failed_image_decodes;
|
||||
|
||||
// A page past its rasterizer coverage leaves the rest out and says so (vendored vello_cpu patch 1).
|
||||
mod coverage_budget;
|
||||
|
||||
// A symbolic TrueType font draws through its (3, 0) cmap subtable first (#4156).
|
||||
mod symbolic_truetype_cmap;
|
||||
|
||||
// A TrueType font whose `head.indexToLocFormat` is out of range reads `loca` in the format its
|
||||
// length gives (vendored hayro-interpret patch 83).
|
||||
mod truetype_loca_format;
|
||||
|
||||
// An Arial (#5889) or Times New Roman (#6449) the PDF doesn't embed draws the glyphs its codes
|
||||
// name.
|
||||
mod non_embedded_arial;
|
||||
|
||||
// A Type 3 font without /Widths advances by its glyphs' d0/d1 widths (PDFBOX-2943).
|
||||
mod type3_proc_widths;
|
||||
|
||||
// A symbolic TrueType font in the standard Macintosh glyph order reads its codes as Mac OS Roman
|
||||
// (vendored hayro-interpret patch 101).
|
||||
mod symbolic_truetype_mac_roman_text;
|
||||
|
||||
/// Render `pdf`'s first page at 72 dpi with no time limit beyond the test's own.
|
||||
fn render_first_page(pdf: Vec<u8>) -> RenderedPage {
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
//! A picture that doesn't decode is decoded once however often a page draws it (vendored
|
||||
//! hayro-interpret patch 91). The decoded-image cache kept only decodes that succeeded, so a
|
||||
//! damaged picture, or a JPX or JBIG2 stream its decoder refuses, was decoded again at every
|
||||
//! draw: a page drawing it a thousand times decoded it a thousand times. The failure is kept for
|
||||
//! the same key now, and every draw still reports it as it did.
|
||||
|
||||
use super::image_cache_pdf;
|
||||
use hayro::hayro_interpret::font::Glyph;
|
||||
use hayro::hayro_interpret::hayro_syntax::Pdf;
|
||||
use hayro::hayro_interpret::{
|
||||
BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, InterpreterWarning, Paint, PathDrawMode,
|
||||
SoftMask, TransformExt, interpret_page,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Rect};
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// What a page handed its device: the raster images with their pixels, those of them with a
|
||||
/// mask, and the stencil masks with theirs. Every image is asked for at the size it is drawn
|
||||
/// at, as the renderer does.
|
||||
#[derive(Default)]
|
||||
struct Draws {
|
||||
rasters: usize,
|
||||
masked: usize,
|
||||
stencils: usize,
|
||||
}
|
||||
|
||||
impl<'a> Device<'a> for Draws {
|
||||
fn set_soft_mask(&mut self, _: Option<SoftMask<'a>>) {}
|
||||
fn set_blend_mode(&mut self, _: BlendMode) {}
|
||||
fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'a>, _: &PathDrawMode) {}
|
||||
fn push_clip_path(&mut self, _: &ClipPath) {}
|
||||
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'a>>, _: BlendMode) {}
|
||||
fn draw_glyph(&mut self, _: &Glyph<'a>, _: Affine, _: Affine, _: &Paint<'a>, _: &GlyphDrawMode) {}
|
||||
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
|
||||
let [a, b, c, d, ..] = transform.as_coeffs();
|
||||
let target = ((a.hypot(b) * f64::from(image.width())).ceil() as u32, (c.hypot(d) * f64::from(image.height())).ceil() as u32);
|
||||
match image {
|
||||
Image::Raster(r) => r.with_rgba(
|
||||
|_, alpha| {
|
||||
self.rasters += 1;
|
||||
self.masked += usize::from(alpha.is_some());
|
||||
},
|
||||
Some(target),
|
||||
),
|
||||
Image::Stencil(s) => s.with_stencil(|_, _| self.stencils += 1, Some(target)),
|
||||
}
|
||||
}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
fn pop_transparency_group(&mut self) {}
|
||||
}
|
||||
|
||||
/// What drawing `/name` five times at one size, and then the picture that decodes (`/G`) once
|
||||
/// on the right, did: the decodes the cache ran, the image decode failures reported, and what
|
||||
/// the device was handed.
|
||||
struct Outcome {
|
||||
decodes: usize,
|
||||
failures: usize,
|
||||
draws: Draws,
|
||||
}
|
||||
|
||||
fn draw_five_times(name: &str) -> Outcome {
|
||||
draw_five_times_at(name, [20; 5])
|
||||
}
|
||||
|
||||
/// [`draw_five_times`], drawing `/name` the five times at `sizes` (units square).
|
||||
fn draw_five_times_at(name: &str, sizes: [u32; 5]) -> Outcome {
|
||||
let flate = |data: &[u8]| {
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
z.write_all(data).unwrap();
|
||||
z.finish().unwrap()
|
||||
};
|
||||
let pixels = flate(&(0..64 * 64 * 3).map(|i| (i % 251) as u8).collect::<Vec<_>>());
|
||||
let image = |filter: &str, extra: &str, len: usize| {
|
||||
format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 64 /Height 64 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /{filter} {extra}/Length {len} >>"
|
||||
)
|
||||
};
|
||||
let good = image("FlateDecode", "", pixels.len());
|
||||
// A JPEG 2000 picture whose codestream is four zero bytes.
|
||||
let bad_jpx = image("JPXDecode", "", 4);
|
||||
// A picture whose soft mask is such a JPEG 2000 stream.
|
||||
let bad_mask = image("FlateDecode", "/SMask 9 0 R ", pixels.len());
|
||||
// A stencil mask whose JBIG2 data is four zero bytes.
|
||||
let bad_stencil = "<< /Type /XObject /Subtype /Image /Width 64 /Height 64 /ImageMask true /Filter /JBIG2Decode /Length 4 >>";
|
||||
let jpx_mask =
|
||||
"<< /Type /XObject /Subtype /Image /Width 64 /Height 64 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /JPXDecode /Length 4 >>";
|
||||
let content = [0, 24, 48, 72, 96].iter().zip(sizes).map(|(x, s)| format!("q {s} 0 0 {s} {x} 10 cm /{name} Do Q")).collect::<Vec<_>>().join(" ")
|
||||
+ " q 10 0 0 10 118 10 cm /G Do Q";
|
||||
let bytes = image_cache_pdf(
|
||||
&content,
|
||||
"<< /XObject << /G 5 0 R /J 6 0 R /S 7 0 R /M 8 0 R >> >>",
|
||||
&[(&good, &pixels), (&bad_jpx, b"\0\0\0\0"), (&bad_mask, &pixels), (bad_stencil, b"\0\0\0\0"), (jpx_mask, b"\0\0\0\0")],
|
||||
);
|
||||
let parsed = Pdf::new(Arc::new(bytes)).expect("parses");
|
||||
let page = &parsed.pages()[0];
|
||||
let cache = InterpreterCache::new();
|
||||
let failures = Arc::new(AtomicUsize::new(0));
|
||||
let others = Arc::new(Mutex::new(Vec::new()));
|
||||
let settings = InterpreterSettings {
|
||||
warning_sink: {
|
||||
let (failures, others) = (failures.clone(), others.clone());
|
||||
Arc::new(move |warning| match warning {
|
||||
InterpreterWarning::ImageDecodeFailure => {
|
||||
failures.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
other => others.lock().unwrap().push(format!("{other:?}")),
|
||||
})
|
||||
},
|
||||
..InterpreterSettings::default()
|
||||
};
|
||||
let initial = page.initial_transform(true).to_kurbo();
|
||||
let mut ctx = Context::new(initial, Rect::new(0.0, 0.0, 130.0, 70.0), &cache, page.xref(), settings);
|
||||
let mut draws = Draws::default();
|
||||
interpret_page(page, &mut ctx, &mut draws);
|
||||
assert!(others.lock().unwrap().is_empty(), "/{name} reported {:?}", others.lock().unwrap());
|
||||
Outcome { decodes: cache.decoded_image_decodes(), failures: failures.load(Ordering::Relaxed), draws }
|
||||
}
|
||||
|
||||
/// A JPEG 2000 picture that doesn't decode, drawn five times at one size, is decoded once, and
|
||||
/// each of the five draws still reports the failure.
|
||||
#[test]
|
||||
fn a_picture_that_does_not_decode_is_decoded_once_however_often_it_is_drawn() {
|
||||
let out = draw_five_times("J");
|
||||
assert_eq!(out.draws.rasters, 1, "only the picture after it drew");
|
||||
assert_eq!(out.decodes, 2, "decoded again at every draw");
|
||||
assert_eq!(out.failures, 5, "every draw reports the failure, as before");
|
||||
}
|
||||
|
||||
/// A JPEG 2000 picture decodes at a reduced resolution for the size it is drawn at, so its
|
||||
/// failure is kept for that size: drawn at two sizes, it is decoded once at each, also when it
|
||||
/// comes back to the first size after the second.
|
||||
#[test]
|
||||
fn a_picture_that_does_not_decode_is_decoded_once_at_each_size() {
|
||||
let out = draw_five_times_at("J", [20, 20, 10, 10, 20]);
|
||||
assert_eq!(out.decodes, 3, "once at each of its two sizes, and the picture after it");
|
||||
assert_eq!(out.failures, 5, "every draw reports the failure");
|
||||
}
|
||||
|
||||
/// A JBIG2 stencil mask that doesn't decode, drawn five times, is decoded once, and each draw
|
||||
/// still reports the failure: it isn't the page's JBIG2 allowance that refused it, which a
|
||||
/// later page would have afresh.
|
||||
#[test]
|
||||
fn a_stencil_mask_that_does_not_decode_is_decoded_once_however_often_it_is_drawn() {
|
||||
let out = draw_five_times("M");
|
||||
assert_eq!((out.draws.stencils, out.draws.rasters), (0, 1), "only the picture after it drew");
|
||||
assert_eq!(out.decodes, 2, "decoded again at every draw");
|
||||
assert_eq!(out.failures, 5, "every draw reports the failure, as before");
|
||||
}
|
||||
|
||||
/// A picture whose soft mask doesn't decode draws without it at every draw, decoded once, and
|
||||
/// the mask's failure is reported once, as it was (the picture decodes; only its mask fails).
|
||||
#[test]
|
||||
fn a_picture_whose_soft_mask_does_not_decode_draws_unmasked_decoded_once() {
|
||||
let out = draw_five_times("S");
|
||||
assert_eq!((out.draws.rasters, out.draws.masked), (6, 0), "drawn five times without its mask, and the picture after it");
|
||||
assert_eq!(out.decodes, 2);
|
||||
assert_eq!(out.failures, 1);
|
||||
}
|
||||
|
||||
/// A picture that decodes is still decoded once and handed to every draw.
|
||||
#[test]
|
||||
fn a_picture_that_decodes_is_still_decoded_once_and_drawn_every_time() {
|
||||
let out = draw_five_times("G");
|
||||
assert_eq!(out.draws.rasters, 6);
|
||||
assert_eq!(out.decodes, 1);
|
||||
assert_eq!(out.failures, 0);
|
||||
}
|
||||
|
||||
/// A JBIG2 picture the page's allowance left out isn't kept as a failure: the next page drawing
|
||||
/// it starts with an allowance of its own and draws it, from the same cache.
|
||||
#[test]
|
||||
fn a_picture_left_out_for_the_pages_allowance_draws_on_the_next_page() {
|
||||
use super::{RenderWarning, black_jbig2_page, jbig2_segment};
|
||||
use crate::raster::{parse, render_page, take_warnings, warning_settings};
|
||||
use crate::{RenderConfig, RenderRequest, RequestKind};
|
||||
use hayro::RenderCache;
|
||||
|
||||
let image = |number: u8, id: u8| {
|
||||
let mut stream = black_jbig2_page();
|
||||
jbig2_segment(&mut stream, 1, 62, &[], &[0, 0, 0, 0, id]);
|
||||
let mut object = format!(
|
||||
"{number} 0 obj << /Type /XObject /Subtype /Image /Width 16 /Height 2 /BitsPerComponent 1 /ColorSpace /DeviceGray /Filter /JBIG2Decode /Length {} >> stream\n",
|
||||
stream.len()
|
||||
)
|
||||
.into_bytes();
|
||||
object.extend_from_slice(&stream);
|
||||
object.extend_from_slice(b"\nendstream endobj\n");
|
||||
object
|
||||
};
|
||||
let page = |number: u8, content: u8| {
|
||||
format!("{number} 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents {content} 0 R /Resources << /XObject << /A 7 0 R /B 8 0 R >> >> >> endobj\n").into_bytes()
|
||||
};
|
||||
let content = |number: u8, text: &str| format!("{number} 0 obj << /Length {} >> stream\n{text}\nendstream endobj\n", text.len()).into_bytes();
|
||||
let mut pdf =
|
||||
b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >> endobj\n".to_vec();
|
||||
pdf.extend(page(3, 5));
|
||||
pdf.extend(page(4, 6));
|
||||
// The first page spends its allowance on A and has nothing left for B; the second draws A,
|
||||
// decoded already, and then B.
|
||||
pdf.extend(content(5, "q 20 0 0 40 0 0 cm /A Do Q q 20 0 0 40 20 0 cm /B Do Q"));
|
||||
pdf.extend(content(6, "q 20 0 0 40 0 0 cm /A Do Q q 20 0 0 40 20 0 cm /B Do Q"));
|
||||
pdf.extend(image(7, 1));
|
||||
pdf.extend(image(8, 2));
|
||||
pdf.extend_from_slice(b"trailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let pdf = parse(&Arc::new(pdf), None).expect("parses");
|
||||
/// A page's pixels and the warnings it reported.
|
||||
type Drawn = (Vec<u8>, Vec<RenderWarning>);
|
||||
let black = |(rgba, _): &Drawn, x: usize| match &rgba[((20 * 40 + x) * 4)..][..4] {
|
||||
[0, 0, 0, 255] => true,
|
||||
[255, 255, 255, 255] => false,
|
||||
other => panic!("neither an image nor the page: {other:?}"),
|
||||
};
|
||||
// Both pages, or the first alone, through one cache, as a render worker keeps it.
|
||||
let pages = |work: u64, count: usize| {
|
||||
let config = RenderConfig { jbig2_work: Some(work), ..RenderConfig::default() };
|
||||
let cache = RenderCache::new();
|
||||
(0..count)
|
||||
.map(|page| {
|
||||
let warnings = Arc::new(Mutex::new(Vec::new()));
|
||||
let settings = warning_settings(&config.settings(), warnings.clone());
|
||||
let req = RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() };
|
||||
let (_, _, rgba, _) = render_page(&pdf, &cache, &settings, &config, req).expect("renders");
|
||||
(rgba[..].to_vec(), take_warnings(&warnings))
|
||||
})
|
||||
.collect::<Vec<Drawn>>()
|
||||
};
|
||||
// The smallest allowance that draws A on the first page. A larger allowance draws at least as
|
||||
// much, so a binary search over the first page alone finds it in about ten draws.
|
||||
let draws_a = |work: u64| pages(work, 1).first().is_some_and(|first| black(first, 10));
|
||||
let candidates: Vec<u64> = (1..1_000).collect();
|
||||
let one = candidates.get(candidates.partition_point(|work| !draws_a(*work))).copied().expect("a small image is cheap");
|
||||
let Ok([first, second]) = <[Drawn; 2]>::try_from(pages(one, 2)) else { panic!("two pages") };
|
||||
assert_eq!((black(&first, 10), black(&first, 30)), (true, false), "the first page had enough for A alone");
|
||||
assert!(first.1.contains(&RenderWarning::ContentTruncated), "{:?}", first.1);
|
||||
assert!(black(&second, 10) && black(&second, 30), "A, and B left out of the first page, draw on the second");
|
||||
assert!(second.1.is_empty(), "{:?}", second.1);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//! An Arial or a Times New Roman the PDF doesn't embed draws the glyphs its codes name (vendored
|
||||
//! hayro-interpret patches 76 and 88, #5889, #6449). Word and PDFMaker write such a font as a
|
||||
//! Type 0 `CIDFontType2` with `Identity-H`, often without `/ToUnicode`, so each code is a glyph
|
||||
//! index in the author's font. Both number their first 258 glyphs in the standard Macintosh order
|
||||
//! of the TrueType `post` table (`L` is 47, `o` 82, `w` 90). The standard substitute numbers its
|
||||
//! glyphs another way: "Low Power" drew as "@ck 'Dck Yf", and "Salarié(e) du régime général" as
|
||||
//! "Salari²*g+f w't²i lo g'ì²p²tcn".
|
||||
//!
|
||||
//! Each document is drawn twice, once as such a file has it and once with a `/ToUnicode` that
|
||||
//! names each code's character, which draws through the substitute's glyph for that character.
|
||||
//! The two must be the same pixels.
|
||||
|
||||
use super::render_first_page;
|
||||
|
||||
/// "Low É" as Arial's glyph indexes: L, o, w, space, Eacute.
|
||||
const LOW_E_ACUTE: [(u16, char); 5] = [(47, 'L'), (82, 'o'), (90, 'w'), (3, ' '), (101, 'É')];
|
||||
|
||||
struct Font<'a> {
|
||||
/// The `/BaseFont` of both the Type 0 font and its descendant.
|
||||
base_font: &'a str,
|
||||
/// The descendant's `/Subtype`.
|
||||
subtype: &'a str,
|
||||
/// `true`: the codes are 1, 2, … and a `/CIDToGIDMap` stream gives each its glyph in the author's font.
|
||||
/// `false`: `/CIDToGIDMap /Identity`, and the codes are the glyph indexes themselves.
|
||||
mapped: bool,
|
||||
to_unicode: bool,
|
||||
}
|
||||
|
||||
/// A page showing [`LOW_E_ACUTE`] at 40 pt in `font`, which has no font program.
|
||||
fn pdf(font: &Font<'_>) -> Vec<u8> {
|
||||
let codes: Vec<u16> = if font.mapped { (1..=LOW_E_ACUTE.len() as u16).collect() } else { LOW_E_ACUTE.iter().map(|(gid, _)| *gid).collect() };
|
||||
let hex: String = codes.iter().map(|code| format!("{code:04X}")).collect();
|
||||
let content = format!("BT /F1 40 Tf 5 15 Td <{hex}> Tj ET");
|
||||
let bfchar: String = codes.iter().zip(LOW_E_ACUTE).map(|(code, (_, c))| format!("<{code:04X}> <{:04X}>\n", u32::from(c))).collect();
|
||||
let cmap = format!(
|
||||
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap
|
||||
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
|
||||
/CMapName /Adobe-Identity-UCS def /CMapType 2 def
|
||||
1 begincodespacerange <0000> <FFFF> endcodespacerange
|
||||
{} beginbfchar
|
||||
{bfchar}endbfchar
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end",
|
||||
codes.len()
|
||||
);
|
||||
// CID 0 is .notdef; CIDs 1… give the glyphs in order.
|
||||
let cid_to_gid: Vec<u8> = std::iter::once(0).chain(LOW_E_ACUTE.iter().map(|(gid, _)| *gid)).flat_map(u16::to_be_bytes).collect();
|
||||
let Font { base_font, subtype, .. } = font;
|
||||
let to_unicode = if font.to_unicode { " /ToUnicode 8 0 R" } else { "" };
|
||||
let map = if font.mapped { "9 0 R" } else { "/Identity" };
|
||||
|
||||
let mut out = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 220 60] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /Font /Subtype /Type0 /BaseFont /{base_font} /Encoding /Identity-H /DescendantFonts [6 0 R]{to_unicode} >> endobj
|
||||
6 0 obj << /Type /Font /Subtype /{subtype} /BaseFont /{base_font} /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>
|
||||
/FontDescriptor 7 0 R /CIDToGIDMap {map} /DW 1000 >> endobj
|
||||
7 0 obj << /Type /FontDescriptor /FontName /{base_font} /Flags 32 /FontBBox [-665 -325 2000 1040] /ItalicAngle 0 /Ascent 728 /Descent -210 /CapHeight 716 /StemV 80 >> endobj
|
||||
8 0 obj << /Length {} >> stream
|
||||
{cmap}
|
||||
endstream endobj
|
||||
9 0 obj << /Length {} >> stream
|
||||
",
|
||||
content.len(),
|
||||
cmap.len(),
|
||||
cid_to_gid.len()
|
||||
)
|
||||
.into_bytes();
|
||||
out.extend_from_slice(&cid_to_gid);
|
||||
out.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF");
|
||||
out
|
||||
}
|
||||
|
||||
/// `font` drawn without and with a `/ToUnicode`; the second must draw ink.
|
||||
fn without_and_with_unicode(font: Font<'_>) -> (Vec<u8>, Vec<u8>) {
|
||||
let without = render_first_page(pdf(&Font { to_unicode: false, ..font }));
|
||||
let with = render_first_page(pdf(&Font { to_unicode: true, ..font }));
|
||||
assert!(with.rgba.as_chunks::<4>().0.iter().any(|pixel| pixel[0] < 128), "{}: the control must draw ink", font.base_font);
|
||||
(without.rgba.to_vec(), with.rgba.to_vec())
|
||||
}
|
||||
|
||||
/// Each Arial face, as Windows exporters name it and as Word names a face it doesn't embed, draws
|
||||
/// "Low É" from its glyph indexes, with `/CIDToGIDMap /Identity` and with a map stream. The name
|
||||
/// is read without regard to case, `MT` included.
|
||||
#[test]
|
||||
fn an_arial_the_pdf_does_not_embed_draws_the_glyphs_its_codes_name() {
|
||||
for base_font in [
|
||||
"ArialMT",
|
||||
"Arial-BoldMT",
|
||||
"Arial-ItalicMT",
|
||||
"Arial-BoldItalicMT",
|
||||
"Arial",
|
||||
"Arial,Bold",
|
||||
"Arial,BoldItalic",
|
||||
"ABCDEF+Arial-BoldMT",
|
||||
"arialmt",
|
||||
"Arial-Boldmt",
|
||||
] {
|
||||
for mapped in [false, true] {
|
||||
let (without, with) = without_and_with_unicode(Font { base_font, subtype: "CIDFontType2", mapped, to_unicode: false });
|
||||
assert!(without == with, "{base_font} (CIDToGIDMap {}): the codes draw \"Low É\"", if mapped { "stream" } else { "/Identity" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Each Times New Roman face, as Windows exporters name it and as Word names a face it doesn't
|
||||
/// embed (`TimesNewRoman,Bold` in #6449), draws "Low É" from its glyph indexes.
|
||||
#[test]
|
||||
fn a_times_new_roman_the_pdf_does_not_embed_draws_the_glyphs_its_codes_name() {
|
||||
for base_font in [
|
||||
"TimesNewRomanPSMT",
|
||||
"TimesNewRomanPS-BoldMT",
|
||||
"TimesNewRomanPS-ItalicMT",
|
||||
"TimesNewRomanPS-BoldItalicMT",
|
||||
"TimesNewRoman",
|
||||
"TimesNewRoman,Bold",
|
||||
"TimesNewRoman,Italic",
|
||||
"TimesNewRoman,BoldItalic",
|
||||
"WFXLUF+TimesNewRoman,Bold",
|
||||
] {
|
||||
for mapped in [false, true] {
|
||||
let (without, with) = without_and_with_unicode(Font { base_font, subtype: "CIDFontType2", mapped, to_unicode: false });
|
||||
assert!(without == with, "{base_font} (CIDToGIDMap {}): the codes draw \"Low É\"", if mapped { "stream" } else { "/Identity" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Only the faces of Arial and Times New Roman are read in that order: Arial Narrow, Arial Black,
|
||||
/// Arial Unicode MS, Times and Times New Roman MT Std are other fonts, and a `CIDFontType0` font's
|
||||
/// CIDs aren't TrueType glyph indexes.
|
||||
#[test]
|
||||
fn codes_of_other_fonts_are_not_read_in_the_macintosh_order() {
|
||||
for (base_font, subtype) in [
|
||||
("ArialNarrow", "CIDFontType2"),
|
||||
("ArialNarrow-Bold", "CIDFontType2"),
|
||||
("Arial-Black", "CIDFontType2"),
|
||||
("ArialUnicodeMS", "CIDFontType2"),
|
||||
("ArialMT", "CIDFontType0"),
|
||||
("Times-Bold", "CIDFontType2"),
|
||||
("TimesNewRomanMTStd", "CIDFontType2"),
|
||||
("TimesNewRoman,Bold", "CIDFontType0"),
|
||||
] {
|
||||
let (without, with) = without_and_with_unicode(Font { base_font, subtype, mapped: false, to_unicode: false });
|
||||
assert!(without != with, "{base_font} ({subtype}) is not read in the Macintosh order");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
//! A symbolic TrueType font with no `/Encoding`, no `/ToUnicode` and no glyph names reads a code
|
||||
//! as Mac OS Roman when its (1, 0) `cmap` subtable selects the glyph the code draws and that glyph
|
||||
//! sits where the standard Macintosh glyph order puts the code's Mac OS Roman character (vendored
|
||||
//! hayro-interpret patch 101; ISO 32000-2 §9.6.5.4: single bytes "look up the associated glyph
|
||||
//! descriptions" in the (1, 0) subtable, and a (3, 0) subtable maps "the single-byte codes in the
|
||||
//! (1, 0) subtable" prepended with 0xF0). pdf.js's issue2017r.pdf has such a font; its text read as
|
||||
//! WinAnsi, `ˆ` for `à`.
|
||||
//!
|
||||
//! The fonts are built here, in memory: [`GLYPHS`] glyphs, those the tests show filled squares, no
|
||||
//! `post` table, and the `cmap` subtables each test asks for. No font file is involved.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Glyphs in each font: past the standard Macintosh order's `quoteright` (183).
|
||||
const GLYPHS: u16 = 200;
|
||||
|
||||
/// The standard Macintosh order's glyphs for `agrave`, `eacute`, `endash` and `quoteright`.
|
||||
const AGRAVE: u8 = 106;
|
||||
const EACUTE: u8 = 112;
|
||||
const ENDASH: u8 = 178;
|
||||
const QUOTERIGHT: u8 = 183;
|
||||
|
||||
fn be16(out: &mut Vec<u8>, n: u16) {
|
||||
out.extend_from_slice(&n.to_be_bytes());
|
||||
}
|
||||
|
||||
fn be32(out: &mut Vec<u8>, n: u32) {
|
||||
out.extend_from_slice(&n.to_be_bytes());
|
||||
}
|
||||
|
||||
/// A (1, 0) format 0 subtable mapping each code to its glyph.
|
||||
fn mac_subtable(map: &[(u8, u8)]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
for n in [0u16, 262, 0] {
|
||||
be16(&mut out, n); // format, length, language
|
||||
}
|
||||
let mut ids = [0u8; 256];
|
||||
for &(code, glyph) in map {
|
||||
ids[usize::from(code)] = glyph;
|
||||
}
|
||||
out.extend(ids);
|
||||
out
|
||||
}
|
||||
|
||||
/// A (3, 0) format 4 subtable mapping each code at 0xF000 to its glyph, one segment each.
|
||||
fn windows_subtable(map: &[(u8, u8)]) -> Vec<u8> {
|
||||
let segments: Vec<(u16, u16)> = map.iter().map(|&(code, glyph)| (0xF000 | u16::from(code), u16::from(glyph))).chain([(0xFFFF, 0)]).collect();
|
||||
let count = segments.len() as u16;
|
||||
let mut out = Vec::new();
|
||||
for n in [4u16, 16 + 8 * count, 0, 2 * count, 2, 0, 0] {
|
||||
be16(&mut out, n); // format, length, language, segCountX2, searchRange, entrySelector, rangeShift
|
||||
}
|
||||
for &(code, _) in &segments {
|
||||
be16(&mut out, code); // endCode
|
||||
}
|
||||
be16(&mut out, 0); // reservedPad
|
||||
for &(code, _) in &segments {
|
||||
be16(&mut out, code); // startCode
|
||||
}
|
||||
for &(code, glyph) in &segments {
|
||||
be16(&mut out, if code == 0xFFFF { 1 } else { glyph.wrapping_sub(code) }); // idDelta
|
||||
}
|
||||
for _ in &segments {
|
||||
be16(&mut out, 0); // idRangeOffset
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A TrueType font of [`GLYPHS`] glyphs, the `inked` ones squares, whose (3, 0) subtable maps
|
||||
/// `windows` and whose (1, 0) subtable, if any, maps `mac`. A `named` font has a version 2.0 `post`
|
||||
/// table naming its `inked` glyphs `bullet0`, `bullet1` and so on.
|
||||
fn font(inked: &[u8], mac: Option<&[(u8, u8)]>, windows: &[(u8, u8)], named: bool) -> Vec<u8> {
|
||||
// One contour, the square 0..1000 on both axes.
|
||||
let mut square = Vec::new();
|
||||
for n in [1u16, 0, 0, 1000, 1000] {
|
||||
be16(&mut square, n);
|
||||
}
|
||||
be16(&mut square, 3); // the contour's last point
|
||||
be16(&mut square, 0); // no instructions
|
||||
square.extend([1u8; 4]); // four on-curve points, two-byte coordinates
|
||||
for dx in [0i16, 1000, 0, -1000] {
|
||||
square.extend_from_slice(&dx.to_be_bytes());
|
||||
}
|
||||
for dy in [0i16, 0, 1000, 0] {
|
||||
square.extend_from_slice(&dy.to_be_bytes());
|
||||
}
|
||||
let mut glyf = Vec::new();
|
||||
let mut loca = Vec::new();
|
||||
for glyph in 0..GLYPHS {
|
||||
be16(&mut loca, (glyf.len() / 2) as u16);
|
||||
if inked.iter().any(|g| u16::from(*g) == glyph) {
|
||||
glyf.extend_from_slice(&square);
|
||||
}
|
||||
}
|
||||
be16(&mut loca, (glyf.len() / 2) as u16);
|
||||
|
||||
let mut head = vec![0; 54];
|
||||
head[0..4].copy_from_slice(&[0, 1, 0, 0]);
|
||||
head[12..16].copy_from_slice(&0x5f0f_3cf5u32.to_be_bytes());
|
||||
head[18..20].copy_from_slice(&1000u16.to_be_bytes());
|
||||
head[40..42].copy_from_slice(&1000i16.to_be_bytes());
|
||||
head[42..44].copy_from_slice(&1000i16.to_be_bytes());
|
||||
let mut hhea = vec![0; 36];
|
||||
hhea[0..4].copy_from_slice(&[0, 1, 0, 0]);
|
||||
hhea[4..6].copy_from_slice(&1000i16.to_be_bytes());
|
||||
hhea[34..36].copy_from_slice(&GLYPHS.to_be_bytes());
|
||||
let mut hmtx = Vec::new();
|
||||
for _ in 0..GLYPHS {
|
||||
be16(&mut hmtx, 1000);
|
||||
be16(&mut hmtx, 0);
|
||||
}
|
||||
let mut maxp = vec![0, 1, 0, 0];
|
||||
be16(&mut maxp, GLYPHS);
|
||||
maxp.resize(32, 0);
|
||||
|
||||
let subtables: Vec<((u16, u16), Vec<u8>)> =
|
||||
mac.map(|m| ((1, 0), mac_subtable(m))).into_iter().chain([((3, 0), windows_subtable(windows))]).collect();
|
||||
let mut cmap = Vec::new();
|
||||
be16(&mut cmap, 0);
|
||||
be16(&mut cmap, subtables.len() as u16);
|
||||
let mut offset = 4 + 8 * subtables.len() as u32;
|
||||
for ((platform, encoding), bytes) in &subtables {
|
||||
be16(&mut cmap, *platform);
|
||||
be16(&mut cmap, *encoding);
|
||||
be32(&mut cmap, offset);
|
||||
offset += bytes.len() as u32;
|
||||
}
|
||||
for (_, bytes) in &subtables {
|
||||
cmap.extend(bytes);
|
||||
}
|
||||
|
||||
let mut tables =
|
||||
vec![(*b"cmap", cmap), (*b"glyf", glyf), (*b"head", head), (*b"hhea", hhea), (*b"hmtx", hmtx), (*b"loca", loca), (*b"maxp", maxp)];
|
||||
if named {
|
||||
let mut post = vec![0, 2, 0, 0];
|
||||
post.resize(32, 0);
|
||||
be16(&mut post, GLYPHS);
|
||||
for glyph in 0..GLYPHS {
|
||||
// Index 258 and on are the table's own names, in order.
|
||||
let own = inked.iter().position(|g| u16::from(*g) == glyph);
|
||||
be16(&mut post, own.map_or(0, |i| 258 + i as u16));
|
||||
}
|
||||
for i in 0..inked.len() {
|
||||
let name = format!("bullet{i}");
|
||||
post.push(name.len() as u8);
|
||||
post.extend(name.bytes());
|
||||
}
|
||||
tables.push((*b"post", post));
|
||||
}
|
||||
let mut out = vec![0, 1, 0, 0];
|
||||
be16(&mut out, tables.len() as u16);
|
||||
for n in [64u16, 2, 48] {
|
||||
be16(&mut out, n);
|
||||
}
|
||||
let mut payload = Vec::new();
|
||||
let mut offset = 12 + 16 * tables.len();
|
||||
for (tag, bytes) in tables {
|
||||
out.extend(tag);
|
||||
let mut padded = bytes.clone();
|
||||
padded.resize(bytes.len().div_ceil(4) * 4, 0);
|
||||
let sum = padded.as_chunks::<4>().0.iter().fold(0u32, |s, b| s.wrapping_add(u32::from_be_bytes(*b)));
|
||||
be32(&mut out, sum);
|
||||
be32(&mut out, offset as u32);
|
||||
be32(&mut out, bytes.len() as u32);
|
||||
offset += padded.len();
|
||||
payload.extend(padded);
|
||||
}
|
||||
out.extend(payload);
|
||||
out
|
||||
}
|
||||
|
||||
/// The text extracted from a page that shows `codes` in `font`, marked symbolic and with no
|
||||
/// `/Encoding` and no `/ToUnicode`.
|
||||
fn extracted(font: &[u8], codes: &[u8]) -> String {
|
||||
extracted_with(font, codes, "AAAAAA+Sym", "")
|
||||
}
|
||||
|
||||
/// [`extracted`] with the font dictionary's `/BaseFont` named `base_font` and the `extra` entries.
|
||||
fn extracted_with(font: &[u8], codes: &[u8], base_font: &str, extra: &str) -> String {
|
||||
let hex: String = codes.iter().map(|c| format!("{c:02X}")).collect();
|
||||
let content = format!("BT /F1 30 Tf 5 5 Td <{hex}> Tj ET").into_bytes();
|
||||
let widths = vec!["1000"; 256].join(" ");
|
||||
let objects: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 80 40] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>".to_vec(),
|
||||
[format!("<< /Length {} >>\nstream\n", content.len()).into_bytes(), content.clone(), b"\nendstream".to_vec()].concat(),
|
||||
format!("<< /Type /Font /Subtype /TrueType /BaseFont /{base_font} /FirstChar 0 /LastChar 255 /Widths [{widths}] /FontDescriptor 6 0 R {extra}>>").into_bytes(),
|
||||
b"<< /Type /FontDescriptor /FontName /AAAAAA+Sym /Flags 4 /FontBBox [0 0 1000 1000] /ItalicAngle 0 /Ascent 1000 /Descent 0 /CapHeight 1000 /StemV 80 /FontFile2 7 0 R >>".to_vec(),
|
||||
[format!("<< /Length {} >>\nstream\n", font.len()).into_bytes(), font.to_vec(), b"\nendstream".to_vec()].concat(),
|
||||
];
|
||||
let mut pdf = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(pdf.len());
|
||||
pdf.extend(format!("{} 0 obj\n", i + 1).into_bytes());
|
||||
pdf.extend(object);
|
||||
pdf.extend(b"\nendobj\n");
|
||||
}
|
||||
let xref = pdf.len();
|
||||
pdf.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).into_bytes());
|
||||
for offset in offsets {
|
||||
pdf.extend(format!("{offset:010} 00000 n \n").into_bytes());
|
||||
}
|
||||
pdf.extend(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).into_bytes());
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let out = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0, ..Default::default() });
|
||||
assert!(out.error.is_none(), "{:?}", out.error);
|
||||
out.text.map(|t| t.plain_text()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbolic_truetype_font_in_the_macintosh_order_reads_its_codes_as_mac_roman() {
|
||||
// As issue2017r.pdf's font: code c and 0xF000 + c select the glyph the standard Macintosh
|
||||
// order gives Mac OS Roman c.
|
||||
let map = [(0x88, AGRAVE), (0x8E, EACUTE)];
|
||||
let font = font(&[AGRAVE, EACUTE], Some(&map), &map, false);
|
||||
assert_eq!(extracted(&font, &[0x88, 0x8E]), "àé", "Mac OS Roman 0x88 is à and 0x8E é");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn winansi_codes_of_a_font_in_the_macintosh_order_read_as_before() {
|
||||
// A Windows subset in the same order: code 0x92 draws `quoteright`, WinAnsi 0x92, where Mac OS
|
||||
// Roman 0x92 is `iacute`.
|
||||
let map = [(0x92, QUOTERIGHT), (0x96, ENDASH)];
|
||||
let font = font(&[ENDASH, QUOTERIGHT], Some(&map), &map, false);
|
||||
assert_eq!(extracted(&font, &[0x92, 0x96]), "\u{2019}\u{2013}", "WinAnsi, not Mac OS Roman's íñ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbolic_truetype_font_without_a_1_0_subtable_reads_as_before() {
|
||||
let font = font(&[AGRAVE, EACUTE], None, &[(0x88, AGRAVE), (0x8E, EACUTE)], false);
|
||||
assert_eq!(extracted(&font, &[0x88, 0x8E]), "ˆŽ", "with no (1, 0) subtable the codes read as WinAnsi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_1_0_subtable_selecting_other_glyphs_than_the_drawn_ones_is_not_read() {
|
||||
// The (3, 0) subtable is used first (#4156); a (1, 0) one that disagrees doesn't say what the
|
||||
// drawn glyphs are.
|
||||
let font = font(&[AGRAVE, EACUTE], Some(&[(0x88, EACUTE), (0x8E, AGRAVE)]), &[(0x88, AGRAVE), (0x8E, EACUTE)], false);
|
||||
assert_eq!(extracted(&font, &[0x88, 0x8E]), "ˆŽ", "the codes read as WinAnsi, as before");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_font_that_names_its_glyphs_is_not_read_by_their_order() {
|
||||
// A Wingdings subset draws code 0x83 with glyph 101, `Eacute` in the order, and names it
|
||||
// `threesans`: a font with glyph names needn't keep the order.
|
||||
let map = [(0x88, AGRAVE), (0x8E, EACUTE)];
|
||||
let font = font(&[AGRAVE, EACUTE], Some(&map), &map, true);
|
||||
assert_eq!(extracted(&font, &[0x88, 0x8E]), "ˆŽ", "the codes read as WinAnsi, as before");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_or_zapfdingbats_font_in_the_macintosh_order_gets_no_mac_roman_reading() {
|
||||
// Symbol and ZapfDingbats have built-in encodings that aren't Latin (ISO 32000-2 §9.6.6.1), so their codes get
|
||||
// no implicit Mac OS Roman reading, as the editor's `unicode_map` gives them none.
|
||||
let map = [(0x88, AGRAVE), (0x8E, EACUTE)];
|
||||
let font = font(&[AGRAVE, EACUTE], Some(&map), &map, false);
|
||||
for name in ["ZapfDingbats", "AAAAAA+Symbol"] {
|
||||
let text = extracted_with(&font, &[0x88, 0x8E], name, "");
|
||||
assert_eq!(text, "ˆŽ", "{name}: the codes read as WinAnsi, as before");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn differences_naming_notdef_clear_the_mac_roman_reading_for_its_code() {
|
||||
// `/Differences` without a base encoding: code 0x8E names `.notdef`, and code 0x88 keeps the Mac OS Roman reading.
|
||||
let map = [(0x88, AGRAVE), (0x8E, EACUTE)];
|
||||
let font = font(&[AGRAVE, EACUTE], Some(&map), &map, false);
|
||||
let text = extracted_with(&font, &[0x88, 0x8E], "AAAAAA+Sym", "/Encoding << /Differences [142 /.notdef] >> ");
|
||||
assert_eq!(text, "à", "0x8E reads as nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_base_encoding_name_this_reader_does_not_know_keeps_the_mac_roman_reading_off() {
|
||||
// An unknown `/Encoding` name is read as the built-in encoding, but it names a base encoding all the same, so the
|
||||
// editor gives it no Mac OS Roman reading and neither does the renderer: the codes read as WinAnsi, as before.
|
||||
let map = [(0x88, AGRAVE), (0x8E, EACUTE)];
|
||||
let font = font(&[AGRAVE, EACUTE], Some(&map), &map, false);
|
||||
assert_eq!(extracted_with(&font, &[0x88, 0x8E], "AAAAAA+Sym", "/Encoding /NoSuchEncoding "), "ˆŽ");
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! A TrueType font whose `head.indexToLocFormat` holds neither 0 nor 1 draws through the `loca`
|
||||
//! format its table is in (vendored hayro-interpret patch 83). The OpenType `head` table allows
|
||||
//! only 0 (short offsets) and 1 (long); the glyph reader took anything but 1 as short, so a font
|
||||
//! whose field reads 256 (pdf.js `issue2537r.pdf`) had its long offsets read as short ones, and
|
||||
//! "LINE UP" drew as boxes. ISO 32000-2 §9.9 has an embedded TrueType program drawn as the
|
||||
//! font program it is.
|
||||
//!
|
||||
//! The font is built here, in memory: two glyphs (`.notdef`, empty, and a filled square) and a
|
||||
//! (1, 0) `cmap` subtable mapping `A` to the square. No font file is involved.
|
||||
|
||||
use super::render_first_page;
|
||||
|
||||
fn be16(out: &mut Vec<u8>, n: u16) {
|
||||
out.extend_from_slice(&n.to_be_bytes());
|
||||
}
|
||||
|
||||
fn be32(out: &mut Vec<u8>, n: u32) {
|
||||
out.extend_from_slice(&n.to_be_bytes());
|
||||
}
|
||||
|
||||
/// The font, its `loca` offsets long when `long` and followed by `padding` zero bytes, its
|
||||
/// `head.indexToLocFormat` set to `format`.
|
||||
fn square_font(long: bool, padding: usize, format: u16) -> Vec<u8> {
|
||||
// Glyph 1: one contour, the square 0..1000 on both axes (glyph 0 is empty).
|
||||
let mut glyf = Vec::new();
|
||||
for n in [1u16, 0, 0, 1000, 1000] {
|
||||
be16(&mut glyf, n);
|
||||
}
|
||||
be16(&mut glyf, 3); // the contour's last point
|
||||
be16(&mut glyf, 0); // no instructions
|
||||
glyf.extend([1u8; 4]); // four on-curve points, two-byte coordinates
|
||||
for dx in [0i16, 1000, 0, -1000] {
|
||||
glyf.extend_from_slice(&dx.to_be_bytes());
|
||||
}
|
||||
for dy in [0i16, 0, 1000, 0] {
|
||||
glyf.extend_from_slice(&dy.to_be_bytes());
|
||||
}
|
||||
let mut loca = Vec::new();
|
||||
for offset in [0u32, 0, glyf.len() as u32] {
|
||||
if long { be32(&mut loca, offset) } else { be16(&mut loca, (offset / 2) as u16) }
|
||||
}
|
||||
loca.resize(loca.len() + padding, 0);
|
||||
|
||||
let mut head = vec![0; 54];
|
||||
head[0..4].copy_from_slice(&[0, 1, 0, 0]);
|
||||
head[12..16].copy_from_slice(&0x5f0f_3cf5u32.to_be_bytes());
|
||||
head[18..20].copy_from_slice(&1000u16.to_be_bytes());
|
||||
head[40..42].copy_from_slice(&1000i16.to_be_bytes());
|
||||
head[42..44].copy_from_slice(&1000i16.to_be_bytes());
|
||||
head[50..52].copy_from_slice(&format.to_be_bytes());
|
||||
let mut hhea = vec![0; 36];
|
||||
hhea[0..4].copy_from_slice(&[0, 1, 0, 0]);
|
||||
hhea[4..6].copy_from_slice(&1000i16.to_be_bytes());
|
||||
hhea[34..36].copy_from_slice(&2u16.to_be_bytes());
|
||||
let mut hmtx = Vec::new();
|
||||
for _ in 0..2 {
|
||||
be16(&mut hmtx, 1000);
|
||||
be16(&mut hmtx, 0);
|
||||
}
|
||||
let mut maxp = vec![0, 1, 0, 0];
|
||||
be16(&mut maxp, 2);
|
||||
maxp.resize(32, 0);
|
||||
|
||||
// (1, 0), format 0: `A` → glyph 1 (the square).
|
||||
let mut mac = Vec::new();
|
||||
be16(&mut mac, 0);
|
||||
be16(&mut mac, 262);
|
||||
be16(&mut mac, 0);
|
||||
let mut ids = [0u8; 256];
|
||||
ids[usize::from(b'A')] = 1;
|
||||
mac.extend(ids);
|
||||
let mut cmap = Vec::new();
|
||||
be16(&mut cmap, 0);
|
||||
be16(&mut cmap, 1);
|
||||
be16(&mut cmap, 1);
|
||||
be16(&mut cmap, 0);
|
||||
be32(&mut cmap, 12);
|
||||
cmap.extend(mac);
|
||||
|
||||
let tables = [(*b"cmap", cmap), (*b"glyf", glyf), (*b"head", head), (*b"hhea", hhea), (*b"hmtx", hmtx), (*b"loca", loca), (*b"maxp", maxp)];
|
||||
let mut out = vec![0, 1, 0, 0];
|
||||
be16(&mut out, tables.len() as u16);
|
||||
for n in [64u16, 2, 48] {
|
||||
be16(&mut out, n);
|
||||
}
|
||||
let mut payload = Vec::new();
|
||||
let mut offset = 12 + 16 * tables.len();
|
||||
for (tag, bytes) in tables {
|
||||
out.extend(tag);
|
||||
let mut padded = bytes.clone();
|
||||
padded.resize(bytes.len().div_ceil(4) * 4, 0);
|
||||
let sum = padded.as_chunks::<4>().0.iter().fold(0u32, |s, b| s.wrapping_add(u32::from_be_bytes(*b)));
|
||||
be32(&mut out, sum);
|
||||
be32(&mut out, offset as u32);
|
||||
be32(&mut out, bytes.len() as u32);
|
||||
offset += padded.len();
|
||||
payload.extend(padded);
|
||||
}
|
||||
out.extend(payload);
|
||||
out
|
||||
}
|
||||
|
||||
/// A 40 × 40 pt page that shows `A` at 30 pt from (5, 5) in the font.
|
||||
fn page_with_font(font: &[u8]) -> Vec<u8> {
|
||||
let content = b"BT /F1 30 Tf 5 5 Td (A) Tj ET".to_vec();
|
||||
let objects: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>".to_vec(),
|
||||
[format!("<< /Length {} >>\nstream\n", content.len()).into_bytes(), content.clone(), b"\nendstream".to_vec()].concat(),
|
||||
b"<< /Type /Font /Subtype /TrueType /BaseFont /AAAAAA+Sq /FirstChar 65 /LastChar 65 /Widths [1000] /FontDescriptor 6 0 R >>".to_vec(),
|
||||
b"<< /Type /FontDescriptor /FontName /AAAAAA+Sq /Flags 4 /FontBBox [0 0 1000 1000] /ItalicAngle 0 /Ascent 1000 /Descent 0 /CapHeight 1000 /StemV 80 /FontFile2 7 0 R >>".to_vec(),
|
||||
[format!("<< /Length {} >>\nstream\n", font.len()).into_bytes(), font.to_vec(), b"\nendstream".to_vec()].concat(),
|
||||
];
|
||||
let mut pdf = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(pdf.len());
|
||||
pdf.extend(format!("{} 0 obj\n", i + 1).into_bytes());
|
||||
pdf.extend(object);
|
||||
pdf.extend(b"\nendobj\n");
|
||||
}
|
||||
let xref = pdf.len();
|
||||
pdf.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).into_bytes());
|
||||
for offset in offsets {
|
||||
pdf.extend(format!("{offset:010} 00000 n \n").into_bytes());
|
||||
}
|
||||
pdf.extend(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).into_bytes());
|
||||
pdf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_truetype_font_with_an_out_of_range_loca_format_draws_its_glyphs() {
|
||||
// The square covers (5, 5)–(35, 35) in page space: row 20 from the top is inside it.
|
||||
let square_drawn = |long: bool, padding: usize, format: u16| {
|
||||
let page = render_first_page(page_with_font(&square_font(long, padding, format)));
|
||||
let at = |x: u32, y: u32| page.rgba.get(((y * page.width + x) * 4) as usize).copied().unwrap_or(255);
|
||||
assert_eq!(at(2, 2), 255, "nothing is drawn outside the square");
|
||||
at(20, 20) < 128
|
||||
};
|
||||
assert!(square_drawn(true, 0, 1), "long offsets, marked long");
|
||||
assert!(square_drawn(false, 0, 0), "short offsets, marked short");
|
||||
assert!(square_drawn(true, 0, 0x0100), "long offsets marked 256: the table's length says long");
|
||||
assert!(square_drawn(false, 0, 0x0100), "short offsets marked 256: the table's length says short");
|
||||
// Three short offsets are 6 bytes and three long ones 12: 8 bytes is short ones and padding.
|
||||
assert!(square_drawn(false, 2, 0x0100), "short offsets and padding marked 256: still short");
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! A Type 3 glyph shown many times decodes its glyph procedure once (vendored hayro-interpret
|
||||
//! patch 87). hayro inflated the procedure again for every copy of the glyph, so a font used as
|
||||
//! a fill pattern, one glyph drawn hundreds of thousands of times, took seconds a page.
|
||||
|
||||
use super::image_cache_pdf;
|
||||
use hayro::hayro_interpret::font::Glyph;
|
||||
use hayro::hayro_interpret::hayro_syntax::Pdf;
|
||||
use hayro::hayro_interpret::{
|
||||
BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, SoftMask, TransformExt,
|
||||
interpret_page,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Records the width in device pixels of each path drawn, and draws each Type 3 glyph through its
|
||||
/// procedure, as the renderer does.
|
||||
#[derive(Default)]
|
||||
struct Paths(Vec<f64>);
|
||||
|
||||
impl<'a> Device<'a> for Paths {
|
||||
fn set_soft_mask(&mut self, _: Option<SoftMask<'a>>) {}
|
||||
fn set_blend_mode(&mut self, _: BlendMode) {}
|
||||
fn draw_path(&mut self, path: &BezPath, transform: Affine, _: &Paint<'a>, _: &PathDrawMode) {
|
||||
self.0.push((transform * path.clone()).bounding_box().width());
|
||||
}
|
||||
fn push_clip_path(&mut self, _: &ClipPath) {}
|
||||
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'a>>, _: BlendMode) {}
|
||||
fn draw_glyph(&mut self, glyph: &Glyph<'a>, transform: Affine, glyph_transform: Affine, paint: &Paint<'a>, _: &GlyphDrawMode) {
|
||||
if let Glyph::Type3(glyph) = glyph {
|
||||
glyph.interpret(self, transform, glyph_transform, paint);
|
||||
}
|
||||
}
|
||||
fn draw_image(&mut self, _: Image<'a, '_>, _: Affine) {}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
fn pop_transparency_group(&mut self) {}
|
||||
}
|
||||
|
||||
fn flate(data: &[u8]) -> Vec<u8> {
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
z.write_all(data).unwrap();
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A Type 3 font on an 8 × 8 grid whose glyphs, named `names`, take the codes from 33 on and
|
||||
/// have their procedures in the objects from `first_object` on.
|
||||
fn font(names: &[String], first_object: usize) -> String {
|
||||
let procs: String = names.iter().enumerate().map(|(i, n)| format!("/{n} {} 0 R ", first_object + i)).collect();
|
||||
let names_list: String = names.iter().map(|n| format!("/{n} ")).collect();
|
||||
format!(
|
||||
"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 8 8] /FontMatrix [0.125 0 0 0.125 0 0] /CharProcs << {procs}>> \
|
||||
/Encoding << /Type /Encoding /Differences [33 {names_list}] >> /FirstChar 33 /LastChar {} /Widths [{}] >>",
|
||||
32 + names.len(),
|
||||
"8 ".repeat(names.len())
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders the page of `bytes` through a new cache; the widths of the paths drawn, and how many
|
||||
/// glyph procedures were decoded.
|
||||
fn render(bytes: Vec<u8>) -> (Vec<f64>, usize) {
|
||||
let parsed = Pdf::new(Arc::new(bytes)).expect("parses");
|
||||
let page = &parsed.pages()[0];
|
||||
let initial = page.initial_transform(true).to_kurbo();
|
||||
let cache = InterpreterCache::new();
|
||||
let mut ctx = Context::new(initial, Rect::new(0.0, 0.0, 130.0, 70.0), &cache, page.xref(), InterpreterSettings::default());
|
||||
let mut device = Paths::default();
|
||||
interpret_page(page, &mut ctx, &mut device);
|
||||
(device.0, cache.type3_proc_decodes())
|
||||
}
|
||||
|
||||
/// A Type 3 font whose one glyph, `!`, is a Flate-compressed procedure filling its 8 × 8 box,
|
||||
/// shown 200 times on one page and rendered twice through one cache: every copy draws, and the
|
||||
/// procedure is decoded once, on the first copy.
|
||||
#[test]
|
||||
fn a_type3_glyph_shown_again_decodes_its_procedure_once() {
|
||||
let proc_bytes = flate(b"8 0 0 0 8 8 d1 0 0 8 8 re f");
|
||||
let proc_dict = format!("<< /Filter /FlateDecode /Length {} >>", proc_bytes.len());
|
||||
let content = format!("BT /F1 0.5 Tf 1 0 0 1 2 30 Tm ({}) Tj ET", "!".repeat(200));
|
||||
let resources = format!("<< /Font << /F1 {} >> >>", font(&["dot".into()], 5));
|
||||
let parsed = Pdf::new(Arc::new(image_cache_pdf(&content, &resources, &[(&proc_dict, &proc_bytes)]))).expect("parses");
|
||||
let page = &parsed.pages()[0];
|
||||
let cache = InterpreterCache::new();
|
||||
for render in ["first", "second"] {
|
||||
let initial = page.initial_transform(true).to_kurbo();
|
||||
let mut ctx = Context::new(initial, Rect::new(0.0, 0.0, 130.0, 70.0), &cache, page.xref(), InterpreterSettings::default());
|
||||
let mut device = Paths::default();
|
||||
interpret_page(page, &mut ctx, &mut device);
|
||||
assert_eq!(device.0.len(), 200, "the {render} render draws every copy of the glyph");
|
||||
assert_eq!(cache.type3_proc_decodes(), 1, "after the {render} render: the procedure is decoded once");
|
||||
}
|
||||
}
|
||||
|
||||
/// A procedure that decodes to more than 64 KiB isn't kept: shown three times, it is decoded three
|
||||
/// times, and draws each time.
|
||||
#[test]
|
||||
fn a_procedure_over_64_kib_is_decoded_for_each_copy() {
|
||||
let mut program = b"8 0 0 0 8 8 d1 0 0 8 8 re f".to_vec();
|
||||
program.resize((64 << 10) + 1, b' ');
|
||||
let proc_bytes = flate(&program);
|
||||
let proc_dict = format!("<< /Filter /FlateDecode /Length {} >>", proc_bytes.len());
|
||||
let resources = format!("<< /Font << /F1 {} >> >>", font(&["big".into()], 5));
|
||||
let bytes = image_cache_pdf("BT /F1 0.5 Tf 1 0 0 1 2 30 Tm (!!!) Tj ET", &resources, &[(&proc_dict, &proc_bytes)]);
|
||||
let (widths, decodes) = render(bytes);
|
||||
assert_eq!(widths.len(), 3);
|
||||
assert_eq!(decodes, 3, "a procedure over the size kept is decoded for each copy");
|
||||
}
|
||||
|
||||
/// The fonts of one cache keep at most 32 MiB of procedures together: 600 glyphs of 60,000 bytes
|
||||
/// each, over three fonts (36 MB), each shown twice, all draw; the procedures kept are decoded
|
||||
/// once and the rest twice, so there are more decodes than glyphs and fewer than copies.
|
||||
#[test]
|
||||
fn the_procedures_one_cache_keeps_stop_at_32_mib() {
|
||||
let mut program = b"8 0 0 0 8 8 d1 0 0 8 8 re f".to_vec();
|
||||
program.resize(60_000, b' ');
|
||||
let proc_bytes = flate(&program);
|
||||
let proc_dict = format!("<< /Filter /FlateDecode /Length {} >>", proc_bytes.len());
|
||||
let (fonts, per_font) = (3, 200);
|
||||
let mut resources = String::from("<< /Font << ");
|
||||
let mut content = String::new();
|
||||
for f in 0..fonts {
|
||||
let names: Vec<String> = (0..per_font).map(|g| format!("g{f}x{g}")).collect();
|
||||
resources.push_str(&format!("/F{f} {} ", font(&names, 5 + f * per_font)));
|
||||
let codes: String = (0..per_font).map(|g| format!("{:02x}", 33 + g)).collect();
|
||||
content.push_str(&format!("BT /F{f} 0.05 Tf 1 0 0 1 2 {} Tm <{codes}{codes}> Tj ET ", 10 + 20 * f));
|
||||
}
|
||||
resources.push_str(">> >>");
|
||||
let objects: Vec<(&str, &[u8])> = (0..fonts * per_font).map(|_| (proc_dict.as_str(), proc_bytes.as_slice())).collect();
|
||||
let bytes = image_cache_pdf(&content, &resources, &objects);
|
||||
let (widths, decodes) = render(bytes);
|
||||
assert_eq!(widths.len(), 2 * fonts * per_font, "every copy draws");
|
||||
assert!(decodes > fonts * per_font && decodes < 2 * fonts * per_font, "{decodes} decodes: some procedures kept, not all");
|
||||
}
|
||||
|
||||
/// Two fonts each with a glyph named `dot`, one filling 8 × 8 and the other 4 × 4, draw their own
|
||||
/// procedures: what one font keeps isn't looked up by another with the same glyph name.
|
||||
#[test]
|
||||
fn fonts_with_the_same_glyph_name_keep_their_own_procedures() {
|
||||
let (big, small) = (b"8 0 0 0 8 8 d1 0 0 8 8 re f".to_vec(), b"8 0 0 0 8 8 d1 0 0 4 4 re f".to_vec());
|
||||
let dict = |data: &[u8]| format!("<< /Length {} >>", data.len());
|
||||
let (big_dict, small_dict) = (dict(&big), dict(&small));
|
||||
let resources = format!("<< /Font << /A {} /B {} >> >>", font(&["dot".into()], 5), font(&["dot".into()], 6));
|
||||
let bytes = image_cache_pdf(
|
||||
"BT /A 8 Tf 1 0 0 1 2 30 Tm (!!) Tj /B 8 Tf (!!) Tj ET",
|
||||
&resources,
|
||||
&[(big_dict.as_str(), big.as_slice()), (small_dict.as_str(), small.as_slice())],
|
||||
);
|
||||
let (widths, _) = render(bytes);
|
||||
assert_eq!(widths.len(), 4);
|
||||
let (a, b) = (widths[0], widths[3]);
|
||||
assert!((a - 2.0 * b).abs() < 0.01, "font A's glyph is twice as wide as font B's: {widths:?}");
|
||||
assert_eq!((widths[1], widths[2]), (a, b), "each font draws its own glyph again: {widths:?}");
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
//! A Type 3 font without `/Widths` advances each glyph by the width its glyph procedure sets
|
||||
//! with `d0` or `d1` (vendored hayro-interpret patch 90). ISO 32000-2 Table 110 requires
|
||||
//! `/Widths` in a Type 3 font, and §9.6.4 has the procedure state the same width: "wx … shall
|
||||
//! be consistent with the corresponding width in the font's Widths array". A font whose
|
||||
//! `/Widths` key was misspelt (PDFBOX-2943) gave every glyph no advance, so its text was drawn
|
||||
//! as one blot and read as characters stacked on one spot. PDFium takes the procedure's width
|
||||
//! here. A font that has `/Widths` is unchanged: Table 110 gives a code outside `/FirstChar` to
|
||||
//! `/LastChar` width 0, as Poppler and MuPDF draw it.
|
||||
//!
|
||||
//! The glyphs are squares 400 units wide (`a`, set by `d1`, advance 500) and 600 units wide
|
||||
//! (`b`, set by `d0`, advance 700), shown at 100 pt from x = 20.
|
||||
|
||||
use super::render_first_page;
|
||||
use crate::RenderWarning;
|
||||
use crate::text::extract_page;
|
||||
use hayro::hayro_interpret::InterpreterSettings;
|
||||
use hayro::hayro_interpret::hayro_syntax::Pdf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A page showing `aabb` in a Type 3 font whose dictionary ends with `widths`.
|
||||
fn type3_page(widths: &str) -> Vec<u8> {
|
||||
type3_page_with(widths, "aabb", "700 0 d0 0 0 600 600 re f")
|
||||
}
|
||||
|
||||
/// A page showing `text` in a Type 3 font whose dictionary ends with `widths` and whose `b`
|
||||
/// glyph is `glyph_b`.
|
||||
fn type3_page_with(widths: &str, text: &str, glyph_b: &str) -> Vec<u8> {
|
||||
let content = format!("BT /F1 100 Tf 20 100 Td ({text}) Tj ET");
|
||||
let glyph_a = "500 0 0 0 400 400 d1 0 0 400 400 re f";
|
||||
let objects = [
|
||||
"<< /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 200] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>".to_string(),
|
||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
||||
format!(
|
||||
"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1000 1000] /FontMatrix [0.001 0 0 0.001 0 0] \
|
||||
/CharProcs << /a 6 0 R /b 7 0 R >> /Encoding << /Type /Encoding /Differences [97 /a /b] >> {widths} >>"
|
||||
),
|
||||
format!("<< /Length {} >>\nstream\n{glyph_a}\nendstream", glyph_a.len()),
|
||||
format!("<< /Length {} >>\nstream\n{glyph_b}\nendstream", glyph_b.len()),
|
||||
];
|
||||
let mut pdf = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(pdf.len());
|
||||
pdf.extend(format!("{} 0 obj\n{object}\nendobj\n", i + 1).into_bytes());
|
||||
}
|
||||
let xref = pdf.len();
|
||||
pdf.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).into_bytes());
|
||||
for offset in offsets {
|
||||
pdf.extend(format!("{offset:010} 00000 n \n").into_bytes());
|
||||
}
|
||||
pdf.extend(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).into_bytes());
|
||||
pdf
|
||||
}
|
||||
|
||||
/// The columns (start, end) of the rendered page that hold ink, as runs.
|
||||
fn ink_runs(pdf: Vec<u8>) -> Vec<(u32, u32)> {
|
||||
let page = render_first_page(pdf);
|
||||
let inked = |x: u32| (0..page.height).any(|y| page.rgba.get(((y * page.width + x) * 4) as usize).is_some_and(|v| *v < 128));
|
||||
let mut runs = Vec::new();
|
||||
let mut start = None;
|
||||
for x in 0..=page.width {
|
||||
match (x < page.width && inked(x), start) {
|
||||
(true, None) => start = Some(x),
|
||||
(false, Some(s)) => {
|
||||
runs.push((s, x));
|
||||
start = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
runs
|
||||
}
|
||||
|
||||
/// The left edge of each extracted glyph, rounded to a point.
|
||||
fn glyph_lefts(pdf: Vec<u8>) -> Vec<i32> {
|
||||
let pdf = Pdf::new(Arc::new(pdf)).expect("parses");
|
||||
let text = extract_page(&pdf, 0, &InterpreterSettings::default()).expect("text");
|
||||
text.glyphs.iter().map(|g| g.rect[0].round() as i32).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_type3_font_without_widths_advances_by_its_glyph_procedures() {
|
||||
// Each glyph in its own place: a at 20 and 70 (d1 width 500), b at 120 and 190 (d0 700).
|
||||
for widths in ["", "/FirstChar 97 /LastChar 98 /WXdths [300 300]"] {
|
||||
assert_eq!(ink_runs(type3_page(widths)), [(20, 60), (70, 110), (120, 180), (190, 250)], "{widths:?}");
|
||||
assert_eq!(glyph_lefts(type3_page(widths)), [20, 70, 120, 190], "{widths:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_code_outside_first_char_to_last_char_still_has_width_zero() {
|
||||
// a takes /Widths (300); b, past /LastChar, has width 0 (Table 110), whatever d0 says.
|
||||
let widths = "/FirstChar 97 /LastChar 97 /Widths [300]";
|
||||
assert_eq!(ink_runs(type3_page(widths)), [(20, 140)]);
|
||||
assert_eq!(glyph_lefts(type3_page(widths)), [20, 50, 80]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_the_font_gives_are_kept() {
|
||||
// /Widths says 300 for both, whatever the procedures say: the glyphs overlap, as written.
|
||||
let widths = "/FirstChar 97 /LastChar 98 /Widths [300 300]";
|
||||
assert_eq!(ink_runs(type3_page(widths)), [(20, 170)]);
|
||||
assert_eq!(glyph_lefts(type3_page(widths)), [20, 50, 80, 110]);
|
||||
// An explicit 0 is an entry too: every glyph stays at x = 20 (text extraction keeps one
|
||||
// of each glyph drawn again on the same spot).
|
||||
let zero = "/FirstChar 97 /LastChar 98 /Widths [0 0]";
|
||||
assert_eq!(glyph_lefts(type3_page(zero)), [20, 20]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_code_without_a_usable_procedure_width_still_advances_nothing() {
|
||||
// b's d0 has a name for its width and c has no procedure: neither advances, and neither
|
||||
// panics.
|
||||
assert_eq!(glyph_lefts(type3_page_with("", "bcb", "/x 0 d0 0 0 600 600 re f")), [20, 20]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_type3_font_without_widths_is_reported() {
|
||||
// The page draws, and says once that a font's /Widths was missing; a font with /Widths
|
||||
// (even one that leaves `b` out) says nothing.
|
||||
for widths in ["", "/FirstChar 97 /LastChar 98 /WXdths [300 300]"] {
|
||||
let page = render_first_page(type3_page(widths));
|
||||
assert_eq!(page.warnings, [RenderWarning::Type3WidthsMissing], "{widths:?}");
|
||||
}
|
||||
for widths in ["/FirstChar 97 /LastChar 98 /Widths [300 300]", "/FirstChar 97 /LastChar 97 /Widths [300]"] {
|
||||
assert!(render_first_page(type3_page(widths)).warnings.is_empty(), "{widths:?}");
|
||||
}
|
||||
}
|
||||
@@ -458,9 +458,17 @@ impl<'a> Device<'a> for TextDevice {
|
||||
eprintln!("{kind} {text:?} transform={:?} glyph_transform={:?}", transform.as_coeffs(), glyph_transform.as_coeffs());
|
||||
}
|
||||
let advance = match glyph {
|
||||
Glyph::Outline(g) => {
|
||||
g.advance_width().filter(|a| *a > 0.0).map(f64::from).unwrap_or_else(|| g.outline().bounding_box().width().max(500.0))
|
||||
}
|
||||
// A vertical font's advance runs down the column; across it, the glyph is as wide as
|
||||
// its horizontal width w0, centred on the column by default (ISO 32000-2 §9.7.4.3). Taking
|
||||
// the outline's width instead gave a column of Latin letters boxes of uneven widths,
|
||||
// off its centre, and a narrow letter cut the line's gap allowance below the spacing
|
||||
// of widely tracked text, so the column fell apart into lines.
|
||||
Glyph::Outline(g) => g
|
||||
.vertical_glyph_width()
|
||||
.or_else(|| g.advance_width())
|
||||
.filter(|a| a.is_finite() && *a > 0.0)
|
||||
.map(f64::from)
|
||||
.unwrap_or_else(|| g.outline().bounding_box().width().max(500.0)),
|
||||
Glyph::Type3(g) => g.advance_width().filter(|a| a.is_finite() && *a > 0.0).map(f64::from).unwrap_or(600.0),
|
||||
};
|
||||
// The baseline direction in view space (y down).
|
||||
|
||||
@@ -664,10 +664,12 @@ fn the_annex_d2_alias_codes_read_as_an_ordinary_hyphen_and_space() {
|
||||
|
||||
/// #3169, #3170: glyph names outside the Adobe Glyph List still read as text. Ligature
|
||||
/// (`f_i`) and suffixed (`hyphen.case`) names go by the glyph list specification's full
|
||||
/// mapping. A name that only numbers the glyph (`c251` small caps, `C39`, `C174`, `C39.sc`)
|
||||
/// reads as nothing, not as a guess from the code (review on #3239: with `/C1` at 233,
|
||||
/// StandardEncoding's 233 would read `Ø` for a glyph drawn as `é`), and so do a ZapfDingbats
|
||||
/// name (`a12`) and a name with no meaning left (`foo`).
|
||||
/// mapping. A name that only numbers the glyph reads as nothing, not as a guess from the code
|
||||
/// (review on #3239: with `/C1` at 233, StandardEncoding's 233 would read `Ø` for a glyph drawn
|
||||
/// as `é`). `C39` at 39 and `C174` at 174 name their own codes, but this font's other numbered
|
||||
/// names (`c251` at 97, `C39.sc` at 61, `C1` at 233, `a12` at 88) don't, so they read as
|
||||
/// nothing too (see `numbered_glyph_names_read_as_their_own_code`). A name with no meaning
|
||||
/// left (`foo`) reads as nothing.
|
||||
#[test]
|
||||
fn glyph_names_outside_the_glyph_list_still_read_as_text() {
|
||||
use lopdf::{Object, dictionary};
|
||||
@@ -696,11 +698,145 @@ fn glyph_names_outside_the_glyph_list_still_read_as_text() {
|
||||
assert_eq!(lines, ["fi", "fl", "-", "fi"], "{text:?}");
|
||||
}
|
||||
|
||||
/// A glyph name that only numbers its glyph, with no `/ToUnicode` (ISO 32000-2 §9.10.2 has no
|
||||
/// route for it), reads as its code's Latin-1 character when the number is that very code, in
|
||||
/// decimal (`C70` at 70, `C0071` at 71, `MT72` at 72, `C39` at 39, `C174` at 174) or
|
||||
/// hexadecimal (`G49` at 0x49, `GE9` at 0xE9), as pdftotext, PDFium and MuPDF read
|
||||
/// it: Windows printer drivers and typesetters name subset glyphs so. Only in a font where every
|
||||
/// numbered name names its own code: one more name numbering another code (`/C1` at 233, review
|
||||
/// on #3239) turns the whole font back to nothing, and so does a font whose names number
|
||||
/// something else, where a name can match its code by coincidence (`/MT40` at 0x40). A subset
|
||||
/// that counts its glyphs in order of first use from code 1 (`/Differences [1 /C1 /C2 … /C80]`)
|
||||
/// names its own codes too, but `/C65` there is the 65th glyph used, not `A`: a numbered name at
|
||||
/// a control code (`C1` at 1, `C9` at 9) gives the counter away and the font reads nothing.
|
||||
/// Code 32 is the first code a numbered name may name: `C32` at 32 reads as a space.
|
||||
#[test]
|
||||
fn numbered_glyph_names_read_as_their_own_code() {
|
||||
use lopdf::{Object, dictionary};
|
||||
let name = |n: &str| Object::Name(n.as_bytes().to_vec());
|
||||
let encoding = |differences: Vec<Object>| Object::Dictionary(dictionary! { "Type" => "Encoding", "Differences" => differences });
|
||||
// One glyph per line, as in `glyph_names_outside_the_glyph_list_still_read_as_text`.
|
||||
let one_per_line = |codes: &[u8]| {
|
||||
let mut content = b"BT /F1 20 Tf 20 290 Td".to_vec();
|
||||
for code in codes {
|
||||
content.extend_from_slice(format!(" <{code:02x}> Tj 0 -25 Td").as_bytes());
|
||||
}
|
||||
content.extend_from_slice(b" ET");
|
||||
content
|
||||
};
|
||||
let own = encoding(vec![
|
||||
Object::Integer(70),
|
||||
name("C70"),
|
||||
name("C0071"),
|
||||
name("MT72"),
|
||||
name("G49"),
|
||||
Object::Integer(233),
|
||||
name("GE9"),
|
||||
Object::Integer(39),
|
||||
name("C39"),
|
||||
Object::Integer(174),
|
||||
name("C174"),
|
||||
]);
|
||||
let one_other = encoding(vec![Object::Integer(70), name("C70"), Object::Integer(233), name("C1")]);
|
||||
let numbered_otherwise = encoding(vec![Object::Integer(1), name("C70"), name("C105"), Object::Integer(64), name("MT40")]);
|
||||
let counter = encoding(std::iter::once(Object::Integer(1)).chain((1..=80).map(|n| name(&format!("C{n}")))).collect());
|
||||
let own_and_a_control = encoding(vec![Object::Integer(9), name("C9"), Object::Integer(70), name("C70"), name("C0071")]);
|
||||
// Code 32 is the first code a numbered name may name: `/C32` at 32 is a space, not a counter.
|
||||
let own_from_32 = encoding(vec![Object::Integer(32), name("C32"), Object::Integer(70), name("C70"), name("C0071")]);
|
||||
let pages = [
|
||||
one_per_line(b"FGHI\xe9'\xae"),
|
||||
one_per_line(b"F\xe9"),
|
||||
one_per_line(b"\x01\x02@"),
|
||||
one_per_line(b"\x01\x20\x41\x42\x50"),
|
||||
one_per_line(b"\x09FG"),
|
||||
b"BT /F1 20 Tf 20 290 Td <462047> Tj ET".to_vec(),
|
||||
];
|
||||
let pdf = encoded_pages(&[
|
||||
(own, &pages[0][..]),
|
||||
(one_other, &pages[1][..]),
|
||||
(numbered_otherwise, &pages[2][..]),
|
||||
(counter, &pages[3][..]),
|
||||
(own_and_a_control, &pages[4][..]),
|
||||
(own_from_32, &pages[5][..]),
|
||||
]);
|
||||
let lines = |page| {
|
||||
let text = extract_page(&pdf, page, &InterpreterSettings::default()).unwrap().plain_text();
|
||||
text.lines().map(str::trim).filter(|l| !l.is_empty()).map(String::from).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(lines(0), ["F", "G", "H", "I", "\u{e9}", "'", "\u{ae}"]);
|
||||
assert!(lines(1).is_empty(), "/C1 at 233 beside /C70 at 70: {:?}", lines(1));
|
||||
assert!(lines(2).is_empty(), "/MT40 at 0x40 among names numbering other codes: {:?}", lines(2));
|
||||
assert!(lines(3).is_empty(), "a first-use counter /C1 … /C80 from code 1: {:?}", lines(3));
|
||||
assert!(lines(4).is_empty(), "/C9 at 9 beside /C70 at 70: {:?}", lines(4));
|
||||
let page5 = extract_page(&pdf, 5, &InterpreterSettings::default()).unwrap().plain_text();
|
||||
assert_eq!(page5.trim_matches('\n'), "F G", "/C32 at 32 beside /C70 at 70 reads as a space");
|
||||
}
|
||||
|
||||
/// `numbered_glyph_names_read_as_their_own_code` in a Type 3 font, which already reads codes
|
||||
/// 32-126 by the code: above 126, `C174` at 174, `C176` at 176 and `GE9` at 0xE9 read as the
|
||||
/// code's Latin-1 character, unless one numbered name in the font numbers another code
|
||||
/// (`/C1` at 233) or sits at a control code, as a first-use counter's `/C1` at 1 does.
|
||||
#[test]
|
||||
fn numbered_type3_glyph_names_read_as_their_own_code() {
|
||||
use lopdf::{Document, Object, Stream, dictionary};
|
||||
let name = |n: &str| Object::Name(n.as_bytes().to_vec());
|
||||
// Each font's /Differences as (code, glyph name) pairs, and the codes its page shows.
|
||||
type Names<'a> = &'a [(i64, &'a str)];
|
||||
let fonts: [(Names, &[u8]); 3] = [
|
||||
(&[(70, "C70"), (174, "C174"), (176, "C176"), (233, "GE9")], b"F\xae\xb0\xe9"),
|
||||
(&[(174, "C174"), (176, "C176"), (233, "C1")], b"\xae\xb0\xe9"),
|
||||
(&[(1, "C1"), (2, "C2"), (174, "C174"), (176, "C176")], b"\xae\xb0"),
|
||||
];
|
||||
let mut doc = Document::with_version("1.7");
|
||||
let pages_id = doc.new_object_id();
|
||||
let mut kids = Vec::new();
|
||||
for (names, codes) in fonts {
|
||||
let glyph = doc.add_object(Stream::new(dictionary! {}, b"600 0 0 0 500 700 d1 0 0 500 700 re f".to_vec()));
|
||||
let mut differences = Vec::new();
|
||||
let mut procs = lopdf::Dictionary::new();
|
||||
for &(code, glyph_name) in names {
|
||||
differences.extend([Object::Integer(code), name(glyph_name)]);
|
||||
procs.set(glyph_name, glyph);
|
||||
}
|
||||
let font = doc.add_object(dictionary! {
|
||||
"Type" => "Font", "Subtype" => "Type3",
|
||||
"FontBBox" => vec![0.into(), 0.into(), 1000.into(), 1000.into()],
|
||||
"FontMatrix" => vec![0.001.into(), 0.into(), 0.into(), 0.001.into(), 0.into(), 0.into()],
|
||||
"CharProcs" => procs,
|
||||
"Encoding" => dictionary! { "Type" => "Encoding", "Differences" => differences },
|
||||
"FirstChar" => 0, "LastChar" => 255, "Widths" => vec![Object::Integer(600); 256],
|
||||
});
|
||||
// One glyph per line, as in `glyph_names_outside_the_glyph_list_still_read_as_text`.
|
||||
let mut content = b"BT /T3 20 Tf 20 290 Td".to_vec();
|
||||
for code in codes {
|
||||
content.extend_from_slice(format!(" <{code:02x}> Tj 0 -25 Td").as_bytes());
|
||||
}
|
||||
content.extend_from_slice(b" ET");
|
||||
let content = doc.add_object(Stream::new(dictionary! {}, content));
|
||||
let page = doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages_id, "MediaBox" => vec![0.into(), 0.into(), 300.into(), 300.into()], "Resources" => dictionary! {"Font" => dictionary! {"T3" => font}}, "Contents" => content });
|
||||
kids.push(Object::Reference(page));
|
||||
}
|
||||
let count = kids.len() as i64;
|
||||
doc.objects.insert(pages_id, dictionary! {"Type" => "Pages", "Kids" => kids, "Count" => count}.into());
|
||||
let catalog = doc.add_object(dictionary! {"Type" => "Catalog", "Pages" => pages_id});
|
||||
doc.trailer.set("Root", catalog);
|
||||
let mut bytes = Vec::new();
|
||||
doc.save_to(&mut bytes).unwrap();
|
||||
let pdf = Pdf::new(std::sync::Arc::new(bytes)).unwrap();
|
||||
let lines = |page| {
|
||||
let text = extract_page(&pdf, page, &InterpreterSettings::default()).unwrap().plain_text();
|
||||
text.lines().map(str::trim).filter(|l| !l.is_empty()).map(String::from).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(lines(0), ["F", "\u{ae}", "\u{b0}", "\u{e9}"]);
|
||||
assert!(lines(1).is_empty(), "/C1 at 233 beside /C174 at 174: {:?}", lines(1));
|
||||
assert!(lines(2).is_empty(), "a counter's /C1 at 1 beside /C174 at 174: {:?}", lines(2));
|
||||
}
|
||||
|
||||
/// Review on #3239: the glyph list specification's `uni` and `u` forms take uppercase hex
|
||||
/// digits only, and map a name with lowercase ones to nothing ("uni20ac" in its examples).
|
||||
/// Names upstream's own lookup can't read (several groups, a suffix) show it: lowercase reads
|
||||
/// as nothing, uppercase as its characters. A bare `uni20ac` or `u1040c` is still read by
|
||||
/// upstream's lenient `unicode_from_name`, outside this mapping.
|
||||
/// as nothing, uppercase as its characters. A bare `uni20ac` or `u1040c` reads as nothing too
|
||||
/// (`agl_unicode_from_name_rule_reads_text`).
|
||||
#[test]
|
||||
fn glyph_names_with_lowercase_hex_read_as_nothing() {
|
||||
use lopdf::{Object, dictionary};
|
||||
@@ -720,6 +856,34 @@ fn glyph_names_with_lowercase_hex_read_as_nothing() {
|
||||
assert_eq!(lines, ["\u{20ac}\u{308}", "\u{1040c}", "f", "f\u{20ac}"], "{text:?}");
|
||||
}
|
||||
|
||||
/// The glyph list specification's `uni` form is `uni` and groups of exactly four uppercase hex
|
||||
/// digits, its `u` form `u` and four to six, outside the surrogate range: `uni65`, `u65`,
|
||||
/// `uni20ac`, `u1f600`, `uni+065` and `uniD800` name no character, and their glyphs read as
|
||||
/// nothing rather than as `e`, `€` or an emoji. The well-formed names read as before.
|
||||
#[test]
|
||||
fn agl_unicode_from_name_rule_reads_text() {
|
||||
use lopdf::{Object, dictionary};
|
||||
let name = |n: &str| Object::Name(n.as_bytes().to_vec());
|
||||
let names = ["uni65", "u65", "uni20ac", "u1f600", "uni+065", "uniD800", "u10FFFF0", "uni0065", "u0066", "u1F600", "uni20AC"];
|
||||
let differences = Object::Dictionary(dictionary! {
|
||||
"Type" => "Encoding",
|
||||
"Differences" => std::iter::once(Object::Integer(65)).chain(names.iter().map(|n| name(n))).collect::<Vec<_>>(),
|
||||
});
|
||||
// One glyph per line, so each name's text stays its own (and a name read as nothing
|
||||
// leaves no line).
|
||||
let mut content = b"BT /F1 20 Tf 20 290 Td".to_vec();
|
||||
for code in (b'A'..).take(names.len()) {
|
||||
content.extend_from_slice(b" (");
|
||||
content.push(code);
|
||||
content.extend_from_slice(b") Tj 0 -25 Td");
|
||||
}
|
||||
content.extend_from_slice(b" ET");
|
||||
let pdf = encoded_pages(&[(differences, &content[..])]);
|
||||
let text = extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap().plain_text();
|
||||
let lines: Vec<&str> = text.lines().map(str::trim).filter(|l| !l.is_empty()).collect();
|
||||
assert_eq!(lines, ["e", "f", "\u{1f600}", "\u{20ac}"], "{text:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn narrow_ligatures_keep_every_letter() {
|
||||
// Split evenly, this "ffi" put its two "f"s so close that the second was dropped as a
|
||||
@@ -771,6 +935,113 @@ fn vertical_ligatures_stay_in_their_column() {
|
||||
assert_eq!(text.line_rects(0..3).len(), 1, "one column");
|
||||
}
|
||||
|
||||
/// A column of Latin capitals in a vertical font, tracked out with negative character spacing
|
||||
/// (pdf.js issue 20930: `-40 Tc` puts 40 pt between 24 pt letters). Across the column each glyph
|
||||
/// is as wide as its `/W` width w0 and centred on the column (ISO 32000-2 §9.7.4.3), so the
|
||||
/// letters stay one line and Find matches the word. Boxes as wide as each outline made a narrow
|
||||
/// letter's line allowance smaller than the tracking, and the column broke into lines.
|
||||
#[test]
|
||||
fn tracked_vertical_latin_stays_one_line() {
|
||||
tracked_vertical_latin(-40.0, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontally_scaled_vertical_latin_box_is_scaled_across_the_column() {
|
||||
// Horizontal scaling Th = Tz / 100 scales the glyph's width w0 across the column (ISO 32000-2
|
||||
// §9.4.4, the text rendering matrix), and not the advance down it. The tracking is 20 pt here:
|
||||
// the line's gap allowance is three times the narrower box across it, 3 × 8 pt at 50 Tz.
|
||||
tracked_vertical_latin(-20.0, 50.0);
|
||||
}
|
||||
|
||||
/// `ABCDE` down one Identity-V column at 24 pt with `tc Tc` and `tz Tz`.
|
||||
fn tracked_vertical_latin(tc: f32, tz: f32) {
|
||||
let text = vertical_latin(
|
||||
&[(36, 667), (37, 667), (38, 722), (39, 722), (40, 667)],
|
||||
None,
|
||||
&format!("BT /F1 24 Tf {tc} Tc {tz} Tz 1 0 0 1 100 340 Tm <00240025002600270028> Tj ET"),
|
||||
);
|
||||
assert_eq!(text.plain_text(), "ABCDE");
|
||||
assert_eq!(text.find("ABCDE"), vec![0..5]);
|
||||
// Each box spans w0 (here 24 pt × 0.667 or 0.722, times Th) centred on the column at x = 100.
|
||||
assert_eq!(text.glyphs.len(), 5);
|
||||
for (g, w0) in text.glyphs.iter().zip([667.0, 667.0, 722.0, 722.0, 667.0]) {
|
||||
let [x0, _, x1, _] = g.rect;
|
||||
assert!((x1 - x0 - 24.0 * w0 / 1000.0 * tz / 100.0).abs() < 0.01, "{:?} is w0 wide: {:?}", g.text, g.rect);
|
||||
assert!(((x0 + x1) / 2.0 - 100.0).abs() < 0.01, "{:?} is centred on the column: {:?}", g.text, g.rect);
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of a page showing `content` in a vertical CIDFontType2 `/F1` with `/DW 1000`, the
|
||||
/// `widths` (CID, w0) in `/W`, and CIDs 36 to 41 reading `A` to `F`. `cmap` is the body of an
|
||||
/// embedded vertical CMap mapping codes to CIDs (`begincidchar` … `endcidchar`); `None` uses
|
||||
/// Identity-V.
|
||||
fn vertical_latin(widths: &[(i64, i64)], cmap: Option<&str>, content: &str) -> crate::text::PageText {
|
||||
use lopdf::{Document, Object, Stream, dictionary};
|
||||
let mut doc = Document::with_version("1.7");
|
||||
let to_unicode = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def /CMapName /TestUnicode def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange 6 beginbfchar <0024> <0041> <0025> <0042> <0026> <0043> <0027> <0044> <0028> <0045> <0029> <0046> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
let unicode = doc.add_object(Stream::new(dictionary! {}, to_unicode.to_vec()));
|
||||
let widths: Vec<Object> = widths.iter().flat_map(|&(cid, w)| [Object::Integer(cid), Object::Array(vec![Object::Integer(w)])]).collect();
|
||||
let cid = doc.add_object(dictionary! {
|
||||
"Type" => "Font", "Subtype" => "CIDFontType2", "BaseFont" => "TestVerticalLatin",
|
||||
"CIDSystemInfo" => dictionary! { "Registry" => Object::string_literal("Adobe"), "Ordering" => Object::string_literal("Identity"), "Supplement" => 0 },
|
||||
"DW" => 1000, "W" => widths, "DW2" => vec![Object::Integer(880), Object::Integer(-1000)], "CIDToGIDMap" => "Identity"
|
||||
});
|
||||
let encoding = match cmap {
|
||||
Some(body) => {
|
||||
let stream = format!(
|
||||
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> def /CMapName /TestCodes-V def /CMapType 1 def /WMode 1 def 1 begincodespacerange <0000> <FFFF> endcodespacerange {body} endcmap CMapName currentdict /CMap defineresource pop end end"
|
||||
);
|
||||
Object::Reference(doc.add_object(Stream::new(
|
||||
dictionary! { "Type" => "CMap", "CMapName" => "TestCodes-V", "WMode" => 1, "CIDSystemInfo" => dictionary! { "Registry" => Object::string_literal("Adobe"), "Ordering" => Object::string_literal("Identity"), "Supplement" => 0 } },
|
||||
stream.into_bytes(),
|
||||
)))
|
||||
}
|
||||
None => Object::Name(b"Identity-V".to_vec()),
|
||||
};
|
||||
let font = doc.add_object(dictionary! { "Type" => "Font", "Subtype" => "Type0", "BaseFont" => "TestVerticalLatin", "Encoding" => encoding, "DescendantFonts" => vec![Object::Reference(cid)], "ToUnicode" => unicode });
|
||||
let content = doc.add_object(Stream::new(dictionary! {}, content.as_bytes().to_vec()));
|
||||
let pages = doc.new_object_id();
|
||||
let page = doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages, "MediaBox" => vec![Object::Integer(0), Object::Integer(0), Object::Integer(400), Object::Integer(500)], "Resources" => dictionary! {"Font" => dictionary! {"F1" => font}}, "Contents" => content });
|
||||
doc.objects.insert(pages, dictionary! {"Type" => "Pages", "Kids" => vec![Object::Reference(page)], "Count" => 1}.into());
|
||||
let catalog = doc.add_object(dictionary! {"Type" => "Catalog", "Pages" => pages});
|
||||
doc.trailer.set("Root", catalog);
|
||||
let mut bytes = Vec::new();
|
||||
doc.save_to(&mut bytes).unwrap();
|
||||
let pdf = Pdf::new(std::sync::Arc::new(bytes)).unwrap();
|
||||
extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap()
|
||||
}
|
||||
|
||||
/// A CID that `/W` doesn't list is `/DW` wide across the column (ISO 32000-2 §9.7.4.3), and the
|
||||
/// widths are looked up by CID, not by character code: an embedded vertical CMap maps the codes
|
||||
/// `<0024>`… (which `/W` lists 500 wide as CIDs) to CIDs 50…. The tracked column stays one line
|
||||
/// that Find matches.
|
||||
#[test]
|
||||
fn vertical_latin_widths_come_from_dw_and_are_looked_up_by_cid() {
|
||||
let cmap = "5 begincidchar <0024> 50 <0025> 51 <0026> 52 <0027> 53 <0028> 54 endcidchar";
|
||||
// CID 52 (`C`) has no /W entry: it is /DW = 1000 wide.
|
||||
let widths = [(36, 500), (37, 500), (38, 500), (39, 500), (40, 500), (50, 667), (51, 667), (53, 722), (54, 667)];
|
||||
let text = vertical_latin(&widths, Some(cmap), "BT /F1 24 Tf -40 Tc 1 0 0 1 100 340 Tm <00240025002600270028> Tj ET");
|
||||
assert_eq!(text.plain_text(), "ABCDE");
|
||||
assert_eq!(text.find("ABCDE"), vec![0..5]);
|
||||
let got: Vec<f32> = text.glyphs.iter().map(|g| g.rect[2] - g.rect[0]).collect();
|
||||
assert_eq!(got.len(), 5);
|
||||
for (w, w0) in got.iter().zip([667.0, 667.0, 1000.0, 722.0, 667.0]) {
|
||||
assert!((w - 24.0 * w0 / 1000.0).abs() < 0.01, "{got:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Two vertical columns of `/DW`-wide letters set 13.5 pt apart at 20 pt overlap across the
|
||||
/// column (each box is 20 pt wide), and still read as two lines, in order, with Find matching
|
||||
/// each column and not a word spanning both.
|
||||
#[test]
|
||||
fn close_vertical_columns_of_dw_wide_letters_stay_apart() {
|
||||
let text = vertical_latin(&[], None, "BT /F1 20 Tf 1 0 0 1 113.5 340 Tm <002400250026> Tj 1 0 0 1 100 340 Tm <002700280029> Tj ET");
|
||||
assert_eq!(text.plain_text().lines().map(str::trim).collect::<Vec<_>>(), ["ABC", "DEF"]);
|
||||
assert_eq!(text.find("ABC").len(), 1);
|
||||
assert_eq!(text.find("DEF").len(), 1);
|
||||
assert!(text.find("CD").is_empty() && text.find("ABCDEF").is_empty(), "{:?}", text.plain_text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upright_mapping_round_trips() {
|
||||
let r = [10.0, 20.0, 30.0, 25.0];
|
||||
@@ -2176,6 +2447,161 @@ fn text_outside_the_crop_box_is_clipped() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-page document showing `codes` in a Type 0 font with `encoding` over an embedded
|
||||
/// `descendant` (`CIDFontType0` takes the bundled CFF program, `CIDFontType2` a TrueType one)
|
||||
/// of the Adobe-Japan1 collection, with `to_unicode` as its `/ToUnicode` CMap when given.
|
||||
fn japan1_page(encoding: &str, descendant: &str, to_unicode: Option<&[u8]>, codes: &str) -> String {
|
||||
collection_page(("Adobe", "Japan1"), encoding, descendant, to_unicode, codes)
|
||||
}
|
||||
|
||||
/// [`japan1_page`] for the character collection `(registry, ordering)`.
|
||||
fn collection_page(collection: (&str, &str), encoding: &str, descendant: &str, to_unicode: Option<&[u8]>, codes: &str) -> String {
|
||||
use lopdf::{Document, Object, Stream, dictionary};
|
||||
let (registry, ordering) = collection;
|
||||
let mut doc = Document::with_version("1.7");
|
||||
let (key, program): (&str, &[u8]) = if descendant == "CIDFontType0" {
|
||||
("FontFile3", include_bytes!("../../../../vendor/hayro-interpret/assets/FoxitFixed.pfb"))
|
||||
} else {
|
||||
("FontFile2", include_bytes!("../../../../assets/fonts/Inter-Regular.ttf"))
|
||||
};
|
||||
let mut file = dictionary! {};
|
||||
if descendant == "CIDFontType0" {
|
||||
file.set("Subtype", "CIDFontType0C");
|
||||
}
|
||||
let program = doc.add_object(Stream::new(file, program.to_vec()));
|
||||
let descriptor = doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "TestJapan1", "Flags" => 4, key => program });
|
||||
let cid = doc.add_object(dictionary! {
|
||||
"Type" => "Font", "Subtype" => descendant, "BaseFont" => "TestJapan1",
|
||||
"CIDSystemInfo" => dictionary! { "Registry" => Object::string_literal(registry), "Ordering" => Object::string_literal(ordering), "Supplement" => 6 },
|
||||
"FontDescriptor" => descriptor, "DW" => 1000, "CIDToGIDMap" => "Identity"
|
||||
});
|
||||
let mut font = dictionary! { "Type" => "Font", "Subtype" => "Type0", "BaseFont" => "TestJapan1", "Encoding" => encoding, "DescendantFonts" => vec![Object::Reference(cid)] };
|
||||
if let Some(cmap) = to_unicode {
|
||||
font.set("ToUnicode", doc.add_object(Stream::new(dictionary! {}, cmap.to_vec())));
|
||||
}
|
||||
let font = doc.add_object(font);
|
||||
let content = doc.add_object(Stream::new(dictionary! {}, format!("BT /F1 20 Tf 1 0 0 1 20 250 Tm <{codes}> Tj ET").into_bytes()));
|
||||
let pages = doc.new_object_id();
|
||||
let page = doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages, "MediaBox" => vec![Object::Integer(0), Object::Integer(0), Object::Integer(300), Object::Integer(300)], "Resources" => dictionary! {"Font" => dictionary! {"F1" => font}}, "Contents" => content });
|
||||
doc.objects.insert(pages, dictionary! {"Type" => "Pages", "Kids" => vec![Object::Reference(page)], "Count" => 1}.into());
|
||||
let catalog = doc.add_object(dictionary! {"Type" => "Catalog", "Pages" => pages});
|
||||
doc.trailer.set("Root", catalog);
|
||||
let mut bytes = Vec::new();
|
||||
doc.save_to(&mut bytes).unwrap();
|
||||
let pdf = Pdf::new(std::sync::Arc::new(bytes)).unwrap();
|
||||
extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap().plain_text()
|
||||
}
|
||||
|
||||
/// An embedded font of an Adobe character collection reads as text through the collection's
|
||||
/// UCS2 CMap, code to CID to Unicode (ISO 32000-2 §9.10.2), where its `/ToUnicode` is missing or
|
||||
/// has nothing for a code: with Identity-H or a predefined CMap such as `/H`, CFF or TrueType.
|
||||
/// Only fonts that are not embedded took that route, so Japanese and Chinese pages whose fonts
|
||||
/// are embedded without a `/ToUnicode` had no text to select, find or copy. `/ToUnicode` still
|
||||
/// comes first where it maps a code.
|
||||
#[test]
|
||||
fn an_embedded_adobe_collection_font_reads_through_its_ucs2_cmap() {
|
||||
// Adobe-Japan1 CIDs 34, 1125 and 843: A, 亜 and あ.
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType0", None, "00220465034B"), "A亜あ", "Identity-H, CFF, no /ToUnicode");
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType2", None, "00220465034B"), "A亜あ", "Identity-H, TrueType, no /ToUnicode");
|
||||
// `/H` maps JIS X 0208 codes to CIDs: 3021 亜, 2422 あ, 2341 A.
|
||||
assert_eq!(japan1_page("H", "CIDFontType2", None, "302124222341"), "亜あA", "predefined /H");
|
||||
let empty = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Empty def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType2", Some(empty), "00220465034B"), "A亜あ", "a /ToUnicode with no mappings");
|
||||
let one = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /One def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange 1 beginbfchar <0022> <005A> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType0", Some(one), "00220465034B"), "Z亜あ", "/ToUnicode first, the UCS2 CMap for the rest");
|
||||
}
|
||||
|
||||
/// The same route for the other Adobe collections, each through its own UCS2 CMap
|
||||
/// (Adobe-GB1-UCS2, Adobe-CNS1-UCS2, Adobe-Korea1-UCS2), and for vertical writing (Identity-V)
|
||||
/// and a predefined CMap over a CFF program. The pairs are read from the bundled CMaps.
|
||||
#[test]
|
||||
fn every_adobe_collection_reads_through_its_own_ucs2_cmap() {
|
||||
for descendant in ["CIDFontType0", "CIDFontType2"] {
|
||||
// Adobe-GB1 CID 4000 is 姓, Adobe-CNS1 CID 4000 is 團, Adobe-Korea1 CID 2000 is 밧; CID 34
|
||||
// is A in each.
|
||||
for (ordering, codes, text) in [("GB1", "00220FA0", "A姓"), ("CNS1", "00220FA0", "A團"), ("Korea1", "002207D0", "A밧")] {
|
||||
assert_eq!(collection_page(("Adobe", ordering), "Identity-H", descendant, None, codes), text, "Adobe-{ordering}, {descendant}");
|
||||
}
|
||||
assert_eq!(japan1_page("Identity-V", descendant, None, "00220465034B"), "A亜あ", "Identity-V, {descendant}");
|
||||
}
|
||||
assert_eq!(japan1_page("H", "CIDFontType0", None, "302124222341"), "亜あA", "predefined /H, CFF");
|
||||
}
|
||||
|
||||
/// A Type 0 font outside the Adobe collections has no UCS2 CMap: without a `/ToUnicode` its codes
|
||||
/// give no text through this route (the CIDs mean nothing outside their collection).
|
||||
#[test]
|
||||
fn a_font_outside_the_adobe_collections_gets_no_text_from_a_ucs2_cmap() {
|
||||
for descendant in ["CIDFontType0", "CIDFontType2"] {
|
||||
assert_eq!(collection_page(("Example", "Custom"), "Identity-H", descendant, None, "00220465034B"), "", "{descendant}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A Type 0 font whose `/ToUnicode` is written as a CID CMap (`begincidchar`, destinations as
|
||||
/// numbers, as Word writes it) reads each number as the code's Unicode value (vendored
|
||||
/// hayro-interpret patch 85), as glyph lookup already did and as pdftotext and PDFium extract it.
|
||||
/// Read as a bfchar map only, such text extracted nothing (pdf.js `issue9367`).
|
||||
#[test]
|
||||
fn a_tounicode_written_as_cid_ranges_gives_text() {
|
||||
let two = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Words def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange 3 begincidchar <0001> 84 <0002> 101 <0004> 116 endcidchar 1 begincidrange <0003> <0003> 120 endcidrange endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(cid_range_to_unicode_text(two), "Text", "two-byte entries");
|
||||
// Entries written with four-byte codes are found too (review on #6359).
|
||||
let four = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Words def /CMapType 2 def 1 begincodespacerange <00000000> <FFFFFFFF> endcodespacerange 2 begincidchar <00000001> 84 <00000002> 101 endcidchar 1 begincidrange <00000003> <00000004> 120 endcidrange endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(cid_range_to_unicode_text(four), "Texy", "four-byte entries");
|
||||
}
|
||||
|
||||
/// When a `/ToUnicode` gives one code both a bf entry and a CID entry, the bf entry is the
|
||||
/// code's text (ISO 32000-2 §9.10.3 defines `/ToUnicode` by `beginbfchar` / `beginbfrange`); the
|
||||
/// CID number is read only for codes no bf entry covers (vendored hayro-interpret patch 85).
|
||||
#[test]
|
||||
fn a_tounicode_bf_entry_wins_over_a_cid_entry_for_the_same_code() {
|
||||
// Codes 1 and 3 have both kinds of entry: bf says `T` / `x`, CID says `X` / `Y`. Codes 2 and 4
|
||||
// have CID entries only.
|
||||
let two = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Words def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange 3 begincidchar <0001> 88 <0002> 101 <0004> 116 endcidchar 1 begincidrange <0003> <0003> 89 endcidrange 1 beginbfchar <0001> <0054> endbfchar 1 beginbfrange <0003> <0003> <0078> endbfrange endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(cid_range_to_unicode_text(two), "Text", "two-byte entries");
|
||||
// The same with one-byte entries. For two-byte entries the CID lookup, tried at one byte
|
||||
// first, already falls back to the bf entries at that length; one-byte entries are where
|
||||
// reading CID entries before bf entries would show, as `XeYt`.
|
||||
let one = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Words def /CMapType 2 def 1 begincodespacerange <00> <FF> endcodespacerange 3 begincidchar <01> 88 <02> 101 <04> 116 endcidchar 1 begincidrange <03> <03> 89 endcidrange 1 beginbfchar <01> <0054> endbfchar 1 beginbfrange <03> <03> <0078> endbfrange endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(cid_range_to_unicode_text(one), "Text", "one-byte entries");
|
||||
}
|
||||
|
||||
/// The page [`a_tounicode_written_as_cid_ranges_gives_text`] and
|
||||
/// [`a_tounicode_bf_entry_wins_over_a_cid_entry_for_the_same_code`] read: four codes in a Type 0 font
|
||||
/// (Inter, Identity-H) whose `/ToUnicode` is `cmap`.
|
||||
fn cid_range_to_unicode_text(cmap: &[u8]) -> String {
|
||||
use lopdf::{Document, Object, Stream, dictionary};
|
||||
let mut doc = Document::with_version("1.7");
|
||||
let program = doc.add_object(Stream::new(dictionary! {}, include_bytes!("../../../../assets/fonts/Inter-Regular.ttf").to_vec()));
|
||||
let descriptor = doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "Inter-Regular", "Flags" => 4, "FontFile2" => program });
|
||||
let cid_font = doc.add_object(dictionary! {
|
||||
"Type" => "Font",
|
||||
"Subtype" => "CIDFontType2",
|
||||
"BaseFont" => "Inter-Regular",
|
||||
"CIDSystemInfo" => dictionary! { "Registry" => Object::string_literal("Adobe"), "Ordering" => Object::string_literal("Identity"), "Supplement" => 0 },
|
||||
"FontDescriptor" => descriptor,
|
||||
"CIDToGIDMap" => "Identity",
|
||||
});
|
||||
let to_unicode = doc.add_object(Stream::new(dictionary! {}, cmap.to_vec()));
|
||||
let font = doc.add_object(dictionary! {
|
||||
"Type" => "Font",
|
||||
"Subtype" => "Type0",
|
||||
"BaseFont" => "Inter-Regular",
|
||||
"Encoding" => "Identity-H",
|
||||
"DescendantFonts" => vec![Object::Reference(cid_font)],
|
||||
"ToUnicode" => to_unicode,
|
||||
});
|
||||
let content = doc.add_object(Stream::new(dictionary! {}, b"BT /F1 20 Tf 20 200 Td <0001000200030004> Tj ET".to_vec()));
|
||||
let pages = doc.new_object_id();
|
||||
let page = doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages, "MediaBox" => vec![Object::Integer(0), Object::Integer(0), Object::Integer(400), Object::Integer(400)], "Resources" => dictionary! {"Font" => dictionary! {"F1" => font}}, "Contents" => content });
|
||||
doc.objects.insert(pages, dictionary! {"Type" => "Pages", "Kids" => vec![Object::Reference(page)], "Count" => 1}.into());
|
||||
let catalog = doc.add_object(dictionary! {"Type" => "Catalog", "Pages" => pages});
|
||||
doc.trailer.set("Root", catalog);
|
||||
let mut bytes = Vec::new();
|
||||
doc.save_to(&mut bytes).unwrap();
|
||||
let pdf = Pdf::new(std::sync::Arc::new(bytes)).unwrap();
|
||||
extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap().plain_text().trim().to_string()
|
||||
}
|
||||
|
||||
/// A page box that encloses no area is read as a missing one (vendored hayro-syntax patch 38,
|
||||
/// ISO 32000-2 §14.11.2): a `/MediaBox [0 0 0 0]` as the default page, a `/CropBox [0 0 0 0]`,
|
||||
/// or one with nothing in common with the media box, as the media box. The page's visible region
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
//! A CID-keyed CFF font (`FontFile3` of subtype `CIDFontType0C`, ISO 32000-2 §9.9 Table 124)
|
||||
//! selects a font dict for each glyph through its FDSelect, and a glyph's charstring needs the
|
||||
//! Private DICT of that font dict (Adobe TN #5176). A subsetter can empty the font dicts no kept
|
||||
//! glyph uses, leaving them without a Private DICT. Those font dicts make only their own glyphs
|
||||
//! missing; the glyphs of the other font dicts still draw (hayro-interpret patch (93)).
|
||||
//!
|
||||
//! The font is built here: `.notdef` and two 500-unit squares, CID 1 in the first font dict and
|
||||
//! CID 2 in the second, which is empty.
|
||||
|
||||
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A CFF INDEX with one-byte offsets.
|
||||
fn index(items: &[&[u8]]) -> Vec<u8> {
|
||||
let mut out = u16::try_from(items.len()).unwrap().to_be_bytes().to_vec();
|
||||
if items.is_empty() {
|
||||
return out;
|
||||
}
|
||||
out.push(1);
|
||||
let mut offset = 1u8;
|
||||
out.push(offset);
|
||||
for item in items {
|
||||
offset += u8::try_from(item.len()).unwrap();
|
||||
out.push(offset);
|
||||
}
|
||||
for item in items {
|
||||
out.extend_from_slice(item);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A DICT integer operand in the 5-byte form, so every DICT has a fixed size.
|
||||
fn int(value: usize) -> Vec<u8> {
|
||||
let mut out = vec![29];
|
||||
out.extend_from_slice(&u32::try_from(value).unwrap().to_be_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn top_dict(charset: usize, fd_select: usize, char_strings: usize, fd_array: usize) -> Vec<u8> {
|
||||
let mut dict = Vec::new();
|
||||
// ROS comes first in a CID font's Top DICT: (Adobe) (Identity) 0.
|
||||
for operand in [391, 392, 0] {
|
||||
dict.extend(int(operand));
|
||||
}
|
||||
dict.extend([12, 30]);
|
||||
dict.extend(int(charset));
|
||||
dict.push(15);
|
||||
dict.extend(int(char_strings));
|
||||
dict.push(17);
|
||||
dict.extend(int(fd_array));
|
||||
dict.extend([12, 36]);
|
||||
dict.extend(int(fd_select));
|
||||
dict.extend([12, 37]);
|
||||
dict
|
||||
}
|
||||
|
||||
fn font_dict(private: Option<(usize, usize)>) -> Vec<u8> {
|
||||
let mut dict = Vec::new();
|
||||
if let Some((size, offset)) = private {
|
||||
dict.extend(int(size));
|
||||
dict.extend(int(offset));
|
||||
dict.push(18);
|
||||
}
|
||||
dict
|
||||
}
|
||||
|
||||
/// The CID-keyed CFF program. The second font dict is always empty; the first has a Private
|
||||
/// DICT only when `first_has_private`.
|
||||
fn cid_cff(first_has_private: bool) -> Vec<u8> {
|
||||
let header = vec![1, 0, 4, 1];
|
||||
let names = index(&[b"T"]);
|
||||
let strings = index(&[b"Adobe", b"Identity"]);
|
||||
let global_subrs = index(&[]);
|
||||
// Format 0: the CIDs of glyphs 1 and 2.
|
||||
let charset = vec![0, 0, 1, 0, 2];
|
||||
// Format 0: the font dict of each glyph.
|
||||
let fd_select = vec![0, 0, 0, 1];
|
||||
// 0 0 rmoveto 500 0 rlineto 0 500 rlineto -500 0 rlineto endchar.
|
||||
let square: &[u8] = &[139, 139, 21, 28, 1, 244, 139, 5, 139, 28, 1, 244, 5, 28, 254, 12, 139, 5, 14];
|
||||
let char_strings = index(&[&[14], square, square]);
|
||||
// defaultWidthX 0.
|
||||
let mut private = int(0);
|
||||
private.push(20);
|
||||
|
||||
let top_len = index(&[&top_dict(0, 0, 0, 0)]).len();
|
||||
let charset_at = header.len() + names.len() + top_len + strings.len() + global_subrs.len();
|
||||
let fd_select_at = charset_at + charset.len();
|
||||
let char_strings_at = fd_select_at + fd_select.len();
|
||||
let fd_array_at = char_strings_at + char_strings.len();
|
||||
let first_len = font_dict(first_has_private.then_some((0, 0))).len();
|
||||
let private_at = fd_array_at + index(&[&vec![0; first_len], &[]]).len();
|
||||
let first = font_dict(first_has_private.then_some((private.len(), private_at)));
|
||||
let fd_array = index(&[&first, &[]]);
|
||||
let top = index(&[&top_dict(charset_at, fd_select_at, char_strings_at, fd_array_at)]);
|
||||
|
||||
let mut out = header;
|
||||
for part in [names, top, strings, global_subrs, charset, fd_select, char_strings, fd_array, private] {
|
||||
out.extend(part);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A page showing the hex string `codes` at 60 pt in an Identity-H Type0 font over `program`
|
||||
/// (no `/FontFile3` when `program` is empty).
|
||||
fn page(program: &[u8], codes: &str) -> Vec<u8> {
|
||||
let font_file = if program.is_empty() { "" } else { " /FontFile3 8 0 R" };
|
||||
let body = format!("BT /F1 60 Tf 20 30 Td <{codes}> Tj ET");
|
||||
let mut stream = format!("<< /Subtype /CIDFontType0C /Length {} >>\nstream\n", program.len()).into_bytes();
|
||||
stream.extend_from_slice(program);
|
||||
stream.extend_from_slice(b"\nendstream");
|
||||
let objects: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 120] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(),
|
||||
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(),
|
||||
b"<< /Type /Font /Subtype /Type0 /BaseFont /ABCDEF+Synthetic /Encoding /Identity-H /DescendantFonts [6 0 R] >>".to_vec(),
|
||||
b"<< /Type /Font /Subtype /CIDFontType0 /BaseFont /ABCDEF+Synthetic /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor 7 0 R /DW 1000 >>".to_vec(),
|
||||
format!("<< /Type /FontDescriptor /FontName /ABCDEF+Synthetic /Flags 4 /FontBBox [0 0 1000 1000] /ItalicAngle 0 /Ascent 1000 /Descent 0 /CapHeight 1000 /StemV 80{font_file} >>").into_bytes(),
|
||||
stream,
|
||||
];
|
||||
|
||||
let mut bytes = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(bytes.len());
|
||||
bytes.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
bytes.extend_from_slice(object);
|
||||
bytes.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = bytes.len();
|
||||
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
|
||||
for offset in offsets {
|
||||
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn pixels(bytes: Vec<u8>) -> Vec<u8> {
|
||||
let mut renderer = PageRenderer::new(Arc::new(bytes), RenderConfig::default());
|
||||
let image = renderer.render(RenderRequest { page: 0, scale: 1.0, ..Default::default() });
|
||||
assert!(image.error.is_none(), "{:?}", image.error);
|
||||
image.rgba.to_vec()
|
||||
}
|
||||
|
||||
fn inked(rgba: &[u8]) -> usize {
|
||||
rgba.chunks(4).filter(|p| p.first().is_some_and(|r| *r < 128)).count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyphs_of_a_usable_font_dict_draw_beside_an_unusable_one() {
|
||||
let first = pixels(page(&cid_cff(true), "0001"));
|
||||
// The square is 30 pt on a side at 60 pt: 900 pixels at scale 1.
|
||||
assert!(inked(&first) >= 800, "the glyph of the usable font dict drew {} pixels", inked(&first));
|
||||
let both = pixels(page(&cid_cff(true), "00010002"));
|
||||
assert!(both == first, "the glyph of the empty font dict drew something, or the first did not draw");
|
||||
}
|
||||
|
||||
/// With no usable font dict the program is not read, as before, and the page still renders.
|
||||
#[test]
|
||||
fn a_program_with_no_usable_font_dict_draws_no_square() {
|
||||
let rgba = pixels(page(&cid_cff(false), "00010002"));
|
||||
let square = pixels(page(&cid_cff(true), "0001"));
|
||||
assert!(rgba != square, "a font dict without a Private DICT drew its glyph");
|
||||
// The page renders exactly as it does with no embedded program at all.
|
||||
assert!(rgba == pixels(page(&[], "00010002")), "the program was kept although no font dict is usable");
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The crate's integration tests, linked into one test binary.
|
||||
|
||||
mod cid_cff_unusable_font_dict;
|
||||
mod cid_fallback_query;
|
||||
mod fill_stroke_knockout;
|
||||
mod inline_image_ei;
|
||||
@@ -16,5 +17,7 @@ mod stray_q_in_a_form;
|
||||
mod stroke_degenerate_cubic;
|
||||
mod stroke_zero_end_tangent;
|
||||
mod substitute_widths;
|
||||
mod symbol_mt_builtin_encoding;
|
||||
mod tiling_cell_placement;
|
||||
mod type3_text_clip;
|
||||
mod widgets_above_annotations;
|
||||
@@ -0,0 +1,113 @@
|
||||
//! A non-embedded font named `SymbolMT` (the name Symbol has as a TrueType font) or a style
|
||||
//! variant such as `Symbol,Bold` is the standard Symbol font, and a non-embedded Symbol draws
|
||||
//! through its built-in encoding (ISO 32000-2 §9.6.2.2), even when the font dictionary
|
||||
//! declares `/WinAnsiEncoding` and the nonsymbolic flag: code 0x6D (`m`) is `mu`, as for
|
||||
//! `/Symbol` itself (hayro-interpret patch (98)).
|
||||
|
||||
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A page showing "abmp" at 40 pt in a non-embedded font `font` (the inside of its dictionary).
|
||||
fn page(font: &str) -> Vec<u8> {
|
||||
page_with(font, None)
|
||||
}
|
||||
|
||||
/// [`page`] with `program` as object 6, a `/FontFile2` stream the font descriptor can point at.
|
||||
fn page_with(font: &str, program: Option<&[u8]>) -> Vec<u8> {
|
||||
let body = "BT /F1 40 Tf 10 30 Td (abmp) Tj ET";
|
||||
let mut objects: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(),
|
||||
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(),
|
||||
format!("<< /Type /Font {font} >>").into_bytes(),
|
||||
];
|
||||
if let Some(program) = program {
|
||||
objects.push([format!("<< /Length {} >>\nstream\n", program.len()).into_bytes(), program.to_vec(), b"\nendstream".to_vec()].concat());
|
||||
}
|
||||
|
||||
let mut bytes = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(bytes.len());
|
||||
bytes.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
bytes.extend_from_slice(object);
|
||||
bytes.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = bytes.len();
|
||||
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
|
||||
for offset in offsets {
|
||||
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn pixels(bytes: Vec<u8>) -> Vec<u8> {
|
||||
let mut renderer = PageRenderer::new(Arc::new(bytes), RenderConfig::default());
|
||||
let image = renderer.render(RenderRequest { page: 0, scale: 1.0, ..Default::default() });
|
||||
assert!(image.error.is_none(), "{:?}", image.error);
|
||||
image.rgba.to_vec()
|
||||
}
|
||||
|
||||
/// A TrueType `name` with WinAnsiEncoding, nonsymbolic flags and no font program, as written
|
||||
/// by spreadsheet exports.
|
||||
fn win_ansi_true_type(name: &str) -> String {
|
||||
format!(
|
||||
"/Subtype /TrueType /BaseFont /{name} /Encoding /WinAnsiEncoding /FontDescriptor << /Type /FontDescriptor /FontName /{name} /Flags 32 /FontBBox [0 -216 1113 693] /ItalicAngle 0 /Ascent 1005 /Descent -216 /CapHeight 693 /StemV 60 >>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_mt_draws_symbols_through_the_built_in_encoding() {
|
||||
let symbol = pixels(page("/Subtype /Type1 /BaseFont /Symbol"));
|
||||
let latin = pixels(page("/Subtype /Type1 /BaseFont /Helvetica"));
|
||||
assert!(symbol != latin, "the Symbol stand-in drew the Latin letters");
|
||||
for name in ["SymbolMT", "ABCDEF+SymbolMT", "SymbolMT,Bold", "Symbol,Italic", "Symbol-Bold"] {
|
||||
assert!(pixels(page(&win_ansi_true_type(name))) == symbol, "{name} did not draw as Symbol");
|
||||
}
|
||||
let type1 = "/Subtype /Type1 /BaseFont /SymbolMT /Encoding /WinAnsiEncoding";
|
||||
assert!(pixels(page(type1)) == symbol, "a Type1 SymbolMT did not draw as Symbol");
|
||||
}
|
||||
|
||||
/// The number of dark pixels on a page.
|
||||
fn inked(rgba: &[u8]) -> usize {
|
||||
rgba.chunks(4).filter(|p| p.first().is_some_and(|r| *r < 128)).count()
|
||||
}
|
||||
|
||||
/// Only the Symbol name itself, alone or with a style, counts: a font whose name merely starts
|
||||
/// with it, or names another font after the separator, keeps its declared encoding and its
|
||||
/// Latin stand-in, drawing the same letters as Helvetica.
|
||||
#[test]
|
||||
fn a_name_that_only_starts_with_symbol_is_not_symbol() {
|
||||
let latin = pixels(page("/Subtype /Type1 /BaseFont /Helvetica"));
|
||||
assert!(inked(&latin) > 100, "the Latin reference drew no text");
|
||||
for name in ["Symbolic", "SymbolPS", "Symbol-Helvetica", "SymbolMT,Helvetica"] {
|
||||
assert!(pixels(page(&win_ansi_true_type(name))) == latin, "{name} did not draw the Latin letters");
|
||||
}
|
||||
}
|
||||
|
||||
/// A `/Differences` array still applies to a SymbolMT: code 0x6D named `alpha` draws alpha, as
|
||||
/// for `/Symbol` with the same differences, not the built-in encoding's `mu`.
|
||||
#[test]
|
||||
fn symbol_mt_applies_its_differences() {
|
||||
let symbol = pixels(page("/Subtype /Type1 /BaseFont /Symbol"));
|
||||
let alpha = pixels(page("/Subtype /Type1 /BaseFont /Symbol /Encoding << /Differences [109 /alpha] >>"));
|
||||
assert!(alpha != symbol, "the differences changed nothing");
|
||||
let font = win_ansi_true_type("SymbolMT")
|
||||
.replace("/Encoding /WinAnsiEncoding", "/Encoding << /BaseEncoding /WinAnsiEncoding /Differences [109 /alpha] >>");
|
||||
assert!(pixels(page(&font)) == alpha, "SymbolMT did not draw the alpha its differences name");
|
||||
}
|
||||
|
||||
/// An embedded font named SymbolMT draws its own program, not the Symbol stand-in.
|
||||
#[test]
|
||||
fn an_embedded_symbol_mt_draws_its_own_program() {
|
||||
let program = include_bytes!("../../../../assets/fonts/JetBrainsMono-Regular.ttf");
|
||||
let embedded = |name: &str| {
|
||||
let font = win_ansi_true_type(name).replace(" >>", " /FontFile2 6 0 R >>");
|
||||
pixels(page_with(&font, Some(program)))
|
||||
};
|
||||
let own = embedded("ABCDEF+JetBrainsMono");
|
||||
assert!(inked(&own) > 100, "the embedded program drew no text");
|
||||
assert!(embedded("ABCDEF+SymbolMT") == own, "an embedded SymbolMT did not draw its own glyphs");
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! A contributor-original synthetic page: a check box widget listed before a filled square
|
||||
//! comment that covers it. Form fields' widgets are drawn after the page's other annotations
|
||||
//! (hayro-interpret patch (86)), so the check box shows; the comment covered it before.
|
||||
|
||||
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A 200 × 200 pt page whose `/Annots` lists `order` of: `W`, a check box drawing a green box,
|
||||
/// `S`, a square comment drawing a black one over it, and `R`, a larger red square comment.
|
||||
fn pdf(order: &str) -> Vec<u8> {
|
||||
let annot = |c: char| match c {
|
||||
'W' => "6 0 R",
|
||||
'S' => "7 0 R",
|
||||
_ => "8 0 R",
|
||||
};
|
||||
let annots: Vec<&str> = order.chars().map(annot).collect();
|
||||
let form = |rgb: &str| {
|
||||
let content = format!("{rgb} rg 0 0 100 100 re f");
|
||||
format!("<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] /Length {} >>\nstream\n{content}\nendstream", content.len())
|
||||
};
|
||||
let objects = [
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R] >> >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [{}] >>", annots.join(" ")),
|
||||
form("0 1 0"),
|
||||
form("0 0 0"),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (box) /F 4 /Rect [50 50 150 150] /AS /Off /AP << /N << /Off 4 0 R /Yes 4 0 R >> >> >>"
|
||||
.to_string(),
|
||||
"<< /Type /Annot /Subtype /Square /F 4 /C [0 0 0] /Rect [40 40 160 160] /AP << /N 5 0 R >> >>".to_string(),
|
||||
"<< /Type /Annot /Subtype /Square /F 4 /C [1 0 0] /Rect [30 30 170 170] /AP << /N 9 0 R >> >>".to_string(),
|
||||
form("1 0 0"),
|
||||
];
|
||||
let mut bytes = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(bytes.len());
|
||||
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = bytes.len();
|
||||
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
|
||||
for offset in offsets {
|
||||
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
/// The colour at the page's centre.
|
||||
fn centre(order: &str) -> [u8; 3] {
|
||||
at(order, 100, 100)
|
||||
}
|
||||
|
||||
/// The colour at (`x`, `y`) px from the top left.
|
||||
fn at(order: &str, x: usize, y: usize) -> [u8; 3] {
|
||||
let mut renderer = PageRenderer::new(Arc::new(pdf(order)), RenderConfig::default());
|
||||
let out = renderer.render(RenderRequest { page: 0, scale: 1.0, ..Default::default() });
|
||||
assert!(out.error.is_none(), "{:?}", out.error);
|
||||
let at = (y * out.width as usize + x) * 4;
|
||||
let px = out.rgba.get(at..at + 3).expect("the centre lies on the page");
|
||||
[px[0], px[1], px[2]]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_widget_is_drawn_above_the_comments_on_its_page() {
|
||||
assert_eq!(centre("WS"), [0, 255, 0], "a check box listed before a comment over it");
|
||||
assert_eq!(centre("SW"), [0, 255, 0], "a check box listed after it");
|
||||
// Comments keep their `/Annots` order among themselves.
|
||||
assert_eq!(centre("RS"), [0, 0, 0], "the black square listed last is on top");
|
||||
assert_eq!(centre("SR"), [255, 0, 0], "the red square listed last is on top");
|
||||
// And with a widget among them: (45, 45) is inside both squares, outside the check box.
|
||||
assert_eq!(at("WRS", 45, 45), [0, 0, 0], "a widget first: the black square listed last is on top");
|
||||
assert_eq!(at("WSR", 45, 45), [255, 0, 0], "a widget first: the red square listed last is on top");
|
||||
assert_eq!(centre("WSR"), [0, 255, 0], "and the check box above both");
|
||||
}
|
||||
@@ -734,8 +734,8 @@ milestone = "M2"
|
||||
status = "partial"
|
||||
commands = []
|
||||
tools = ["page_render"]
|
||||
evidence = ["crates/render/src/raster/tests.rs::renders_a_blue_rectangle", "crates/automation/tests/it/automation.rs::open_inspect_render_and_find", "crates/render/src/raster/tests.rs::user_unit_scales_the_page_and_everything_on_it", "crates/render/src/raster/tests.rs::non_interpolated_minification_keeps_thin_strokes", "crates/render/src/raster/tests.rs::a_shrunk_stencil_mask_keeps_thin_strokes", "crates/render/src/raster/tests.rs::minified_image_edges_snap_to_whole_pixels", "crates/render/src/raster/tests.rs::large_non_interpolated_images_are_smoothed_when_enlarged", "crates/render/src/raster/tests.rs::small_non_interpolated_images_keep_hard_pixels_when_enlarged", "crates/render/src/raster/tests.rs::a_page_without_a_media_box_is_letter"]
|
||||
notes = "hayro bootstrap renderer; own DisplayList devices in M2.6. Pages with /UserUnit are shown at their size in points, as Acrobat reads the value. An image drawn below its resolution is shrunk once to whole device pixels: by area averaging unless it asks for /Interpolate (then Catmull-Rom), so the text of a 300 dpi scan at 100% keeps its strokes, a 1-bit /ImageMask text layer included (it was nearest-neighbour, which dropped them). Enlarged by more than 1%, an image of page-scan size (512 samples or more on its shorter side) is smoothed bilinearly whatever its /Interpolate, so a zoomed scan has no staircases; smaller images keep their hard pixels. A page with no `/MediaBox`, or one without area, is US Letter in the renderer, the page model and the inspector alike. Not measured against Acrobat"
|
||||
evidence = ["crates/render/src/raster/tests.rs::renders_a_blue_rectangle", "crates/automation/tests/it/automation.rs::open_inspect_render_and_find", "crates/render/src/raster/tests.rs::user_unit_scales_the_page_and_everything_on_it", "crates/render/src/raster/tests.rs::non_interpolated_minification_keeps_thin_strokes", "crates/render/src/raster/tests.rs::a_shrunk_stencil_mask_keeps_thin_strokes", "crates/render/src/raster/tests.rs::minified_image_edges_snap_to_whole_pixels", "crates/render/src/raster/tests.rs::large_non_interpolated_images_are_smoothed_when_enlarged", "crates/render/src/raster/tests.rs::small_non_interpolated_images_keep_hard_pixels_when_enlarged", "crates/render/src/raster/tests.rs::a_font_without_a_subtype_is_drawn_by_its_base_font", "crates/render/src/raster/tests.rs::a_page_without_a_media_box_is_letter"]
|
||||
notes = "hayro bootstrap renderer; own DisplayList devices in M2.6. Pages with /UserUnit are shown at their size in points, as Acrobat reads the value. An image drawn below its resolution is shrunk once to whole device pixels: by area averaging unless it asks for /Interpolate (then Catmull-Rom), so the text of a 300 dpi scan at 100% keeps its strokes, a 1-bit /ImageMask text layer included (it was nearest-neighbour, which dropped them). Enlarged by more than 1%, an image of page-scan size (512 samples or more on its shorter side) is smoothed bilinearly whatever its /Interpolate, so a zoomed scan has no staircases; smaller images keep their hard pixels. A font dictionary with a missing or empty `/Subtype` is drawn as a Type 1 font by its `/BaseFont`. A page with no `/MediaBox`, or one without area, is US Letter in the renderer, the page model and the inspector alike. Not measured against Acrobat"
|
||||
|
||||
[[feature]]
|
||||
id = "view.render-shadings-patterns"
|
||||
@@ -782,8 +782,8 @@ milestone = "M2"
|
||||
status = "partial"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = "hayro bootstrap with vendored patches. Text rendering modes follow ISO 32000-2 §9.3.6 for Type 3 fonts: 3 and 7 render nothing, the others render the glyph descriptions filled (never stroked), and 4 to 7 add nothing to the clipping path (hayro-interpret patches 42 and 64). Coverage is synthetic fixtures; no fidelity comparison across a Type 3 corpus yet"
|
||||
evidence = ["crates/render/src/raster/tests/type3_proc_widths.rs::a_type3_font_without_widths_advances_by_its_glyph_procedures", "crates/render/src/raster/tests/type3_proc_widths.rs::widths_the_font_gives_are_kept", "crates/render/src/raster/tests/type3_proc_widths.rs::a_type3_font_without_widths_is_reported"]
|
||||
notes = "hayro bootstrap with vendored patches. Text rendering modes follow ISO 32000-2 §9.3.6 for Type 3 fonts: 3 and 7 render nothing, the others render the glyph descriptions filled (never stroked), and 4 to 7 add nothing to the clipping path (hayro-interpret patches 42 and 64). A font without /Widths advances each glyph by its glyph procedure's d0/d1 width (§9.6.4) in rendering and text alike, and the page reports a render warning that the font's /Widths is missing (patch 90). Coverage is synthetic fixtures; no fidelity comparison across a Type 3 corpus yet"
|
||||
|
||||
[[feature]]
|
||||
id = "view.render-cid-fonts"
|
||||
@@ -794,8 +794,8 @@ milestone = "M2"
|
||||
status = "partial"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/render/src/raster/tests.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster/tests.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face", "crates/render/src/raster/tests.rs::mincho_substitute_covers_half_width_katakana", "crates/render/src/raster/tests.rs::non_embedded_chinese_and_korean_cid_fonts_draw_with_an_installed_face", "crates/engine/src/tests.rs::installed_cjk_fonts_draw_in_the_viewer_but_not_in_an_export", "crates/render/src/text/tests.rs::identity_to_unicode_names_give_text", "crates/fonts/src/pdf/tests.rs::identity_to_unicode_names_read_codes_as_unicode", "crates/render/src/raster/tests.rs::a_soft_hyphen_a_substitute_lacks_draws_its_hyphen", "crates/render/src/raster/tests.rs::a_bfchar_cmap_reads_codes_of_its_codespace_length"]
|
||||
notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name, else by the descendant font descriptor's Serif flag; bold Gothic for a Bold name or a `/FontWeight` of 600 or more; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; the desktop viewer can fall back to an installed CJK face for GB1, CNS1, Korea1 and Japan1, including GBK-EUC-H TrueType CID fonts without ToUnicode. Windows preserves installed SimSun, SimHei, KaiTi and FangSong families, including subset-prefixed Founder aliases. Combine thumbnails and accessibility figure previews use the same on-screen fallback. System faces are display-only, disabled by PDFCRAFT_SYSTEM_FONTS=0, and not used by export, printing, OCR or automation; machines without a candidate and web builds remain unsupported. A `/ToUnicode` given as the name `/Identity-H` or `/Identity-V` (WSCAD) reads codes as Unicode for selection, search and editing, without control characters. A non-embedded Identity font whose `/ToUnicode` gives a soft hyphen (U+00AD, as Windows exporters write Arial's hyphen) draws the standard substitute's hyphen (#5888). Any other character the substitute lacks still falls back to the CID taken as one of the substitute's glyph indexes, an unrelated glyph (#3529, #5726). An embedded CMap that writes its CID mappings as `bfchar` reads each code at the length its codespace ranges give it (ISO 32000-2 §9.7.6.2), so a two-byte code is one glyph, not two (hayro-cmap patch 4)"
|
||||
evidence = ["crates/render/src/raster/tests.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster/tests.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face", "crates/render/src/raster/tests.rs::mincho_substitute_covers_half_width_katakana", "crates/render/src/raster/tests.rs::non_embedded_chinese_and_korean_cid_fonts_draw_with_an_installed_face", "crates/engine/src/tests.rs::installed_cjk_fonts_draw_in_the_viewer_but_not_in_an_export", "crates/render/src/text/tests.rs::identity_to_unicode_names_give_text", "crates/fonts/src/pdf/tests.rs::identity_to_unicode_names_read_codes_as_unicode", "crates/render/src/raster/tests.rs::a_soft_hyphen_a_substitute_lacks_draws_its_hyphen", "crates/render/src/raster/tests/non_embedded_arial.rs::an_arial_the_pdf_does_not_embed_draws_the_glyphs_its_codes_name", "crates/render/src/raster/tests/non_embedded_arial.rs::a_times_new_roman_the_pdf_does_not_embed_draws_the_glyphs_its_codes_name", "crates/render/src/raster/tests/non_embedded_arial.rs::codes_of_other_fonts_are_not_read_in_the_macintosh_order", "crates/render/src/raster/tests.rs::a_bfchar_cmap_reads_codes_of_its_codespace_length"]
|
||||
notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name, else by the descendant font descriptor's Serif flag; bold Gothic for a Bold name or a `/FontWeight` of 600 or more; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; the desktop viewer can fall back to an installed CJK face for GB1, CNS1, Korea1 and Japan1, including GBK-EUC-H TrueType CID fonts without ToUnicode. Windows preserves installed SimSun, SimHei, KaiTi and FangSong families, including subset-prefixed Founder aliases. Combine thumbnails and accessibility figure previews use the same on-screen fallback. System faces are display-only, disabled by PDFCRAFT_SYSTEM_FONTS=0, and not used by export, printing, OCR or automation; machines without a candidate and web builds remain unsupported. A `/ToUnicode` given as the name `/Identity-H` or `/Identity-V` (WSCAD) reads codes as Unicode for selection, search and editing, without control characters. A non-embedded Identity font whose `/ToUnicode` gives a soft hyphen (U+00AD, as Windows exporters write Arial's hyphen) draws the standard substitute's hyphen (#5888). A non-embedded Arial or Times New Roman (`CIDFontType2`; ArialMT, Arial-BoldMT, Arial,Bold, TimesNewRomanPSMT, TimesNewRoman,Bold and their other faces) whose codes no Unicode route draws is read as glyph indexes of the author's font, whose first 258 glyphs follow the standard Macintosh order: the substitute draws each glyph by that name, .notdef when it has none (#5889, #5726, #6449). Its text still can't be selected, searched or copied without a `/ToUnicode`, Courier New and other faces whose numbering isn't confirmed aren't read this way, and Arial and Times New Roman glyphs past the first 258 (Cyrillic, Greek) and every other font's characters the substitute lacks still fall back to the CID taken as one of the substitute's glyph indexes, an unrelated glyph (#3529, #5726). An embedded CMap that writes its CID mappings as `bfchar` reads each code at the length its codespace ranges give it (ISO 32000-2 §9.7.6.2), so a two-byte code is one glyph, not two (hayro-cmap patch 4)"
|
||||
|
||||
[[feature]]
|
||||
id = "view.render-image-codecs"
|
||||
|
||||
Vendored
+6
-4
@@ -39,8 +39,8 @@ Other resource categories retain their existing parent-chain lookup.
|
||||
|
||||
| Crate | Version | Licence | Patches | Test |
|
||||
|---|---|---|---|---|
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.nLine truncated
|
||||
| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) `/Interpolate` governs the pre-resize too (#624): a minified image that does not request interpolation is pre-resized with nearest-neighbour (since (21), only inside Type 3 glyphs), planned without the Catmull-Rom filter limits (`image_resampling_size` takes the flag); a Type 3 glyph image is sampled bicubically only when interpolated; a minified image with a matching soft mask is nearest-sampled straight into premultiplied RGBA; (11) `may_paint_rect`: an image whose stream-size box, mapped to the device, misses the canvas is skipped before decoding (decoded data can only shrink against that box), so a tile of an image-heavy page no longer decodes, resamples and rasterizes every image on it; (12) preview renders (`InterpreterSettings::preview`: backdrops, thumbnails, grid cells, signature-drag backgrounds) skip images under `PREVIEW_MIN_IMAGE_AREA` device pixels, while final renders always draw them; (13) reading colours (#897): `RenderSettings::color_replacement` takes a `ColorReplacement` (text colour, background, `only_black`, `line_art`) that remaps solid text and line-art paint while rendering, glyph outlines counting as text and stencil-mask ink as line art; images, image alpha masks, soft masks and pattern cells keep their colours; `render_pdf` gains a `color_replacement: Option<ColorReplacement>` argument (pass `None` for true colours); the upstream-only `[dev-dependencies.log]` entry is removed from `Cargo.toml` (log is a normal dependency since the logging patch), so `cargo test -p hayro` runs this crate's unit tests; (14) a shrunk image that is interpolated (or, since (21), area-averaged) and whose axes lie along the device's (upright, mirrored or a quarter turn, as on a page under `/Rotate`) is snapped to whole device pixels: shrunk once to its rounded device rectangle (halves round up, so a tile snaps an edge where the whole page does) and drawn 1:1 from the rounded corner, instead of a ceil-sized raster resampled again at a fractional scale and offset (`snap_minified_image`; scans looked soft next to Acrobat, and until #5530 a turned one still did, up to 200 grey levels off the same page drawn upright). An image counts as axis-aligned when the terms the snap drops move its far edges by at most a hundredth of a device pixel (`SNAP_SKEW`), not by a tolerance on the coefficients, which let a slight skew of a very wide image through; and the snapped 1:1 transform is used only when the shrink produced the snapped size, so a failed resize keeps the full transform instead of drawing the source raster at its source size; (15) a soft mask of another size than its image is filtered when shrunk, whatever its `/Interpolate` (#1649): such a mask carries the shape on its own (Word saves text as a tiny black image with a glyph-bitmap mask), and nearest-neighbour dropped every stroLine truncated
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.nLine truncated
|
||||
| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) `/Interpolate` governs the pre-resize too (#624): a minified image that does not request interpolation is pre-resized with nearest-neighbour (since (21), only inside Type 3 glyphs), planned without the Catmull-Rom filter limits (`image_resampling_size` takes the flag); a Type 3 glyph image is sampled bicubically only when interpolated; a minified image with a matching soft mask is nearest-sampled straight into premultiplied RGBA; (11) `may_paint_rect`: an image whose stream-size box, mapped to the device, misses the canvas is skipped before decoding (decoded data can only shrink against that box), so a tile of an image-heavy page no longer decodes, resamples and rasterizes every image on it; (12) preview renders (`InterpreterSettings::preview`: backdrops, thumbnails, grid cells, signature-drag backgrounds) skip images under `PREVIEW_MIN_IMAGE_AREA` device pixels, while final renders always draw them; (13) reading colours (#897): `RenderSettings::color_replacement` takes a `ColorReplacement` (text colour, background, `only_black`, `line_art`) that remaps solid text and line-art paint while rendering, glyph outlines counting as text and stencil-mask ink as line art; images, image alpha masks, soft masks and pattern cells keep their colours; `render_pdf` gains a `color_replacement: Option<ColorReplacement>` argument (pass `None` for true colours); the upstream-only `[dev-dependencies.log]` entry is removed from `Cargo.toml` (log is a normal dependency since the logging patch), so `cargo test -p hayro` runs this crate's unit tests; (14) a shrunk image that is interpolated (or, since (21), area-averaged) and whose axes lie along the device's (upright, mirrored or a quarter turn, as on a page under `/Rotate`) is snapped to whole device pixels: shrunk once to its rounded device rectangle (halves round up, so a tile snaps an edge where the whole page does) and drawn 1:1 from the rounded corner, instead of a ceil-sized raster resampled again at a fractional scale and offset (`snap_minified_image`; scans looked soft next to Acrobat, and until #5530 a turned one still did, up to 200 grey levels off the same page drawn upright). An image counts as axis-aligned when the terms the snap drops move its far edges by at most a hundredth of a device pixel (`SNAP_SKEW`), not by a tolerance on the coefficients, which let a slight skew of a very wide image through; and the snapped 1:1 transform is used only when the shrink produced the snapped size, so a failed resize keeps the full transform instead of drawing the source raster at its source size; (15) a soft mask of another size than its image is filtered when shrunk, whatever its `/Interpolate` (#1649): such a mask carries the shape on its own (Word saves text as a tiny black image with a glyph-bitmap mask), and nearest-neighbour dropped every stroLine truncated
|
||||
| hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap and page bitmap, and 2^25 cells per halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one, and a 4-byte value per cell); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer); (4) a segment header's long-form referred-to segment count is capped at `u16::MAX` and a larger count is refused before its retention or referred-to bytes are read: the 29-bit count went straight to `Vec::with_capacity`, a reservation of about 2 GiB whose failure aborts the process (from storytold/craft-libs#176); (5) what a page's segments hold for later ones (intermediate regions, pattern and symbol dictionaries with their headers and retained contexts, and Huffman tables) is at most 64 MiB, two square bitmaps of 16,384 a side (`MAX_RETAINED_WORDS`), and a dictionary's new and exported symbols are counted against it as they are made: each was bounded on its own, but a few dozen bytes an intermediate region held 33 MB each, and 8 MiB symbols exported again and again killed the process, so a few kilobytes could exhaust memory; (6) a segment may refer to at most 2^20 symbols, checked before any are collected: references may repeat, and 65,535 of them to one dictionary, in a 78 KB stream, asked for 17 GB; (7) a pattern dictionary keeps at most 16 MiB of shifted copies, which only speed up drawing, and none that wouldn't fit what the page has left (`MAX_SHIFTED_WORDS`); (8) a custom Huffman table has at most 2^16 lines, and a stream with its globals at most 2^20 segments and 2^22 references, checked as they are read: both grew before anything counted them, and PDFs of a kilobyte or so reached 5 GB and more behind nested Flate; (9) a page past the bounds of (1) is refused when the stream is opened: a caller sizes its output from the header before the page bitmap is made, and an Indexed image in a 743-byte PDF asked for 137 GB, which aborts; (10) a stream may ask for at most 2^30 units of decoding work, four of the largest pages, about 6 s to 17 s (`MAX_WORK`): decoding a bitmap costs a pixel each and 4 a row, copying one a word each and 1 a row, and each integer an item reads costs 38, what one takes at worst, so one with no pixels pays too, before it is decoded: each bitmap was bounded, but nothing bounded how many there were, and a 795-byte PDF took 265 s; (11) `Image::decode_within` spends from what a run of images may still do, starting with its page's pixels, and `charge_reading` charges reading a stream and its globals before anything is parsed, so an operation over a document can bound what `MAX_WORK` bounds for one image, and images that fail or decode nothing add up too: a file of thousands of images made to reach that limit took as many times as long; (12) adding to a location, a reference offset, a symbol's corner or a Huffman range line near the end of an `i32`, and negating a decoded integer of 2^31, overflowed: a panic wherever overflow is checked. The refinement context, a text region's corners, the integer decoder and the Huffman table wrap, as a release build did, and `Bitmap::combine` saturates, which comes to the same, as a location that overflows is past the end of any page. What is drawn doesn't change; (13) a bitmap filled with a default pixel of 1 (a page, or an intermediate text or halftone region) had 1s past its width in the last word of each row, and a refinement, which reads its reference a word at a time, read them as pixels where the standard has 0 (6.3.5.2): refining such a reference not a multiple of 32 wide decoded to other pixels than were coded, release builds too. `Bitmap::clear_padding` clears those bits when a bitmap is filled with 1s, as cutting symbols and patterns from a collective bitmap already did. Ghostscript reads the test's page and text streams as they decode now; (14) a region's location is unsigned (5.4.4), so one past `i32::MAX` is off the page, where the decoder took it for a signed one: a region there was drawn back onto the page from the left or the top (at 0xFFFFFFFC, four pixels left of it), and a refinement there, of the page or of a region at the same place (7.4.7.5), was refused as an overflow, and the page with it. `Bitmap::combine_region` keeps such a location past the page. A refinement of another region refines it where it is, as Table 35 has it (GRREFERENCEDX and GRREFERENCEDY 0), whatever the two locations say; a refinement of the page, see (16); (15) a refinement whose reference is offset right of it (GRREFERENCEDX above 0, which a refined text instance has from a wider bitmap, TaLine truncated
|
||||
| lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589); (4) the object lookup keeps its cycle history through compressed objects, requires an ordinary in-file container, and caps the chain of indirect lookups at 16 (a chain of new objects, each a `/Length` compressed in a container whose own `/Length` names the next, or a stream whose `/Length` names the next stream, overflowed the stack: a dev build's 2 MiB thread within 800 hops); (5) object-stream members are parsed within their own extent (up to the next distinct offset), an entry whose offset repeats an earlier entry's is skipped (so a repeated object number can resolve to its earlier entry), and offsets are processed in index order; (6) `find_latest_trailer`: a rebuild takes a `/Type /XRef` stream dictionary as the trailer when the file has no `trailer` keyword (the latest in the file wins, as a later `trailer` would), so a concatenated or truncated file keeps its /Info and /ID instead of failing to load; a candidate whose /Root is the catalog beats one whose /Root is another dictionary, and an in-file /Root must match its generation (#2365) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding`; (4) `inspect::tests::compressed_object_self_container_does_not_recurse`, `inspect::tests::compressed_object_mutual_containers_do_not_recurse`, `inspect::tests::object_stream_length_cycle_does_not_recurse`, `inspect::tests::compressed_object_container_chain_does_not_recurse`, `inspect::tests::compressed_length_chain_does_not_recurse`, `inspect::tests::plain_stream_length_chain_does_not_recurse`, `inspect::tests::compressed_objects_and_indirect_stream_length_still_load`, `inspect::tests::compressed_length_in_a_container_with_an_indirect_length_still_loads`; (5) `inspect::tests::object_stream_member_extents_bound_owned_strings`, `inspect::tests::object_stream_member_extents_preserve_index_order_and_whitespace`; (6) `inspect::tests::rebuilt_file_keeps_the_info_of_its_cross_reference_stream`, `inspect::tests::rebuilt_file_prefers_a_trailer_whose_root_is_the_catalog` |
|
||||
| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); an EI followed by a delimiter (EI%..., EI[...) ends the image like one followed by white space; only a regular character after EI skips the candidate; (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW, RunLength and ASCII85 never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking; (13) a page's `/UserUnit` scales `render_dimensions` and `initial_transform`, so the page and everything on it are sized in points, as Acrobat shows them; it is read as Acrobat reads it (the page's own entry, not inherited; a number from 1 up, at most 75,000; anything else counts as 1), and a page without it keeps exactly the transform it had; (14) revision 2–4 passwords are tried as given and then in PDFDocEncoding (`legacy_passwords` in `crypto/mod.rs`, which resolves the upstream `TODO: Convert to PDFDocEncoding`): passwords arrive as UTF-8 text, so a password with characters outside ASCII (`Çok güzel`, `ılık€`) written in PDFDocEncoding, as ISO 32000-2 §7.6.4.3.2 asks and qpdf and Acrobat write it, was always wrong; (15) page content is read a piece at a time (`page::ContentReader`, `Page::content_reader`): plain streams and Flate streams without a predictor are decoded as they are read, with a budget of 64 times their compressed size and never less than `MAX_DECODED_STREAM` (a stream that expands past it is cut and reported); other streams are decoded whole, as before, and share `MAX_DECODED_STREAM`. The content tokenizer reads a window at a time (`UntypedIter::new_window`, `TypedIter::needs_more`, `TypedIter::committed`): an instruction that runs past the window is not decided until more data follows, which `Reader` records by flagging reads and `at_end` past the end of the data, including through the clones the parser makes. The Flate decoder keeps its own 32 KiB history, so it pauses at any output size. `ContentReader::new` reuses raw/decrypted stream bytes for budget accounting and piece creation, and `decoded_raw_within` applies the existing Whole-stream filters and limits to those bytes without decrypting twice; (16) revision 5–6 (AES-256) passwords are tried in their SASLprep form (RFC 4013, then UTF-8, at most 127 bytes, as ISO 32000-2 §7.6.4.3.3 Algorithm 2.A asks and pdfcraft-crypt writes them) and then, if that differs, as given (`aes256_passwLine truncated
|
||||
@@ -97,8 +97,10 @@ Glyph names outside the Adobe Glyph List (#3169, #3170): hayro-interpret's `glyp
|
||||
maps a name by the glyph list specification's full mapping (drop from the first `.`, split on `_`,
|
||||
each part through the list, `uniXXXX…` or `uXXXX`; a `uni` part with any group that isn't a
|
||||
character maps to nothing), so `f_i`, `f_l` and `hyphen.case` read as text. A name that only
|
||||
numbers the glyph (`C39`, `c251`) still reads as nothing: the code's StandardEncoding character
|
||||
would be a guess, wrong wherever the font's /Encoding puts the glyph elsewhere (review on #3239). Covered by `text::tests::glyph_names_outside_the_glyph_list_still_read_as_text`.
|
||||
numbers the glyph (`c251` at 97, `C1` at 233) still reads as nothing: the code's StandardEncoding
|
||||
character would be a guess, wrong wherever the font's /Encoding puts the glyph elsewhere (review
|
||||
on #3239). The one exception is patch (100): a font whose numbered names all name their own codes.
|
||||
Covered by `text::tests::glyph_names_outside_the_glyph_list_still_read_as_text`.
|
||||
|
||||
Text extraction: hayro-interpret exposes `OutlineGlyph::is_vertical` from a Type0 font’s CMap writing mode. PdfCraft follows negative glyph-space y for WMode 1 so upright vertical columns can be searched (#126).
|
||||
|
||||
|
||||
+13
-6
@@ -146,9 +146,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "font-types"
|
||||
version = "0.11.3"
|
||||
version = "0.12.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7"
|
||||
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
@@ -297,6 +297,12 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
@@ -387,12 +393,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.39.2"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81"
|
||||
checksum = "d5a0cf4bb6cbce4c29756270d189a5e400ac05e91192161d83a0bcb6e68492e8"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"font-types",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -415,9 +422,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.42.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564"
|
||||
checksum = "526e654b303ff4ad73a5a40b25de2b3ce3d039c5e3b6da5f89ae6e57004cb40f"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts",
|
||||
|
||||
Vendored
+120
-27
@@ -1,10 +1,11 @@
|
||||
use crate::util::hash128;
|
||||
use crate::{InterpreterWarning, WarningSinkFn};
|
||||
use hayro_syntax::object::{Array, Dict, MaybeRef, Name, Null, ObjRef, Object, Stream};
|
||||
use kurbo::Affine;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
type CacheMap = HashMap<u128, Option<Box<dyn Any + Send + Sync>>>;
|
||||
@@ -69,6 +70,14 @@ impl Cache {
|
||||
/// them; when an insert would pass the budget the whole map is dropped (a fresh epoch), which
|
||||
/// only costs re-decoding, and an image larger than the whole budget is never kept. The budget
|
||||
/// caps memory per interpreter cache. A poisoned lock decodes without the cache.
|
||||
///
|
||||
/// PdfCraft patch (91): a decode that fails is kept too, as the warnings it reported, so an
|
||||
/// image drawn again and again that doesn't decode (damaged data, a JPX or JBIG2 stream its
|
||||
/// decoder refuses) isn't decoded again at every draw: a later draw reports the same warnings
|
||||
/// and gets nothing, as the decode did. A failure counts [`FAILED_DECODE_BYTES`] towards the
|
||||
/// budget, so failures can't grow the map without bound either. A failure that may come from
|
||||
/// what the page had left (its JBIG2 allowance, `ContentTruncated`) isn't kept: another page,
|
||||
/// or the same page drawn again, starts with a fresh allowance.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DecodedImageCache {
|
||||
inner: Arc<Mutex<DecodedImageCacheInner>>,
|
||||
@@ -108,14 +117,55 @@ pub(crate) struct DecodedImageKey {
|
||||
pub(crate) default_space: Option<u128>,
|
||||
pub(crate) target: Option<(u32, u32)>,
|
||||
pub(crate) preview: bool,
|
||||
/// PdfCraft patch (82): the size a soft mask far larger than where it is drawn was averaged
|
||||
/// down to (`ImageXObject::decoded_soft_mask`), `None` for a mask kept at its own size.
|
||||
pub(crate) shrunk_to: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
/// PdfCraft patch (91): what the budget charges for a failed decode, about what its key and
|
||||
/// entry take in the map.
|
||||
const FAILED_DECODE_BYTES: usize = 128;
|
||||
|
||||
/// PdfCraft patch (91): what a [`DecodedImageCache`] keeps for a key.
|
||||
enum DecodedEntry {
|
||||
/// The decoded image.
|
||||
Decoded(Box<dyn Any + Send + Sync>),
|
||||
/// The decode failed, reporting these warnings ([`warning_bit`] flags).
|
||||
Failed(u8),
|
||||
}
|
||||
|
||||
/// PdfCraft patch (91): `warning` as a flag in a set of warnings.
|
||||
fn warning_bit(warning: InterpreterWarning) -> u8 {
|
||||
match warning {
|
||||
InterpreterWarning::UnsupportedFont => 1,
|
||||
InterpreterWarning::ImageDecodeFailure => 2,
|
||||
InterpreterWarning::ContentTruncated => 4,
|
||||
InterpreterWarning::ColorFallback => 8,
|
||||
InterpreterWarning::Type3WidthsMissing => 16,
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (91): the warnings in a set of [`warning_bit`] flags.
|
||||
fn warnings_in(bits: u8) -> impl Iterator<Item = InterpreterWarning> {
|
||||
[
|
||||
InterpreterWarning::UnsupportedFont,
|
||||
InterpreterWarning::ImageDecodeFailure,
|
||||
InterpreterWarning::ContentTruncated,
|
||||
InterpreterWarning::ColorFallback,
|
||||
InterpreterWarning::Type3WidthsMissing,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(move |warning| bits & warning_bit(*warning) != 0)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DecodedImageCacheInner {
|
||||
map: HashMap<DecodedImageKey, Option<Box<dyn Any + Send + Sync>>>,
|
||||
map: HashMap<DecodedImageKey, DecodedEntry>,
|
||||
bytes: usize,
|
||||
/// PdfCraft patch (32): how many keyed decodes ran (cache misses).
|
||||
decodes: usize,
|
||||
/// PdfCraft patch (82): how many of `decodes` were of masks (keys with `mask_output`).
|
||||
mask_decodes: usize,
|
||||
}
|
||||
|
||||
impl DecodedImageCache {
|
||||
@@ -132,6 +182,15 @@ impl DecodedImageCache {
|
||||
self.inner.lock().map(|locked| locked.decodes).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): how many of [`Self::decodes`] were of soft masks and `/Mask` images,
|
||||
/// for tests. 0 when the lock is poisoned.
|
||||
pub(crate) fn mask_decodes(&self) -> usize {
|
||||
self.inner
|
||||
.lock()
|
||||
.map(|locked| locked.mask_decodes)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The same cache, for a preview (`true`) or a final render.
|
||||
pub(crate) fn for_render(&self, preview: bool) -> Self {
|
||||
Self {
|
||||
@@ -140,44 +199,76 @@ impl DecodedImageCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// The decoded image for `key`, from the cache or from `f`. `None` as the key decodes
|
||||
/// without the cache.
|
||||
/// The decoded image for `key`, from the cache or from `f`, which decodes reporting its
|
||||
/// warnings to the sink it is handed. `None` as the key decodes without the cache.
|
||||
/// PdfCraft patch (91): `sink` gets the warnings of the decode, or those of the failed
|
||||
/// decode kept for `key`.
|
||||
pub(crate) fn get_or_insert_with<T: Clone + Send + Sync + 'static>(
|
||||
&self,
|
||||
key: Option<DecodedImageKey>,
|
||||
budget: usize,
|
||||
size_of: impl FnOnce(&T) -> usize,
|
||||
f: impl FnOnce() -> Option<T>,
|
||||
sink: &WarningSinkFn,
|
||||
f: impl FnOnce(&WarningSinkFn) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
let Some(key) = key else { return f() };
|
||||
// Fast path: a hit clones the pixels without decoding.
|
||||
let Some(key) = key else { return f(sink) };
|
||||
// Fast path: a hit clones the pixels without decoding, and a kept failure reports its
|
||||
// warnings again without decoding.
|
||||
let hit = match self.inner.lock() {
|
||||
Ok(locked) => locked
|
||||
.map
|
||||
.get(&key)
|
||||
.and_then(|val| val.as_ref())
|
||||
.and_then(|val| val.downcast_ref::<T>().cloned()),
|
||||
Ok(locked) => match locked.map.get(&key) {
|
||||
Some(DecodedEntry::Decoded(val)) => val.downcast_ref::<T>().cloned().map(Ok),
|
||||
Some(DecodedEntry::Failed(bits)) => Some(Err(*bits)),
|
||||
None => None,
|
||||
},
|
||||
// A thread panicked while holding the lock: decode without the cache.
|
||||
Err(_) => return f(),
|
||||
Err(_) => return f(sink),
|
||||
};
|
||||
if let Some(hit) = hit {
|
||||
return Some(hit);
|
||||
match hit {
|
||||
Some(Ok(hit)) => return Some(hit),
|
||||
Some(Err(bits)) => {
|
||||
warnings_in(bits).for_each(|warning| sink(warning));
|
||||
return None;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
// Miss: decode without holding the lock (decoding must never deadlock the cache).
|
||||
let val = f();
|
||||
// Miss: decode without holding the lock (decoding must never deadlock the cache),
|
||||
// noting the warnings it reports.
|
||||
let reported = Arc::new(AtomicU8::new(0));
|
||||
let recording: WarningSinkFn = {
|
||||
let (reported, sink) = (reported.clone(), sink.clone());
|
||||
Arc::new(move |warning| {
|
||||
reported.fetch_or(warning_bit(warning), Ordering::Relaxed);
|
||||
sink(warning);
|
||||
})
|
||||
};
|
||||
let val = f(&recording);
|
||||
let reported = reported.load(Ordering::Relaxed);
|
||||
let Ok(mut locked) = self.inner.lock() else {
|
||||
return val;
|
||||
};
|
||||
locked.decodes = locked.decodes.saturating_add(1);
|
||||
if key.mask_output {
|
||||
locked.mask_decodes = locked.mask_decodes.saturating_add(1);
|
||||
}
|
||||
// Another thread may have decoded it meanwhile: keep the first copy.
|
||||
if let Some(existing) = locked.map.get(&key)
|
||||
&& let Some(existing) = existing.as_ref().and_then(|val| val.downcast_ref::<T>())
|
||||
{
|
||||
return Some(existing.clone());
|
||||
match locked.map.get(&key) {
|
||||
Some(DecodedEntry::Decoded(existing)) => {
|
||||
if let Some(existing) = existing.downcast_ref::<T>() {
|
||||
return Some(existing.clone());
|
||||
}
|
||||
}
|
||||
Some(DecodedEntry::Failed(_)) if val.is_none() => return None,
|
||||
_ => {}
|
||||
}
|
||||
let size = match &val {
|
||||
Some(v) => size_of(v),
|
||||
None => 0,
|
||||
// A failure the page's JBIG2 allowance may have caused isn't the image's to keep.
|
||||
None if reported & warning_bit(InterpreterWarning::ContentTruncated) != 0
|
||||
|| hayro_syntax::page_jbig2_work_spent() =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
None => FAILED_DECODE_BYTES,
|
||||
};
|
||||
if size > budget {
|
||||
return val;
|
||||
@@ -186,12 +277,14 @@ impl DecodedImageCache {
|
||||
locked.map.clear();
|
||||
locked.bytes = 0;
|
||||
}
|
||||
let entry = match &val {
|
||||
Some(val) => DecodedEntry::Decoded(Box::new(val.clone())),
|
||||
None => DecodedEntry::Failed(reported),
|
||||
};
|
||||
// An entry replaced (a failure another thread kept) isn't given back: the budget
|
||||
// errs towards a fresh epoch.
|
||||
locked.bytes = locked.bytes.saturating_add(size);
|
||||
locked.map.insert(
|
||||
key,
|
||||
val.clone()
|
||||
.map(|val| Box::new(val) as Box<dyn Any + Send + Sync>),
|
||||
);
|
||||
locked.map.insert(key, entry);
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
+68
-1
@@ -5,7 +5,7 @@ use crate::font::{Font, StandardFont};
|
||||
use crate::interpret::state::{ClipType, State, TextStateFont};
|
||||
use crate::ocg::OcgState;
|
||||
use crate::util::{BezPathExt, Float64Ext};
|
||||
use crate::{ClipPath, Device, FillRule, InterpreterSettings, StrokeProps};
|
||||
use crate::{ClipPath, Device, FillRule, InterpreterSettings, InterpreterWarning, StrokeProps};
|
||||
use hayro_syntax::content::ops::Transform;
|
||||
use hayro_syntax::object::Dict;
|
||||
use hayro_syntax::object::Name;
|
||||
@@ -92,6 +92,37 @@ pub(crate) fn decode_content<'a>(stream: &Stream<'a>) -> Option<DecodedContent<'
|
||||
Some(DecodedContent { data, held })
|
||||
}
|
||||
|
||||
/// PdfCraft patch (87): what is left of this page's [`MAX_PAGE_CONTENT`].
|
||||
pub(crate) fn content_left() -> usize {
|
||||
CONTENT_LEFT.with(core::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// PdfCraft patch (87): `len` bytes of content held within the page's [`MAX_PAGE_CONTENT`]
|
||||
/// until dropped, as [`DecodedContent`] holds what it decoded: content kept from an earlier
|
||||
/// decode (a cached Type 3 glyph procedure) counts as it did when it was decoded.
|
||||
pub(crate) struct HeldContent(usize);
|
||||
|
||||
impl Drop for HeldContent {
|
||||
fn drop(&mut self) {
|
||||
let held = self.0;
|
||||
CONTENT_LEFT.with(|c| c.set(c.get().saturating_add(held).min(MAX_PAGE_CONTENT)));
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (87): hold `len` bytes of content within what is left of the page's
|
||||
/// [`MAX_PAGE_CONTENT`]. `None`, and the page reported truncated, when they don't fit, as
|
||||
/// [`decode_content`] skips content the budget can't hold.
|
||||
pub(crate) fn hold_content(len: usize) -> Option<HeldContent> {
|
||||
let left = content_left();
|
||||
if left == 0 || left < len {
|
||||
CONTENT_TRUNCATED.with(|truncated| truncated.set(true));
|
||||
warn!("page content budget exceeded");
|
||||
return None;
|
||||
}
|
||||
charge_content(len);
|
||||
Some(HeldContent(len))
|
||||
}
|
||||
|
||||
/// PdfCraft patch: count `len` bytes held for the rest of the page (its own contents).
|
||||
pub(crate) fn charge_content(len: usize) {
|
||||
CONTENT_LEFT.with(|c| c.set(c.get().saturating_sub(len)));
|
||||
@@ -129,6 +160,22 @@ pub struct InterpreterCache<'a> {
|
||||
/// PdfCraft patch: decoded images, shared by every render through this cache so a tile
|
||||
/// no longer decodes its page's images again.
|
||||
pub(crate) decoded_cache: DecodedImageCache,
|
||||
/// PdfCraft patch (87): what the Type 3 fonts of this cache keep of their decoded glyph
|
||||
/// procedures, together (see `font::type3`).
|
||||
pub(crate) type3_procs: Rc<Type3Procs>,
|
||||
}
|
||||
|
||||
/// PdfCraft patch (87): decoded Type 3 glyph procedure bytes all the fonts of one
|
||||
/// [`InterpreterCache`] keep together. Fonts live as long as the cache, so the bound is on the
|
||||
/// cache, not on each font: a document can have thousands of Type 3 fonts.
|
||||
pub(crate) const MAX_CACHED_TYPE3_PROCS: usize = 32 << 20;
|
||||
|
||||
/// PdfCraft patch (87): the decoded glyph procedure bytes the Type 3 fonts of a cache keep,
|
||||
/// and how many procedures they decoded.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Type3Procs {
|
||||
pub(crate) held: core::cell::Cell<usize>,
|
||||
pub(crate) decodes: core::cell::Cell<usize>,
|
||||
}
|
||||
|
||||
impl<'a> Default for InterpreterCache<'a> {
|
||||
@@ -144,15 +191,29 @@ impl<'a> InterpreterCache<'a> {
|
||||
font_cache: Rc::new(RefCell::new(HashMap::new())),
|
||||
object_cache: Cache::new(),
|
||||
decoded_cache: DecodedImageCache::new(),
|
||||
type3_procs: Rc::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (87): how many Type 3 glyph procedures the fonts of this cache decoded,
|
||||
/// for diagnostics and tests. A procedure kept from an earlier copy of its glyph isn't
|
||||
/// decoded again.
|
||||
pub fn type3_proc_decodes(&self) -> usize {
|
||||
self.type3_procs.decodes.get()
|
||||
}
|
||||
|
||||
/// PdfCraft patch (32): how many images were decoded through this cache (its misses, not
|
||||
/// counting images that are never cached), for diagnostics and tests.
|
||||
pub fn decoded_image_decodes(&self) -> usize {
|
||||
self.decoded_cache.decodes()
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): how many of [`Self::decoded_image_decodes`] were of soft masks and
|
||||
/// `/Mask` images, for diagnostics and tests.
|
||||
pub fn decoded_mask_decodes(&self) -> usize {
|
||||
self.decoded_cache.mask_decodes()
|
||||
}
|
||||
|
||||
/// PdfCraft patch (48): how many `/ICCBased` profiles were read through this cache, for
|
||||
/// diagnostics and tests. A profile used again, by the same stream object or by a copy with
|
||||
/// the same dictionary and bytes, is read once.
|
||||
@@ -586,6 +647,12 @@ impl<'a> Context<'a> {
|
||||
.clone()
|
||||
};
|
||||
|
||||
// PdfCraft patch (90): tell the caller a Type 3 font lacks the /Widths it requires,
|
||||
// so the spacing taken from its glyph procedures can be reported.
|
||||
if resolved.as_ref().is_some_and(Font::type3_widths_missing) {
|
||||
(self.settings.warning_sink)(InterpreterWarning::Type3WidthsMissing);
|
||||
}
|
||||
|
||||
if let Some(resolved) = resolved {
|
||||
Some(TextStateFont::Font(resolved))
|
||||
} else {
|
||||
|
||||
Vendored
+83
-5
@@ -226,7 +226,8 @@ fn encode_axial_shading(
|
||||
}
|
||||
|
||||
/// PdfCraft patch: pixel visits one mesh shading may make, per pixel of the area it is drawn in
|
||||
/// and per triangle. Sampling walks each triangle's bounding box (rounded out to whole pixels),
|
||||
/// and per triangle. Sampling walks each triangle's bounding box (rounded out to whole pixels;
|
||||
/// patch 99: only the columns of each row the triangle can reach),
|
||||
/// so triangles far off the page (a fuzzed patch decoded onto ±40000) asked for 65536 × 65536
|
||||
/// samples, and many triangles covering the same pixels repeat the work. A triangle's box is
|
||||
/// about twice its area, and a sub-pixel triangle's box is at most 2 × 2 pixels.
|
||||
@@ -346,13 +347,43 @@ impl TriangleSampler {
|
||||
}
|
||||
self.visits_left = self.visits_left.saturating_add(MESH_VISITS_PER_TRIANGLE);
|
||||
|
||||
let (columns_start, columns_end) = (bbox.x0.floor() as u16, bbox.x1.ceil() as u16);
|
||||
for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) {
|
||||
for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) {
|
||||
if self.visits_left == 0 {
|
||||
warn!("mesh shading visit budget exceeded; the rest is not drawn");
|
||||
// PdfCraft patch (99): only the columns where this row's sample points (the
|
||||
// corner and the centre, at heights `y` and `y + 0.5`) can lie in the triangle,
|
||||
// not the whole box. A thin triangle lying across the pixel grid has a box many
|
||||
// times its area (the cells of a narrow curved band of patches, cut into a fixed
|
||||
// 19 × 19 grid, are a few pixels long and a fraction of one wide, at every angle),
|
||||
// and walking the box spent the visit budget before the last patches: painted
|
||||
// through a path the size of its own box, a rainbow of tensor patches lost its
|
||||
// inner bands. A box two columns wide or less is walked whole (no row of it is
|
||||
// narrower by more than a column, which isn't worth finding), and a row with no
|
||||
// column still costs a visit, so the walk stays within the budget however tall
|
||||
// the triangle is.
|
||||
let columns = if columns_end.saturating_sub(columns_start) <= 2 {
|
||||
columns_start..columns_end
|
||||
} else {
|
||||
let top = f64::from(y);
|
||||
match x_extent_between(&t, top - SPAN_SLACK, top + 0.5 + SPAN_SLACK) {
|
||||
// Pixel `x` samples at `x` and `x + 0.5`; the casts saturate.
|
||||
Some((lo, hi)) => {
|
||||
let start = (lo - 0.5 - SPAN_SLACK).floor() as u16;
|
||||
let end = ((hi + SPAN_SLACK).floor() as u16).saturating_add(1);
|
||||
columns_start.max(start)..columns_end.min(end)
|
||||
}
|
||||
None => 0..0,
|
||||
}
|
||||
};
|
||||
if columns.is_empty() {
|
||||
if !self.take_visit() {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for x in columns {
|
||||
if !self.take_visit() {
|
||||
return false;
|
||||
}
|
||||
self.visits_left -= 1;
|
||||
let corner = Point::new(x as f64, y as f64);
|
||||
let sampled = if t.contains_point(corner) {
|
||||
self.map.insert((x, y), t.interpolate(corner));
|
||||
@@ -379,6 +410,53 @@ impl TriangleSampler {
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Spend one visit; `false` (with a warning) when none is left.
|
||||
fn take_visit(&mut self) -> bool {
|
||||
if self.visits_left == 0 {
|
||||
warn!("mesh shading visit budget exceeded; the rest is not drawn");
|
||||
return false;
|
||||
}
|
||||
self.visits_left -= 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (99): how far past a row's sample points the triangle's extent is still
|
||||
/// walked, in pixels. A sample a little outside the triangle can still pass `covers`, whose
|
||||
/// tolerance is relative to the triangle's size (1e-9 of it, under 1e-4 pixel on a 65536-pixel
|
||||
/// canvas); this is wider than that and than the rounding of the extent itself. A triangle
|
||||
/// whose corners all but coincide (two of them 1e-13 pixel apart, its area 1e-14) can pass
|
||||
/// `covers` for points well outside it, through rounding in its barycentric division; the
|
||||
/// box walk took such points, the extent leaves them out.
|
||||
const SPAN_SLACK: f64 = 1e-3;
|
||||
|
||||
/// PdfCraft patch (99): the least and greatest x of the part of `t` between the heights `y0`
|
||||
/// and `y1`, or `None` when no part of it is there: the ends of that part are the triangle's
|
||||
/// corners inside the band and the points where its edges cross the band's two lines.
|
||||
fn x_extent_between(t: &Triangle, y0: f64, y1: f64) -> Option<(f64, f64)> {
|
||||
let corners = [t.p0.point, t.p1.point, t.p2.point];
|
||||
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
let mut add = |x: f64| {
|
||||
lo = lo.min(x);
|
||||
hi = hi.max(x);
|
||||
};
|
||||
for (a, b) in corners.iter().zip(corners.iter().cycle().skip(1)) {
|
||||
if (y0..=y1).contains(&a.y) {
|
||||
add(a.x);
|
||||
}
|
||||
// A level edge lies along the band or misses it; its corners are taken above.
|
||||
if a.y != b.y {
|
||||
for line in [y0, y1] {
|
||||
let s = (line - a.y) / (b.y - a.y);
|
||||
if (0.0..=1.0).contains(&s) {
|
||||
add(a.x + s * (b.x - a.x));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// NaN corners add nothing (every comparison fails), so this is `None` for them.
|
||||
(lo <= hi).then_some((lo, hi))
|
||||
}
|
||||
|
||||
/// The triangle `v0`, `v1`, `v2` with its points mapped through `transform` (its colours as
|
||||
|
||||
+54
-8
@@ -67,18 +67,23 @@ impl CffFontBlob {
|
||||
let font = CffFontRef::new(data.as_ref().as_ref(), 0, None).ok()?;
|
||||
let cff = Cff::read(ReadFontData::new(data.as_ref().as_ref())).ok()?;
|
||||
let charset = font.charset();
|
||||
// PdfCraft patch (93): a font dict that cannot be read (a CID font's FDArray entry
|
||||
// emptied by a subsetter, with no Private DICT) leaves only the glyphs it selects
|
||||
// missing. The font is dropped only when no font dict is usable.
|
||||
let subfonts = (0..font.num_subfonts())
|
||||
.map(|index| font.subfont(index, &[]).ok())
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
.collect::<Vec<_>>();
|
||||
if !subfonts.iter().any(Option::is_some) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let yoke = Yoke::<CFFYoke<'static>, FontData>::attach_to_cart(data.clone(), |data| {
|
||||
// PdfCraft patch (93): the subfonts read above own their data, so they move into the
|
||||
// yoke instead of being read again.
|
||||
let yoke = Yoke::<CFFYoke<'static>, FontData>::attach_to_cart(data.clone(), move |data| {
|
||||
let bytes = data.as_ref();
|
||||
let font = CffFontRef::new(bytes, 0, None).unwrap();
|
||||
let cff = Cff::read(ReadFontData::new(bytes)).unwrap();
|
||||
let charset = font.charset();
|
||||
let subfonts = (0..font.num_subfonts())
|
||||
.map(|index| font.subfont(index, &[]).unwrap())
|
||||
.collect();
|
||||
CFFYoke {
|
||||
font,
|
||||
cff,
|
||||
@@ -87,7 +92,7 @@ impl CffFontBlob {
|
||||
}
|
||||
});
|
||||
|
||||
let _ = (cff, charset, subfonts);
|
||||
let _ = (cff, charset);
|
||||
Some(Self(Arc::new(yoke), Arc::new(OnceLock::new())))
|
||||
}
|
||||
|
||||
@@ -105,7 +110,7 @@ impl CffFontBlob {
|
||||
|
||||
fn subfont(&self, glyph: GlyphId) -> Option<&Subfont> {
|
||||
let index = self.font().subfont_index(glyph)? as usize;
|
||||
self.0.as_ref().get().subfonts.get(index)
|
||||
self.0.as_ref().get().subfonts.get(index)?.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
|
||||
@@ -195,8 +200,48 @@ impl Debug for OpenTypeFontBlob {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (83): a copy of the font with its `head.indexToLocFormat` set to the format its
|
||||
/// `loca` table is in, when the field holds neither 0 (short offsets) nor 1 (long), or `None` when
|
||||
/// it needs no change. The glyph outline reader takes only 1 as long, so a font whose field reads
|
||||
/// 256 (the value's bytes swapped) had its long `loca` read as short offsets: every glyph came
|
||||
/// out wrong, and a line of text set in it drew boxes (pdf.js `issue2537r.pdf`). The `loca`
|
||||
/// table's length decides: room for `numGlyphs + 1` offsets of 4 bytes is long, room for only
|
||||
/// that many of 2 bytes short (a table may carry padding after its offsets), and a table too
|
||||
/// short for either is left as it is.
|
||||
fn with_loca_format_fixed(data: &[u8], index: u32) -> Option<Vec<u8>> {
|
||||
use skrifa::raw::types::Tag;
|
||||
const HEAD: Tag = Tag::new(b"head");
|
||||
const LOCA: Tag = Tag::new(b"loca");
|
||||
// The `indexToLocFormat` field's offset in `head`.
|
||||
const FORMAT_AT: usize = 50;
|
||||
let f = FontRef::from_index(data, index).ok()?;
|
||||
let format = f.head().ok()?.index_to_loc_format();
|
||||
if format == 0 || format == 1 {
|
||||
return None;
|
||||
}
|
||||
let offsets = usize::from(f.maxp().ok()?.num_glyphs()).checked_add(1)?;
|
||||
let loca_len = f.table_data(LOCA)?.len();
|
||||
let long = if loca_len >= offsets.checked_mul(4)? {
|
||||
true
|
||||
} else if loca_len >= offsets.checked_mul(2)? {
|
||||
false
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let head_at = f.table_directory.table_records().iter().find(|r| r.tag() == HEAD)?.offset() as usize;
|
||||
let at = head_at.checked_add(FORMAT_AT)?;
|
||||
let mut fixed = data.to_vec();
|
||||
fixed.get_mut(at..at.checked_add(2)?)?.copy_from_slice(&i16::from(long).to_be_bytes());
|
||||
Some(fixed)
|
||||
}
|
||||
|
||||
impl OpenTypeFontBlob {
|
||||
pub(crate) fn new(data: FontData, index: u32) -> Option<Self> {
|
||||
// PdfCraft patch (83): see `with_loca_format_fixed`.
|
||||
let data = match with_loca_format_fixed(data.as_ref().as_ref(), index) {
|
||||
Some(fixed) => Arc::new(fixed) as FontData,
|
||||
None => data,
|
||||
};
|
||||
// Check first whether the font is valid so we can unwrap in the closure.
|
||||
let f = FontRef::from_index(data.as_ref().as_ref(), index).ok()?;
|
||||
// Reject fonts with invalid post table version, fixes pdf.js issue 9462. Not sure if there
|
||||
@@ -377,7 +422,8 @@ struct CFFYoke<'a> {
|
||||
font: CffFontRef<'a>,
|
||||
cff: Cff<'a>,
|
||||
charset: Option<Charset<'a>>,
|
||||
subfonts: Vec<Subfont>,
|
||||
/// PdfCraft patch (93): `None` for a font dict that could not be read.
|
||||
subfonts: Vec<Option<Subfont>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+157
-9
@@ -16,10 +16,12 @@ use kurbo::{BezPath, Vec2};
|
||||
use skrifa::attribute::Style;
|
||||
use skrifa::charmap::MapVariant;
|
||||
use skrifa::raw::collections::int_set::Domain;
|
||||
use skrifa::raw::tables::post::DEFAULT_GLYPH_NAMES;
|
||||
use skrifa::{FontRef, GlyphId, MetadataProvider};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Type0Font {
|
||||
@@ -42,6 +44,9 @@ pub(crate) struct Type0Font {
|
||||
/// Whether the `to_unicode` map is a UCS2 `CMap` (CID-indexed) rather than
|
||||
/// a `ToUnicode` `CMap` (code-indexed).
|
||||
to_unicode_is_cid_indexed: bool,
|
||||
/// PdfCraft patch (84): the character collection's UCS2 CMap (by CID), for the codes the
|
||||
/// font's own `/ToUnicode` gives nothing for, whether or not its program is embedded.
|
||||
ucs2: Option<Ucs2>,
|
||||
/// PdfCraft patch: whether the CIDs belong to an Adobe character collection (Japan1, GB1,
|
||||
/// CNS1, Korea1), whose glyph numbering no substitute other than a CID-keyed CFF font shares.
|
||||
adobe_collection: bool,
|
||||
@@ -50,6 +55,11 @@ pub(crate) struct Type0Font {
|
||||
/// such text as the code's Unicode value; the identity CMap the name resolves to maps codes
|
||||
/// to CIDs only and gave no text at all.
|
||||
identity_to_unicode: bool,
|
||||
/// PdfCraft patch: an Arial or a Times New Roman the PDF doesn't embed, as a TrueType CID
|
||||
/// font (`CIDFontType2`) whose CIDs aren't of an Adobe character collection. Its CIDToGIDMap
|
||||
/// gives glyph indexes in the author's font, which numbers its first 258 glyphs in the
|
||||
/// standard Macintosh order of the TrueType `post` table (#5889, #6449).
|
||||
mac_glyph_order: bool,
|
||||
}
|
||||
|
||||
impl Type0Font {
|
||||
@@ -131,18 +141,24 @@ impl Type0Font {
|
||||
.is_some_and(|n| matches!(n.as_ref(), b"Identity-H" | b"Identity-V"));
|
||||
let mut to_unicode_is_cid_indexed = false;
|
||||
|
||||
// PdfCraft patch (84): the character collection's UCS2 CMap (ISO 32000-2 §9.10.2), CID
|
||||
// to Unicode, for a font of an Adobe collection whether or not its program is embedded.
|
||||
let mut ucs2 = character_collection
|
||||
.as_ref()
|
||||
.and_then(|cc| cc.family.ucs2_cmap())
|
||||
.and_then(|name| (cmap_resolver)(name))
|
||||
.map(|data| Ucs2 {
|
||||
data,
|
||||
resolver: cmap_resolver.clone(),
|
||||
map: OnceLock::new(),
|
||||
});
|
||||
// If there is no ToUnicode map, try to get the UCS2 CMap.
|
||||
if fallback
|
||||
&& to_unicode.is_none()
|
||||
&& let Some(cc) = character_collection.as_ref()
|
||||
&& let Some(ucs2_name) = cc.family.ucs2_cmap()
|
||||
&& let Some(data) = (cmap_resolver)(ucs2_name)
|
||||
&& let Some(ucs2_cmap) = ucs2.take().and_then(|u| u.parse())
|
||||
{
|
||||
let resolver = cmap_resolver.clone();
|
||||
if let Some(ucs2_cmap) = CMap::parse(data, move |n| (resolver)(n)) {
|
||||
to_unicode = Some(ucs2_cmap);
|
||||
to_unicode_is_cid_indexed = true;
|
||||
}
|
||||
to_unicode = Some(ucs2_cmap);
|
||||
to_unicode_is_cid_indexed = true;
|
||||
}
|
||||
|
||||
let adobe_collection = character_collection
|
||||
@@ -153,6 +169,17 @@ impl Type0Font {
|
||||
.get::<Name<'_>>(BASE_FONT)
|
||||
.map(|n| strip_subset_prefix(n.as_str()).to_string());
|
||||
|
||||
// PdfCraft patch: the descendant names the font program itself; a Type 0 font's own
|
||||
// `/BaseFont` may carry the CMap's name after it.
|
||||
let mac_glyph_order = fallback
|
||||
&& !adobe_collection
|
||||
&& descendant_font
|
||||
.get::<Name<'_>>(SUBTYPE)
|
||||
.is_some_and(|s| s.deref() == CID_FONT_TYPE2)
|
||||
&& descendant_font
|
||||
.get::<Name<'_>>(BASE_FONT)
|
||||
.is_some_and(|n| numbers_glyphs_in_mac_order(strip_subset_prefix(n.as_str())));
|
||||
|
||||
// Extract font flags from descriptor
|
||||
let font_flags = font_descriptor
|
||||
.get::<u32>(FLAGS)
|
||||
@@ -173,8 +200,10 @@ impl Type0Font {
|
||||
font_flags,
|
||||
fallback,
|
||||
to_unicode_is_cid_indexed,
|
||||
ucs2,
|
||||
adobe_collection,
|
||||
identity_to_unicode,
|
||||
mac_glyph_order,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,6 +227,16 @@ impl Type0Font {
|
||||
return glyph;
|
||||
}
|
||||
|
||||
// PdfCraft patch: the CID of an Arial or a Times New Roman the PDF doesn't embed names
|
||||
// one of that font's glyphs, which the substitute draws by that name. Taken as the
|
||||
// substitute's own glyph index it drew an unrelated glyph: "Low Power" as
|
||||
// "@ck 'Dck Yf" (#5889), "Salarié(e) du régime" as "Salari²*g+f w't²i lo" (#6449).
|
||||
if self.mac_glyph_order
|
||||
&& let Some(glyph) = self.map_via_mac_glyph_name(cid)
|
||||
{
|
||||
return glyph;
|
||||
}
|
||||
|
||||
// PdfCraft patch: a substitute that isn't CID-keyed numbers its glyphs its own way, so
|
||||
// the CID of an Adobe character collection taken as its glyph index draws an unrelated
|
||||
// glyph. Draw .notdef, as for a CID the font has no glyph for.
|
||||
@@ -307,6 +346,27 @@ impl Type0Font {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the substitute's glyph for the author's glyph that `cid` gives, found by
|
||||
/// that glyph's name in the standard Macintosh order. `None` past the 258 glyphs the order
|
||||
/// names, and for a substitute that isn't a name-keyed CFF font (the standard substitutes
|
||||
/// are); .notdef for a name the substitute has no glyph for.
|
||||
fn map_via_mac_glyph_name(&self, cid: u32) -> Option<GlyphId> {
|
||||
let FontType::Cff(c) = &self.font_type else {
|
||||
return None;
|
||||
};
|
||||
if c.is_cid() {
|
||||
return None;
|
||||
}
|
||||
let gid = self.cid_to_gid_map.map(u16::try_from(cid).ok()?);
|
||||
let name = *DEFAULT_GLYPH_NAMES.get(usize::try_from(gid.to_u32()).ok()?)?;
|
||||
// `.null` and `nonmarkingreturn` draw nothing, and `nonbreakingspace` is a space.
|
||||
let name = match name {
|
||||
".null" | "nonmarkingreturn" | "nonbreakingspace" => "space",
|
||||
name => name,
|
||||
};
|
||||
Some(c.glyph_index_by_name(name).unwrap_or(GlyphId::NOTDEF))
|
||||
}
|
||||
|
||||
fn code_to_cid(&self, code: u32) -> Option<u32> {
|
||||
for byte_len in 1..=4_u8 {
|
||||
if let Some(cid) = self.encoding.lookup_cid_code(code, byte_len) {
|
||||
@@ -415,6 +475,13 @@ impl Type0Font {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the code's width w0 (its horizontal displacement, from `/W` or `/DW`)
|
||||
/// in either writing mode. In vertical writing it is the glyph's extent across the column
|
||||
/// (ISO 32000-2 §9.7.4.3).
|
||||
pub(crate) fn code_horizontal_width(&self, code: u32) -> f32 {
|
||||
self.horizontal_width(self.code_to_cid(code).unwrap_or(0))
|
||||
}
|
||||
|
||||
fn horizontal_width(&self, cid: u32) -> f32 {
|
||||
self.widths.get(&cid).copied().unwrap_or(self.dw)
|
||||
}
|
||||
@@ -466,13 +533,63 @@ impl Type0Font {
|
||||
} else {
|
||||
code
|
||||
};
|
||||
return to_unicode.lookup_bf_string(key);
|
||||
// PdfCraft patch (85): a `/ToUnicode` written as a CID CMap (`begincidrange` /
|
||||
// `begincidchar`, whose destinations are numbers) gives the code's Unicode value as
|
||||
// that number, as glyph lookup already reads it (`map_via_unicode`) and as pdftotext
|
||||
// and PDFium extract it. ISO 32000-2 §9.10.3 has `beginbfrange` / `beginbfchar` here;
|
||||
// Word writes CID ranges, and such text extracted nothing.
|
||||
if let Some(s) = to_unicode.lookup_bf_string(key).or_else(|| {
|
||||
(1..=4)
|
||||
.find_map(|len| {
|
||||
to_unicode
|
||||
.lookup_cid_code(key, len)
|
||||
.and_then(|cid| char::from_u32(cid.to_u32()))
|
||||
})
|
||||
.map(BfString::Char)
|
||||
}) {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
// PdfCraft patch (84): then the collection's UCS2 CMap, by the code's CID.
|
||||
if let Some(ucs2) = self.ucs2.as_ref().and_then(Ucs2::cmap)
|
||||
&& let Some(s) = self
|
||||
.code_to_cid(code)
|
||||
.and_then(|cid| ucs2.lookup_bf_string(cid))
|
||||
{
|
||||
return Some(s);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (84): a collection's UCS2 CMap, parsed the first time a code needs it: a font
|
||||
/// whose `/ToUnicode` maps every code it shows never parses it.
|
||||
struct Ucs2 {
|
||||
data: &'static [u8],
|
||||
resolver: CMapResolverFn,
|
||||
map: OnceLock<Option<CMap>>,
|
||||
}
|
||||
|
||||
impl Ucs2 {
|
||||
fn parse(&self) -> Option<CMap> {
|
||||
let resolver = self.resolver.clone();
|
||||
CMap::parse(self.data, move |n| (resolver)(n))
|
||||
}
|
||||
|
||||
fn cmap(&self) -> Option<&CMap> {
|
||||
self.map.get_or_init(|| self.parse()).as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Ucs2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Ucs2")
|
||||
.field("bytes", &self.data.len())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheKey for Type0Font {
|
||||
fn cache_key(&self) -> u128 {
|
||||
self.cache_key
|
||||
@@ -582,6 +699,37 @@ fn is_variation_selector(c: char) -> bool {
|
||||
matches!(c, '\u{FE00}'..='\u{FE0F}' | '\u{E0100}'..='\u{E01EF}')
|
||||
}
|
||||
|
||||
/// PdfCraft patch: whether `name`, a `/BaseFont` without its subset tag, is one of the four
|
||||
/// faces of Arial or Times New Roman as exporters name them: `ArialMT`, `Arial-BoldMT`,
|
||||
/// `Arial-ItalicMT`, `Arial-BoldItalicMT`, `TimesNewRomanPSMT`, `TimesNewRomanPS-BoldMT` and so
|
||||
/// on, or `Arial` or `TimesNewRoman` followed by `,Bold`, `,Italic` or `,BoldItalic`. Both
|
||||
/// number their first 258 glyphs in the standard Macintosh order. Arial Narrow, Arial Black,
|
||||
/// Arial Unicode MS and Arial Rounded are other fonts, numbered their own way. Letter case
|
||||
/// doesn't matter, `MT` included.
|
||||
fn numbers_glyphs_in_mac_order(name: &str) -> bool {
|
||||
let (family, style) = name.split_once([',', '-', ' ']).unwrap_or((name, ""));
|
||||
let style: String = style
|
||||
.chars()
|
||||
.filter(|c| !matches!(c, ',' | '-' | ' '))
|
||||
.collect();
|
||||
["Arial", "TimesNewRoman", "TimesNewRomanPS"]
|
||||
.iter()
|
||||
.any(|f| without_mt(family).eq_ignore_ascii_case(f))
|
||||
&& ["", "Regular", "Bold", "Italic", "BoldItalic"]
|
||||
.iter()
|
||||
.any(|s| without_mt(&style).eq_ignore_ascii_case(s))
|
||||
}
|
||||
|
||||
/// PdfCraft patch: `name` without a trailing `MT` in any letter case (Monotype's mark in
|
||||
/// PostScript names such as `ArialMT`).
|
||||
fn without_mt(name: &str) -> &str {
|
||||
name.len()
|
||||
.checked_sub(2)
|
||||
.and_then(|stem| Some((name.get(..stem)?, name.get(stem..)?)))
|
||||
.filter(|(_, suffix)| suffix.eq_ignore_ascii_case("MT"))
|
||||
.map_or(name, |(stem, _)| stem)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the largest CID (ISO 32000-2 §9.7.2; CIDs are at most two bytes).
|
||||
const MAX_CID: u32 = 65_535;
|
||||
|
||||
|
||||
+133
-5
@@ -87,6 +87,8 @@ impl Glyph<'_> {
|
||||
/// 1. `ToUnicode` cmap
|
||||
/// 2. Glyph name → Unicode (via Adobe Glyph List)
|
||||
/// 3. Unicode naming conventions (e.g., "uni0041", "u0041")
|
||||
/// 4. PdfCraft patch (100), Type1 and CFF fonts only: a name that only numbers its glyph, as
|
||||
/// for Type3 fonts below
|
||||
///
|
||||
/// **For CID Fonts (Type0):**
|
||||
/// 1. `ToUnicode` cmap
|
||||
@@ -94,6 +96,12 @@ impl Glyph<'_> {
|
||||
///
|
||||
/// **For Type3 Fonts:**
|
||||
/// 1. `ToUnicode` cmap
|
||||
/// 2. PdfCraft patch: the `/Encoding` glyph name → Unicode (an Annex D.2 alias code reads as
|
||||
/// the ordinary character)
|
||||
/// 3. PdfCraft patch: a printable ASCII code (32–126) as itself
|
||||
/// 4. PdfCraft patch (100): above 126, a name that only numbers its glyph (`C174`) as the
|
||||
/// code's Latin-1 character, when the number is that code and every numbered name in the
|
||||
/// font names its own code
|
||||
///
|
||||
/// Returns `None` if the Unicode value could not be determined.
|
||||
///
|
||||
@@ -179,6 +187,19 @@ impl OutlineGlyph {
|
||||
self.font.glyph_advance_width(self.char_code)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: for a glyph of a vertical-writing Type0 font, its width w0 (the
|
||||
/// horizontal displacement from `/W` or `/DW`), which is how wide the glyph sits across the
|
||||
/// column (ISO 32000-2 §9.7.4.3). `None` for horizontal writing, where
|
||||
/// [`advance_width`](Self::advance_width) gives it.
|
||||
pub fn vertical_glyph_width(&self) -> Option<f32> {
|
||||
match &self.font {
|
||||
OutlineFont::Type0(font) if !font.is_horizontal() => {
|
||||
Some(font.code_horizontal_width(self.char_code))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cache key for this glyph's font.
|
||||
///
|
||||
/// This identifies the font uniquely, even when `font_data()` returns `None`
|
||||
@@ -216,9 +237,11 @@ impl<'a> Type3Glyph<'a> {
|
||||
.render_glyph(self, transform, glyph_transform, paint, device);
|
||||
}
|
||||
|
||||
/// Returns the Unicode code point for this glyph, if available.
|
||||
/// Returns the Unicode code point for this glyph, if available: the `ToUnicode` cmap, then
|
||||
/// the `/Encoding` glyph name, a printable ASCII code, and a numbered glyph name at its own
|
||||
/// code (PdfCraft patches).
|
||||
///
|
||||
/// Note: Type3 fonts can only provide Unicode via `ToUnicode` cmap.
|
||||
/// See [`Glyph::as_unicode`] for details on the fallback chain used.
|
||||
pub fn as_unicode(&self) -> Option<BfString> {
|
||||
self.font.char_code_to_unicode(self.char_code)
|
||||
}
|
||||
@@ -244,8 +267,14 @@ impl<'a> Font<'a> {
|
||||
font_resolver: &FontResolverFn,
|
||||
cmap_resolver: &CMapResolverFn,
|
||||
) -> Option<Self> {
|
||||
let f_type = match dict.get::<Name<'_>>(SUBTYPE)?.deref() {
|
||||
TYPE1 | MM_TYPE1 => {
|
||||
// PdfCraft patch (94): a font dictionary whose `/Subtype` is missing or the empty name
|
||||
// (`/Subtype /`) is read as a Type 1 font, so its `/BaseFont` still picks the font: a
|
||||
// `/Courier-Bold` was drawn in the Helvetica fallback (PDFBOX-1988). `/Subtype` is
|
||||
// required (ISO 32000-2 §9.6.2.1, Table 109); other readers fall back to Type 1 this way.
|
||||
let subtype = dict.get::<Name<'_>>(SUBTYPE);
|
||||
let subtype: &[u8] = subtype.as_deref().unwrap_or(b"");
|
||||
let f_type = match subtype {
|
||||
b"" | TYPE1 | MM_TYPE1 => {
|
||||
FontType::Type1(Rc::new(Type1Font::new(dict, font_resolver, cmap_resolver)?))
|
||||
}
|
||||
// PDFBOX-5463: PDF viewers seem to accept OpenType as well.
|
||||
@@ -271,6 +300,12 @@ impl<'a> Font<'a> {
|
||||
Some(Self(cache_key, f_type))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (90): whether this is a Type 3 font without `/Widths` (or
|
||||
/// `/FirstChar`/`/LastChar`), whose glyphs advance by their `d0`/`d1` widths.
|
||||
pub(crate) fn type3_widths_missing(&self) -> bool {
|
||||
matches!(&self.1, FontType::Type3(t) if t.widths_missing())
|
||||
}
|
||||
|
||||
pub(crate) fn new_standard(
|
||||
standard_font: StandardFont,
|
||||
font_resolver: &FontResolverFn,
|
||||
@@ -669,7 +704,7 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
|
||||
return unicode_str.chars().next();
|
||||
}
|
||||
|
||||
unicode_from_name(name).or_else(|| {
|
||||
agl_unicode_from_name(name).or_else(|| {
|
||||
warn!("failed to map glyph name {} to unicode", name);
|
||||
|
||||
None
|
||||
@@ -719,6 +754,99 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option<BfString> {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the numbers a glyph name that only numbers its glyph gives (`C39`, `G27`,
|
||||
/// `MT174`: one to three ASCII letters, then one to six decimal or hexadecimal digits), for
|
||||
/// every split into letters and digits, read in decimal and in hexadecimal (`GA9` is `G` with
|
||||
/// hex `A9`); nothing for any other name. Everything from the first `.` is dropped first, as
|
||||
/// `glyph_name_to_text` drops it.
|
||||
fn glyph_name_numbers(name: &str) -> impl Iterator<Item = u32> + '_ {
|
||||
let base = name.split('.').next().unwrap_or_default();
|
||||
(1..=3).flat_map(move |prefix| {
|
||||
// `from_str_radix` would also take a leading `+`: the number is digits only, and at
|
||||
// most six of them (a code is under 256, leading zeros as in `C0032`).
|
||||
let digits = base
|
||||
.get(..prefix)
|
||||
.zip(base.get(prefix..))
|
||||
.and_then(|(letters, digits)| {
|
||||
(letters.bytes().all(|b| b.is_ascii_alphabetic())
|
||||
&& (1..=6).contains(&digits.len())
|
||||
&& digits.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
.then_some(digits)
|
||||
});
|
||||
digits.into_iter().flat_map(|d| {
|
||||
[10, 16]
|
||||
.into_iter()
|
||||
.filter_map(move |radix| u32::from_str_radix(d, radix).ok())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch: whether every glyph name in a simple font's encoding that only numbers its
|
||||
/// glyph (see `glyph_name_numbers`) names the very code it sits at, in decimal or hexadecimal.
|
||||
/// Names the glyph list or its `uni`/`u` forms read are not numbered names. A font whose names
|
||||
/// number something else (`/Differences [1 /C70 /C105 …]`, a glyph's order in the subset)
|
||||
/// fails, and so does one where a single name numbers another code: there a name that happens
|
||||
/// to match its code (`/MT40` at 0x40, `/C1` at 233 beside `/C70` at 70, review on #3239) is a
|
||||
/// coincidence, not evidence. A subset that counts its glyphs in order of first use from code 1
|
||||
/// (`/Differences [1 /C1 /C2 … /C80]`) names its own codes too, but the counter says nothing
|
||||
/// about the characters (`/C65` is the 65th glyph used, not `A`): a numbered name at a C0
|
||||
/// control code (below 32), which no text shows, gives such a counter away and fails the font.
|
||||
pub(crate) fn numbered_names_name_their_codes<'a>(
|
||||
names: impl IntoIterator<Item = (u8, &'a str)>,
|
||||
) -> bool {
|
||||
names
|
||||
.into_iter()
|
||||
.filter(|(_, name)| {
|
||||
glyph_names::get(name).is_none()
|
||||
&& unicode_from_name(name).is_none()
|
||||
&& glyph_name_to_text(name).is_none()
|
||||
})
|
||||
.all(|(code, name)| {
|
||||
let mut numbers = glyph_name_numbers(name).peekable();
|
||||
numbers.peek().is_none() || (code >= 0x20 && numbers.any(|n| n == u32::from(code)))
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the text of a glyph name outside the glyph list that only numbers its glyph
|
||||
/// (see `glyph_name_numbers`) when that number is the very code the glyph is shown with: the
|
||||
/// code's Latin-1 character, for codes above 127 too (`C174` reads `®`), as pdftotext, PDFium
|
||||
/// and MuPDF read it. The name and the code then agree, so nothing is guessed from an
|
||||
/// encoding table. A number naming another code (`/C1` at 233, review on #3239) and a control
|
||||
/// character read as nothing. Callers apply it only in a font where
|
||||
/// `numbered_names_name_their_codes` holds.
|
||||
pub(crate) fn numbered_glyph_name_at_its_code(name: &str, code: u8) -> Option<BfString> {
|
||||
let c = char::from(code);
|
||||
if c.is_control() {
|
||||
return None;
|
||||
}
|
||||
glyph_name_numbers(name)
|
||||
.any(|n| n == u32::from(code))
|
||||
.then_some(BfString::Char(c))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (92): the character a `uniXXXX` or `uXXXX` to `uXXXXXX` glyph name stands
|
||||
/// for, by the Adobe Glyph List Specification: `uni` and exactly four uppercase hex digits, or
|
||||
/// `u` and four to six; a value in the surrogate range or past U+10FFFF is no character. The
|
||||
/// text of a glyph goes by this rule, so `uni65`, `uni20ac` and `u41` read as nothing, as they
|
||||
/// do in [`glyph_name_to_text`].
|
||||
pub(crate) fn agl_unicode_from_name(name: &str) -> Option<char> {
|
||||
let upper_hex = |d: &str| d.bytes().all(|b| matches!(b, b'0'..=b'9' | b'A'..=b'F'));
|
||||
let digits = name
|
||||
.strip_prefix("uni")
|
||||
.filter(|d| d.len() == 4)
|
||||
.or_else(|| {
|
||||
name.strip_prefix('u')
|
||||
.filter(|d| (4..=6).contains(&d.len()))
|
||||
})
|
||||
.filter(|d| upper_hex(d))?;
|
||||
u32::from_str_radix(digits, 16)
|
||||
.ok()
|
||||
.and_then(char::from_u32)
|
||||
}
|
||||
|
||||
/// Upstream's lenient reading of a `uni…` or `u…` name (any number of hex digits, either case),
|
||||
/// kept for finding a glyph in a font: PdfCraft patch (92) reads text by
|
||||
/// [`agl_unicode_from_name`] instead.
|
||||
pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
|
||||
let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
|
||||
|
||||
|
||||
+39
-2
@@ -4,7 +4,7 @@ use crate::font::generated::{glyph_names, metrics, standard, symbol, zapf_dings}
|
||||
use crate::font::true_type::{Width, read_encoding, read_widths};
|
||||
use crate::font::{
|
||||
Encoding, FontData, FontQuery, glyph_name_to_unicode, normalized_glyph_name, stretch_glyph,
|
||||
strip_subset_prefix,
|
||||
strip_subset_prefix, unicode_from_name,
|
||||
};
|
||||
use hayro_syntax::object::Dict;
|
||||
use hayro_syntax::object::Name;
|
||||
@@ -201,6 +201,32 @@ impl StandardFont {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (98): the style names that may follow `Symbol` or `SymbolMT` after a `,` or
|
||||
/// `-` and still name the Symbol font (any case).
|
||||
const SYMBOL_STYLES: [&str; 9] = [
|
||||
"Bold",
|
||||
"Italic",
|
||||
"BoldItalic",
|
||||
"Oblique",
|
||||
"BoldOblique",
|
||||
"Regular",
|
||||
"Roman",
|
||||
"Normal",
|
||||
"Plain",
|
||||
];
|
||||
|
||||
/// PdfCraft patch (98): whether `name` (without a subset prefix) is `Symbol` or `SymbolMT`, alone
|
||||
/// or followed by `,` or `-` and a style from [`SYMBOL_STYLES`]. Another name after the
|
||||
/// separator (`Symbol-Helvetica`) is a different font.
|
||||
fn is_symbol_name(name: &str) -> bool {
|
||||
let (family, style) = match name.split_once([',', '-']) {
|
||||
Some((family, style)) => (family, Some(style)),
|
||||
None => (name, None),
|
||||
};
|
||||
matches!(family, "Symbol" | "SymbolMT")
|
||||
&& style.is_none_or(|style| SYMBOL_STYLES.iter().any(|s| s.eq_ignore_ascii_case(style)))
|
||||
}
|
||||
|
||||
enum StandardFontFamily {
|
||||
Helvetica,
|
||||
Courier,
|
||||
@@ -233,6 +259,13 @@ pub(crate) fn select_standard_font(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// PdfCraft patch (98): `SymbolMT` (the name Symbol has as a TrueType font) and the style
|
||||
// variants `Symbol,Bold`, `SymbolMT,Italic` or `Symbol-Bold` are the Symbol font, drawn with
|
||||
// its built-in encoding like `Symbol` itself, whatever `/Encoding` and flags they declare.
|
||||
if is_symbol_name(name) {
|
||||
return Some((StandardFont::Symbol, true));
|
||||
}
|
||||
|
||||
// Now, we bruteforce, trying to determine a suitable font based on the
|
||||
// keywords that appear in the name.
|
||||
let lower = name.to_ascii_lowercase();
|
||||
@@ -769,7 +802,11 @@ fn accented_parts(name: &str) -> Option<(&'static str, &'static str, Place)> {
|
||||
.map(|&(_, base, accent, place)| (base, accent, place))
|
||||
};
|
||||
by_name(name).or_else(|| {
|
||||
let c = glyph_name_to_unicode(name)?;
|
||||
// Drawing keeps upstream's lenient reading of `uni` names; text goes by the glyph list
|
||||
// specification (PdfCraft patch 92).
|
||||
let c = glyph_names::get(name)
|
||||
.and_then(|s| s.chars().next())
|
||||
.or_else(|| unicode_from_name(name))?;
|
||||
match COMMA_BELOW_BY_CHAR.iter().find(|(letter, _)| *letter == c) {
|
||||
Some((_, comma_below)) => by_name(comma_below),
|
||||
None => by_name(glyph_names::get_reverse(c)?),
|
||||
|
||||
+90
@@ -18,6 +18,7 @@ use kurbo::BezPath;
|
||||
use skrifa::attribute::Style;
|
||||
use skrifa::raw::TableProvider;
|
||||
use skrifa::raw::tables::cmap::PlatformId;
|
||||
use skrifa::raw::tables::post::DEFAULT_GLYPH_NAMES;
|
||||
use skrifa::{GlyphId, MetadataProvider};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -220,6 +221,9 @@ struct EmbeddedKind {
|
||||
cached_mappings: RefCell<HashMap<u8, GlyphId>>,
|
||||
/// PostScript name from the PDF.
|
||||
postscript_name: Option<String>,
|
||||
/// PdfCraft patch (101): whether the font may take the Mac OS Roman reading at all, worked out
|
||||
/// once when it is loaded (`EmbeddedKind::mac_roman_name`).
|
||||
mac_roman_eligible: bool,
|
||||
}
|
||||
|
||||
impl EmbeddedKind {
|
||||
@@ -247,6 +251,27 @@ impl EmbeddedKind {
|
||||
.get::<Name<'_>>(BASE_FONT)
|
||||
.map(|n| strip_subset_prefix(n.as_str()).to_string());
|
||||
|
||||
// PdfCraft patch (101): the font names no base encoding, not even one this reader doesn't
|
||||
// know (which `read_encoding` reads as the built-in one), and isn't Symbol or ZapfDingbats,
|
||||
// whose built-in encodings aren't Latin (ISO 32000-2 §9.6.6.1). Both as the editor's
|
||||
// `unicode_map` decides them, Symbol and ZapfDingbats recognised by name as there.
|
||||
let names_base_encoding = dict.get::<Name<'_>>(ENCODING).is_some()
|
||||
|| dict
|
||||
.get::<Dict<'_>>(ENCODING)
|
||||
.is_some_and(|d| d.get::<Name<'_>>(BASE_ENCODING).is_some());
|
||||
let symbolic_builtin = dict.get::<Name<'_>>(BASE_FONT).is_some_and(|n| {
|
||||
let name = n.as_str();
|
||||
let flat: String = name
|
||||
.rsplit('+')
|
||||
.next()
|
||||
.unwrap_or(name)
|
||||
.chars()
|
||||
.filter(char::is_ascii_alphanumeric)
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect();
|
||||
flat.contains("symbol") || flat.contains("zapf") || flat.contains("dingbat")
|
||||
});
|
||||
|
||||
Some(Self {
|
||||
base_font,
|
||||
differences,
|
||||
@@ -258,6 +283,7 @@ impl EmbeddedKind {
|
||||
encoding,
|
||||
cached_mappings: RefCell::new(HashMap::new()),
|
||||
postscript_name,
|
||||
mac_roman_eligible: !names_base_encoding && !symbolic_builtin,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -291,6 +317,8 @@ impl EmbeddedKind {
|
||||
// `Encoding::text_alias`.
|
||||
.or_else(|| self.encoding.text_alias(code))
|
||||
.or_else(|| self.encoding.map_code(code))
|
||||
// PdfCraft patch (101): see `EmbeddedKind::mac_roman_name`.
|
||||
.or_else(|| self.mac_roman_name(code))
|
||||
// The last resort reads the code as WinAnsi whatever the font said, so it takes the
|
||||
// same aliasing.
|
||||
.or_else(|| Encoding::WinAnsi.text_alias(code))
|
||||
@@ -400,6 +428,68 @@ impl EmbeddedKind {
|
||||
glyph
|
||||
}
|
||||
|
||||
/// PdfCraft patch (101): the Mac OS Roman glyph name a code of a symbolic font without an
|
||||
/// encoding or glyph names reads as, when two things say the code is Mac OS Roman:
|
||||
///
|
||||
/// - its Macintosh Roman (1, 0) `cmap` subtable selects the glyph the code draws. ISO
|
||||
/// 32000-2 §9.6.5.4 has single bytes "look up the associated glyph descriptions" there, and
|
||||
/// has a (3, 0) subtable map "the single-byte codes in the (1, 0) subtable" prepended with
|
||||
/// 0xF0;
|
||||
/// - the glyph sits where the standard Macintosh glyph order (the 258 names of a version 1.0
|
||||
/// `post` table) puts that character. Fonts distilled from Macintosh originals keep that
|
||||
/// order and drop the names (`post` version 3.0): pdf.js's issue2017r.pdf draws code 0x88
|
||||
/// with glyph 106, `agrave`, Mac OS Roman 0x88.
|
||||
///
|
||||
/// The second test is needed: Windows subsets in the same order carry a (1, 0) subtable too,
|
||||
/// but their codes are WinAnsi (code 0x92 draws glyph 183, `quoteright`, not Mac OS Roman's
|
||||
/// `iacute`), and reading every such code as Mac OS Roman turned "USDA’s" into "USDAís".
|
||||
/// A font that names its glyphs in its `post` table needn't keep the order (a Wingdings subset
|
||||
/// draws code 0x83 with glyph 101, `Eacute` in the order, which it names `threesans`).
|
||||
/// `None` when the font is non-symbolic, names a base encoding (one this reader doesn't know
|
||||
/// too), is Symbol or ZapfDingbats (whose
|
||||
/// built-in encodings aren't Latin, ISO 32000-2 §9.6.6.1; recognised by name as the editor's
|
||||
/// `unicode_map` recognises them), is a CFF program, names its glyphs, or either test fails
|
||||
/// (SAS's AMT fonts carry a wrong (1, 0) table, #4156): the code then reads as before. Glyph
|
||||
/// selection is unchanged.
|
||||
fn mac_roman_name(&self, code: u8) -> Option<&'static str> {
|
||||
if self.is_non_symbolic()
|
||||
|| !self.mac_roman_eligible
|
||||
|| !matches!(self.encoding, Encoding::BuiltIn)
|
||||
|| self.cff_blob.is_some()
|
||||
|| !self.glyph_names.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let glyph = self.map_code(code);
|
||||
if glyph == GlyphId::NOTDEF {
|
||||
return None;
|
||||
}
|
||||
let cmap = self.base_font.font_ref().cmap().ok()?;
|
||||
let mac = cmap.encoding_records().iter().find_map(|record| {
|
||||
if record.platform_id() == PlatformId::Macintosh && record.encoding_id() == 0 {
|
||||
record
|
||||
.subtable(cmap.offset_data())
|
||||
.ok()?
|
||||
.map_codepoint(code)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
if mac != glyph {
|
||||
return None;
|
||||
}
|
||||
let standard = match *DEFAULT_GLYPH_NAMES.get(usize::try_from(glyph.to_u32()).ok()?)? {
|
||||
// The order's name for Mac OS Roman 0xCA; the tables here call it `nbspace`.
|
||||
"nonbreakingspace" => "nbspace",
|
||||
name => name,
|
||||
};
|
||||
let name = mac_os_roman::get(code).or_else(|| mac_roman::get(code))?;
|
||||
if name != standard {
|
||||
return None;
|
||||
}
|
||||
Encoding::MacRoman.text_alias(code).or(Some(name))
|
||||
}
|
||||
|
||||
fn glyph_width(&self, code: u8) -> f32 {
|
||||
match self.widths.get(code as usize).copied() {
|
||||
Some(Width::Value(w)) => w,
|
||||
|
||||
+41
-13
@@ -3,7 +3,7 @@ use crate::font::standard_font::{StandardFont, StandardKind, select_standard_fon
|
||||
use crate::font::true_type::{Width, read_encoding, read_widths};
|
||||
use crate::font::{
|
||||
Encoding, FallbackFontQuery, glyph_name_to_text, glyph_name_to_unicode, normalized_glyph_name,
|
||||
read_to_unicode,
|
||||
numbered_glyph_name_at_its_code, numbered_names_name_their_codes, read_to_unicode,
|
||||
};
|
||||
use crate::{CMapResolverFn, CacheKey, FontResolverFn};
|
||||
use hayro_cmap::{BfString, CMap};
|
||||
@@ -13,10 +13,17 @@ use hayro_syntax::object::dict::keys::{FONT_DESC, FONT_FILE, FONT_FILE3};
|
||||
use kurbo::BezPath;
|
||||
use skrifa::GlyphId;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Type1Font(u128, Kind, Option<CMap>);
|
||||
pub(crate) struct Type1Font(
|
||||
u128,
|
||||
Kind,
|
||||
Option<CMap>,
|
||||
/// PdfCraft patch: whether the font's numbered glyph names all name their own codes
|
||||
/// (`numbered_names_name_their_codes`), worked out on first use.
|
||||
OnceLock<bool>,
|
||||
);
|
||||
|
||||
impl Type1Font {
|
||||
pub(crate) fn new(
|
||||
@@ -50,23 +57,29 @@ impl Type1Font {
|
||||
resolver,
|
||||
)?),
|
||||
to_unicode.clone(),
|
||||
OnceLock::new(),
|
||||
))
|
||||
};
|
||||
|
||||
let inner = if is_cff(dict) {
|
||||
if let Some(cff) = CffKind::new(dict) {
|
||||
Self(cache_key, Kind::Cff(cff), to_unicode)
|
||||
Self(cache_key, Kind::Cff(cff), to_unicode, OnceLock::new())
|
||||
} else {
|
||||
return fallback();
|
||||
}
|
||||
} else if is_type1(dict) {
|
||||
if let Some(f) = Type1Kind::new(dict) {
|
||||
Self(cache_key, Kind::Type1(f), to_unicode)
|
||||
Self(cache_key, Kind::Type1(f), to_unicode, OnceLock::new())
|
||||
} else {
|
||||
return fallback();
|
||||
}
|
||||
} else if let Some(standard) = StandardKind::new(dict, resolver) {
|
||||
Self(cache_key, Kind::Standard(standard), to_unicode)
|
||||
Self(
|
||||
cache_key,
|
||||
Kind::Standard(standard),
|
||||
to_unicode,
|
||||
OnceLock::new(),
|
||||
)
|
||||
} else {
|
||||
return fallback();
|
||||
};
|
||||
@@ -78,7 +91,7 @@ impl Type1Font {
|
||||
let dict = Dict::default();
|
||||
let standard = StandardKind::new_with_standard(&dict, font, true, resolver)?;
|
||||
|
||||
Some(Self(0, Kind::Standard(standard), None))
|
||||
Some(Self(0, Kind::Standard(standard), None, OnceLock::new()))
|
||||
}
|
||||
|
||||
pub(crate) fn map_code(&self, code: u8) -> GlyphId {
|
||||
@@ -128,15 +141,30 @@ impl Type1Font {
|
||||
}
|
||||
// PdfCraft patch (#3169, #3170): a name outside the glyph list still reads by the
|
||||
// glyph list specification's full mapping (`f_i`, `hyphen.case`). A name that only
|
||||
// numbers the glyph (`C39`, `c251`) says nothing about the character and reads as
|
||||
// nothing: the code's StandardEncoding character would be a guess, wrong wherever the
|
||||
// font's /Encoding puts the glyph elsewhere (review on #3239).
|
||||
let name = match &self.1 {
|
||||
// numbers the glyph (`C39`, `c251`) reads as the code's Latin-1 character only when its
|
||||
// number is that code (`C39` at 39) and every numbered name in the font names its own
|
||||
// code, as pdftotext, PDFium and MuPDF read it. Anywhere else the number is no
|
||||
// evidence and reads as nothing: the code's StandardEncoding character would be a
|
||||
// guess, wrong wherever the font's /Encoding puts the glyph elsewhere (review on #3239).
|
||||
let name = self.code_to_ps_name(code)?;
|
||||
glyph_name_to_text(name).or_else(|| {
|
||||
let own_codes = *self.3.get_or_init(|| {
|
||||
numbered_names_name_their_codes(
|
||||
(0..=u8::MAX).filter_map(|c| Some((c, self.code_to_ps_name(c)?))),
|
||||
)
|
||||
});
|
||||
own_codes
|
||||
.then(|| numbered_glyph_name_at_its_code(name, code))
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
fn code_to_ps_name(&self, code: u8) -> Option<&str> {
|
||||
match &self.1 {
|
||||
Kind::Standard(s) => s.code_to_ps_name(code),
|
||||
Kind::Cff(c) => c.code_to_ps_name(code),
|
||||
Kind::Type1(t) => t.code_to_ps_name(code),
|
||||
}?;
|
||||
glyph_name_to_text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+135
-5
@@ -1,9 +1,14 @@
|
||||
use crate::CMapResolverFn;
|
||||
use crate::context::Context;
|
||||
use crate::context::{
|
||||
Context, HeldContent, MAX_CACHED_TYPE3_PROCS, Type3Procs, content_left, hold_content,
|
||||
};
|
||||
use crate::device::Device;
|
||||
use crate::font::glyph_simulator::GlyphSimulator;
|
||||
use crate::font::true_type::{Width, read_encoding, read_widths};
|
||||
use crate::font::{Encoding, Glyph, Type3Glyph, UNITS_PER_EM, read_to_unicode};
|
||||
use crate::font::{
|
||||
Encoding, Glyph, Type3Glyph, UNITS_PER_EM, numbered_glyph_name_at_its_code,
|
||||
numbered_names_name_their_codes, read_to_unicode,
|
||||
};
|
||||
use crate::interpret::state::TextState;
|
||||
use crate::soft_mask::SoftMask;
|
||||
use crate::util::RectExt;
|
||||
@@ -19,12 +24,18 @@ use hayro_syntax::object::dict::keys::{CHAR_PROCS, FONT_BBOX, FONT_MATRIX, RESOU
|
||||
use hayro_syntax::page::Resources;
|
||||
use kurbo::{Affine, BezPath, Rect};
|
||||
use skrifa::GlyphId;
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Type3<'a> {
|
||||
widths: Vec<Width>,
|
||||
missing_width: f32,
|
||||
/// PdfCraft patch (90): the width each code's glyph procedure gives with `d0`/`d1`, read
|
||||
/// the first time the code is shown in a font without `/Widths`. One cell per code.
|
||||
proc_widths: Vec<OnceCell<Option<f32>>>,
|
||||
encoding: Encoding,
|
||||
encodings: HashMap<u8, String>,
|
||||
dict: Dict<'a>,
|
||||
@@ -33,8 +44,23 @@ pub(crate) struct Type3<'a> {
|
||||
font_bbox: Rect,
|
||||
matrix: Affine,
|
||||
to_unicode: Option<CMap>,
|
||||
/// PdfCraft patch (87): glyph procedures already decoded, by glyph name, so a glyph shown
|
||||
/// again and again (a Type 3 font used as a fill pattern draws one glyph hundreds of
|
||||
/// thousands of times) isn't inflated again for every copy. Only procedures of at most
|
||||
/// [`MAX_CACHED_PROC`] bytes are kept, and the fonts of one interpreter cache keep at most
|
||||
/// `context::MAX_CACHED_TYPE3_PROCS` bytes together.
|
||||
decoded_procs: RefCell<HashMap<String, Rc<[u8]>>>,
|
||||
/// PdfCraft patch: whether the font's numbered glyph names all name their own codes
|
||||
/// (`numbered_names_name_their_codes`), worked out on first use.
|
||||
own_code_names: OnceLock<bool>,
|
||||
}
|
||||
|
||||
/// PdfCraft patch (87): the largest decoded glyph procedure [`Type3`] keeps.
|
||||
const MAX_CACHED_PROC: usize = 64 << 10;
|
||||
/// PdfCraft patch (87): what a kept procedure costs the cache besides its bytes and its glyph
|
||||
/// name (the map entry and the shared allocation's counts), so many empty procedures count too.
|
||||
const CACHED_PROC_OVERHEAD: usize = 64;
|
||||
|
||||
impl<'a> Type3<'a> {
|
||||
pub(crate) fn new(dict: &Dict<'a>, cmap_resolver: &CMapResolverFn) -> Option<Self> {
|
||||
let (encoding, encodings) = read_encoding(dict);
|
||||
@@ -71,13 +97,57 @@ impl<'a> Type3<'a> {
|
||||
char_procs,
|
||||
widths,
|
||||
missing_width,
|
||||
proc_widths: vec![OnceCell::new(); usize::from(u8::MAX) + 1],
|
||||
encodings,
|
||||
matrix,
|
||||
dict: dict.clone(),
|
||||
to_unicode,
|
||||
decoded_procs: RefCell::new(HashMap::new()),
|
||||
own_code_names: OnceLock::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch (87): the decoded procedure of glyph `name`, held within the page's
|
||||
/// content budget while the glyph is painted, whether it was decoded now or kept from an
|
||||
/// earlier copy, so a page skips the same glyphs either way once the budget is spent. It is
|
||||
/// decoded the first time and kept, when small enough and while `procs` has room, for the
|
||||
/// next copies. Only a procedure decoded with more budget left than [`MAX_CACHED_PROC`] is
|
||||
/// kept, so one the budget may have cut short isn't.
|
||||
fn decoded_proc(
|
||||
&self,
|
||||
name: &str,
|
||||
program: &Stream<'a>,
|
||||
procs: &Type3Procs,
|
||||
) -> Option<(Rc<[u8]>, HeldContent)> {
|
||||
let kept = self.decoded_procs.borrow().get(name).cloned();
|
||||
if let Some(bytes) = kept {
|
||||
let held = hold_content(bytes.len())?;
|
||||
return Some((bytes, held));
|
||||
}
|
||||
let left = content_left();
|
||||
// PdfCraft patch: see `context::MAX_PAGE_CONTENT`.
|
||||
let decoded = crate::context::decode_content(program)?;
|
||||
procs.decodes.set(procs.decodes.get().saturating_add(1));
|
||||
let bytes: Rc<[u8]> = Rc::from(decoded.as_ref());
|
||||
drop(decoded);
|
||||
let held = hold_content(bytes.len())?;
|
||||
let cost = bytes
|
||||
.len()
|
||||
.saturating_add(name.len())
|
||||
.saturating_add(CACHED_PROC_OVERHEAD);
|
||||
let total = procs.held.get().saturating_add(cost);
|
||||
if left > MAX_CACHED_PROC
|
||||
&& bytes.len() <= MAX_CACHED_PROC
|
||||
&& total <= MAX_CACHED_TYPE3_PROCS
|
||||
{
|
||||
procs.held.set(total);
|
||||
self.decoded_procs
|
||||
.borrow_mut()
|
||||
.insert(name.to_string(), bytes.clone());
|
||||
}
|
||||
Some((bytes, held))
|
||||
}
|
||||
|
||||
pub(crate) fn map_code(&self, code: u8) -> GlyphId {
|
||||
self.encodings
|
||||
.get(&code)
|
||||
@@ -88,13 +158,53 @@ impl<'a> Type3<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn glyph_width(&self, code: u8) -> f32 {
|
||||
// PdfCraft patch (90): /Widths, /FirstChar and /LastChar are required for Type 3 fonts
|
||||
// (Table 110), but a font without them gave every glyph no advance and its text was drawn on one spot. The glyph
|
||||
// procedure's `d0`/`d1` operator states the glyph's width too (9.6.4), so such a font
|
||||
// takes that width, as PDFium does. A font with /Widths is read as before: a code
|
||||
// outside FirstChar..LastChar has width 0 (Table 110), as in Poppler and MuPDF.
|
||||
let w = match self.widths.get(code as usize).copied() {
|
||||
Some(Width::Value(w)) => w,
|
||||
_ if self.widths.is_empty() => self.proc_width(code).unwrap_or(self.missing_width),
|
||||
_ => self.missing_width,
|
||||
};
|
||||
(w * self.matrix.as_coeffs()[0] as f32) * UNITS_PER_EM
|
||||
}
|
||||
|
||||
/// PdfCraft patch (90): whether the font has no `/Widths` (or `/FirstChar`/`/LastChar`), so
|
||||
/// that `glyph_width` falls back on the glyph procedures' `d0`/`d1` widths.
|
||||
pub(crate) fn widths_missing(&self) -> bool {
|
||||
self.widths.is_empty()
|
||||
}
|
||||
|
||||
/// PdfCraft patch (90): the horizontal width `wx` that the glyph procedure for `code`
|
||||
/// sets with `d0` or `d1`, in glyph space. `None` when the code has no procedure, the
|
||||
/// procedure sets no width or its width isn't finite. A procedure the page's content
|
||||
/// budget can't decode isn't remembered, so a later page may still read it.
|
||||
fn proc_width(&self, code: u8) -> Option<f32> {
|
||||
let cell = self.proc_widths.get(usize::from(code))?;
|
||||
if let Some(width) = cell.get() {
|
||||
return *width;
|
||||
}
|
||||
let name = self.glyph_simulator.glyph_to_string(self.map_code(code))?;
|
||||
let program = self.char_procs.get(&name)?;
|
||||
let decoded = crate::context::decode_content(program)?;
|
||||
let mut iter = TypedIter::new(decoded.as_ref());
|
||||
let mut width = None;
|
||||
while let Some(op) = iter.next() {
|
||||
match op {
|
||||
TypedInstruction::ColorGlyph(d0) => width = Some(d0.0.as_f32()),
|
||||
TypedInstruction::ShapeGlyph(d1) => width = Some(d1.0.as_f32()),
|
||||
_ => continue,
|
||||
}
|
||||
break;
|
||||
}
|
||||
let width = width.filter(|w| w.is_finite());
|
||||
// Setting can't fail here: the cell was empty above and nothing in between fills it.
|
||||
let _ = cell.set(width);
|
||||
width
|
||||
}
|
||||
|
||||
pub(crate) fn char_code_to_unicode(&self, char_code: u32) -> Option<BfString> {
|
||||
// PdfCraft patch: upstream only consulted ToUnicode. Many Type3 fonts (e.g. TeX/dvips
|
||||
// bitmap fonts) have none; fall back to the /Encoding glyph names, then to printable
|
||||
@@ -115,9 +225,30 @@ impl<'a> Type3<'a> {
|
||||
name.and_then(crate::font::glyph_name_to_unicode)
|
||||
.or_else(|| (0x20..0x7f).contains(&code).then_some(code as char))
|
||||
.map(BfString::Char)
|
||||
// PdfCraft patch (100): above 126, a name that only numbers its glyph reads
|
||||
// as the code's Latin-1 character when the number is that code and every
|
||||
// numbered name in the font names its own code (`C174` at 174 reads `®`),
|
||||
// as in a Type 1 font.
|
||||
.or_else(|| {
|
||||
let own_codes = *self.own_code_names.get_or_init(|| {
|
||||
numbered_names_name_their_codes(
|
||||
(0..=u8::MAX).filter_map(|c| Some((c, self.code_to_name(c)?))),
|
||||
)
|
||||
});
|
||||
name.filter(|_| own_codes)
|
||||
.and_then(|name| numbered_glyph_name_at_its_code(name, code))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the glyph name `/Encoding` gives a code, as `map_code` reads it.
|
||||
fn code_to_name(&self, code: u8) -> Option<&str> {
|
||||
self.encodings
|
||||
.get(&code)
|
||||
.map(String::as_str)
|
||||
.or_else(|| self.encoding.map_code(code))
|
||||
}
|
||||
|
||||
pub(crate) fn render_glyph(
|
||||
&self,
|
||||
glyph: &Type3Glyph<'a>,
|
||||
@@ -144,9 +275,8 @@ impl<'a> Type3<'a> {
|
||||
|
||||
let name = self.glyph_simulator.glyph_to_string(glyph.glyph_id)?;
|
||||
let program = self.char_procs.get(&name)?;
|
||||
// PdfCraft patch: see `context::MAX_PAGE_CONTENT`.
|
||||
let decoded = crate::context::decode_content(program)?;
|
||||
let iter = TypedIter::new(decoded.as_ref());
|
||||
let (decoded, _held) = self.decoded_proc(&name, program, &glyph.cache.type3_procs)?;
|
||||
let iter = TypedIter::new(&decoded);
|
||||
|
||||
let is_shape_glyph = {
|
||||
let mut iter = iter.clone();
|
||||
|
||||
+13
@@ -166,6 +166,10 @@ pub enum InterpreterWarning {
|
||||
/// read, or a Separation or DeviceN tint transform that cannot be evaluated) and was painted
|
||||
/// in a fallback colour. Reported at most once per content stream.
|
||||
ColorFallback,
|
||||
/// PdfCraft patch (90): a Type 3 font without `/Widths` (or `/FirstChar`/`/LastChar`),
|
||||
/// which ISO 32000-2 Table 110 requires, was selected; its glyphs advance by the widths
|
||||
/// their glyph procedures set with `d0`/`d1`. Reported each time such a font is selected.
|
||||
Type3WidthsMissing,
|
||||
}
|
||||
|
||||
/// PdfCraft patch (73): ends a page's JBIG2 allowance with the page, so that what decodes on its
|
||||
@@ -200,7 +204,15 @@ pub fn interpret_page<'a>(
|
||||
// PdfCraft patch (29): bracket the annotations so devices can tell their paints from
|
||||
// the document's own (the reading-colours setting recolours the document, not comments).
|
||||
device.begin_annotations();
|
||||
// PdfCraft patch (86): form fields' widgets are drawn after the page's other
|
||||
// annotations, each group in `/Annots` order, as other readers draw them: a filled
|
||||
// comment listed after a check box covered the box. ISO 32000-2 doesn't order the two.
|
||||
// Two passes over the array, so nothing is held for every entry of a long one.
|
||||
for widgets in [false, true] {
|
||||
for annot in annot_arr.iter::<Dict<'_>>() {
|
||||
if (annot.get::<hayro_syntax::object::Name<'_>>(SUBTYPE).as_deref() == Some(b"Widget")) != widgets {
|
||||
continue;
|
||||
}
|
||||
let flags = annot.get::<u32>(F).unwrap_or(0);
|
||||
|
||||
// Annotation should be hidden (Hidden = bit 2, NoView = bit 6).
|
||||
@@ -316,6 +328,7 @@ pub fn interpret_page<'a>(
|
||||
draw_highlight_without_appearance(&annot, context, device);
|
||||
}
|
||||
}
|
||||
}
|
||||
device.end_annotations();
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -23,6 +23,10 @@ use hayro_syntax::xref::XRef;
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// PdfCraft patch (78): the source of [`TilingPattern::instance`].
|
||||
static NEXT_TILING_INSTANCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// A PDF pattern.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -156,6 +160,8 @@ impl CacheKey for ShadingPattern {
|
||||
#[derive(Clone)]
|
||||
pub struct TilingPattern<'a> {
|
||||
cache_key: u128,
|
||||
/// PdfCraft patch (78): see [`Self::instance`].
|
||||
instance: u64,
|
||||
ctx_bbox: Rect,
|
||||
/// The bbox of the tiling pattern.
|
||||
pub bbox: Rect,
|
||||
@@ -233,6 +239,7 @@ impl<'a> TilingPattern<'a> {
|
||||
|
||||
Some(Self {
|
||||
cache_key,
|
||||
instance: NEXT_TILING_INSTANCE.fetch_add(1, Ordering::Relaxed),
|
||||
bbox,
|
||||
x_step,
|
||||
y_step,
|
||||
@@ -310,6 +317,23 @@ impl<'a> TilingPattern<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl TilingPattern<'_> {
|
||||
/// PdfCraft patch (78): which setting of the pattern (`scn`/`SCN`) this is. Each setting is
|
||||
/// numbered apart, and its copies keep the number. A coloured pattern's cell depends only on
|
||||
/// what the setting captured (its stream, resources, settings and the bounding box then in
|
||||
/// effect) and on the scale it is drawn at, so a renderer can draw the cell once per number
|
||||
/// and scale for every fill of that setting. An uncoloured pattern's cell also takes the
|
||||
/// colour and opacity of the paint, which copies can change.
|
||||
pub fn instance(&self) -> u64 {
|
||||
self.instance
|
||||
}
|
||||
|
||||
/// Whether the pattern is coloured (`/PaintType 1`).
|
||||
pub fn is_coloured(&self) -> bool {
|
||||
self.is_color
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheKey for TilingPattern<'_> {
|
||||
fn cache_key(&self) -> u128 {
|
||||
self.cache_key
|
||||
|
||||
+155
-4
@@ -596,7 +596,8 @@ impl<'a> ImageXObject<'a> {
|
||||
self.decode_key(cache, true, target_dimension),
|
||||
MAX_DECODED_IMAGE_BYTES,
|
||||
decoded_mask_size,
|
||||
|| decode_mask(self, target_dimension),
|
||||
&self.warning_sink,
|
||||
|sink| decode_mask(&self.reporting_to(sink), target_dimension),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -616,7 +617,8 @@ impl<'a> ImageXObject<'a> {
|
||||
self.decode_key(cache, false, target_dimension),
|
||||
MAX_DECODED_IMAGE_BYTES,
|
||||
decoded_raster_size,
|
||||
|| decode_raster(self, target_dimension),
|
||||
&self.warning_sink,
|
||||
|sink| decode_raster(&self.reporting_to(sink), target_dimension),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -645,13 +647,73 @@ impl<'a> ImageXObject<'a> {
|
||||
interpolate: self.interpolate,
|
||||
// PdfCraft patch (38): the same image drawn in a default colour space or without one.
|
||||
default_space: self.default_space,
|
||||
// PdfCraft patch (32): see `decode_reads_target`.
|
||||
// PdfCraft patch (32): see `decode_reads_target`; patch (82): see `has_larger_mask`.
|
||||
target: if self.decode_reads_target() {
|
||||
target_dimension
|
||||
} else if self.has_larger_mask() {
|
||||
target_dimension.map(size_bucket)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
preview: cache.preview,
|
||||
shrunk_to: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): this soft mask (or `/Mask` image) for an image of `parent` samples
|
||||
/// drawn at `target_dimension` device pixels. A mask larger than its image and more than
|
||||
/// [`SOFT_MASK_SHRINK_FACTOR`] times larger than where it is drawn, on a side, is averaged down
|
||||
/// to that size, and the result is cached at that size: drawn
|
||||
/// through a form many times (a 2 × 2 image whose mask is 34,862 × 4,332, drawn 40 times on
|
||||
/// one slide, pdf.js `issue16263.pdf`), it was decoded, copied and drawn at full size each
|
||||
/// time, about 3.6 s apiece, and the page took minutes. Any other mask decodes as before; a mask
|
||||
/// no larger than its image keeps its size, so the renderer still samples it onto the image's
|
||||
/// grid.
|
||||
pub(crate) fn decoded_soft_mask(&self, target_dimension: Option<(u32, u32)>, parent: (u32, u32)) -> Option<DecodedMask> {
|
||||
let larger_than_image = self.width > parent.0 || self.height > parent.1;
|
||||
let shrink_to = target_dimension.filter(|&(tw, th)| {
|
||||
larger_than_image
|
||||
&& tw > 0
|
||||
&& th > 0
|
||||
&& (u64::from(self.width) > u64::from(tw) * SOFT_MASK_SHRINK_FACTOR
|
||||
|| u64::from(self.height) > u64::from(th) * SOFT_MASK_SHRINK_FACTOR)
|
||||
});
|
||||
// Decoded within the image's own decode, as before: the image's cache entry holds it as
|
||||
// its alpha, and patch (91) keeps that entry when the mask fails, so a second entry for
|
||||
// the mask would only count it twice.
|
||||
let Some((tw, th)) = shrink_to else {
|
||||
return decode_mask(self, target_dimension);
|
||||
};
|
||||
// Averaged down to the drawn size rounded up to a power of two (`size_bucket`), so draws a
|
||||
// pixel apart share one mask and the renderer filters the rest, and only on the sides that
|
||||
// are larger than that.
|
||||
let (bw, bh) = size_bucket((tw, th));
|
||||
let to = (bw.min(self.width).max(1), bh.min(self.height).max(1));
|
||||
// Decoded in full and averaged down to `to`. A truncated stream decodes fewer rows than
|
||||
// the dictionary says: never more target samples than decoded ones on a side.
|
||||
let from_full = |image: &Self, to: (u32, u32)| {
|
||||
let full = decode_mask(image, target_dimension)?;
|
||||
Some(DecodedMask { luma: shrink_luma_within(full.luma, to)? })
|
||||
};
|
||||
let Some(cache) = &self.decoded_cache else {
|
||||
return from_full(self, to);
|
||||
};
|
||||
let key = |to| self.decode_key(cache, true, target_dimension).map(|key| DecodedImageKey { shrunk_to: Some(to), ..key });
|
||||
// One image drawn at several sizes (rotated and scaled copies on one slide) needs the
|
||||
// mask at several sizes. Each comes from one intermediate averaged-down mask, cached at
|
||||
// most `SOFT_MASK_INTERMEDIATE` on a side, so the full mask is decoded once, not once per
|
||||
// size; a size larger than the intermediate comes from the full mask.
|
||||
let between = (self.width.min(SOFT_MASK_INTERMEDIATE), self.height.min(SOFT_MASK_INTERMEDIATE));
|
||||
if to == between || to.0 > between.0 || to.1 > between.1 {
|
||||
return cache.get_or_insert_with(key(to), MAX_DECODED_IMAGE_BYTES, decoded_mask_size, &self.warning_sink, |sink| {
|
||||
from_full(&self.reporting_to(sink), to)
|
||||
});
|
||||
}
|
||||
cache.get_or_insert_with(key(to), MAX_DECODED_IMAGE_BYTES, decoded_mask_size, &self.warning_sink, |sink| {
|
||||
let mid = cache.get_or_insert_with(key(between), MAX_DECODED_IMAGE_BYTES, decoded_mask_size, sink, |sink| {
|
||||
from_full(&self.reporting_to(sink), between)
|
||||
})?;
|
||||
Some(DecodedMask { luma: shrink_luma_within(mid.luma, to)? })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -667,6 +729,29 @@ impl<'a> ImageXObject<'a> {
|
||||
|| dict.get::<Stream<'_>>(MASK).is_some_and(|s| uses_jpx(&s))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): whether this image has a soft mask (or `/Mask` image) larger than
|
||||
/// itself, which may be averaged down to the size the image is drawn at
|
||||
/// (`decoded_soft_mask`): the decoded raster holds it as its alpha, so it depends on that size,
|
||||
/// to the [`size_bucket`] it is averaged down to.
|
||||
fn has_larger_mask(&self) -> bool {
|
||||
let dict = self.stream.dict();
|
||||
let larger = |s: &Stream<'_>| {
|
||||
let d = s.dict();
|
||||
// The dimension keys as `ImageXObject::new` reads them, abbreviations first.
|
||||
let (w, h) = (d.get::<u32>(W).or_else(|| d.get::<u32>(WIDTH)), d.get::<u32>(H).or_else(|| d.get::<u32>(HEIGHT)));
|
||||
w.is_some_and(|w| w > self.width) || h.is_some_and(|h| h > self.height)
|
||||
};
|
||||
dict.get::<Stream<'_>>(SMASK).is_some_and(|s| larger(&s)) || dict.get::<Stream<'_>>(MASK).is_some_and(|s| larger(&s))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (91): this image, reporting its warnings to `sink`.
|
||||
fn reporting_to(&self, sink: &WarningSinkFn) -> Self {
|
||||
Self {
|
||||
warning_sink: sink.clone(),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
@@ -690,6 +775,71 @@ impl<'a> ImageXObject<'a> {
|
||||
/// image pixels shared across tiles and scales. Past it the cache starts a fresh epoch.
|
||||
const MAX_DECODED_IMAGE_BYTES: usize = 64 << 20;
|
||||
|
||||
/// PdfCraft patch (82): how many times larger than where it is drawn, on a side, a soft mask must be
|
||||
/// before it is averaged down to that size (`ImageXObject::decoded_soft_mask`). Below it the mask
|
||||
/// keeps its own resolution, which the renderer filters as it draws.
|
||||
const SOFT_MASK_SHRINK_FACTOR: u64 = 2;
|
||||
|
||||
/// PdfCraft patch (82): the largest side of the intermediate mask that every smaller averaged-down
|
||||
/// size of one soft mask is made from (`ImageXObject::decoded_soft_mask`): 4096 × 4096 one-byte
|
||||
/// samples are 16 MiB, a quarter of the decoded-image budget, so it stays cached between draws.
|
||||
const SOFT_MASK_INTERMEDIATE: u32 = 4096;
|
||||
|
||||
/// PdfCraft patch (82): the size a large soft mask is averaged down to for an image drawn at
|
||||
/// `(w, h)` device pixels: each side rounded up to a power of two. Draws of one image a pixel or so
|
||||
/// apart (scaled by 0.36 and 0.36167, say) share one averaged mask and one cache entry, and a mask
|
||||
/// is never averaged below where it is drawn, so the renderer shrinks the rest at most twofold.
|
||||
fn size_bucket((w, h): (u32, u32)) -> (u32, u32) {
|
||||
(w.checked_next_power_of_two().unwrap_or(u32::MAX), h.checked_next_power_of_two().unwrap_or(u32::MAX))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): `luma` averaged down to `to` (no larger than it on either side): each
|
||||
/// target sample is the mean of the source samples whose centres fall in it. Source sample `x`
|
||||
/// of `sw`, centred at `x + ½`, falls in target sample `⌊(2x + 1)·tw / 2sw⌋`. The only buffers are
|
||||
/// the target's sums and counts, reserved fallibly; nothing is sized by the source's width.
|
||||
fn shrink_luma(luma: LumaData, to: (u32, u32)) -> Option<LumaData> {
|
||||
let (sw, sh) = (luma.width as usize, luma.height as usize);
|
||||
let (tw, th) = (to.0 as usize, to.1 as usize);
|
||||
if sw == 0 || sh == 0 || tw == 0 || th == 0 || tw > sw || th > sh || luma.data.len() != sw.checked_mul(sh)? {
|
||||
return None;
|
||||
}
|
||||
let cells = tw.checked_mul(th)?;
|
||||
let (mut sums, mut counts): (Vec<u64>, Vec<u32>) = (Vec::new(), Vec::new());
|
||||
sums.try_reserve_exact(cells).ok()?;
|
||||
counts.try_reserve_exact(cells).ok()?;
|
||||
sums.resize(cells, 0);
|
||||
counts.resize(cells, 0);
|
||||
// The target sample a source sample's centre falls in, along an axis of `src` samples
|
||||
// shrunk to `dst`. `2·i + 1 < 2·src` and `dst ≤ src ≤ 2^32`, so the product fits a u64.
|
||||
let centre = |i: usize, src: usize, dst: usize| -> usize {
|
||||
let at = (2 * i as u64 + 1) * dst as u64 / (2 * src as u64);
|
||||
(at as usize).min(dst - 1)
|
||||
};
|
||||
for (y, row) in luma.data.chunks_exact(sw).enumerate() {
|
||||
let base = centre(y, sh, th) * tw;
|
||||
for (x, &v) in row.iter().enumerate() {
|
||||
let i = base + centre(x, sw, tw);
|
||||
if let (Some(sum), Some(count)) = (sums.get_mut(i), counts.get_mut(i)) {
|
||||
*sum += u64::from(v);
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let data = sums.iter().zip(&counts).map(|(&sum, &count)| if count == 0 { 0 } else { (sum / u64::from(count)) as u8 }).collect();
|
||||
Some(LumaData { data, width: to.0, height: to.1, ..luma })
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): `luma` averaged down to `to`, but never to more samples than it has on a
|
||||
/// side (a truncated stream decodes fewer rows than its dictionary says), and returned as it is
|
||||
/// when that is its own size.
|
||||
fn shrink_luma_within(luma: LumaData, to: (u32, u32)) -> Option<LumaData> {
|
||||
let to = (to.0.min(luma.width).max(1), to.1.min(luma.height).max(1));
|
||||
if to == (luma.width, luma.height) {
|
||||
return Some(luma);
|
||||
}
|
||||
shrink_luma(luma, to)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: pixel bytes held by a decoded mask.
|
||||
fn decoded_mask_size(mask: &DecodedMask) -> usize {
|
||||
mask.luma.width as usize * mask.luma.height as usize
|
||||
@@ -1145,7 +1295,8 @@ fn resolve_alpha(
|
||||
{
|
||||
let obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None, None)?;
|
||||
|
||||
decode_mask(&obj, target_dimension).map(|decoded| decoded.luma)
|
||||
// PdfCraft patch (82): averaged down when far larger than where it is drawn, and cached.
|
||||
obj.decoded_soft_mask(target_dimension, (width, *height)).map(|decoded| decoded.luma)
|
||||
} else if let Some(color_key_mask) = dict.get::<SmallVec<[u16; 4]>>(MASK) {
|
||||
let mut mask_data = vec![];
|
||||
|
||||
|
||||
+3
-1
@@ -28,7 +28,9 @@ pub fn set_page_jbig2_work(work: u64) {
|
||||
}
|
||||
|
||||
/// Pays for reading `bytes` of a stream and its globals out of what the page has left, or `None`.
|
||||
/// An image that doesn't decode isn't kept, so a page drawing it again and again reads it again.
|
||||
/// An image that doesn't decode isn't always kept (not past the page's allowance, nor drawn under a
|
||||
/// transfer function: hayro-interpret patch 91), so a page drawing it again and again can read it
|
||||
/// again.
|
||||
#[cfg(feature = "std")]
|
||||
fn charge_page_reading(bytes: usize) -> Option<()> {
|
||||
let mut left = PAGE_WORK_LEFT.with(core::cell::Cell::get);
|
||||
|
||||
Vendored
+3
@@ -65,6 +65,9 @@ pub use renderer::{image_resampling_size, tiling_cell_pixels, tiling_cell_scale,
|
||||
/// PdfCraft patch: exported for its regression test.
|
||||
#[doc(hidden)]
|
||||
pub use renderer::set_skip_offscreen_paths;
|
||||
/// PdfCraft patch (30): exported for its regression tests.
|
||||
#[doc(hidden)]
|
||||
pub use renderer::{OffscreenDraws, offscreen_draws};
|
||||
|
||||
/// A cache used by the renderer.
|
||||
///
|
||||
|
||||
Vendored
+229
-119
@@ -637,6 +637,47 @@ pub fn set_skip_offscreen_paths(skip: bool) {
|
||||
SKIP_OFFSCREEN_PATHS.with(|s| s.set(skip));
|
||||
}
|
||||
|
||||
/// PdfCraft patch (30): what the renders on this thread have drawn offscreen, for tests that check
|
||||
/// a page's cost by counting rather than by timing it (see [`offscreen_draws`]).
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct OffscreenDraws {
|
||||
/// Tiling pattern cells drawn; a cell taken from `tiling_cells` isn't drawn again.
|
||||
pub tiling_cells: u64,
|
||||
/// The pixels of every pixmap drawn offscreen: tiling cells, masks, groups and the like.
|
||||
pub pixels: u64,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// PdfCraft patch (30): see [`OffscreenDraws`].
|
||||
static OFFSCREEN_DRAWS: std::cell::Cell<OffscreenDraws> =
|
||||
const { std::cell::Cell::new(OffscreenDraws { tiling_cells: 0, pixels: 0 }) };
|
||||
}
|
||||
|
||||
/// PdfCraft patch (30): exported for its regression tests (see [`OffscreenDraws`]). The counts
|
||||
/// only grow; a test takes the difference across a render.
|
||||
#[doc(hidden)]
|
||||
pub fn offscreen_draws() -> OffscreenDraws {
|
||||
OFFSCREEN_DRAWS
|
||||
.try_with(std::cell::Cell::get)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// PdfCraft patch (30): a `width` × `height` pixmap to draw offscreen, counted in
|
||||
/// [`OffscreenDraws`]. Every pixmap the renderer renders into, besides the page, is made here.
|
||||
fn offscreen_pixmap(width: u16, height: u16, tiling_cell: bool) -> Pixmap {
|
||||
// Gone only while the thread exits, when there is nothing left to count for.
|
||||
let _ = OFFSCREEN_DRAWS.try_with(|draws| {
|
||||
let mut counts = draws.get();
|
||||
counts.pixels = counts
|
||||
.pixels
|
||||
.saturating_add(u64::from(width) * u64::from(height));
|
||||
counts.tiling_cells = counts.tiling_cells.saturating_add(u64::from(tiling_cell));
|
||||
draws.set(counts);
|
||||
});
|
||||
Pixmap::new(width, height)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: whether a device-space box can paint a pixel of a `width` × `height`
|
||||
/// canvas. A box whose bounds miss the canvas is skipped without changing a pixel.
|
||||
/// Bounds that aren't finite are drawn as before.
|
||||
@@ -802,6 +843,9 @@ pub(crate) struct Renderer {
|
||||
/// render, every nested renderer starting empty) and so to one `preview` setting: a mask
|
||||
/// drawn for a preview, which may leave out tiny images, never reaches a final render.
|
||||
pub(crate) soft_mask_cache: HashMap<u128, Mask>,
|
||||
/// PdfCraft patch (30): coloured tiling cells drawn during this render (see [`TilingCells`]).
|
||||
/// Like `soft_mask_cache` it belongs to one renderer, so to one `preview` setting.
|
||||
tiling_cells: TilingCells,
|
||||
pub(crate) outline_cache: Rc<std::cell::RefCell<HashMap<u128, Rc<BezPath>>>>,
|
||||
pub(crate) cur_mask: Option<Mask>,
|
||||
pub(crate) in_type3_glyph: bool,
|
||||
@@ -815,6 +859,40 @@ pub(crate) struct Renderer {
|
||||
ink: InkState,
|
||||
}
|
||||
|
||||
/// PdfCraft patch (30): the cell pixmaps of coloured tiling patterns, keyed by the setting of the
|
||||
/// pattern (`TilingPattern::instance`) and the cell's size and scale. A page that fills many
|
||||
/// shapes with one pattern (pdf.js issue 8078 paints 73,047 stencil masks with a pattern whose
|
||||
/// cell is 3.5 MB) drew and kept a cell per fill, which ran out of memory; the fills now share
|
||||
/// one. A handful of entries covers a run of fills with one pattern, and bounds what the cache
|
||||
/// keeps besides what the scene holds anyway.
|
||||
#[derive(Default)]
|
||||
struct TilingCells(Vec<(TilingCellKey, Arc<Pixmap>)>);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
struct TilingCellKey {
|
||||
instance: u64,
|
||||
/// Whether this is the bordered cell of a pattern whose step shows one cell (patch (20)).
|
||||
one_cell: bool,
|
||||
is_stroke: bool,
|
||||
pixels: (u16, u16),
|
||||
scale: (u64, u64),
|
||||
}
|
||||
|
||||
impl TilingCells {
|
||||
const MAX_ENTRIES: usize = 8;
|
||||
|
||||
fn get(&self, key: &TilingCellKey) -> Option<Arc<Pixmap>> {
|
||||
self.0.iter().find(|(k, _)| k == key).map(|(_, cell)| cell.clone())
|
||||
}
|
||||
|
||||
fn insert(&mut self, key: TilingCellKey, cell: Arc<Pixmap>) {
|
||||
if self.0.len() >= Self::MAX_ENTRIES {
|
||||
self.0.remove(0);
|
||||
}
|
||||
self.0.push((key, cell));
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (27): which inks a pass of a `DeviceCMYK` group draws, and whether the
|
||||
/// group asked for something a pass can't draw.
|
||||
///
|
||||
@@ -913,6 +991,7 @@ impl Renderer {
|
||||
erase_groups: 0,
|
||||
inside_pattern: false,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
tiling_cells: TilingCells::default(),
|
||||
outline_cache: cache.outline_cache.clone(),
|
||||
cur_mask: None,
|
||||
in_type3_glyph: false,
|
||||
@@ -1019,6 +1098,7 @@ impl Renderer {
|
||||
erase_groups: 0,
|
||||
inside_pattern: false,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
tiling_cells: TilingCells::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
cur_mask: None,
|
||||
in_type3_glyph: false,
|
||||
@@ -1027,7 +1107,7 @@ impl Renderer {
|
||||
image_transparency_stack: Vec::new(),
|
||||
ink: InkState::default(),
|
||||
};
|
||||
let mut mask_pix = Pixmap::new(self.ctx.width(), self.ctx.height());
|
||||
let mut mask_pix = offscreen_pixmap(self.ctx.width(), self.ctx.height(), false);
|
||||
let rgb_data = ImageData::Rgb(RgbData {
|
||||
data: dummy_rgb,
|
||||
width: alpha_data.width,
|
||||
@@ -1540,6 +1620,52 @@ impl Renderer {
|
||||
self.draw_pixmap(Arc::new(pixmap), quality, draw_transform);
|
||||
}
|
||||
|
||||
/// PdfCraft patch (30): the cell pixmap of tiling pattern `t`, `size` pixels drawn at
|
||||
/// `initial_transform`; a cell with a `key` is drawn once and then taken from `tiling_cells`.
|
||||
fn tiling_cell(
|
||||
&mut self,
|
||||
t: &hayro_interpret::pattern::TilingPattern<'_>,
|
||||
key: Option<TilingCellKey>,
|
||||
size: (u16, u16),
|
||||
initial_transform: Affine,
|
||||
is_stroke: bool,
|
||||
) -> Arc<Pixmap> {
|
||||
if let Some(cell) = key.and_then(|key| self.tiling_cells.get(&key)) {
|
||||
return cell;
|
||||
}
|
||||
let mut renderer = Self {
|
||||
ctx: RenderContext::new_with(size.0, size.1, derive_settings(self.ctx.render_settings())),
|
||||
// PdfCraft patch: a pattern cell is painted as one artwork, so its content is never
|
||||
// remapped by the reading-colours setting.
|
||||
color_replacement: None,
|
||||
in_annotations: false,
|
||||
knockout: Vec::new(),
|
||||
erasing: false,
|
||||
knockout_passes: Vec::new(),
|
||||
erase_groups: 0,
|
||||
cur_mask: None,
|
||||
inside_pattern: true,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
tiling_cells: TilingCells::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
in_type3_glyph: false,
|
||||
scaler: self.scaler,
|
||||
preview: self.preview,
|
||||
image_transparency_stack: Vec::new(),
|
||||
ink: InkState::default(),
|
||||
};
|
||||
t.interpret(&mut renderer, initial_transform, is_stroke);
|
||||
let mut pix = offscreen_pixmap(size.0, size.1, true);
|
||||
renderer.ctx.flush();
|
||||
let mut resources = vello_cpu::Resources::default();
|
||||
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
|
||||
let pix = Arc::new(pix);
|
||||
if let Some(key) = key {
|
||||
self.tiling_cells.insert(key, pix.clone());
|
||||
}
|
||||
pix
|
||||
}
|
||||
|
||||
fn push_clip_path_inner(&mut self, clip_path: &BezPath, fill: FillRule) {
|
||||
let old_transform = *self.ctx.transform();
|
||||
|
||||
@@ -1585,6 +1711,7 @@ impl Renderer {
|
||||
in_annotations: self.in_annotations,
|
||||
inside_pattern: self.inside_pattern,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
tiling_cells: TilingCells::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
cur_mask: None,
|
||||
in_type3_glyph: false,
|
||||
@@ -1609,7 +1736,7 @@ impl Renderer {
|
||||
if renderer.ink.unsupported {
|
||||
return None;
|
||||
}
|
||||
let mut pix = Pixmap::new(width, height);
|
||||
let mut pix = offscreen_pixmap(width, height, false);
|
||||
renderer.ctx.flush();
|
||||
let mut resources = vello_cpu::Resources::default();
|
||||
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
|
||||
@@ -1741,7 +1868,9 @@ impl Renderer {
|
||||
AlphaColor::from_rgba8(c[0], c[1], c[2], c[3]).into()
|
||||
}
|
||||
Paint::Pattern(p) => {
|
||||
let path_transform = self.ctx.transform();
|
||||
// PdfCraft patch (30): a copy, so a tiling cell can be drawn and kept on `self`.
|
||||
let current_transform = *self.ctx.transform();
|
||||
let path_transform = ¤t_transform;
|
||||
|
||||
match *p {
|
||||
Pattern::Shading(s) => {
|
||||
@@ -1848,39 +1977,20 @@ impl Renderer {
|
||||
const BORDER: f64 = 1.0;
|
||||
let pix_width = ((bbox.width() as f32 * xs).ceil() as u16).saturating_add(2);
|
||||
let pix_height = ((bbox.height() as f32 * ys).ceil() as u16).saturating_add(2);
|
||||
let mut renderer = Self {
|
||||
ctx: RenderContext::new_with(
|
||||
pix_width,
|
||||
pix_height,
|
||||
derive_settings(self.ctx.render_settings()),
|
||||
),
|
||||
color_replacement: None,
|
||||
in_annotations: false,
|
||||
knockout: Vec::new(),
|
||||
erasing: false,
|
||||
knockout_passes: Vec::new(),
|
||||
erase_groups: 0,
|
||||
cur_mask: None,
|
||||
inside_pattern: true,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
in_type3_glyph: false,
|
||||
scaler: self.scaler,
|
||||
preview: self.preview,
|
||||
image_transparency_stack: Vec::new(),
|
||||
ink: InkState::default(),
|
||||
};
|
||||
let initial_transform = Affine::translate((BORDER, BORDER))
|
||||
* Affine::scale_non_uniform(xs as f64, ys as f64)
|
||||
* Affine::translate((-bbox.x0, -bbox.y0));
|
||||
t.interpret(&mut renderer, initial_transform, is_stroke);
|
||||
let mut pix = Pixmap::new(pix_width, pix_height);
|
||||
renderer.ctx.flush();
|
||||
let mut resources = vello_cpu::Resources::default();
|
||||
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
|
||||
let key = t.is_coloured().then(|| TilingCellKey {
|
||||
instance: t.instance(),
|
||||
one_cell: true,
|
||||
is_stroke,
|
||||
pixels: (pix_width, pix_height),
|
||||
scale: (u64::from(xs.to_bits()), u64::from(ys.to_bits())),
|
||||
});
|
||||
let pix = self.tiling_cell(&t, key, (pix_width, pix_height), initial_transform, is_stroke);
|
||||
paint_transform = path_transform.inverse() * t.matrix * initial_transform.inverse();
|
||||
let image = Image {
|
||||
image: ImageSource::Pixmap(Arc::new(pix)),
|
||||
image: ImageSource::Pixmap(pix),
|
||||
sampler: ImageSampler {
|
||||
x_extend: peniko::Extend::Pad,
|
||||
y_extend: peniko::Extend::Pad,
|
||||
@@ -1910,37 +2020,17 @@ impl Renderer {
|
||||
let scaled_width = bbox.width() * xs;
|
||||
let scaled_height = bbox.height() * ys;
|
||||
|
||||
let mut renderer = Self {
|
||||
ctx: RenderContext::new_with(
|
||||
pix_width,
|
||||
pix_height,
|
||||
derive_settings(self.ctx.render_settings()),
|
||||
),
|
||||
// PdfCraft patch: a pattern cell is painted as one artwork, so its
|
||||
// content is never remapped by the reading-colours setting.
|
||||
color_replacement: None,
|
||||
in_annotations: false,
|
||||
knockout: Vec::new(),
|
||||
erasing: false,
|
||||
knockout_passes: Vec::new(),
|
||||
erase_groups: 0,
|
||||
cur_mask: None,
|
||||
inside_pattern: true,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
in_type3_glyph: false,
|
||||
scaler: self.scaler,
|
||||
preview: self.preview,
|
||||
image_transparency_stack: Vec::new(),
|
||||
ink: InkState::default(),
|
||||
};
|
||||
let mut initial_transform = Affine::scale_non_uniform(xs, ys)
|
||||
* Affine::translate((-bbox.x0, -bbox.y0));
|
||||
t.interpret(&mut renderer, initial_transform, is_stroke);
|
||||
let mut pix = Pixmap::new(pix_width, pix_height);
|
||||
renderer.ctx.flush();
|
||||
let mut resources = vello_cpu::Resources::default();
|
||||
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
|
||||
// PdfCraft patch (30): a coloured cell is drawn once per setting and scale.
|
||||
let key = t.is_coloured().then(|| TilingCellKey {
|
||||
instance: t.instance(),
|
||||
one_cell: false,
|
||||
is_stroke,
|
||||
pixels: (pix_width, pix_height),
|
||||
scale: (xs.to_bits(), ys.to_bits()),
|
||||
});
|
||||
let pix = self.tiling_cell(&t, key, (pix_width, pix_height), initial_transform, is_stroke);
|
||||
|
||||
// TODO: Fix these
|
||||
if t.x_step < 0.0 {
|
||||
@@ -1957,7 +2047,7 @@ impl Renderer {
|
||||
path_transform.inverse() * t.matrix * initial_transform.inverse();
|
||||
|
||||
let image = Image {
|
||||
image: ImageSource::Pixmap(Arc::new(pix)),
|
||||
image: ImageSource::Pixmap(pix),
|
||||
sampler: ImageSampler {
|
||||
x_extend: peniko::Extend::Repeat,
|
||||
y_extend: peniko::Extend::Repeat,
|
||||
@@ -2195,69 +2285,57 @@ impl<'a> Device<'a> for Renderer {
|
||||
self.ctx.set_fill_rule(old_rule);
|
||||
}
|
||||
Paint::Pattern(_) => {
|
||||
let (width, height) = (self.ctx.width(), self.ctx.height());
|
||||
let stencil_rect = Rect::new(
|
||||
0.0,
|
||||
0.0,
|
||||
stencil.width as f64,
|
||||
stencil.height as f64,
|
||||
);
|
||||
let mask_pix = {
|
||||
let rgb_bytes = ImageData::Rgb(RgbData {
|
||||
data: vec![
|
||||
255;
|
||||
stencil.width as usize
|
||||
* stencil.height as usize
|
||||
* 3
|
||||
],
|
||||
width: stencil.width,
|
||||
height: stencil.height,
|
||||
interpolate: stencil.interpolate,
|
||||
scale_factors: stencil.scale_factors,
|
||||
});
|
||||
let mut sub_renderer = Self {
|
||||
ctx: RenderContext::new_with(
|
||||
width,
|
||||
height,
|
||||
derive_settings(self.ctx.render_settings()),
|
||||
),
|
||||
// PdfCraft patch: a stencil mask must keep its original
|
||||
// coverage values, so its content is never remapped by
|
||||
// the reading-colours setting.
|
||||
color_replacement: None,
|
||||
in_annotations: false,
|
||||
knockout: Vec::new(),
|
||||
erasing: false,
|
||||
knockout_passes: Vec::new(),
|
||||
erase_groups: 0,
|
||||
inside_pattern: false,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
cur_mask: None,
|
||||
in_type3_glyph: false,
|
||||
scaler: self.scaler,
|
||||
preview: self.preview,
|
||||
image_transparency_stack: Vec::new(),
|
||||
ink: InkState::default(),
|
||||
};
|
||||
let mut sub_pix = Pixmap::new(width, height);
|
||||
sub_renderer.ctx.set_transform(transform);
|
||||
sub_renderer.draw_image(rgb_bytes, Some(stencil));
|
||||
sub_renderer.ctx.flush();
|
||||
let mut resources = vello_cpu::Resources::default();
|
||||
sub_renderer
|
||||
.ctx
|
||||
.render_to_pixmap(&mut resources, &mut sub_pix);
|
||||
sub_pix
|
||||
let Some(len) = source_byte_len(
|
||||
stencil.width,
|
||||
stencil.height,
|
||||
3,
|
||||
MAX_IMAGE_PIXELS,
|
||||
) else {
|
||||
log::warn!(
|
||||
"Stencil mask dimensions exceeded the buffer limit; image skipped"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
self.ctx.push_layer(
|
||||
None,
|
||||
Some(self.ctx.blend_mode()),
|
||||
None,
|
||||
Some(Mask::new_luminance(&mask_pix)),
|
||||
None,
|
||||
);
|
||||
let Some(mut white) = image_buffer(len) else {
|
||||
log::warn!(
|
||||
"Stencil mask buffer could not be allocated; image skipped"
|
||||
);
|
||||
return;
|
||||
};
|
||||
white.resize(len, 255);
|
||||
// PdfCraft patch (30): under a clip of the stencil's device
|
||||
// rectangle, the pattern is painted into a layer of its own and
|
||||
// cut to the stencil by a layer holding the stencil, composited
|
||||
// with `Compose::DestIn`; a layer is only as large as the clip,
|
||||
// so a draw costs what its rectangle covers. A luminance mask
|
||||
// (which vello_cpu only takes at canvas size) cost a
|
||||
// canvas-sized render, mask and composite per draw: a page of
|
||||
// 73,047 patterned 16 × 16 stencils took minutes. The clip is
|
||||
// padded past the rectangle so that a stencil snapped to whole
|
||||
// device pixels lies inside it; the cut clears the padding.
|
||||
let device_rect = (transform * stencil_rect.to_path(0.1))
|
||||
.bounding_box()
|
||||
.inflate(2.0, 2.0);
|
||||
if ![device_rect.x0, device_rect.y0, device_rect.x1, device_rect.y1]
|
||||
.iter()
|
||||
.all(|v| v.is_finite())
|
||||
{
|
||||
log::warn!("Stencil mask has an invalid transform; image skipped");
|
||||
return;
|
||||
}
|
||||
let layer_clip = device_rect.to_path(0.1);
|
||||
let blend_mode = self.ctx.blend_mode();
|
||||
self.ctx.set_transform(Affine::IDENTITY);
|
||||
self.ctx.push_clip_layer(&layer_clip);
|
||||
// Unclipped, so isolated: a layer with only a clip draws
|
||||
// straight into what is under it, which the cut would clear.
|
||||
self.ctx.push_layer(None, Some(blend_mode), None, None, None);
|
||||
self.ctx.set_transform(transform);
|
||||
|
||||
// PdfCraft patch: a stencil mask is painted with the fill
|
||||
@@ -2275,6 +2353,37 @@ impl<'a> Device<'a> for Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
// The stencil, as white ink under its own coverage, keeps the
|
||||
// pattern where it paints and clears the rest of the clip.
|
||||
let paint_blend = self.ctx.blend_mode();
|
||||
self.ctx.set_blend_mode(peniko::BlendMode::default());
|
||||
// The pattern left its own paint transform, which would place
|
||||
// the stencil's pixels.
|
||||
self.ctx.set_paint_transform(Affine::IDENTITY);
|
||||
self.ctx.push_blend_layer(peniko::BlendMode::new(
|
||||
Mix::Normal,
|
||||
Compose::DestIn,
|
||||
));
|
||||
self.ctx.set_transform(transform);
|
||||
// The coverage is drawn as a stencil outside a Type 3 glyph is:
|
||||
// a glyph's image rules (nearest shrink, padded frame) are for
|
||||
// the glyph's own images.
|
||||
let in_type3_glyph = std::mem::replace(&mut self.in_type3_glyph, false);
|
||||
self.draw_image(
|
||||
ImageData::Rgb(RgbData {
|
||||
data: white,
|
||||
width: stencil.width,
|
||||
height: stencil.height,
|
||||
interpolate: stencil.interpolate,
|
||||
scale_factors: stencil.scale_factors,
|
||||
}),
|
||||
Some(stencil),
|
||||
);
|
||||
self.in_type3_glyph = in_type3_glyph;
|
||||
self.ctx.pop_layer();
|
||||
self.ctx.set_blend_mode(paint_blend);
|
||||
|
||||
self.ctx.pop_layer();
|
||||
self.ctx.pop_layer();
|
||||
}
|
||||
};
|
||||
@@ -2366,7 +2475,7 @@ impl<'a> Device<'a> for Renderer {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut result = Pixmap::new(width, height);
|
||||
let mut result = offscreen_pixmap(width, height, false);
|
||||
for ((px, rgb), coverage) in result.data_mut().iter_mut().zip(rgb.chunks_exact(3)).zip(cmy.data()) {
|
||||
let a = u32::from(coverage.a);
|
||||
let premul = |v: u8| ((u32::from(v) * a + 127) / 255) as u8;
|
||||
@@ -2750,6 +2859,7 @@ fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, hei
|
||||
inside_pattern: false,
|
||||
cur_mask: None,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
tiling_cells: TilingCells::default(),
|
||||
outline_cache: Rc::new(std::cell::RefCell::new(HashMap::new())),
|
||||
in_type3_glyph: false,
|
||||
scaler: Scaler::new(ResamplingFunction::CatmullRom),
|
||||
@@ -2777,7 +2887,7 @@ fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, hei
|
||||
renderer.ctx.pop_layer();
|
||||
}
|
||||
|
||||
let mut pix = Pixmap::new(width, height);
|
||||
let mut pix = offscreen_pixmap(width, height, false);
|
||||
renderer.ctx.flush();
|
||||
let mut resources = vello_cpu::Resources::default();
|
||||
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
|
||||
|
||||
Reference in new issue
Block a user