mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 22:22:25 +00:00
Merge pull request #6142 from dotCooCoo/fix/pdfimport-params
PDF import: cropTo, password, colorMode and textAs reject a value of the wrong type
This commit is contained in:
@@ -7,7 +7,7 @@ use vectorcraft_doc::{ColorMode, Document};
|
||||
use vectorcraft_pdf::{Choice, CropTo, ImportOptions, PdfError, TextAs};
|
||||
|
||||
use super::super::colormgmt::Grays;
|
||||
use super::super::{Result, Session, bad, f64_or, parse_range, str_param};
|
||||
use super::super::{Result, Session, bad, check_numbers, number_or, parse_range, text_param};
|
||||
use super::{err, source};
|
||||
|
||||
/// The `document.open` options besides the file. The PDF ones (pages, box, password, text,
|
||||
@@ -75,16 +75,17 @@ impl LoadOptions {
|
||||
Some(Value::Number(n)) => Some(n.to_string()),
|
||||
Some(v) => return Err(bad(cmd, format!("pages must be a range such as \"2-3\" or a page number, not {v}"))),
|
||||
};
|
||||
let crop = choice(cmd, "cropTo", str_param(p, "cropTo").or_else(|| str_param(p, "crop")))?;
|
||||
let password = str_param(p, "password").filter(|s| !s.is_empty()).map(str::to_string);
|
||||
let color_mode = match str_param(p, "colorMode").map(str::to_ascii_lowercase).as_deref() {
|
||||
let (crop_to, crop_alias) = (text_param(p, "cropTo", cmd)?, text_param(p, "crop", cmd)?);
|
||||
let crop = choice(cmd, "cropTo", crop_to.or(crop_alias))?;
|
||||
let password = text_param(p, "password", cmd)?.filter(|s| !s.is_empty()).map(str::to_string);
|
||||
let color_mode = match text_param(p, "colorMode", cmd)?.map(str::to_ascii_lowercase).as_deref() {
|
||||
None => None,
|
||||
Some("rgb") => Some(ColorMode::Rgb),
|
||||
Some("cmyk") => Some(ColorMode::Cmyk),
|
||||
Some(m) => return Err(bad(cmd, format!("colorMode must be rgb or cmyk, not `{m}`"))),
|
||||
};
|
||||
let grays = Grays::param(cmd, p)?;
|
||||
let text_as = choice(cmd, "textAs", str_param(p, "textAs"))?;
|
||||
let text_as = choice(cmd, "textAs", text_param(p, "textAs", cmd)?)?;
|
||||
let layers = match p.get("layers").filter(|v| !v.is_null()) {
|
||||
None => true,
|
||||
Some(v) => v.as_bool().ok_or_else(|| bad(cmd, "layers must be true or false"))?,
|
||||
@@ -172,6 +173,7 @@ const INFO: &str = "document.pdfInfo";
|
||||
pub(super) fn pdf_info(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
let bytes = source(p, INFO)?.bytes;
|
||||
let o = LoadOptions::from_params(INFO, p)?;
|
||||
check_numbers(p, &["thumbnailSize"], INFO)?;
|
||||
// Programs whose header is only `%!` are PostScript too, as `clipboard.importEps` reads them.
|
||||
if bytes.starts_with(b"%!") || vectorcraft_pdf::is_postscript(&bytes) {
|
||||
return postscript_info(&bytes, p);
|
||||
@@ -229,7 +231,7 @@ fn postscript_info(bytes: &[u8], p: &Value) -> Result<Value> {
|
||||
/// A PNG of `doc`'s first artboard, `thumbnailSize` px (default 160, from 16 to 1024) on its longer
|
||||
/// side, base64.
|
||||
fn thumbnail(doc: &Document, p: &Value) -> Result<String> {
|
||||
let size = f64_or(p, "thumbnailSize", 160.0).clamp(16.0, 1024.0);
|
||||
let size = number_or(p, "thumbnailSize", 160.0, INFO)?.clamp(16.0, 1024.0);
|
||||
let r = doc.artboards.first().map(|a| a.rect).ok_or_else(|| err("the page has no area"))?;
|
||||
let png = super::encode(doc, "png", &json!({"scale": size / r.width().max(r.height()).max(1.0)}))?;
|
||||
Ok(vectorcraft_format::base64_encode(&png))
|
||||
|
||||
@@ -147,6 +147,76 @@ fn load_options_read_pages_crop_and_password() {
|
||||
assert_eq!(LoadOptions::from_params("x", &Value::Null).unwrap(), LoadOptions::default());
|
||||
}
|
||||
|
||||
/// `cropTo` (and `crop`), `password`, `colorMode` and `textAs` given as anything but a string are an
|
||||
/// error naming the param: `document.open` opens nothing and `file.place` places nothing and records
|
||||
/// no undo step. Null is the same as leaving the param out.
|
||||
#[test]
|
||||
fn pdf_options_of_the_wrong_type_are_refused() {
|
||||
let bytes = three_pages();
|
||||
let wrong = [
|
||||
("cropTo", json!(5)),
|
||||
("crop", json!(true)),
|
||||
("password", json!(123)),
|
||||
("colorMode", json!(5)),
|
||||
("textAs", json!(true)),
|
||||
("textAs", json!(["outlines"])),
|
||||
];
|
||||
let one = |key: &str, v: &Value| {
|
||||
let mut p = json!({});
|
||||
p[key] = v.clone();
|
||||
p
|
||||
};
|
||||
let mut s = Session::new();
|
||||
for (key, v) in &wrong {
|
||||
let e = open(&mut s, &bytes, one(key, v)).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("`{key}` must be a string")), "{key}: {e}");
|
||||
assert!(s.active().is_none(), "{key}: nothing opened");
|
||||
}
|
||||
// A wrong `cropTo` is refused even when `crop` is a box, and a wrong `crop` when `cropTo` is.
|
||||
let e = open(&mut s, &bytes, json!({"cropTo": 2, "crop": "art"})).unwrap_err().to_string();
|
||||
assert!(e.contains("`cropTo` must be a string"), "{e}");
|
||||
let e = open(&mut s, &bytes, json!({"cropTo": "art", "crop": 2})).unwrap_err().to_string();
|
||||
assert!(e.contains("`crop` must be a string"), "{e}");
|
||||
assert!(s.active().is_none(), "nothing opened");
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let nodes = s.doc().unwrap().doc.node_count();
|
||||
for (key, v) in &wrong {
|
||||
let mut p = json!({"name": "pages.pdf", "dataBase64": b64(&bytes)});
|
||||
p[*key] = v.clone();
|
||||
let e = s.execute("file.place", &p).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("`{key}` must be a string")), "{key}: {e}");
|
||||
assert_eq!((s.doc().unwrap().history.undo.len(), s.doc().unwrap().doc.node_count()), (undo, nodes), "{key}: nothing placed");
|
||||
}
|
||||
// Null is left out; valid strings read as before.
|
||||
let nulls = json!({"cropTo": null, "crop": null, "password": null, "colorMode": null, "textAs": null});
|
||||
assert_eq!(LoadOptions::from_params("x", &nulls).unwrap(), LoadOptions::default());
|
||||
let o = LoadOptions::from_params("x", &json!({"cropTo": null, "crop": "Bleed", "colorMode": "CMYK", "textAs": "outlines"})).unwrap();
|
||||
assert_eq!(
|
||||
(o.crop, o.color_mode, o.text_as),
|
||||
(vectorcraft_pdf::CropTo::Bleed, Some(vectorcraft_doc::ColorMode::Cmyk), vectorcraft_pdf::TextAs::Outlines)
|
||||
);
|
||||
}
|
||||
|
||||
/// `document.pdfInfo`'s `thumbnailSize` given as anything but a number is an error, with or without
|
||||
/// a `thumbnail`; null is 160 px.
|
||||
#[test]
|
||||
fn pdf_info_refuses_a_thumbnail_size_that_is_not_a_number() {
|
||||
let bytes = three_pages();
|
||||
let mut s = Session::new();
|
||||
for v in [json!("100"), json!(true)] {
|
||||
for p in [json!({"dataBase64": b64(&bytes), "thumbnail": 1, "thumbnailSize": v}), json!({"dataBase64": b64(&bytes), "thumbnailSize": v})] {
|
||||
let e = s.execute("document.pdfInfo", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("`thumbnailSize` must be a number"), "{v}: {e}");
|
||||
}
|
||||
}
|
||||
let e = s.execute("document.pdfInfo", &json!({"dataBase64": b64(&bytes), "colorMode": 1})).unwrap_err().to_string();
|
||||
assert!(e.contains("`colorMode` must be a string"), "{e}");
|
||||
let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(&bytes), "thumbnail": 1, "thumbnailSize": null})).unwrap();
|
||||
let png = vectorcraft_format::base64_decode(v["thumbnail"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(image::load_from_memory(&png).unwrap().width(), 160);
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
||||
@@ -840,6 +840,9 @@ empty `boxes`, and `thumbnail: 1` shows what that page prints. A page whose `Use
|
||||
opens at its size in points, the file's numbers times the `UserUnit` (one above 75,000 counts as 75,000), and
|
||||
`document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of
|
||||
such a page opens from that data at the page's size.
|
||||
`document.open`, `file.place` and `document.pdfInfo` reject a `cropTo`, `crop`, `password`, `colorMode` or `textAs`
|
||||
that is not a string, and `document.pdfInfo` rejects a `thumbnailSize` that is not a number. The error names the param,
|
||||
and nothing opens or is placed. Leaving a param out or giving null keeps its default.
|
||||
Imported colours keep their model:
|
||||
DeviceCMYK and CMYK ICC colours stay CMYK (a file painted mostly in CMYK opens as a CMYK document), DeviceGray is Gray,
|
||||
and Separation and DeviceN inks become spot swatches the art links to at its tint (gradient stops too). CMYK images
|
||||
|
||||
@@ -1277,6 +1277,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
In a `.ai` opened from its editing data, type with Warp on the object or on one of its fills or strokes opens as type with the effect live when the live warp draws as the art saved with it. When it draws differently, or when the file's limit of checks has no check left for it, it opens as that art, and one note says how many did. Type stays live without a comparison when the saved art paints nothing or draws type of its own, in frames of threaded type, when a text variable binds it, and when its text object doesn't come in as one text object (`crates/eps/src/import/ai/envelope.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` opened from its editing data, an object with a Tweak that moves nothing or Pucker & Bloat at 0 opens as itself with the effect live. A path, compound path or group whose only effect is Transform without copies, or a Free Distort whose corners make a parallelogram, opens as itself with it live, and so does an unstroked path or compound path with straight segments whose only effect is Twist. A gradient or pattern, an image, type, a clipping group, an opacity mask or effects inside the object, or a stroke with a Transform that scales, rotates or reflects, keep the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/ai/read/ops.rs`).
|
||||
A PDF page whose `UserUnit` (PDF 1.6) is above 1 opens at its size in points, its numbers times the `UserUnit` (one above 75,000 counts as 75,000), and `document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of such a page opens from that data at the page's size, with its layers, and when the layers can't be read its pages still take the artboard layout of the data, scaled the same way (part of #1482).
|
||||
`document.open`, `file.place` and `document.pdfInfo` reject a `cropTo`, `crop`, `password`, `colorMode` or `textAs` that is not a string, such as `"textAs": true`, and `document.pdfInfo` rejects a `thumbnailSize` that is not a number; the error names the param, and nothing opens or is placed. A param left out or given as null keeps its default (`crates/engine/src/cmd/fileio/tests_pdfimport.rs`).
|
||||
When the PDF content of a `.ai` has a page for each artboard, the layers are compared with every page on its artboard (until the pages compared take 8,388,608 pixels at the comparison's scale, about 67 letter-size pages, or number 128): art that only the layers or only a later page draws is handled as art that differs on the first page is, and a text object that a later page doesn't draw is left out unless its layer doesn't print (`crates/eps/src/import/tests_native.rs`).
|
||||
When the editing data of a `.ai` or EPS is for a page of another size than the file's, the warning that its layers weren't read gives both sizes in points, and the scale of the editing data's page to the file's page (1:10 for a tenth of its size) when their sides are in proportion (#2034; `crates/eps/src/import/tests_native.rs`).
|
||||
A `.ai`, or a PDF saved with Illustrator's editing data, whose editing data can't be decompressed opens from its PDF part with a warning that its layers weren't read and why, and an export or Save As to its own path asks first (`acknowledgeLoss`), as for a file whose layers the reader can't read (`crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
|
||||
Reference in New Issue
Block a user