Merge pull request #1871 from storytold/fix/pdf-nested-containers
Importers: PDF nesting inside compressed streams no longer crashes (hayro 0.8)
This commit is contained in:
commit
cd1b590aa8
|
|
@ -1304,6 +1304,12 @@ version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
|
checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fearless_simd"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3296eb97d869113eaf148a6bdf01884e90b3828ad7ae2340b0f93362fc158611"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
|
|
@ -1760,9 +1766,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hayro-ccitt"
|
name = "hayro-ccitt"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a"
|
checksum = "2866f0d976e76497480b853d5e258dab6e4b89ecfe991124e08f15dfa8fd03ad"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hayro-cmap"
|
name = "hayro-cmap"
|
||||||
|
|
@ -1776,9 +1782,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hayro-interpret"
|
name = "hayro-interpret"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d"
|
checksum = "ac9df074ff163a63864cec9b0bf640ccbff8abf4e44481df7369ce0ac172ffbe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"hayro-cmap",
|
"hayro-cmap",
|
||||||
|
|
@ -1788,28 +1794,28 @@ dependencies = [
|
||||||
"phf",
|
"phf",
|
||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
"siphasher",
|
"siphasher",
|
||||||
"skrifa 0.42.1",
|
"skrifa 0.46.2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"yoke",
|
"yoke",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hayro-jbig2"
|
name = "hayro-jbig2"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69374b3668dd45aeb3d3145cda68f2c7b4f223aaa2511e67d076f1c7d741388d"
|
checksum = "000e025850f4592ddcaee85640db70704151f2bafd43b6f431aa5b6a2c6a35a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fearless_simd",
|
"fearless_simd 1.1.0",
|
||||||
"hayro-ccitt",
|
"hayro-ccitt",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hayro-jpeg2000"
|
name = "hayro-jpeg2000"
|
||||||
version = "0.3.5"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1"
|
checksum = "122c5f834c37a2260b9c851bb8be685cf539b2a8b94b704a931ef7d2525d5835"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fearless_simd",
|
"fearless_simd 1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1820,9 +1826,9 @@ checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hayro-syntax"
|
name = "hayro-syntax"
|
||||||
version = "0.7.2"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27"
|
checksum = "6cdc818d8d265dcb42081d488f7599df4f73fb79e9d347ecedcd3c23809d2026"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flate2",
|
"flate2",
|
||||||
"hayro-ccitt",
|
"hayro-ccitt",
|
||||||
|
|
@ -3833,6 +3839,16 @@ dependencies = [
|
||||||
"read-fonts 0.41.0",
|
"read-fonts 0.41.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "skrifa"
|
||||||
|
version = "0.46.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4febebda507fb889c545a37a135517769d1391941013561292897961d1887d6"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"read-fonts 0.43.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "skrifa"
|
name = "skrifa"
|
||||||
version = "0.47.0"
|
version = "0.47.0"
|
||||||
|
|
@ -4986,7 +5002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2"
|
checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"fearless_simd",
|
"fearless_simd 0.4.1",
|
||||||
"guillotiere",
|
"guillotiere",
|
||||||
"log",
|
"log",
|
||||||
"peniko",
|
"peniko",
|
||||||
|
|
@ -5001,7 +5017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd"
|
checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"fearless_simd",
|
"fearless_simd 0.4.1",
|
||||||
"guillotiere",
|
"guillotiere",
|
||||||
"log",
|
"log",
|
||||||
"peniko",
|
"peniko",
|
||||||
|
|
|
||||||
|
|
@ -538,17 +538,29 @@ proptest! {
|
||||||
// ---------- PDF ----------
|
// ---------- PDF ----------
|
||||||
|
|
||||||
fn handmade_pdf(content: &str, resources: &str, media: &str) -> Vec<u8> {
|
fn handmade_pdf(content: &str, resources: &str, media: &str) -> Vec<u8> {
|
||||||
let objs = [
|
handmade_pdf_with(content, resources, media, &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same, followed by the objects `more` (numbered from 5).
|
||||||
|
fn handmade_pdf_with(content: &str, resources: &str, media: &str, more: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let page = [
|
||||||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
||||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox {media} /Contents 4 0 R /Resources {resources} >>"),
|
format!("<< /Type /Page /Parent 2 0 R /MediaBox {media} /Contents 4 0 R /Resources {resources} >>"),
|
||||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len() + 1),
|
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len() + 1),
|
||||||
];
|
];
|
||||||
|
pdf_of(&page.into_iter().map(String::into_bytes).chain(more.iter().cloned()).collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PDF of the objects `objs` (numbered from 1, the catalog first).
|
||||||
|
fn pdf_of(objs: &[Vec<u8>]) -> Vec<u8> {
|
||||||
let mut out = b"%PDF-1.7\n".to_vec();
|
let mut out = b"%PDF-1.7\n".to_vec();
|
||||||
let mut offsets = vec![];
|
let mut offsets = vec![];
|
||||||
for (i, o) in objs.iter().enumerate() {
|
for (i, o) in objs.iter().enumerate() {
|
||||||
offsets.push(out.len());
|
offsets.push(out.len());
|
||||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||||
|
out.extend_from_slice(o);
|
||||||
|
out.extend_from_slice(b"\nendobj\n");
|
||||||
}
|
}
|
||||||
let xref = out.len();
|
let xref = out.len();
|
||||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||||
|
|
@ -2157,10 +2169,15 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||||
|
|
||||||
/// The testkit's sample editing data, compressed as older `.ai` files compress it.
|
/// The testkit's sample editing data, compressed as older `.ai` files compress it.
|
||||||
fn ai_sample_compressed() -> Vec<u8> {
|
fn ai_sample_compressed() -> Vec<u8> {
|
||||||
|
[&b"%AI12_CompressedData"[..], &deflated(&vectorcraft_testkit::ai::sample_data())].concat()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `data` compressed (zlib).
|
||||||
|
fn deflated(data: &[u8]) -> Vec<u8> {
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||||
e.write_all(&vectorcraft_testkit::ai::sample_data()).unwrap();
|
e.write_all(data).unwrap();
|
||||||
[&b"%AI12_CompressedData"[..], &e.finish().unwrap()].concat()
|
e.finish().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
|
|
@ -2533,6 +2550,281 @@ fn deep_pdf(content: &str, resources: &str) -> Vec<u8> {
|
||||||
handmade_pdf(content, resources, "[0 0 100 100]")
|
handmade_pdf(content, resources, "[0 0 100 100]")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A stream object with the entries `dict`, its `data` compressed.
|
||||||
|
fn packed(dict: &str, data: &str) -> Vec<u8> {
|
||||||
|
let z = deflated(data.as_bytes());
|
||||||
|
// (A length the test gives stands.)
|
||||||
|
let length = if dict.contains("/Length") { String::new() } else { format!(" /Length {}", z.len()) };
|
||||||
|
[format!("<< {dict} /Filter /FlateDecode{length} >>\nstream\n").as_bytes(), &z, b"\nendstream"].concat()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page whose content is `content`, with `resources` and the objects `more` (numbered from 5).
|
||||||
|
fn pdf_with(content: &str, resources: &str, more: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
handmade_pdf_with(content, resources, "[0 0 100 100]", more)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arrays in arrays, and dictionaries in dictionaries, far deeper than any reader goes: as the
|
||||||
|
/// operands of content.
|
||||||
|
fn nested_operands() -> [String; 2] {
|
||||||
|
let (n, d) = (100_000, 50_000);
|
||||||
|
[format!("BT {}{} TJ ET 0 0 5 5 re f", "[".repeat(n), "]".repeat(n)), format!("/P {}{} DP 0 0 5 5 re f", "<< /A ".repeat(d), ">> ".repeat(d))]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page drawing through the compressed stream that `container` makes of each of
|
||||||
|
/// [`nested_operands`] (object 5), one file each.
|
||||||
|
fn nested_in(content: &str, resources: &str, container: impl Fn(&str) -> Vec<Vec<u8>>) -> Vec<Vec<u8>> {
|
||||||
|
nested_operands().iter().map(|deep| pdf_with(content, resources, &container(deep))).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORM: &str = "/Type /XObject /Subtype /Form /BBox [0 0 100 100]";
|
||||||
|
|
||||||
|
/// A Type 3 font (object 5) whose glyph `a` is object 6.
|
||||||
|
fn type3_font(resources: &str) -> Vec<u8> {
|
||||||
|
format!(
|
||||||
|
"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 10 10] /FontMatrix [0.1 0 0 0.1 0 0] /CharProcs << /a 6 0 R >> \
|
||||||
|
/Encoding << /Type /Encoding /Differences [97 /a] >> /FirstChar 97 /LastChar 97 /Widths [10] /Resources {resources} >>"
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page whose box is object 6, kept with object 7 (a number) in a compressed object stream
|
||||||
|
/// (object 5, with the entries `stream`): a file with a cross-reference stream, as it must be.
|
||||||
|
fn pdf_with_object_stream(boxed: &str, stream: &str) -> Vec<u8> {
|
||||||
|
let content = "0 0 5 5 re f";
|
||||||
|
let inside = format!("{boxed} 77");
|
||||||
|
let index = format!("6 0 7 {} ", boxed.len() + 1);
|
||||||
|
let objs = [
|
||||||
|
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 6 0 R /Contents 4 0 R /Resources << >> >>".to_vec(),
|
||||||
|
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len() + 1).into_bytes(),
|
||||||
|
packed(&format!("/Type /ObjStm /N 2 /First {} {stream}", index.len()), &format!("{index}{inside}")),
|
||||||
|
];
|
||||||
|
let mut out = b"%PDF-1.7\n".to_vec();
|
||||||
|
// One entry an object: its kind, then where it is (an offset, or the stream it is in and its
|
||||||
|
// place there).
|
||||||
|
let mut entries = vec![[0u8, 0, 0, 0, 0, 255, 255]];
|
||||||
|
let at = |kind: u8, a: usize, b: u16| {
|
||||||
|
let (a, b) = ((a as u32).to_be_bytes(), b.to_be_bytes());
|
||||||
|
[kind, a[0], a[1], a[2], a[3], b[0], b[1]]
|
||||||
|
};
|
||||||
|
for (i, o) in objs.iter().enumerate() {
|
||||||
|
entries.push(at(1, out.len(), 0));
|
||||||
|
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||||
|
out.extend_from_slice(o);
|
||||||
|
out.extend_from_slice(b"\nendobj\n");
|
||||||
|
}
|
||||||
|
entries.extend([at(2, 5, 0), at(2, 5, 1), at(1, out.len(), 0)]);
|
||||||
|
let xref = out.len();
|
||||||
|
out.extend_from_slice(format!("8 0 obj\n<< /Type /XRef /Size 9 /Root 1 0 R /W [1 4 2] /Length {} >>\nstream\n", entries.len() * 7).as_bytes());
|
||||||
|
out.extend_from_slice(&entries.concat());
|
||||||
|
out.extend_from_slice(format!("\nendstream\nendobj\nstartxref\n{xref}\n%%EOF\n").as_bytes());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `n` objects from object 5 on, each made by `link` of the number of the next, then `last`.
|
||||||
|
fn chain(n: usize, link: impl Fn(usize) -> String, last: &str) -> Vec<Vec<u8>> {
|
||||||
|
(0..n).map(|i| link(i + 6).into_bytes()).chain([last.as_bytes().to_vec()]).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page painting with the shading whose function is object 5.
|
||||||
|
fn pdf_with_function(more: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let shading = "/Shading << /S << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function 5 0 R >> >>";
|
||||||
|
pdf_with("/S sh", &format!("<< {shading} >>"), more)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page filling in the colour space that is object 5.
|
||||||
|
fn pdf_with_color_space(more: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
pdf_with("/C cs 0 sc 0 0 5 5 re f /C CS 0 SC 0 0 5 5 re S", "<< /ColorSpace << /C 5 0 R >> >>", more)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every hostile PDF here opens as an error or as a document within the cap, on a small stack.
|
||||||
|
fn open_each(name: &str, files: impl IntoIterator<Item = Vec<u8>> + Send + 'static) {
|
||||||
|
let name = name.to_string();
|
||||||
|
on_a_small_stack(move || {
|
||||||
|
for (i, bytes) in files.into_iter().enumerate() {
|
||||||
|
open_deep(&format!("{name}-{i}.pdf"), &bytes);
|
||||||
|
// As `.ai` (the PDF part of one) and placed, too.
|
||||||
|
open_deep(&format!("{name}-{i}.ai"), &bytes);
|
||||||
|
let mut s = vectorcraft_engine::Session::new();
|
||||||
|
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
|
||||||
|
let _ = no_panic(s.execute("file.place", &json!({"name": "deep.pdf", "dataBase64": vectorcraft_format::base64_encode(&bytes)})));
|
||||||
|
assert!(!s.doc().unwrap().doc.too_deep(None));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nesting where only the reader of the decoded data sees it: inside each kind of compressed
|
||||||
|
// stream a page draws through.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_nesting_inside_a_compressed_form() {
|
||||||
|
open_each("form", nested_in("/F Do", "<< /XObject << /F 5 0 R >> >>", |deep| vec![packed(FORM, deep)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_nesting_inside_a_compressed_pattern() {
|
||||||
|
let tile = "/PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >>";
|
||||||
|
open_each("pattern", nested_in("/Pattern cs /P scn 0 0 50 50 re f", "<< /Pattern << /P 5 0 R >> >>", |deep| vec![packed(tile, deep)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_nesting_inside_a_compressed_glyph() {
|
||||||
|
let glyph = |deep: &str| vec![type3_font("<< >>"), packed("", &format!("10 0 d0 {deep}"))];
|
||||||
|
open_each("glyph", nested_in("BT /F1 12 Tf 10 10 Td (aa) Tj ET", "<< /Font << /F1 5 0 R >> >>", glyph));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_nesting_inside_a_compressed_soft_mask() {
|
||||||
|
let group = format!("{FORM} /Group << /S /Transparency /CS /DeviceGray >>");
|
||||||
|
let state = "<< /ExtGState << /G << /Type /ExtGState /SMask << /S /Luminosity /G 5 0 R >> >> >> >>";
|
||||||
|
open_each("mask", nested_in("/G gs 0 0 50 50 re f", state, move |deep| vec![packed(&group, deep)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_nesting_inside_a_compressed_annotation() {
|
||||||
|
let annots = "<< >> /Annots [<< /Type /Annot /Subtype /Square /Rect [0 0 50 50] /AP << /N 5 0 R >> >>]";
|
||||||
|
open_each("annotation", nested_in("0 0 5 5 re f", annots, |deep| vec![packed(FORM, deep)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_nesting_inside_compressed_page_content() {
|
||||||
|
let files = nested_operands().map(|deep| {
|
||||||
|
let mut objs: Vec<Vec<u8>> = ["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>"].map(Vec::from).into();
|
||||||
|
objs.push(b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents [4 0 R 4 0 R] /Resources << >> >>".to_vec());
|
||||||
|
objs.push(packed("", &deep));
|
||||||
|
pdf_of(&objs)
|
||||||
|
});
|
||||||
|
open_each("content", files);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_nesting_inside_an_object_stream() {
|
||||||
|
let (n, d) = (100_000, 50_000);
|
||||||
|
open_each(
|
||||||
|
"objects",
|
||||||
|
[
|
||||||
|
// A plain one first: the file is read as it should be.
|
||||||
|
pdf_with_object_stream("[0 0 100 100]", ""),
|
||||||
|
pdf_with_object_stream(&format!("{}0 0 100 100{}", "[".repeat(n), "]".repeat(n)), ""),
|
||||||
|
pdf_with_object_stream(&format!("{}{}", "<< /A ".repeat(d), ">> ".repeat(d)), ""),
|
||||||
|
// Its length is an object kept in the stream itself.
|
||||||
|
pdf_with_object_stream("[0 0 100 100]", "/Length 7 0 R"),
|
||||||
|
// It extends itself.
|
||||||
|
pdf_with_object_stream("[0 0 100 100]", "/Extends 5 0 R"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let plain = vectorcraft_engine::cmd::fileio::load("plain.pdf", &pdf_with_object_stream("[0 0 120 80]", "")).expect("an object stream is read");
|
||||||
|
assert_eq!(plain.doc.artboards[0].rect.width(), 120.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What refers to itself, or to the next of its kind without end.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_page_trees_that_hold_themselves() {
|
||||||
|
let page = |parent: usize| format!("<< /Type /Page /Parent {parent} 0 R /MediaBox [0 0 100 100] /Contents 4 0 R /Resources << >> >>");
|
||||||
|
let content = "<< /Length 13 >>\nstream\n0 0 5 5 re f\nendstream";
|
||||||
|
let own = [
|
||||||
|
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||||
|
"<< /Type /Pages /Kids [2 0 R 3 0 R 2 0 R] /Count 1 >>".to_string(),
|
||||||
|
page(2),
|
||||||
|
content.to_string(),
|
||||||
|
];
|
||||||
|
// Thousands of nodes, each holding the next; the page is the last.
|
||||||
|
let n = 30_000;
|
||||||
|
let mut deep = vec!["<< /Type /Catalog /Pages 5 0 R >>".to_string(), "null".to_string(), "null".to_string(), content.to_string()];
|
||||||
|
deep.extend((0..n).map(|i| format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", i + 6)));
|
||||||
|
deep.push(page(n + 4));
|
||||||
|
// Two nodes holding each other.
|
||||||
|
let pair = [
|
||||||
|
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||||
|
"<< /Type /Pages /Kids [5 0 R] /Count 1 >>".to_string(),
|
||||||
|
page(5),
|
||||||
|
content.to_string(),
|
||||||
|
"<< /Type /Pages /Kids [2 0 R 3 0 R] /Count 1 >>".to_string(),
|
||||||
|
];
|
||||||
|
let bytes = |objs: &[String]| pdf_of(&objs.iter().map(|o| o.clone().into_bytes()).collect::<Vec<_>>());
|
||||||
|
open_each("pages", [bytes(&own), bytes(&deep), bytes(&pair)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_functions_that_hold_themselves() {
|
||||||
|
let stitch = |next: usize| format!("<< /FunctionType 3 /Domain [0 1] /Functions [{next} 0 R] /Bounds [] /Encode [0 1] >>");
|
||||||
|
let ramp = "<< /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >>";
|
||||||
|
let calculator = "/FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1]";
|
||||||
|
let n = 100_000;
|
||||||
|
open_each(
|
||||||
|
"functions",
|
||||||
|
[
|
||||||
|
pdf_with_function(&[stitch(5).into_bytes()]),
|
||||||
|
pdf_with_function(&[stitch(6).into_bytes(), stitch(5).into_bytes()]),
|
||||||
|
pdf_with_function(&chain(20_000, stitch, ramp)),
|
||||||
|
// A calculator function of procedures in procedures.
|
||||||
|
pdf_with_function(&[packed(calculator, &format!("{}{}", "{".repeat(n), "}".repeat(n)))]),
|
||||||
|
pdf_with_function(&[packed(calculator, &format!("{{ {} 0 0 0 {} }}", "true {".repeat(n), "} if ".repeat(n)))]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_color_spaces_that_hold_themselves() {
|
||||||
|
let indexed = |next: usize| format!("[/Indexed {next} 0 R 1 <000000ffffff>]");
|
||||||
|
let tint = "<< /FunctionType 2 /Domain [0 1] /C0 [1 1 1] /C1 [0 0 0] /N 1 >>";
|
||||||
|
let profile = |alternate: &str| format!("<< /N 3 /Alternate {alternate} /Length 4 >>\nstream\njunk\nendstream").into_bytes();
|
||||||
|
open_each(
|
||||||
|
"spaces",
|
||||||
|
[
|
||||||
|
pdf_with_color_space(&[indexed(5).into_bytes()]),
|
||||||
|
pdf_with_color_space(&[indexed(6).into_bytes(), indexed(5).into_bytes()]),
|
||||||
|
pdf_with_color_space(&chain(20_000, indexed, "/DeviceRGB")),
|
||||||
|
pdf_with_color_space(&[b"[/Separation /Ink 5 0 R 6 0 R]".to_vec(), tint.as_bytes().to_vec()]),
|
||||||
|
pdf_with_color_space(&[b"[/DeviceN [/Ink] 5 0 R 6 0 R]".to_vec(), tint.as_bytes().to_vec()]),
|
||||||
|
pdf_with_color_space(&[b"[/Pattern 5 0 R]".to_vec()]),
|
||||||
|
pdf_with_color_space(&[b"[/ICCBased 6 0 R]".to_vec(), profile("5 0 R")]),
|
||||||
|
pdf_with_color_space(&[b"[/ICCBased 6 0 R]".to_vec(), profile("[/ICCBased 6 0 R]")]),
|
||||||
|
// As the space of a shading and of an image.
|
||||||
|
pdf_with(
|
||||||
|
"/S sh",
|
||||||
|
"<< /Shading << /S << /ShadingType 2 /ColorSpace 5 0 R /Coords [0 0 9 0] /Function 6 0 R >> >> >>",
|
||||||
|
&[indexed(5).into_bytes(), tint.as_bytes().to_vec()],
|
||||||
|
),
|
||||||
|
pdf_with(
|
||||||
|
"q 50 0 0 50 0 0 cm /I Do Q",
|
||||||
|
"<< /XObject << /I 6 0 R >> >>",
|
||||||
|
&[
|
||||||
|
indexed(5).into_bytes(),
|
||||||
|
b"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /BitsPerComponent 8 /ColorSpace 5 0 R /Length 1 >>\nstream\n0\nendstream"
|
||||||
|
.to_vec(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_art_that_draws_itself() {
|
||||||
|
let own = "<< /XObject << /F 5 0 R >> /Pattern << /P 5 0 R >> /Font << /F1 5 0 R >> /ExtGState << /G << /SMask << /S /Alpha /G 5 0 R >> >> >> >>";
|
||||||
|
let tile = format!("/PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources {own}");
|
||||||
|
let group = format!("{FORM} /Group << /S /Transparency >> /Resources {own}");
|
||||||
|
open_each(
|
||||||
|
"itself",
|
||||||
|
[
|
||||||
|
// A form drawing itself twice, a pattern painting its own tile, a glyph showing
|
||||||
|
// itself, and a mask masked by itself.
|
||||||
|
pdf_with("/F Do", own, &[packed(&format!("{FORM} /Resources {own}"), "0 0 5 5 re f /F Do 1 0 0 1 3 3 cm /F Do")]),
|
||||||
|
pdf_with("/Pattern cs /P scn 0 0 50 50 re f", own, &[packed(&tile, "/Pattern cs /P scn 0 0 5 5 re f")]),
|
||||||
|
pdf_with("BT /F1 12 Tf (a) Tj ET", own, &[type3_font(own), packed("", "10 0 d0 0 0 5 5 re f BT /F1 1 Tf (a) Tj ET")]),
|
||||||
|
pdf_with("/G gs 0 0 50 50 re f", own, &[packed(&group, "/G gs 0 0 5 5 re f")]),
|
||||||
|
// An image that is its own mask, and objects that are only a reference to each other.
|
||||||
|
pdf_with("q 50 0 0 50 0 0 cm /F Do Q", own, &[
|
||||||
|
b"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceGray /SMask 5 0 R /Length 1 >>\nstream\n0\nendstream".to_vec(),
|
||||||
|
]),
|
||||||
|
handmade_pdf_with("0 0 5 5 re f", "5 0 R", "6 0 R", &[b"6 0 R".to_vec(), b"5 0 R".to_vec()]),
|
||||||
|
handmade_pdf_with("0 0 5 5 re f", "<< >>", "[0 0 9 9]", &[b"<< /Length 5 0 R >>\nstream\nx\nendstream".to_vec()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// An EPS whose program is `body`.
|
/// An EPS whose program is `body`.
|
||||||
fn deep_eps(body: &str) -> Vec<u8> {
|
fn deep_eps(body: &str) -> Vec<u8> {
|
||||||
format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n{body}\nshowpage\n%%EOF\n").into_bytes()
|
format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n{body}\nshowpage\n%%EOF\n").into_bytes()
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ impl<W: Write> Write for Out<W> {
|
||||||
const OUT_CHUNK: usize = 64 << 10;
|
const OUT_CHUNK: usize = 64 << 10;
|
||||||
|
|
||||||
/// A gzip stream, written as [`flate2::write::GzEncoder`] at the default level writes it (the
|
/// A gzip stream, written as [`flate2::write::GzEncoder`] at the default level writes it (the
|
||||||
/// same header, and the same bytes for a file without compressed images), except that data which
|
/// same header and the same data; the deflate blocks are the same too where flate2 deflates with
|
||||||
|
/// miniz_oxide, not with zlib-rs, which writes them by how the data is fed), except that data which
|
||||||
/// is compressed already ([`Gz::packed`]) is stored as it is: the default level spends a second
|
/// is compressed already ([`Gz::packed`]) is stored as it is: the default level spends a second
|
||||||
/// or more per photo on it for next to nothing, and makes it slower to unpack. Where the level
|
/// or more per photo on it for next to nothing, and makes it slower to unpack. Where the level
|
||||||
/// changes the deflate blocks so far end with a sync flush and a fresh compressor writes on,
|
/// changes the deflate blocks so far end with a sync flush and a fresh compressor writes on,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Saving and opening as streams: the bytes a save writes are those an in-memory save wrote
|
//! Saving and opening as streams: the bytes a save writes are those an in-memory save wrote
|
||||||
//! before (the same JSON and data after it, unpacked; the same gzip bytes when no image is
|
//! before (the same JSON and data after it, unpacked; the same gzip header), files written the old
|
||||||
//! compressed already), files written the old way still open, entries are read whatever their
|
//! way still open, entries are read whatever their order, and the limits apply per part of the
|
||||||
//! order, and the limits apply per part of the file.
|
//! file.
|
||||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||||
|
|
||||||
|
|
@ -67,13 +67,17 @@ fn compressed_files_unpack_to_the_plain_file() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The deflate bytes themselves depend on flate2's backend: zlib-rs (which hayro turns on) writes
|
||||||
|
/// other blocks for data fed in other pieces than the in-memory save fed it, miniz_oxide didn't.
|
||||||
#[test]
|
#[test]
|
||||||
fn without_compressed_images_the_gzip_bytes_are_the_old_ones() {
|
fn without_compressed_images_the_gzip_stream_is_the_old_one() {
|
||||||
let mut d = doc_with(&[("scan", "image/tiff", INLINE_MAX * 2), ("icon", "image/png", 100)]);
|
let mut d = doc_with(&[("scan", "image/tiff", INLINE_MAX * 2), ("icon", "image/png", 100)]);
|
||||||
d.title = "No photos".into();
|
d.title = "No photos".into();
|
||||||
for o in [SaveOptions::default(), SaveOptions { pdf: Some(vec![b'%'; INLINE_MAX]), ..SaveOptions::default() }] {
|
for o in [SaveOptions::default(), SaveOptions { pdf: Some(vec![b'%'; INLINE_MAX]), ..SaveOptions::default() }] {
|
||||||
let plain = save_with(&d, &o).unwrap();
|
let plain = save_with(&d, &o).unwrap();
|
||||||
assert_eq!(save_with(&d, &SaveOptions { compress: true, ..o }).unwrap(), gzip(&plain));
|
let (packed, old) = (save_with(&d, &SaveOptions { compress: true, ..o }).unwrap(), gzip(&plain));
|
||||||
|
assert_eq!(packed.get(..10), old.get(..10), "the same header");
|
||||||
|
assert_eq!(gunzip(&packed), plain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ image = { workspace = true }
|
||||||
# CMYK JPEGs (the image crate writes RGB and grey ones only).
|
# CMYK JPEGs (the image crate writes RGB and grey ones only).
|
||||||
jpeg-encoder = { workspace = true }
|
jpeg-encoder = { workspace = true }
|
||||||
krilla = { workspace = true }
|
krilla = { workspace = true }
|
||||||
hayro-syntax = "0.7.2"
|
hayro-syntax = "0.8.0"
|
||||||
hayro-interpret = { version = "0.7.0", default-features = false, features = ["embed-fonts", "embed-cmaps"] }
|
hayro-interpret = { version = "0.8.0", default-features = false, features = ["embed-fonts", "embed-cmaps"] }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
smallvec = "1"
|
smallvec = "1"
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use hayro_interpret::font::Glyph;
|
use hayro_interpret::font::{Glyph, GlyphRun as ShownGlyphs};
|
||||||
use hayro_interpret::hayro_cmap::BfString;
|
use hayro_interpret::hayro_cmap::BfString;
|
||||||
use hayro_interpret::pattern::{Pattern, TilingPattern};
|
use hayro_interpret::pattern::{Pattern, TilingPattern};
|
||||||
use hayro_interpret::{
|
use hayro_interpret::{
|
||||||
CacheKey, ClipPath, Context, Device, GlyphDrawMode, Image, ImageData, InterpreterCache, InterpreterSettings, InterpreterWarning, LumaData,
|
CacheKey, ClipPath, Context, Device, DrawMode, DrawProps, Image, ImageData, ImageDrawProps, InterpreterCache, InterpreterSettings,
|
||||||
MaskType, PathDrawMode, SoftMask, StrokeProps, interpret_page,
|
InterpreterWarning, LumaData, MaskType, SoftMask, StrokeProps, interpret_page,
|
||||||
};
|
};
|
||||||
use hayro_syntax::object::Name;
|
use hayro_syntax::object::Name;
|
||||||
use kurbo::{Affine, BezPath, PathEl, Rect, Shape};
|
use kurbo::{Affine, BezPath, PathEl, Rect, Shape};
|
||||||
|
|
@ -147,9 +147,15 @@ pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it,
|
||||||
|
|
||||||
/// Import a PDF, returning the document plus warnings about content that was approximated or skipped.
|
/// Import a PDF, returning the document plus warnings about content that was approximated or skipped.
|
||||||
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
|
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
|
||||||
|
// The interpreter draws forms, patterns, glyphs and masks inside one another by recursion, in
|
||||||
|
// large stack frames: it caps that (50 deep), and gets room for the cap whatever thread asks.
|
||||||
|
vectorcraft_doc::depth::with_deep_stack(|| read(bytes, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`import_with_report`] on the thread that asks.
|
||||||
|
pub(crate) fn read(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
|
||||||
let original = crate::pages::open(bytes, opts.password.as_deref())?;
|
let original = crate::pages::open(bytes, opts.password.as_deref())?;
|
||||||
let picked = crate::pages::picked(opts, original.pages().len())?;
|
let picked = crate::pages::picked(opts, original.pages().len())?;
|
||||||
picked.iter().filter_map(|&n| original.pages().get(n)).try_for_each(crate::pages::content_too_deep)?;
|
|
||||||
let mut ocgs = Ocgs::read(&original);
|
let mut ocgs = Ocgs::read(&original);
|
||||||
let route = opts.layers && !ocgs.list.is_empty();
|
let route = opts.layers && !ocgs.list.is_empty();
|
||||||
// Content that is off imports as hidden layers: an update of the file turns every group on
|
// Content that is off imports as hidden layers: an update of the file turns every group on
|
||||||
|
|
@ -167,7 +173,6 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||||
let all_on = turned_on.is_some();
|
let all_on = turned_on.is_some();
|
||||||
let pdf = turned_on.unwrap_or(original);
|
let pdf = turned_on.unwrap_or(original);
|
||||||
let pages = pdf.pages();
|
let pages = pdf.pages();
|
||||||
picked.iter().filter_map(|&n| pages.get(n)).try_for_each(crate::pages::content_too_deep)?;
|
|
||||||
|
|
||||||
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
|
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
|
||||||
let sink2 = sink.clone();
|
let sink2 = sink.clone();
|
||||||
|
|
@ -176,6 +181,9 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||||
let msg = match w {
|
let msg = match w {
|
||||||
InterpreterWarning::UnsupportedFont => "a font could not be read; its text was skipped",
|
InterpreterWarning::UnsupportedFont => "a font could not be read; its text was skipped",
|
||||||
InterpreterWarning::ImageDecodeFailure => "an image could not be decoded and was skipped",
|
InterpreterWarning::ImageDecodeFailure => "an image could not be decoded and was skipped",
|
||||||
|
// Annotations aren't drawn (`render_annotations` below), and a kind of warning
|
||||||
|
// added later has no message yet.
|
||||||
|
_ => return,
|
||||||
};
|
};
|
||||||
if let Ok(mut v) = sink2.lock()
|
if let Ok(mut v) = sink2.lock()
|
||||||
&& !v.iter().any(|x| x == msg)
|
&& !v.iter().any(|x| x == msg)
|
||||||
|
|
@ -1262,8 +1270,8 @@ impl<'p> Builder<'p> {
|
||||||
/// before it on the open line was stroked so too, is held with the line instead of ending it
|
/// before it on the open line was stroked so too, is held with the line instead of ending it
|
||||||
/// (an editor writes type on a path glyph by glyph, each followed by the stroke added to the
|
/// (an editor writes type on a path glyph by glyph, each followed by the stroke added to the
|
||||||
/// type). `false`: it isn't one.
|
/// type). `false`: it isn't one.
|
||||||
fn hold_glyph_stroke(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'_>, draw_mode: &PathDrawMode) -> bool {
|
fn hold_glyph_stroke(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'_>, draw_mode: &DrawMode) -> bool {
|
||||||
let PathDrawMode::Stroke(props) = draw_mode else { return false };
|
let DrawMode::Stroke(props) = draw_mode else { return false };
|
||||||
if self.mask.is_some() || self.blend != BlendMode::Normal || matches!(paint, hayro_interpret::Paint::Pattern(_)) {
|
if self.mask.is_some() || self.blend != BlendMode::Normal || matches!(paint, hayro_interpret::Paint::Pattern(_)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1355,7 +1363,11 @@ impl<'p> Builder<'p> {
|
||||||
Some(art) => {
|
Some(art) => {
|
||||||
let [r, g, b, _] = m.background_color().to_rgba().components();
|
let [r, g, b, _] = m.background_color().to_rgba().components();
|
||||||
let backdrop = luminance(&Color::rgb(r, g, b));
|
let backdrop = luminance(&Color::rgb(r, g, b));
|
||||||
let inverted = m.transfer_function().is_some_and(|tf| tf.apply(0.0) > tf.apply(1.0));
|
let inverted = m.transfer_function().is_some_and(|tf| {
|
||||||
|
let mut ends = [0, 255];
|
||||||
|
tf.apply_to(&mut ends);
|
||||||
|
ends[0] > ends[1]
|
||||||
|
});
|
||||||
let id = self.id();
|
let id = self.id();
|
||||||
mask_spec(art, m.mask_type() == MaskType::Alpha, backdrop, inverted, page, |all| Node::group(id, all))
|
mask_spec(art, m.mask_type() == MaskType::Alpha, backdrop, inverted, page, |all| Node::group(id, all))
|
||||||
}
|
}
|
||||||
|
|
@ -1684,7 +1696,14 @@ fn resize_alpha(a: &LumaData, w: u32, h: u32) -> Vec<u8> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Device<'a> for Builder<'_> {
|
/// What the interpreter draws, as the device below gets it.
|
||||||
|
impl<'a> Builder<'_> {
|
||||||
|
/// The soft mask and blend mode of what is drawn next.
|
||||||
|
fn set_state(&mut self, mask: Option<SoftMask<'a>>, blend_mode: hayro_interpret::BlendMode) {
|
||||||
|
self.set_soft_mask(mask);
|
||||||
|
self.set_blend_mode(blend_mode);
|
||||||
|
}
|
||||||
|
|
||||||
fn set_soft_mask(&mut self, mask: Option<SoftMask<'a>>) {
|
fn set_soft_mask(&mut self, mask: Option<SoftMask<'a>>) {
|
||||||
let spec = mask.map(|m| self.soft_mask(&m));
|
let spec = mask.map(|m| self.soft_mask(&m));
|
||||||
let same = match (&spec, &self.mask) {
|
let same = match (&spec, &self.mask) {
|
||||||
|
|
@ -1707,7 +1726,7 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &PathDrawMode) {
|
fn path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &DrawMode) {
|
||||||
self.flush_glyphs();
|
self.flush_glyphs();
|
||||||
if self.hold_glyph_stroke(path, transform, paint, draw_mode) {
|
if self.hold_glyph_stroke(path, transform, paint, draw_mode) {
|
||||||
self.pending = false;
|
self.pending = false;
|
||||||
|
|
@ -1729,19 +1748,19 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
hayro_interpret::Paint::Color(_) => None,
|
hayro_interpret::Paint::Color(_) => None,
|
||||||
};
|
};
|
||||||
// Patch and triangle meshes are gradient meshes.
|
// Patch and triangle meshes are gradient meshes.
|
||||||
if let (PathDrawMode::Fill(_), Some(sp)) = (draw_mode, shading)
|
if let (DrawMode::Fill(_), Some(sp)) = (draw_mode, shading)
|
||||||
&& let Some(meshes) = mesh_shading(sp, &mut self.colors)
|
&& let Some(meshes) = mesh_shading(sp, &mut self.colors)
|
||||||
{
|
{
|
||||||
self.draw_meshes(meshes, &bp, sp.opacity);
|
self.draw_meshes(meshes, &bp, sp.opacity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let pd = PathData::from_bezpath(&bp);
|
let pd = PathData::from_bezpath(&bp);
|
||||||
let stroke = matches!(draw_mode, PathDrawMode::Stroke(_));
|
let stroke = matches!(draw_mode, DrawMode::Stroke(_));
|
||||||
let (paint, opacity) = self.paint(paint, stroke);
|
let (paint, opacity) = self.paint(paint, stroke);
|
||||||
// A gradient that doesn't extend past its ends paints only between them.
|
// A gradient that doesn't extend past its ends paints only between them.
|
||||||
let width = match draw_mode {
|
let width = match draw_mode {
|
||||||
PathDrawMode::Stroke(p) => p.line_width as f64 * mean_scale(transform),
|
DrawMode::Stroke(p) => p.line_width as f64 * mean_scale(transform),
|
||||||
PathDrawMode::Fill(_) => 0.0,
|
_ => 0.0,
|
||||||
};
|
};
|
||||||
let band = shading.and_then(|sp| extend_clip(sp, bp.bounding_box().inflate(width, width)));
|
let band = shading.and_then(|sp| extend_clip(sp, bp.bounding_box().inflate(width, width)));
|
||||||
let wrap = |b: &mut Self, n: Node| match &band {
|
let wrap = |b: &mut Self, n: Node| match &band {
|
||||||
|
|
@ -1753,7 +1772,7 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
None => n,
|
None => n,
|
||||||
};
|
};
|
||||||
match draw_mode {
|
match draw_mode {
|
||||||
PathDrawMode::Fill(rule) => {
|
DrawMode::Fill(rule) => {
|
||||||
let mut f = FillLayer::new(paint);
|
let mut f = FillLayer::new(paint);
|
||||||
(f.opacity, f.overprint) = (opacity, overprint);
|
(f.opacity, f.overprint) = (opacity, overprint);
|
||||||
let mut n = Node::path(self.id(), pd, Appearance { items: vec![AppearanceItem::Fill(f)], ..Default::default() });
|
let mut n = Node::path(self.id(), pd, Appearance { items: vec![AppearanceItem::Fill(f)], ..Default::default() });
|
||||||
|
|
@ -1764,7 +1783,7 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
let n = wrap(self, n);
|
let n = wrap(self, n);
|
||||||
self.push_node(n);
|
self.push_node(n);
|
||||||
}
|
}
|
||||||
PathDrawMode::Stroke(props) => {
|
DrawMode::Stroke(props) => {
|
||||||
let mut st = stroke_layer(paint, opacity, props, mean_scale(transform));
|
let mut st = stroke_layer(paint, opacity, props, mean_scale(transform));
|
||||||
st.overprint = overprint;
|
st.overprint = overprint;
|
||||||
// Fill-then-stroke of the same path (the `B` operator, or a fill and then a stroke
|
// Fill-then-stroke of the same path (the `B` operator, or a fill and then a stroke
|
||||||
|
|
@ -1799,6 +1818,71 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
let n = wrap(self, n);
|
let n = wrap(self, n);
|
||||||
self.push_node(n);
|
self.push_node(n);
|
||||||
}
|
}
|
||||||
|
// (`draw_path` gives a fill and a stroke one after the other, and no path is invisible.)
|
||||||
|
DrawMode::FillAndStroke(..) | DrawMode::Invisible => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One glyph: filled, or stroked with `stroke`.
|
||||||
|
fn glyph(
|
||||||
|
&mut self,
|
||||||
|
glyph: &Glyph<'a>,
|
||||||
|
transform: Affine,
|
||||||
|
glyph_transform: Affine,
|
||||||
|
paint: &hayro_interpret::Paint<'a>,
|
||||||
|
stroke: Option<&StrokeProps>,
|
||||||
|
) {
|
||||||
|
self.pending = false;
|
||||||
|
match glyph {
|
||||||
|
Glyph::Outline(o) => {
|
||||||
|
let scale = mean_scale(transform);
|
||||||
|
let overprint = self.glyph_overprint(stroke.is_some());
|
||||||
|
if self.type_glyph(o, transform * glyph_transform, scale, paint, stroke, overprint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.flush_text();
|
||||||
|
let mut bp = o.outline();
|
||||||
|
bp.apply_affine(transform * glyph_transform);
|
||||||
|
let (paint, opacity) = self.paint(paint, stroke.is_some());
|
||||||
|
let same = self.glyphs.as_ref().is_some_and(|r| {
|
||||||
|
r.paint == paint
|
||||||
|
&& r.opacity == opacity
|
||||||
|
&& r.overprint == overprint
|
||||||
|
&& r.stroke.as_ref().map(|s| s.line_width) == stroke.map(|s| s.line_width)
|
||||||
|
&& (r.scale - scale).abs() < 1e-9
|
||||||
|
});
|
||||||
|
if !same {
|
||||||
|
self.flush_glyphs();
|
||||||
|
self.glyphs = Some(GlyphRun { path: BezPath::new(), paint, opacity, stroke: stroke.cloned(), scale, overprint });
|
||||||
|
}
|
||||||
|
if let Some(r) = &mut self.glyphs {
|
||||||
|
r.path.extend(bp.iter());
|
||||||
|
}
|
||||||
|
self.warn("text was converted to outlines");
|
||||||
|
}
|
||||||
|
Glyph::Type3(t3) => {
|
||||||
|
if self.nested >= MAX_NESTED {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.flush();
|
||||||
|
self.nested += 1;
|
||||||
|
t3.interpret(self, transform, glyph_transform, paint);
|
||||||
|
self.nested -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Device<'a> for Builder<'_> {
|
||||||
|
fn draw_path(&mut self, path: &BezPath, props: DrawProps<'a>, draw_mode: &DrawMode) {
|
||||||
|
self.set_state(props.soft_mask, props.blend_mode);
|
||||||
|
match draw_mode {
|
||||||
|
DrawMode::Invisible => {}
|
||||||
|
DrawMode::FillAndStroke(rule, stroke) => {
|
||||||
|
self.path(path, props.transform, &props.paint, &DrawMode::Fill(*rule));
|
||||||
|
self.path(path, props.transform, &props.paint, &DrawMode::Stroke(stroke.clone()));
|
||||||
|
}
|
||||||
|
one => self.path(path, props.transform, &props.paint, one),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1834,60 +1918,22 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
self.blend = BlendMode::Normal;
|
self.blend = BlendMode::Normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_glyph(
|
fn draw_glyph_run(&mut self, glyph_run: &ShownGlyphs<'_, 'a>, props: DrawProps<'a>, draw_mode: &DrawMode) {
|
||||||
&mut self,
|
// (A run is filled or stroked: one filled and stroked is given twice.)
|
||||||
glyph: &Glyph<'a>,
|
|
||||||
transform: Affine,
|
|
||||||
glyph_transform: Affine,
|
|
||||||
paint: &hayro_interpret::Paint<'a>,
|
|
||||||
draw_mode: &GlyphDrawMode,
|
|
||||||
) {
|
|
||||||
self.pending = false;
|
|
||||||
let stroke = match draw_mode {
|
let stroke = match draw_mode {
|
||||||
GlyphDrawMode::Invisible => return,
|
DrawMode::Invisible => return,
|
||||||
GlyphDrawMode::Fill => None,
|
DrawMode::Fill(_) => None,
|
||||||
GlyphDrawMode::Stroke(p) => Some(p.clone()),
|
DrawMode::Stroke(p) | DrawMode::FillAndStroke(_, p) => Some(p),
|
||||||
};
|
};
|
||||||
match glyph {
|
for g in glyph_run.glyphs() {
|
||||||
Glyph::Outline(o) => {
|
self.set_state(props.soft_mask.clone(), props.blend_mode);
|
||||||
let scale = mean_scale(transform);
|
self.glyph(g, props.transform, g.transform(), &props.paint, stroke);
|
||||||
let overprint = self.glyph_overprint(stroke.is_some());
|
|
||||||
if self.type_glyph(o, transform * glyph_transform, scale, paint, stroke.as_ref(), overprint) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.flush_text();
|
|
||||||
let mut bp = o.outline();
|
|
||||||
bp.apply_affine(transform * glyph_transform);
|
|
||||||
let (paint, opacity) = self.paint(paint, stroke.is_some());
|
|
||||||
let same = self.glyphs.as_ref().is_some_and(|r| {
|
|
||||||
r.paint == paint
|
|
||||||
&& r.opacity == opacity
|
|
||||||
&& r.overprint == overprint
|
|
||||||
&& r.stroke.as_ref().map(|s| s.line_width) == stroke.as_ref().map(|s| s.line_width)
|
|
||||||
&& (r.scale - scale).abs() < 1e-9
|
|
||||||
});
|
|
||||||
if !same {
|
|
||||||
self.flush_glyphs();
|
|
||||||
self.glyphs = Some(GlyphRun { path: BezPath::new(), paint, opacity, stroke, scale, overprint });
|
|
||||||
}
|
|
||||||
if let Some(r) = &mut self.glyphs {
|
|
||||||
r.path.extend(bp.iter());
|
|
||||||
}
|
|
||||||
self.warn("text was converted to outlines");
|
|
||||||
}
|
|
||||||
Glyph::Type3(t3) => {
|
|
||||||
if self.nested >= MAX_NESTED {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.flush();
|
|
||||||
self.nested += 1;
|
|
||||||
t3.interpret(self, transform, glyph_transform, paint);
|
|
||||||
self.nested -= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
|
fn draw_image(&mut self, image: Image<'a, '_>, props: ImageDrawProps<'a>) {
|
||||||
|
self.set_state(props.soft_mask, props.blend_mode);
|
||||||
|
let transform = props.transform;
|
||||||
self.flush_glyphs();
|
self.flush_glyphs();
|
||||||
self.flush_text();
|
self.flush_text();
|
||||||
self.pending = false;
|
self.pending = false;
|
||||||
|
|
@ -1975,7 +2021,7 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pop_clip_path(&mut self) {
|
fn pop_clip(&mut self) {
|
||||||
self.flush();
|
self.flush();
|
||||||
self.pending = false;
|
self.pending = false;
|
||||||
self.pop_frame();
|
self.pop_frame();
|
||||||
|
|
|
||||||
|
|
@ -379,7 +379,7 @@ impl<'p> Colors<'p> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An object written in place (references are visited as objects of their own).
|
/// An object written in place (references are visited as objects of their own).
|
||||||
fn direct(m: MaybeRef<Object<'_>>) -> Option<Object<'_>> {
|
pub(crate) fn direct(m: MaybeRef<Object<'_>>) -> Option<Object<'_>> {
|
||||||
match m {
|
match m {
|
||||||
MaybeRef::NotRef(o) => Some(o),
|
MaybeRef::NotRef(o) => Some(o),
|
||||||
MaybeRef::Ref(_) => None,
|
MaybeRef::Ref(_) => None,
|
||||||
|
|
|
||||||
|
|
@ -485,7 +485,8 @@ impl Walker<'_, '_> {
|
||||||
{
|
{
|
||||||
self.out.groups.push((g.get::<bool>(b"I").unwrap_or(false), g.get::<bool>(b"K").unwrap_or(false)));
|
self.out.groups.push((g.get::<bool>(b"I").unwrap_or(false), g.get::<bool>(b"K").unwrap_or(false)));
|
||||||
}
|
}
|
||||||
let inner = Resources::from_parent(d.get::<Dict<'_>>(b"Resources").unwrap_or_default(), res.clone());
|
// (Its own resources, or without any those of what draws it: as the interpreter reads them.)
|
||||||
|
let inner = d.get::<Dict<'_>>(b"Resources").map(Resources::new).unwrap_or_else(|| res.clone());
|
||||||
self.save();
|
self.save();
|
||||||
self.walk(TypedIter::new(data.as_ref()), &inner, depth + 1);
|
self.walk(TypedIter::new(data.as_ref()), &inner, depth + 1);
|
||||||
self.restore();
|
self.restore();
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ pub(crate) fn shading_gradient(sp: &ShadingPattern, colors: &mut Colors<'_>) ->
|
||||||
let space = colors.shading_space(cs, at(0.0)?.len());
|
let space = colors.shading_space(cs, at(0.0)?.len());
|
||||||
// RGB to decide which samples are stops; the stops keep the shading's model.
|
// RGB to decide which samples are stops; the stops keep the shading's model.
|
||||||
let sample = |t: f32| -> Option<Color> {
|
let sample = |t: f32| -> Option<Color> {
|
||||||
let [r, g, b, _] = cs.to_rgba(&at(t)?, 1.0, false).components();
|
let [r, g, b, _] = cs.to_rgba(&at(t)?, 1.0).components();
|
||||||
Some(Color::rgb(round3(r), round3(g), round3(b)))
|
Some(Color::rgb(round3(r), round3(g), round3(b)))
|
||||||
};
|
};
|
||||||
// Sample densely, then drop samples that linear interpolation reproduces.
|
// Sample densely, then drop samples that linear interpolation reproduces.
|
||||||
|
|
@ -147,7 +147,7 @@ fn mesh_color(v: &[f32], function: Option<&ShadingFunction>, sp: &ShadingPattern
|
||||||
};
|
};
|
||||||
let space = colors.shading_space(&sp.shading.color_space, comps.len());
|
let space = colors.shading_space(&sp.shading.color_space, comps.len());
|
||||||
colors.native(space, &comps).map(|n| n.color).unwrap_or_else(|| {
|
colors.native(space, &comps).map(|n| n.color).unwrap_or_else(|| {
|
||||||
let [r, g, b, _] = sp.shading.color_space.to_rgba(&comps, 1.0, false).components();
|
let [r, g, b, _] = sp.shading.color_space.to_rgba(&comps, 1.0).components();
|
||||||
Color::rgb(round3(r), round3(g), round3(b))
|
Color::rgb(round3(r), round3(g), round3(b))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
//! The pages of a PDF to import: opening with a password, the pages picked, the box each page
|
//! The pages of a PDF to import: opening with a password, the pages picked, the box each page
|
||||||
//! is cropped to ([`CropTo`]) and [`info`].
|
//! is cropped to ([`CropTo`]) and [`info`].
|
||||||
|
|
||||||
use hayro_syntax::object::{Dict, Stream};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use hayro_syntax::object::{Array, Dict, MaybeRef, Name, Object, ObjectIdentifier, Stream, dict_or_stream};
|
||||||
use hayro_syntax::page::Page;
|
use hayro_syntax::page::Page;
|
||||||
|
use hayro_syntax::xref::XRef;
|
||||||
use hayro_syntax::{DecryptionError, LoadPdfError, Pdf};
|
use hayro_syntax::{DecryptionError, LoadPdfError, Pdf};
|
||||||
use kurbo::{Affine, Rect};
|
use kurbo::{Affine, Rect};
|
||||||
use vectorcraft_doc::setup::MAX_BLEED;
|
use vectorcraft_doc::setup::MAX_BLEED;
|
||||||
|
|
||||||
|
use crate::import_color::direct;
|
||||||
use crate::{Choice, CropTo, ImportOptions, PdfError};
|
use crate::{Choice, CropTo, ImportOptions, PdfError};
|
||||||
|
|
||||||
/// What `info` tells about one page.
|
/// What `info` tells about one page.
|
||||||
|
|
@ -33,76 +37,116 @@ pub fn is_postscript(bytes: &[u8]) -> bool {
|
||||||
bytes.starts_with(b"%!PS") || bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6])
|
bytes.starts_with(b"%!PS") || bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The deepest a file's arrays and dictionaries may nest. The PDF reader (hayro) reads them by
|
/// The most functions and colour spaces that may be built on one another (a function stitching
|
||||||
/// recursion, without a cap of its own: a file nested deeply enough overflows the stack, which is
|
/// functions together, an indexed space on a space with an alternate). The interpreter (hayro)
|
||||||
/// an abort, not an error. Far beyond what a PDF writer makes.
|
/// builds them by recursion, without a cap of its own: one built on itself, or thousands in a
|
||||||
const MAX_OBJECT_NESTING: usize = 256;
|
/// chain, overflows the stack, which is an abort, not an error. Files have two or three.
|
||||||
|
const MAX_LINKS: usize = 16;
|
||||||
|
|
||||||
fn too_deep() -> PdfError {
|
/// The deepest the procedures of a calculator function may nest: read by recursion, too.
|
||||||
PdfError::Parse(format!("its arrays and dictionaries are nested more than {MAX_OBJECT_NESTING} deep"))
|
const MAX_PROCEDURES: usize = 64;
|
||||||
|
|
||||||
|
/// What the function or colour space `o` is built on: the functions a stitching function joins;
|
||||||
|
/// the base, alternate and tint of a colour space; the alternate of a colour profile. Nothing for
|
||||||
|
/// any other object.
|
||||||
|
fn links<'a>(o: &Object<'a>, xref: &'a XRef) -> Vec<MaybeRef<Object<'a>>> {
|
||||||
|
const SPACES: [&[u8]; 6] = [b"Indexed", b"I", b"Separation", b"DeviceN", b"Pattern", b"ICCBased"];
|
||||||
|
if let Object::Array(a) = o {
|
||||||
|
let mut items = a.raw_iter();
|
||||||
|
let kind = match items.next() {
|
||||||
|
Some(MaybeRef::NotRef(Object::Name(n))) => Some(n),
|
||||||
|
Some(MaybeRef::Ref(r)) => xref.get::<Name<'a>>(r.into()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
return if kind.is_some_and(|n| SPACES.contains(&n.as_ref())) { items.collect() } else { vec![] };
|
||||||
|
}
|
||||||
|
let Some((d, _)) = dict_or_stream(o) else { return vec![] };
|
||||||
|
let functions = d.get::<Array<'a>>(b"Functions").map(|a| a.raw_iter().collect::<Vec<_>>()).unwrap_or_default();
|
||||||
|
functions.into_iter().chain(d.get_raw::<Object<'a>>(b"Alternate")).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Do the arrays and dictionaries in the text of `pdf` nest deeper than [`MAX_OBJECT_NESTING`]?
|
/// How many of its kind the function or colour space `o` is built on, one on the next (0 for any
|
||||||
/// Read as tokens (strings and comments hold none), an object at a time. The data of a stream
|
/// other object); `None` past `room`, or when it is built on itself. `known`: the objects looked
|
||||||
/// with a filter is skipped: it isn't PDF text until decoded, which this doesn't do, so nesting
|
/// at so far (`None` while one is being looked at, too). One `None` refuses the file, so that an
|
||||||
/// inside compressed streams is not seen here.
|
/// object is `None` only for the room there was where it was first met doesn't matter.
|
||||||
fn nests_too_deep(pdf: &[u8]) -> bool {
|
fn built_on<'a>(o: MaybeRef<Object<'a>>, xref: &'a XRef, known: &mut HashMap<ObjectIdentifier, Option<usize>>, room: usize) -> Option<usize> {
|
||||||
let after =
|
let (id, o) = match o {
|
||||||
|from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
|
MaybeRef::Ref(r) => {
|
||||||
let word = |at: usize, w: &[u8]| pdf.get(at..).is_some_and(|rest| rest.starts_with(w));
|
let id = ObjectIdentifier::from(r);
|
||||||
let (mut depth, mut filtered) = (0usize, false);
|
if let Some(found) = known.get(&id) {
|
||||||
let mut i = 0;
|
return *found;
|
||||||
while let Some(&c) = pdf.get(i) {
|
}
|
||||||
i += 1;
|
let Some(o) = xref.get::<Object<'a>>(id) else { return Some(0) };
|
||||||
|
known.insert(id, None);
|
||||||
|
(Some(id), o)
|
||||||
|
}
|
||||||
|
MaybeRef::NotRef(o) => (None, o),
|
||||||
|
};
|
||||||
|
let links = links(&o, xref);
|
||||||
|
let most = match room.checked_sub(1) {
|
||||||
|
_ if links.is_empty() => Some(0),
|
||||||
|
Some(room) => links.into_iter().try_fold(0, |most, on| built_on(on, xref, known, room).map(|n| most.max(n + 1))),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
// (What it is built on may be known already, and deeper than there is room for here.)
|
||||||
|
let most = most.filter(|n| *n <= room);
|
||||||
|
if let Some(id) = id {
|
||||||
|
known.insert(id, most);
|
||||||
|
}
|
||||||
|
most
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is `o` a calculator function whose procedures nest deeper than [`MAX_PROCEDURES`]?
|
||||||
|
fn procedures_too_deep(o: &Object<'_>) -> bool {
|
||||||
|
let Object::Stream(s) = o else { return false };
|
||||||
|
if s.dict().get::<u8>(b"FunctionType") != Some(4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Ok(program) = s.decoded() else { return false };
|
||||||
|
let mut depth = 0usize;
|
||||||
|
program.iter().any(|c| {
|
||||||
match c {
|
match c {
|
||||||
b'%' => i += pdf.get(i..).and_then(|rest| rest.iter().position(|c| matches!(c, b'\n' | b'\r'))).unwrap_or(pdf.len()),
|
b'{' => depth += 1,
|
||||||
b'(' => {
|
b'}' => depth = depth.saturating_sub(1),
|
||||||
// A string: parentheses nest in it, a backslash takes the next byte.
|
|
||||||
let mut open = 1usize;
|
|
||||||
while open > 0 {
|
|
||||||
match pdf.get(i) {
|
|
||||||
None => return false,
|
|
||||||
Some(b'\\') => i += 1,
|
|
||||||
Some(b'(') => open += 1,
|
|
||||||
Some(b')') => open -= 1,
|
|
||||||
Some(_) => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b'[' => depth += 1,
|
|
||||||
b']' => depth = depth.saturating_sub(1),
|
|
||||||
b'<' if pdf.get(i) == Some(&b'<') => (depth, i) = (depth + 1, i + 1),
|
|
||||||
b'>' if pdf.get(i) == Some(&b'>') => (depth, i) = (depth.saturating_sub(1), i + 1),
|
|
||||||
// A string in hexadecimal.
|
|
||||||
b'<' => i = after(i, b">").unwrap_or(pdf.len()),
|
|
||||||
b'/' if word(i - 1, b"/Filter") => filtered = true,
|
|
||||||
b'e' if word(i - 1, b"endobj") => (depth, filtered) = (0, false),
|
|
||||||
b'e' if word(i - 1, b"endstream") => i += 8,
|
|
||||||
b's' if word(i - 1, b"stream") => {
|
|
||||||
depth = 0;
|
|
||||||
if std::mem::take(&mut filtered) {
|
|
||||||
i = after(i, b"endstream").unwrap_or(pdf.len());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
if depth > MAX_OBJECT_NESTING {
|
depth > MAX_PROCEDURES
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does `pdf` hold functions or colour spaces built on one another past [`MAX_LINKS`] (or on
|
||||||
|
/// themselves), or a calculator function nested past [`MAX_PROCEDURES`]? Every object is looked
|
||||||
|
/// at, wherever it is used from: the interpreter builds them from resources, shadings, images,
|
||||||
|
/// patterns, masks and graphics states alike.
|
||||||
|
fn nests_without_end(pdf: &Pdf) -> bool {
|
||||||
|
let xref = pdf.xref();
|
||||||
|
let mut known = HashMap::new();
|
||||||
|
// The objects of the file, then what each holds directly (64 deep at most: the reader's cap).
|
||||||
|
let mut pending: Vec<Object<'_>> = pdf.objects().into_iter().collect();
|
||||||
|
while let Some(o) = pending.pop() {
|
||||||
|
if procedures_too_deep(&o) || built_on(MaybeRef::NotRef(o.clone()), xref, &mut known, MAX_LINKS).is_none() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
match &o {
|
||||||
|
Object::Array(a) => pending.extend(a.raw_iter().filter_map(direct)),
|
||||||
|
_ => pending.extend(dict_or_stream(&o).into_iter().flat_map(|(d, _)| d.entries()).filter_map(|(_, v)| direct(v))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions
|
/// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions
|
||||||
/// password opens a file too.
|
/// password opens a file too.
|
||||||
|
///
|
||||||
|
/// What a hostile file nests or loops is the reader's to refuse (hayro 0.8 reads arrays and
|
||||||
|
/// dictionaries 64 deep and no deeper, wherever they are: the file's text, an object stream, the
|
||||||
|
/// content of a page, form, pattern or glyph; and follows a page tree or a chain of object streams
|
||||||
|
/// once). What it reads beyond that is left out, as an object it can't read. What the interpreter
|
||||||
|
/// builds by recursion with no cap is refused here ([`nests_without_end`]).
|
||||||
pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError> {
|
pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError> {
|
||||||
if is_postscript(bytes) {
|
if is_postscript(bytes) {
|
||||||
return Err(PdfError::PostScript);
|
return Err(PdfError::PostScript);
|
||||||
}
|
}
|
||||||
if nests_too_deep(bytes) {
|
|
||||||
return Err(too_deep());
|
|
||||||
}
|
|
||||||
let password = password.unwrap_or_default();
|
let password = password.unwrap_or_default();
|
||||||
let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw);
|
let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw);
|
||||||
read(password)
|
read(password)
|
||||||
|
|
@ -119,12 +163,12 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError
|
||||||
LoadPdfError::Decryption(DecryptionError::PasswordProtected) => PdfError::WrongPassword,
|
LoadPdfError::Decryption(DecryptionError::PasswordProtected) => PdfError::WrongPassword,
|
||||||
e => PdfError::Parse(format!("{e:?}")),
|
e => PdfError::Parse(format!("{e:?}")),
|
||||||
})
|
})
|
||||||
}
|
// Twice: looking at every object makes the reader repair a damaged table of objects (once
|
||||||
|
// only), and the objects are other ones after that.
|
||||||
/// The error of a page whose content, decoded, nests arrays and dictionaries too deep (see
|
.and_then(|pdf| match (0..2).any(|_| nests_without_end(&pdf)) {
|
||||||
/// [`MAX_OBJECT_NESTING`]): its operands are read the same way as the file's objects.
|
true => Err(PdfError::Parse("its functions or colour spaces are built on one another without end".into())),
|
||||||
pub(crate) fn content_too_deep(page: &Page<'_>) -> Result<(), PdfError> {
|
false => Ok(pdf),
|
||||||
if page.page_stream().is_some_and(nests_too_deep) { Err(too_deep()) } else { Ok(()) }
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pages and their boxes. An encrypted PDF needs its `password`
|
/// The pages and their boxes. An encrypted PDF needs its `password`
|
||||||
|
|
|
||||||
|
|
@ -734,3 +734,46 @@ fn opacity_mask_exports_as_luminosity_soft_mask() {
|
||||||
assert!(text.contains("/SMask"), "soft mask in an ExtGState");
|
assert!(text.contains("/SMask"), "soft mask in an ExtGState");
|
||||||
assert!(text.contains("/Luminosity"));
|
assert!(text.contains("/Luminosity"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A function or colour space built on itself is refused before the interpreter builds it (by
|
||||||
|
/// recursion, with no cap: a stack overflow), and those of real files are read.
|
||||||
|
#[test]
|
||||||
|
fn functions_and_colour_spaces_built_on_themselves_are_refused() {
|
||||||
|
use vectorcraft_testkit::pdf::{PdfPage, first_extra, pdf_with};
|
||||||
|
let first = first_extra(1);
|
||||||
|
let (me, next) = (format!("{first} 0 R"), format!("{} 0 R", first + 1));
|
||||||
|
let shaded = PdfPage {
|
||||||
|
resources: format!("/Shading << /S << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function {me} >> >>"),
|
||||||
|
..PdfPage::new(100.0, 100.0, "/S sh")
|
||||||
|
};
|
||||||
|
let filled = PdfPage { resources: format!("/ColorSpace << /C {me} >>"), ..PdfPage::new(100.0, 100.0, "/C cs 0 sc 0 0 50 50 re f") };
|
||||||
|
let stitch = |on: &str| format!("<< /FunctionType 3 /Domain [0 1] /Functions [{on}] /Bounds [] /Encode [0 1] >>");
|
||||||
|
let indexed = |on: &str| format!("[/Indexed {on} 1 <000000ffffff>]");
|
||||||
|
let ramp = "<< /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >>";
|
||||||
|
// `n` functions, each stitching the next, then a plain one.
|
||||||
|
let chain = |n: usize| (1..=n).map(|i| stitch(&format!("{} 0 R", first + i))).chain([ramp.to_string()]).collect::<Vec<_>>();
|
||||||
|
let calculator =
|
||||||
|
|program: &str| format!("<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Length {} >>\nstream\n{program}\nendstream", program.len());
|
||||||
|
for (what, page, objects) in [
|
||||||
|
("a function stitching itself", &shaded, vec![stitch(&me)]),
|
||||||
|
("two functions stitching each other", &shaded, vec![stitch(&next), stitch(&me)]),
|
||||||
|
("twenty functions, each stitching the next", &shaded, chain(20)),
|
||||||
|
("procedures in procedures", &shaded, vec![calculator(&format!("{}{}", "{".repeat(100), "}".repeat(100)))]),
|
||||||
|
("a colour space on itself", &filled, vec![indexed(&me)]),
|
||||||
|
("two colour spaces on each other", &filled, vec![indexed(&next), indexed(&me)]),
|
||||||
|
] {
|
||||||
|
let objects: Vec<&str> = objects.iter().map(String::as_str).collect();
|
||||||
|
let e = import(&pdf_with(std::slice::from_ref(page), &objects, None)).err().map(|e| e.to_string());
|
||||||
|
assert!(e.as_deref().is_some_and(|e| e.contains("built on one another without end")), "{what}: {e:?}");
|
||||||
|
}
|
||||||
|
for (what, page, objects) in [
|
||||||
|
("a function stitching one", &shaded, vec![stitch(&next), ramp.to_string()]),
|
||||||
|
("three functions, each stitching the next", &shaded, chain(3)),
|
||||||
|
("a calculator function", &shaded, vec![calculator("{ dup 1 exch sub 0 }")]),
|
||||||
|
("an indexed colour space", &filled, vec![indexed("/DeviceRGB")]),
|
||||||
|
] {
|
||||||
|
let objects: Vec<&str> = objects.iter().map(String::as_str).collect();
|
||||||
|
let d = import(&pdf_with(std::slice::from_ref(page), &objects, None)).unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||||
|
assert!(d.layers.iter().any(|l| l.children().is_some_and(|c| !c.is_empty())), "{what}: its art is read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,7 @@ fn a_sublayer_that_paints_first_brings_its_parent_layer() {
|
||||||
#[test]
|
#[test]
|
||||||
fn hostile_layer_orders_stay_bounded() {
|
fn hostile_layer_orders_stay_bounded() {
|
||||||
// A group listed twice, under itself, and an order nested far deeper than layers go.
|
// A group listed twice, under itself, and an order nested far deeper than layers go.
|
||||||
let deep = format!("{}{{2}}{}", "[".repeat(100), "]".repeat(100));
|
let deep = format!("{}{{2}}{}", "[".repeat(40), "]".repeat(40));
|
||||||
let order = format!("[{{0}} [{{0}} {{1}}] {{1}} [{{0}}] {deep}]");
|
let order = format!("[{{0}} [{{0}} {{1}}] {{1}} [{{0}}] {deep}]");
|
||||||
let d = import(&three_groups([&ocg("A", ""), &ocg("B", ""), &ocg("C", "")], &[], &format!("/Order {order}"))).unwrap();
|
let d = import(&three_groups([&ocg("A", ""), &ocg("B", ""), &ocg("C", "")], &[], &format!("/Order {order}"))).unwrap();
|
||||||
assert_eq!(names(&d), ["A", "C"], "C, listed too deep, stays a top-level layer");
|
assert_eq!(names(&d), ["A", "C"], "C, listed too deep, stays a top-level layer");
|
||||||
|
|
@ -317,7 +317,8 @@ fn thousands_of_layers_hidden_ones_included_all_come_in() {
|
||||||
/// Import `bytes` with the scan's operator limit lowered to `n`.
|
/// Import `bytes` with the scan's operator limit lowered to `n`.
|
||||||
fn import_with_max_ops(bytes: &[u8], n: usize) -> ImportReport {
|
fn import_with_max_ops(bytes: &[u8], n: usize) -> ImportReport {
|
||||||
let old = crate::import_scan::TEST_MAX_OPS.replace(n);
|
let old = crate::import_scan::TEST_MAX_OPS.replace(n);
|
||||||
let r = import_with_report(bytes, &ImportOptions::default());
|
// (On this thread: the limit is this thread's.)
|
||||||
|
let r = crate::import::read(bytes, &ImportOptions::default());
|
||||||
crate::import_scan::TEST_MAX_OPS.set(old);
|
crate::import_scan::TEST_MAX_OPS.set(old);
|
||||||
r.unwrap()
|
r.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue