mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 11:17:14 -04:00
Placed PDFs: render with pdfcraft's patched hayro crates
Placed PDFs rendered through hayro 0.7 from crates.io, whose hayro-interpret divides integers in calculator functions without a check, does not limit how deep functions nest and samples each mesh triangle over its whole bounding box. A placed PDF whose calculator function runs `1 0 idiv` or `-2147483648 -1 idiv` panicked the render. A chain of 100,000 functions, each stitching function naming the next, overflowed the stack and aborted the process. Mesh shadings with points far off the page, ten thousand overlapping triangles or forty thousand patches ran for over a minute or passed 2 GiB of memory. [patch.crates-io] now takes hayro, hayro-interpret, hayro-syntax and hayro-jbig2 from pdfcraft's vendor/ directory at b5467d93, with the patches its vendor/README.md lists. The crate versions do not change. The patched hayro-syntax sizes a page with a /UserUnit in points, and pdf_page_box scales the page's boxes by the same unit, so with Crop to the frame still shows the chosen box. NOTICE names the new source and the licenses. The function and mesh tests in crates/render/tests/placed_pdf.rs follow pdfcraft's tests in crates/render/src/raster.rs.
This commit is contained in:
1 parent
e862e0a613
commit
7a89954937
6 files changed
+285
-14
No files matched your search
Generated
+18
-9
@@ -2004,13 +2004,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hayro"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4caa128ab87fd48ffb7490617cf93f77f606820dffbc9fd9ef6ab0ed077f56d"
|
||||
source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"hayro-interpret",
|
||||
"image",
|
||||
"kurbo",
|
||||
"log",
|
||||
"pic-scale",
|
||||
"vello_cpu 0.0.8",
|
||||
]
|
||||
@@ -2034,8 +2034,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hayro-interpret"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d"
|
||||
source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"hayro-cmap",
|
||||
@@ -2045,7 +2044,7 @@ dependencies = [
|
||||
"phf",
|
||||
"rustc-hash 2.1.3",
|
||||
"siphasher",
|
||||
"skrifa 0.42.1",
|
||||
"skrifa 0.47.0",
|
||||
"smallvec",
|
||||
"yoke",
|
||||
]
|
||||
@@ -2053,8 +2052,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hayro-jbig2"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69374b3668dd45aeb3d3145cda68f2c7b4f223aaa2511e67d076f1c7d741388d"
|
||||
source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9"
|
||||
dependencies = [
|
||||
"fearless_simd",
|
||||
"hayro-ccitt",
|
||||
@@ -2078,8 +2076,7 @@ checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa"
|
||||
[[package]]
|
||||
name = "hayro-syntax"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27"
|
||||
source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"hayro-ccitt",
|
||||
@@ -2088,6 +2085,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"rustc-hash 2.1.3",
|
||||
"smallvec",
|
||||
"stringprep",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
@@ -4199,6 +4197,17 @@ dependencies = [
|
||||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subsetter"
|
||||
version = "0.2.6"
|
||||
|
||||
@@ -101,3 +101,8 @@ debug = 0
|
||||
# Opt-in cancelable macOS termination on the pinned winit 0.30 line.
|
||||
[patch.crates-io]
|
||||
winit = { git = "https://github.com/skylartaylor/winit.git", rev = "3d51077fd1a966f84df9ef287839dc84fd5b4ca7" }
|
||||
# The hayro crates from pdfcraft's vendor/ directory, with the patches its README lists.
|
||||
hayro = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" }
|
||||
hayro-interpret = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" }
|
||||
hayro-syntax = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" }
|
||||
hayro-jbig2 = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" }
|
||||
@@ -58,6 +58,11 @@ list (author, source, license) is in ASSETS.md.
|
||||
Shippori Mincho), under the SIL Open Font License 1.1. Their licence texts ship with the
|
||||
packages as OFL-<family>.txt. Builds without CRAFT_FONTS_DIR do not include them.
|
||||
- All UI icons are drawn in code (crates/ui-egui/src/icons.rs) and are original work, MIT OR Apache-2.0.
|
||||
- hayro PDF crates (placed PDFs): hayro, hayro-interpret, hayro-syntax and hayro-jbig2
|
||||
(Apache-2.0 OR MIT) are pdfcraft's patched copies, fetched by git from its vendor/ directory at
|
||||
the commit that Cargo.toml pins (https://github.com/storytold/pdfcraft, MIT OR Apache-2.0).
|
||||
hayro-interpret embeds PDFium's Foxit standard-14 fonts under PDFium's BSD-style license
|
||||
(vendor/hayro-interpret/assets/LICENSE_FOXIT) and a CMYK profile under CC0-1.0.
|
||||
|
||||
Rust dependencies (vello_cpu, kurbo, skrifa, harfrust, image, egui, eframe, …) are permissively
|
||||
licensed (MIT/Apache-2.0/BSD/ISC/Zlib) and fetched through crates.io.
|
||||
|
||||
@@ -147,6 +147,7 @@ Newest first.
|
||||
- **2026-10-11** · `story.replaceRange`, `story.setText`, notes, footnotes, endnotes, type on a path and table cells read `\r` and `\r\n` as paragraph returns, as `frame.create` and `text.insert` already did. No readiness percentage change.
|
||||
- **2026-10-11** · The View menu follows InDesign's: proofing first, then zoom, Rotate Spread, Screen Mode, Display Performance, rulers, Extras, Grids & Guides and Structure; the view items that sat at the end of the Type menu moved there. No readiness percentage change.
|
||||
- **2026-10-12** · Underlines and strikethroughs are drawn as one bar per stretch of text on screen, as in PDF export: a bar per letter left faint seams between the letters. No readiness percentage change.
|
||||
- **2026-10-13** · Placed PDFs are drawn through pdfcraft's patched hayro crates: calculator functions whose `idiv` divides by zero or overflows, chains of stitching functions a hundred thousand deep, and mesh shadings with points far off the page, ten thousand overlapping triangles or forty thousand patches no longer panic, overflow the stack, run for over a minute or use more than 2 GiB of memory. A page with a `/UserUnit`, and its trim, bleed and art boxes, are sized in points. No readiness percentage change.
|
||||
- **2026-10-12** · Properties › Transform shows rotation, shear and X/Y scale, and Rotate 90° and Flip Horizontal/Vertical buttons, as InDesign's does. No readiness percentage change.
|
||||
- **2026-10-12** · Edit › Step and Repeat has its InDesign dialog: Repeat (Count, or Rows and Columns with Create as a grid), Offset (Vertical, Horizontal, in ruler units) and Preview, which shows the copies while the dialog is open; Cancel takes them away and OK makes them as one undo step. No readiness percentage change.
|
||||
- **2026-10-12** · Double-clicking the Fill or Stroke box in the Tools panel opens the Color Picker with the selection's colour, as in InDesign (it opened the Swatches panel). No readiness percentage change.
|
||||
|
||||
@@ -1081,18 +1081,20 @@ pub fn pdf_page_box(bytes: &[u8], page: usize, kind: &str) -> Option<(f64, f64,
|
||||
let pdf = hayro::hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).ok()?;
|
||||
let p = pdf.pages().get(page)?;
|
||||
let visible = p.intersected_crop_box();
|
||||
// The boxes are in user space units; `/UserUnit` is the size of one in points.
|
||||
let unit = p.user_unit() as f64;
|
||||
let key: &[u8] = match kind {
|
||||
"trim" => keys::TRIM_BOX,
|
||||
"bleed" => keys::BLEED_BOX,
|
||||
"art" => keys::ART_BOX,
|
||||
"media" => return Some(rel(visible, p.media_box())),
|
||||
_ => return Some(rel(visible, visible)),
|
||||
"media" => return Some(rel(visible, p.media_box(), unit)),
|
||||
_ => return Some(rel(visible, visible, unit)),
|
||||
};
|
||||
let b = p.raw().get::<PRect>(key).map(|b| b.intersect(p.media_box())).unwrap_or(visible);
|
||||
fn rel(v: PRect, b: PRect) -> (f64, f64, f64, f64) {
|
||||
(b.x0 - v.x0, v.y1 - b.y1, b.width(), b.height())
|
||||
fn rel(v: PRect, b: PRect, unit: f64) -> (f64, f64, f64, f64) {
|
||||
((b.x0 - v.x0) * unit, (v.y1 - b.y1) * unit, b.width() * unit, b.height() * unit)
|
||||
}
|
||||
Some(rel(visible, b))
|
||||
Some(rel(visible, b, unit))
|
||||
}
|
||||
|
||||
/// Separations Preview on a rendered image: `plate` 0–3 shows that process plate (C, M, Y, K) as
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
//! Placed PDFs are drawn through pdfcraft's patched hayro crates. Calculator functions whose `idiv`
|
||||
//! divides by zero or overflows, stitching functions nested a hundred thousand deep, and mesh
|
||||
//! shadings with points far off the page, ten thousand overlapping triangles or forty thousand
|
||||
//! patches do not panic, overflow the stack or take more than a minute, and the rest of the page is
|
||||
//! drawn. A page with a `/UserUnit` is sized in points, and so are its boxes. The function and mesh
|
||||
//! cases follow pdfcraft's tests in `crates/render/src/raster.rs`.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{self, RecvTimeoutError};
|
||||
use std::time::Duration;
|
||||
|
||||
use designcraft_compose::Cache;
|
||||
use designcraft_doc::build::NewDocument;
|
||||
use designcraft_doc::{Asset, AssetId, Content, Document, Graphic, Item, ItemId, Shape, SpreadRef};
|
||||
use designcraft_geom::{Affine, Rect, shapes};
|
||||
use designcraft_render::{Rendered, Renderer, pdf_page_box, pdf_page_size};
|
||||
use hayro::hayro_interpret::Function;
|
||||
use hayro::hayro_syntax::Pdf;
|
||||
use hayro::hayro_syntax::object::{Object, ObjectIdentifier};
|
||||
|
||||
const WHITE: [u8; 4] = [255, 255, 255, 255];
|
||||
const RED: [u8; 4] = [255, 0, 0, 255];
|
||||
const BLUE: [u8; 4] = [0, 0, 255, 255];
|
||||
|
||||
/// An exponential function of one input with one output.
|
||||
const LEAF: &str = "<</FunctionType 2/Domain[0 1]/C0[0]/C1[1]/N 1>>";
|
||||
|
||||
/// Mesh entries common to the shadings below: RGB colors, 8-bit coordinates and components.
|
||||
const MESH: &str = "/ColorSpace /DeviceRGB /BitsPerCoordinate 8 /BitsPerComponent 8";
|
||||
|
||||
/// One triangle of a free-form triangle mesh: three vertices of a flag, x, y and a color.
|
||||
const TRIANGLE: [u8; 18] = [0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255];
|
||||
|
||||
/// A stream object with the `dict` entries, its `/Length` and `data`.
|
||||
fn stream(dict: &str, data: &[u8]) -> Vec<u8> {
|
||||
let mut s = format!("<<{dict} /Length {}>>\nstream\n", data.len()).into_bytes();
|
||||
s.extend_from_slice(data);
|
||||
s.extend_from_slice(b"\nendstream");
|
||||
s
|
||||
}
|
||||
|
||||
/// A one-page PDF whose page dictionary holds the `page` entries (boxes, resources) and whose page
|
||||
/// draws `content`. The `objects` are numbered from 5.
|
||||
fn pdf(page: &str, content: &str, objects: &[Vec<u8>]) -> Vec<u8> {
|
||||
let mut bodies = vec![
|
||||
b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
|
||||
b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
|
||||
format!("<</Type/Page/Parent 2 0 R{page}/Contents 4 0 R>>").into_bytes(),
|
||||
stream("", content.as_bytes()),
|
||||
];
|
||||
bodies.extend_from_slice(objects);
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, body) in bodies.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
out.extend_from_slice(body);
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f\r\n", bodies.len() + 1).as_bytes());
|
||||
for offset in offsets {
|
||||
out.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<</Size {}/Root 1 0 R>>\nstartxref\n{xref}\n%%EOF\n", bodies.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
/// The page of a document `size` points square with `pdf` placed to fill it, rendered by
|
||||
/// `Renderer::render_page` at one pixel per point. Fails when the render panics or takes more
|
||||
/// than a minute.
|
||||
fn placed(size: f64, pdf: Vec<u8>) -> Rendered {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut d = Document::new(&NewDocument { width: size, height: size, facing_pages: false, ..Default::default() });
|
||||
let id = AssetId(d.alloc());
|
||||
let asset =
|
||||
d.add_asset(Asset { id, name: "art.pdf".into(), mime: "application/pdf".into(), link: None, data: Arc::new(pdf), pixels: None, page: 0 });
|
||||
let id = ItemId(d.alloc());
|
||||
let mut item = Item::new(id, d.default_layer(), Shape::Rectangle, shapes::rectangle(Rect::new(0.0, 0.0, size, size)));
|
||||
item.content =
|
||||
Content::Graphic(Graphic { asset, size: (size, size), xf: Affine::IDENTITY, auto_fit: Default::default(), fit_align: 4, crop: [0.0; 4] });
|
||||
d.insert_item(SpreadRef::Doc(0), item, None).unwrap();
|
||||
let mut renderer = Renderer::new();
|
||||
renderer.threads = 0;
|
||||
let _ = tx.send(renderer.render_page(&d, &Cache::new(), 0, 1.0, false, &Default::default()));
|
||||
});
|
||||
match rx.recv_timeout(Duration::from_secs(60)) {
|
||||
Ok(page) => page.expect("the page renders"),
|
||||
Err(RecvTimeoutError::Timeout) => panic!("the placed PDF did not render within a minute"),
|
||||
Err(RecvTimeoutError::Disconnected) => panic!("the render of the placed PDF panicked"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A 40-point page that paints an axial shading through the first of `functions` (objects 6
|
||||
/// onwards), then a blue square over the whole page.
|
||||
fn shaded(functions: &[Vec<u8>]) -> Vec<u8> {
|
||||
let mut objects = vec![b"<</ShadingType 2/ColorSpace/DeviceRGB/Coords[0 0 40 0]/Function 6 0 R>>".to_vec()];
|
||||
objects.extend_from_slice(functions);
|
||||
pdf("/MediaBox[0 0 40 40]/Resources<</Shading<</S 5 0 R>>>>", "/S sh 0 0 1 rg 0 0 40 40 re f", &objects)
|
||||
}
|
||||
|
||||
/// A calculator (Type 4) function of one input that runs `program`.
|
||||
fn calculator(program: &str) -> Vec<u8> {
|
||||
stream("/FunctionType 4/Domain[0 1]/Range[0 1 0 1 0 1]", program.as_bytes())
|
||||
}
|
||||
|
||||
/// The function that is object 5 of a PDF holding `objects`.
|
||||
fn function(objects: &[&str]) -> Option<Function> {
|
||||
let objects: Vec<Vec<u8>> = objects.iter().map(|o| o.as_bytes().to_vec()).collect();
|
||||
let parsed = Pdf::new(pdf("/MediaBox[0 0 20 20]", "", &objects)).unwrap();
|
||||
let object = parsed.xref().get::<Object<'_>>(ObjectIdentifier::new(5, 0)).unwrap();
|
||||
Function::new(&object)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_pdf_function_dividing_by_zero_leaves_the_page_drawn() {
|
||||
assert_eq!(placed(40.0, shaded(&[calculator("{ pop 1 0 idiv }")])).pixel(20, 20), BLUE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_pdf_function_dividing_the_smallest_integer_by_minus_one_leaves_the_page_drawn() {
|
||||
assert_eq!(placed(40.0, shaded(&[calculator("{ pop -2147483648 -1 idiv }")])).pixel(20, 20), BLUE);
|
||||
}
|
||||
|
||||
/// A chain of 100,000 functions: each stitching function names the next by reference, and the last
|
||||
/// is an exponential function.
|
||||
#[test]
|
||||
fn placed_pdf_chain_of_stitching_functions_leaves_the_page_drawn() {
|
||||
let mut functions: Vec<Vec<u8>> =
|
||||
(7..100_006).map(|next| format!("<</FunctionType 3/Domain[0 1]/Functions[{next} 0 R]/Bounds[]/Encode[0 1]>>").into_bytes()).collect();
|
||||
functions.push(b"<</FunctionType 2/Domain[0 1]/C0[0 0 0]/C1[1 1 1]/N 1>>".to_vec());
|
||||
assert_eq!(placed(40.0, shaded(&functions)).pixel(20, 20), BLUE);
|
||||
}
|
||||
|
||||
/// Stitching functions that each name the next through `N 0 R`: 64 evaluate, 65 are refused.
|
||||
#[test]
|
||||
fn function_limits_depth_guard_refuses_a_chain_of_references() {
|
||||
for functions in [64, 65] {
|
||||
let objects: Vec<String> = (5..5 + functions)
|
||||
.map(|number| {
|
||||
if number < 4 + functions {
|
||||
format!("<</FunctionType 3/Domain[0 1]/Functions[{} 0 R]/Bounds[]/Encode[0 1]>>", number + 1)
|
||||
} else {
|
||||
LEAF.to_owned()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let objects: Vec<&str> = objects.iter().map(String::as_str).collect();
|
||||
let chain = function(&objects);
|
||||
if functions == 64 {
|
||||
assert_eq!(chain.unwrap().eval([0.5].into_iter().collect()).unwrap().as_slice(), &[0.5]);
|
||||
} else {
|
||||
assert!(chain.is_none(), "65 functions are one past the function-depth guard");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stitching functions with a cycle, no children or a missing child are refused; one child named
|
||||
/// twice still evaluates.
|
||||
#[test]
|
||||
fn function_limits_stitching_cycles_are_refused_and_shared_children_work() {
|
||||
for objects in [
|
||||
&["<</FunctionType 3/Domain[0 1]/Functions[5 0 R]/Bounds[]/Encode[0 1]>>"][..],
|
||||
&[
|
||||
"<</FunctionType 3/Domain[0 1]/Functions[6 0 R]/Bounds[]/Encode[0 1]>>",
|
||||
"<</FunctionType 3/Domain[0 1]/Functions[5 0 R]/Bounds[]/Encode[0 1]>>",
|
||||
],
|
||||
&["<</FunctionType 3/Domain[0 1]/Functions[]/Bounds[]/Encode[]>>"],
|
||||
&["<</FunctionType 3/Domain[0 1]/Functions[99 0 R]/Bounds[]/Encode[0 1]>>"],
|
||||
&["<</FunctionType 3/Domain[0 1]/Functions[6 0 R 99 0 R]/Bounds[0.5]/Encode[0 1 0 1]>>", LEAF],
|
||||
] {
|
||||
assert!(function(objects).is_none(), "accepted invalid children: {objects:?}");
|
||||
}
|
||||
let shared = function(&["<</FunctionType 3/Domain[0 1]/Functions[6 0 R 6 0 R]/Bounds[0.5]/Encode[0 1 0 1]>>", LEAF]).unwrap();
|
||||
let output = shared.eval([0.75].into_iter().collect()).unwrap();
|
||||
assert!((output[0] - 0.5).abs() < 0.001);
|
||||
}
|
||||
|
||||
/// Ten thousand triangles that each cover half of the placed page, which is drawn 1000 pixels
|
||||
/// square.
|
||||
#[test]
|
||||
fn many_overlapping_mesh_triangles_finish() {
|
||||
let shading = stream(&format!("/ShadingType 4 {MESH} /BitsPerFlag 8 /Decode [0 125 0 125 0 1 0 1 0 1]"), &TRIANGLE.repeat(10_000));
|
||||
let page = placed(125.0, pdf("/MediaBox[0 0 125 125]/Resources<</Shading<</Sh0 5 0 R>>>>", "/Sh0 sh", &[shading]));
|
||||
let p = page.pixel(12, 112);
|
||||
assert!(p != WHITE && p[3] == 255, "the triangles are drawn: {p:?}");
|
||||
}
|
||||
|
||||
/// One Coons patch: its flag, twelve points around a square of 8-bit coordinates, four colors.
|
||||
fn coons() -> Vec<u8> {
|
||||
let mut patch = vec![0u8];
|
||||
for p in [(0, 0), (0, 85), (0, 170), (0, 255), (85, 255), (170, 255), (255, 255), (255, 170), (255, 85), (255, 0), (170, 0), (85, 0)] {
|
||||
patch.extend_from_slice(&[p.0, p.1]);
|
||||
}
|
||||
patch.extend_from_slice(&[255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0]);
|
||||
patch
|
||||
}
|
||||
|
||||
/// A 40-point page that paints `shading` (object 5), then a red square at its bottom left.
|
||||
fn meshed(shading: Vec<u8>) -> Vec<u8> {
|
||||
pdf("/MediaBox[0 0 40 40]/Resources<</Shading<</Sh0 5 0 R>>>>", "/Sh0 sh 1 0 0 rg 0 0 4 4 re f", &[shading])
|
||||
}
|
||||
|
||||
/// Meshes whose points lie far off the page are sampled only where the page shows them.
|
||||
#[test]
|
||||
fn mesh_shadings_far_off_the_page_render() {
|
||||
let far = "/Decode [-40000 40000 -40000 40000 0 1 0 1 0 1]";
|
||||
for (what, shading) in [
|
||||
("a Coons patch", stream(&format!("/ShadingType 6 {MESH} /BitsPerFlag 8 {far}"), &coons())),
|
||||
("a triangle mesh", stream(&format!("/ShadingType 4 {MESH} /BitsPerFlag 8 {far}"), &TRIANGLE)),
|
||||
] {
|
||||
assert_eq!(placed(40.0, meshed(shading)).pixel(1, 38), RED, "{what} far off the page: the rest of the page draws");
|
||||
}
|
||||
for (what, decode) in [("on the page", "[0 40 0 40 0 1 0 1 0 1]"), ("covering the page", "[-40000 40000 -40000 40000 0 1 0 1 0 1]")] {
|
||||
let page = placed(40.0, meshed(stream(&format!("/ShadingType 6 {MESH} /BitsPerFlag 8 /Decode {decode}"), &coons())));
|
||||
let center = page.pixel(20, 20);
|
||||
assert!(center != WHITE && center[3] == 255, "a patch {what} is shaded: {center:?}");
|
||||
}
|
||||
// A patch filling a 4 × 4 pt rectangle as a pattern is still shaded.
|
||||
let shading = stream(&format!("/ShadingType 6 {MESH} /BitsPerFlag 8 /Decode [10 14 10 14 0 1 0 1 0 1]"), &coons());
|
||||
let page = placed(
|
||||
40.0,
|
||||
pdf("/MediaBox[0 0 40 40]/Resources<</Pattern<</P0<</PatternType 2/Shading 5 0 R>>>>>>", "/Pattern cs /P0 scn 10 10 4 4 re f", &[shading]),
|
||||
);
|
||||
let middle = page.pixel(12, 28);
|
||||
assert!(middle != WHITE && middle[3] == 255, "a small patch is shaded: {middle:?}");
|
||||
}
|
||||
|
||||
/// Forty thousand Coons patches beside the page, and a lattice with no vertices per row.
|
||||
#[test]
|
||||
fn mesh_shadings_with_many_patches_or_no_rows_finish() {
|
||||
for shading in [
|
||||
stream(&format!("/ShadingType 6 {MESH} /BitsPerFlag 8 /Decode [100 140 100 140 0 1 0 1 0 1]"), &coons().repeat(40_000)),
|
||||
stream(&format!("/ShadingType 5 {MESH} /VerticesPerRow 0 /Decode [0 40 0 40 0 1 0 1 0 1]"), &[0; 64]),
|
||||
] {
|
||||
assert_eq!(placed(40.0, meshed(shading)).pixel(1, 38), RED, "the rest of the page draws");
|
||||
}
|
||||
}
|
||||
|
||||
/// A page with `/UserUnit 2` is twice its media box in points, and Image Import Options › Crop
|
||||
/// to measures the trim box in points as well.
|
||||
#[test]
|
||||
fn a_page_with_a_user_unit_and_its_boxes_are_sized_in_points() {
|
||||
let page = pdf("/MediaBox[0 0 100 50]/TrimBox[10 10 90 40]/UserUnit 2", "", &[]);
|
||||
assert_eq!(pdf_page_size(&page, 0), Some((200.0, 100.0)));
|
||||
assert_eq!(pdf_page_box(&page, 0, "trim"), Some((20.0, 20.0, 160.0, 60.0)));
|
||||
assert_eq!(pdf_page_box(&page, 0, "media"), Some((0.0, 0.0, 200.0, 100.0)));
|
||||
}
|
||||
Reference in new issue
Block a user