Files
vectorcraft/crates/engine/src/cmd/fileio/tests_pdfimport.rs
T
dotCooCoo 31ffbb2152 PDF/.ai import: the PostScript file error names File › Open and File › Place, which read the file
document.pdfInfo and clipboard.importPdf, given a PostScript file (an
EPS, or an .ai in an older PostScript format), failed with "this is a
PostScript file (...), which can't be opened yet: save it as PDF,
PDF-compatible .ai or SVG". document.open and file.place read the same
file through the EPS reader. The error told users to convert a file that
VectorCraft opens.

The error now says the file is not a PDF and that File › Open and
File › Place read it as EPS. The 13 catalogs that carry the message get
the new text.
2026-10-16 03:36:20 +07:00

212 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `document.open` PDF options (pages, Crop To, password) and `document.pdfInfo`.
use serde_json::{Value, json};
use vectorcraft_testkit::pdf::{PdfPage, pdf};
use super::*;
fn b64(bytes: &[u8]) -> String {
vectorcraft_format::base64_encode(bytes)
}
/// Three pages of widths 100, 200 and 300 pt, the second with every box set.
fn three_pages() -> Vec<u8> {
let mut second = PdfPage::new(200.0, 400.0, "0 0 1 rg 20 30 40 50 re f");
second.crop = Some([10.0, 10.0, 190.0, 390.0]);
second.bleed = Some([5.0, 5.0, 195.0, 395.0]);
second.trim = Some([15.0, 15.0, 185.0, 385.0]);
second.art = Some([20.0, 20.0, 120.0, 220.0]);
pdf(&[PdfPage::new(100.0, 100.0, "0 g 0 0 10 10 re f"), second, PdfPage::new(300.0, 100.0, "")], None)
}
fn open(s: &mut Session, bytes: &[u8], extra: Value) -> crate::Result<Value> {
let mut p = json!({"name": "pages.pdf", "dataBase64": b64(bytes)});
if let (Some(o), Value::Object(e)) = (p.as_object_mut(), extra) {
o.extend(e);
}
s.execute("document.open", &p)
}
fn sizes(s: &Session) -> Vec<(f64, f64)> {
s.active().unwrap().doc.artboards.iter().map(|a| (a.rect.width(), a.rect.height())).collect()
}
#[test]
fn document_open_imports_the_pages_named() {
let bytes = three_pages();
let mut s = Session::new();
open(&mut s, &bytes, json!({"pages": "2-3"})).unwrap();
assert_eq!(sizes(&s), [(180.0, 380.0), (300.0, 100.0)]);
let layers: Vec<String> = s.active().unwrap().doc.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect();
assert_eq!(layers, ["Page 2", "Page 3"]);
open(&mut s, &bytes, json!({"page": 1})).unwrap();
assert_eq!(sizes(&s), [(100.0, 100.0)]);
open(&mut s, &bytes, json!({"pages": "all"})).unwrap();
assert_eq!(sizes(&s).len(), 3);
let e = open(&mut s, &bytes, json!({"pages": "4"})).unwrap_err().to_string();
assert!(e.contains("pages") && e.contains("1 to 3"), "{e}");
assert!(open(&mut s, &bytes, json!({"pages": true})).is_err());
}
#[test]
fn document_open_crops_each_page_to_the_box_asked_for() {
let bytes = three_pages();
let mut s = Session::new();
for (crop, size) in [
("media", (200.0, 400.0)),
("crop", (180.0, 380.0)),
("bleed", (190.0, 390.0)),
("trim", (170.0, 370.0)),
("art", (100.0, 200.0)),
("bounding", (40.0, 50.0)),
] {
open(&mut s, &bytes, json!({"page": 2, "cropTo": crop})).unwrap();
assert_eq!(sizes(&s), [size], "{crop}");
}
// `crop` is an alias (the file.place name); case doesn't matter.
open(&mut s, &bytes, json!({"page": 2, "crop": "Art"})).unwrap();
assert_eq!(sizes(&s), [(100.0, 200.0)]);
let e = open(&mut s, &bytes, json!({"cropTo": "page"})).unwrap_err().to_string();
assert!(e.contains("bounding, art, crop, trim, bleed, media"), "{e}");
}
#[test]
fn an_encrypted_pdf_needs_its_password() {
let bytes = pdf(&[PdfPage::new(100.0, 50.0, "1 0 0 rg 0 0 20 20 re f"), PdfPage::new(100.0, 50.0, "")], Some("open sesame"));
let mut s = Session::new();
let e = open(&mut s, &bytes, json!({})).unwrap_err().to_string();
assert!(e.contains("password-protected"), "{e}");
let e = open(&mut s, &bytes, json!({"password": "nope"})).unwrap_err().to_string();
assert!(e.contains("password is wrong"), "{e}");
assert!(s.active().is_none(), "nothing opened");
let r = open(&mut s, &bytes, json!({"password": "open sesame", "pages": "1"})).unwrap();
assert_eq!(r["format"], "pdf");
assert_eq!(sizes(&s), [(100.0, 50.0)]);
assert_eq!(s.active().unwrap().doc.node_count(), 2, "the layer and its decrypted rectangle");
}
#[test]
fn pdf_info_lists_pages_boxes_and_a_thumbnail() {
let bytes = three_pages();
let mut s = Session::new();
let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(&bytes), "thumbnail": 2, "thumbnailSize": 100, "cropTo": "art"})).unwrap();
assert_eq!(v["pages"], 3);
assert_eq!(v["needsPassword"], false);
let p2 = &v["pageInfo"][1];
assert_eq!((p2["width"].as_f64(), p2["height"].as_f64()), (Some(180.0), Some(380.0)));
assert_eq!(p2["boxes"]["trim"], json!([15.0, 15.0, 185.0, 385.0]));
assert_eq!(p2["boxes"]["media"], json!([0.0, 0.0, 200.0, 400.0]));
let png = vectorcraft_format::base64_decode(v["thumbnail"].as_str().unwrap()).unwrap();
let img = image::load_from_memory(&png).unwrap();
assert_eq!((img.width(), img.height()), (50, 100), "the art box at 100 px on its longest side");
assert!(s.execute("document.pdfInfo", &json!({"dataBase64": b64(&bytes), "thumbnail": 4})).is_err());
// Locked: no pages until the password is given.
let locked = pdf(&[PdfPage::new(10.0, 10.0, "")], Some("pw"));
let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(&locked)})).unwrap();
assert_eq!((v["pages"].as_u64(), v["needsPassword"].as_bool(), v["wrongPassword"].as_bool()), (Some(0), Some(true), Some(false)));
let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(&locked), "password": "x"})).unwrap();
assert_eq!(v["wrongPassword"], true);
let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(&locked), "password": "pw"})).unwrap();
assert_eq!((v["pages"].as_u64(), v["needsPassword"].as_bool()), (Some(1), Some(false)));
assert!(s.execute("document.pdfInfo", &json!({"dataBase64": b64(b"not a pdf")})).is_err());
assert!(s.execute("document.pdfInfo", &json!({})).is_err());
}
#[test]
fn file_place_crops_a_pdf_page_to_any_box_and_takes_its_password() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let place = |s: &mut Session, bytes: &[u8], extra: Value| {
let mut p = json!({"name": "pages.pdf", "dataBase64": b64(bytes)});
if let (Some(o), Value::Object(e)) = (p.as_object_mut(), extra) {
o.extend(e);
}
s.execute("file.place", &p)
};
let bytes = three_pages();
for (crop, size) in
[("trim", (170.0, 370.0)), ("art", (100.0, 200.0)), ("media", (200.0, 400.0)), ("crop", (180.0, 380.0)), ("bounding", (40.0, 50.0))]
{
let r = place(&mut s, &bytes, json!({"page": 2, "crop": crop})).unwrap();
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(size.0), Some(size.1)), "{crop}");
}
let info = s.execute("file.place.info", &json!({"name": "pages.pdf", "dataBase64": b64(&bytes), "page": 2, "crop": "bleed"})).unwrap();
assert_eq!((info["width"].as_f64(), info["height"].as_f64()), (Some(190.0), Some(390.0)));
let locked = pdf(&[PdfPage::new(60.0, 30.0, "0 g 0 0 10 10 re f")], Some("pw"));
assert!(place(&mut s, &locked, json!({})).unwrap_err().to_string().contains("password-protected"));
assert_eq!(place(&mut s, &locked, json!({"password": "pw"})).unwrap()["width"].as_f64(), Some(60.0));
}
#[test]
fn load_options_read_pages_crop_and_password() {
let o = LoadOptions::from_params("x", &json!({"page": 3, "crop": "trim", "password": "p"})).unwrap();
assert_eq!(o, LoadOptions { pages: Some("3".into()), crop: vectorcraft_pdf::CropTo::Trim, password: Some("p".into()), ..Default::default() });
// `pages` wins over `page`; an empty password is none.
let o = LoadOptions::from_params("x", &json!({"pages": "1-2", "page": 3, "password": ""})).unwrap();
assert_eq!((o.pages.as_deref(), o.password), (Some("1-2"), None));
assert_eq!(LoadOptions::from_params("x", &Value::Null).unwrap(), LoadOptions::default());
}
#[test]
fn a_postscript_ai_file_says_it_cannot_be_opened_yet() {
// PostScript opens through the EPS reader now (tests_epsimport.rs): a program it can't read,
// with no preview to fall back on, still says so.
let ps = b"%!PS-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n0 0 moveto 100 100 frobnicate stroke\nshowpage\n%%EOF\n";
let mut s = Session::new();
for name in ["legacy.ai", "art.eps", "named.pdf", "template.ait"] {
let e = s.execute("document.open", &json!({"name": name, "dataBase64": b64(ps)})).unwrap_err().to_string();
assert!(e.contains(name) && e.contains("PostScript") && e.contains("save it as PDF"), "{name}: {e}");
}
// An EPS with a binary header too, and the PDF info of either.
let mut binary = vec![0xC5, 0xD0, 0xD3, 0xC6];
binary.extend_from_slice(&[0; 28]);
binary.extend_from_slice(ps);
assert!(s.execute("document.open", &json!({"name": "x.eps", "dataBase64": b64(&binary)})).unwrap_err().to_string().contains("PostScript"));
assert!(s.execute("document.pdfInfo", &json!({"dataBase64": b64(ps)})).unwrap_err().to_string().contains("PostScript"));
assert!(s.active().is_none());
}
/// PostScript given where a PDF is read (`document.pdfInfo`, `clipboard.importPdf`) is refused
/// with an error that names File › Open and File › Place, and both of those read the file.
#[test]
fn postscript_read_as_a_pdf_names_open_and_place() {
let ps = b"%!PS-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n1 0 0 setrgbcolor 10 10 50 50 rectfill\nshowpage\n%%EOF\n";
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
for cmd in ["document.pdfInfo", "clipboard.importPdf"] {
let e = s.execute(cmd, &json!({"dataBase64": b64(ps)})).unwrap_err().to_string();
assert!(e.contains("PostScript") && e.contains("not a PDF: File › Open and File › Place read it as EPS"), "{cmd}: {e}");
assert!(!e.contains("can't be opened"), "{cmd}: {e}");
}
for name in ["legacy.ai", "art.eps"] {
let r = s.execute("document.open", &json!({"name": name, "dataBase64": b64(ps)})).unwrap();
assert_eq!(r["warnings"], json!([]), "{name}: {r}");
let r = s.execute("file.place", &json!({"name": name, "dataBase64": b64(ps)})).unwrap();
assert_eq!(r["ids"].as_array().map(Vec::len), Some(1), "{name}: {r}");
}
}
/// A page showing only text, with an editor's private data when `private`.
fn text_page(private: bool, also: &str) -> PdfPage {
PdfPage {
resources: "/Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >>".into(),
entries: if private { "/PieceInfo << /Editor << /Private << /AIPrivateData1 7 /RoundTripVersion 24 >> >> >>".into() } else { String::new() },
..PdfPage::new(300.0, 100.0, &format!("BT /F1 12 Tf 10 50 Td (Saved without PDF content) Tj ET {also}"))
}
}
#[test]
fn a_placeholder_only_pdf_part_is_detected() {
let mut s = Session::new();
let e = open(&mut s, &pdf(&[text_page(true, "")], None), json!({})).unwrap_err().to_string();
assert!(e.contains("placeholder page") && e.contains("PDF compatibility"), "{e}");
assert!(s.active().is_none());
// Text without private data, and private data with art, are ordinary files.
open(&mut s, &pdf(&[text_page(false, "")], None), json!({})).unwrap();
open(&mut s, &pdf(&[text_page(true, "0 g 0 0 10 10 re f")], None), json!({})).unwrap();
// One real page among the pages picked is enough.
let two = pdf(&[text_page(true, ""), text_page(true, "0 g 0 0 10 10 re f")], None);
assert!(open(&mut s, &two, json!({"pages": "1"})).is_err());
open(&mut s, &two, json!({})).unwrap();
}