mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
1095 lines
58 KiB
Rust
1095 lines
58 KiB
Rust
//! The PostScript reader: each operator family on a small program, files the EPS writer made,
|
||
//! the preview fallback and refused files.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::{Color, GradientKind, Paint};
|
||
use vectorcraft_doc::{AppearanceItem, ColorMode, Document, LineCap, LineJoin, Node, NodeKind};
|
||
use vectorcraft_geom::{Point, Rect};
|
||
|
||
use crate::import::{Dsc, import};
|
||
use crate::{EpsOptions, Level, Preview, Raster};
|
||
|
||
/// `body` as an EPS file with a 100 × 100 pt bounding box.
|
||
pub(super) fn 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()
|
||
}
|
||
|
||
pub(super) fn read(body: &str) -> crate::Imported {
|
||
import(&eps(body)).unwrap()
|
||
}
|
||
|
||
/// #1714: arrays and dictionaries a program links into themselves or each other are freed with
|
||
/// the interpreter, also the ones it no longer reaches and when it stops on an error.
|
||
#[test]
|
||
fn arrays_and_dictionaries_holding_themselves_are_freed() {
|
||
// The arrays still alive once a program's interpreter has released what it made: the ones
|
||
// the interpreter itself holds, the same for every program here.
|
||
let left = |program: &str| {
|
||
let _ = import(&eps(program));
|
||
crate::import::obj::ARRAYS_LEFT.with(std::cell::Cell::get)
|
||
};
|
||
let plain = left("/a 1 array def /b 1 array def a 0 b put");
|
||
let cycles = "/a 1 array def a 0 a put /b 1 array def /c 1 array def b 0 c put c 0 b put /d 1 dict def /e 1 array def d /e e put e 0 d put 1 array dup 0 1 index put pop /a null def /b null def";
|
||
for program in [cycles.to_string(), format!("{cycles} 1 0 div"), format!("{cycles} /p {{ p }} def")] {
|
||
assert_eq!(left(&program), plain, "{program}");
|
||
}
|
||
}
|
||
|
||
/// The objects of the document's layer.
|
||
pub(super) fn objects(d: &Document) -> Vec<Arc<Node>> {
|
||
d.layers[0].children().unwrap().clone()
|
||
}
|
||
|
||
/// Every object, down through groups, in order.
|
||
pub(super) fn all(d: &Document) -> Vec<Arc<Node>> {
|
||
fn walk(n: &Arc<Node>, out: &mut Vec<Arc<Node>>) {
|
||
out.push(n.clone());
|
||
for c in n.children().into_iter().flatten() {
|
||
walk(c, out);
|
||
}
|
||
}
|
||
let mut out = vec![];
|
||
for n in objects(d) {
|
||
walk(&n, &mut out);
|
||
}
|
||
out
|
||
}
|
||
|
||
pub(super) fn fill(n: &Node) -> Option<&Paint> {
|
||
n.appearance.items.iter().find_map(|i| match i {
|
||
AppearanceItem::Fill(f) => Some(&f.paint),
|
||
_ => None,
|
||
})
|
||
}
|
||
|
||
pub(super) fn stroke(n: &Node) -> Option<&vectorcraft_doc::StrokeLayer> {
|
||
n.appearance.items.iter().find_map(|i| match i {
|
||
AppearanceItem::Stroke(s) => Some(s),
|
||
_ => None,
|
||
})
|
||
}
|
||
|
||
pub(super) fn bounds(n: &Node) -> Rect {
|
||
n.geometric_bounds().unwrap()
|
||
}
|
||
|
||
pub(super) fn near(a: Rect, b: Rect) -> bool {
|
||
[(a.x0, b.x0), (a.y0, b.y0), (a.x1, b.x1), (a.y1, b.y1)].iter().all(|(x, y)| (x - y).abs() < 0.05)
|
||
}
|
||
|
||
#[test]
|
||
fn paths_fill_and_stroke_in_document_space() {
|
||
let r = read("newpath 10 20 moveto 60 20 lineto 60 70 lineto closepath 1 0 0 setrgbcolor fill");
|
||
let n = &objects(&r.document)[0];
|
||
// PostScript's y goes up from the bottom of the bounding box.
|
||
assert!(near(bounds(n), Rect::new(10.0, 30.0, 60.0, 80.0)), "{:?}", bounds(n));
|
||
assert_eq!(fill(n), Some(&Paint::solid(Color::rgb(1.0, 0.0, 0.0))));
|
||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||
// Curves and arcs.
|
||
let r = read("0 0 moveto 0 50 50 50 50 0 curveto stroke 50 50 20 0 360 arc fill");
|
||
let o = objects(&r.document);
|
||
assert_eq!(o.len(), 2);
|
||
assert!(stroke(&o[0]).is_some() && fill(&o[0]).is_none());
|
||
assert!(near(bounds(&o[1]), Rect::new(30.0, 30.0, 70.0, 70.0)), "{:?}", bounds(&o[1]));
|
||
// Relative operators, rectangles.
|
||
let r = read("10 10 moveto 20 0 rlineto 0 20 rlineto closepath fill 5 5 10 10 rectfill 0 0 4 4 rectstroke");
|
||
let o = objects(&r.document);
|
||
assert!(near(bounds(&o[0]), Rect::new(10.0, 70.0, 30.0, 90.0)));
|
||
assert!(near(bounds(&o[1]), Rect::new(5.0, 85.0, 15.0, 95.0)));
|
||
assert!(stroke(&o[2]).is_some());
|
||
}
|
||
|
||
/// Each `closepath` adds an element to the path, so on a path that holds `MAX_PATH` (1,048,576)
|
||
/// elements it stops the reading with the same error as `lineto`, run from a procedure or from an
|
||
/// encoded user path.
|
||
#[test]
|
||
fn closepath_stops_at_the_path_limit() {
|
||
for body in [
|
||
// A moveto and 1,100,000 closepaths.
|
||
"newpath 0 0 moveto 1100000 { closepath } repeat",
|
||
// An encoded user path: setbbox and moveto, then 4,750 pairs of a repeat count of 223 (the
|
||
// byte 255) and closepath (10), 1,059,250 closepaths.
|
||
"/s 9502 string def s 0 0 put s 1 1 put 2 2 9500 { dup s exch 255 put 1 add s exch 10 put } for \
|
||
newpath [ [0 0 10 10 0 0] s ] uappend",
|
||
] {
|
||
// The square drawn before the error stays, and the one after it is left out.
|
||
let r = read(&format!("0 0 10 10 rectfill {body} 20 20 10 10 rectfill"));
|
||
assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings);
|
||
assert!(r.warnings[0].contains("a path has too many points"), "{body}: {:?}", r.warnings);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn colours_in_every_model() {
|
||
let r = read(
|
||
"0 0 10 10 rectfill 0.25 setgray 0 0 10 10 rectfill 0 1 0 0 setcmykcolor 0 0 10 10 rectfill 0 1 1 sethsbcolor 0 0 10 10 rectfill \
|
||
[/Separation (Gold) /DeviceCMYK {dup 0 exch 0.5 mul 0} bind] setcolorspace 0.5 setcolor 0 0 10 10 rectfill \
|
||
/DeviceRGB setcolorspace 0 0 1 setcolor 0 0 10 10 rectfill \
|
||
[/Indexed /DeviceRGB 1 <ff0000 00ff00>] setcolorspace 1 setcolor 0 0 10 10 rectfill",
|
||
);
|
||
let fills: Vec<Paint> = objects(&r.document).iter().map(|n| fill(n).unwrap().clone()).collect();
|
||
// The initial colour is black; grey is ink coverage in the document.
|
||
assert_eq!(fills[0], Paint::solid(Color::gray(1.0)));
|
||
assert_eq!(fills[1], Paint::solid(Color::gray(0.75)));
|
||
assert_eq!(fills[2], Paint::solid(Color::cmyk(0.0, 1.0, 0.0, 0.0)));
|
||
let [r0, g0, b0] = fills[3].color().unwrap().to_rgb();
|
||
assert!(r0 > 0.99 && g0 < 0.01 && b0 < 0.01);
|
||
// A spot ink links to its spot swatch at the tint.
|
||
let Paint::Solid { swatch: Some(name), tint, .. } = &fills[4] else { panic!("{:?}", fills[4]) };
|
||
assert_eq!((name.as_str(), *tint), ("Gold", 0.5));
|
||
let gold = r.document.swatch("Gold").unwrap();
|
||
assert!(gold.spot && gold.paint.color() == Some(Color::cmyk(1.0, 0.0, 0.5, 0.0)), "{gold:?}");
|
||
assert_eq!(fills[5], Paint::solid(Color::rgb(0.0, 0.0, 1.0)));
|
||
assert_eq!(fills[6], Paint::solid(Color::rgb(0.0, 1.0, 0.0)));
|
||
// Mostly CMYK art opens in CMYK.
|
||
let r = read("0 0 0 1 setcmykcolor 0 0 5 5 rectfill 1 0 0 0 setcmykcolor 0 0 5 5 rectfill");
|
||
assert_eq!(r.document.color_mode, ColorMode::Cmyk);
|
||
assert_eq!(read("1 0 0 setrgbcolor 0 0 5 5 rectfill").document.color_mode, ColorMode::Rgb);
|
||
}
|
||
|
||
#[test]
|
||
fn gsave_and_grestore_bring_the_state_back() {
|
||
let r = read("gsave 1 0 0 setrgbcolor 5 setlinewidth 50 50 translate 0 0 10 10 rectfill grestore 0 0 10 10 rectfill");
|
||
let o = objects(&r.document);
|
||
assert_eq!(fill(&o[0]), Some(&Paint::solid(Color::rgb(1.0, 0.0, 0.0))));
|
||
assert!(near(bounds(&o[0]), Rect::new(50.0, 40.0, 60.0, 50.0)));
|
||
assert_eq!(fill(&o[1]), Some(&Paint::solid(Color::gray(1.0))));
|
||
assert!(near(bounds(&o[1]), Rect::new(0.0, 90.0, 10.0, 100.0)));
|
||
// save/restore too.
|
||
let o = objects(&read("save 0.5 setgray restore 0 0 1 1 rectfill").document);
|
||
assert_eq!(fill(&o[0]), Some(&Paint::solid(Color::gray(1.0))));
|
||
}
|
||
|
||
#[test]
|
||
fn transforms_concat() {
|
||
let r = read("[2 0 0 2 10 10] concat 0 0 10 10 rectfill 90 rotate 0 0 5 5 rectfill");
|
||
let o = objects(&r.document);
|
||
assert!(near(bounds(&o[0]), Rect::new(10.0, 70.0, 30.0, 90.0)), "{:?}", bounds(&o[0]));
|
||
// Rotated a quarter turn about (10, 10), scaled 2: x goes up, y to the left.
|
||
assert!(near(bounds(&o[1]), Rect::new(0.0, 80.0, 10.0, 90.0)), "{:?}", bounds(&o[1]));
|
||
let r = read("10 20 translate 2 3 scale 0 0 moveto 10 10 lineto stroke");
|
||
assert!(near(bounds(&objects(&r.document)[0]), Rect::new(10.0, 50.0, 30.0, 80.0)));
|
||
// Matrix operators: transform and itransform.
|
||
let r = read("2 2 scale 3 4 transform itransform translate 0 0 1 1 rectfill");
|
||
assert!(near(bounds(&objects(&r.document)[0]), Rect::new(6.0, 90.0, 8.0, 92.0)), "{:?}", bounds(&objects(&r.document)[0]));
|
||
}
|
||
|
||
#[test]
|
||
fn line_state_reaches_the_stroke() {
|
||
let r = read("2 setlinewidth 1 setlinecap 2 setlinejoin 4 setmiterlimit [3 1] 0.5 setdash 2 2 scale 0 0 moveto 10 0 lineto stroke");
|
||
let n = &objects(&r.document)[0];
|
||
let st = stroke(n).unwrap();
|
||
// The width and dashes grow with the transform.
|
||
assert_eq!((st.width, st.cap, st.join, st.miter_limit), (4.0, LineCap::Round, LineJoin::Bevel, 4.0));
|
||
let dash = st.dash.as_ref().unwrap();
|
||
assert_eq!((dash.pattern.clone(), dash.offset), (vec![6.0, 2.0], 1.0));
|
||
// A zero-width line is a hairline.
|
||
assert!(stroke(&objects(&read("0 setlinewidth 0 0 moveto 9 9 lineto stroke").document)[0]).unwrap().width > 0.0);
|
||
}
|
||
|
||
/// `setdash` takes a dash pattern of up to 65,535 entries, the most elements an array has in a
|
||
/// typical implementation (PLRM 3rd ed., Table B.1), and raises `limitcheck` for a longer one.
|
||
#[test]
|
||
fn dash_patterns_hold_at_most_65535_entries() {
|
||
let dash = |n: usize| format!("/a {n} array def 0 1 {} {{ a exch 1 put }} for a 0 setdash", n - 1);
|
||
check(&format!("{} currentdash pop length 65535 eq", dash(65535)));
|
||
let r = read(&format!("0 0 10 10 rectfill {}", dash(65536)));
|
||
assert_eq!(objects(&r.document).len(), 1, "{:?}", r.warnings);
|
||
assert!(r.warnings.iter().any(|w| w.contains("limitcheck in `setdash`")), "{:?}", r.warnings);
|
||
}
|
||
|
||
/// Saved and copied graphics states share the dash pattern, and `setdash`, `currentdash` and
|
||
/// each stroke count the entries they read or copy against the budgets. Under a pattern of
|
||
/// 65,535 entries, 4,000,000 passes of `gsave grestore` and 2,000,000 passes of
|
||
/// `gstate currentgstate setgstate` finish, and loops of `setdash`, `currentdash` and `strokepath`
|
||
/// run out of a budget.
|
||
#[test]
|
||
fn work_on_the_longest_dash_pattern_ends_soon() {
|
||
let dash = "0 0 10 10 rectfill /a 65535 array def 0 1 65534 { a exch 1 put } for a 0 setdash";
|
||
for body in ["4000000 { gsave grestore } repeat", "2000000 { gstate currentgstate setgstate } repeat"] {
|
||
let r = read_soon(&format!("{dash} {body} 20 20 10 10 rectfill"));
|
||
assert_eq!(objects(&r.document).len(), 2, "{body}: {:?}", r.warnings);
|
||
assert!(r.warnings.is_empty(), "{body}: {:?}", r.warnings);
|
||
}
|
||
for (body, why) in [
|
||
("{ a 0 setdash } loop", "runs too long"),
|
||
("{ currentdash pop pop } loop", "uses too much memory"),
|
||
("{ newpath 0 0 moveto 100 0 lineto strokepath } loop", "runs too long"),
|
||
] {
|
||
let r = read_soon(&format!("{dash} {body}"));
|
||
assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings);
|
||
assert!(r.warnings.iter().any(|w| w.contains(why)), "{body}: {:?}", r.warnings);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn procedures_bind_def_loops_and_dictionaries() {
|
||
let r = read(
|
||
"/bd {bind def} bind def /sq {newpath moveto 10 0 rlineto 0 10 rlineto -10 0 rlineto closepath fill} bd \
|
||
0 20 60 {0 sq} for 3 {} repeat /n 0 def {n 1 add /n exch def n 2 ge {exit} if} loop \
|
||
5 dict begin /x 70 def x 70 sq end \
|
||
<< /a 1 /b 2 >> /b get 2 eq {0 0 sq} if \
|
||
[1 2 3] {pop} forall (abc) length 3 eq {80 80 sq} if \
|
||
/nothing where {pop} {80 0 sq} ifelse {nonsense} stopped {90 90 moveto 95 95 lineto stroke} if",
|
||
);
|
||
let o = objects(&r.document);
|
||
// Four squares from the loop, one in the dictionary, one from get, one from length, one from
|
||
// where, the stroke after stopped.
|
||
assert_eq!(o.len(), 9, "{:?}", r.warnings);
|
||
assert!(near(bounds(&o[3]), Rect::new(60.0, 90.0, 70.0, 100.0)));
|
||
assert!(near(bounds(&o[4]), Rect::new(70.0, 20.0, 80.0, 30.0)));
|
||
}
|
||
|
||
#[test]
|
||
fn a_fill_then_a_stroke_of_the_same_path_is_one_object() {
|
||
let r = read("10 10 moveto 50 50 lineto 90 10 lineto closepath gsave 1 0 0 setrgbcolor fill grestore 0 0 1 setrgbcolor stroke");
|
||
let o = objects(&r.document);
|
||
assert_eq!(o.len(), 1);
|
||
assert_eq!(fill(&o[0]), Some(&Paint::solid(Color::rgb(1.0, 0.0, 0.0))));
|
||
assert_eq!(stroke(&o[0]).unwrap().paint, Paint::solid(Color::rgb(0.0, 0.0, 1.0)));
|
||
}
|
||
|
||
#[test]
|
||
fn clips_become_clipping_groups() {
|
||
let r = read("gsave 0 0 50 50 rectclip 0 0 100 100 rectfill 10 10 5 5 rectfill grestore 60 60 5 5 rectfill");
|
||
let o = objects(&r.document);
|
||
assert_eq!(o.len(), 2);
|
||
let NodeKind::Group { children, clip: true } = &o[0].kind else { panic!("{:?}", o[0].kind) };
|
||
assert_eq!(children.len(), 3);
|
||
assert!(matches!(children[0].kind, NodeKind::Path { clipping: true, .. }));
|
||
assert!(near(bounds(&children[0]), Rect::new(0.0, 50.0, 50.0, 100.0)));
|
||
// A clip around the whole page clips nothing.
|
||
let o = objects(&read("0 0 100 100 rectclip 1 1 5 5 rectfill").document);
|
||
assert!(matches!(o[0].kind, NodeKind::Path { .. }));
|
||
// eoclip of a path.
|
||
let o = objects(&read("10 10 moveto 30 10 lineto 30 30 lineto closepath eoclip newpath 0 0 50 50 rectfill").document);
|
||
assert!(matches!(o[0].kind, NodeKind::Group { clip: true, .. }));
|
||
// #1542: a diamond touching the page edges has the page as its bounding box but still clips.
|
||
let o = objects(&read("50 0 moveto 100 50 lineto 50 100 lineto 0 50 lineto closepath clip newpath 0 0 100 100 rectfill").document);
|
||
assert!(matches!(o[0].kind, NodeKind::Group { clip: true, .. }), "{:?}", o[0].kind);
|
||
// A turned square around the whole page clips nothing.
|
||
let o = objects(&read("50 -60 moveto 160 50 lineto 50 160 lineto -60 50 lineto closepath clip newpath 0 0 100 100 rectfill").document);
|
||
assert!(matches!(o[0].kind, NodeKind::Path { .. }), "{:?}", o[0].kind);
|
||
}
|
||
|
||
#[test]
|
||
fn clips_nest_only_so_deep() {
|
||
// Each clip inside the last one used to nest a clipping group one level deeper, past the stack.
|
||
let r = read("1 1 200000 { pop 0 0 50 50 rectclip } for 1 1 5 5 rectfill");
|
||
let mut depth = 0;
|
||
let mut n = objects(&r.document)[0].clone();
|
||
while let NodeKind::Group { children, clip: true } = &n.kind {
|
||
depth += 1;
|
||
n = children.last().unwrap().clone();
|
||
}
|
||
assert_eq!(depth, vectorcraft_doc::clipnest::MAX_NEST);
|
||
assert!(r.warnings.iter().any(|w| w.contains("clips nested more than")), "{:?}", r.warnings);
|
||
}
|
||
|
||
#[test]
|
||
fn shadings_become_gradients() {
|
||
let axial =
|
||
"<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> >>";
|
||
let r = read(&format!("gsave 10 10 moveto 90 10 lineto 50 90 lineto closepath clip newpath {axial} shfill grestore"));
|
||
let o = objects(&r.document);
|
||
// The clip path filled with the gradient.
|
||
assert_eq!(o.len(), 1, "{o:?}");
|
||
let Some(Paint::Gradient(g)) = fill(&o[0]) else { panic!("{:?}", o[0]) };
|
||
assert_eq!(g.gradient.kind, GradientKind::Linear);
|
||
assert_eq!(g.gradient.stops.len(), 2);
|
||
assert_eq!(g.gradient.stops[1].color, Color::rgb(0.0, 0.0, 1.0));
|
||
let geom = g.geom.unwrap();
|
||
assert_eq!((geom.start, geom.end), (Point::new(0.0, 100.0), Point::new(100.0, 100.0)));
|
||
// Stitched functions keep their bounds as stops; radial shadings, and shading patterns.
|
||
let radial = "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [50 50 0 50 50 40] /Function << /FunctionType 3 /Domain [0 1] /Bounds [0.25] /Encode [0 1 0 1] /Functions [<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> << /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] >> >>";
|
||
let r = read(&format!("<< /PatternType 2 /Shading {radial} >> matrix makepattern setpattern 0 0 100 100 rectfill"));
|
||
let o = objects(&r.document);
|
||
let Some(Paint::Gradient(g)) = fill(&o[0]) else { panic!() };
|
||
assert_eq!(g.gradient.kind, GradientKind::Radial);
|
||
let offsets: Vec<f32> = g.gradient.stops.iter().map(|s| s.offset).collect();
|
||
assert_eq!(offsets, vec![0.0, 0.25, 1.0]);
|
||
// Pieces that don't meet at a bound make a hard stop: the colour before the bound and the
|
||
// one after it, at the same offset (the PDF reader does the same).
|
||
let hard = "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 200 0] /Function << /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [<< /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >>] >> >>";
|
||
let r = read(&format!("0 0 200 50 rectclip {hard} shfill"));
|
||
let o = objects(&r.document);
|
||
let Some(Paint::Gradient(g)) = fill(&o[0]) else { panic!("{:?}", o[0]) };
|
||
let stops: Vec<(f32, Color)> = g.gradient.stops.iter().map(|s| (s.offset, s.color)).collect();
|
||
let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0));
|
||
assert_eq!(stops.len(), 4, "{stops:?}");
|
||
assert_eq!((stops[0], stops[3]), ((0.0, red), (1.0, blue)), "{stops:?}");
|
||
assert_eq!((stops[1].0, stops[2]), (0.5, (0.5, red)), "{stops:?}");
|
||
let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6);
|
||
assert!(near(stops[1].1.to_rgb(), blue.to_rgb()), "{stops:?}");
|
||
}
|
||
|
||
/// Tiling patterns and spot inks are found again through a hash of what tells them apart, so a
|
||
/// program that makes 200,000 patterns and 200,001 inks reads within [`read_soon`]'s 20 seconds.
|
||
/// Patterns are named Pattern 1, Pattern 2 and so on in the order they are made. A pattern made
|
||
/// again from its prototype is the same pattern, and so is an uncolored pattern set again in a
|
||
/// color it was set in before.
|
||
#[test]
|
||
fn many_tiling_patterns_and_spot_inks_read_in_linear_time() {
|
||
let r = read(
|
||
"/pp { pop 0 0 1 1 rectfill } def \
|
||
/a << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 2 /YStep 2 /PaintProc /pp load >> def \
|
||
/b << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 3 /YStep 2 /PaintProc /pp load >> def \
|
||
/u << /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 1 1] /XStep 2 /YStep 2 /PaintProc /pp load >> matrix makepattern def \
|
||
a matrix makepattern setpattern 0 0 10 10 rectfill b matrix makepattern setpattern 10 0 10 10 rectfill \
|
||
a [2 0 0 2 0 0] makepattern setpattern 20 0 10 10 rectfill /DeviceRGB setcolorspace \
|
||
1 0 0 u setpattern 30 0 10 10 rectfill 0 0 1 u setpattern 40 0 10 10 rectfill 1 0 0 u setpattern 50 0 10 10 rectfill",
|
||
);
|
||
let d = &r.document;
|
||
let names: Vec<&str> = d.patterns.iter().map(|p| p.name.as_str()).collect();
|
||
assert_eq!(names, ["Pattern 1", "Pattern 2", "Pattern 3", "Pattern 4"]);
|
||
let fills: Vec<String> = objects(d)
|
||
.iter()
|
||
.map(|o| match fill(o) {
|
||
Some(Paint::Pattern { pattern, .. }) => pattern.clone(),
|
||
p => panic!("{p:?}"),
|
||
})
|
||
.collect();
|
||
assert_eq!(fills, ["Pattern 1", "Pattern 2", "Pattern 1", "Pattern 3", "Pattern 4", "Pattern 3"]);
|
||
assert!(names.iter().all(|n| d.swatch(n).is_some_and(|s| matches!(s.paint, Paint::Pattern { .. }))));
|
||
// 100,000 patterns whose cells differ, an uncolored pattern in 100,000 tints of an ink that is
|
||
// white at every tint (only the tint tells those patterns apart), and 200,000 more inks.
|
||
let r = read_soon(
|
||
"0 1 99999 { /i exch def << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep i 2 add /YStep 1 \
|
||
/PaintProc { pop 0 0 1 1 rectfill } >> matrix makepattern setpattern } for \
|
||
[/Separation (Spot) /DeviceGray { pop 1 }] setcolorspace \
|
||
/p << /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 1 1] /XStep 2 /YStep 2 /PaintProc { pop 0 0 1 1 rectfill } >> \
|
||
matrix makepattern def 0 1 99999 { 100000 div p setpattern } for \
|
||
0 1 199999 { /i exch def [/Separation i 8 string cvs /DeviceGray {}] setcolorspace } for 0 0 1 1 rectfill",
|
||
);
|
||
let d = &r.document;
|
||
assert_eq!(d.patterns.len(), 200_000);
|
||
assert!(d.patterns.iter().enumerate().all(|(i, p)| p.name == format!("Pattern {}", i + 1)));
|
||
assert_eq!(d.swatches.iter().filter(|s| matches!(s.paint, Paint::Pattern { .. })).count(), 200_000);
|
||
assert_eq!(d.swatches.iter().filter(|s| s.spot).count(), 200_001);
|
||
}
|
||
|
||
#[test]
|
||
fn images_and_image_masks() {
|
||
// 2 × 2 RGB from hex data in the file, through a procedure.
|
||
let r = read(
|
||
"10 10 translate 20 20 scale 2 2 8 [2 0 0 -2 0 2] {currentfile 6 string readhexstring pop} false 3 colorimage\nff000000ff000000ffffffff\n",
|
||
);
|
||
let o = objects(&r.document);
|
||
let NodeKind::Image(im) = &o[0].kind else { panic!("{:?} {:?}", o, r.warnings) };
|
||
assert_eq!((im.width, im.height), (2, 2));
|
||
assert!(near(bounds(&o[0]), Rect::new(10.0, 70.0, 30.0, 90.0)), "{:?}", bounds(&o[0]));
|
||
let png = image::load_from_memory(&r.document.images[&im.key].bytes).unwrap().to_rgba8();
|
||
assert_eq!(png.get_pixel(0, 0).0, [255, 0, 0, 255]);
|
||
assert_eq!(png.get_pixel(1, 1).0, [255, 255, 255, 255]);
|
||
// The dictionary form through ASCII85 and run-length filters, grey with a decode array.
|
||
let rl = crate::ps::ascii85(&[1, 0, 255, 128]);
|
||
let r = read(&format!(
|
||
"/src currentfile /ASCII85Decode filter /RunLengthDecode filter def /DeviceGray setcolorspace \
|
||
<< /ImageType 1 /Width 2 /Height 1 /BitsPerComponent 8 /Decode [1 0] /ImageMatrix [2 0 0 1 0 0] /DataSource src >> image\n{rl}"
|
||
));
|
||
let NodeKind::Image(im) = &objects(&r.document)[0].kind else { panic!("{:?}", r.warnings) };
|
||
let png = image::load_from_memory(&r.document.images[&im.key].bytes).unwrap().to_rgba8();
|
||
assert_eq!((png.get_pixel(0, 0).0, png.get_pixel(1, 0).0), ([255, 255, 255, 255], [0, 0, 0, 255]));
|
||
// An image mask paints the current colour where its bits say.
|
||
let r = read("1 0 0 setrgbcolor 8 1 true [8 0 0 1 0 0] <f0> imagemask");
|
||
let NodeKind::Image(im) = &objects(&r.document)[0].kind else { panic!() };
|
||
let png = image::load_from_memory(&r.document.images[&im.key].bytes).unwrap().to_rgba8();
|
||
assert_eq!((png.get_pixel(0, 0).0, png.get_pixel(7, 0).0), ([255, 0, 0, 255], [255, 0, 0, 0]));
|
||
}
|
||
|
||
#[test]
|
||
fn type_becomes_point_type_in_the_font_named() {
|
||
let r = read("/Helvetica-Bold findfont 12 scalefont setfont 10 20 moveto (Hi) show (!) show");
|
||
let o = objects(&r.document);
|
||
assert_eq!(o.len(), 2);
|
||
let NodeKind::Text(t) = &o[0].kind else { panic!() };
|
||
assert_eq!(t.plain_text(), "Hi");
|
||
let style = &t.runs[0].style;
|
||
assert_eq!((style.font_family.as_str(), style.font_style.as_str()), ("Helvetica", "Bold"));
|
||
assert!((style.size - 12.0).abs() < 1e-6);
|
||
// On the baseline at (10, 80) in the document, upright.
|
||
assert!((t.xf.translation().x - 10.0).abs() < 1e-6 && (t.xf.translation().y - 80.0).abs() < 1e-6, "{:?}", t.xf);
|
||
assert!((t.xf.as_coeffs()[0] - 1.0).abs() < 1e-6 && (t.xf.as_coeffs()[3] - 1.0).abs() < 1e-6, "{:?}", t.xf);
|
||
// The second show starts where the first ended.
|
||
let NodeKind::Text(t2) = &o[1].kind else { panic!() };
|
||
assert!(t2.xf.translation().x > 15.0);
|
||
assert_eq!(crate::family_style("ABCDEF+TimesNewRomanPS-BoldItalicMT"), ("Times New Roman".into(), "Bold Italic".into()));
|
||
assert_eq!(crate::family_style("Times-Roman"), ("Times".into(), "Regular".into()));
|
||
}
|
||
|
||
#[test]
|
||
fn the_writers_own_output_reads_back() {
|
||
use vectorcraft_doc::{Appearance, Dash};
|
||
use vectorcraft_geom::shapes;
|
||
let mut d = Document::new(200.0, 100.0);
|
||
let layer = d.layers[0].id;
|
||
let look = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::solid(Color::rgb(0.0, 0.0, 1.0)), 4.0);
|
||
let mut red = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 90.0, 60.0)), look);
|
||
if let Some(AppearanceItem::Stroke(s)) = red.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Stroke(_))) {
|
||
s.dash = Some(Dash { pattern: vec![6.0, 3.0], offset: 0.0, align_corners: false });
|
||
s.cap = LineCap::Round;
|
||
}
|
||
d.insert(Some(layer), 0, red).unwrap();
|
||
let mut g = vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient::default());
|
||
g.geom = Some(vectorcraft_color::GradientGeom { start: Point::new(110.0, 50.0), end: Point::new(190.0, 50.0), aspect: 1.0, focal: None });
|
||
let grad = Node::path(
|
||
d.alloc_id(),
|
||
shapes::ellipse(Rect::new(110.0, 10.0, 190.0, 90.0)),
|
||
Appearance::basic(Paint::Gradient(Box::new(g)), Paint::None, 0.0),
|
||
);
|
||
d.insert(Some(layer), 1, grad).unwrap();
|
||
for level in [Level::Three, Level::Two] {
|
||
let r = d.artboards[0].rect;
|
||
let o = EpsOptions { region: r, origin: Point::new(r.x0, r.y1), preview: Preview::None, level, ..EpsOptions::default() };
|
||
let bytes = crate::export(&d, &o, None, None).unwrap().bytes;
|
||
let back = import(&bytes).unwrap();
|
||
assert!(back.warnings.is_empty(), "{level:?}: {:?}", back.warnings);
|
||
assert_eq!(back.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0));
|
||
let objs = all(&back.document);
|
||
let rect = objs.iter().find(|n| fill(n).is_some_and(|p| p.color() == Some(Color::rgb(1.0, 0.0, 0.0)))).unwrap();
|
||
assert!(near(bounds(rect), Rect::new(10.0, 10.0, 90.0, 60.0)), "{:?}", bounds(rect));
|
||
let st = objs.iter().find_map(|n| stroke(n)).unwrap();
|
||
assert_eq!((st.width, st.cap, st.dash.as_ref().unwrap().pattern.clone()), (4.0, LineCap::Round, vec![6.0, 3.0]));
|
||
// Level 3 writes the gradient as a shading (a gradient again); level 2 as bands.
|
||
let gradients = objs.iter().filter(|n| matches!(fill(n), Some(Paint::Gradient(_)))).count();
|
||
assert_eq!(gradients, usize::from(level == Level::Three), "{level:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn an_unreadable_program_falls_back_to_the_preview() {
|
||
let ps = eps("0 0 10 10 rectfill frobnicate");
|
||
let tiff = crate::tiff::encode(&Raster { width: 4, height: 4, rgba: [0, 128, 0, 255].repeat(16) }, false, false).unwrap();
|
||
let bytes = crate::dos_eps(&ps, &tiff).unwrap();
|
||
let r = import(&bytes).unwrap();
|
||
assert!(r.preview);
|
||
assert!(r.warnings[0].contains("frobnicate") && r.warnings[0].contains("preview"), "{:?}", r.warnings);
|
||
let o = objects(&r.document);
|
||
let NodeKind::Image(im) = &o[0].kind else { panic!() };
|
||
assert_eq!((im.width, im.height), (4, 4));
|
||
assert!(near(bounds(&o[0]), Rect::new(0.0, 0.0, 100.0, 100.0)));
|
||
// Without a preview, what was drawn before the error stays, with a warning.
|
||
let r = import(&ps).unwrap();
|
||
assert!(!r.preview && objects(&r.document).len() == 1);
|
||
assert!(r.warnings[0].contains("frobnicate"), "{:?}", r.warnings);
|
||
}
|
||
|
||
#[test]
|
||
fn garbage_and_runaway_programs_are_refused() {
|
||
assert!(import(b"").is_err());
|
||
assert!(import(b"hello world").is_err());
|
||
assert!(import(&[0xC5, 0xD0, 0xD3, 0xC6, 0xff, 0xff]).is_err());
|
||
let e = import(b"%!PS\nfoo bar").unwrap_err();
|
||
assert!(e.contains("foo"), "{e}");
|
||
for body in ["{} loop", "/f {f} def f", "0 1 1e300 {pop} for", "[ 0 1 200000 {} for ]", "1000000000 array", "(abc", "{ 1 2"] {
|
||
assert!(import(&eps(body)).is_err(), "{body}");
|
||
}
|
||
// Damaged numbers draw nothing far away.
|
||
let r = read("1e300 1e300 moveto 1e301 0 lineto stroke 0 0 1 1 rectfill");
|
||
assert_eq!(objects(&r.document).len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn the_bounding_box_comes_from_the_header_or_the_trailer() {
|
||
let d = Dsc::read(b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 1 2 30 40\n%%HiResBoundingBox: 1.5 2.5 29.5 39.5\n%%EndComments\n");
|
||
assert_eq!(d.bbox, Some([1.5, 2.5, 29.5, 39.5]));
|
||
let d = Dsc::read(b"%!PS-Adobe-3.0\n%%BoundingBox: (atend)\n%%Pages: 3\n%%EndComments\nshowpage\n%%Trailer\n%%BoundingBox: 0 0 10 20\n");
|
||
assert_eq!((d.bbox, d.pages), (Some([0.0, 0.0, 10.0, 20.0]), Some(3)));
|
||
let r = import(b"%!PS-Adobe-3.0\n%%Pages: 2\n0 0 5 5 rectfill showpage 0 0 9 9 rectfill showpage\n").unwrap();
|
||
// No bounding box: a letter page; only the first page.
|
||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 612.0, 792.0));
|
||
assert_eq!(objects(&r.document).len(), 1);
|
||
assert!(r.warnings.iter().any(|w| w.contains("first of the file's 2 pages")), "{:?}", r.warnings);
|
||
}
|
||
|
||
/// #1552: a Type 42 font's glyph space is one unit an em, so its identity FontMatrix scaled to 12
|
||
/// sets 12 pt type (a Type 1's 0.001 matrix needs the 1000-unit space).
|
||
#[test]
|
||
fn type_42_fonts_set_type_at_their_size() {
|
||
for (kind, matrix) in [(42, "[1 0 0 1 0 0]"), (1, "[0.001 0 0 0.001 0 0]")] {
|
||
let body = format!(
|
||
"/T 10 dict begin /FontName /T def /FontType {kind} def /FontMatrix {matrix} def /Encoding StandardEncoding def currentdict end /T exch definefont pop \
|
||
/T findfont 12 scalefont setfont 10 20 moveto (Hi) show"
|
||
);
|
||
let r = read(&body);
|
||
let NodeKind::Text(t) = &objects(&r.document)[0].kind else { panic!("{:?}", r.warnings) };
|
||
let size = t.runs[0].style.size * t.xf.determinant().abs().sqrt();
|
||
assert!((size - 12.0).abs() < 1e-6, "FontType {kind}: {size} pt");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn embedded_font_programs_are_skipped() {
|
||
let body = "/MyFont 10 dict begin /FontName /MyFont def /FontMatrix [0.001 0 0 0.001 0 0] def currentdict end currentfile eexec\n\
|
||
0123456789abcdef garbage that is encrypted\n0000000000000000000000000000000000000000000000000000000000000000\ncleartomark\n\
|
||
/MyFont findfont 10 scalefont setfont 10 10 moveto (x) show";
|
||
let r = read(body);
|
||
let NodeKind::Text(t) = &objects(&r.document)[0].kind else { panic!("{:?}", r.warnings) };
|
||
assert_eq!(t.runs[0].style.font_family, "My Font");
|
||
}
|
||
|
||
/// A document with what the writer writes in its own ways: gradients, type, an image with
|
||
/// transparent pixels, a clipping group, a spot colour, dashes.
|
||
fn rich_doc() -> Document {
|
||
use vectorcraft_color::{Gradient, GradientGeom, GradientPaint, Swatch};
|
||
use vectorcraft_doc::{Appearance, CharStyle, ImageBlob, ImageObject, TextObject};
|
||
use vectorcraft_geom::{Affine, shapes};
|
||
let mut d = Document::new(300.0, 200.0);
|
||
let layer = d.layers[0].id;
|
||
let add = |d: &mut Document, n: Node| d.insert(Some(layer), usize::MAX, n).unwrap();
|
||
let solid = |r, g, b| Paint::solid(Color::rgb(r, g, b));
|
||
let n = Node::path(
|
||
d.alloc_id(),
|
||
shapes::rectangle(Rect::new(10.0, 10.0, 90.0, 60.0)),
|
||
Appearance::basic(solid(1.0, 0.0, 0.0), solid(0.0, 0.0, 1.0), 4.0),
|
||
);
|
||
add(&mut d, n);
|
||
for (kind, r) in [(GradientKind::Linear, Rect::new(110.0, 10.0, 190.0, 90.0)), (GradientKind::Radial, Rect::new(200.0, 10.0, 290.0, 90.0))] {
|
||
let mut g = GradientPaint::new(Gradient { kind, ..Gradient::default() });
|
||
g.geom = Some(GradientGeom::fit(kind, r, 0.0));
|
||
let n = Node::path(d.alloc_id(), shapes::ellipse(r), Appearance::basic(Paint::Gradient(Box::new(g)), Paint::None, 0.0));
|
||
add(&mut d, n);
|
||
}
|
||
let style = CharStyle { size: 30.0, fill: solid(0.0, 0.5, 0.0), stroke: Paint::None, ..CharStyle::default() };
|
||
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 120.0), "Type", style))));
|
||
add(&mut d, n);
|
||
let mut img = image::RgbaImage::new(8, 4);
|
||
for (x, _, p) in img.enumerate_pixels_mut() {
|
||
*p = image::Rgba(if x < 4 { [0, 0, 255, 255] } else { [255, 200, 0, 255] });
|
||
}
|
||
let mut png = vec![];
|
||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||
d.images.insert("pic".into(), ImageBlob::new("image/png", png));
|
||
let im = ImageObject {
|
||
key: "pic".into(),
|
||
width: 8,
|
||
height: 4,
|
||
xf: Affine::new([10.0, 0.0, 0.0, 10.0, 110.0, 110.0]),
|
||
link: None,
|
||
placement: Default::default(),
|
||
};
|
||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||
add(&mut d, n);
|
||
d.swatches.push(Swatch { name: "Spot".into(), paint: Paint::solid(Color::cmyk(0.0, 0.5, 1.0, 0.0)), global: true, spot: true });
|
||
let spot = Paint::Solid { color: Color::cmyk(0.0, 0.5, 1.0, 0.0), swatch: Some("Spot".into()), tint: 1.0 };
|
||
let mut clip = Node::path(d.alloc_id(), shapes::ellipse(Rect::new(200.0, 110.0, 290.0, 190.0)), Appearance::default());
|
||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||
*clipping = true;
|
||
}
|
||
let mut inner = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(190.0, 100.0, 300.0, 150.0)), Appearance::basic(spot, Paint::None, 0.0));
|
||
inner.appearance.items.push(AppearanceItem::Stroke({
|
||
let mut st = vectorcraft_doc::StrokeLayer::new(solid(0.0, 0.0, 0.0), 2.0);
|
||
st.dash = Some(vectorcraft_doc::Dash { pattern: vec![4.0, 2.0], offset: 0.0, align_corners: false });
|
||
st
|
||
}));
|
||
let g = Node::new(d.alloc_id(), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(inner)], clip: true });
|
||
add(&mut d, g);
|
||
d
|
||
}
|
||
|
||
#[test]
|
||
fn the_writers_output_reads_back_looking_the_same() {
|
||
let d = rich_doc();
|
||
let r = d.artboards[0].rect;
|
||
let mut renderer = vectorcraft_render::Renderer::new();
|
||
let want = renderer.render_region(&d, r, 1.0, true);
|
||
for level in [Level::Three, Level::Two] {
|
||
let o = EpsOptions { region: r, origin: Point::new(r.x0, r.y1), preview: Preview::None, level, ..EpsOptions::default() };
|
||
let back = import(&crate::export(&d, &o, None, None).unwrap().bytes).unwrap();
|
||
assert!(back.warnings.is_empty(), "{level:?}: {:?}", back.warnings);
|
||
assert!(back.document.swatch("Spot").is_some_and(|s| s.spot), "{level:?}");
|
||
let got = renderer.render_region(&back.document, back.document.artboards[0].rect, 1.0, true);
|
||
assert_eq!((got.width, got.height), (want.width, want.height));
|
||
// Pixels that differ visibly: anti-aliased edges, gradient steps at level 2.
|
||
let off = want.pixels.chunks(4).zip(got.pixels.chunks(4)).filter(|(a, b)| a.iter().zip(b.iter()).any(|(x, y)| x.abs_diff(*y) > 48)).count();
|
||
assert!(off * 100 < want.pixels.len() / 4, "{level:?}: {off} pixels differ");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn device_settings_are_accepted_and_change_nothing() {
|
||
let r = read(
|
||
"currentscreen setscreen currenttransfer settransfer currentcolortransfer setcolortransfer currenthalftone sethalftone \
|
||
currentblackgeneration setblackgeneration currentundercolorremoval setundercolorremoval 1 setflat true setstrokeadjust \
|
||
<< /PageSize [100 100] >> setpagedevice 1000 setcachelimit mark 1 2 setucacheparams ucache /StandardEncoding findencoding pop \
|
||
0 0 5 5 rectfill",
|
||
);
|
||
assert_eq!(objects(&r.document).len(), 1, "{:?}", r.warnings);
|
||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||
}
|
||
|
||
/// Check that `body` draws one square, blue: it sets blue when what it checks holds, else red.
|
||
fn blue(body: &str) {
|
||
let r = read(&format!("{body} 10 10 80 80 rectfill"));
|
||
let o = objects(&r.document);
|
||
assert_eq!(o.len(), 1, "{body}: {:?}", r.warnings);
|
||
assert_eq!(fill(&o[0]), Some(&Paint::solid(Color::rgb(0.0, 0.0, 1.0))), "{body}");
|
||
assert!(r.warnings.is_empty(), "{body}: {:?}", r.warnings);
|
||
}
|
||
|
||
/// Check that PostScript `cond` leaves `true`.
|
||
pub(super) fn check(cond: &str) {
|
||
blue(&format!("{cond} {{ 0 0 1 setrgbcolor }} {{ 1 0 0 setrgbcolor }} ifelse"));
|
||
}
|
||
|
||
/// #3547: a line before counted metadata ends in CR/LF. Leaving LF unread shifts the
|
||
/// metadata by one byte and makes its final `>` look like PostScript instead of data.
|
||
#[test]
|
||
fn readline_keeps_counted_metadata_aligned_for_every_line_ending() {
|
||
let tiff = crate::tiff::encode(&Raster { width: 4, height: 4, rgba: [0, 128, 0, 255].repeat(16) }, false, false).unwrap();
|
||
for eol in ["\n", "\r", "\r\n"] {
|
||
let ps = eps(&format!(
|
||
"/consume {{ currentfile 80 string readline pop pop currentfile 4 string readstring pop pop }} bind def\n\
|
||
consume\nheader{eol}<x/>\n0 0 1 setrgbcolor 10 20 30 40 rectfill"
|
||
));
|
||
for bytes in [ps.clone(), crate::dos_eps(&ps, &tiff).unwrap()] {
|
||
let r = import(&bytes).unwrap();
|
||
assert!(!r.preview, "valid art must not become the low-resolution TIFF preview");
|
||
assert!(r.warnings.is_empty(), "{eol:?}: {:?}", r.warnings);
|
||
let art = objects(&r.document);
|
||
assert_eq!(art.len(), 1);
|
||
assert!(matches!(art[0].kind, NodeKind::Path { .. }));
|
||
assert_eq!(fill(&art[0]), Some(&Paint::solid(Color::rgb(0.0, 0.0, 1.0))));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// CR/LF is one terminator in buffered and decoded streams too; lone CR never consumes
|
||
/// the next payload byte, EOF is safe, and ordinary `readstring` preserves binary CR/LF.
|
||
#[test]
|
||
fn readline_streams_preserve_the_next_payload_byte() {
|
||
for eol in ["\n", "\r", "\r\n"] {
|
||
let bytes = format!("header{eol}XY");
|
||
let hex: String = bytes.bytes().map(|b| format!("{b:02x}")).collect();
|
||
for source in [format!("<{hex}> /ReusableStreamDecode filter"), format!("({hex}>) /ASCIIHexDecode filter")] {
|
||
check(&format!(
|
||
"/f {source} def f 32 string readline exch (header) eq and \
|
||
f 2 string readstring exch (XY) eq and and"
|
||
));
|
||
}
|
||
}
|
||
check("/f <6865616465720d> /ReusableStreamDecode filter def f 32 string readline exch (header) eq and f read not and");
|
||
check("/f <0d0a5859> /ReusableStreamDecode filter def f 4 string readstring exch <0d0a5859> eq and");
|
||
}
|
||
|
||
#[test]
|
||
fn the_generic_category_defines_new_categories() {
|
||
// #234: the reference app's prolog.
|
||
blue("/Generic /Category findresource pop 0 0 1 setrgbcolor");
|
||
blue(
|
||
"[/CSA /Gradient /Procedure] { /Generic /Category findresource dup length dict copy /Category defineresource pop } forall 0 0 1 setrgbcolor",
|
||
);
|
||
let define = "[/CSA /Gradient] { /Generic /Category findresource dup length dict copy /Category defineresource pop } forall \
|
||
/G1 << /Kind 7 >> /Gradient defineresource pop";
|
||
check(&format!("{define} /G1 /Gradient findresource /Kind get 7 eq"));
|
||
check(&format!("{define} /Gradient /Category resourcestatus {{ pop pop true }} {{ false }} ifelse"));
|
||
check(&format!("{define} /G1 /Gradient resourcestatus {{ pop pop true }} {{ false }} ifelse"));
|
||
check(&format!("{define} /G1 /Gradient undefineresource /G1 /Gradient resourcestatus not"));
|
||
check(&format!("{define} {{ /G2 /Gradient findresource }} stopped"));
|
||
// Unknown categories aren't instances of Category; a category is a dictionary.
|
||
check("/Nonsense /Category resourcestatus not");
|
||
check("{ /Nonsense /Category findresource } stopped");
|
||
check("{ /X 5 /Category defineresource } stopped");
|
||
}
|
||
|
||
#[test]
|
||
fn clipsave_and_cliprestore_bring_the_clip_back() {
|
||
// #234.
|
||
blue("clipsave 0 0 50 50 rectclip cliprestore 0 0 1 setrgbcolor");
|
||
// The fill after cliprestore covers the whole square, unclipped.
|
||
let r = read("clipsave 0 0 50 50 rectclip 1 0 0 setrgbcolor 0 0 10 10 rectfill cliprestore 0 0 1 setrgbcolor 10 10 80 80 rectfill");
|
||
let o = objects(&r.document);
|
||
assert_eq!(o.len(), 2, "{:?}", r.warnings);
|
||
assert!(matches!(o[0].kind, NodeKind::Group { clip: true, .. }), "{:?}", o[0].kind);
|
||
assert!(matches!(o[1].kind, NodeKind::Path { clipping: false, .. }), "{:?}", o[1].kind);
|
||
assert!(near(bounds(&o[1]), Rect::new(10.0, 10.0, 90.0, 90.0)), "{:?}", bounds(&o[1]));
|
||
// Nested, leaving the rest of the graphics state alone.
|
||
let r = read(
|
||
"0 0 60 60 rectclip clipsave 0 0 20 20 rectclip clipsave 0 0 5 5 rectclip cliprestore 0 1 0 setrgbcolor cliprestore \
|
||
10 10 80 80 rectfill",
|
||
);
|
||
let NodeKind::Group { children, clip: true } = &objects(&r.document)[0].kind else { panic!("{:?}", r.warnings) };
|
||
assert!(near(bounds(&children[0]), Rect::new(0.0, 40.0, 60.0, 100.0)), "{:?}", bounds(&children[0]));
|
||
assert_eq!(fill(&children[1]), Some(&Paint::solid(Color::rgb(0.0, 1.0, 0.0))));
|
||
// grestore undoes the clipsaves after its gsave; cliprestore with nothing saved does nothing.
|
||
let o = objects(&read("cliprestore gsave clipsave 0 0 50 50 rectclip grestore cliprestore 10 10 80 80 rectfill").document);
|
||
assert!(matches!(o[0].kind, NodeKind::Path { .. }), "{:?}", o[0].kind);
|
||
}
|
||
|
||
#[test]
|
||
fn languagelevel_is_an_integer_and_runs() {
|
||
// #234.
|
||
blue(
|
||
"/Level2? systemdict /languagelevel known dup { pop systemdict /languagelevel get 2 ge } if def \
|
||
Level2? { 0 0 1 setrgbcolor } { 1 0 0 setrgbcolor } ifelse",
|
||
);
|
||
check("languagelevel 3 eq");
|
||
check("{ languagelevel } bind exec 3 eq");
|
||
}
|
||
|
||
#[test]
|
||
fn intervals_share_their_arrays_and_strings() {
|
||
// #234: astore into a subarray stores into the array.
|
||
blue("/a 4 array def 1 2 a 0 2 getinterval astore pop a 0 get 1 eq { 0 0 1 setrgbcolor } { 1 0 0 setrgbcolor } ifelse");
|
||
// put, putinterval and copy into an interval; an interval of an interval.
|
||
check("/a [0 0 0 0 0] def a 1 3 getinterval 1 2 getinterval 0 7 put a 2 get 7 eq");
|
||
check("/a [0 0 0 0] def a 2 2 getinterval 0 [8 9] putinterval a 3 get 9 eq");
|
||
check("/a [0 0 0 0] def [5 6] a 1 2 getinterval copy pop a 2 get 6 eq");
|
||
check("/s (abcd) def s 1 2 getinterval 0 (XY) putinterval s (aXYd) eq");
|
||
check("/s (abcd) def s 2 1 getinterval 0 90 put s (abZd) eq");
|
||
check("/s 8 string def 42 s 2 6 getinterval cvs pop s 2 2 getinterval (42) eq");
|
||
// The same interval is the same object; another one is not.
|
||
check("/a 4 array def a 0 2 getinterval a 0 2 getinterval eq");
|
||
check("/a 4 array def a 0 2 getinterval a 0 3 getinterval ne a 0 2 getinterval a 1 2 getinterval ne and");
|
||
// Reads and writes stay inside the interval.
|
||
check("/a [1 2 3 4] def { a 1 2 getinterval 2 get } stopped { a 1 2 getinterval 2 0 put } stopped and");
|
||
check("/a [1 2 3 4] def a 1 2 getinterval length 2 eq a 1 2 getinterval aload pop 3 eq exch 2 eq and and");
|
||
check("/n 0 def [1 2 3 4] 1 2 getinterval { n add /n exch def } forall n 5 eq");
|
||
// A procedure from an interval runs only its part.
|
||
check("{ 1 2 3 } 1 2 getinterval cvx exec add 5 eq");
|
||
// The prolog's stack save: the operands into part of one array, and back.
|
||
check(
|
||
"/stk 10 array def /lev 3 array def 1 2 3 count dup lev exch 0 exch put stk exch 0 exch getinterval astore pop \
|
||
clear stk 0 lev 0 get getinterval aload pop add add 6 eq",
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn resourceforall_enumerates_matching_names() {
|
||
// #234.
|
||
blue("(*) { pop } 128 string /Category resourceforall 0 0 1 setrgbcolor");
|
||
let count = |template: &str| format!("/n 0 def ({template}) {{ pop /n n 1 add def }} 128 string /ProcSet resourceforall n");
|
||
let define = "[/Foo /Fop /Bar /F*x] { 1 dict /ProcSet defineresource pop } forall";
|
||
check(&format!("{define} {} 4 eq", count("*")));
|
||
check(&format!("{define} {} 2 eq", count("Fo?")));
|
||
check(&format!("{define} {} 3 eq", count("F*")));
|
||
check(&format!("{define} {} 1 eq", count("F\\\\*x")));
|
||
check(&format!("{define} {} 0 eq", count("Q*")));
|
||
check(&format!("{define} {} 1 eq", count("*a*")));
|
||
// Each name is copied into the scratch string; exit stops.
|
||
check(&format!("{define} /last () def (B*) {{ dup length string copy /last exch def }} 8 string /ProcSet resourceforall last (Bar) eq"));
|
||
check(&format!("{define} /n 0 def (*) {{ pop /n n 1 add def exit }} 8 string /ProcSet resourceforall n 1 eq"));
|
||
// A scratch string too small for the names, as the prolog probes it.
|
||
check(&format!("{define} {{ (*) {{ pop }} 2 string /ProcSet resourceforall }} stopped"));
|
||
check("/n 0 def (Gen*) { pop /n n 1 add def } 16 string /Category resourceforall n 1 eq");
|
||
}
|
||
|
||
#[test]
|
||
fn the_standard_dictionaries_are_dictionaries_in_systemdict() {
|
||
// The reference app's EPS prolog looks them up by name rather than running them.
|
||
check("systemdict /userdict get type /dicttype eq");
|
||
check("/globaldict where { /globaldict get begin currentdict globaldict eq end } { false } ifelse");
|
||
check("systemdict /systemdict get systemdict eq systemdict /statusdict known and");
|
||
check("userdict /x 5 put systemdict /userdict get /x get 5 eq");
|
||
check("/errordict load /handleerror known errordict $error eq and");
|
||
check("SharedFontDirectory type /dicttype eq");
|
||
}
|
||
|
||
#[test]
|
||
fn dictstack_and_internaldict_answer() {
|
||
check("10 array dictstack length countdictstack eq");
|
||
check("userdict begin 10 array dictstack dup length 1 sub get userdict eq end");
|
||
check("{ 1 array dictstack } stopped");
|
||
check("1183615869 internaldict /k 1 put 1183615869 internaldict /k get 1 eq");
|
||
check("currentsystemparams /MaxPatternCache get type /integertype eq");
|
||
check("<< /MaxPatternItem currentsystemparams /MaxPatternCache get >> setuserparams currentuserparams /MaxFontItem known");
|
||
}
|
||
|
||
#[test]
|
||
fn an_executable_filter_runs_its_data_as_a_program() {
|
||
// A string run as a program through a filter (0 and an empty string set no end of data).
|
||
check("(1 2 add) 0 () /SubFileDecode filter cvx exec 3 eq");
|
||
check("(1 2 add) 0 () /SubFileDecode filter dup xcheck not exch cvx xcheck and");
|
||
check("(abcd) 2 () /SubFileDecode filter 8 string readstring pop (ab) eq");
|
||
check("{ (1 add) 0 () /SubFileDecode filter cvx exec } stopped");
|
||
// The data following it in the file: `74727565` is `true`.
|
||
check("currentfile /ASCIIHexDecode filter cvx exec 74727565>");
|
||
// A literal file is only pushed.
|
||
check("(1 2 add) 0 () /SubFileDecode filter exec type /filetype eq");
|
||
}
|
||
|
||
#[test]
|
||
fn flushfile_skips_the_data_of_a_filter() {
|
||
// Metadata that isn't PostScript, skipped up to its end marker.
|
||
blue("currentfile 0 (%%EndPacket) /SubFileDecode filter flushfile\n<?xml version=\"1.0\"?><x a=\"(\"/>\n%%EndPacket\n0 0 1 setrgbcolor");
|
||
}
|
||
|
||
/// Executable files are untrusted programs too: running one inside itself stops at the nesting
|
||
/// limit, and the program's own file runs once. Type laid out in a loop (`cshow` on a long
|
||
/// string, `stringwidth` in `loop`) counts against the operation budget like the loop itself.
|
||
#[test]
|
||
fn hostile_executable_files_and_type_loops_are_refused() {
|
||
let refused = |body: &str, why: &str| {
|
||
let e = import(&eps(body)).err().unwrap_or_default();
|
||
assert!(e.contains(why), "{body}: {e}");
|
||
};
|
||
refused("/p { (p) 0 () /SubFileDecode filter cvx exec } def p", "too deeply");
|
||
refused("/p { (p) 0 () /SubFileDecode filter cvx exec } def { p } stopped pop p", "too deeply");
|
||
refused("/a { (b) 0 () /SubFileDecode filter cvx exec } def /b { (a) 0 () /SubFileDecode filter cvx exec } def a", "too deeply");
|
||
refused("/Helvetica findfont 10 scalefont setfont { pop pop pop } 1000000 string cshow", "too long");
|
||
// The program's own file, made executable, is already running.
|
||
blue("currentfile cvx exec currentfile flushfile 0 0 1 setrgbcolor");
|
||
}
|
||
|
||
/// Reads `body` as [`read`] does, on a thread of its own, and fails the test when reading takes
|
||
/// more than 20 s.
|
||
fn read_soon(body: &str) -> crate::Imported {
|
||
let (tx, rx) = std::sync::mpsc::channel();
|
||
let ps = eps(body);
|
||
std::thread::spawn(move || tx.send(import(&ps)).ok());
|
||
rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|_| panic!("reading {body} did not finish")).unwrap()
|
||
}
|
||
|
||
/// Reads `body` after a square through [`read_soon`]: the square stays, and the program runs out
|
||
/// of the operation budget.
|
||
fn runs_out_of_budget(body: &str) {
|
||
let r = read_soon(&format!("0 0 10 10 rectfill {body}"));
|
||
assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings);
|
||
assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{body}: {:?}", r.warnings);
|
||
}
|
||
|
||
/// A loop of operators whose work grows with the path they make runs out of the operation budget
|
||
/// (PLRM 3rd ed., chapter 8): `arc` and `arct` with a huge radius, which make thousands of curve
|
||
/// segments, `strokepath` with a dash far finer than the line, dashes of length zero included, and
|
||
/// `rectfill` with an array of 2,000 rectangles.
|
||
#[test]
|
||
fn paths_made_in_a_loop_run_out_of_the_budget() {
|
||
for body in [
|
||
"{ newpath 0 0 2.2e18 0 1440 arc } loop",
|
||
"{ newpath 0 0 moveto 1 0 1 1 1e30 arct } loop",
|
||
"[1] 0 setdash { newpath 0 0 moveto 9900 0 lineto strokepath } loop",
|
||
"[0 1] 0 setdash { newpath 0 0 moveto 9900 0 lineto strokepath } loop",
|
||
"/a [ 0 1 1999 { pop 0 0 1 1 } for ] def nulldevice { a rectfill } loop",
|
||
] {
|
||
runs_out_of_budget(body);
|
||
}
|
||
}
|
||
|
||
/// A loop of operators that read or copy the whole current path or clip path runs out of the
|
||
/// operation budget (PLRM 3rd ed., chapter 8): `pathbbox`, `clip`, `clippath`, `shfill` (which
|
||
/// reads the clip path for the area it paints), `pathforall` (which copies the path before it runs
|
||
/// a procedure), `gstate`, `currentgstate` and `setgstate`.
|
||
#[test]
|
||
fn paths_read_or_copied_in_a_loop_run_out_of_the_budget() {
|
||
// 10,001 elements reaching 10,000,000 points to the right: what `shfill` paints in a clip of
|
||
// them lies far outside the page and is left out.
|
||
let path = "newpath 0 0 moveto 0 1 9999 { pop 1000 0 rlineto } for";
|
||
let sh = "/sh << /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] \
|
||
/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> def";
|
||
for body in [
|
||
"{ pathbbox pop pop pop pop } loop",
|
||
"{ clip } loop",
|
||
"clip newpath { clippath } loop",
|
||
"clip newpath { sh shfill } loop",
|
||
"{ { pop pop exit } { } { } { } pathforall } loop",
|
||
"{ gstate pop } loop",
|
||
"/g gstate def { g currentgstate pop } loop",
|
||
"/g gstate def { g setgstate } loop",
|
||
] {
|
||
runs_out_of_budget(&format!("{sh} {path} {body}"));
|
||
}
|
||
}
|
||
|
||
/// A loop of `uappend` on an encoded user path runs out of the operation budget (PLRM 3rd ed.,
|
||
/// section 4.6.2 and Table 4.3: each byte of the operator string is an operator, or a count of up
|
||
/// to 223 repeats of the next one): one that runs `ucache` millions of times, one of repeat counts
|
||
/// alone, and one with a long data array.
|
||
#[test]
|
||
fn encoded_user_paths_in_a_loop_run_out_of_the_budget() {
|
||
for body in [
|
||
// 100,000 pairs of a count of 223 repeats (byte 255) and `ucache` (byte 11): each
|
||
// `uappend` runs `ucache` 22,300,000 times.
|
||
"/s 200000 string def 0 2 199998 { dup s exch 255 put 1 add s exch 11 put } for /up [ [ ] s ] def",
|
||
// 200,000 repeat counts and no operator.
|
||
"/s 200000 string def 0 1 199999 { s exch 255 put } for /up [ [ ] s ] def",
|
||
// A data array of 400,000 nulls and an empty operator string.
|
||
"/up [ 400000 array () ] def",
|
||
] {
|
||
runs_out_of_budget(&format!("{body} {{ up uappend }} loop"));
|
||
}
|
||
}
|
||
|
||
/// `arc` raises its second angle by multiples of 360 until it is at least the first, and `arcn`
|
||
/// lowers it until it is at most the first (PLRM 3rd ed., `arc` and `arcn` in chapter 8), however
|
||
/// large the angles: the arc runs between the angles' remainders after whole turns and ends at
|
||
/// the second angle, the new current point.
|
||
#[test]
|
||
fn arcs_with_huge_angles_end_at_their_second_angle() {
|
||
// `at(deg)` is the point at `deg` degrees on the circle of radius 10 around (50, 50), in
|
||
// document space.
|
||
let at = |deg: f64| Point::new(50.0 + 10.0 * deg.to_radians().cos(), 50.0 - 10.0 * deg.to_radians().sin());
|
||
// Each program is listed with the remainders of its first and second angles after whole turns.
|
||
for (body, first, second) in [
|
||
("50 50 10 1e18 0 arc", 280.0, 0.0),
|
||
("50 50 10 1e308 0 arc", 296.0, 0.0),
|
||
("50 50 10 0 1e18 arcn", 0.0, 280.0),
|
||
("50 50 10 0 1e308 arcn", 0.0, 296.0),
|
||
] {
|
||
let r = read_soon(&format!("newpath {body} currentpoint stroke 1 1 rectfill"));
|
||
let o = objects(&r.document);
|
||
assert_eq!(o.len(), 2, "{body}: {:?}", r.warnings);
|
||
// The arc lies within one quarter of the circle, so its endpoints span its bounds.
|
||
let (a, b) = (at(first), at(second));
|
||
assert!(near(bounds(&o[0]), Rect::from_points(a, b)), "{body}: {:?}", bounds(&o[0]));
|
||
// The second object is the square drawn at the current point.
|
||
assert!(near(bounds(&o[1]), Rect::new(b.x, b.y - 1.0, b.x + 1.0, b.y)), "{body}: {:?}", bounds(&o[1]));
|
||
}
|
||
}
|
||
|
||
/// An axial shading's function is one function dictionary or an array of one for each color
|
||
/// component (PLRM 3rd ed., Table 4.13). One evaluation visits at most 257 functions, and past
|
||
/// that the shading raises `limitcheck`: when four levels of arrays each have 100 entries that all
|
||
/// refer to the same array of the level below (at the last level, the same function), one
|
||
/// evaluation would visit 101,010,101 functions. Each function visited also counts against the
|
||
/// operation budget.
|
||
#[test]
|
||
fn shading_functions_in_shared_arrays_stop_at_the_limits() {
|
||
let axial = |function: &str| format!("<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 100 0] /Function {function} >>");
|
||
let f = "/f << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> def";
|
||
// `a`: an array of `n` references to the function `f`.
|
||
let array = |n: usize| format!("{f} /a {n} array def 0 1 {} {{ a exch f put }} for", n - 1);
|
||
// Each level is an array of 100 references to the level below.
|
||
let levels = "/l2 100 array def 0 1 99 { l2 exch a put } for /l3 100 array def 0 1 99 { l3 exch l2 put } for \
|
||
/l4 100 array def 0 1 99 { l4 exch l3 put } for";
|
||
let limitcheck = |r: &crate::Imported| r.warnings.iter().any(|w| w.contains("limitcheck in `shfill`: Function"));
|
||
let r = read_soon(&format!("0 0 10 10 rectfill {} {levels} {} shfill", array(100), axial("l4")));
|
||
assert!(limitcheck(&r) && objects(&r.document).len() == 1, "{:?}", r.warnings);
|
||
// 256 references are 257 functions for each sample, the most allowed; one more raises `limitcheck`.
|
||
let r = read_soon(&format!("{} {} shfill", array(256), axial("a")));
|
||
let o = objects(&r.document);
|
||
assert!(r.warnings.is_empty() && matches!(fill(&o[0]), Some(Paint::Gradient(_))), "{:?}", r.warnings);
|
||
let r = read_soon(&format!("0 0 10 10 rectfill {} {} shfill", array(257), axial("a")));
|
||
assert!(limitcheck(&r) && objects(&r.document).len() == 1, "{:?}", r.warnings);
|
||
// Each of these shadings visits 33 * 257 functions (33 samples), and 2000 of them count for
|
||
// more than the operation budget.
|
||
let r = read(&format!("{} /sh {} def 2000 {{ sh shfill }} repeat", array(256), axial("a")));
|
||
assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{:?}", r.warnings);
|
||
}
|
||
|
||
#[test]
|
||
fn cshow_runs_its_procedure_for_each_character() {
|
||
check("/n 0 def { pop pop pop /n n 1 add def } (abc) cshow n 3 eq");
|
||
// Each call gets the character's code and width.
|
||
check("/c 0 def { pop pop /c exch def } (A) cshow c 65 eq");
|
||
check(
|
||
"/Helvetica findfont 10 scalefont setfont /w 0 def { pop w add /w exch def pop } (ab) cshow \
|
||
(ab) stringwidth pop w sub abs 0.001 lt",
|
||
);
|
||
}
|
||
|
||
/// A `w` × `h` 8-bit palette TIFF with an alpha sample (`ExtraSamples` [1]), uncompressed in
|
||
/// strips of `per` rows, in either byte order, as the reference app writes its EPS previews:
|
||
/// pixel (x, y) is entry `(x + y) % 3` of a red, green, blue map, at alpha `12 x`.
|
||
fn palette_tiff(w: u16, h: u16, per: u16, big: bool) -> Vec<u8> {
|
||
let u16b = |v: u16| if big { v.to_be_bytes() } else { v.to_le_bytes() };
|
||
let u32b = |v: u32| if big { v.to_be_bytes() } else { v.to_le_bytes() };
|
||
// Shorts in an entry, left-justified.
|
||
let shorts = |v: &[u16]| -> [u8; 4] {
|
||
let mut f = [0; 4];
|
||
for (i, s) in v.iter().enumerate() {
|
||
f[2 * i..2 * i + 2].copy_from_slice(&u16b(*s));
|
||
}
|
||
f
|
||
};
|
||
let row = u32::from(w) * 2;
|
||
let mut out = vec![];
|
||
out.extend(if big { *b"MM\0*" } else { *b"II*\0" });
|
||
out.extend([0; 4]);
|
||
let data_at = out.len() as u32;
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
out.extend([((x + y) % 3) as u8, (x * 12) as u8]);
|
||
}
|
||
}
|
||
let map_at = out.len() as u32;
|
||
for (c, full) in [0xFFFF, 0x8000, 0xFFFF].into_iter().enumerate() {
|
||
for i in 0..256 {
|
||
out.extend(u16b(if i == c { full } else { 0 }));
|
||
}
|
||
}
|
||
let strips: Vec<u16> = (0..h.div_ceil(per)).collect();
|
||
let offsets_at = out.len() as u32;
|
||
for k in &strips {
|
||
out.extend(u32b(data_at + u32::from(k * per) * row));
|
||
}
|
||
let counts_at = out.len() as u32;
|
||
for k in &strips {
|
||
out.extend(u32b(u32::from(per.min(h - k * per)) * row));
|
||
}
|
||
let ifd = out.len() as u32;
|
||
out[4..8].copy_from_slice(&u32b(ifd));
|
||
let n = strips.len() as u32;
|
||
let entries: [(u16, u16, u32, [u8; 4]); 12] = [
|
||
(256, 3, 1, shorts(&[w])),
|
||
(257, 3, 1, shorts(&[h])),
|
||
(258, 3, 2, shorts(&[8, 8])),
|
||
(259, 3, 1, shorts(&[1])),
|
||
(262, 3, 1, shorts(&[3])),
|
||
(273, 4, n, u32b(if n == 1 { data_at } else { offsets_at })),
|
||
(277, 3, 1, shorts(&[2])),
|
||
(278, 3, 1, shorts(&[per])),
|
||
(279, 4, n, u32b(if n == 1 { u32::from(h) * row } else { counts_at })),
|
||
(284, 3, 1, shorts(&[1])),
|
||
(320, 3, 768, u32b(map_at)),
|
||
(338, 3, 1, shorts(&[1])),
|
||
];
|
||
out.extend(u16b(entries.len() as u16));
|
||
for (tag, kind, count, field) in entries {
|
||
out.extend(u16b(tag));
|
||
out.extend(u16b(kind));
|
||
out.extend(u32b(count));
|
||
out.extend(field);
|
||
}
|
||
out.extend([0; 4]);
|
||
out
|
||
}
|
||
|
||
#[test]
|
||
fn a_palette_preview_with_alpha_is_placed() {
|
||
// #234: the program fails, so the preview is placed.
|
||
let ps = eps("0 0 1 setrgbcolor 10 10 80 80 rectfill frobnicate");
|
||
for (big, per) in [(false, 5), (true, 5), (false, 10), (true, 3)] {
|
||
let tiff = palette_tiff(20, 10, per, big);
|
||
let r = import(&crate::dos_eps(&ps, &tiff).unwrap()).unwrap();
|
||
assert!(r.preview, "{big} {per}");
|
||
assert!(r.warnings[0].contains("preview"), "{:?}", r.warnings);
|
||
let o = objects(&r.document);
|
||
let NodeKind::Image(im) = &o[0].kind else { panic!("{:?}", o[0].kind) };
|
||
assert_eq!((im.width, im.height), (20, 10));
|
||
assert!(near(bounds(&o[0]), Rect::new(0.0, 0.0, 100.0, 100.0)));
|
||
let png = image::load_from_memory(&r.document.images[&im.key].bytes).unwrap().to_rgba8();
|
||
for (x, y) in [(0, 0), (1, 0), (2, 0), (19, 9), (7, 4), (3, 6)] {
|
||
let [r, g, b] = [[255, 0, 0], [0, 128, 0], [0, 0, 255]][((x + y) % 3) as usize];
|
||
assert_eq!(png.get_pixel(x, y).0, [r, g, b, (x * 12) as u8], "({x}, {y}) {big} {per}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_damaged_palette_preview_is_refused_without_panicking() {
|
||
let tiff = palette_tiff(20, 10, 5, false);
|
||
assert!(crate::tiff::palette_rgba(&tiff).is_some());
|
||
// Cut anywhere before the last entry's value (the directory is at the end): nothing.
|
||
for cut in 0..tiff.len() - 6 {
|
||
assert!(crate::tiff::palette_rgba(&tiff[..cut]).is_none(), "{cut}");
|
||
}
|
||
// Any byte changed: a picture or nothing.
|
||
for big in [false, true] {
|
||
let tiff = palette_tiff(20, 10, 5, big);
|
||
for at in 0..tiff.len() {
|
||
for v in [0, 1, 2, 0x7f, 0x80, 0xff] {
|
||
let mut t = tiff.clone();
|
||
t[at] = v;
|
||
let _ = crate::tiff::palette_rgba(&t);
|
||
}
|
||
}
|
||
}
|
||
// A damaged preview of an unreadable program: the hard error.
|
||
let ps = eps("frobnicate");
|
||
for t in [&tiff[..tiff.len() / 2], b"II*\0garbage", &[0; 64]] {
|
||
let e = import(&crate::dos_eps(&ps, t).unwrap()).unwrap_err();
|
||
assert!(e.contains("can't be read"), "{e}");
|
||
}
|
||
}
|