Files
pdfcraft/crates/ui-egui/tests/it/marks.rs
T
alehel 9aaac56417 Merge remote-tracking branch 'refs/remotes/origin/main' into fix/background-hidden-by-page-backdrop
# Conflicts:
#	crates/engine/src/tests.rs
#	crates/ui-egui/src/dialogs/marks_ui.rs
#	parity/acrobat-features.toml
2026-10-28 23:18:52 +01:00

633 lines
28 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.
//! Header & footer, watermark and background dialogs in the real shell (egui_kittest).
use crate::support;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use pdfcraft_engine::MarkKind;
use pdfcraft_ui_egui::{Dialog, PdfCraftApp};
const FIXTURE: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 /MediaBox [0 0 300 400] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
4 0 obj << /Type /Page /Parent 2 0 R >> endobj
5 0 obj << /Type /Page /Parent 2 0 R >> endobj
trailer << /Root 1 0 R >>
%%EOF";
fn harness() -> Harness<'static, PdfCraftApp> {
harness_with(FIXTURE.to_vec())
}
fn harness_with(pdf: Vec<u8>) -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("doc.pdf", None, pdf.clone()).unwrap();
app
});
h.run_steps(4);
h
}
fn marks(h: &Harness<'static, PdfCraftApp>) -> Vec<MarkKind> {
let s = h.state();
s.session.get(s.views[0].id).unwrap().marks().to_vec()
}
#[test]
fn header_and_footer_dialog_inserts_tokens_and_applies_to_a_subset() {
let mut h = harness();
assert!(!h.state_mut().execute("edit.header_footer.remove"), "nothing to remove yet");
assert!(h.state_mut().execute("edit.header_footer"));
h.run_steps(3);
assert_eq!(h.state().dialog, Some(Dialog::Marks(MarkKind::HeaderFooter)));
// The center footer box takes the page-number token.
h.state_mut().marks_draft.focused_box = 4;
h.get_by_label("Insert Page Number").click();
h.run_steps(2);
assert_eq!(h.state().marks_draft.hf.text[4], "<<1>>");
h.state_mut().marks_draft.range.subset = pdfcraft_ui_egui::marks::Subset::Odd;
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(4);
assert_eq!(h.state().dialog, None);
assert_eq!(marks(&h), [MarkKind::HeaderFooter]);
// Update and Remove are now available.
assert!(h.state_mut().execute("edit.header_footer.update"));
h.run_steps(2);
h.get_by_label("Update Header and Footer");
h.get_by_label("Cancel").click();
h.run_steps(2);
assert!(h.state_mut().execute("edit.header_footer.remove"));
assert!(marks(&h).is_empty());
}
#[test]
fn header_and_footer_dialog_draws_in_the_chosen_face() {
// #3754: the dialog offers Helvetica, Times and Courier (it only said "Font: Helvetica"),
// and the face chosen is the one the page's header is drawn in.
let mut h = harness();
assert!(h.state_mut().execute("edit.header_footer"));
h.run_steps(3);
h.get_by_label("Font:");
h.get_by_value("Helvetica").click();
h.run_steps(2);
h.get_by_label("Times").click();
h.run_steps(2);
assert_eq!(h.state().marks_draft.hf.font, pdfcraft_engine::FontFamily::Times);
h.state_mut().marks_draft.hf.text[0] = "Serif".into();
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(4);
assert_eq!(marks(&h), [MarkKind::HeaderFooter]);
let s = h.state();
let doc = pdfcraft_cos::Document::open(s.session.get(s.views[0].id).unwrap().bytes.clone()).unwrap();
let page = &pdfcraft_model::pages(&doc)[0];
let res = doc.resolve(page.dict.get(b"Resources").unwrap());
let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap()).as_dict().cloned().unwrap();
let face = doc.resolve(fonts.get(b"PCTimes").expect("Times is added")).as_dict().cloned().unwrap();
assert_eq!(face.name(b"BaseFont"), Some(&b"Times-Roman"[..]));
}
#[test]
fn header_and_footer_dialog_can_shrink_the_page_content() {
let mut h = harness();
assert!(h.state_mut().execute("edit.bates"));
h.run_steps(3);
assert!(!h.state().marks_draft.hf.shrink, "off by default, as in Acrobat");
h.get_by_label("Shrink the page content to make room for the header and footer").click();
h.run_steps(2);
assert!(h.state().marks_draft.hf.shrink);
h.get_by_label("OK").click();
h.run_steps(4);
assert_eq!(h.state().dialog, None);
assert_eq!(marks(&h), [MarkKind::HeaderFooter]);
}
#[test]
fn watermark_and_background_dialogs() {
let mut h = harness();
h.state_mut().execute("edit.watermark");
h.run_steps(2);
assert!(h.query_by(|n| n.label().as_deref() == Some("OK") && n.is_disabled()).is_some(), "OK waits for text");
h.get_by_label("Offset from centre");
h.state_mut().marks_draft.wm.text = "DRAFT".into();
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(3);
h.state_mut().execute("edit.background");
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(marks(&h).len(), 2);
}
#[test]
fn backgrounds_and_watermarks_from_a_file() {
let mut h = harness();
h.state_mut().execute("edit.background");
h.run_steps(2);
support::outside_menus(&h, "File").click();
h.run_steps(2);
h.get_by_label("No file chosen");
assert!(h.query_by(|n| n.label().as_deref() == Some("OK") && n.is_disabled()).is_some(), "OK waits for a file");
// (Browse… opens the system picker; the test supplies the file directly.)
h.state_mut().marks_draft.file = Some(("letterhead.pdf".into(), std::sync::Arc::new(FIXTURE.to_vec())));
h.run_steps(2);
h.get_by_label("Page number");
h.get_by_label("Scale relative to target page");
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(marks(&h), vec![MarkKind::Background]);
let s = h.state();
assert_eq!(s.session.get(s.views[0].id).unwrap().can_undo(), Some("Add background"));
}
/// Three pages: a white backdrop of its own (as browsers write), a page-sized picture (a scan)
/// and bare paper.
fn backdrop_fixture() -> Vec<u8> {
let stream = |data: &str| format!("<< /Length {} >> stream\n{data}\nendstream", data.len());
let objs = [
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
"<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 /MediaBox [0 0 300 400] /Resources << /XObject << /Im0 9 0 R >> >> >>".into(),
"<< /Type /Page /Parent 2 0 R /Contents 6 0 R >>".into(),
"<< /Type /Page /Parent 2 0 R /Contents 7 0 R >>".into(),
"<< /Type /Page /Parent 2 0 R /Contents 8 0 R >>".into(),
stream("q 1 1 1 rg 0 0 300 400 re f Q 0 g 20 20 50 50 re f"),
stream("q 300 0 0 400 0 0 cm /Im0 Do Q"),
stream("0 g 20 20 50 50 re f"),
"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >> stream\n\u{80}\nendstream"
.into(),
];
let mut out = String::from("%PDF-1.7\n");
for (i, o) in objs.iter().enumerate() {
out.push_str(&format!("{} 0 obj {o} endobj\n", i + 1));
}
out.push_str("trailer << /Root 1 0 R >>\n%%EOF");
out.into_bytes()
}
#[test]
fn the_background_dialog_offers_to_replace_a_white_backdrop_and_warns_about_scans() {
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("doc.pdf", None, backdrop_fixture()).unwrap();
app
});
h.run_steps(4);
let backdrop = |h: &Harness<'static, PdfCraftApp>, page| {
let s = h.state();
s.session.get(s.views[0].id).unwrap().page_backdrop(page)
};
assert_eq!(backdrop(&h, 0), Some(pdfcraft_engine::Backdrop::Fill));
h.state_mut().execute("edit.background");
h.run_steps(3);
let replace = "Page 1 paints its own white backdrop, which would hide the background. Replace it";
h.get_by_label("A picture covers page 2 (a scan?), so the background won't show there.");
assert!(h.state().marks_draft.bg.replace_backdrop, "on by default");
// Left unticked, the backdrop stays and still hides the background.
h.get_by_label(replace).click();
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(backdrop(&h, 0), Some(pdfcraft_engine::Backdrop::Fill));
assert!(h.state_mut().execute("edit.undo"));
h.run_steps(2);
// Reopened, the choice starts on again; ticked, the backdrop is replaced and the scan is
// reported, not touched.
h.state_mut().execute("edit.background");
h.run_steps(3);
h.get_by_label(replace);
assert!(h.state().marks_draft.bg.replace_backdrop, "on again when the dialog opens");
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(backdrop(&h, 0), None);
assert_eq!(backdrop(&h, 1), Some(pdfcraft_engine::Backdrop::Image));
// Nothing left to replace: the offer is gone. A choice left off from an earlier dialog
// doesn't carry into Update, which restores the backdrop first and would otherwise keep it
// with no checkbox shown.
h.state_mut().marks_draft.bg.replace_backdrop = false;
h.state_mut().execute("edit.background.update");
h.run_steps(3);
assert!(h.query_by_label(replace).is_none());
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(backdrop(&h, 0), None, "Update keeps the backdrop replaced");
}
/// The preview shows what OK will do to each page: the colour on bare paper and on a page whose
/// white backdrop is replaced, none on a scan, and none on that page with Replace unticked.
#[test]
fn the_background_preview_follows_the_replace_checkbox() {
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("doc.pdf", None, backdrop_fixture()).unwrap();
app
});
h.run_steps(4);
assert!(h.state_mut().execute("edit.background"));
h.run_steps(3);
let replace = "Page 1 paints its own white backdrop, which would hide the background. Replace it";
h.get_by_label(replace);
let [red, green, blue] = pdfcraft_engine::Background::default().color.map(|c| (c * 255.0).round() as u8);
let colour = egui::Color32::from_rgb(red, green, blue);
let tint = |h: &mut Harness<'static, PdfCraftApp>, page: usize| {
h.state_mut().marks_draft.preview = Some(page);
h.run_steps(2);
wait_for_picture(h, page).2
};
assert_eq!(tint(&mut h, 0), colour, "page 1's backdrop is replaced, so the colour shows");
assert_eq!(tint(&mut h, 1), egui::Color32::WHITE, "a picture covers page 2");
assert_eq!(tint(&mut h, 2), colour, "page 3 is bare paper");
h.get_by_label(replace).click();
h.run_steps(2);
assert!(!h.state().marks_draft.bg.replace_backdrop);
assert_eq!(tint(&mut h, 0), egui::Color32::WHITE, "unticked, page 1 keeps its backdrop, which hides the colour");
assert_eq!(tint(&mut h, 2), colour, "page 3 has no backdrop to keep");
}
/// Remove background says which pages keep their own white backdrop off: here one whose
/// backdrop copy changed before the fill since the background replaced it.
#[test]
fn remove_background_says_which_pages_keep_their_own_backdrop_off() {
let copy = "1 1 1 rg 0 0 300 400 re n 0 g 20 20 50 50 re f";
let bg = "q 1 0 0 rg 0 0 300 400 re f Q";
let fixture = format!(
"%PDF-1.7\n\
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >> endobj\n\
3 0 obj << /Type /Page /Parent 2 0 R /Contents [4 0 R 5 0 R] >> endobj\n\
4 0 obj << /PCMark /Background /Length {} >> stream\n{bg}\nendstream endobj\n\
5 0 obj << /PCBackdrop << /Index 2 /Op (f) /Before <{}> >> /Length {} >> stream\n{copy}\nendstream endobj\n\
trailer << /Root 1 0 R >>\n%%EOF",
bg.len(),
"00".repeat(32),
copy.len()
);
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("doc.pdf", None, fixture.into_bytes()).unwrap();
app
});
h.run_steps(4);
assert_eq!(marks(&h), [MarkKind::Background]);
h.state_mut().execute("edit.background.remove");
h.run_steps(2);
assert!(marks(&h).is_empty());
let toast = h.state().toast.as_ref().map(|(t, _)| t.clone()).unwrap_or_default();
assert_eq!(toast, "Page 1 changed after the background was added, so its own white backdrop stays off. It looks the same on white paper.");
}
/// Two 300 × 400 pages with content (a dark bar), the second turned a quarter (/Rotate 90), so it
/// is shown landscape, 400 × 300.
fn content_fixture() -> Vec<u8> {
let bar = "0 0 0 rg 40 300 220 60 re f";
format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 300 400] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /Contents 5 0 R >> endobj
4 0 obj << /Type /Page /Parent 2 0 R /Rotate 90 /Contents 5 0 R >> endobj
5 0 obj << /Length {} >>
stream
{bar}
endstream
endobj
trailer << /Root 1 0 R >>
%%EOF",
bar.len()
)
.into_bytes()
}
/// The pictures of page `page` (its thumbnail) painted last frame: paint order, bounds, tint.
fn page_pictures(h: &Harness<'static, PdfCraftApp>, page: usize) -> Vec<(usize, egui::Rect, egui::Color32)> {
let Some(tex) = h.state().views[0].thumb(page).map(|t| t.id()) else { return Vec::new() };
h.output()
.shapes
.iter()
.enumerate()
.filter_map(|(k, s)| match &s.shape {
egui::Shape::Mesh(m) if m.texture_id == tex => Some((k, m.calc_bounds(), m.vertices.first()?.color)),
_ => None,
})
.collect()
}
/// The preview's picture of `page`, once its thumbnail has arrived (it renders off the UI thread).
fn wait_for_picture(h: &mut Harness<'static, PdfCraftApp>, page: usize) -> (usize, egui::Rect, egui::Color32) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
loop {
h.step();
if let [one] = page_pictures(h, page)[..] {
return one;
}
assert!(std::time::Instant::now() < deadline, "the preview never showed page {}'s picture", page + 1);
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
/// A text painted on the page last frame (the dialog's text boxes show it too): its paint order
/// and its laid-out rectangle.
fn painted(h: &Harness<'static, PdfCraftApp>, text: &str, page: egui::Rect) -> (usize, egui::Rect) {
h.output()
.shapes
.iter()
.enumerate()
.find_map(|(k, s)| match &s.shape {
egui::Shape::Text(t) if t.galley.text() == text => {
Some((k, t.galley.rect.translate(t.pos.to_vec2()))).filter(|(_, r)| page.contains(r.center()))
}
_ => None,
})
.unwrap_or_else(|| panic!("{text:?} is painted on the page"))
}
fn near(a: f32, b: f32, what: &str) {
assert!((a - b).abs() < 1.0, "{what}: {a} against {b}");
}
#[test]
fn the_preview_shows_the_page_the_range_selects_under_the_marks() {
use pdfcraft_ui_egui::marks::{PageRange, Subset};
let mut h = harness_with(content_fixture());
assert!(h.state_mut().execute("edit.header_footer"));
h.run_steps(2);
{
let d = &mut h.state_mut().marks_draft;
d.hf.text[1] = "TOP".into();
d.hf.text[5] = "END".into();
// Page 2 alone, not the current page: the preview follows the range (#2837).
d.range = PageRange { all: false, from: 2, to: 2, subset: Subset::All };
}
let (at, page, tint) = wait_for_picture(&mut h, 1);
h.get_by_label("Preview: page 2 of 2");
assert_eq!(tint, egui::Color32::WHITE);
assert!((page.width() / page.height() - 4.0 / 3.0).abs() < 0.01, "page 2 is shown turned, as displayed: {page:?}");
// The marks are drawn over the page, where the engine puts them on its displayed box
// (default margins: 36 pt top and bottom, 72 pt left and right).
let s = page.width() / 400.0;
let (top_at, top) = painted(&h, "TOP", page);
let (end_at, end) = painted(&h, "END", page);
assert!(at < top_at && at < end_at, "the page is under the marks");
near(top.center().x, page.center().x, "centre header, across");
near(top.center().y, page.top() + 36.0 * s, "centre header, down");
near(end.right(), page.right() - 72.0 * s, "right footer, across");
near(end.center().y, page.bottom() - 36.0 * s, "right footer, down");
h.get_by_label("Cancel").click();
h.run_steps(2);
// The watermark sits on the page's centre.
assert!(h.state_mut().execute("edit.watermark"));
h.run_steps(2);
{
let d = &mut h.state_mut().marks_draft;
d.wm.text = "DRAFT".into();
d.wm.rotation = 0.0;
}
let (at, page, _) = wait_for_picture(&mut h, 1);
let (mark_at, mark) = painted(&h, "DRAFT", page);
assert!(at < mark_at, "the page is under the watermark");
near(mark.center().x, page.center().x, "watermark, across");
near(mark.center().y, page.center().y, "watermark, down");
h.get_by_label("Cancel").click();
h.run_steps(2);
// A background colour shows where the page is white: it tints the page's picture.
assert!(h.state_mut().execute("edit.background"));
h.run_steps(2);
let (_, _, tint) = wait_for_picture(&mut h, 1);
let [red, green, blue] = pdfcraft_engine::Background::default().color.map(|c| (c * 255.0).round() as u8);
assert_eq!(tint, egui::Color32::from_rgb(red, green, blue), "the default background colour, opaque");
}
/// The preview's page stepper, beside its "page N of M" label: the dialog's own button, not the
/// page navigation's of the same name.
fn preview_step(h: &mut Harness<'static, PdfCraftApp>, label: &str, now: &str) {
let row = h.get_by_label(now).rect().center().y;
h.query_all_by_label(label).find(|n| (n.rect().center().y - row).abs() < 15.0).unwrap_or_else(|| panic!("{label} beside {now:?}")).click();
h.run_steps(2);
}
#[test]
fn the_preview_steps_through_the_pages_and_starts_at_the_current_one() {
let mut h = harness();
h.state_mut().views[0].go_to_page(1);
h.run_steps(2);
assert!(h.state_mut().execute("edit.watermark"));
h.run_steps(2);
h.get_by_label("Preview: page 2 of 3");
preview_step(&mut h, "Next page", "Preview: page 2 of 3");
h.get_by_label("Preview: page 3 of 3");
preview_step(&mut h, "Previous page", "Preview: page 3 of 3");
preview_step(&mut h, "Previous page", "Preview: page 2 of 3");
h.get_by_label("Preview: page 1 of 3");
// Opened again, the preview is back at the current page.
h.get_by_label("Cancel").click();
h.run_steps(2);
assert!(h.state_mut().execute("edit.watermark"));
h.run_steps(2);
h.get_by_label("Preview: page 2 of 3");
}
/// Keeps every texture the app uploads, so a test can read what a picture it paints shows.
#[derive(Clone, Default)]
struct Uploads(std::sync::Arc<std::sync::Mutex<std::collections::HashMap<egui::TextureId, std::sync::Arc<egui::ColorImage>>>>);
impl egui_kittest::TestRenderer for Uploads {
fn handle_delta(&mut self, delta: &mut egui::TexturesDelta) {
let mut seen = self.0.lock().unwrap();
for (id, images) in delta.set.drain() {
for d in images.into_iter().filter(|d| d.pos.is_none()) {
let egui::ImageData::Color(image) = d.image;
seen.insert(id, image);
}
}
// Freed textures stay readable: ids are never reused.
delta.free.clear();
}
fn render(&mut self, _: &egui::Context, _: &egui::FullOutput) -> Result<image::RgbaImage, String> {
Err("this renderer only keeps the uploads".into())
}
}
/// The page picture the dialog's preview painted last frame, left of its "Preview: page …" label,
/// once its texture is in: its tint and its image.
fn preview_picture(h: &Harness<'static, PdfCraftApp>, uploads: &Uploads) -> Option<(egui::Color32, std::sync::Arc<egui::ColorImage>)> {
let label = h.query_by_label_contains("Preview: page")?.rect();
h.output().shapes.iter().find_map(|s| {
let egui::Shape::Mesh(m) = &s.shape else { return None };
let r = m.calc_bounds();
// The size of the 210-point box left of the label (which is centred down its side), and
// not the stepper's arrow beside the label. A landscape page is shorter than the box, so
// it can sit below the label: within half the box of it.
let beside = r.width() > 50.0
&& r.height() <= 215.0
&& r.right() <= label.left()
&& label.left() - r.right() < 260.0
&& (r.center().y - label.center().y).abs() <= 105.0;
if !beside {
return None;
}
let image = uploads.0.lock().unwrap().get(&m.texture_id).cloned()?;
Some((m.vertices.first()?.color, image))
})
}
/// [`preview_picture`], waiting until `ready` accepts it (the page renders off the UI thread).
fn wait_for_preview(
h: &mut Harness<'static, PdfCraftApp>,
uploads: &Uploads,
what: &str,
ready: impl Fn(&egui::ColorImage) -> bool,
) -> (egui::Color32, std::sync::Arc<egui::ColorImage>) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
loop {
h.step();
let shown = preview_picture(h, uploads);
if let Some((tint, image)) = shown.clone().filter(|(_, image)| ready(image)) {
return (tint, image);
}
assert!(
std::time::Instant::now() < deadline,
"the preview never showed {what}; last (tint, size, middle of the lower half): {:?}",
shown.map(|(tint, i)| (tint, i.size, at(&i, 0.5, 0.75)))
);
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
/// The pixel of `image` at (u, v), from 0 to 1 across and down.
fn at(image: &egui::ColorImage, u: f32, v: f32) -> egui::Color32 {
let [w, h] = image.size;
let (x, y) = (((u * w as f32) as usize).min(w - 1), ((v * h as f32) as usize).min(h - 1));
image.pixels[y * w + x]
}
fn count(image: &egui::ColorImage, f: impl Fn(egui::Color32) -> bool) -> usize {
image.pixels.iter().filter(|&&c| f(c)).count()
}
/// Update draws its preview on the page without the marks it replaces, which the page's thumbnail
/// still has: a pale yellow background updated to light blue previews light blue, not blue
/// multiplied onto the yellow, and a new watermark or header isn't drawn over the old one. The
/// page's content and its other kinds of mark stay.
#[test]
fn update_previews_the_page_without_the_marks_it_replaces() {
let uploads = Uploads::default();
let pdf = content_fixture();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).renderer(uploads.clone()).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("doc.pdf", None, pdf.clone()).unwrap();
app
});
h.run_steps(4);
let yellow = egui::Color32::from_rgb(255, 255, 153);
let red = |c: egui::Color32| c.r() > 200 && c.g() < 80 && c.b() < 80;
let blue = |c: egui::Color32| c.b() > 150 && c.r() < 100 && c.g() < 100;
let dark = |c: egui::Color32| c.r() < 60 && c.g() < 60 && c.b() < 60;
let apply = |h: &mut Harness<'static, PdfCraftApp>, command: &str, draft: &dyn Fn(&mut pdfcraft_ui_egui::marks::MarksDraft)| {
assert!(h.state_mut().execute(command));
h.run_steps(2);
draft(&mut h.state_mut().marks_draft);
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(3);
};
// A pale yellow background, a red watermark and a blue header.
apply(&mut h, "edit.background", &|d| d.bg.color = [1.0, 1.0, 0.6]);
apply(&mut h, "edit.watermark", &|d| {
d.wm.text = "OLD".into();
d.wm.color = [1.0, 0.0, 0.0];
d.wm.opacity = 1.0;
d.wm.rotation = 0.0;
});
apply(&mut h, "edit.header_footer", &|d| {
d.hf.text[1] = "OLD".into();
d.hf.color = [0.0, 0.0, 1.0];
d.hf.font_size = 40.0;
});
// Add keeps the marks there, and previews on the thumbnail, which has all three.
assert!(h.state_mut().execute("edit.watermark"));
h.run_steps(2);
let all = |i: &egui::ColorImage| at(i, 0.5, 0.75) == yellow && count(i, red) > 200 && count(i, blue) > 40;
wait_for_preview(&mut h, &uploads, "the page with its marks", all);
h.get_by_label("Cancel").click();
h.run_steps(2);
assert!(h.state_mut().execute("edit.background.update"));
h.run_steps(2);
h.state_mut().marks_draft.bg.color = [0.6, 0.8, 1.0];
h.step();
// Update shows the page without the marks from the frame it opens: neither a blank sheet nor
// the old marks while it renders.
let shown = |h: &Harness<'static, PdfCraftApp>| preview_picture(h, &uploads).expect("a page is previewed");
let (tint, page) = shown(&h);
assert_eq!(tint, egui::Color32::from_rgb(153, 204, 255), "the new colour tints the page");
assert_eq!(at(&page, 0.5, 0.75), egui::Color32::WHITE, "the page is previewed without the yellow background");
assert!(count(&page, red) > 200 && count(&page, blue) > 40 && count(&page, dark) > 1000, "the watermark, header and page content stay");
// So does a step to page 2: the pages are prepared once, and each is drawn as it is shown.
h.state_mut().marks_draft.preview = Some(1);
h.step();
assert_eq!(at(&shown(&h).1, 0.5, 0.75), egui::Color32::WHITE, "page 2 is previewed without its background at once");
h.get_by_label("Cancel").click();
h.run_steps(2);
assert!(h.state_mut().execute("edit.watermark.update"));
h.run_steps(2);
h.state_mut().marks_draft.wm.text = "NEW".into();
h.step();
let (_, page) = shown(&h);
assert_eq!(count(&page, red), 0, "the new watermark isn't previewed over the old one");
assert!(at(&page, 0.5, 0.75) == yellow && count(&page, blue) > 40, "the background and header stay");
h.get_by_label("Cancel").click();
h.run_steps(2);
assert!(h.state_mut().execute("edit.header_footer.update"));
h.run_steps(2);
let (_, page) = shown(&h);
assert_eq!(count(&page, blue), 0, "the new header isn't previewed over the old one");
assert!(at(&page, 0.5, 0.75) == yellow && count(&page, red) > 200, "the background and watermark stay");
}
/// Pages from 1 to 1 stays one page: the dialog used to turn "to" back into the last page on
/// every frame while both were 1, so only page 1 could not be chosen alone. The field starts on
/// the last page.
#[test]
fn page_range_from_one_to_one_selects_only_the_first_page() {
let mut h = harness();
h.state_mut().execute("edit.background");
h.run_steps(2);
assert_eq!(h.state().marks_draft.range.pages(3), [0, 1, 2]);
h.get_by_label("Pages from").click();
h.run_steps(2);
let range = h.state().marks_draft.range;
assert_eq!((range.from, range.to), (1, 3), "from the first page to the last");
h.state_mut().marks_draft.range.to = 1;
h.run_steps(3);
let range = h.state().marks_draft.range;
assert_eq!((range.from, range.to), (1, 1), "the range stays as typed");
h.get_by_label("Applies to 1 page");
h.get_by_label("OK").click();
h.run_steps(3);
let s = h.state();
let doc = s.session.get(s.views[0].id).unwrap();
assert_eq!(doc.marks(), [MarkKind::Background]);
// Rendered: page 1 has the default background colour, page 2 is still blank.
let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), pdfcraft_render::RenderConfig::default());
let corner = |r: &mut pdfcraft_render::PageRenderer, page: usize| {
let p = r.render(pdfcraft_render::RenderRequest { page, scale: 1.0, ..Default::default() });
assert!(p.error.is_none(), "{:?}", p.error);
p.rgba[..4].to_vec()
};
let [red, green, blue] = pdfcraft_engine::Background::default().color.map(|c| (c * 255.0).round() as u8);
assert_eq!(corner(&mut r, 0), vec![red, green, blue, 255], "page 1 has the background");
assert_eq!(corner(&mut r, 1), vec![255, 255, 255, 255], "page 2 does not");
}