From 3485e92a95de7865077857deafb20bc590a17a1c Mon Sep 17 00:00:00 2001 From: alehel Date: Fri, 23 Oct 2026 23:58:42 +0200 Subject: [PATCH] fix(edit): backdrop copies keep no reference that outlives a redaction The tag on a page's backdrop copy, `/PCBackdrop << /Original ref /At /Len >>`, kept the page's original content stream reachable. After Add background, then Redact (or Remove Hidden Information, or Edit text) and a full save, the text taken off the page was still in the file, in the original stream the tag pointed at. Text extraction didn't show it; reading the file's bytes did. Restore also gave up after any edit, so the fill stayed off without a word. The tag is now `/PCBackdrop << /Index n /Op (f) /Before /Was [num gen] >>`: how many of the copy's operators come before the `n`, the fill it stands in for, a hash of those operators by value (redaction writes the stream out again with other spacing), and the original's number as two integers, which keep nothing alive. Remove background turns the fill back on in the copy when the operators before it still hash the same and a scan with the fill restored finds it painted first, covering the page. Edits and redactions after the fill are kept. When the original stream is still there with the same bytes, the page points at it again. Otherwise the copy stays as it is, and Remove background says which pages keep their own backdrop off: `EditNotice::BackdropsLeftOff` (engine), a toast in the app, `warnings` from `doc_remove_marks`. `edit_notices` holds `EditNotice` now, with the undrawn-text notice as one of its kinds. The two new UI strings are in all 33 catalogues. `sha2` (already in the lock file) becomes a dependency of `pdfcraft-edit`. Tests, each seen failing with its piece of the fix reverted: - `the_tag_holds_no_reference_and_a_full_save_keeps_no_second_copy_of_the_page` and `removed_text_leaves_the_file_when_a_background_replaced_the_pages_backdrop` (automation, redaction and Remove Hidden Information through a full save): fail with `/Original` back. - `a_fill_that_would_cover_content_painted_before_it_stays_off`: fails without the re-scan. - `a_copy_written_out_again_with_other_spacing_still_comes_back` and the automation test: fail when the raw bytes are hashed instead of the operators. - `an_edit_after_the_fill_is_kept_and_the_backdrop_still_comes_back`: fails under the old rule (only an unchanged copy comes back). - `remove_background_names_the_pages_whose_own_backdrop_stays_off` (engine), `remove_background_warns_about_pages_whose_own_backdrop_stays_off` (automation), `remove_background_says_which_pages_keep_their_own_backdrop_off` (UI): fail without the notice. - `a_change_before_the_fill_leaves_it_off_and_names_the_page`, `a_tag_that_does_not_add_up_is_left_alone` (hostile and old-format tags, never a panic), `operators_hash_by_value_not_by_spelling`. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + ROADMAP.md | 2 +- crates/automation/src/tools.rs | 2 +- crates/automation/tests/it/backdrop.rs | 117 ++++++++++- crates/edit/Cargo.toml | 2 + crates/edit/src/backdrop.rs | 270 ++++++++++++++++++++----- crates/edit/src/backdrop_tests.rs | 167 ++++++++++++++- crates/edit/src/lib.rs | 8 +- crates/engine/src/lib.rs | 59 +++++- crates/engine/src/tests.rs | 65 +++++- crates/ui-egui/src/editing.rs | 10 +- crates/ui-egui/src/i18n/ar.tsv | 2 + crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/ca.tsv | 2 + crates/ui-egui/src/i18n/ckb.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fi.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hi.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/kk.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/lo.tsv | 2 + crates/ui-egui/src/i18n/lt.tsv | 2 + crates/ui-egui/src/i18n/nb.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ro.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sl.tsv | 2 + crates/ui-egui/src/i18n/sq.tsv | 2 + crates/ui-egui/src/i18n/te.tsv | 2 + crates/ui-egui/src/i18n/th.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/marks_ui.rs | 10 + crates/ui-egui/tests/it/marks.rs | 33 +++ parity/acrobat-features.toml | 6 +- 47 files changed, 739 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 496eeab0a4..df78711ad1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4100,6 +4100,7 @@ dependencies = [ "pdfcraft-cos", "pdfcraft-fonts", "pdfcraft-model", + "sha2 0.11.0", "thiserror 2.0.21", "unicode-bidi", ] diff --git a/ROADMAP.md b/ROADMAP.md index 89ce2dadbd..4fd2db9aae 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -265,7 +265,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-19 (M3, #3360):** Switching between Read and the docking workspace retains the current document’s scroll position at fixed zoom, including within-page position; separate tabs retain their own offsets. Estimates unchanged. - **2026-10-18 (M8, #1350):** Integrated current main while preserving content-stream comment boundaries. Hidden-text cleanup retires unused named replacement descriptions on rewritten pages and forms, including inherited/shared resource holders, without removing descriptions still used by surviving content. Synthetic saved-byte and preservation regressions cover the cleanup. Estimates unchanged. - **2026-10-17 (M4, #2997):** Dragging page thumbnails in the Organize grid with Ctrl held (Option on macOS) drops copies at the gap instead of moving the pages: a "+" badge sits by the pointer, the copies land at the gap selected, and one undo step ("Duplicate pages") takes them out again. `page_duplicate` takes `at` (1-based, where the first copy goes; left out, after the last of them) so agents can drive the same thing, and the UI control channel's `ui.drag` modifiers now reach gestures that read the input state. Estimates unchanged. -- **2026-10-16 (M7, Add Background):** A background added behind a page that paints its own opaque white backdrop (browsers' Save as PDF, print drivers) was hidden completely, so the tool looked broken. The dialog now detects such pages and offers to replace the backdrop (on by default): the one fill becomes `n` in a per-page copy of its stream, and Remove background puts the original back unless the copy was edited since. Scanned pages are warned about, not touched. New `doc_backdrops` tool; `doc_background` takes `replace_backdrop`. The default colour is a visible light yellow and the preview shows text over it. Acrobat's own behaviour on such pages is unobserved. Estimates unchanged. +- **2026-10-16 (M7, Add Background):** A background added behind a page that paints its own opaque white backdrop (browsers' Save as PDF, print drivers) was hidden completely, so the tool looked broken. The dialog now detects such pages and offers to replace the backdrop (on by default): the one fill becomes `n` in a per-page copy of its stream, and Remove background turns the fill back on, after edits and redactions too, unless the operators before it changed or it would no longer be painted first; then it says which pages keep it off. The copy's tag holds no reference to the original stream, which had kept text a redaction or Remove Hidden Information removed in the saved file. Scanned pages are warned about, not touched. New `doc_backdrops` tool; `doc_background` takes `replace_backdrop`. The default colour is a visible light yellow and the preview shows text over it. Acrobat's own behaviour on such pages is unobserved. Estimates unchanged. - **2026-10-15 (M6, AcroForm XML text):** Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. Synthetic regressions verify exact values and unknown catalog data after incremental and full save/reopen. Nested-element and XFA interpretation are unchanged. Estimates unchanged. - **2026-10-15 (M6, #1917 refresh):** Integrated upstream `99941daa` while retaining the XML/XFDF attribute whitespace correction and all upstream progress. Only independent documentation conflicts required resolution; focused production and test files are unchanged from the previously green CI head. Estimates unchanged. - **2026-10-15 (M1, #2267 refresh):** Integrated upstream `99941daa` while retaining the raw-deflate fallback correction and all upstream progress. Only independent documentation conflicts required resolution; focused production and test files are unchanged from the previously green CI head. Estimates unchanged. diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index f526ffc3d9..19153b1387 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -971,7 +971,7 @@ pub fn tools() -> Vec { }), &["doc"], )), - t("doc_background", "Add background", "Fill page backgrounds with a colour, or a picture from `file` (an image, or page `file_page` of a PDF) fitted at `scale` (default 1), behind the content. A page that paints its own opaque white backdrop would hide it, so that backdrop is replaced unless replace_backdrop is false (removing the background puts it back); a scanned page's picture still hides it (doc_backdrops lists both). Undoable.") + t("doc_background", "Add background", "Fill page backgrounds with a colour, or a picture from `file` (an image, or page `file_page` of a PDF) fitted at `scale` (default 1), behind the content. A page that paints its own opaque white backdrop would hide it, so that backdrop is replaced unless replace_backdrop is false (removing the background turns it back on where that is still safe, and doc_remove_marks warns about the pages where it isn't); a scanned page's picture still hides it (doc_backdrops lists both). Undoable.") .cmd("edit.background") .with(schema( json!({ "doc": doc(), "pages": pages("to fill (default: all)"), "color": { "type": "string" }, "file": { "type": "string" }, "file_page": { "type": "integer", "minimum": 1 }, "scale": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }, "opacity": { "type": "number", "minimum": 0, "maximum": 1 }, "replace": { "type": "boolean" }, "replace_backdrop": { "type": "boolean", "description": "Replace a white backdrop the page paints itself, which would hide the background. Default true." } }), diff --git a/crates/automation/tests/it/backdrop.rs b/crates/automation/tests/it/backdrop.rs index 1a8a71eb8e..2f7a002981 100644 --- a/crates/automation/tests/it/backdrop.rs +++ b/crates/automation/tests/it/backdrop.rs @@ -40,7 +40,12 @@ fn fixture() -> Vec { } fn workdir() -> PathBuf { - let dir = std::env::temp_dir().join(format!("pdfcraft-automation-backdrop-{}", std::process::id())); + named_workdir("main") +} + +/// A folder of its own per test: the tests of this binary run at the same time. +fn named_workdir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("pdfcraft-automation-backdrop-{}-{name}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("a.pdf"), fixture()).unwrap(); @@ -107,3 +112,113 @@ fn a_background_shows_on_a_page_that_paints_its_own_white_backdrop() { ); let _ = std::fs::remove_dir_all(&dir); } + +/// One 200 × 100 pt page on a white fill of its own: a line to redact, invisible (`3 Tr`) text +/// that Remove Hidden Information removes, and a line that stays (`0 Tr` again: the render mode +/// outlives `ET`). With `tag`, the content is a +/// backdrop copy carrying that tag, under a background. +fn secret_pdf(tag: Option<&str>) -> Vec { + let fill = if tag.is_some() { "n" } else { "f" }; + let content = format!( + "1 1 1 rg 0 0 200 100 re {fill} 0 g BT /F1 12 Tf 10 70 Td (SECRET-LINE) Tj ET BT 3 Tr /F1 12 Tf 10 45 Td (HIDDEN-LINE) Tj ET BT 0 Tr /F1 12 Tf 10 20 Td (KEEP) Tj ET" + ); + let bg = "q 1 0 0 rg 0 0 200 100 re f Q"; + let (contents, extra) = match tag { + Some(t) => ( + "[5 0 R 6 0 R]", + vec![ + format!("<< /PCMark /Background /Length {} >>\nstream\n{bg}\nendstream", bg.len()), + format!("<< /PCBackdrop {t} /Length {} >>\nstream\n{content}\nendstream", content.len()), + ], + ), + None => ("[5 0 R]", vec![format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len())]), + }; + let mut objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 100] >>".into(), + format!("<< /Type /Page /Parent 2 0 R /Contents {contents} /Resources << /Font << /F1 4 0 R >> >> >>"), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".into(), + ]; + objs.extend(extra); + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offs = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offs.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let x = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offs { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes()); + out +} + +/// Every stream of a saved file, decoded, joined: what anyone reading the file can recover. +fn every_stream(path: &std::path::Path) -> String { + let doc = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(path).unwrap())).unwrap(); + doc.scan_objects() + .filter_map(|(_, o)| match &*o { + pdfcraft_cos::Object::Stream(s) => Some(String::from_utf8_lossy(&s.decoded().unwrap_or_default()).into_owned()), + _ => None, + }) + .collect::>() + .join("\n") +} + +/// Text a redaction or Remove Hidden Information took off a page whose backdrop a background +/// replaced is gone from the saved file. The backdrop copy's tag used to keep a reference to the +/// page's original content stream, and with it the removed text. Removing the background +/// afterwards still turns the page's white fill back on. +#[test] +fn removed_text_leaves_the_file_when_a_background_replaced_the_pages_backdrop() { + for (remove, gone) in [ + (vec![("redact_mark", json!({ "find": "SECRET-LINE" })), ("redact_apply", json!({}))], "SECRET-LINE"), + (vec![("doc_remove_hidden", json!({ "categories": ["hidden-text"] }))], "HIDDEN-LINE"), + ] { + let dir = named_workdir(gone); + std::fs::write(dir.join("s.pdf"), secret_pdf(None)).unwrap(); + let mut a = Automation::new().with_root(&dir).unwrap(); + let doc = ok(&mut a, "doc_open", json!({ "path": "s.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "doc_background", json!({ "doc": doc, "color": "#FF0000" })); + for (tool, args) in &remove { + let mut args = args.clone(); + args["doc"] = json!(doc); + ok(&mut a, tool, args); + } + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "out.pdf", "full": true })); + let file = every_stream(&dir.join("out.pdf")); + assert!(file.contains("(KEEP)"), "{gone}: the page's other text is still there: {file}"); + assert!(!file.contains(gone), "{gone} is still in the file: {file}"); + + let saved = ok(&mut a, "doc_open", json!({ "path": "out.pdf" }))["doc"].as_u64().unwrap(); + let removed = ok(&mut a, "doc_remove_marks", json!({ "doc": saved, "kind": "background" })); + assert!(removed.get("warnings").is_none(), "{gone}: {removed}"); + assert_eq!( + ok(&mut a, "doc_backdrops", json!({ "doc": saved }))["pages"], + json!([{ "page": 1, "backdrop": "fill" }]), + "{gone}: the fill is back" + ); + assert_eq!(blank_spot(&mut a, saved, 1), WHITE); + let _ = std::fs::remove_dir_all(&dir); + } +} + +/// When the backdrop can't be turned back on safely, Remove background says which pages keep it +/// off. +#[test] +fn remove_background_warns_about_pages_whose_own_backdrop_stays_off() { + let dir = named_workdir("left-off"); + // A tag whose hash doesn't match the bytes before the fill: they changed since. + let tag = format!("<< /Index 2 /Op (f) /Before <{}> >>", "00".repeat(32)); + std::fs::write(dir.join("t.pdf"), secret_pdf(Some(&tag))).unwrap(); + let mut a = Automation::new().with_root(&dir).unwrap(); + let doc = ok(&mut a, "doc_open", json!({ "path": "t.pdf" }))["doc"].as_u64().unwrap(); + let removed = ok(&mut a, "doc_remove_marks", json!({ "doc": doc, "kind": "background" })); + let warnings = removed["warnings"].as_array().unwrap_or_else(|| panic!("{removed}")); + assert_eq!(warnings.len(), 1, "{removed}"); + assert!(warnings[0].as_str().unwrap().starts_with("page 1 changed after the background was added"), "{removed}"); + assert_eq!(ok(&mut a, "doc_backdrops", json!({ "doc": doc }))["pages"], json!([]), "the fill stays off"); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index 4eb2d6132b..89d9387989 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -15,6 +15,8 @@ pdfcraft-content = { workspace = true } pdfcraft-fonts = { workspace = true } pdfcraft-model = { workspace = true } thiserror = { workspace = true } +# A backdrop copy's tag records a hash of the operators before the fill it turned off (backdrop.rs). +sha2 = "0.11" kurbo = { workspace = true } # Display order of right-to-left lines in added text, and reading and writing order in Edit text; # already in the tree via the interface. diff --git a/crates/edit/src/backdrop.rs b/crates/edit/src/backdrop.rs index 86924b8cb7..d55cb753ba 100644 --- a/crates/edit/src/backdrop.rs +++ b/crates/edit/src/backdrop.rs @@ -5,19 +5,34 @@ //! //! A white fill can be replaced: the one fill operator becomes `n` (end the path without //! painting), which looks the same on white paper. The page then gets a copy of the content -//! stream that held it, tagged `/PCBackdrop << /Original ref /At n /Len n >>`, and the stream -//! itself is left alone (other pages may share it). [`restore`] puts the original back when the -//! background is removed, unless the copy has been edited since. A picture can't be replaced -//! without losing it, so it is only reported. +//! stream that held it, tagged `/PCBackdrop << /Index n /Op (f) /Before /Was [num gen] >>`: +//! how many of the copy's operators come before the `n`, the operator it stands in for, a hash of +//! those operators (their values, not their spacing: redaction writes a stream out again with +//! other whitespace), and the number of the stream it was copied from. The stream itself is left +//! alone (other pages may share it). [`restore`] turns the fill back on when the background is +//! removed, even after the page was edited or redacted, as long as the operators before the fill +//! are unchanged and the fill would again be the first thing painted, covering the page. +//! Otherwise the fill stays off, and [`backdrops_left_off`] names those pages. A picture can't be +//! replaced without losing it, so it is only reported. +//! +//! The tag holds no page content and no reference that keeps another object alive (`/Was` is two +//! numbers, not a reference): a reference to the original stream would keep text in the file +//! that a redaction, Remove Hidden Information or Edit Text removed from the copy. //! //! Detection is deliberately strict: only one rectangle (`re`), in DeviceGray, DeviceRGB or //! DeviceCMYK, opaque in the graphics state, with an axis-aligned transform, filled before any //! other painting, and covering the crop box. Anything else is real content and stays. use pdfcraft_content::{Matrix, Op, Pieces}; -use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; +use sha2::{Digest, Sha256}; -use crate::EditError; +use crate::{EditError, MarkKind}; + +/// The dictionary key of a backdrop copy's tag. +const TAG: &[u8] = b"PCBackdrop"; +/// The fill operators a backdrop can be painted with, and so the only ones a tag may restore. +const FILLS: [&[u8]; 3] = [b"f", b"F", b"f*"]; /// How much of a page's content is read looking for a backdrop: it has to come first. const SCAN_BYTES: usize = 64 * 1024; @@ -39,13 +54,23 @@ pub enum Backdrop { } /// Where a replaceable fill is: the content streams (references in `/Contents` order, untagged -/// ones only), their joined bytes, and the fill operator's joined byte range. +/// ones only), their joined bytes, where each starts in them, and the fill operator's joined +/// byte range. struct Found { refs: Vec<(usize, ObjRef)>, pieces: Pieces, + starts: Vec, op: std::ops::Range, } +impl Found { + /// The piece holding the fill operator, and the operator's offset in it. + fn place(&self) -> Option<(usize, usize)> { + let piece = self.pieces.piece_of(self.op.start); + Some((piece, self.op.start.checked_sub(*self.starts.get(piece)?)?)) + } +} + #[derive(Clone)] struct State { ctm: Matrix, @@ -63,7 +88,7 @@ struct State { pub fn page_backdrop(doc: &Document, page: usize) -> Result, EditError> { let all = pdfcraft_model::pages(doc); let p = all.get(page).ok_or(EditError::NoSuchPage(page))?; - Ok(scan(doc, p).map(|(b, _)| b)) + Ok(scan(doc, p, None).map(|(b, _)| b)) } /// The backdrop each of `pages` (0-based) paints, in the same order, walking the page tree once. @@ -71,32 +96,56 @@ pub fn page_backdrop(doc: &Document, page: usize) -> Result, Ed /// elsewhere (the viewer's) can disagree with the page tree's on a malformed document. pub fn page_backdrops(doc: &Document, pages: &[usize]) -> Vec> { let all = pdfcraft_model::pages(doc); - pages.iter().map(|&p| all.get(p).and_then(|page| scan(doc, page)).map(|(b, _)| b)).collect() + pages.iter().map(|&p| all.get(p).and_then(|page| scan(doc, page, None)).map(|(b, _)| b)).collect() +} + +/// Pages (0-based, ascending) whose own white backdrop [`replace`] turned off and [`restore`] +/// couldn't turn back on: a tagged copy is still in their content, with no background over it. +/// They look the same on white paper. +pub fn backdrops_left_off(doc: &Document) -> Vec { + let tag_of = |o: &Object| crate::stream_tag(doc, o); + let copy = |o: &Object| doc.resolve(o).as_dict().is_some_and(|d| d.get(TAG).is_some()); + pdfcraft_model::pages(doc) + .iter() + .enumerate() + .filter(|(_, p)| { + let list: Vec = match p.dict.get(b"Contents").map(|c| doc.resolve(c)).as_deref() { + Some(Object::Array(a)) => a.clone(), + Some(_) => p.dict.get(b"Contents").cloned().into_iter().collect(), + None => Vec::new(), + }; + list.iter().any(copy) && !list.iter().any(|o| tag_of(o).as_deref() == Some(MarkKind::Background.tag())) + }) + .map(|(i, _)| i) + .collect() } /// Replace `page`'s white backdrop fill, if it has one (see the module doc). Returns whether it /// did; when it did, `page` no longer matches the document. A picture backdrop is left alone. pub(crate) fn replace(doc: &mut Document, page: &pdfcraft_model::Page) -> Result { - let Some((Backdrop::Fill, Some(found))) = scan(doc, page) else { return Ok(false) }; + let Some((Backdrop::Fill, Some(found))) = scan(doc, page, None) else { return Ok(false) }; + let Some((piece, at)) = found.place() else { return Ok(false) }; + let op = found.pieces.data().get(found.op.clone()).unwrap_or_default().to_vec(); let edited = found.pieces.splice([(found.op.clone(), b"n".to_vec())]); - let piece = found.pieces.piece_of(found.op.start); let (Some(&(_, original)), Some(bytes)) = (found.refs.get(piece), edited.get(piece)) else { return Ok(false) }; - // Where the operator sits in that piece: the joined offset less the pieces before it. - let before: usize = (0..piece).filter_map(|i| edited.get(i)).map(|p| p.len() + 1).sum(); - let at = found.op.start.saturating_sub(before); - let len = found.op.end.saturating_sub(found.op.start); + // The `n` among the copy's own operators. A copy whose fill can't be found that way again is + // not made: there would be no safe way back. + let parsed = pdfcraft_content::parse(head(bytes)); + let Some(index) = parsed.ops.iter().position(|o| o.span.start == at && o.op == b"n") else { return Ok(false) }; + let Some(before) = parsed.ops.get(..index) else { return Ok(false) }; let mut note = Dict::new(); - note.set(b"Original".to_vec(), Object::Ref(original)); - note.set(b"At".to_vec(), Object::Int(i64::try_from(at).unwrap_or(i64::MAX))); - note.set(b"Len".to_vec(), Object::Int(i64::try_from(len).unwrap_or(i64::MAX))); + note.set(b"Index".to_vec(), Object::Int(i64::try_from(index).unwrap_or(i64::MAX))); + note.set(b"Op".to_vec(), Object::String(PdfString::literal(op))); + note.set(b"Before".to_vec(), Object::String(PdfString { bytes: ops_hash(before), hex: true })); + note.set(b"Was".to_vec(), Object::Array(vec![Object::Int(original.num.into()), Object::Int(original.generation.into())])); // The copy keeps the original's dictionary (unknown entries included) less what describes // the old encoding; `scan` refused streams whose bytes live in an external file (`/F`). let Object::Stream(source) = &*doc.get(original) else { return Ok(false) }; let mut dict = source.dict.clone(); - for key in [&b"Length"[..], b"DL", b"PCBackdrop"] { + for key in [&b"Length"[..], b"DL", TAG] { dict.remove(key); } - dict.set(b"PCBackdrop".to_vec(), Object::Dict(note)); + dict.set(TAG.to_vec(), Object::Dict(note)); let copy = doc.add(Object::Stream(Stream::flate(dict, bytes))); // Every slot that draws the original: a page may list the same stream twice, and a second // copy of the fill would cover the background again. @@ -112,14 +161,41 @@ pub(crate) fn replace(doc: &mut Document, page: &pdfcraft_model::Page) -> Result Ok(replaced) } -/// Put back the original content streams [`replace`] copied on `page`, where the copy is still -/// exactly what it wrote. Returns how many it restored. +/// Turn the fills [`replace`] turned off on `page` back on, where [`restored_copy`] finds that +/// safe; a copy listed in several slots is restored in each. Returns how many slots it restored. +/// A copy it can't restore stays as it is, tag and all, so [`backdrops_left_off`] names its page. pub(crate) fn restore(doc: &mut Document, page: &pdfcraft_model::Page) -> Result { let mut list = crate::contents(doc, page)?; + // Per copy: the stream that takes its place, or `None` when it stays. + let mut done: Vec<(ObjRef, Option)> = Vec::new(); let mut restored = 0; for entry in list.iter_mut() { - if let Some(original) = unchanged_copy_of(doc, entry) { - *entry = Object::Ref(original); + let Object::Ref(copy) = *entry else { continue }; + let with = match done.iter().find(|(c, _)| *c == copy) { + Some(&(_, with)) => with, + None => { + let with = restored_copy(doc, page, copy).map(|(bytes, dict, was)| { + // The stream it was copied from, while that is still here with these bytes + // (other pages may share it): the page is then exactly as it was. `/Was` is + // only a number, and a full save renumbers objects, so the bytes decide. + let same = |w: &ObjRef| match &*doc.get(*w) { + Object::Stream(s) => { + [TAG, &b"PCMark"[..], b"Subtype"].iter().all(|k| s.dict.get(k).is_none()) + && s.decoded().ok().as_deref() == Some(bytes.as_slice()) + } + _ => false, + }; + match was.filter(same) { + Some(w) => w, + None => doc.add(Object::Stream(Stream::flate(dict, &bytes))), + } + }); + done.push((copy, with)); + with + } + }; + if let Some(with) = with { + *entry = Object::Ref(with); restored += 1; } } @@ -129,27 +205,125 @@ pub(crate) fn restore(doc: &mut Document, page: &pdfcraft_model::Page) -> Result Ok(restored) } -/// The original of a backdrop copy whose bytes are still the original's with the fill turned to -/// `n`; `None` for anything else (including a copy edited since, which is kept). -fn unchanged_copy_of(doc: &Document, entry: &Object) -> Option { - let copy = doc.resolve(entry); - let Object::Stream(copy) = &*copy else { return None }; - let note = copy.dict.get(b"PCBackdrop").map(|n| doc.resolve(n))?; +/// `copy` (a stream [`replace`] wrote for `page`) with its fill turned back on: the bytes, its +/// dictionary without the tag, and the stream it was copied from. `None` when it isn't such a +/// copy, or when turning the fill on could paint over something: the operators before the fill +/// changed since (their hash no longer matches), or the page would no longer paint the fill +/// first, covering it. What comes after the fill (an edit, a redaction) is kept as it is. +fn restored_copy(doc: &Document, page: &pdfcraft_model::Page, copy: ObjRef) -> Option<(Vec, Dict, Option)> { + let obj = doc.get(copy); + let Object::Stream(s) = &*obj else { return None }; + let note = s.dict.get(TAG).map(|n| doc.resolve(n))?; let note = note.as_dict()?; - let Some(Object::Ref(original)) = note.get(b"Original") else { return None }; - let at = usize::try_from(note.get(b"At")?.as_int()?).ok()?; - let len = usize::try_from(note.get(b"Len")?.as_int()?).ok()?; - let orig = doc.get(*original); - let Object::Stream(orig) = &*orig else { return None }; - let source = orig.decoded().ok()?; - let mut expect = source.get(..at)?.to_vec(); - expect.push(b'n'); - expect.extend_from_slice(source.get(at.checked_add(len)?..)?); - (copy.decoded().ok()? == expect).then_some(*original) + let index = usize::try_from(note.get(b"Index")?.as_int()?).ok()?; + let op = note.get(b"Op")?.as_string()?.bytes.as_slice(); + if !FILLS.contains(&op) { + return None; + } + let hash = note.get(b"Before")?.as_string()?; + let bytes = s.decoded().ok()?; + let parsed = pdfcraft_content::parse(head(&bytes)); + // The `n` standing in for the fill, after exactly the operators that came before it. + let fill = parsed.ops.get(index)?; + if fill.op != b"n" || !fill.operands.is_empty() || ops_hash(parsed.ops.get(..index)?) != hash.bytes { + return None; + } + let mut out = Vec::with_capacity(bytes.len().saturating_add(op.len())); + out.extend_from_slice(bytes.get(..fill.span.start)?); + out.extend_from_slice(op); + out.extend_from_slice(bytes.get(fill.span.end..)?); + let (Backdrop::Fill, Some(found)) = scan(doc, page, Some((copy, &out)))? else { return None }; + let (piece, offset) = found.place()?; + if found.refs.get(piece).map(|&(_, r)| r) != Some(copy) || offset != fill.span.start || found.op.len() != op.len() { + return None; + } + let mut dict = s.dict.clone(); + for key in [&b"Length"[..], b"DL", TAG] { + dict.remove(key); + } + let was = match note.get(b"Was").and_then(Object::as_array).map(Vec::as_slice) { + Some([num, generation]) => Some(ObjRef { num: u32::try_from(num.as_int()?).ok()?, generation: u16::try_from(generation.as_int()?).ok()? }), + _ => None, + }; + Some((out, dict, was)) } -/// Find the backdrop, and for a fill, where it is. -fn scan(doc: &Document, p: &pdfcraft_model::Page) -> Option<(Backdrop, Option)> { +/// As much of a content stream as a backdrop could be in: it has to come first. +fn head(bytes: &[u8]) -> &[u8] { + bytes.get(..bytes.len().min(SCAN_BYTES)).unwrap_or_default() +} + +/// A SHA-256 of what `ops` are, not how they are written: keywords and operand values, with +/// numbers in one form (`1`, `1.0` and `1.00000` are the same), so a stream written out again +/// with other spacing or number formatting still matches. +fn ops_hash(ops: &[Op]) -> Vec { + let mut h = Sha256::new(); + for op in ops { + for o in &op.operands { + canonical(o, &mut h, 0); + } + if let Some((dict, data)) = &op.inline { + canonical_dict(dict, &mut h, 0); + bytes(b'i', data, &mut h); + } + bytes(b'k', &op.op, &mut h); + } + h.finalize().to_vec() +} + +/// How deep [`canonical`] follows nested arrays and dictionaries; deeper ones count as equal. +const CANONICAL_DEPTH: usize = 32; + +fn canonical(o: &Object, h: &mut Sha256, depth: usize) { + if depth > CANONICAL_DEPTH { + h.update(b"~"); + return; + } + match o { + Object::Null => h.update(b"z"), + Object::Bool(b) => h.update(if *b { b"T" } else { b"F" }), + Object::Int(i) => number(&i.to_string(), h), + Object::Real(r) if *r == 0.0 => number("0", h), + Object::Real(r) => number(&format!("{r:.5}"), h), + Object::String(s) => bytes(b's', &s.bytes, h), + Object::Name(n) => bytes(b'/', n, h), + Object::Array(a) => { + h.update(b"["); + for x in a { + canonical(x, h, depth.saturating_add(1)); + } + h.update(b"]"); + } + Object::Dict(d) => canonical_dict(d, h, depth), + Object::Stream(_) | Object::Ref(_) => h.update(b"?"), + } +} + +fn canonical_dict(d: &Dict, h: &mut Sha256, depth: usize) { + h.update(b"<"); + for (k, v) in d.iter() { + bytes(b'/', k, h); + canonical(v, h, depth.saturating_add(1)); + } + h.update(b">"); +} + +/// A number written with up to five decimals, without trailing zeros. +fn number(text: &str, h: &mut Sha256) { + let t = if text.contains('.') { text.trim_end_matches('0').trim_end_matches('.') } else { text }; + bytes(b'#', t.as_bytes(), h); +} + +/// A typed, length-prefixed value, so no two different sequences hash the same way. +fn bytes(kind: u8, b: &[u8], h: &mut Sha256) { + h.update([kind]); + h.update(u64::try_from(b.len()).unwrap_or(u64::MAX).to_le_bytes()); + h.update(b); +} + +/// Find the backdrop, and for a fill, where it is. With `swap`, that stream reads as those bytes +/// instead of its own (to check a change before making it). +fn scan(doc: &Document, p: &pdfcraft_model::Page, swap: Option<(ObjRef, &[u8])>) -> Option<(Backdrop, Option)> { let crop = p.crop(doc); let c = p.dict.get(b"Contents")?; let list: Vec = match &*doc.resolve(c) { @@ -160,6 +334,7 @@ fn scan(doc: &Document, p: &pdfcraft_model::Page) -> Option<(Backdrop, Option= SCAN_BYTES { @@ -175,18 +350,21 @@ fn scan(doc: &Document, p: &pdfcraft_model::Page) -> Option<(Backdrop, Option b.to_vec(), + _ => s.decoded().unwrap_or_default(), + }; + starts.push(total); total = total.saturating_add(bytes.len()).saturating_add(1); refs.push((slot, *r)); data.push(bytes); } let pieces = Pieces::join(&data); - let head = pieces.data().get(..pieces.data().len().min(SCAN_BYTES)).unwrap_or_default(); - let parsed = pdfcraft_content::parse(head); + let parsed = pdfcraft_content::parse(head(pieces.data())); let res = resources(doc, &p.dict); let found = walk(doc, &res, crop, &parsed.ops); found.map(|(b, op)| match (b, op) { - (Backdrop::Fill, Some(op)) => (b, Some(Found { refs, pieces, op })), + (Backdrop::Fill, Some(op)) => (b, Some(Found { refs, pieces, starts, op })), _ => (b, None), }) } diff --git a/crates/edit/src/backdrop_tests.rs b/crates/edit/src/backdrop_tests.rs index 1c0744b59c..c7bfcaa8b4 100644 --- a/crates/edit/src/backdrop_tests.rs +++ b/crates/edit/src/backdrop_tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use pdfcraft_cos::{Document, ObjRef, Object, SaveOptions, Stream, write_incremental}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full, write_incremental}; use super::*; use crate::{Background, MarkKind, add_background, remove_marks}; @@ -188,19 +188,166 @@ fn replacing_can_be_turned_off() { assert_eq!(streams(&doc)[1..], before[..]); } -#[test] -fn a_backdrop_copy_edited_since_is_kept_when_the_background_goes() { - let mut doc = page(&[&format!("1 g 0 0 300 200 re f {TEXT}")]); - add_background(&mut doc, &[0], &red(), false).unwrap(); - // Something else rewrites the copy (as Edit text does), keeping its dictionary. - let p = &pdfcraft_model::pages(&doc)[0]; +/// Rewrite the page's backdrop copy (the stream after the background) as another tool would: +/// Edit text and redaction keep its dictionary and change its bytes. +fn rewrite_copy(doc: &mut Document, bytes: &str) -> ObjRef { + let p = &pdfcraft_model::pages(doc)[0]; let Some(Object::Array(list)) = p.dict.get(b"Contents").cloned() else { panic!("contents") }; let Object::Ref(copy) = list[1] else { panic!("a reference") }; let Object::Stream(s) = (*doc.get(copy)).clone() else { panic!("a stream") }; - let edited = "1 g 0 0 300 200 re n BT /F1 24 Tf 40 90 Td (Edited) Tj ET"; - doc.set(copy, Object::Stream(Stream::flate(s.dict.clone(), edited.as_bytes()))); + assert!(s.dict.get(b"PCBackdrop").is_some(), "the copy"); + doc.set(copy, Object::Stream(Stream::flate(s.dict.clone(), bytes.as_bytes()))); + copy +} + +#[test] +fn an_edit_after_the_fill_is_kept_and_the_backdrop_still_comes_back() { + let mut doc = page(&[&format!("1 g 0 0 300 200 re f {TEXT}")]); + add_background(&mut doc, &[0], &red(), false).unwrap(); + rewrite_copy(&mut doc, "1 g 0 0 300 200 re n BT /F1 24 Tf 40 90 Td (Edited) Tj ET"); remove_marks(&mut doc, &[0], MarkKind::Background).unwrap(); - assert_eq!(streams(&doc), [edited.to_string()], "the edit survives"); + assert_eq!(streams(&doc), ["1 g 0 0 300 200 re f BT /F1 24 Tf 40 90 Td (Edited) Tj ET"], "the edit survives, under the fill"); + assert_eq!(page_backdrop(&doc, 0).unwrap(), Some(Backdrop::Fill)); + assert_eq!(backdrops_left_off(&doc), Vec::::new()); +} + +#[test] +fn a_change_before_the_fill_leaves_it_off_and_names_the_page() { + for edited in [ + // A different colour before the fill: the hash of what comes before it no longer matches. + "0.99 g 0 0 300 200 re n BT /F1 24 Tf 40 90 Td (Edited) Tj ET", + // Something painted before it. + "BT /F1 9 Tf (x) Tj ET 1 g 0 0 300 200 re n", + // The fill is gone. + "BT /F1 24 Tf 40 90 Td (Edited) Tj ET", + ] { + let mut doc = page(&[&format!("1 g 0 0 300 200 re f {TEXT}")]); + add_background(&mut doc, &[0], &red(), false).unwrap(); + assert_eq!(backdrops_left_off(&doc), Vec::::new(), "a background is over it"); + rewrite_copy(&mut doc, edited); + remove_marks(&mut doc, &[0], MarkKind::Background).unwrap(); + assert_eq!(streams(&doc), [edited.to_string()], "{edited}: the copy stays as it is"); + assert_eq!(backdrops_left_off(&doc), [0], "{edited}"); + } +} + +#[test] +fn a_fill_that_would_cover_content_painted_before_it_stays_off() { + let mut doc = page(&[&format!("1 g 0 0 300 200 re f {TEXT}")]); + add_background(&mut doc, &[0], &red(), false).unwrap(); + // A tool puts a stream of its own before the copy, untagged: turning the fill back on would + // now paint white over that text. + let over = doc.add(Object::Stream(Stream::flate(pdfcraft_cos::Dict::new(), b"BT /F1 9 Tf 10 10 Td (Over) Tj ET"))); + let p = pdfcraft_model::pages(&doc)[0].clone(); + let Some(Object::Array(mut list)) = p.dict.get(b"Contents").cloned() else { panic!("contents") }; + list.insert(1, Object::Ref(over)); + doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), Object::Array(list))).unwrap(); + remove_marks(&mut doc, &[0], MarkKind::Background).unwrap(); + let s = streams(&doc); + assert_eq!(s.len(), 2, "{s:?}"); + assert!(s[1].contains("re n") && !s[1].contains("re f"), "nothing white goes over the text: {s:?}"); + assert_eq!(backdrops_left_off(&doc), [0]); +} + +#[test] +fn the_tag_holds_no_reference_and_a_full_save_keeps_no_second_copy_of_the_page() { + let mut doc = page(&[&format!("1 g 0 0 300 200 re f {TEXT}")]); + add_background(&mut doc, &[0], &red(), false).unwrap(); + let saved = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap(); + // Every stream in the file that holds the page's text: only the copy the page draws. A + // reference from the tag to the original stream kept a second, which a redaction or Remove + // Hidden Information on the copy left in the file. + let holding: Vec<(ObjRef, Dict)> = saved + .scan_objects() + .filter_map(|(r, o)| match &*o { + Object::Stream(s) if String::from_utf8_lossy(&s.decoded().unwrap_or_default()).contains("(Hello)") => Some((r, s.dict.clone())), + _ => None, + }) + .collect(); + assert_eq!(holding.len(), 1, "{holding:?}"); + let Some(Object::Dict(note)) = holding[0].1.get(b"PCBackdrop").map(|n| (*saved.resolve(n)).clone()) else { panic!("the tag") }; + fn refs(o: &Object) -> bool { + match o { + Object::Ref(_) => true, + Object::Array(a) => a.iter().any(refs), + Object::Dict(d) => d.iter().any(|(_, v)| refs(v)), + _ => false, + } + } + assert!(!note.iter().any(|(_, v)| refs(v)), "{note:?}"); + assert_eq!(note.get(b"Op").and_then(Object::as_string).map(|s| s.bytes.clone()), Some(b"f".to_vec())); + assert_eq!(note.get(b"Before").and_then(Object::as_string).map(|s| s.bytes.len()), Some(32), "a SHA-256"); + // The original is gone, so Remove writes the fill back into a stream of its own. + let mut saved = saved; + remove_marks(&mut saved, &[0], MarkKind::Background).unwrap(); + assert_eq!(streams(&saved), [format!("1 g 0 0 300 200 re f {TEXT}")]); + assert_eq!(page_backdrop(&saved, 0).unwrap(), Some(Backdrop::Fill)); +} + +#[test] +fn a_copy_written_out_again_with_other_spacing_still_comes_back() { + // Redaction writes the whole stream out again: one operator a line, numbers its own way. + let mut doc = page(&[&format!("1 g 0 0 300 200 re f {TEXT}")]); + add_background(&mut doc, &[0], &red(), false).unwrap(); + rewrite_copy(&mut doc, "1.0 g\n0 0 300.000 200 re\nn\nBT\n/F1 24 Tf\n40 90 Td\n[-6505] TJ\nET"); + remove_marks(&mut doc, &[0], MarkKind::Background).unwrap(); + assert_eq!(streams(&doc), ["1.0 g\n0 0 300.000 200 re\nf\nBT\n/F1 24 Tf\n40 90 Td\n[-6505] TJ\nET"], "the fill is back, the rest as written"); + assert_eq!(backdrops_left_off(&doc), Vec::::new()); +} + +#[test] +fn a_tag_that_does_not_add_up_is_left_alone() { + let content = format!("1 g 0 0 300 200 re f {TEXT}"); + let copied = format!("1 g 0 0 300 200 re n {TEXT}"); + let good = ops_hash(&pdfcraft_content::parse(b"1 g 0 0 300 200 re").ops); + let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); + for tag in [ + // Not a fill operator, a wrong hash, an index that isn't the `n` or runs past the end. + format!("<< /Index 2 /Op (B) /Before <{}> >>", hex(&good)), + format!("<< /Index 2 /Op (f) /Before <{}> >>", hex(&[0; 32])), + format!("<< /Index 1 /Op (f) /Before <{}> >>", hex(&good)), + format!("<< /Index 99999 /Op (f) /Before <{}> >>", hex(&good)), + format!("<< /Index -1 /Op (f) /Before <{}> >>", hex(&good)), + // Missing entries, wrong types, and the reference earlier test builds wrote. + "<< /Index 2 /Op (f) >>".to_string(), + "<< /Index (2) /Op /f /Before 3 >>".to_string(), + "<< /Original 5 0 R /At 19 /Len 1 >>".to_string(), + "(not a dictionary)".to_string(), + ] { + let mut doc = build(&[ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 200] >>".into(), + "<< /Type /Page /Parent 2 0 R /Contents [4 0 R] >>".into(), + stream(&format!("/PCBackdrop {tag}"), &copied), + stream("", &content), + ]); + remove_marks(&mut doc, &[0], MarkKind::Background).unwrap(); + assert_eq!(streams(&doc), std::slice::from_ref(&copied), "{tag}"); + assert_eq!(backdrops_left_off(&doc), [0], "{tag}"); + } + // The same page with an honest tag comes back. + let mut doc = build(&[ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 200] >>".into(), + "<< /Type /Page /Parent 2 0 R /Contents [4 0 R] >>".into(), + stream(&format!("/PCBackdrop << /Index 2 /Op (f) /Before <{}> >>", hex(&good)), &copied), + ]); + remove_marks(&mut doc, &[0], MarkKind::Background).unwrap(); + assert_eq!(streams(&doc), [content]); +} + +#[test] +fn operators_hash_by_value_not_by_spelling() { + let h = |s: &[u8]| ops_hash(&pdfcraft_content::parse(s).ops); + assert_eq!(h(b"1 g 0 0 300 200 re"), h(b"1.0 g\n0 0 300.00000 200 re")); + assert_eq!(h(b"0 g"), h(b"-0.0 g")); + assert_ne!(h(b"1 g 0 0 300 200 re"), h(b"0.99 g 0 0 300 200 re")); + assert_ne!(h(b"(ab) Tj"), h(b"(a) (b) Tj"), "operand boundaries count"); + assert_ne!(h(b"/a /b BDC"), h(b"/ab BDC")); + assert_ne!(h(b""), h(b"q")); + // Nesting is bounded: deep arrays hash without running out of stack. + let deep = format!("{}{} Tj", "[".repeat(1000), "]".repeat(1000)); + let _ = h(deep.as_bytes()); } #[test] diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index 4b524b70ed..99d80e8ae9 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -205,7 +205,8 @@ pub struct Background { /// Pictures: size relative to the page (1.0 fits the page). pub scale: f64, /// Replace a white backdrop the page paints itself, which would hide the background - /// ([`backdrop`]). Removing the background puts it back. + /// ([`backdrop`]). Removing the background turns it back on where that is still safe + /// ([`backdrops_left_off`] are the pages where it isn't). pub replace_backdrop: bool, } @@ -971,7 +972,8 @@ pub fn add_background(doc: &mut Document, pages: &[usize], bg: &Background, repl /// Remove marks of `kind` from `pages`; returns how many were removed. Wrappers left with /// nothing after them go too, restoring the original content list; a wrapper that content /// after it still relies on (another tool's appended stream) stays. Removing backgrounds also -/// puts back a backdrop [`add_background`] replaced, unless it was edited since. +/// turns a backdrop [`add_background`] replaced back on where that is still safe; the pages where +/// it isn't are [`backdrops_left_off`]. pub fn remove_marks(doc: &mut Document, pages: &[usize], kind: MarkKind) -> Result { let all = page_list(doc); check(pages, all.len())?; @@ -1034,7 +1036,7 @@ pub fn marks_present(doc: &Document) -> Vec { } pub mod backdrop; -pub use backdrop::{Backdrop, page_backdrop, page_backdrops}; +pub use backdrop::{Backdrop, backdrops_left_off, page_backdrop, page_backdrops}; mod background; pub use background::page_background; pub mod objects; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 37b1dd2637..b329211129 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -434,9 +434,51 @@ pub struct Document { /// XFA forms: what was approximated, rewritten or could not be written to the XFA data /// (also in `info.warnings`, kept there when the document is re-read). pub xfa_warnings: Vec, - /// What the last successful [`Session::apply`] wrote that its fonts can't draw: kept as typed, - /// drawn with `?` in its place (see [`TextNotice`]). The interface shows it; tools return it. - pub edit_notices: Vec, + /// What the last successful [`Session::apply`] wants said (see [`EditNotice`]): text its fonts + /// can't draw, pages whose own backdrop stays off. The interface shows it; tools return it. + pub edit_notices: Vec, +} + +/// Something the last successful edit did that the user should hear about, never left silent +/// (AGENTS.md §4). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditNotice { + /// Text stored as typed but drawn with `?`. + Undrawn(TextNotice), + /// Remove background couldn't turn back on the white backdrop these pages (0-based, + /// ascending) paint themselves: their content changed after the background replaced it, so + /// turning it on could paint over something (`pdfcraft_edit::backdrops_left_off`). They look + /// the same on white paper. + BackdropsLeftOff { pages: Vec }, +} + +impl std::fmt::Display for EditNotice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EditNotice::Undrawn(n) => n.fmt(f), + EditNotice::BackdropsLeftOff { pages } => { + let (which, own) = if pages.len() == 1 { ("page", "its") } else { ("pages", "their") }; + write!( + f, + "{which} {} changed after the background was added, so {own} own white backdrop was not turned back on (it looks the same on white paper)", + page_runs(pages) + ) + } + } + } +} + +/// 0-based pages as 1-based runs: `[0, 1, 2, 5]` is "1-3, 6". +fn page_runs(pages: &[usize]) -> String { + let mut runs: Vec<(usize, usize)> = Vec::new(); + for &p in pages { + match runs.last_mut() { + Some((_, end)) if end.checked_add(1) == Some(p) => *end = p, + _ => runs.push((p, p)), + } + } + let one = |p: usize| p.saturating_add(1); + runs.iter().map(|&(a, b)| if a == b { one(a).to_string() } else { format!("{}-{}", one(a), one(b)) }).collect::>().join(", ") } /// Text an edit stored as typed but drew with `?` in place of `ch`, because the font it is drawn @@ -3965,11 +4007,18 @@ impl Session { } // What this edit drew with `?` (a value or comment text its font can't draw, stored as // typed): reported, never silent (AGENTS.md §4). - let notices = if matches!(scope_of(&edit), Scope::Form | Scope::Comments | Scope::Full) { - guard(|| undrawn_notices(&editor.cos, &next)).unwrap_or_default() + let mut notices: Vec = if matches!(scope_of(&edit), Scope::Form | Scope::Comments | Scope::Full) { + guard(|| undrawn_notices(&editor.cos, &next)).unwrap_or_default().into_iter().map(EditNotice::Undrawn).collect() } else { Vec::new() }; + // Pages whose own white backdrop Remove background couldn't turn back on: said, not silent. + if matches!(edit, Edit::RemoveMarks { kind: MarkKind::Background }) { + let pages = guard(|| pdfcraft_edit::backdrops_left_off(&next)).unwrap_or_default(); + if !pages.is_empty() { + notices.push(EditNotice::BackdropsLeftOff { pages }); + } + } let previous = std::mem::replace(&mut editor.cos, next); let keys = keys_after(&edit).unwrap_or_else(|| editor.keys.clone()); let previous_keys = std::mem::replace(&mut editor.keys, keys); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ed4e6efa1c..9574c49c31 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2132,10 +2132,10 @@ fn a_value_its_font_cannot_draw_is_reported_by_the_edit_that_drew_it() { fill(&mut s, "name", FieldValue::Text("张三".into())).unwrap(); let d = s.get(id).unwrap(); assert_eq!(d.form[0].value, ["张三"]); - let n = &d.edit_notices; - assert_eq!(n.len(), 1, "{n:?}"); - assert_eq!((n[0].field.as_deref(), n[0].page, n[0].ch, n[0].redrawn_elsewhere), (Some("name"), 0, '张', true)); - assert!(n[0].to_string().contains("\"张\" (U+5F20)") && n[0].to_string().contains("other PDF viewers redraw"), "{}", n[0]); + let [EditNotice::Undrawn(n)] = d.edit_notices.as_slice() else { panic!("{:?}", d.edit_notices) }; + assert_eq!((n.field.as_deref(), n.page, n.ch, n.redrawn_elsewhere), (Some("name"), 0, '张', true)); + let said = d.edit_notices[0].to_string(); + assert!(said.contains("\"张\" (U+5F20)") && said.contains("other PDF viewers redraw"), "{said}"); fill(&mut s, "ok", FieldValue::Check(true)).unwrap(); assert!(s.get(id).unwrap().edit_notices.is_empty(), "an unrelated edit reports nothing"); fill(&mut s, "name", FieldValue::Text("李四".into())).unwrap(); @@ -2681,6 +2681,59 @@ fn headers_footers_watermarks_and_backgrounds_show_update_and_remove() { assert_eq!(s.get(id).unwrap().can_undo(), Some("Remove background")); } +/// One 100 × 100 page under a background (`/PCMark /Background`), its own content painting a +/// white backdrop first. With `copy`, that content is a backdrop copy whose tag is `copy`. +fn backdrop_pdf(copy: Option<&str>) -> Vec { + let bg = "q 1 0 0 rg 0 0 100 100 re f Q"; + let (tag, fill) = match copy { + Some(tag) => (format!("/PCBackdrop {tag} "), "n"), + None => (String::new(), "f"), + }; + let content = format!("1 g 0 0 100 100 re {fill} 0 g BT /F1 12 Tf 10 50 Td (Hi) Tj ET"); + 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 >> endobj\n\ + 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents [4 0 R 5 0 R] /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> >> endobj\n\ + 4 0 obj << /PCMark /Background /Length {} >> stream\n{bg}\nendstream endobj\n\ + 5 0 obj << {tag}/Length {} >> stream\n{content}\nendstream endobj\n\ + trailer << /Root 1 0 R >>\n%%EOF", + bg.len(), + content.len() + ) + .into_bytes() +} + +/// Remove background says which pages keep their own white backdrop off (it couldn't be turned +/// back on safely), and says nothing when every backdrop comes back. +#[test] +fn remove_background_names_the_pages_whose_own_backdrop_stays_off() { + let mut s = Session::new(); + // A tag whose hash doesn't match the bytes before the fill: something changed them since. + let tag = format!("<< /Index 2 /Op (f) /Before <{}> >>", "00".repeat(32)); + let id = s.open("a.pdf", None, Arc::new(backdrop_pdf(Some(&tag))), None).unwrap(); + s.apply(id, Edit::RemoveMarks { kind: MarkKind::Background }).unwrap(); + let d = s.get(id).unwrap(); + assert_eq!(d.edit_notices, [EditNotice::BackdropsLeftOff { pages: vec![0] }]); + let said = d.edit_notices[0].to_string(); + assert!(said.starts_with("page 1 changed after the background was added"), "{said}"); + // An unrelated edit after it says nothing. + s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap(); + assert!(s.get(id).unwrap().edit_notices.is_empty()); + + // Added and removed here: the backdrop comes back, and nothing is said. + let id = s.open("b.pdf", None, Arc::new(backdrop_pdf(None)), None).unwrap(); + s.apply(id, Edit::RemoveMarks { kind: MarkKind::Background }).unwrap(); + let background = Background { color: [1.0, 0.0, 0.0], ..Background::default() }; + s.apply(id, Edit::AddBackground { pages: vec![0], settings: background, replace: false, file: None }).unwrap(); + assert_eq!(s.get(id).unwrap().page_backdrops(&[0]), [None], "replaced"); + s.apply(id, Edit::RemoveMarks { kind: MarkKind::Background }).unwrap(); + assert!(s.get(id).unwrap().edit_notices.is_empty()); + assert_eq!(s.get(id).unwrap().page_backdrops(&[0]), [Some(Backdrop::Fill)], "turned back on"); + assert_eq!(page_runs(&[0, 1, 2, 5, 7, 8]), "1-3, 6, 8-9"); + assert_eq!(page_runs(&[]), ""); +} + #[test] fn marker_indexes_match_after_open_and_save_when_requested() { let (mut source, source_id) = session_with(1); @@ -7029,11 +7082,11 @@ fn undo_and_redo_roll_back_on_a_refresh_panic_or_error() { // 5. Metadata scope: a panic after `doc.info.title` was written rolls back `info.title` and keeps `edit_notices`. s.apply(id, Edit::SetInfo { key: "Title".into(), value: "Draft".into() }).unwrap(); let notice = TextNotice { field: None, page: 0, ch: '☺', redrawn_elsewhere: false }; - s.doc_mut(id).unwrap().edit_notices.push(notice.clone()); + s.doc_mut(id).unwrap().edit_notices.push(EditNotice::Undrawn(notice.clone())); panic_on_refresh(1); assert_eq!(s.undo(id), Err(EditError::Reopen("injected refresh panic".into()))); assert_eq!(s.get(id).unwrap().info.title.as_deref(), Some("Draft")); - assert_eq!(s.get(id).unwrap().edit_notices, [notice]); + assert_eq!(s.get(id).unwrap().edit_notices, [EditNotice::Undrawn(notice)]); assert_eq!(s.undo(id).unwrap().as_str(), "Change Title"); assert_eq!(s.get(id).unwrap().info.title, None); assert!(s.get(id).unwrap().edit_notices.is_empty()); diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 2fdc2e9d02..e03dd52fe2 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -173,10 +173,14 @@ impl PdfCraftApp { if std::mem::take(&mut view.comments.tool_done) && !self.comment_prefs.pinned { self.quick_tool = crate::QuickTool::Select; } - // Text saved as typed but drawn with `?` (its font can't draw it): said, not silent. - // Before the scripts' output, so an alert they raise still shows. + // Text saved as typed but drawn with `?` (its font can't draw it), or pages whose + // own backdrop stays off: said, not silent. Before the scripts' output, so an alert + // they raise still shows. if let Some(n) = self.session.get(id).and_then(|d| d.edit_notices.first()).cloned() { - self.notify(crate::content_ui::undrawn_message(&n)); + self.notify(match &n { + pdfcraft_engine::EditNotice::Undrawn(t) => crate::content_ui::undrawn_message(t), + pdfcraft_engine::EditNotice::BackdropsLeftOff { pages } => crate::marks_ui::backdrops_left_off_message(pages), + }); } let out = self.session.take_js_output(id); self.handle_js(id, out); diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index e41a752411..20931ad0bb 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -862,6 +862,8 @@ Removing the background puts the page's own backdrop back. إزالة الخلفية تعيد التعبئة البيضاء الخاصة بالصفحة. A picture covers page {pages} (a scan?), so the background won't show there. تغطي صورة الصفحة {pages} (مسح ضوئي؟)، لذا لن تظهر الخلفية فيها. Pictures cover pages {pages} (scans?), so the background won't show there. تغطي صور الصفحات {pages} (مسح ضوئي؟)، لذا لن تظهر الخلفية فيها. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. تغيّرت الصفحة {pages} بعد إضافة الخلفية، لذا تبقى تعبئتها البيضاء الخاصة متوقفة. تبدو كما هي على الورق الأبيض. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. تغيّرت الصفحات {pages} بعد إضافة الخلفية، لذا تبقى تعبئتها البيضاء الخاصة متوقفة. تبدو كما هي على الورق الأبيض. PDF or image PDF أو صورة No file chosen لم يُختر ملف Page number رقم الصفحة diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 1854c469ef..f71d01dc01 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -533,6 +533,8 @@ Removing the background puts the page's own backdrop back. Премахването на фона връща собственото бяло запълване на страницата. A picture covers page {pages} (a scan?), so the background won't show there. Изображение покрива страница {pages} (сканиране?), затова фонът няма да се вижда там. Pictures cover pages {pages} (scans?), so the background won't show there. Изображения покриват страници {pages} (сканирания?), затова фонът няма да се вижда там. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Страница {pages} е променена след добавянето на фона, затова собственото ѝ бяло запълване остава изключено. На бяла хартия изглежда по същия начин. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Страници {pages} са променени след добавянето на фона, затова собственото им бяло запълване остава изключено. На бяла хартия изглеждат по същия начин. PDF or image PDF или изображение No file chosen Не е избран файл Page number Номер на страница diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 34ea8188e7..5164d43a10 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -821,6 +821,8 @@ Removing the background puts the page's own backdrop back. En eliminar el fons, es restaura el farciment blanc propi de la pàgina. A picture covers page {pages} (a scan?), so the background won't show there. Una imatge cobreix la pàgina {pages} (un escaneig?), així que el fons no s'hi veurà. Pictures cover pages {pages} (scans?), so the background won't show there. Hi ha imatges que cobreixen les pàgines {pages} (escanejos?), així que el fons no s'hi veurà. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. La pàgina {pages} ha canviat després d'afegir el fons, així que el seu propi farciment blanc continua desactivat. Es veu igual sobre paper blanc. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Les pàgines {pages} han canviat després d'afegir el fons, així que el seu propi farciment blanc continua desactivat. Es veuen igual sobre paper blanc. PDF or image PDF o imatge No file chosen Cap fitxer triat Page number Número de pàgina diff --git a/crates/ui-egui/src/i18n/ckb.tsv b/crates/ui-egui/src/i18n/ckb.tsv index 2e7d8f69a9..a87c272310 100644 --- a/crates/ui-egui/src/i18n/ckb.tsv +++ b/crates/ui-egui/src/i18n/ckb.tsv @@ -831,6 +831,8 @@ Removing the background puts the page's own backdrop back. لابردنی پاشبنەما پڕکردنەوە سپییەکەی خودی پەڕەکە دەگەڕێنێتەوە. A picture covers page {pages} (a scan?), so the background won't show there. وێنەیەک پەڕەی {pages} دادەپۆشێت (سکان؟)، بۆیە پاشبنەماکە لەوێ دەرناکەوێت. Pictures cover pages {pages} (scans?), so the background won't show there. وێنەکان پەڕەکانی {pages} دادەپۆشن (سکان؟)، بۆیە پاشبنەماکە لەوێ دەرناکەوێت. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. پەڕەی {pages} دوای زیادکردنی پاشبنەما گۆڕا، بۆیە پڕکردنەوە سپییەکەی خۆی کوژاوە دەمێنێتەوە. لەسەر کاغەزی سپی هەمان شێوەیە. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. پەڕەکانی {pages} دوای زیادکردنی پاشبنەما گۆڕان، بۆیە پڕکردنەوە سپییەکانی خۆیان کوژاوە دەمێننەوە. لەسەر کاغەزی سپی هەمان شێوەن. PDF or image PDF یان وێنە No file chosen هیچ فایلێک هەڵنەبژێردراوە Page number ژمارەی پەڕە diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index a2d3f1b0e9..10433a6543 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -937,6 +937,8 @@ key Space Mezerník Removing the background puts the page's own backdrop back. Odebráním pozadí se vrátí vlastní bílá výplň stránky. A picture covers page {pages} (a scan?), so the background won't show there. Stránku {pages} pokrývá obrázek (sken?), takže pozadí na ní nebude vidět. Pictures cover pages {pages} (scans?), so the background won't show there. Stránky {pages} pokrývají obrázky (skeny?), takže pozadí na nich nebude vidět. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Stránka {pages} se po přidání pozadí změnila, proto její vlastní bílá výplň zůstává vypnutá. Na bílém papíře vypadá stejně. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Stránky {pages} se po přidání pozadí změnily, proto jejich vlastní bílá výplň zůstává vypnutá. Na bílém papíře vypadají stejně. PDF or image PDF nebo obrázek No file chosen Není vybrán žádný soubor Page number Číslo stránky diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index f7b6083f6b..4018a19604 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -840,6 +840,8 @@ Removing the background puts the page's own backdrop back. Wird der Hintergrund entfernt, kehrt die eigene weiße Fläche der Seite zurück. A picture covers page {pages} (a scan?), so the background won't show there. Ein Bild bedeckt Seite {pages} (ein Scan?), daher ist der Hintergrund dort nicht zu sehen. Pictures cover pages {pages} (scans?), so the background won't show there. Bilder bedecken die Seiten {pages} (Scans?), daher ist der Hintergrund dort nicht zu sehen. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Seite {pages} wurde nach dem Hinzufügen des Hintergrunds geändert, daher bleibt ihre eigene weiße Fläche ausgeschaltet. Auf weißem Papier sieht sie gleich aus. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Seiten {pages} wurden nach dem Hinzufügen des Hintergrunds geändert, daher bleiben ihre eigenen weißen Flächen ausgeschaltet. Auf weißem Papier sehen sie gleich aus. PDF or image PDF oder Bild No file chosen Keine Datei ausgewählt Page number Seitenzahl diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 143c915388..bd549327b6 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -444,6 +444,8 @@ Removing the background puts the page's own backdrop back. Η αφαίρεση του φόντου επαναφέρει τη λευκή γέμιση της σελίδας. A picture covers page {pages} (a scan?), so the background won't show there. Μια εικόνα καλύπτει τη σελίδα {pages} (σάρωση;), οπότε το φόντο δεν θα φαίνεται εκεί. Pictures cover pages {pages} (scans?), so the background won't show there. Εικόνες καλύπτουν τις σελίδες {pages} (σαρώσεις;), οπότε το φόντο δεν θα φαίνεται εκεί. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Η σελίδα {pages} άλλαξε μετά την προσθήκη του φόντου, οπότε η δική της λευκή γέμιση μένει απενεργοποιημένη. Σε λευκό χαρτί φαίνεται ίδια. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Οι σελίδες {pages} άλλαξαν μετά την προσθήκη του φόντου, οπότε οι δικές τους λευκές γεμίσεις μένουν απενεργοποιημένες. Σε λευκό χαρτί φαίνονται ίδιες. PDF or image PDF ή εικόνα No file chosen Δεν έχει επιλεγεί αρχείο Scale relative to target page Κλίμακα ως προς τη σελίδα προορισμού diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 451b0489bc..31af1e9251 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -856,6 +856,8 @@ Removing the background puts the page's own backdrop back. Al quitar el fondo, se restaura el relleno blanco propio de la página. A picture covers page {pages} (a scan?), so the background won't show there. Una imagen cubre la página {pages} (¿un escaneo?), así que el fondo no se verá ahí. Pictures cover pages {pages} (scans?), so the background won't show there. Hay imágenes que cubren las páginas {pages} (¿escaneos?), así que el fondo no se verá ahí. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. La página {pages} cambió después de añadir el fondo, así que su propio relleno blanco sigue desactivado. Se ve igual sobre papel blanco. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Las páginas {pages} cambiaron después de añadir el fondo, así que sus propios rellenos blancos siguen desactivados. Se ven igual sobre papel blanco. PDF or image PDF o imagen No file chosen Ningún archivo elegido Page number Número de página diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index ba4a72b142..5e7c1ea2d8 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -820,6 +820,8 @@ Removing the background puts the page's own backdrop back. Taustan poistaminen palauttaa sivun oman valkoisen täytön. A picture covers page {pages} (a scan?), so the background won't show there. Kuva peittää sivun {pages} (skannaus?), joten tausta ei näy siinä. Pictures cover pages {pages} (scans?), so the background won't show there. Kuvat peittävät sivut {pages} (skannauksia?), joten tausta ei näy niissä. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Sivu {pages} muuttui taustan lisäämisen jälkeen, joten sen oma valkoinen täyttö pysyy pois päältä. Valkoisella paperilla se näyttää samalta. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Sivut {pages} muuttuivat taustan lisäämisen jälkeen, joten niiden omat valkoiset täytöt pysyvät pois päältä. Valkoisella paperilla ne näyttävät samalta. PDF or image PDF tai kuva No file chosen Tiedostoa ei ole valittu Page number Sivunumero diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 7eed4b9d4c..c6d2c4090e 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -836,6 +836,8 @@ Removing the background puts the page's own backdrop back. Supprimer l'arrière-plan rétablit le remplissage blanc propre à la page. A picture covers page {pages} (a scan?), so the background won't show there. Une image couvre la page {pages} (une numérisation ?), l'arrière-plan n'y sera donc pas visible. Pictures cover pages {pages} (scans?), so the background won't show there. Des images couvrent les pages {pages} (des numérisations ?), l'arrière-plan n'y sera donc pas visible. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. La page {pages} a changé après l'ajout de l'arrière-plan, son propre remplissage blanc reste donc désactivé. Elle paraît identique sur papier blanc. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Les pages {pages} ont changé après l'ajout de l'arrière-plan, leurs propres remplissages blancs restent donc désactivés. Elles paraissent identiques sur papier blanc. PDF or image PDF ou image No file chosen Aucun fichier choisi Page number Numéro de page diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 4a6baf1819..b45b026068 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -804,6 +804,8 @@ Removing the background puts the page's own backdrop back. पृष्ठभूमि हटाने पर पृष्ठ की अपनी सफ़ेद भराई वापस आ जाती है। A picture covers page {pages} (a scan?), so the background won't show there. एक छवि पृष्ठ {pages} को ढकती है (स्कैन?), इसलिए वहाँ पृष्ठभूमि नहीं दिखेगी। Pictures cover pages {pages} (scans?), so the background won't show there. छवियाँ पृष्ठ {pages} को ढकती हैं (स्कैन?), इसलिए वहाँ पृष्ठभूमि नहीं दिखेगी। + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. पृष्ठभूमि जोड़ने के बाद पृष्ठ {pages} बदल गया, इसलिए उसकी अपनी सफ़ेद भराई बंद रहती है। सफ़ेद कागज़ पर यह वैसा ही दिखता है। + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. पृष्ठभूमि जोड़ने के बाद पृष्ठ {pages} बदल गए, इसलिए उनकी अपनी सफ़ेद भराई बंद रहती है। सफ़ेद कागज़ पर वे वैसे ही दिखते हैं। PDF or image पीडीएफ़ या छवि No file chosen कोई फ़ाइल नहीं चुनी गई Page number पृष्ठ संख्या diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index f21705c6e1..e67bb6761e 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -833,6 +833,8 @@ Removing the background puts the page's own backdrop back. A háttér eltávolítása visszaállítja az oldal saját fehér kitöltését. A picture covers page {pages} (a scan?), so the background won't show there. Egy kép fedi le ezt az oldalt: {pages} (szkennelt?), így ott nem látszik a háttér. Pictures cover pages {pages} (scans?), so the background won't show there. Képek fedik le ezeket az oldalakat: {pages} (szkenneltek?), így ott nem látszik a háttér. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Ez az oldal megváltozott a háttér hozzáadása után, ezért a saját fehér kitöltése kikapcsolva marad: {pages}. Fehér papíron ugyanúgy néz ki. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Ezek az oldalak megváltoztak a háttér hozzáadása után, ezért a saját fehér kitöltésük kikapcsolva marad: {pages}. Fehér papíron ugyanúgy néznek ki. PDF or image PDF vagy kép No file chosen Nincs fájl kiválasztva Page number Oldalszám diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 002cd54de7..b96e6fc8cb 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -950,6 +950,8 @@ Removing the background puts the page's own backdrop back. Menghapus latar belakang akan mengembalikan isian putih milik halaman. A picture covers page {pages} (a scan?), so the background won't show there. Sebuah gambar menutupi halaman {pages} (hasil pindai?), jadi latar belakang tidak akan terlihat di sana. Pictures cover pages {pages} (scans?), so the background won't show there. Gambar menutupi halaman-halaman {pages} (hasil pindai?), jadi latar belakang tidak akan terlihat di sana. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Halaman {pages} berubah setelah latar belakang ditambahkan, jadi isian putihnya sendiri tetap mati. Di kertas putih tampilannya sama. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Halaman {pages} berubah setelah latar belakang ditambahkan, jadi isian putihnya sendiri tetap mati. Di kertas putih tampilannya sama. Report Laporan Report… Laporan… Requires user to enter a password for: Memerlukan pengguna untuk memasukkan kata sandi untuk: diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index fb64615371..8bcfd6da1d 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -859,6 +859,8 @@ Removing the background puts the page's own backdrop back. Rimuovendo lo sfondo, il riempimento bianco della pagina viene ripristinato. A picture covers page {pages} (a scan?), so the background won't show there. Un'immagine copre la pagina {pages} (una scansione?), quindi lì lo sfondo non sarà visibile. Pictures cover pages {pages} (scans?), so the background won't show there. Delle immagini coprono le pagine {pages} (scansioni?), quindi lì lo sfondo non sarà visibile. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. La pagina {pages} è cambiata dopo l'aggiunta dello sfondo, quindi il suo riempimento bianco resta disattivato. Su carta bianca appare uguale. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Le pagine {pages} sono cambiate dopo l'aggiunta dello sfondo, quindi i loro riempimenti bianchi restano disattivati. Su carta bianca appaiono uguali. PDF or image PDF o immagine No file chosen Nessun file scelto Page number Numero di pagina diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 79b12f015a..47bca878f9 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -863,6 +863,8 @@ Removing the background puts the page's own backdrop back. 背景を削除すると、ページ独自の白い塗りが元に戻ります。 A picture covers page {pages} (a scan?), so the background won't show there. {pages} ページは画像(スキャン?)で覆われているため、背景は表示されません。 Pictures cover pages {pages} (scans?), so the background won't show there. {pages} ページが画像(スキャン?)で覆われているため、そこでは背景は表示されません。 + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. 背景を追加した後に {pages} ページが変更されたため、そのページ独自の白い塗りはオフのままです。白い紙では見た目は変わりません。 + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. 背景を追加した後に {pages} ページが変更されたため、それらのページ独自の白い塗りはオフのままです。白い紙では見た目は変わりません。 PDF or image PDF または画像 No file chosen ファイルが選択されていません Page number ページ番号 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index a040c013b5..6953af8a2c 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -818,6 +818,8 @@ Removing the background puts the page's own backdrop back. Фонды жою беттің өз ақ толтыруын қайтарады. A picture covers page {pages} (a scan?), so the background won't show there. Сурет жауып тұрған бет (сканер ме?): {pages}; онда фон көрінбейді. Pictures cover pages {pages} (scans?), so the background won't show there. Суреттер жауып тұрған беттер (сканер ме?): {pages}; онда фон көрінбейді. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Фон қосылғаннан кейін өзгерген бет, сондықтан оның өз ақ толтыруы өшірулі қалады: {pages}. Ақ қағазда бірдей көрінеді. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Фон қосылғаннан кейін өзгерген беттер, сондықтан олардың өз ақ толтырулары өшірулі қалады: {pages}. Ақ қағазда бірдей көрінеді. PDF or image PDF немесе сурет No file chosen Файл таңдалмады Page number Бет нөмірі diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 873c93b6f9..ab333c55b3 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -804,6 +804,8 @@ Removing the background puts the page's own backdrop back. 배경을 제거하면 페이지 자체의 흰색 채우기가 복원됩니다. A picture covers page {pages} (a scan?), so the background won't show there. 이미지가 {pages}페이지를 덮고 있어(스캔본?) 배경이 보이지 않습니다. Pictures cover pages {pages} (scans?), so the background won't show there. 이미지들이 {pages}페이지를 덮고 있어(스캔본?) 배경이 보이지 않습니다. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. 배경을 추가한 뒤 {pages}페이지가 바뀌어서 자체 흰색 채우기가 꺼진 채로 남습니다. 흰 종이에서는 똑같이 보입니다. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. 배경을 추가한 뒤 {pages}페이지가 바뀌어서 자체 흰색 채우기가 꺼진 채로 남습니다. 흰 종이에서는 똑같이 보입니다. PDF or image PDF 또는 이미지 No file chosen 선택한 파일 없음 Page number 페이지 번호 diff --git a/crates/ui-egui/src/i18n/lo.tsv b/crates/ui-egui/src/i18n/lo.tsv index 04d5b73d1d..d86deae1cd 100644 --- a/crates/ui-egui/src/i18n/lo.tsv +++ b/crates/ui-egui/src/i18n/lo.tsv @@ -830,6 +830,8 @@ Removing the background puts the page's own backdrop back. ການລຶບພື້ນຫຼັງຈະເອົາພື້ນສີຂາວເດີມຂອງໜ້າກັບຄືນມາ. A picture covers page {pages} (a scan?), so the background won't show there. ມີຮູບພາບປົກໜ້າ {pages} ຢູ່ (ພາບສະແກນ?), ສະນັ້ນພື້ນຫຼັງຈະບໍ່ສະແດງຢູ່ບ່ອນນັ້ນ. Pictures cover pages {pages} (scans?), so the background won't show there. ມີຮູບພາບຫຼາຍຮູບປົກໜ້າ {pages} ຢູ່ (ພາບສະແກນ?), ສະນັ້ນພື້ນຫຼັງຈະບໍ່ສະແດງຢູ່ບ່ອນນັ້ນ. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. ໜ້າ {pages} ປ່ຽນໄປຫຼັງຈາກເພີ່ມພື້ນຫຼັງ, ສະນັ້ນພື້ນສີຂາວຂອງມັນເອງຍັງປິດຢູ່. ເທິງເຈ້ຍສີຂາວມັນເບິ່ງຄືເກົ່າ. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. ໜ້າ {pages} ປ່ຽນໄປຫຼັງຈາກເພີ່ມພື້ນຫຼັງ, ສະນັ້ນພື້ນສີຂາວຂອງພວກມັນເອງຍັງປິດຢູ່. ເທິງເຈ້ຍສີຂາວພວກມັນເບິ່ງຄືເກົ່າ. PDF or image PDF ຫຼື ຮູບພາບ No file chosen ຍັງບໍ່ໄດ້ເລືອກໄຟລ໌ Page number ໝາຍເລກໜ້າ diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index 86242b6f41..30c15f1d1e 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -1002,6 +1002,8 @@ Removing the background puts the page's own backdrop back. Pašalinus foną, grąžinamas paties puslapio baltas užpildas. A picture covers page {pages} (a scan?), so the background won't show there. Paveikslėliu padengtas puslapis (nuskaitytas?): {pages}; fonas ten nesimatys. Pictures cover pages {pages} (scans?), so the background won't show there. Paveikslėliais padengti puslapiai (nuskaityti?): {pages}; fonas ten nesimatys. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Puslapis pasikeitė po fono pridėjimo, todėl jo baltas užpildas lieka išjungtas: {pages}. Ant balto popieriaus jis atrodo taip pat. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Puslapiai pasikeitė po fono pridėjimo, todėl jų balti užpildai lieka išjungti: {pages}. Ant balto popieriaus jie atrodo taip pat. Report Ataskaita Report… Ataskaita… Requires user to enter a password for: Naudotojas turi įvesti slaptažodį, kad galėtų: diff --git a/crates/ui-egui/src/i18n/nb.tsv b/crates/ui-egui/src/i18n/nb.tsv index 54bafb923a..f128c21a03 100644 --- a/crates/ui-egui/src/i18n/nb.tsv +++ b/crates/ui-egui/src/i18n/nb.tsv @@ -959,6 +959,8 @@ Removing the background puts the page's own backdrop back. Når bakgrunnen fjernes, kommer sidens egen hvite flate tilbake. A picture covers page {pages} (a scan?), so the background won't show there. Et bilde dekker side {pages} (en skanning?), så bakgrunnen vises ikke der. Pictures cover pages {pages} (scans?), so the background won't show there. Bilder dekker sidene {pages} (skanninger?), så bakgrunnen vises ikke der. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Side {pages} ble endret etter at bakgrunnen ble lagt til, så sidens egen hvite flate forblir slått av. På hvitt papir ser den lik ut. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Sidene {pages} ble endret etter at bakgrunnen ble lagt til, så sidenes egne hvite flater forblir slått av. På hvitt papir ser de like ut. Report Rapport Report… Rapport… Requires user to enter a password for: Krever at brukeren skriver inn et passord for: diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index d3b5abf68c..6567745a04 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -854,6 +854,8 @@ Removing the background puts the page's own backdrop back. Usunięcie tła przywraca własne białe wypełnienie strony. A picture covers page {pages} (a scan?), so the background won't show there. Strona zakryta obrazem (skan?): {pages}; tło nie będzie tam widoczne. Pictures cover pages {pages} (scans?), so the background won't show there. Strony zakryte obrazami (skany?): {pages}; tło nie będzie tam widoczne. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Strona zmieniona po dodaniu tła, więc jej własne białe wypełnienie pozostaje wyłączone: {pages}. Na białym papierze wygląda tak samo. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Strony zmienione po dodaniu tła, więc ich własne białe wypełnienia pozostają wyłączone: {pages}. Na białym papierze wyglądają tak samo. PDF or image PDF lub obraz No file chosen Nie wybrano pliku Page number Numer strony diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 53c2d8461d..5dfb7470c3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -488,6 +488,8 @@ Removing the background puts the page's own backdrop back. Remover o plano de fundo restaura o preenchimento branco da página. A picture covers page {pages} (a scan?), so the background won't show there. Uma imagem cobre a página {pages} (uma digitalização?), então o plano de fundo não aparecerá nela. Pictures cover pages {pages} (scans?), so the background won't show there. Imagens cobrem as páginas {pages} (digitalizações?), então o plano de fundo não aparecerá nelas. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. A página {pages} mudou depois que o plano de fundo foi adicionado, então seu preenchimento branco próprio continua desligado. Em papel branco ela parece igual. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. As páginas {pages} mudaram depois que o plano de fundo foi adicionado, então seus preenchimentos brancos próprios continuam desligados. Em papel branco elas parecem iguais. PDF or image PDF ou imagem No file chosen Nenhum arquivo escolhido Page number Número da página diff --git a/crates/ui-egui/src/i18n/ro.tsv b/crates/ui-egui/src/i18n/ro.tsv index 5eb10620fb..a4200b7836 100644 --- a/crates/ui-egui/src/i18n/ro.tsv +++ b/crates/ui-egui/src/i18n/ro.tsv @@ -959,6 +959,8 @@ Removing the background puts the page's own backdrop back. Eliminarea fundalului readuce umplerea albă proprie a paginii. A picture covers page {pages} (a scan?), so the background won't show there. O imagine acoperă pagina {pages} (o scanare?), deci fundalul nu se va vedea acolo. Pictures cover pages {pages} (scans?), so the background won't show there. Imagini acoperă paginile {pages} (scanări?), deci fundalul nu se va vedea acolo. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Pagina {pages} s-a schimbat după adăugarea fundalului, așa că umplerea ei albă proprie rămâne oprită. Pe hârtie albă arată la fel. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Paginile {pages} s-au schimbat după adăugarea fundalului, așa că umplerile lor albe proprii rămân oprite. Pe hârtie albă arată la fel. Report Raport Report… Raport… Requires user to enter a password for: Utilizatorul trebuie să introducă o parolă pentru: diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c3eaea9ba3..357ff1aa8b 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -856,6 +856,8 @@ Removing the background puts the page's own backdrop back. При удалении фона собственная белая заливка страницы возвращается. A picture covers page {pages} (a scan?), so the background won't show there. Страница, закрытая изображением (скан?): {pages}; фон там не будет виден. Pictures cover pages {pages} (scans?), so the background won't show there. Страницы, закрытые изображениями (сканы?): {pages}; фон там не будет виден. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Страница изменена после добавления фона, поэтому её собственная белая заливка остаётся выключенной: {pages}. На белой бумаге она выглядит так же. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Страницы изменены после добавления фона, поэтому их собственные белые заливки остаются выключенными: {pages}. На белой бумаге они выглядят так же. PDF or image PDF или изображение No file chosen Файл не выбран Page number Номер страницы diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index d81b47794c..f899f273a9 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -1528,6 +1528,8 @@ Removing the background puts the page's own backdrop back. Odstranitev ozadja povrne lastno belo polnilo strani. A picture covers page {pages} (a scan?), so the background won't show there. Stran, ki jo prekriva slika (skeniranje?): {pages}; ozadje tam ne bo vidno. Pictures cover pages {pages} (scans?), so the background won't show there. Strani, ki jih prekrivajo slike (skeniranja?): {pages}; ozadje tam ne bo vidno. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Stran se je spremenila po dodajanju ozadja, zato njeno lastno belo polnilo ostane izklopljeno: {pages}. Na belem papirju je videti enako. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Strani so se spremenile po dodajanju ozadja, zato njihova lastna bela polnila ostanejo izklopljena: {pages}. Na belem papirju so videti enako. Replacing images arrives on the web with image file pickers Zamenjava slik bo v spletni različici na voljo z izbirniki slikovnih datotek Replacing images on page {p} arrives on the web with image pickers ({i}) Zamenjava slik na strani {p} bo v spletni različici na voljo z izbirniki slik ({i}) Reply Odgovori diff --git a/crates/ui-egui/src/i18n/sq.tsv b/crates/ui-egui/src/i18n/sq.tsv index e0b8e0e726..2c638a388d 100644 --- a/crates/ui-egui/src/i18n/sq.tsv +++ b/crates/ui-egui/src/i18n/sq.tsv @@ -1481,6 +1481,8 @@ Removing the background puts the page's own backdrop back. Heqja e sfondit e rikthen mbushjen e bardhë të faqes. A picture covers page {pages} (a scan?), so the background won't show there. Një imazh e mbulon faqen {pages} (skanim?), prandaj sfondi nuk do të duket aty. Pictures cover pages {pages} (scans?), so the background won't show there. Imazhe mbulojnë faqet {pages} (skanime?), prandaj sfondi nuk do të duket aty. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Faqja {pages} ndryshoi pasi u shtua sfondi, ndaj mbushja e saj e bardhë mbetet e fikur. Në letër të bardhë duket njësoj. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Faqet {pages} ndryshuan pasi u shtua sfondi, ndaj mbushjet e tyre të bardha mbeten të fikura. Në letër të bardhë duken njësoj. Replacing images arrives on the web with image file pickers Zëvendësimi i imazheve vjen në ueb me zgjedhësit e skedarëve të imazhit Replacing images on page {p} arrives on the web with image pickers ({i}) Zëvendësimi i imazheve në faqen {p} vjen në ueb me zgjedhësit e imazhit ({i}) Reply Përgjigju diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index adaefdbedf..78a7e72189 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -836,6 +836,8 @@ comment menu Edit సవరించు Removing the background puts the page's own backdrop back. నేపథ్యాన్ని తీసివేస్తే పేజీ సొంత తెల్లని నింపు తిరిగి వస్తుంది. A picture covers page {pages} (a scan?), so the background won't show there. పేజీ {pages}ని ఒక చిత్రం కప్పుతోంది (స్కాన్?), కాబట్టి అక్కడ నేపథ్యం కనిపించదు. Pictures cover pages {pages} (scans?), so the background won't show there. పేజీలు {pages}ని చిత్రాలు కప్పుతున్నాయి (స్కాన్‌లు?), కాబట్టి అక్కడ నేపథ్యం కనిపించదు. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. నేపథ్యాన్ని జోడించిన తర్వాత పేజీ {pages} మారింది, కాబట్టి దాని సొంత తెల్లని నింపు ఆపివేయబడే ఉంటుంది. తెల్ల కాగితంపై అది అలాగే కనిపిస్తుంది. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. నేపథ్యాన్ని జోడించిన తర్వాత పేజీలు {pages} మారాయి, కాబట్టి వాటి సొంత తెల్లని నింపులు ఆపివేయబడే ఉంటాయి. తెల్ల కాగితంపై అవి అలాగే కనిపిస్తాయి. PDF or image PDF లేదా చిత్రం No file chosen ఫైల్ ఏదీ ఎంచుకోలేదు Page number పేజీ సంఖ్య diff --git a/crates/ui-egui/src/i18n/th.tsv b/crates/ui-egui/src/i18n/th.tsv index 2740313342..857ff8be03 100644 --- a/crates/ui-egui/src/i18n/th.tsv +++ b/crates/ui-egui/src/i18n/th.tsv @@ -815,6 +815,8 @@ Removing the background puts the page's own backdrop back. การเอาพื้นหลังออกจะคืนพื้นสีขาวเดิมของหน้า A picture covers page {pages} (a scan?), so the background won't show there. มีรูปภาพปิดหน้า {pages} อยู่ (ภาพสแกน?) จึงจะมองไม่เห็นพื้นหลังที่นั่น Pictures cover pages {pages} (scans?), so the background won't show there. มีรูปภาพหลายรูปปิดหน้า {pages} อยู่ (ภาพสแกน?) จึงจะมองไม่เห็นพื้นหลังที่นั่น + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. หน้า {pages} เปลี่ยนไปหลังจากเพิ่มพื้นหลัง พื้นสีขาวของตัวเองจึงยังปิดอยู่ บนกระดาษสีขาวจะดูเหมือนเดิม + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. หน้า {pages} เปลี่ยนไปหลังจากเพิ่มพื้นหลัง พื้นสีขาวของหน้าเหล่านั้นจึงยังปิดอยู่ บนกระดาษสีขาวจะดูเหมือนเดิม PDF or image PDF หรือรูปภาพ No file chosen ยังไม่ได้เลือกไฟล์ Page number เลขหน้า diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index a31216ca53..cf27544d3b 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -817,6 +817,8 @@ Removing the background puts the page's own backdrop back. Arka planı kaldırmak, sayfanın kendi beyaz dolgusunu geri getirir. A picture covers page {pages} (a scan?), so the background won't show there. Resimle kaplı sayfa (tarama mı?): {pages}; arka plan orada görünmez. Pictures cover pages {pages} (scans?), so the background won't show there. Resimlerle kaplı sayfalar (tarama mı?): {pages}; arka plan orada görünmez. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Arka plan eklendikten sonra değişen sayfa, bu yüzden kendi beyaz dolgusu kapalı kalıyor: {pages}. Beyaz kâğıtta aynı görünür. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Arka plan eklendikten sonra değişen sayfalar, bu yüzden kendi beyaz dolguları kapalı kalıyor: {pages}. Beyaz kâğıtta aynı görünürler. PDF or image PDF veya resim No file chosen Dosya seçilmedi Page number Sayfa numarası diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index c1f294dfd1..265c8f9dbc 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -860,6 +860,8 @@ Removing the background puts the page's own backdrop back. Вилучення тла повертає власну білу заливку сторінки. A picture covers page {pages} (a scan?), so the background won't show there. Сторінка, закрита зображенням (скан?): {pages}; тло там не буде видно. Pictures cover pages {pages} (scans?), so the background won't show there. Сторінки, закриті зображеннями (скани?): {pages}; тло там не буде видно. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Сторінка змінена після додавання тла, тому її власна біла заливка залишається вимкненою: {pages}. На білому папері вона виглядає так само. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Сторінки змінені після додавання тла, тому їхні власні білі заливки залишаються вимкненими: {pages}. На білому папері вони виглядають так само. PDF or image PDF або зображення No file chosen Файл не вибрано Page number Номер сторінки diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 526fbac0b1..2b9189c02c 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -996,6 +996,8 @@ theme Appearance Giao diện Removing the background puts the page's own backdrop back. Gỡ nền sẽ khôi phục lớp tô trắng riêng của trang. A picture covers page {pages} (a scan?), so the background won't show there. Một hình ảnh phủ kín trang {pages} (bản quét?), nên nền sẽ không hiện ở đó. Pictures cover pages {pages} (scans?), so the background won't show there. Hình ảnh phủ kín các trang {pages} (bản quét?), nên nền sẽ không hiện ở đó. + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. Trang {pages} đã thay đổi sau khi thêm nền, nên lớp tô trắng riêng của nó vẫn tắt. Trên giấy trắng trông vẫn như cũ. + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. Các trang {pages} đã thay đổi sau khi thêm nền, nên lớp tô trắng riêng của chúng vẫn tắt. Trên giấy trắng trông vẫn như cũ. Report Báo cáo Report… Báo cáo… Requires user to enter a password for: Yêu cầu người dùng nhập mật khẩu cho: diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 0055556061..5b13735333 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -540,6 +540,8 @@ Removing the background puts the page's own backdrop back. 移除背景后,页面自己的白色填充会恢复。 A picture covers page {pages} (a scan?), so the background won't show there. 第 {pages} 页被一张图片覆盖(扫描件?),因此那里不会显示背景。 Pictures cover pages {pages} (scans?), so the background won't show there. 第 {pages} 页被图片覆盖(扫描件?),因此那里不会显示背景。 + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. 第 {pages} 页在添加背景后有改动,因此其自身的白色填充保持关闭。在白纸上看起来一样。 + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. 第 {pages} 页在添加背景后有改动,因此它们自身的白色填充保持关闭。在白纸上看起来一样。 PDF or image PDF 或图像 No file chosen 未选择文件 Page number 页码 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index bb42c169a6..292d9d0e5e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -492,6 +492,8 @@ Removing the background puts the page's own backdrop back. 移除背景後,頁面自己的白色填色會恢復。 A picture covers page {pages} (a scan?), so the background won't show there. 第 {pages} 頁被一張圖片覆蓋(掃描檔?),因此那裡不會顯示背景。 Pictures cover pages {pages} (scans?), so the background won't show there. 第 {pages} 頁被圖片覆蓋(掃描檔?),因此那裡不會顯示背景。 + Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper. 第 {pages} 頁在加入背景後有變更,因此其本身的白色填色維持關閉。在白紙上看起來一樣。 + Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper. 第 {pages} 頁在加入背景後有變更,因此它們本身的白色填色維持關閉。在白紙上看起來一樣。 PDF or image PDF 或圖片 No file chosen 未選擇檔案 Page number 頁碼 diff --git a/crates/ui-egui/src/marks_ui.rs b/crates/ui-egui/src/marks_ui.rs index 2cf2450336..e9652be1a6 100644 --- a/crates/ui-egui/src/marks_ui.rs +++ b/crates/ui-egui/src/marks_ui.rs @@ -154,6 +154,16 @@ impl MarksDraft { /// How many runs of pages a message lists before it stops with "…". const LISTED_PAGE_RUNS: usize = 8; +/// After Remove background: the pages (0-based, ascending) whose own white backdrop stays off, +/// because they changed after the background was added (`pdfcraft_engine::EditNotice`). +pub(crate) fn backdrops_left_off_message(pages: &[usize]) -> String { + pages_phrase( + pages, + tl!("Page {pages} changed after the background was added, so its own white backdrop stays off. It looks the same on white paper."), + tl!("Pages {pages} changed after the background was added, so their own white backdrops stay off. They look the same on white paper."), + ) +} + /// `one` or `many` with `{pages}` replaced by `pages` (0-based, ascending) as 1-based runs /// ("1-3, 6"), so the message says which pages it means. fn pages_phrase(pages: &[usize], one: &str, many: &str) -> String { diff --git a/crates/ui-egui/tests/it/marks.rs b/crates/ui-egui/tests/it/marks.rs index 8edc9e3ce6..aaeddfd7ec 100644 --- a/crates/ui-egui/tests/it/marks.rs +++ b/crates/ui-egui/tests/it/marks.rs @@ -212,6 +212,39 @@ fn the_background_dialog_offers_to_replace_a_white_backdrop_and_warns_about_scan assert_eq!(backdrop(&h, 0), None, "Update keeps the backdrop replaced"); } +/// 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."); +} + /// 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 ac21f74bf4..98b5096c6c 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -2509,7 +2509,7 @@ status = "shipped" commands = ["edit.background"] tools = ["doc_background", "doc_backdrops"] 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/automation/tests/it/backdrop.rs::a_background_shows_on_a_page_that_paints_its_own_white_backdrop", "crates/ui-egui/tests/it/marks.rs::the_background_dialog_offers_to_replace_a_white_backdrop_and_warns_about_scans", "crates/edit/src/backdrop_tests.rs::content_that_is_not_a_page_backdrop_is_left_alone"] -notes = "colour, or a picture from a file (an image or a page of a PDF) scaled relative to the page. A page that paints its own opaque white backdrop (browsers' Save as PDF, print drivers) used to hide the background entirely; the dialog now offers to replace that fill (on by default; Remove puts it back unless the content was edited since). Our own choice: Acrobat's behaviour on such pages has not been observed. Still hidden: scanned pages (warned), backdrops drawn inside a form XObject, by a non-rectangular path, or in a named colour space; the dialog preview is a schematic, not the page" +notes = "colour, or a picture from a file (an image or a page of a PDF) scaled relative to the page. A page that paints its own opaque white backdrop (browsers' Save as PDF, print drivers) used to hide the background entirely; the dialog now offers to replace that fill (on by default; Remove turns it back on, after edits and redactions too, unless the operators before it changed or it would no longer be painted first, and then says which pages keep it off). The replaced copy's tag holds no page content or reference, so text a redaction or Remove Hidden Information takes off such a page leaves the file. Our own choice: Acrobat's behaviour on such pages has not been observed. Still hidden: scanned pages (warned), backdrops drawn inside a form XObject, by a non-rectangular path, or in a named colour space; the dialog preview is a schematic, not the page" [[feature]] id = "edit.background-update-remove" @@ -2520,8 +2520,8 @@ milestone = "M7" status = "shipped" commands = ["edit.background.update", "edit.background.remove"] tools = ["doc_background", "doc_remove_marks"] -evidence = ["crates/edit/src/tests.rs::replace_and_remove_restore_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/edit/src/backdrop_tests.rs::a_white_fill_backdrop_is_replaced_and_put_back_when_the_background_goes", "crates/edit/src/backdrop_tests.rs::a_backdrop_copy_edited_since_is_kept_when_the_background_goes"] -notes = "Remove also restores a page backdrop Add replaced" +evidence = ["crates/edit/src/tests.rs::replace_and_remove_restore_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/edit/src/backdrop_tests.rs::a_white_fill_backdrop_is_replaced_and_put_back_when_the_background_goes", "crates/edit/src/backdrop_tests.rs::an_edit_after_the_fill_is_kept_and_the_backdrop_still_comes_back", "crates/edit/src/backdrop_tests.rs::a_copy_written_out_again_with_other_spacing_still_comes_back", "crates/edit/src/backdrop_tests.rs::a_change_before_the_fill_leaves_it_off_and_names_the_page", "crates/edit/src/backdrop_tests.rs::a_fill_that_would_cover_content_painted_before_it_stays_off", "crates/engine/src/tests.rs::remove_background_names_the_pages_whose_own_backdrop_stays_off", "crates/automation/tests/it/backdrop.rs::removed_text_leaves_the_file_when_a_background_replaced_the_pages_backdrop"] +notes = "Remove also turns back on a page backdrop Add replaced, keeping edits made after it; where that isn't safe it says which pages keep it off" [[feature]] id = "edit.decoration-profiles"