diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 360517a87c..4c061ba809 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2999,6 +2999,42 @@ fn headers_footers_watermarks_and_backgrounds_show_update_and_remove() { assert_eq!(s.get(id).unwrap().can_undo(), Some("Remove background")); } +/// Update previews its marks on the page without the ones it replaces: a yellow background +/// updated to blue isn't previewed as blue over yellow, nor a new watermark over the old one. +/// The other kinds of mark and the page's own content stay, a page without that kind has nothing +/// to prepare, and preparing changes nothing in the document. +#[test] +fn update_previews_on_the_page_without_the_marks_it_replaces() { + let (mut s, id) = session_with(2); + let yellow = Background { color: [1.0, 1.0, 0.0], ..Background::default() }; + s.apply(id, Edit::AddBackground { pages: vec![0], settings: yellow, replace: false, file: None }).unwrap(); + let red = Watermark { text: "SECRET".into(), color: [1.0, 0.0, 0.0], opacity: 1.0, ..Watermark::default() }; + s.apply(id, Edit::AddWatermark { pages: vec![0], settings: red, replace: false, file: None }).unwrap(); + let doc = s.get(id).unwrap(); + let (generation, dirty) = (doc.edit_generation(), doc.dirty); + let px = |p: &pdfcraft_render::RenderedPage, x: u32, y: u32| -> [u8; 3] { + p.rgba[(((300 - y) * p.width + x) * 4) as usize..][..3].try_into().unwrap() + }; + let reds = |p: &pdfcraft_render::RenderedPage| p.rgba.as_chunks::<4>().0.iter().filter(|[r, g, b, _]| *r > 200 && *g < 80 && *b < 80).count(); + let dark = |p: &pdfcraft_render::RenderedPage| p.rgba.as_chunks::<4>().0.iter().filter(|[r, g, b, _]| *r < 100 && *g < 100 && *b < 100).count(); + assert_eq!(pixel(&s, id, 0, 5, 5)[..3], [255, 255, 0], "the page has the yellow background"); + + let page = doc.page_without_marks(0, MarkKind::Background).unwrap().expect("page 1 has a background").render_background(1.0).unwrap(); + assert_eq!(px(&page, 5, 5), [255, 255, 255], "without the background the page is white"); + assert!(reds(&page) > 50, "the watermark stays"); + assert!(dark(&page) > 10, "the page text stays"); + + let page = doc.page_without_marks(0, MarkKind::Watermark).unwrap().expect("page 1 has a watermark").render_background(1.0).unwrap(); + assert_eq!(reds(&page), 0, "without the watermark"); + assert_eq!(px(&page, 5, 5), [255, 255, 0], "the background stays"); + + assert!(doc.page_without_marks(1, MarkKind::Background).unwrap().is_none(), "page 2 has none: its thumbnail is the page"); + assert!(doc.page_without_marks(0, MarkKind::HeaderFooter).unwrap().is_none(), "the document has no header"); + assert!(doc.page_without_marks(9, MarkKind::Background).is_err(), "no page 10"); + let doc = s.get(id).unwrap(); + assert_eq!((doc.edit_generation(), doc.dirty, doc.can_undo()), (generation, dirty, Some("Add watermark")), "the document is unchanged"); +} + #[test] fn marker_indexes_match_after_open_and_save_when_requested() { let (mut source, source_id) = session_with(1); diff --git a/crates/engine/src/text_preview.rs b/crates/engine/src/text_preview.rs index 290231cf73..d19272fcef 100644 --- a/crates/engine/src/text_preview.rs +++ b/crates/engine/src/text_preview.rs @@ -1,4 +1,5 @@ -//! The page beneath an inline text editor, with the source glyphs removed on a COS clone. +//! The page beneath an inline text editor, with the source glyphs removed on a COS clone; and, +//! the same way, the page beneath a dragged added item or the marks an Update replaces. use std::sync::Arc; @@ -90,6 +91,24 @@ impl Document { })? } + /// The page without its marks of `kind`: what Update replaces them on, so its preview doesn't + /// draw the new marks over the old ones (or tint the old background). `None` when the page has + /// none, and its thumbnail already is that page. Read-only, like [`Self::text_edit_preview`]. + pub fn page_without_marks(&self, page: usize, kind: crate::MarkKind) -> Result, String> { + if !self.marks().contains(&kind) { + return Ok(None); + } + guard(|| { + let editor = self.editor.as_ref().ok_or("the document can't be read")?; + let mut cos = editor.cos.clone(); + if pdfcraft_edit::remove_marks(&mut cos, &[page], kind).map_err(|e| e.to_string())? == 0 { + return Ok(None); + } + let bytes = Arc::new(pdfcraft_cos::write_incremental(&cos, &pdfcraft_cos::SaveOptions::default()).map_err(|e| e.to_string())?); + Ok(Some(TextEditPreview { page, bytes, config: self.config.clone() })) + })? + } + /// An added item alone, on a transparent page: what follows the pointer while it is dragged, /// so the page under its first place doesn't come along. Read-only, like /// [`Self::added_content_preview`]. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index b3437246a0..85b8a11c24 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1586,6 +1586,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { app.view_draft = None; app.custom_draft = None; app.bulk_field_props = None; + app.unmarked_page = Default::default(); } else { app.dialog = Some(next); } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 79f46bee7b..e7452210bb 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -955,6 +955,8 @@ pub struct PdfCraftApp { pub boxes_draft: pageboxes::BoxesDraft, /// Header & footer / watermark / background dialog state. pub marks_draft: marks_ui::MarksDraft, + /// The Update dialogs' preview page without the marks they replace. + pub(crate) unmarked_page: marks_ui::UnmarkedPage, /// Export dialog settings. pub export_draft: export_ui::ExportDraft, /// A running export's progress. @@ -1279,6 +1281,7 @@ impl PdfCraftApp { protect_draft: Default::default(), boxes_draft: Default::default(), marks_draft: Default::default(), + unmarked_page: Default::default(), export_draft: Default::default(), export_status: None, signature: None, @@ -2878,7 +2881,7 @@ impl PdfCraftApp { /// `true` while any open document still waits for page renders (used by headless capture). pub fn render_pending(&self) -> bool { - self.views.iter().any(|v| v.render_pending()) + self.views.iter().any(|v| v.render_pending()) || self.unmarked_page.pending() } /// Apply a named view option (`--page 3`, `--panel bookmarks`, `--theme dark`, …). diff --git a/crates/ui-egui/src/marks_ui.rs b/crates/ui-egui/src/marks_ui.rs index 5340cef393..1b6f66c2d1 100644 --- a/crates/ui-egui/src/marks_ui.rs +++ b/crates/ui-egui/src/marks_ui.rs @@ -12,6 +12,9 @@ use crate::{PdfCraftApp, icons, widgets}; /// How far a watermark may be moved from the page centre, in points. const MAX_OFFSET: f64 = 20_000.0; +/// The preview page's longer side, in points. +const PREVIEW_SIDE: f32 = 190.0; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Subset { All, @@ -152,7 +155,7 @@ fn preview( draw: impl FnOnce(&egui::Painter, Rect, f32), ) { let (w, h) = (size.0.max(1.0), size.1.max(1.0)); - let s = (190.0 / w.max(h)) as f32; + let s = (f64::from(PREVIEW_SIDE) / w.max(h)) as f32; let (r, _) = ui.allocate_exact_size(vec2(210.0, 210.0), egui::Sense::hover()); let page = Rect::from_center_size(r.center(), vec2(w as f32 * s, h as f32 * s)); let painter = ui.painter_at(r); @@ -180,6 +183,69 @@ fn behind(color: [f64; 3], opacity: f64) -> Color32 { rgb32(color.map(|c| 1.0 - a * (1.0 - c))) } +/// In Update, the page previewed without the marks the dialog replaces. Its thumbnail still has +/// them, so the new marks would be drawn over the old ones, and a new background colour would tint +/// the old one. Rendered off the UI thread like the thumbnail, once per page shown. +#[derive(Default)] +pub(crate) struct UnmarkedPage { + /// The document, its edit generation, the page and the kind of mark taken away. + key: Option<(pdfcraft_engine::DocId, u64, usize, MarkKind)>, + renderer: Option, + texture: Option, + tag: Option, +} + +impl UnmarkedPage { + pub(crate) fn pending(&self) -> bool { + self.renderer.is_some() && self.texture.is_none() + } + + /// The picture to preview `page` on in place of its thumbnail: `None` when the thumbnail is + /// that page (not Update, or no marks of `kind` on it, or the page can't be prepared), and + /// `Some(None)` while it renders, when the page is a blank sheet. + fn picture( + &mut self, + ctx: &egui::Context, + (doc, id): (&pdfcraft_engine::Document, pdfcraft_engine::DocId), + page: usize, + kind: MarkKind, + replace: bool, + ) -> Option> { + let key = replace.then(|| (id, doc.edit_generation(), page, kind)); + if self.key != key { + *self = Self { key, ..Default::default() }; + if replace { + self.renderer = doc.page_without_marks(page, kind).ok().flatten().map(|p| p.renderer()); + } + } + let renderer = self.renderer.as_ref()?; + // At the preview's size in pixels. + let p = doc.info.pages.get(page)?; + let scale = PREVIEW_SIDE * ctx.pixels_per_point() / p.width.max(p.height).max(1.0); + let tag = (scale * 1000.0) as u64; + if self.tag != Some(tag) { + renderer.set_queue(vec![pdfcraft_render::RenderRequest { page, scale, tag, ..Default::default() }]); + self.tag = Some(tag); + } + if let Some(out) = renderer.try_recv() { + if out.error.is_some() { + // The thumbnail, as before, rather than no page at all. + self.renderer = None; + self.texture = None; + return None; + } + if out.request.tag == tag { + let image = crate::canvas::texture_image([out.width as usize, out.height as usize], out.rgba); + self.texture = Some(ctx.load_texture("marks-preview-unmarked", image, egui::TextureOptions::LINEAR)); + } + } + if self.pending() { + ctx.request_repaint_after(std::time::Duration::from_millis(16)); + } + Some(self.texture.as_ref().map(|t| t.id())) + } +} + /// The page (0-based) the preview shows: the current one when the range selects it, otherwise the /// first page the range selects (the current one again when it selects none). fn preview_page(range: &PageRange, current: usize, count: usize) -> usize { @@ -378,12 +444,15 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M ui.add_space(8.0); // The preview: the current page (or the page stepped to), or the first one the range selects // if it leaves that out, drawn from its thumbnail at its displayed size (after /Rotate), the - // frame the engine lays the marks out in. + // frame the engine lays the marks out in. Update draws it without the marks it replaces. let target = shown_preview(&d.range, &mut d.preview, current, count); let page = doc.info.pages.get(target).map(|p| (p.width as f64, p.height as f64)).unwrap_or((612.0, 792.0)); - let picture = app.views.get_mut(i).and_then(|view| { - view.need_thumbnail(target, true); - view.thumb_id(target) + let unmarked = app.unmarked_page.picture(ui.ctx(), (doc, id), target, kind, d.replace); + let picture = unmarked.unwrap_or_else(|| { + app.views.get_mut(i).and_then(|view| { + view.need_thumbnail(target, true); + view.thumb_id(target) + }) }); let tint = if kind == MarkKind::Background && !d.use_file { behind(d.bg.color, d.bg.opacity) } else { Color32::WHITE }; let hf = d.hf.clone(); diff --git a/crates/ui-egui/tests/it/marks.rs b/crates/ui-egui/tests/it/marks.rs index 8861310115..e833aa0fae 100644 --- a/crates/ui-egui/tests/it/marks.rs +++ b/crates/ui-egui/tests/it/marks.rs @@ -294,6 +294,159 @@ fn the_preview_steps_through_the_pages_and_starts_at_the_current_one() { 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>>>); + +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 { + 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)> { + 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. + let beside = r.width() > 50.0 + && r.height() <= 215.0 + && r.right() <= label.left() + && label.left() - r.right() < 260.0 + && r.top() <= label.bottom() + && r.bottom() >= label.top(); + 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) { + 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]; + let (tint, page) = wait_for_preview(&mut h, &uploads, "a page", |_| true); + 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"); + 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(); + let (_, page) = wait_for_preview(&mut h, &uploads, "a page", |_| true); + 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) = wait_for_preview(&mut h, &uploads, "a page", |_| true); + 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. diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 5e1c4a7e75..a0dadf4adc 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -2423,8 +2423,8 @@ milestone = "M7" status = "shipped" commands = ["edit.header_footer"] tools = ["doc_header_footer"] -evidence = ["crates/ui-egui/tests/it/marks.rs::header_and_footer_dialog_inserts_tokens_and_applies_to_a_subset", "crates/edit/src/tests.rs::header_and_footer_are_drawn_in_display_space_and_wrap_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks"] -notes = "Standard faces only: Helvetica, Times or Courier (#3754; no bold, italic or embedded fonts yet); drawn in display space on rotated pages; artifact-tagged; the dialog previews the text over the page's own thumbnail (the current page, or the first one the range selects), in the interface font and with tokens unexpanded" +evidence = ["crates/ui-egui/tests/it/marks.rs::header_and_footer_dialog_inserts_tokens_and_applies_to_a_subset", "crates/edit/src/tests.rs::header_and_footer_are_drawn_in_display_space_and_wrap_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks", "crates/ui-egui/tests/it/marks.rs::update_previews_the_page_without_the_marks_it_replaces", "crates/engine/src/tests.rs::update_previews_on_the_page_without_the_marks_it_replaces"] +notes = "Standard faces only: Helvetica, Times or Courier (#3754; no bold, italic or embedded fonts yet); drawn in display space on rotated pages; artifact-tagged; the dialog previews the text over the page's own thumbnail (the current page, or the first one the range selects), in the interface font and with tokens unexpanded; Update previews it on the page without the headers and footers it replaces" [[feature]] id = "edit.header-footer-update-remove" @@ -2483,8 +2483,8 @@ milestone = "M7" status = "shipped" commands = ["edit.watermark"] tools = ["doc_watermark"] -evidence = ["crates/edit/src/tests.rs::watermarks_rotate_fade_and_can_go_behind", "crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/automation/tests/it/automation.rs::headers_watermarks_and_backgrounds_through_tools", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks"] -notes = "Helvetica; fit-to-page or a size; the dialog previews it over the page's own thumbnail, always on top (a watermark behind the page is not drawn under its content there)" +evidence = ["crates/edit/src/tests.rs::watermarks_rotate_fade_and_can_go_behind", "crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/automation/tests/it/automation.rs::headers_watermarks_and_backgrounds_through_tools", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks", "crates/ui-egui/tests/it/marks.rs::update_previews_the_page_without_the_marks_it_replaces", "crates/engine/src/tests.rs::update_previews_on_the_page_without_the_marks_it_replaces"] +notes = "Helvetica; fit-to-page or a size; the dialog previews it over the page's own thumbnail, always on top (a watermark behind the page is not drawn under its content there); Update previews on the page without the watermarks it replaces" [[feature]] id = "edit.watermark-add-file" @@ -2531,8 +2531,8 @@ milestone = "M7" status = "shipped" commands = ["edit.background"] tools = ["doc_background"] -evidence = ["crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/engine/src/tests.rs::backgrounds_and_watermarks_from_files", "crates/ui-egui/tests/it/marks.rs::backgrounds_and_watermarks_from_a_file", "crates/organize/src/tests.rs::a_page_becomes_a_form_xobject_upright", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks"] -notes = "colour, or a picture from a file (an image or a page of a PDF) scaled relative to the page; the dialog previews a colour as a tint of the page's own thumbnail (opaque pictures on the page are tinted there too, not in the result), a file as its frame and name" +evidence = ["crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/engine/src/tests.rs::backgrounds_and_watermarks_from_files", "crates/ui-egui/tests/it/marks.rs::backgrounds_and_watermarks_from_a_file", "crates/organize/src/tests.rs::a_page_becomes_a_form_xobject_upright", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks", "crates/ui-egui/tests/it/marks.rs::update_previews_the_page_without_the_marks_it_replaces", "crates/engine/src/tests.rs::update_previews_on_the_page_without_the_marks_it_replaces"] +notes = "colour, or a picture from a file (an image or a page of a PDF) scaled relative to the page; the dialog previews a colour as a tint of the page's own thumbnail (opaque pictures on the page are tinted there too, not in the result), a file as its frame and name; Update previews on the page without the backgrounds it replaces, not tinting the old one" [[feature]] id = "edit.background-update-remove"