mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 03:57:57 -06:00
531 lines
26 KiB
Rust
531 lines
26 KiB
Rust
//! The `print.*` commands: state, cells, templates and the PDF.
|
||
|
||
use lightcraft_catalog::MemStore;
|
||
use serde_json::{Value, json};
|
||
|
||
use crate::Session;
|
||
use crate::library::LibraryStores;
|
||
|
||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
|
||
}
|
||
|
||
fn fails(s: &mut Session, id: &str, p: Value) -> String {
|
||
s.execute(id, &p).expect_err(&format!("{id} {p} should fail")).to_string()
|
||
}
|
||
|
||
/// A scratch folder that goes away with the test, however it ends.
|
||
struct Scratch(std::path::PathBuf);
|
||
|
||
impl Scratch {
|
||
fn new(tag: &str) -> Scratch {
|
||
let dir = std::env::temp_dir().join(format!("lightcraft-print-{tag}-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
Scratch(dir)
|
||
}
|
||
}
|
||
|
||
impl Drop for Scratch {
|
||
fn drop(&mut self) {
|
||
let _ = std::fs::remove_dir_all(&self.0);
|
||
}
|
||
}
|
||
|
||
fn package(s: &mut Session) {
|
||
run(s, "print.set", json!({"layout": {"style": "customPackage", "page": {"paper": "letter"}}}));
|
||
}
|
||
|
||
#[test]
|
||
fn get_set_and_reset() {
|
||
let mut s = Session::with_demo();
|
||
let g = run(&mut s, "print.get", json!({}));
|
||
assert_eq!(g["settings"]["layout"]["style"], "singleImage");
|
||
assert_eq!(g["pageWidth"], 8.5);
|
||
assert_eq!(g["printable"]["x"], 0.25);
|
||
assert_eq!(g["settings"]["job"]["ppi"], 240.0);
|
||
let papers = g["papers"].as_array().unwrap();
|
||
assert!(papers.iter().any(|p| p["name"] == "a4"));
|
||
// The Japanese photo and postcard papers are listed with their metric sizes in inches.
|
||
let paper = |name: &str| papers.iter().find(|p| p["name"] == name).cloned().unwrap_or_else(|| panic!("{name}"));
|
||
let l = paper("l");
|
||
assert!((l["width"].as_f64().unwrap() - 89.0 / 25.4).abs() < 1e-9 && (l["height"].as_f64().unwrap() - 127.0 / 25.4).abs() < 1e-9);
|
||
assert!(papers.iter().any(|p| p["name"] == "postcard") && papers.iter().any(|p| p["name"] == "6cut"));
|
||
assert_eq!(g["gridCells"].as_array().unwrap().len(), 1);
|
||
|
||
let g = run(
|
||
&mut s,
|
||
"print.set",
|
||
json!({"layout": {"style": "contactSheet", "grid": {"columns": 2, "rows": 3}, "page": {"paper": "a4", "orientation": "landscape", "margins": {"left": 1.0}}}, "image": {"zoomToFill": true}, "job": {"ppi": 300.0}}),
|
||
);
|
||
assert_eq!(g["settings"]["layout"]["page"]["margins"]["left"], 1.0);
|
||
assert_eq!(g["settings"]["layout"]["page"]["margins"]["top"], 0.25, "a patch merges");
|
||
assert_eq!(g["gridCells"].as_array().unwrap().len(), 6);
|
||
assert!((g["pageWidth"].as_f64().unwrap() - 11.693).abs() < 1e-3);
|
||
assert_eq!(g["settings"]["image"]["zoomToFill"], true);
|
||
// the page count follows the photos: 6 cells per page
|
||
let g = run(&mut s, "print.get", json!({"ids": vec![1; 7]}));
|
||
assert_eq!(g["pageCount"], 2);
|
||
|
||
let g = run(&mut s, "print.reset", json!({}));
|
||
assert_eq!(g["settings"]["job"]["ppi"], 240.0);
|
||
}
|
||
|
||
#[test]
|
||
fn bad_settings_are_refused_and_change_nothing() {
|
||
let mut s = Session::with_demo();
|
||
let before = run(&mut s, "print.get", json!({}));
|
||
for p in [
|
||
json!({"typo": 1}),
|
||
json!({"layout": {"typo": 1}}),
|
||
json!({"layout": {"style": "poster"}}),
|
||
json!({"layout": {"cells": []}}),
|
||
json!({"layout": {"pages": 3}}),
|
||
json!({"layout": {"page": {"paper": "poster"}}}),
|
||
json!({"layout": {"page": {"paper": "custom", "customWidth": -1}}}),
|
||
json!({"layout": {"page": {"margins": {"top": 1e9}}}}),
|
||
json!({"layout": {"page": {"margins": {"top": 6.0, "bottom": 5.0}}}}),
|
||
json!({"layout": {"grid": {"columns": 0}}}),
|
||
json!({"layout": {"grid": {"columns": 9999}}}),
|
||
json!({"layout": {"spacing": -1}}),
|
||
json!({"image": {"borderWidth": 99}}),
|
||
json!({"image": {"strokeWidthPt": -2}}),
|
||
json!({"image": {"borderColor": [1, 2]}}),
|
||
json!({"image": {"zoomToFill": "yes"}}),
|
||
json!({"guides": {"gridSize": 0}}),
|
||
json!({"guides": {"units": "furlong"}}),
|
||
json!({"job": {"ppi": 1e30}}),
|
||
json!({"job": {"ppi": 10}}),
|
||
json!({"job": null}),
|
||
] {
|
||
fails(&mut s, "print.set", p);
|
||
}
|
||
assert_eq!(run(&mut s, "print.get", json!({})), before);
|
||
assert!(s.execute("print.set", &json!([1])).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn cells_are_commands() {
|
||
let mut s = Session::with_demo();
|
||
// grid styles have no cells
|
||
fails(&mut s, "print.cell.add", json!({"width": 2, "height": 2}));
|
||
package(&mut s);
|
||
let a = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0}))["id"].as_u64().unwrap();
|
||
let b = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0, "x": 5.0, "y": 5.0}))["id"].as_u64().unwrap();
|
||
let g = run(&mut s, "print.get", json!({}));
|
||
assert_eq!(g["settings"]["layout"]["cells"].as_array().unwrap().len(), 2);
|
||
assert_eq!(g["overlaps"], false);
|
||
// move, clamped inside the margins
|
||
let r = run(&mut s, "print.cell.move", json!({"id": a, "x": 99.0, "y": 0.0}));
|
||
assert_eq!(r["rect"]["x"], 8.25 - 3.0);
|
||
assert_eq!(r["rect"]["y"], 0.25);
|
||
// resize keeps what is not given
|
||
let r = run(&mut s, "print.cell.resize", json!({"id": b, "width": 4.0}));
|
||
assert_eq!((r["rect"]["w"].as_f64().unwrap(), r["rect"]["h"].as_f64().unwrap()), (4.0, 2.0));
|
||
// snapping to the other cell's edge
|
||
let ra = run(&mut s, "print.get", json!({}))["settings"]["layout"]["cells"][0]["rect"].clone();
|
||
let left = ra["x"].as_f64().unwrap();
|
||
let r = run(&mut s, "print.cell.move", json!({"id": b, "x": left - 4.0 - 0.04, "y": 5.0, "snap": true}));
|
||
assert!((r["rect"]["x"].as_f64().unwrap() - (left - 4.0)).abs() < 1e-9, "{r}");
|
||
let r = run(&mut s, "print.cell.move", json!({"id": b, "x": left - 4.0 - 0.04, "y": 5.0}));
|
||
assert!((r["rect"]["x"].as_f64().unwrap() - (left - 4.04)).abs() < 1e-9, "no snap unless asked: {r}");
|
||
run(&mut s, "print.set", json!({"guides": {"snap": "off"}}));
|
||
let r = run(&mut s, "print.cell.move", json!({"id": b, "x": left - 4.0 - 0.04, "y": 5.0, "snap": true}));
|
||
assert!((r["rect"]["x"].as_f64().unwrap() - (left - 4.04)).abs() < 1e-9, "snap off");
|
||
// stacking
|
||
let r = run(&mut s, "print.cell.stack", json!({"id": a, "order": "toFront"}));
|
||
assert_eq!(r["order"], json!([b, a]));
|
||
fails(&mut s, "print.cell.stack", json!({"id": a, "order": "sideways"}));
|
||
// photos and pages
|
||
let photo = s.catalog.photos().next().unwrap().id.0;
|
||
run(&mut s, "print.cell.photo", json!({"id": a, "photo": photo}));
|
||
fails(&mut s, "print.cell.photo", json!({"id": a, "photo": 999_999}));
|
||
run(&mut s, "print.layout.newPage", json!({}));
|
||
run(&mut s, "print.cell.page", json!({"id": b, "page": 1}));
|
||
fails(&mut s, "print.cell.page", json!({"id": b, "page": 7}));
|
||
let g = run(&mut s, "print.get", json!({}));
|
||
assert_eq!(g["pageCount"], 2);
|
||
// delete, clear
|
||
run(&mut s, "print.cell.delete", json!({"id": b}));
|
||
fails(&mut s, "print.cell.delete", json!({"id": b}));
|
||
fails(&mut s, "print.cell.move", json!({"id": 99, "x": 0, "y": 0}));
|
||
fails(&mut s, "print.cell.move", json!({"x": 0, "y": 0}));
|
||
run(&mut s, "print.layout.clear", json!({}));
|
||
let g = run(&mut s, "print.get", json!({}));
|
||
assert_eq!((g["settings"]["layout"]["cells"].as_array().unwrap().len(), g["pageCount"].as_u64()), (0, Some(1)));
|
||
}
|
||
|
||
#[test]
|
||
fn hostile_cell_numbers_are_refused() {
|
||
let mut s = Session::with_demo();
|
||
package(&mut s);
|
||
for (w, h) in [(0.0, 2.0), (-1.0, 2.0), (1e12, 1e12), (2.0, 1e-9), (50.0, 50.0)] {
|
||
fails(&mut s, "print.cell.add", json!({"width": w, "height": h}));
|
||
}
|
||
fails(&mut s, "print.cell.add", json!({"width": "wide", "height": 2}));
|
||
fails(&mut s, "print.cell.add", json!({"width": 2}));
|
||
let id = run(&mut s, "print.cell.add", json!({"width": 2, "height": 2}))["id"].as_u64().unwrap();
|
||
fails(&mut s, "print.cell.move", json!({"id": id, "x": "left", "y": 0}));
|
||
fails(&mut s, "print.cell.resize", json!({"id": id, "width": -4}));
|
||
fails(&mut s, "print.cell.resize", json!({"id": id, "width": "x"}));
|
||
fails(&mut s, "print.cell.move", json!({"id": id, "x": 1, "y": 1, "snap": "yes"}));
|
||
fails(&mut s, "print.cell.move", json!({"id": u64::MAX, "x": 1, "y": 1}));
|
||
let r = run(&mut s, "print.cell.move", json!({"id": id, "x": -1e300, "y": 1e300}));
|
||
assert_eq!(r["rect"]["x"], 0.25);
|
||
}
|
||
|
||
#[test]
|
||
fn auto_layout_fills_pages_without_overlap() {
|
||
let mut s = Session::with_demo();
|
||
run(&mut s, "print.set", json!({"layout": {"style": "picturePackage"}}));
|
||
for (w, h) in [(4.0, 6.0), (4.0, 6.0), (5.0, 7.0), (2.5, 3.5), (2.5, 3.5), (2.5, 3.5), (2.5, 3.5), (8.0, 10.0)] {
|
||
run(&mut s, "print.cell.add", json!({"width": w, "height": h}));
|
||
}
|
||
let r = run(&mut s, "print.layout.autoLayout", json!({}));
|
||
assert!(r["pages"].as_u64().unwrap() >= 2);
|
||
let g = run(&mut s, "print.get", json!({}));
|
||
assert_eq!(g["overlaps"], false);
|
||
let again = run(&mut s, "print.layout.autoLayout", json!({}));
|
||
assert_eq!(again["cells"], r["cells"], "deterministic");
|
||
// an oversize cell that arrived by a paper change is pulled in, not left outside the page
|
||
run(&mut s, "print.set", json!({"layout": {"page": {"paper": "4x6"}}}));
|
||
let g = run(&mut s, "print.get", json!({}));
|
||
for c in g["settings"]["layout"]["cells"].as_array().unwrap() {
|
||
assert!(c["rect"]["x"].as_f64().unwrap() + c["rect"]["w"].as_f64().unwrap() <= 4.0 - 0.25 + 1e-9);
|
||
}
|
||
}
|
||
|
||
/// The first page's MediaBox width and height in points.
|
||
fn media_box(pdf: &[u8]) -> (f64, f64) {
|
||
let text = String::from_utf8_lossy(pdf);
|
||
let at = text.find("/MediaBox [").expect("a MediaBox") + "/MediaBox [".len();
|
||
let nums: Vec<f64> = text[at..].split(']').next().unwrap().split_whitespace().map(|v| v.parse().unwrap()).collect();
|
||
(nums[2], nums[3])
|
||
}
|
||
|
||
fn temp_pdf(name: &str) -> String {
|
||
std::env::temp_dir().join(format!("lightcraft-print-{name}-{}.pdf", std::process::id())).to_string_lossy().to_string()
|
||
}
|
||
|
||
#[test]
|
||
fn export_pdf_prints_the_current_settings() {
|
||
let mut s = Session::with_demo();
|
||
let ids: Vec<u64> = s.catalog.photos().take(3).map(|p| p.id.0).collect();
|
||
run(
|
||
&mut s,
|
||
"print.set",
|
||
json!({"layout": {"style": "contactSheet", "grid": {"columns": 1, "rows": 2}, "page": {"paper": "a5"}}, "job": {"ppi": 100.0}}),
|
||
);
|
||
let path = temp_pdf("export");
|
||
let r = run(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
|
||
assert_eq!((r["pages"].as_u64(), r["photos"].as_u64()), (Some(2), Some(3)));
|
||
let bytes = std::fs::read(&path).unwrap();
|
||
assert_eq!(bytes.len() as u64, r["bytes"].as_u64().unwrap());
|
||
let text = String::from_utf8_lossy(&bytes);
|
||
assert!(text.starts_with("%PDF-") && text.contains("/Count 2"));
|
||
// A5 is 148 x 210 mm = 419.53 x 595.28 pt, rounded to whole pixels at 100 ppi (583 x 827)
|
||
let (w, h) = media_box(&bytes);
|
||
assert!((w - 419.53).abs() < 1.0 && (h - 595.28).abs() < 1.0, "A5, got {w} x {h} pt");
|
||
// the row in the activity stack is gone when the print is done
|
||
assert!(s.activity.list().is_empty());
|
||
fails(&mut s, "print.exportPdf", json!({}));
|
||
fails(&mut s, "print.exportPdf", json!({"path": path, "typo": 1}));
|
||
fails(&mut s, "print.exportPdf", json!({"path": path, "ids": ["x"]}));
|
||
fails(&mut s, "print.exportPdf", json!({"path": path, "ids": [999_999]}));
|
||
// a package with no cells has nothing to print
|
||
run(&mut s, "print.set", json!({"layout": {"style": "picturePackage"}}));
|
||
fails(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
|
||
}
|
||
|
||
/// The Japanese photo and postcard papers reach the PDF page at their metric sizes: はがき is
|
||
/// 100 × 148 mm (283.46 × 419.53 pt) and L is 89 × 127 mm (252.28 × 359.99 pt). 254 ppi is exactly
|
||
/// ten pixels per millimetre, so the page pixels are whole millimetres and the MediaBox is exact.
|
||
#[test]
|
||
fn export_pdf_uses_the_japanese_paper_sizes() {
|
||
let mut s = Session::with_demo();
|
||
let id = s.catalog.photos().next().map(|p| p.id.0).unwrap();
|
||
for (paper, w_pt, h_pt) in [("postcard", 283.46, 419.53), ("はがき", 283.46, 419.53), ("l", 252.28, 359.99)] {
|
||
run(&mut s, "print.set", json!({"layout": {"style": "singleImage", "page": {"paper": paper}}, "job": {"ppi": 254.0}}));
|
||
let path = temp_pdf(paper);
|
||
run(&mut s, "print.exportPdf", json!({"path": path, "ids": [id]}));
|
||
let bytes = std::fs::read(&path).unwrap();
|
||
let (w, h) = media_box(&bytes);
|
||
assert!((w - w_pt).abs() < 0.1 && (h - h_pt).abs() < 0.1, "{paper}: {w} x {h} pt, wanted {w_pt} x {h_pt}");
|
||
}
|
||
// The customary names are accepted and the page size follows them; an unknown name is refused.
|
||
let g = run(&mut s, "print.set", json!({"layout": {"page": {"paper": "はがき"}}}));
|
||
assert!((g["pageWidth"].as_f64().unwrap() - 100.0 / 25.4).abs() < 1e-9);
|
||
assert!((g["pageHeight"].as_f64().unwrap() - 148.0 / 25.4).abs() < 1e-9);
|
||
fails(&mut s, "print.set", json!({"layout": {"page": {"paper": "poster"}}}));
|
||
}
|
||
|
||
#[test]
|
||
fn export_pdf_with_a_printer_profile() {
|
||
let mut s = Session::with_demo();
|
||
let ids: Vec<u64> = s.catalog.photos().take(1).map(|p| p.id.0).collect();
|
||
let dir = std::env::temp_dir().join(format!("lightcraft-print-icc-{}", std::process::id()));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
let icc = dir.join("Fine Art Matte.icc");
|
||
std::fs::write(&icc, lightcraft_codecs::icc::write_named(lightcraft_codecs::NamedSpace::AdobeRgb)).unwrap();
|
||
let color = json!({"profile": {"kind": "icc", "path": icc}, "intent": "relative", "sharpen": "high", "media": "matte", "brightness": 10});
|
||
run(
|
||
&mut s,
|
||
"print.set",
|
||
json!({"layout": {"style": "contactSheet", "grid": {"columns": 1, "rows": 1}, "page": {"paper": "4x6"}}, "job": {"ppi": 72.0, "color": color}}),
|
||
);
|
||
assert_eq!(run(&mut s, "print.get", json!({}))["settings"]["job"]["color"]["media"], "matte");
|
||
let path = temp_pdf("icc");
|
||
run(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
|
||
let text = String::from_utf8_lossy(&std::fs::read(&path).unwrap()).into_owned();
|
||
assert!(text.contains("/OutputIntents") && text.contains("/ICCBased") && text.contains("/BitsPerComponent 16"));
|
||
// out-of-range adjustments are refused when set; a missing profile fails before rendering
|
||
fails(&mut s, "print.set", json!({"job": {"color": {"contrast": 150}}}));
|
||
run(&mut s, "print.set", json!({"job": {"color": {"profile": {"kind": "icc", "path": dir.join("gone.icc")}}}}));
|
||
let err = fails(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
|
||
assert!(err.contains("gone.icc"), "{err}");
|
||
assert!(s.activity.list().is_empty(), "nothing started");
|
||
}
|
||
|
||
#[test]
|
||
fn custom_package_prints_each_cells_photo() {
|
||
let mut s = Session::with_demo();
|
||
let ids: Vec<u64> = s.catalog.photos().take(2).map(|p| p.id.0).collect();
|
||
run(&mut s, "print.set", json!({"layout": {"style": "customPackage", "page": {"paper": "4x6"}}, "job": {"ppi": 72.0}}));
|
||
let a = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0}))["id"].as_u64().unwrap();
|
||
let b = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0}))["id"].as_u64().unwrap();
|
||
run(&mut s, "print.cell.photo", json!({"id": a, "photo": ids[0]}));
|
||
run(&mut s, "print.cell.photo", json!({"id": b, "photo": ids[1]}));
|
||
let r = run(&mut s, "print.exportPdf", json!({"path": temp_pdf("custom"), "ids": [ids[0]]}));
|
||
assert_eq!(r["pages"], 1);
|
||
}
|
||
|
||
#[test]
|
||
fn templates_save_apply_delete_and_persist() {
|
||
let files = MemStore::new();
|
||
let open = |files: &MemStore| {
|
||
let mut s = Session::with_demo();
|
||
let stores = LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(files.clone()), on_disk: false };
|
||
s.open_library_in(stores, false).unwrap();
|
||
s
|
||
};
|
||
let mut s = open(&files);
|
||
run(
|
||
&mut s,
|
||
"print.set",
|
||
json!({"layout": {"style": "contactSheet", "grid": {"columns": 4, "rows": 5}}, "page": {"background": [10, 20, 30]}, "guides": {"gridSize": 1.0}}),
|
||
);
|
||
let r = run(&mut s, "print.template.save", json!({"name": "Proofs"}));
|
||
assert_eq!(r["templates"][0]["name"], "Proofs");
|
||
assert!(r["templates"][0]["settings"].get("guides").is_none());
|
||
run(&mut s, "print.template.save", json!({"name": "proofs"}));
|
||
assert_eq!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().len(), 1, "the same name in another case replaces");
|
||
|
||
run(&mut s, "print.reset", json!({}));
|
||
run(&mut s, "print.set", json!({"guides": {"gridSize": 0.5}}));
|
||
let g = run(&mut s, "print.template.apply", json!({"name": "PROOFS"}));
|
||
assert_eq!(g["settings"]["layout"]["grid"]["columns"], 4);
|
||
assert_eq!(g["settings"]["page"]["background"], json!([10, 20, 30]));
|
||
assert_eq!(g["settings"]["guides"]["gridSize"], 0.5, "guides stay as they were");
|
||
|
||
// it survives reopening the library
|
||
let mut again = open(&files);
|
||
assert_eq!(run(&mut again, "print.template.list", json!({}))["templates"][0]["name"], "proofs");
|
||
run(&mut again, "print.template.apply", json!({"name": "Proofs"}));
|
||
|
||
fails(&mut s, "print.template.save", json!({"name": " "}));
|
||
fails(&mut s, "print.template.save", json!({}));
|
||
fails(&mut s, "print.template.apply", json!({"name": "Nope"}));
|
||
fails(&mut s, "print.template.delete", json!({"name": "Nope"}));
|
||
run(&mut s, "print.template.delete", json!({"name": "proofs"}));
|
||
assert!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().is_empty());
|
||
assert!(run(&mut open(&files), "print.template.list", json!({}))["templates"].as_array().unwrap().is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn templates_export_to_a_file_and_import_from_one() {
|
||
let files = MemStore::new();
|
||
let open = |files: &MemStore| {
|
||
let mut s = Session::with_demo();
|
||
let stores = LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(files.clone()), on_disk: false };
|
||
s.open_library_in(stores, false).unwrap();
|
||
s
|
||
};
|
||
let scratch = Scratch::new("templates");
|
||
let dir = &scratch.0;
|
||
let mut s = open(&files);
|
||
run(&mut s, "print.set", json!({"layout": {"style": "contactSheet", "grid": {"columns": 3, "rows": 4}}, "page": {"background": [5, 6, 7]}}));
|
||
run(&mut s, "print.template.save", json!({"name": "Proofs"}));
|
||
|
||
// the extension is added when the path has none
|
||
let r = run(&mut s, "print.template.export", json!({"name": "proofs", "path": dir.join("proofs").to_string_lossy()}));
|
||
assert_eq!(r["name"], "Proofs");
|
||
let file = dir.join("proofs.lcprint");
|
||
assert_eq!(r["path"], file.to_string_lossy().as_ref());
|
||
let doc: Value = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
|
||
assert_eq!((doc["format"].as_str(), doc["version"].as_u64(), doc["name"].as_str()), (Some("lightcraft.printTemplate"), Some(1), Some("Proofs")));
|
||
|
||
// into the same library: the name is taken, so the copy is renamed
|
||
let r = run(&mut s, "print.template.import", json!({"path": file.to_string_lossy()}));
|
||
assert_eq!((r["name"].as_str(), r["renamed"].as_bool()), (Some("Proofs (2)"), Some(true)), "{r}");
|
||
assert_eq!(r["templates"].as_array().unwrap().len(), 2);
|
||
assert_eq!(r["templates"][1]["settings"], r["templates"][0]["settings"]);
|
||
|
||
// into another library, under its own name, and it is kept there
|
||
let other = MemStore::new();
|
||
let mut b = open(&other);
|
||
let r = run(&mut b, "print.template.import", json!({"path": file.to_string_lossy()}));
|
||
assert_eq!((r["name"].as_str(), r["renamed"].as_bool()), (Some("Proofs"), Some(false)));
|
||
let g = run(&mut open(&other), "print.template.apply", json!({"name": "Proofs"}));
|
||
assert_eq!(g["settings"]["layout"]["grid"]["columns"], 3);
|
||
assert_eq!(g["settings"]["page"]["background"], json!([5, 6, 7]));
|
||
|
||
// refusals leave the templates as they were
|
||
let bad_file = dir.join("bad.lcprint");
|
||
std::fs::write(&bad_file, br#"{"format": "lightcraft.printTemplate", "version": 1, "name": "Bad", "settings": {"layout": {"pages": 0}}}"#)
|
||
.unwrap();
|
||
let huge = dir.join("huge.lcprint");
|
||
std::fs::write(&huge, vec![b' '; crate::print::template::MAX_FILE as usize + 1]).unwrap();
|
||
for (p, why) in [
|
||
(json!({}), "missing `path`"),
|
||
(json!({"path": " "}), "missing `path`"),
|
||
(json!({"path": dir.join("absent.lcprint").to_string_lossy()}), "absent.lcprint"),
|
||
(json!({"path": dir.to_string_lossy()}), "not a file"),
|
||
(json!({"path": huge.to_string_lossy()}), "too large"),
|
||
(json!({"path": bad_file.to_string_lossy()}), "template `Bad`"),
|
||
] {
|
||
let e = fails(&mut s, "print.template.import", p.clone());
|
||
assert!(e.contains(why), "{p}: {e}");
|
||
}
|
||
assert_eq!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().len(), 2);
|
||
assert!(fails(&mut s, "print.template.export", json!({"name": "Nope", "path": dir.join("x").to_string_lossy()})).contains("no print template"));
|
||
assert!(fails(&mut s, "print.template.export", json!({"name": "Proofs"})).contains("missing `path`"));
|
||
assert!(fails(&mut s, "print.template.export", json!({"path": dir.join("x").to_string_lossy()})).contains("missing `name`"));
|
||
assert!(!dir.join("x.lcprint").exists());
|
||
|
||
// a full list takes no more
|
||
while s.print_templates.len() < crate::print::template::MAX_TEMPLATES {
|
||
let n = s.print_templates.len();
|
||
run(&mut s, "print.template.save", json!({"name": format!("T{n}")}));
|
||
}
|
||
assert!(fails(&mut s, "print.template.import", json!({"path": file.to_string_lossy()})).contains("at most"));
|
||
}
|
||
|
||
/// A library whose `prefs.json` can't be read, so saving the preferences fails.
|
||
#[derive(Clone, Default)]
|
||
struct PrefsUnreadable(MemStore);
|
||
|
||
impl lightcraft_catalog::Store for PrefsUnreadable {
|
||
fn read(&mut self, name: &str) -> std::io::Result<Option<Vec<u8>>> {
|
||
if name == "prefs.json" {
|
||
return Err(std::io::Error::other("locked by another process"));
|
||
}
|
||
self.0.read(name)
|
||
}
|
||
fn write_atomic(&mut self, name: &str, data: &[u8]) -> std::io::Result<()> {
|
||
self.0.write_atomic(name, data)
|
||
}
|
||
fn append(&mut self, name: &str, data: &[u8]) -> std::io::Result<()> {
|
||
self.0.append(name, data)
|
||
}
|
||
fn truncate(&mut self, name: &str, len: u64) -> std::io::Result<()> {
|
||
self.0.truncate(name, len)
|
||
}
|
||
fn describe(&self) -> String {
|
||
"prefs unreadable".into()
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn template_changes_that_cant_be_saved_are_undone() {
|
||
let mut s = Session::with_demo();
|
||
let stores =
|
||
LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(PrefsUnreadable::default()), on_disk: false };
|
||
s.open_library_in(stores, false).unwrap();
|
||
s.print_templates.push(crate::print::template::PrintTemplate::new("Proofs", &s.print).unwrap());
|
||
let names = |s: &Session| s.print_templates.iter().map(|t| (t.name.clone(), t.settings.clone())).collect::<Vec<_>>();
|
||
let kept = names(&s);
|
||
let scratch = Scratch::new("unsaved");
|
||
let file = scratch.0.join("proofs.lcprint");
|
||
run(&mut s, "print.template.export", json!({"name": "Proofs", "path": file.to_string_lossy()}));
|
||
|
||
run(&mut s, "print.set", json!({"layout": {"style": "contactSheet"}}));
|
||
for (id, p) in [
|
||
("print.template.save", json!({"name": "Other"})),
|
||
("print.template.save", json!({"name": "proofs"})),
|
||
("print.template.delete", json!({"name": "Proofs"})),
|
||
("print.template.import", json!({"path": file.to_string_lossy()})),
|
||
] {
|
||
assert!(fails(&mut s, id, p.clone()).contains("prefs.json"), "{id} {p}");
|
||
assert_eq!(names(&s), kept, "{id} {p}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_hostile_template_in_the_preferences_does_not_break_the_library() {
|
||
let files = MemStore::new();
|
||
let hostile = json!({"printTemplates": [
|
||
{"name": "Bad", "settings": {"layout": {"pages": 0}, "extra": true}},
|
||
{"name": "Huge", "settings": {"job": {"ppi": 1e300}}},
|
||
{"name": "Fine", "settings": {"layout": {"style": "contactSheet"}}}
|
||
], "cacheMb": 123});
|
||
files.set("prefs.json", serde_json::to_vec(&hostile).unwrap());
|
||
let mut s = Session::with_demo();
|
||
let stores = LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(files.clone()), on_disk: false };
|
||
s.open_library_in(stores, false).unwrap();
|
||
assert!(s.take_library_warnings().is_empty(), "the preferences are not damaged");
|
||
assert_eq!(s.cache_mb, 123);
|
||
assert_eq!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().len(), 3);
|
||
fails(&mut s, "print.template.apply", json!({"name": "Bad"}));
|
||
fails(&mut s, "print.template.apply", json!({"name": "Huge"}));
|
||
run(&mut s, "print.template.apply", json!({"name": "Fine"}));
|
||
}
|
||
|
||
#[test]
|
||
fn every_print_command_is_registered_with_a_description() {
|
||
for id in [
|
||
"print.get",
|
||
"print.set",
|
||
"print.reset",
|
||
"print.cell.add",
|
||
"print.cell.move",
|
||
"print.cell.resize",
|
||
"print.cell.delete",
|
||
"print.cell.stack",
|
||
"print.cell.photo",
|
||
"print.cell.page",
|
||
"print.layout.autoLayout",
|
||
"print.layout.newPage",
|
||
"print.layout.clear",
|
||
"print.exportPdf",
|
||
"print.template.list",
|
||
"print.template.save",
|
||
"print.template.apply",
|
||
"print.template.delete",
|
||
"print.template.export",
|
||
"print.template.import",
|
||
] {
|
||
let c = crate::find_command(id).unwrap_or_else(|| panic!("{id} missing"));
|
||
assert!(c.params.len() > 3, "{id}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn page_options_are_print_settings_and_template_fields() {
|
||
let mut s = Session::with_demo();
|
||
let g = run(&mut s, "print.get", json!({}));
|
||
assert_eq!(g["settings"]["page"], json!({"background": [255, 255, 255], "pageNumbers": false, "pageInfo": false, "cropMarks": false}));
|
||
let g = run(&mut s, "print.set", json!({"page": {"pageNumbers": true, "cropMarks": true}}));
|
||
assert_eq!((g["settings"]["page"]["pageNumbers"].as_bool(), g["settings"]["page"]["pageInfo"].as_bool()), (Some(true), Some(false)));
|
||
assert_eq!(g["settings"]["page"]["background"], json!([255, 255, 255]), "a patch merges");
|
||
fails(&mut s, "print.set", json!({"page": {"pageInfo": "yes"}}));
|
||
fails(&mut s, "print.set", json!({"page": {"pageNumber": true}}));
|
||
// a template keeps them
|
||
run(&mut s, "print.template.save", json!({"name": "Marked"}));
|
||
run(&mut s, "print.reset", json!({}));
|
||
assert!(!s.print.page.crop_marks);
|
||
run(&mut s, "print.template.apply", json!({"name": "Marked"}));
|
||
assert!(s.print.page.page_numbers && s.print.page.crop_marks && !s.print.page.page_info);
|
||
let set = crate::find_command("print.set").unwrap();
|
||
assert!(set.params.contains("pageNumbers") && set.params.contains("pageInfo") && set.params.contains("cropMarks"), "{}", set.params);
|
||
}
|