Revert "feat(print): custom paper sizes and the paper remembered per printer (page setup) (#2381)"

This reverts commit 239491cbfe.
This commit is contained in:
bflatastic
2026-11-09 17:49:53 +07:00
parent da438371aa
commit 4b4315064c
15 changed files with 25 additions and 622 deletions
+2 -33
View File
@@ -38,14 +38,8 @@ fn bad(m: impl Into<String>) -> ToolError {
}
/// `doc_print`'s `paper`, by its name or a short form ("Letter", "ARCH D"), as its `PAPERS`
/// index and size (or a size such as "8.5x14in", whose index is past the list); US Letter when
/// none is given.
/// index and size; US Letter when none is given.
fn paper_named(name: Option<&str>) -> Result<(usize, (f64, f64))> {
// A size of its own: "8.5x14in", "210x297mm", "10x15cm", "612x792pt". It has no place in
// `PAPERS` (index `PAPERS.len()`), so no `media` option is sent for it.
if let Some(size) = name.and_then(paper_size) {
return Ok((PAPERS.len(), size));
}
let mut papers = PAPERS.iter().enumerate();
let found = match name {
None => papers.next(),
@@ -57,7 +51,7 @@ fn paper_named(name: Option<&str>) -> Result<(usize, (f64, f64))> {
};
found.map(|(index, (_, size))| (index, *size)).ok_or_else(|| {
bad(format!(
"unknown paper {:?}; choose one of {}, or a size such as 8.5x14in or 210x297mm (1 to 200 inches a side)",
"unknown paper {:?}; choose one of {}",
name.unwrap_or_default(),
PAPERS.iter().map(|(name, _)| *name).collect::<Vec<_>>().join(", ")
))
@@ -243,19 +237,6 @@ impl Automation {
}
}
/// "WxH" with a unit (in, mm, cm or pt) as a portrait paper size in points; each side 1 to 200
/// inches (PDF's own page limit).
fn paper_size(spec: &str) -> Option<(f64, f64)> {
let s = spec.trim().to_ascii_lowercase();
let (body, per_point) = [("in", 1.0 / 72.0), ("mm", 25.4 / 72.0), ("cm", 2.54 / 72.0), ("pt", 1.0)]
.into_iter()
.find_map(|(unit, per_point)| s.strip_suffix(unit).map(|b| (b, per_point)))?;
let (w, h) = body.split_once('x')?;
let side = |t: &str| t.trim().parse::<f64>().ok().map(|v| v / per_point).filter(|v| (72.0..=14_400.0).contains(v));
let (w, h) = (side(w)?, side(h)?);
Some((w.min(h), w.max(h)))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -308,19 +289,7 @@ mod tests {
assert_eq!(job(json!({}), Some("letter")).options, [media("Letter")]);
assert_eq!(job(json!({}), Some("ARCH D")).options, [media("na_arch-d_24x36in")]);
assert!(matches!(paper_named(Some("Quarto")), Err(ToolError::InvalidArgs(m)) if m.contains("US Letter")));
// A size of its own is laid out as asked and names no media.
assert_eq!(paper_named(Some("8.5x14in")).ok(), Some((PAPERS.len(), (612.0, 1008.0))));
assert!(job(json!({}), Some("8.5x14in")).options.is_empty());
assert_eq!(spool_job(&Args(&json!({})), "default", String::new(), None, 0).ok().map(|j| j.printer), Some(None));
assert!(spool_job(&Args(&json!({ "options": { "InputSlot": 3 } })), "Office", String::new(), None, 0).is_err());
}
#[test]
fn a_paper_size_reads_its_numbers_and_needs_a_unit() {
assert_eq!(paper_size("8.5 x 14 in"), Some((612.0, 1008.0)));
assert_eq!(paper_size("1e3x1e3pt"), Some((1000.0, 1000.0)));
for spec in ["0x0mm", "-5x10cm", "5x-10cm", "10x15"] {
assert_eq!(paper_size(spec), None, "{spec}");
}
}
}
+1 -1
View File
@@ -741,7 +741,7 @@ pub fn tools() -> Vec<ToolDef> {
"cut_marks": { "type": "boolean" },
"orientation": { "type": "string", "enum": ["auto", "portrait", "landscape"] },
"comments_forms": { "type": "string", "enum": ["document", "document-and-markups", "document-and-stamps", "form-fields-only"] },
"paper": { "type": "string", "description": "A paper name (US Letter, A4…) or a size of its own such as 8.5x14in, 210x297mm, 10x15cm or 612x792pt (1 to 200 inches a side)." },
"paper": { "type": "string" },
"path": { "type": "string" },
"printer": { "type": "string" },
"copies": { "type": "integer", "minimum": 1, "maximum": 999 },
-26
View File
@@ -11161,32 +11161,6 @@ fn a_missing_path_under_the_root_names_where_it_was_looked_for() {
let _ = std::fs::remove_dir_all(base);
}
/// `doc_print` takes a paper size of its own besides the named ones, in inches, millimetres,
/// centimetres or points, either way round; a size it can't read or outside 1 to 200 inches is
/// refused.
#[test]
fn doc_print_takes_a_paper_size_of_its_own() {
let dir = workdir("print-paper-size");
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
for (paper, want) in
[("8.5x14in", (612.0, 1008.0)), ("297x210mm", (595.28, 841.89)), ("10 x 15 cm", (283.46, 425.2)), ("288x432PT", (288.0, 432.0))]
{
let file = format!("{}.pdf", want.0);
ok(&mut a, "doc_print", json!({ "doc": doc, "pages": "1", "paper": paper, "orientation": "portrait", "path": file }));
let bytes = std::fs::read(dir.join(&file)).unwrap();
let out = pdfcraft_cos::Document::open(std::sync::Arc::new(bytes)).unwrap();
let page = pdfcraft_model::pages(&out)[0].display_size(&out);
assert!((page.0 - want.0).abs() < 0.5 && (page.1 - want.1).abs() < 0.5, "{paper}: {page:?}");
}
for paper in ["8.5xbroad", "8.5x14", "0.5x11in", "8.5x300in", "-8.5x11in", "NaNx11in"] {
match a.call("doc_print", &json!({ "doc": doc, "paper": paper, "path": "x.pdf" })) {
Err(ToolError::InvalidArgs(m)) => assert!(m.contains("unknown paper"), "{paper}: {m}"),
other => panic!("{paper}: {other:?}"),
}
}
}
/// `doc_print` prints as image: pictures at `dpi`, grey with `grayscale`; a resolution outside
/// 72–600 is refused.
#[test]
+7 -17
View File
@@ -161,9 +161,6 @@ impl PdfCraftApp {
v["pinned_tools"] = serde_json::json!(self.pinned_tools);
// Set outside json! above, which is at serde_json's macro recursion limit.
v["hide_empty_highlight_hover"] = serde_json::Value::Bool(self.comment_prefs.hide_empty_highlight_hover);
// The Print dialog's paper list and each printer's paper (print.page-setup).
v["print_custom_papers"] = serde_json::json!(self.print_draft.custom_papers);
v["print_paper_by_printer"] = serde_json::json!(self.print_draft.remembered_paper);
v["show_page_edge_shadows"] = self.show_page_edge_shadows.into();
// Not `check_updates_at_start`: that key was an opt-in of its own, off unless chosen, and
// settings written then would turn this off unasked.
@@ -173,10 +170,8 @@ impl PdfCraftApp {
v["show_secured_notice"] = serde_json::Value::Bool(self.show_secured_notice);
v["text_links"] = serde_json::Value::Bool(self.text_links);
v["organize_zoom"] = serde_json::json!(self.organize_zoom);
// Canonical names survive changes to the order of PAPERS and do not depend on UI language;
// a custom paper is stored by the name it was given.
let d = &self.print_draft;
v["print_paper"] = serde_json::json!((d.paper < d.paper_count()).then(|| d.paper_name()));
// Canonical names survive changes to the order of PAPERS and do not depend on UI language.
v["print_paper"] = serde_json::json!(pdfcraft_engine::print::PAPERS.get(self.print_draft.paper).map(|(name, _)| *name));
v.to_string()
}
@@ -297,16 +292,11 @@ impl PdfCraftApp {
if let Some(on) = v["always_default_view"].as_bool() {
self.always_default_view = on;
}
// The Print dialog's paper list and each printer's paper; settings are untrusted, so each
// custom paper is checked as if it were typed.
if v.get("print_custom_papers").is_some() {
self.print_draft.custom_papers = print_paper::read_custom(&v["print_custom_papers"]);
}
if v.get("print_paper_by_printer").is_some() {
self.print_draft.remembered_paper = print_paper::read_remembered(&v["print_paper_by_printer"]);
}
// After the custom papers, so a custom paper chosen last time is found by its name.
if let Some(paper) = v.get("print_paper").and_then(serde_json::Value::as_str).and_then(|name| self.print_draft.paper_index(name)) {
if let Some(paper) = v
.get("print_paper")
.and_then(serde_json::Value::as_str)
.and_then(|name| pdfcraft_engine::print::PAPERS.iter().position(|(canonical, _)| *canonical == name))
{
self.print_draft.paper = paper;
}
if let Some(on) = v["highlight_fields"].as_bool() {
-1
View File
@@ -197,7 +197,6 @@ use workflows::ocr_verify;
#[cfg(target_arch = "wasm32")]
use workflows::ocr_web;
use workflows::optimize_ui;
use workflows::print_paper;
use workflows::print_ui;
use workflows::scan_clean_ui;
use workflows::web_print;
-1
View File
@@ -15,7 +15,6 @@ pub(super) mod ocr_verify;
#[cfg(target_arch = "wasm32")]
pub(super) mod ocr_web;
pub(super) mod optimize_ui;
pub(super) mod print_paper;
pub(super) mod print_ui;
pub(super) mod scan_clean_ui;
pub(super) mod web_print;
-224
View File
@@ -1,224 +0,0 @@
//! The Print dialog's paper list and the paper each printer is remembered with (print.page-setup):
//! paper sizes of a person's own, added with Custom… beside Paper and kept with the settings, and
//! for each printer the paper chosen when "Remember this paper for …" was ticked at Print time.
//! Two-sided, grayscale, collate, sizing and comments & forms are left to the dialog itself.
use std::collections::BTreeMap;
use pdfcraft_engine::print::PAPERS;
use serde::{Deserialize, Serialize};
use crate::print_ui::PrintDraft;
/// The largest side a custom paper can have, in points: PDF's own page limit (200 inches).
pub const MAX_SIDE: f64 = 14_400.0;
/// The smallest side, in points (one inch).
pub const MIN_SIDE: f64 = 72.0;
/// How many custom papers the settings keep.
pub const MAX_CUSTOM: usize = 100;
/// The longest name a custom paper keeps, in characters.
const MAX_NAME: usize = 64;
/// A paper size added in the Print dialog (points, portrait).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CustomPaper {
pub name: String,
pub width: f64,
pub height: f64,
}
impl CustomPaper {
/// A custom paper from a name and two sides in points, portrait; None when the name is empty
/// or a side is outside [`MIN_SIDE`]..=[`MAX_SIDE`], or a standard paper already has the name
/// (any case), which would otherwise be found first when the paper is looked up by name.
pub fn new(name: &str, a: f64, b: f64) -> Option<Self> {
let name: String = name.trim().chars().filter(|c| !c.is_control()).take(MAX_NAME).collect();
let fits = |v: f64| v.is_finite() && (MIN_SIDE..=MAX_SIDE).contains(&v);
let standard = PAPERS.iter().any(|(n, _)| n.eq_ignore_ascii_case(&name));
(!name.is_empty() && !standard && fits(a) && fits(b)).then(|| Self { name, width: a.min(b), height: a.max(b) })
}
}
/// The paper remembered for each printer, by the printer's name ("" is Save as PDF).
pub type RememberedPaper = BTreeMap<String, String>;
/// Custom papers read back from the settings, which are untrusted: each one is checked as if it
/// were typed, a name already taken keeps its first size, and at most [`MAX_CUSTOM`] are kept.
pub fn read_custom(value: &serde_json::Value) -> Vec<CustomPaper> {
let Some(list) = value.as_array() else { return Vec::new() };
let mut out: Vec<CustomPaper> = Vec::new();
for item in list {
let (Some(name), Some(w), Some(h)) = (item["name"].as_str(), item["width"].as_f64(), item["height"].as_f64()) else { continue };
if let Some(paper) = CustomPaper::new(name, w, h)
&& !out.iter().any(|p| p.name == paper.name)
&& out.len() < MAX_CUSTOM
{
out.push(paper);
}
}
out
}
/// The remembered papers read back from the settings: printer name to paper name, strings only.
pub fn read_remembered(value: &serde_json::Value) -> RememberedPaper {
let Some(map) = value.as_object() else { return RememberedPaper::new() };
map.iter().filter_map(|(printer, paper)| paper.as_str().map(|p| (printer.clone(), p.to_string()))).collect()
}
impl PrintDraft {
/// The paper list: the standard sizes, then the custom ones.
pub fn paper_count(&self) -> usize {
PAPERS.len() + self.custom_papers.len()
}
/// The chosen paper's size (points, portrait).
pub fn paper_size(&self) -> (f64, f64) {
match self.paper.checked_sub(PAPERS.len()) {
None => PAPERS[self.paper.min(PAPERS.len() - 1)].1,
Some(c) => self.custom_papers.get(c).map_or(PAPERS[0].1, |p| (p.width, p.height)),
}
}
/// The chosen paper's name (a standard name, or the custom paper's).
pub fn paper_name(&self) -> String {
match self.paper.checked_sub(PAPERS.len()) {
None => PAPERS[self.paper.min(PAPERS.len() - 1)].0.to_string(),
Some(c) => self.custom_papers.get(c).map_or_else(|| PAPERS[0].0.to_string(), |p| p.name.clone()),
}
}
/// The index of the paper called `name`, standard or custom.
pub fn paper_index(&self, name: &str) -> Option<usize> {
PAPERS.iter().position(|(n, _)| *n == name).or_else(|| self.custom_papers.iter().position(|p| p.name == name).map(|i| PAPERS.len() + i))
}
/// True when the chosen paper is a custom one (which can be removed).
pub fn paper_is_custom(&self) -> bool {
self.paper >= PAPERS.len()
}
/// Add (or replace, by name) a custom paper and choose it.
pub fn add_custom_paper(&mut self, paper: CustomPaper) {
self.custom_papers.retain(|p| p.name != paper.name);
if self.custom_papers.len() >= MAX_CUSTOM {
self.custom_papers.remove(0);
}
self.custom_papers.push(paper);
self.paper = PAPERS.len() + self.custom_papers.len() - 1;
}
/// Take the chosen custom paper off the list; the dialog goes back to the first size.
pub fn remove_custom_paper(&mut self) {
if let Some(i) = self.paper.checked_sub(PAPERS.len())
&& i < self.custom_papers.len()
{
self.custom_papers.remove(i);
self.paper = 0;
}
}
/// The key the printer's paper is remembered under ("" is Save as PDF).
fn printer_key(&self) -> String {
self.printer.clone().unwrap_or_default()
}
/// Start from the paper remembered for the printer chosen, if one was and it is still on the
/// list; the Remember box says whether one was.
pub fn apply_remembered(&mut self) {
let remembered = self.remembered_paper.get(&self.printer_key()).cloned();
self.remember_paper = remembered.is_some();
if let Some(i) = remembered.and_then(|name| self.paper_index(&name)) {
self.paper = i;
}
}
/// At Print time: keep the paper for the printer when the box is ticked, forget it when not.
pub fn remember_now(&mut self) {
let key = self.printer_key();
if self.remember_paper {
let name = self.paper_name();
self.remembered_paper.insert(key, name);
} else {
self.remembered_paper.remove(&key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_custom_paper_is_portrait_named_and_within_pdfs_limits() {
let p = CustomPaper::new(" Legal, tray 2 ", 1008.0, 612.0).unwrap();
assert_eq!((p.name.as_str(), p.width, p.height), ("Legal, tray 2", 612.0, 1008.0));
assert!(CustomPaper::new("", 612.0, 792.0).is_none());
assert!(CustomPaper::new("\u{1}", 612.0, 792.0).is_none());
assert!(CustomPaper::new("Tiny", 10.0, 792.0).is_none());
assert!(CustomPaper::new("Huge", 612.0, 20_000.0).is_none());
assert!(CustomPaper::new("NaN", f64::NAN, 792.0).is_none());
assert_eq!(CustomPaper::new(&"x".repeat(500), 612.0, 792.0).unwrap().name.chars().count(), MAX_NAME);
}
#[test]
fn settings_are_read_as_untrusted() {
let v = serde_json::json!([
{ "name": "Label", "width": 288.0, "height": 432.0 },
{ "name": "Label", "width": 300.0, "height": 400.0 },
{ "name": "Bad", "width": -1.0, "height": 432.0 },
{ "name": 5, "width": 288.0, "height": 432.0 },
"nonsense",
]);
assert_eq!(read_custom(&v), vec![CustomPaper { name: "Label".into(), width: 288.0, height: 432.0 }]);
assert!(read_custom(&serde_json::json!(5)).is_empty());
let many: Vec<_> = (0..300).map(|i| serde_json::json!({ "name": format!("P{i}"), "width": 300.0, "height": 400.0 })).collect();
assert_eq!(read_custom(&serde_json::Value::Array(many)).len(), MAX_CUSTOM);
let r = read_remembered(&serde_json::json!({ "Office": "A4", "": "Label", "Odd": 3 }));
assert_eq!(r.len(), 2);
assert_eq!(r.get("").map(String::as_str), Some("Label"));
assert!(read_remembered(&serde_json::json!([1])).is_empty());
}
#[test]
fn the_paper_list_is_the_standard_sizes_then_the_custom_ones() {
let mut d = PrintDraft::default();
assert_eq!(d.paper_count(), PAPERS.len());
d.add_custom_paper(CustomPaper::new("Label", 288.0, 432.0).unwrap());
assert_eq!((d.paper, d.paper_name().as_str(), d.paper_size()), (PAPERS.len(), "Label", (288.0, 432.0)));
assert_eq!(d.paper_index("A4"), PAPERS.iter().position(|(n, _)| *n == "A4"));
assert_eq!(d.paper_index("Label"), Some(PAPERS.len()));
// The same name replaces the size; removing goes back to the first standard size.
d.add_custom_paper(CustomPaper::new("Label", 300.0, 400.0).unwrap());
assert_eq!((d.paper_count(), d.paper_size()), (PAPERS.len() + 1, (300.0, 400.0)));
d.remove_custom_paper();
assert_eq!((d.paper_count(), d.paper), (PAPERS.len(), 0));
// A stale index reads as the first size, never out of bounds.
d.paper = PAPERS.len() + 7;
assert_eq!(d.paper_size(), PAPERS[0].1);
}
#[test]
fn the_paper_is_remembered_per_printer_only_when_asked() {
let mut d = PrintDraft::default();
let a4 = d.paper_index("A4").unwrap();
d.printer = Some("Office".into());
d.paper = a4;
d.remember_paper = true;
d.remember_now();
d.printer = Some("Plotter".into());
d.paper = 0;
d.apply_remembered();
assert_eq!((d.paper, d.remember_paper), (0, false), "another printer keeps the dialog's paper");
d.printer = Some("Office".into());
d.apply_remembered();
assert_eq!((d.paper, d.remember_paper), (a4, true));
d.remember_paper = false;
d.remember_now();
assert!(d.remembered_paper.is_empty());
// A remembered custom paper that was removed since leaves the paper alone.
d.remembered_paper.insert("Office".into(), "Gone".into());
d.paper = 0;
d.apply_remembered();
assert_eq!((d.paper, d.remember_paper), (0, true));
}
}
+1 -9
View File
@@ -171,10 +171,8 @@ impl PdfCraftApp {
sheet: 0,
selected,
which,
custom_new: None,
..keep
};
self.print_draft.apply_remembered();
self.dialog = Some(crate::Dialog::Print);
}
@@ -316,13 +314,7 @@ impl PdfCraftApp {
// A picked printer that is no longer listed falls back to the default too.
if !d.printer_picked || !offered(d.printer.as_deref(), &printers) {
d.printer_picked = false;
// Only a different printer starts over with its remembered paper; the
// same one keeps the paper and the Remember box as they were left.
let default = default_printer(&printers);
if default != d.printer {
d.printer = default;
d.apply_remembered();
}
d.printer = default_printer(&printers);
}
d.printers = printers;
d.printers_loading = false;
+7 -68
View File
@@ -158,7 +158,6 @@ pub(crate) fn body(
d.printer_picked = true;
d.show_driver_options = false;
d.driver_error = None;
d.apply_remembered();
}
ui.end_row();
// The spooler answered with no printer at all (#160): say so, rather than
@@ -203,35 +202,15 @@ pub(crate) fn body(
ui.end_row();
}
let paper_label = ui.label(tl!("Paper:"));
d.paper = d.paper.min(d.paper_count().saturating_sub(1));
let paper_name = PAPERS.get(d.paper).map_or("US Letter", |p| p.0);
let before = d.paper;
ui.horizontal(|ui| {
// A standard size's name is translated; a custom paper keeps the name it was given.
let shown = if d.paper_is_custom() { d.paper_name() } else { tl!(d.paper_name().as_str()).to_string() };
craft_ui::ComboBox::from_id_salt("paper").selected_text(shown).width(260.0).show_ui(ui, |ui| {
for (i, (name, _)) in PAPERS.iter().enumerate() {
ui.selectable_value(&mut d.paper, i, tl!(name));
}
if !d.custom_papers.is_empty() {
ui.separator();
}
for (i, p) in d.custom_papers.iter().enumerate() {
let label = format!("{} ({})", p.name, d.units.size(p.width, p.height));
ui.selectable_value(&mut d.paper, PAPERS.len() + i, label);
}
})
.response
.labelled_by(paper_label.id);
if ui.button(tl!("Custom…")).on_hover_text(tl!("Add a paper size of your own to the list")).clicked() {
d.custom_new = match d.custom_new {
Some(_) => None,
None => Some((String::new(), String::new(), String::new())),
};
craft_ui::ComboBox::from_id_salt("paper").selected_text(tl!(paper_name)).width(260.0).show_ui(ui, |ui| {
for (i, (name, _)) in PAPERS.iter().enumerate() {
ui.selectable_value(&mut d.paper, i, tl!(name));
}
if d.paper_is_custom() && ui.button(tl!("Remove")).on_hover_text(tl!("Take this paper size off the list")).clicked() {
d.remove_custom_paper();
}
});
})
.response
.labelled_by(paper_label.id);
// Label stock gets the label layout (cut, turned upright, fitted).
if d.paper != before && d.paper == print::LABEL_PAPER {
d.paper_before_label = Some(before);
@@ -239,11 +218,6 @@ pub(crate) fn body(
d.sheet = 0;
}
ui.end_row();
if d.custom_new.is_some() {
ui.label("");
custom_paper_row(ui, d, t);
ui.end_row();
}
});
// Windows says which drivers can't print two-sided as they're asked; they keep their
// own setting, even when Two-sided is Off.
@@ -585,11 +559,6 @@ pub(crate) fn body(
});
ui.add_space(12.0);
let (mut go, mut cancel) = (false, false);
// The paper is kept for this printer (or Save as PDF) when this is ticked at Print time. On its
// own row above the buttons: sharing their row moved them while the dialog settled.
let for_whom = d.printer.as_deref().map_or_else(|| tl!("Save as PDF").to_string(), printer_label);
ui.add(widgets::checkbox(&mut d.remember_paper, crate::i18n::fmt(tl!("Remember this paper for {printer}"), &[("printer", &for_whom)])));
ui.add_space(8.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let looking = still_looking(d);
let label = if d.printer.is_some() || looking { tl!("Print") } else { tl!("Save as PDF") };
@@ -600,35 +569,5 @@ pub(crate) fn body(
cancel = true;
}
});
if go {
d.remember_now();
}
(go, cancel)
}
/// The row that adds a custom paper: name, width and height in the preferences' unit.
fn custom_paper_row(ui: &mut egui::Ui, d: &mut PrintDraft, t: &Tokens) {
let unit = d.units;
let Some((name, width, height)) = d.custom_new.as_mut() else { return };
let side = |s: &str| s.trim().replace(',', ".").parse::<f64>().ok().map(|v| v / unit.per_point());
let paper = match (side(width), side(height)) {
(Some(w), Some(h)) => crate::print_paper::CustomPaper::new(name, w, h),
_ => None,
};
let (mut add, mut cancel) = (false, false);
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(name).hint_text(tl!("Name")).desired_width(110.0)).accessible_name(tl!("Name"));
ui.add(egui::TextEdit::singleline(width).hint_text(tl!("Width")).desired_width(52.0)).accessible_name(tl!("Width"));
ui.label("×");
ui.add(egui::TextEdit::singleline(height).hint_text(tl!("Height")).desired_width(52.0)).accessible_name(tl!("Height"));
ui.label(egui::RichText::new(unit.label()).color(t.text_muted));
add = ui.add_enabled(paper.is_some(), egui::Button::new(tl!("Add"))).clicked();
cancel = ui.button(tl!("Cancel")).clicked();
});
if add && let Some(paper) = paper {
d.add_custom_paper(paper);
d.custom_new = None;
} else if cancel {
d.custom_new = None;
}
}
+2 -15
View File
@@ -1,4 +1,4 @@
use super::{Binding, BookletSubset, Content, Layout, Orientation, PageOrder, SizeMode, Subset, print, spool};
use super::{Binding, BookletSubset, Content, Layout, Orientation, PAPERS, PageOrder, SizeMode, Subset, print, spool};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Which {
@@ -62,16 +62,7 @@ pub struct PrintDraft {
/// Print as image (Acrobat's Advanced Print Setup) at `dpi`.
pub as_image: bool,
pub dpi: f64,
/// Index into the paper list: the standard sizes, then `custom_papers`.
pub paper: usize,
/// Paper sizes added with Custom…, kept with the settings.
pub custom_papers: Vec<crate::print_paper::CustomPaper>,
/// The paper each printer is remembered with, kept with the settings.
pub remembered_paper: crate::print_paper::RememberedPaper,
/// "Remember this paper for …", applied at Print time.
pub remember_paper: bool,
/// The custom paper being added: name, width and height as typed (the preferences' unit).
pub custom_new: Option<(String, String, String)>,
/// The paper chosen before Label put label stock in, so leaving Label gives it back (#3564).
pub paper_before_label: Option<usize>,
/// The sheet shown in the preview (0-based).
@@ -135,10 +126,6 @@ impl Default for PrintDraft {
as_image: false,
dpi: 300.0,
paper: 0,
custom_papers: Vec::new(),
remembered_paper: Default::default(),
remember_paper: false,
custom_new: None,
paper_before_label: None,
sheet: 0,
current_page: 0,
@@ -219,7 +206,7 @@ impl PrintDraft {
};
Ok(print::Settings {
pages,
paper: self.paper_size(),
paper: PAPERS[self.paper.min(PAPERS.len() - 1)].1,
orientation: self.orientation,
layout,
content: self.content,
-2
View File
@@ -66,8 +66,6 @@ mod pointer;
mod preferences_layout;
mod prepare;
mod print;
mod print_custom_paper_names;
mod print_late_printer_list;
mod protect;
mod reading_colors;
mod recent_files;
-92
View File
@@ -918,98 +918,6 @@ fn restored_paper_keeps_preview_and_saved_sheet_dimensions() {
std::fs::remove_file(out).unwrap();
}
/// After the Print dialog opens: frames until its printer list has loaded (its arrival changes
/// the dialog's rows, even an empty list, which adds the "no printers" line), then a few more so
/// the dialog has settled where it stays. A click before then can land where a control was
/// (windows-latest CI).
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
for _ in 0..500 {
h.run_steps(1);
if !h.state().print_draft.printers_loading {
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert!(!h.state().print_draft.printers_loading, "the printer list loaded");
h.run_steps(8);
}
/// Page setup: a paper size of one's own joins the Paper list (Custom…), the paper is kept for
/// the printer when "Remember this paper for …" is ticked at Print time, starts the dialog next
/// time, survives a restart with the settings, and is forgotten when unticked; Remove takes a
/// custom size off the list.
#[test]
fn custom_papers_and_the_paper_remembered_for_a_printer() {
let mut h = harness();
// No printers here: Save as PDF is the dialog's printer, now and when it opens again.
h.state_mut().printer_source = Some(std::sync::Arc::new(Vec::new));
assert!(h.state_mut().execute("print.dialog"));
settle(&mut h);
assert_eq!(h.state().print_draft.printer, None);
h.get_by_label("Custom…").click();
h.run_steps(2);
// Typed in the preferences' unit: 4 × 6 inches.
let per_point = h.state().print_draft.units.per_point();
h.state_mut().print_draft.custom_new = Some(("Label 4×6".into(), format!("{}", 432.0 * per_point), format!("{}", 288.0 * per_point)));
h.run_steps(2);
h.get_by_label("Add").click();
h.run_steps(2);
{
let d = &h.state().print_draft;
assert!(d.paper_is_custom() && d.paper_name() == "Label 4×6", "{}", d.paper_name());
let (w, l) = d.paper_size();
assert!((w - 288.0).abs() < 0.01 && (l - 432.0).abs() < 0.01, "portrait: {w}×{l}");
}
// The custom row has closed: let the dialog settle at its new height before clicking.
h.run_steps(8);
h.get_by_label("Remember this paper for Save as PDF").click();
h.run_steps(1);
assert!(h.state().print_draft.remember_paper, "ticked");
let out = std::env::temp_dir().join(format!("pdfcraft-print-paper-test-{}.pdf", std::process::id()));
h.state_mut().save_override = Some(out.to_string_lossy().into_owned());
h.run_steps(8);
h.get_by_label("Save as PDF").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
let bytes = std::fs::read(&out).expect("saved");
let _ = std::fs::remove_file(&out);
let saved = pdfcraft_cos::Document::open(std::sync::Arc::new(bytes)).unwrap();
let sheet = pdfcraft_model::pages(&saved)[0].display_size(&saved);
assert!((sheet.0.min(sheet.1) - 288.0).abs() < 0.5 && (sheet.0.max(sheet.1) - 432.0).abs() < 0.5, "the label paper: {sheet:?}");
assert_eq!(h.state().print_draft.remembered_paper.get("").map(String::as_str), Some("Label 4×6"));
// Next time the dialog starts with it.
h.state_mut().print_draft.paper = 0;
assert!(h.state_mut().execute("print.dialog"));
settle(&mut h);
assert_eq!((h.state().print_draft.paper_name().as_str(), h.state().print_draft.remember_paper), ("Label 4×6", true));
// And after a restart.
let settings = h.state().persist();
let mut fresh = PdfCraftApp::new();
fresh.restore(&settings);
assert_eq!(fresh.print_draft.paper_count(), h.state().print_draft.paper_count());
assert_eq!(fresh.print_draft.remembered_paper, h.state().print_draft.remembered_paper);
// Unticked at Print time, the paper is forgotten for Save as PDF.
h.get_by_label("Remember this paper for Save as PDF").click();
h.run_steps(1);
assert!(!h.state().print_draft.remember_paper, "unticked");
h.state_mut().save_override = Some(out.to_string_lossy().into_owned());
h.run_steps(8);
h.get_by_label("Save as PDF").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None, "saved");
let _ = std::fs::remove_file(&out);
assert!(h.state().print_draft.remembered_paper.is_empty());
// Remove takes the custom size off the list.
assert!(h.state_mut().execute("print.dialog"));
settle(&mut h);
let custom = h.state().print_draft.paper_count() - 1;
h.state_mut().print_draft.paper = custom;
h.run_steps(2);
h.get_by_label("Remove").click();
h.run_steps(2);
assert_eq!((h.state().print_draft.paper_count(), h.state().print_draft.paper), (custom, 0));
}
/// #2707: a booklet decides two-sided itself, as Acrobat does (its Print dialog hides Two-sided
/// while Booklet is chosen): the job flips on the short edge for both sides and prints
/// one-sided for the front or back only, and the Two-sided choice is kept for other layouts.
@@ -1,62 +0,0 @@
//! A custom paper cannot take the name of a standard one: papers are looked up by name, so the
//! standard size would be found first and a remembered custom paper would come back as it.
use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use pdfcraft_ui_egui::PdfCraftApp;
fn harness() -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("form.pdf", None, include_bytes!("../data/form.pdf").to_vec()).unwrap();
app
});
h.run_steps(4);
h
}
#[test]
fn a_custom_paper_cannot_take_a_standard_name() {
let mut h = harness();
// No printers here: the list is empty as soon as it is asked for.
h.state_mut().printer_source = Some(std::sync::Arc::new(Vec::new));
assert!(h.state_mut().execute("print.dialog"));
for _ in 0..500 {
h.run_steps(1);
if !h.state().print_draft.printers_loading {
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert!(!h.state().print_draft.printers_loading, "the printer list loaded");
h.run_steps(8);
h.get_by_label("Custom…").click();
h.run_steps(2);
let before = h.state().print_draft.paper_count();
let per_point = h.state().print_draft.units.per_point();
let (w, l) = (format!("{}", 288.0 * per_point), format!("{}", 432.0 * per_point));
// The name of a standard paper, in either case, is refused: Add stays disabled.
for name in ["A4", " a4 "] {
h.state_mut().print_draft.custom_new = Some((name.into(), w.clone(), l.clone()));
h.run_steps(3);
assert!(h.get_by_role_and_label(Role::Button, "Add").accesskit_node().is_disabled(), "{name:?} is a standard paper's name");
assert_eq!(h.state().print_draft.paper_count(), before);
}
// Any other name is accepted by the same row.
h.state_mut().print_draft.custom_new = Some(("A4 label".into(), w.clone(), l.clone()));
h.run_steps(3);
assert!(!h.get_by_role_and_label(Role::Button, "Add").accesskit_node().is_disabled());
// Settings read back are checked the same way: the custom "A4" is dropped, the rest kept.
let paper = |name: &str| serde_json::json!({ "name": name, "width": 288.0, "height": 432.0 });
let settings = serde_json::json!({
"print_custom_papers": [paper("A4"), paper("Label"), paper("us letter")],
"print_paper_by_printer": { "": "A4" },
});
let mut fresh = PdfCraftApp::new();
fresh.restore(&settings.to_string());
let names: Vec<&str> = fresh.print_draft.custom_papers.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["Label"]);
}
@@ -1,66 +0,0 @@
//! A printer list that arrives late must not undo what was picked while it was being read, when
//! the printer it names is the one the dialog already has.
use egui_kittest::Harness;
use pdfcraft_engine::print::spool::Printer;
use pdfcraft_ui_egui::PdfCraftApp;
use std::sync::{Arc, Mutex};
fn harness() -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("form.pdf", None, include_bytes!("../data/form.pdf").to_vec()).unwrap();
app
});
h.run_steps(4);
h
}
fn wait_for_list(h: &mut Harness<'static, PdfCraftApp>) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while h.state().print_draft.printers_loading && std::time::Instant::now() < deadline {
h.run_steps(1);
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(!h.state().print_draft.printers_loading, "the printer list loaded");
}
#[test]
fn a_late_list_keeps_the_paper_picked_meanwhile() {
let mut h = harness();
// The source waits for the test to let it answer, once the first list is in.
let gate = Arc::new(Mutex::new(()));
let wait = gate.clone();
h.state_mut().printer_source = Some(Arc::new(move || {
let _open = wait.lock();
vec![Printer { name: "Office".into(), default: true, two_sided: None, description: String::new() }]
}));
assert!(h.state_mut().execute("print.dialog"));
wait_for_list(&mut h);
h.run_steps(4);
assert_eq!(h.state().print_draft.printer.as_deref(), Some("Office"));
// "Office" is remembered with Tabloid (the third size).
h.state_mut().print_draft.remembered_paper.insert("Office".into(), "Tabloid".into());
h.state_mut().dialog = None;
let hold = gate.lock().unwrap();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(2);
assert!(h.state().print_draft.printers_loading, "the fresh list is still being read");
assert_eq!(h.state().print_draft.printer.as_deref(), Some("Office"), "last time's list shows at once");
assert_eq!((h.state().print_draft.paper_name().as_str(), h.state().print_draft.remember_paper), ("Tabloid", true));
// Meanwhile the person picks another paper and unticks Remember.
{
let d = &mut h.state_mut().print_draft;
d.paper = 0;
d.remember_paper = false;
}
drop(hold);
wait_for_list(&mut h);
h.run_steps(2);
let d = &h.state().print_draft;
assert_eq!(d.printer.as_deref(), Some("Office"), "the same printer");
assert_eq!((d.paper, d.remember_paper), (0, false), "the paper and the Remember box are as they were left");
}
+5 -5
View File
@@ -8390,11 +8390,11 @@ title = "Page setup"
area = "L"
tier = "P1"
milestone = "M10"
status = "partial"
commands = ["print.dialog"]
tools = ["doc_print"]
evidence = ["crates/ui-egui/tests/it/print.rs::custom_papers_and_the_paper_remembered_for_a_printer", "crates/ui-egui/src/workflows/print_paper.rs::the_paper_is_remembered_per_printer_only_when_asked", "crates/ui-egui/src/workflows/print_paper.rs::settings_are_read_as_untrusted", "crates/automation/tests/it/automation.rs::doc_print_takes_a_paper_size_of_its_own"]
notes = "In the Print dialog, as the rest of printing is (no separate Page Setup window): Custom… beside Paper adds a named size in the preferences' unit (1 to 200 inches a side), kept with the settings and removable; 'Remember this paper for <printer>' at Print time keeps the paper for that printer (or Save as PDF) and the dialog starts with it. doc_print takes sizes such as 8.5x14in or 210x297mm. Missing: margins, and the dialog's other choices (orientation, two-sided, sizing and so on) remembered per printer."
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
[[feature]]
id = "print.print-ready-pdf"