diff --git a/Cargo.lock b/Cargo.lock index 93c047eef..6ce24d013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4235,6 +4235,7 @@ dependencies = [ "pdfcraft-annot", "pdfcraft-content", "pdfcraft-cos", + "pdfcraft-filters", "pdfcraft-model", "pdfcraft-render", "thiserror 2.0.21", diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 842f6eb73..bf4c55083 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -1261,6 +1261,7 @@ impl Automation { ("discard_print_settings", &mut settings.discard_print_settings), ("flate_unencoded", &mut settings.flate_unencoded), ("object_streams", &mut settings.object_streams), + ("lzw_to_flate", &mut settings.lzw_to_flate), ("remove_invalid_links", &mut settings.remove_invalid_links), ("remove_unreferenced_dests", &mut settings.remove_unreferenced_dests), ] { @@ -1314,6 +1315,7 @@ impl Automation { "alternate_images": o.alternate_images, "tags_removed": o.tags_removed, "streams_compressed": o.streams_compressed, + "lzw_streams": o.lzw_streams, "invalid_links": o.invalid_links, "invalid_bookmarks": o.invalid_bookmarks, "unreferenced_dests": o.unreferenced_dests, diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 32eadd640..92c6ea12d 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -1375,7 +1375,7 @@ pub fn tools() -> Vec { .with(schema(json!({ "doc": doc() }), &["doc"])), t("doc_open_revision", "Open a revision", "Open saved revision `revision` (1 = the oldest) of a document as a new, unsaved document, to see the file as it was then.") .with(schema(json!({ "doc": doc(), "revision": { "type": "integer", "minimum": 1 } }), &["doc", "revision"])), - t("doc_optimize", "Optimize PDF", "Write an optimized copy to `path` (Acrobat's PDF Optimizer). preset: standard (default; Reduce File Size's choices) or mobile (images to 96 ppi above 144, JPEG high quality); the options below change it. compatibility: retain (default; keeps the document's version, or 1.5 when object streams are written), 1.4, 1.5, 1.6, 1.7 or 2.0. merge_fragments (default true) first puts a page picture drawn as many stacked image strips (as some PDF printers write it) back together into one image, so it can be resampled (merged_images, fragments_merged). color / gray: { downsample, ppi, above_ppi, compression: jpeg|zip|retain, quality 1–100 } (defaults: downsample to 150 ppi above 225, JPEG 60). Images are measured where pages draw them and replaced only when smaller. discard_*: thumbnails (default true), alternate_images (true), tags, print_settings; flate_unencoded (true); object_streams (default true; always off for 1.4); remove_invalid_links and remove_unreferenced_dests (true); images and forms the pages list but never draw are always dropped (unused_xobjects). discard: Remove Hidden Information categories (metadata, attachments, comments, form-fields, hidden-text, hidden-layers, bookmarks, links-actions-scripts, private-data). Signed documents are refused. The open document is unchanged.") + t("doc_optimize", "Optimize PDF", "Write an optimized copy to `path` (Acrobat's PDF Optimizer). preset: standard (default; Reduce File Size's choices) or mobile (images to 96 ppi above 144, JPEG high quality); the options below change it. compatibility: retain (default; keeps the document's version, or 1.5 when object streams are written), 1.4, 1.5, 1.6, 1.7 or 2.0. merge_fragments (default true) first puts a page picture drawn as many stacked image strips (as some PDF printers write it) back together into one image, so it can be resampled (merged_images, fragments_merged). color / gray: { downsample, ppi, above_ppi, compression: jpeg|zip|retain, quality 1–100 } (defaults: downsample to 150 ppi above 225, JPEG 60). Images are measured where pages draw them and replaced only when smaller. discard_*: thumbnails (default true), alternate_images (true), tags, print_settings; flate_unencoded (true); object_streams (default true; always off for 1.4); lzw_to_flate (true) decodes LZW streams and stores them with Flate instead, leaving any it cannot decode unchanged (lzw_streams counts the streams converted); remove_invalid_links and remove_unreferenced_dests (true); images and forms the pages list but never draw are always dropped (unused_xobjects). discard: Remove Hidden Information categories (metadata, attachments, comments, form-fields, hidden-text, hidden-layers, bookmarks, links-actions-scripts, private-data). Signed documents are refused. The open document is unchanged.") .cmd("optimize.advanced") .with(schema( json!({ @@ -1392,6 +1392,7 @@ pub fn tools() -> Vec { "discard_print_settings": { "type": "boolean" }, "flate_unencoded": { "type": "boolean" }, "object_streams": { "type": "boolean" }, + "lzw_to_flate": { "type": "boolean" }, "remove_invalid_links": { "type": "boolean" }, "remove_unreferenced_dests": { "type": "boolean" }, "discard": { "type": "array", "items": { "type": "string" } }, diff --git a/crates/automation/tests/it/automation.rs b/crates/automation/tests/it/automation.rs index cadd3e262..7106ea87a 100644 --- a/crates/automation/tests/it/automation.rs +++ b/crates/automation/tests/it/automation.rs @@ -5784,6 +5784,56 @@ fn optimizing_through_tools() { assert!(matches!(a.call("doc_optimize", &json!({ "doc": doc, "path": "x.pdf", "discard": ["everything"] })), Err(ToolError::InvalidArgs(_)))); } +/// `data` as LZW with one 9-bit literal code per byte (clear-table first, EOD last): valid for +/// fewer than 250 bytes, before the code width grows. +fn lzw_literals(data: &[u8]) -> Vec { + let codes: Vec = std::iter::once(256).chain(data.iter().map(|&b| u16::from(b))).chain([257]).collect(); + let bits: Vec = codes.iter().flat_map(|c| (0..9).rev().map(move |i| c >> i & 1 == 1)).collect(); + bits.chunks(8).map(|byte| byte.iter().enumerate().fold(0u8, |acc, (i, &b)| acc | (u8::from(b) << (7 - i)))).collect() +} + +#[test] +fn lzw_streams_are_stored_with_flate_through_tools() { + use pdfcraft_cos::{Dict, Document, Object, SaveOptions, Stream, write_full}; + let dir = workdir("optimize-lzw"); + let ops = b"0.2 0.4 0.8 rg 72 72 200 100 re f 0 0 1 RG 72 300 m 400 500 l S".to_vec(); + let mut doc = Document::new_empty(); + let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap(); + let lzw = Stream::from_raw(Dict::from_iter([(b"Filter".to_vec(), Object::name("LZWDecode"))]), lzw_literals(&ops)); + assert_eq!(lzw.decoded_strict().unwrap(), ops); + let content = doc.add(Object::Stream(lzw)); + let page = doc.add(Object::Dict(Dict::from_iter([ + (b"Type".to_vec(), Object::name("Page")), + (b"Parent".to_vec(), Object::Ref(pages)), + (b"MediaBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 612.into(), 792.into()])), + (b"Contents".to_vec(), Object::Ref(content)), + ]))); + doc.update_dict(pages, |d| { + d.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(page)])); + d.set(b"Count".to_vec(), Object::Int(1)); + }) + .unwrap(); + std::fs::write(dir.join("lzw.pdf"), write_full(&doc, &SaveOptions::default()).unwrap()).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "lzw.pdf" }))["doc"].as_u64().unwrap(); + let filters = |name: &str| { + let saved = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(dir.join(name)).unwrap())).unwrap(); + let catalog = saved.get(saved.root().unwrap()).as_dict().cloned().unwrap(); + let pages = saved.get(catalog.reference(b"Pages").unwrap()).as_dict().cloned().unwrap(); + let page = saved.get(pages.get(b"Kids").and_then(|k| k.as_array()?.first()?.as_ref()).unwrap()).as_dict().cloned().unwrap(); + match &*saved.get(page.reference(b"Contents").unwrap()) { + Object::Stream(s) => (s.dict.get(b"Filter").cloned(), s.decoded_strict().unwrap()), + _ => panic!("the page content is not a stream"), + } + }; + let r = ok(&mut a, "doc_optimize", json!({ "doc": doc, "path": "flate.pdf" })); + assert_eq!(r["lzw_streams"].as_u64(), Some(1), "{r}"); + assert_eq!(filters("flate.pdf"), (Some(Object::name("FlateDecode")), ops.clone())); + let off = ok(&mut a, "doc_optimize", json!({ "doc": doc, "path": "kept.pdf", "lzw_to_flate": false })); + assert_eq!(off["lzw_streams"].as_u64(), Some(0), "{off}"); + assert_eq!(filters("kept.pdf"), (Some(Object::name("LZWDecode")), ops)); +} + /// A one-page PDF whose page picture is drawn as `rows` grey images one pixel row high, 600 px /// wide on a 72 pt square (600 ppi), as some PDF printers write it. fn printed_in_strips(rows: usize) -> Vec { diff --git a/crates/optimize/Cargo.toml b/crates/optimize/Cargo.toml index 184ee9697..3b3bbac75 100644 --- a/crates/optimize/Cargo.toml +++ b/crates/optimize/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] pdfcraft-cos = { workspace = true } +pdfcraft-filters = { workspace = true } pdfcraft-content = { workspace = true } pdfcraft-model = { workspace = true } pdfcraft-annot = { workspace = true } diff --git a/crates/optimize/README.md b/crates/optimize/README.md index 3d39eb666..d061d2cad 100644 --- a/crates/optimize/README.md +++ b/crates/optimize/README.md @@ -32,7 +32,11 @@ let report = optimize_with_progress(&mut doc, &settings, &mut |stage| keep_going - **Discard objects:** thumbnails, alternate images, document tags, print settings. - **Clean up:** Flate for streams with no filter; for streams with an outer ASCII85 or ASCIIHex layer, that layer is removed when the result is smaller (the inner filters keep their encoded - bytes). Streams with `/DecodeParms`, images and object streams are left alone. + bytes; streams with `/DecodeParms`, images and object streams keep their ASCII layer. LZW-compressed + streams, content and images alike, are decoded and stored with Flate instead (Acrobat's "In + streams that use LZW encoding, use Flate instead"): a predictor carries over to Flate without + `EarlyChange`, filters applied before the LZW one go with it and those after it keep their + bytes. A stream whose LZW data doesn't decode cleanly is left as it is. Not yet: inline images. The engine runs Remove Hidden Information (`pdfcraft-redact`) for the user-data categories, then this, then merges identical objects and writes a full, compressed save diff --git a/crates/optimize/src/lib.rs b/crates/optimize/src/lib.rs index 917787e2c..e2bc2cf50 100644 --- a/crates/optimize/src/lib.rs +++ b/crates/optimize/src/lib.rs @@ -14,7 +14,7 @@ //! print settings. //! - **Clean up:** drop images and forms that pages list but never draw (such as a deleted //! image); Flate-compress streams that have no filter; remove a redundant outer ASCII85 or -//! ASCIIHex layer from streams that have one. +//! ASCIIHex layer from streams that have one; store LZW-compressed streams with Flate instead. //! //! Metadata, attachments, comments, scripts, private data, hidden layers, bookmarks and form //! fields are discarded by `pdfcraft-redact`'s Remove Hidden Information, which the engine @@ -30,7 +30,8 @@ mod unused; pub use audit::{SpaceCategory, SpaceUse, audit_space}; -use pdfcraft_cos::{Document, ObjRef, Object, Stream, serialize}; +use pdfcraft_cos::{Dict, Document, MAX_PAGE_DECODED, ObjRef, Object, Stream, serialize}; +use pdfcraft_filters::{Filter, Params}; pub use images::effective_resolutions; @@ -155,6 +156,8 @@ pub struct Settings { /// Clean Up: pack non-stream objects into compressed object streams with a cross-reference /// stream (PDF 1.5+, §7.5.7–7.5.8). Always off when `compatibility` is [`Compatibility::Pdf1_4`]. pub object_streams: bool, + /// Clean Up: decode LZW-compressed streams and store them with Flate instead. + pub lzw_to_flate: bool, /// Clean Up: remove links (and bookmark destinations) that point nowhere. pub remove_invalid_links: bool, /// Clean Up: remove named destinations nothing refers to. @@ -183,6 +186,7 @@ impl Default for Settings { discard_print_settings: false, flate_unencoded: true, object_streams: true, + lzw_to_flate: true, remove_invalid_links: true, remove_unreferenced_dests: true, } @@ -243,6 +247,8 @@ pub struct Report { pub tags_removed: bool, pub print_settings: usize, pub streams_compressed: usize, + /// Streams whose LZW compression was replaced with Flate (Clean Up). + pub lzw_streams: usize, /// Links removed and bookmarks whose destination was cleared (Clean Up). pub invalid_links: usize, pub invalid_bookmarks: usize, @@ -375,6 +381,19 @@ pub fn optimize_with_progress(doc: &mut Document, settings: &Settings, progress: if settings.remove_unreferenced_dests { report.unreferenced_dests = links::remove_unreferenced_dests(doc)?; } + // Before the unencoded-stream pass, which would otherwise take the ASCII layer off an + // `[/ASCII85Decode /LZWDecode]` stream first and count it twice. + if settings.lzw_to_flate { + for num in doc.object_numbers() { + let r = ObjRef { num, generation: doc.generation(num) }; + let obj = doc.get(r); + let Object::Stream(s) = &*obj else { continue }; + if let Some(replacement) = lzw_to_flate(s, MAX_LZW_DECODED) { + doc.set(r, Object::Stream(replacement)); + report.lzw_streams += 1; + } + } + } if settings.flate_unencoded { for num in doc.object_numbers() { let r = ObjRef { num, generation: doc.generation(num) }; @@ -401,6 +420,101 @@ pub fn optimize_with_progress(doc: &mut Document, settings: &Settings, progress: Ok(report) } +/// Largest decoded LZW stream converted, as for a page's content: the data and its check are +/// held together, so a few hundred kilobytes of hostile LZW can't ask for gigabytes. +const MAX_LZW_DECODED: usize = MAX_PAGE_DECODED; + +/// `s` with its LZW compression replaced by Flate (Acrobat's "In streams that use LZW encoding, +/// use Flate instead"). The filters up to the last LZW one are decoded, strictly and with its +/// predictor; the result is Flate-encoded under the same predictor, without `EarlyChange`, and +/// the filters after it keep their encoded bytes. A stream that doesn't decode cleanly, or +/// whose filters or parameters can't be read for certain, or that decodes past `max` bytes, is +/// left as it is. +fn lzw_to_flate(s: &Stream, max: usize) -> Option { + // An external file's data isn't in `raw`; cross-reference and object streams are rewritten + // on save, so converting them would change nothing in the file. + if s.dict.contains(b"F") || matches!(s.dict.name(b"Type"), Some(b"XRef" | b"ObjStm")) { + return None; + } + let names: Vec<&[u8]> = match s.dict.get(b"Filter")? { + Object::Name(n) => vec![n.as_slice()], + Object::Array(a) => a.iter().map(Object::as_name).collect::>()?, + _ => return None, + }; + // `/Crypt` was applied when the object was loaded, and dropping it would change how the + // stream is encrypted. + if names.contains(&&b"Crypt"[..]) { + return None; + } + let lzw = names.iter().rposition(|n| matches!(*n, b"LZWDecode" | b"LZW"))?; + // One entry per filter, each a dictionary or null; anything else (an indirect reference, an + // array of the wrong length) would be read as defaults, so the data might not decode as + // written. + let parms: Vec> = match s.dict.get(b"DecodeParms") { + None | Some(Object::Null) => vec![None; names.len()], + Some(Object::Dict(d)) if names.len() == 1 => vec![Some(d)], + Some(Object::Array(a)) if a.len() == names.len() => a + .iter() + .map(|p| match p { + Object::Dict(d) => Some(Some(d)), + Object::Null => Some(None), + _ => None, + }) + .collect::>()?, + _ => return None, + }; + let chain = s.filters(); + let stages = chain.get(..=lzw)?; + let (_, params) = stages.last()?; + // The decoded stages' parameters must be direct integers: `filters` reads anything else (a + // reference, 12.5, a name) as the default, and the dictionary carried over would then + // describe the new data differently from how it was encoded. + let direct = |p: &Option<&Dict>| p.is_none_or(|d| d.iter().all(|(_, v)| matches!(v, Object::Int(_)))); + if !parms.get(..=lzw)?.iter().all(direct) { + return None; + } + let data = pdfcraft_filters::decode(stages, &s.raw, max).ok()?; + let flate = Params { early_change: 1, ..params.clone() }; + let raw = pdfcraft_filters::encode(&Filter::Flate, &flate, &data).ok()?; + // The predictor's encoder must give back exactly what its decoder read (a short last row, + // unusual parameters), or the stream would change. The limit counts a PNG predictor's tag + // bytes too, at most one per row of at least one byte. + if pdfcraft_filters::decode(&[(Filter::Flate, flate)], &raw, data.len().saturating_mul(2).saturating_add(1)).ok()? != data { + return None; + } + // Without a predictor the other parameters say nothing to Flate. + let first = parms.get(lzw).copied().flatten().filter(|_| params.predictor > 1).map(|d| { + let mut d = d.clone(); + d.remove(b"EarlyChange"); + Object::Dict(d) + }); + let rest_names = names.get(lzw + 1..)?; + let rest_parms = parms.get(lzw + 1..)?; + let mut dict = s.dict.clone(); + dict.remove(b"Length"); + if rest_names.is_empty() { + dict.set(b"Filter".to_vec(), Object::name("FlateDecode")); + match first { + Some(p) => dict.set(b"DecodeParms".to_vec(), p), + None => { + dict.remove(b"DecodeParms"); + } + } + } else { + let filters = std::iter::once(Object::name("FlateDecode")).chain(rest_names.iter().map(|n| Object::Name(n.to_vec()))); + dict.set(b"Filter".to_vec(), Object::Array(filters.collect())); + let parms: Vec = std::iter::once(first.unwrap_or(Object::Null)) + .chain(rest_parms.iter().map(|p| p.map_or(Object::Null, |d| Object::Dict(d.clone())))) + .collect(); + if parms.iter().all(|p| matches!(p, Object::Null)) { + dict.remove(b"DecodeParms"); + } else { + dict.set(b"DecodeParms".to_vec(), Object::Array(parms)); + } + } + Some(Stream::from_raw(dict, raw)) +} + /// `s` without its outer ASCII85 or ASCIIHex layer (`[/ASCII85Decode /FlateDecode]` becomes /// `[/FlateDecode]`), when that is smaller. Only the outer layer is decoded: strictly, and within /// the stored size, which ASCII text never falls below, so the inner encoded bytes stay exact. @@ -431,3 +545,20 @@ fn stored_len(s: &Stream) -> Option { serialize(&Object::Dict(dict), &mut out); out.len().checked_add(s.raw.len()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_lzw_stream_that_decodes_past_the_limit_is_left_as_it_is() { + let data = b"0 0 m 612 792 l S\n".repeat(64); + let raw = pdfcraft_filters::encode(&Filter::Lzw, &Params::default(), &data).unwrap(); + let mut dict = Dict::new(); + dict.set(b"Filter".to_vec(), Object::name("LZWDecode")); + let s = Stream::from_raw(dict, raw); + assert!(lzw_to_flate(&s, data.len() - 1).is_none()); + let converted = lzw_to_flate(&s, data.len()).unwrap(); + assert_eq!(converted.decoded_strict().unwrap(), data); + } +} diff --git a/crates/optimize/tests/optimize.rs b/crates/optimize/tests/optimize.rs index 581925150..8802ef351 100644 --- a/crates/optimize/tests/optimize.rs +++ b/crates/optimize/tests/optimize.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full}; +use pdfcraft_filters::{Filter, Params}; use pdfcraft_optimize::{ Compatibility, Compression, ImageSettings, OptimizeError, Settings, SpaceCategory, audit_space, effective_resolutions, optimize, }; @@ -1280,3 +1281,214 @@ fn named_dest_keys(doc: &Document) -> Vec> { let arr = doc.resolve(tree.get(b"Names").unwrap()).as_array().cloned().unwrap(); arr.chunks(2).filter_map(|c| doc.resolve(&c[0]).as_string().map(|s| s.bytes.clone())).collect() } + +/// Page content long enough for LZW codes to grow past 9 bits, where `EarlyChange` matters. +fn lzw_ops() -> Vec { + (0..300).flat_map(|i| format!("{} {} m {} {} l S\n", i % 97, i * 7 % 389, i * 13 % 612, i * 31 % 792).into_bytes()).collect() +} + +fn lzw(data: &[u8], params: &Params) -> Vec { + pdfcraft_filters::encode(&Filter::Lzw, params, data).unwrap() +} + +/// `params` as a `/DecodeParms` dictionary, only the entries that differ from the defaults. +fn decode_parms(p: &Params) -> Dict { + let mut d = Dict::new(); + for (k, v, default) in [ + (&b"Predictor"[..], p.predictor, 1), + (b"Colors", p.colors, 1), + (b"BitsPerComponent", p.bits_per_component, 8), + (b"Columns", p.columns, 1), + (b"EarlyChange", p.early_change, 1), + ] { + if v != default { + d.set(k.to_vec(), Object::Int(v)); + } + } + d +} + +/// A 64 × 48 RGB image, LZW-encoded with `params`. +fn lzw_image(doc: &mut Document, params: &Params) -> (ObjRef, Vec) { + let (w, h) = (64u32, 48u32); + let px: Vec = (0..w * h).flat_map(|i| [(i % w * 4) as u8, (i / w * 5) as u8, ((i % w + i / w) * 2) as u8]).collect(); + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("XObject")); + d.set(b"Subtype".to_vec(), Object::name("Image")); + d.set(b"Width".to_vec(), Object::Int(w.into())); + d.set(b"Height".to_vec(), Object::Int(h.into())); + d.set(b"BitsPerComponent".to_vec(), Object::Int(8)); + d.set(b"ColorSpace".to_vec(), Object::name("DeviceRGB")); + d.set(b"Filter".to_vec(), Object::name("LZWDecode")); + d.set(b"DecodeParms".to_vec(), Object::Dict(decode_parms(params))); + (doc.add(Object::Stream(Stream::from_raw(d, lzw(&px, params)))), px) +} + +/// Settings that leave images to the clean-up: no resampling, no recompression. +fn clean_up_only() -> Settings { + let keep = ImageSettings { downsample: false, compression: Compression::Retain, ..ImageSettings::REDUCE }; + Settings { color: keep, gray: keep, ..Settings::default() } +} + +#[test] +fn lzw_streams_become_flate_and_decode_to_the_same_bytes() { + let ops = lzw_ops(); + let early = Params::default(); + let late = Params { early_change: 0, ..Params::default() }; + let png = Params { predictor: 15, colors: 3, columns: 64, ..Params::default() }; + let png_late = Params { early_change: 0, ..png.clone() }; + let mut doc = Document::new_empty(); + let content = |doc: &mut Document, p: &Params| { + let mut d = Dict::new(); + d.set(b"Filter".to_vec(), Object::name("LZWDecode")); + if *p != Params::default() { + d.set(b"DecodeParms".to_vec(), Object::Dict(decode_parms(p))); + } + doc.add(Object::Stream(Stream::from_raw(d, lzw(&ops, p)))) + }; + let c_early = content(&mut doc, &early); + let c_late = content(&mut doc, &late); + let (img, px) = lzw_image(&mut doc, &png); + let (img_late, _) = lzw_image(&mut doc, &png_late); + page(&mut doc, &[("Im", img), ("Im2", img_late)], "q 64 0 0 48 72 600 cm /Im Do Q q 64 0 0 48 172 600 cm /Im2 Do Q"); + let pg = pdfcraft_annot::page_refs(&doc).unwrap()[0]; + doc.update_dict(pg, |d| { + d.set(b"Contents".to_vec(), Object::Array(vec![Object::Ref(c_early), Object::Ref(c_late), d.get(b"Contents").unwrap().clone()])) + }) + .unwrap(); + // Each LZW stream reads as its own data before, so a decoder ignoring EarlyChange fails here. + assert_eq!((stream(&doc, c_early).decoded_strict().unwrap(), stream(&doc, c_late).decoded_strict().unwrap()), (ops.clone(), ops.clone())); + assert!(lzw(&ops, &early) != lzw(&ops, &late), "the two code widths differ on this data"); + + let r = optimize(&mut doc, &clean_up_only()).unwrap(); + assert_eq!(r.lzw_streams, 4, "{r:?}"); + for (label, at, data) in + [("content", c_early, &ops), ("content, EarlyChange 0", c_late, &ops), ("image", img, &px), ("image, EarlyChange 0", img_late, &px)] + { + let after = stream(&doc, at); + assert_eq!(after.dict.get(b"Filter"), Some(&Object::name("FlateDecode")), "{label}"); + assert_eq!(&after.decoded_strict().unwrap(), data, "{label}"); + let parms = after.dict.get(b"DecodeParms").and_then(Object::as_dict); + assert!(parms.is_none_or(|p| !p.contains(b"EarlyChange")), "{label}: EarlyChange is LZW's alone"); + } + // The image keeps its PNG predictor, now applied under Flate. + let parms = stream(&doc, img).dict.get(b"DecodeParms").and_then(Object::as_dict).cloned().unwrap(); + assert_eq!((parms.int(b"Predictor"), parms.int(b"Colors"), parms.int(b"Columns")), (Some(15), Some(3), Some(64))); + assert!(!stream(&doc, c_late).dict.contains(b"DecodeParms"), "nothing left to say for the content stream"); +} + +#[test] +fn a_page_drawn_from_lzw_streams_renders_the_same_after_conversion() { + let png = Params { predictor: 12, colors: 3, columns: 64, early_change: 0, ..Params::default() }; + let mut doc = Document::new_empty(); + let (img, _) = lzw_image(&mut doc, &png); + let pg = page(&mut doc, &[("Im", img)], ""); + let ops = b"0.2 0.5 0.8 rg 36 36 300 200 re f q 256 0 0 192 72 400 cm /Im Do Q\n".repeat(4); + let mut d = Dict::new(); + d.set(b"Filter".to_vec(), Object::name("LZWDecode")); + d.set(b"DecodeParms".to_vec(), Object::Dict(decode_parms(&Params { early_change: 0, ..Params::default() }))); + let c = doc.add(Object::Stream(Stream::from_raw(d, lzw(&ops, &Params { early_change: 0, ..Params::default() })))); + doc.update_dict(pg, |p| p.set(b"Contents".to_vec(), Object::Ref(c))).unwrap(); + let render = |doc: &Document| { + let bytes = write_full(doc, &SaveOptions::default()).unwrap(); + let mut r = pdfcraft_render::PageRenderer::new(Arc::new(bytes), pdfcraft_render::RenderConfig::default()); + let out = r.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() }); + assert!(out.error.is_none(), "{:?}", out.error); + out.rgba + }; + let before = render(&doc); + // The picture and the rectangle are drawn: the page is not blank. + assert!(before.chunks(4).any(|p| p[..3] != [255, 255, 255])); + let r = optimize(&mut doc, &clean_up_only()).unwrap(); + assert_eq!(r.lzw_streams, 2, "{r:?}"); + assert_eq!(stream(&doc, c).dict.name(b"Filter"), Some(&b"FlateDecode"[..])); + assert!(render(&doc) == before, "the page renders pixel for pixel as before"); +} + +#[test] +fn lzw_inside_a_filter_chain_is_replaced_and_the_other_stages_kept() { + let ops = lzw_ops(); + let inner = lzw(&ops, &Params::default()); + // An ASCII85 layer outside the LZW goes with it; a JPEG inside it stays encoded as it was. + let (mut doc, c) = with_content(filtered(&["ASCII85Decode", "LZWDecode"], ascii85(&inner))); + let photo = image(&mut doc, 40, 30, 3, true, None); + let jpeg = stream(&doc, photo).raw.to_vec(); + let mut picture = filtered(&["LZWDecode", "DCTDecode"], lzw(&jpeg, &Params::default())); + picture.dict.set(b"DecodeParms".to_vec(), Object::Array(vec![Object::Null, Object::Null])); + let pic = doc.add(Object::Stream(picture)); + // An ASCII85 layer over LZW with a PNG predictor: the predictor's parameters move to the one + // Flate filter left, without EarlyChange. + let rows: Vec = (0..40 * 30).map(|i| (i % 40 * 3 + i / 40) as u8).collect(); + let png = Params { predictor: 12, columns: 40, early_change: 0, ..Params::default() }; + let mut predicted = filtered(&["ASCII85Decode", "LZWDecode"], ascii85(&lzw(&rows, &png))); + predicted.dict.set(b"DecodeParms".to_vec(), Object::Array(vec![Object::Null, Object::Dict(decode_parms(&png))])); + let pred = doc.add(Object::Stream(predicted)); + let r = optimize(&mut doc, &clean_up_only()).unwrap(); + assert_eq!(r.lzw_streams, 3, "{r:?}"); + let after = stream(&doc, pred); + assert_eq!(after.dict.get(b"Filter"), Some(&Object::name("FlateDecode"))); + let parms = after.dict.get(b"DecodeParms").and_then(Object::as_dict).cloned().unwrap(); + assert_eq!((parms.int(b"Predictor"), parms.int(b"Columns"), parms.contains(b"EarlyChange")), (Some(12), Some(40), false)); + assert_eq!(after.decoded_strict().unwrap(), rows); + let after = stream(&doc, c); + assert_eq!(after.dict.get(b"Filter"), Some(&Object::name("FlateDecode"))); + assert_eq!(after.decoded_strict().unwrap(), ops); + let after = stream(&doc, pic); + assert_eq!(after.dict.get(b"Filter"), Some(&Object::Array(vec![Object::name("FlateDecode"), Object::name("DCTDecode")]))); + assert!(!after.dict.contains(b"DecodeParms"), "all null: dropped"); + let inside = pdfcraft_filters::decode(&[(Filter::Flate, Params::default())], &after.raw, 1 << 20).unwrap(); + assert_eq!(inside, jpeg, "the JPEG bytes are kept exactly"); +} + +#[test] +fn lzw_streams_that_cannot_be_read_or_are_not_asked_for_stay_unchanged() { + let ops = lzw_ops(); + let good = lzw(&ops, &Params::default()); + // 9-bit codes 511 after a literal: past the table, corrupt. + let corrupt = vec![0x80, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]; + assert!(filtered(&["LZWDecode"], corrupt.clone()).decoded_strict().is_err()); + let mut bad_predictor = filtered(&["LZWDecode"], good.clone()); + let mut parms = Dict::new(); + parms.set(b"Predictor".to_vec(), Object::Int(12)); + parms.set(b"Columns".to_vec(), Object::Int(7)); + bad_predictor.dict.set(b"DecodeParms".to_vec(), Object::Dict(parms)); + let mut indirect_parms = filtered(&["LZWDecode"], good.clone()); + indirect_parms.dict.set(b"DecodeParms".to_vec(), Object::Ref(ObjRef { num: 999, generation: 0 })); + // Columns the reader can't take as an integer: read as the default 1 here, so the data is + // predicted with one column and decodes, but a viewer resolving the value would use another. + let one_column = Params { predictor: 12, ..Params::default() }; + let odd_columns = |columns: Object| { + let mut s = filtered(&["LZWDecode"], lzw(&ops, &one_column)); + let mut parms = Dict::new(); + parms.set(b"Predictor".to_vec(), Object::Int(12)); + parms.set(b"Columns".to_vec(), columns); + s.dict.set(b"DecodeParms".to_vec(), Object::Dict(parms)); + s + }; + assert_eq!(odd_columns(Object::Real(12.5)).decoded_strict().unwrap(), ops, "read with one column"); + let mut objstm = filtered(&["LZWDecode"], good.clone()); + objstm.dict.set(b"Type".to_vec(), Object::name("ObjStm")); + let mut external = filtered(&["LZWDecode"], good.clone()); + external.dict.set(b"F".to_vec(), Object::String(pdfcraft_cos::PdfString::literal(b"data.bin".to_vec()))); + let cases = [ + ("corrupt LZW", filtered(&["LZWDecode"], corrupt), Settings::default()), + ("corrupt under a PNG predictor", bad_predictor, Settings::default()), + ("indirect DecodeParms", indirect_parms, Settings::default()), + ("external file", external, Settings::default()), + ("object stream, rewritten on save", objstm, Settings::default()), + ("Columns by reference", odd_columns(Object::Ref(ObjRef { num: 9, generation: 0 })), Settings::default()), + ("Columns not an integer", odd_columns(Object::Real(12.5)), Settings::default()), + ("crypt filter", filtered(&["Crypt", "LZWDecode"], good.clone()), Settings::default()), + ("option off", filtered(&["LZWDecode"], good.clone()), Settings { lzw_to_flate: false, ..Settings::default() }), + ]; + for (label, s, settings) in cases { + let (mut doc, c) = with_content(s.clone()); + let r = optimize(&mut doc, &settings).unwrap(); + assert_eq!(r.lzw_streams, 0, "{label}"); + assert_eq!(stream(&doc, c), s, "{label}"); + } + // The same data, readable and asked for, is converted: the cases above fail for their reason. + let (mut doc, c) = with_content(filtered(&["LZWDecode"], good)); + assert_eq!(optimize(&mut doc, &Settings::default()).unwrap().lzw_streams, 1); + assert_eq!(stream(&doc, c).decoded_strict().unwrap(), ops); +} diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 765bbba55..3b8e5c138 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -611,6 +611,7 @@ Discard private data of other applications تجاهل البيانات الخاصة بالتطبيقات الأخرى Discard hidden text تجاهل النص المخفي Use Flate to encode streams that are not encoded استخدام Flate لترميز التدفقات غير المرمَّزة + In streams that use LZW encoding, use Flate instead في التدفقات التي تستخدم ترميز LZW، استخدام Flate بدلاً منه Remove invalid links and bookmarks إزالة الروابط والإشارات المرجعية غير الصالحة Remove unreferenced named destinations إزالة الوجهات المسماة غير المشار إليها Compress document structure (object streams) ضغط بنية المستند (تدفقات الكائنات) diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 9bb377a93..efa132ed3 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -282,6 +282,7 @@ Discard private data of other applications Премахни частните данни на други приложения Discard hidden text Премахни скрития текст Use Flate to encode streams that are not encoded Кодирай с Flate некодираните потоци + In streams that use LZW encoding, use Flate instead Кодирай с Flate потоците, кодирани с LZW Remove invalid links and bookmarks Премахни невалидните връзки и отметки Remove unreferenced named destinations Премахни неизползваните именувани местоназначения Compress document structure (object streams) Компресирай структурата на документа (потоци от обекти) diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index dd2f497b8..83962d0cb 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -574,6 +574,7 @@ Discard private data of other applications Descarta les dades privades d'altres aplicacions Discard hidden text Descarta el text ocult Use Flate to encode streams that are not encoded Utilitza Flate per codificar els fluxos que no estan codificats + In streams that use LZW encoding, use Flate instead Als fluxos que utilitzen codificació LZW, utilitza Flate Remove invalid links and bookmarks Elimina els enllaços i marcadors no vàlids Remove unreferenced named destinations Elimina les destinacions amb nom sense referència Compress document structure (object streams) Comprimeix l'estructura del document (fluxos d'objectes) diff --git a/crates/ui-egui/src/i18n/ckb.tsv b/crates/ui-egui/src/i18n/ckb.tsv index 804e7a97a..e290099f5 100644 --- a/crates/ui-egui/src/i18n/ckb.tsv +++ b/crates/ui-egui/src/i18n/ckb.tsv @@ -581,6 +581,7 @@ Discard private data of other applications فڕێدانی داتا تایبەتەکانی بەرنامەکانی تر Discard hidden text فڕێدانی دەقی شاراوە Use Flate to encode streams that are not encoded بەکارهێنانی Flate بۆ کۆدکردنی ئەو لێشاوانەی کۆد نەکراون + In streams that use LZW encoding, use Flate instead بەکارهێنانی Flate لە جیاتی LZW بۆ ئەو لێشاوانەی بە LZW کۆد کراون Remove invalid links and bookmarks لابردنی بەستەر و دڵخوازە نادروستەکان Remove unreferenced named destinations لابردنی مەبەستە ناونراوەکانی ئاماژەپێنەکراو Compress document structure (object streams) پەستاندنی پێکهاتەی بەڵگەنامە (لێشاوەکانی تەن) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 66cac29aa..ef380d623 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -692,6 +692,7 @@ key Space Mezerník Discard private data of other applications Zahodit soukromá data jiných aplikací Discard hidden text Zahodit skrytý text Use Flate to encode streams that are not encoded Kódovat nekódované proudy metodou Flate + In streams that use LZW encoding, use Flate instead Proudy kódované LZW kódovat metodou Flate Remove invalid links and bookmarks Odebrat neplatné odkazy a záložky Remove unreferenced named destinations Odebrat nepoužívané pojmenované cíle Compress document structure (object streams) Komprimovat strukturu dokumentu (proudy objektů) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 76366d452..75c4b6274 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -584,6 +584,7 @@ Discard private data of other applications Private Daten anderer Anwendungen verwerfen Discard hidden text Ausgeblendeten Text verwerfen Use Flate to encode streams that are not encoded Nicht codierte Streams mit Flate codieren + In streams that use LZW encoding, use Flate instead In LZW-codierten Streams stattdessen Flate verwenden Remove invalid links and bookmarks Ungültige Links und Lesezeichen entfernen Remove unreferenced named destinations Nicht referenzierte benannte Ziele entfernen Compress document structure (object streams) Dokumentstruktur komprimieren (Objekt-Streams) diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 18464664a..7c8160901 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1923,6 +1923,7 @@ Discard private data of other applications Αφαίρεση ιδιωτικών δεδομένων άλλων εφαρμογών Discard hidden text Αφαίρεση κρυφού κειμένου Use Flate to encode streams that are not encoded Χρήση συμπίεσης Flate για ροές που δεν είναι συμπιεσμένες + In streams that use LZW encoding, use Flate instead Χρήση Flate αντί για LZW στις ροές με κωδικοποίηση LZW Remove invalid links and bookmarks Αφαίρεση μη έγκυρων συνδέσμων και σελιδοδεικτών Remove unreferenced named destinations Αφαίρεση ονομασμένων προορισμών χωρίς αναφορές Compress document structure (object streams) Συμπίεση δομής εγγράφου (ροές αντικειμένων) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 73b0ffbf1..bfdb3b10f 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -607,6 +607,7 @@ Discard private data of other applications Descartar datos privados de otras aplicaciones Discard hidden text Descartar texto oculto Use Flate to encode streams that are not encoded Usar Flate para codificar las secuencias sin codificar + In streams that use LZW encoding, use Flate instead En las secuencias con codificación LZW, usar Flate en su lugar Remove invalid links and bookmarks Quitar enlaces y marcadores no válidos Remove unreferenced named destinations Quitar destinos con nombre sin referencias Compress document structure (object streams) Comprimir la estructura del documento (secuencias de objetos) diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index efcf3e3c6..2142dd98a 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -574,6 +574,7 @@ Discard private data of other applications Hylkää muiden sovellusten yksityiset tiedot Discard hidden text Hylkää piilotettu teksti Use Flate to encode streams that are not encoded Koodaa koodaamattomat virrat Flate-menetelmällä + In streams that use LZW encoding, use Flate instead Koodaa LZW-koodatut virrat Flate-menetelmällä Remove invalid links and bookmarks Poista virheelliset linkit ja kirjanmerkit Remove unreferenced named destinations Poista viittaamattomat nimetyt kohteet Compress document structure (object streams) Pakkaa asiakirjan rakenne (objektivirrat) diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index ec6e59e56..82c17ed7d 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -584,6 +584,7 @@ Discard private data of other applications Ignorer les données privées d'autres applications Discard hidden text Ignorer le texte masqué Use Flate to encode streams that are not encoded Utiliser Flate pour coder les flux qui ne sont pas codés + In streams that use LZW encoding, use Flate instead Dans les flux qui utilisent le codage LZW, utiliser Flate à la place Remove invalid links and bookmarks Retirer les liens et signets non valides Remove unreferenced named destinations Retirer les destinations nommées non référencées Compress document structure (object streams) Compresser la structure du document (flux d'objets) diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 7be5ec688..d81157c75 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -558,6 +558,7 @@ Discard private data of other applications अन्य ऐप्लिकेशन का निजी डेटा हटाएँ Discard hidden text छिपा हुआ टेक्स्ट हटाएँ Use Flate to encode streams that are not encoded जो स्ट्रीम एन्कोडेड नहीं हैं उन्हें Flate से एन्कोड करें + In streams that use LZW encoding, use Flate instead LZW एन्कोडिंग वाली स्ट्रीम में इसके बजाय Flate का उपयोग करें Remove invalid links and bookmarks अमान्य लिंक और बुकमार्क हटाएँ Remove unreferenced named destinations बिना संदर्भ वाले नामित गंतव्य हटाएँ Compress document structure (object streams) दस्तावेज़ संरचना संपीड़ित करें (ऑब्जेक्ट स्ट्रीम) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index f1723629c..e8c45743b 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -582,6 +582,7 @@ Discard private data of other applications Más alkalmazások privát adatainak elvetése Discard hidden text Rejtett szöveg elvetése Use Flate to encode streams that are not encoded A kódolatlan adatfolyamok Flate kódolása + In streams that use LZW encoding, use Flate instead Az LZW kódolású adatfolyamok Flate kódolása Remove invalid links and bookmarks Érvénytelen hivatkozások és könyvjelzők eltávolítása Remove unreferenced named destinations Hivatkozás nélküli elnevezett célok eltávolítása Compress document structure (object streams) Dokumentumszerkezet tömörítése (objektumfolyamok) diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 3585b344e..6e730763d 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1219,6 +1219,7 @@ Update Perbarui Use 72 DPI (one point per pixel) Gunakan 72 DPI (satu titik per piksel) Use Flate to encode streams that are not encoded Gunakan Flate untuk mengodekan stream yang belum dikodekan + In streams that use LZW encoding, use Flate instead Pada stream yang memakai pengodean LZW, gunakan Flate sebagai gantinya Use a Digital ID from a file Gunakan ID Digital dari file Use custom DPI Gunakan DPI kustom Use each image's embedded DPI; use 72 DPI when it is absent. Gunakan DPI tertanam pada setiap gambar; gunakan 72 DPI jika tidak ada. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 0dc6bb087..1c941e6c4 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -608,6 +608,7 @@ Discard private data of other applications Elimina dati privati di altre applicazioni Discard hidden text Elimina testo nascosto Use Flate to encode streams that are not encoded Usa Flate per codificare i flussi non codificati + In streams that use LZW encoding, use Flate instead Nei flussi con codifica LZW, usa Flate Remove invalid links and bookmarks Rimuovi collegamenti e segnalibri non validi Remove unreferenced named destinations Rimuovi destinazioni con nome non referenziate Compress document structure (object streams) Comprimi struttura del documento (flussi di oggetti) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 963e8d364..ae6f48b81 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -611,6 +611,7 @@ Discard private data of other applications 他のアプリケーションのプライベートデータを破棄 Discard hidden text 非表示テキストを破棄 Use Flate to encode streams that are not encoded 未エンコードのストリームを Flate でエンコード + In streams that use LZW encoding, use Flate instead LZW エンコードのストリームを Flate でエンコード Remove invalid links and bookmarks 無効なリンクとしおりを削除 Remove unreferenced named destinations 参照されていない名前付き移動先を削除 Compress document structure (object streams) 文書構造を圧縮(オブジェクトストリーム) diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 311626f34..3c3f938b0 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -571,6 +571,7 @@ Discard private data of other applications Басқа қосымшалардың жеке деректерін алып тастау Discard hidden text Жасырын мәтінді алып тастау Use Flate to encode streams that are not encoded Кодталмаған ағындарды Flate көмегімен кодтау + In streams that use LZW encoding, use Flate instead LZW кодталған ағындарда оның орнына Flate қолдану Remove invalid links and bookmarks Жарамсыз сілтемелерді және бетбелгілерді алып тастау Remove unreferenced named destinations Сілтеме берілмеген атаулы межелерді алып тастау Compress document structure (object streams) Құжат құрылымын сығу (нысан ағындары) diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 48ef31e19..e754e5b28 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -558,6 +558,7 @@ Discard private data of other applications 다른 응용 프로그램의 개인 데이터 삭제 Discard hidden text 숨겨진 텍스트 삭제 Use Flate to encode streams that are not encoded 인코딩되지 않은 스트림을 Flate로 인코딩 + In streams that use LZW encoding, use Flate instead LZW 인코딩 스트림을 Flate로 인코딩 Remove invalid links and bookmarks 잘못된 링크 및 책갈피 제거 Remove unreferenced named destinations 참조되지 않은 이름이 지정된 대상 제거 Compress document structure (object streams) 문서 구조 압축(개체 스트림) diff --git a/crates/ui-egui/src/i18n/lo.tsv b/crates/ui-egui/src/i18n/lo.tsv index a8a9f6f64..66e1cbd7a 100644 --- a/crates/ui-egui/src/i18n/lo.tsv +++ b/crates/ui-egui/src/i18n/lo.tsv @@ -579,6 +579,7 @@ Discard private data of other applications ຕັດຂໍ້ມູນສ່ວນຕົວຂອງແອັບພລິເຄຊັນອື່ນອອກ Discard hidden text ຕັດຂໍ້ຄວາມທີ່ເຊື່ອງໄວ້ອອກ Use Flate to encode streams that are not encoded ໃຊ້ Flate ເພື່ອເຂົ້າລະຫັດສະຕຣີມທີ່ຍັງບໍ່ໄດ້ເຂົ້າລະຫັດ + In streams that use LZW encoding, use Flate instead ໃຊ້ Flate ແທນ LZW ໃນສະຕຣີມທີ່ເຂົ້າລະຫັດດ້ວຍ LZW Remove invalid links and bookmarks ລຶບລິ້ງຄ໌ ແລະ ບຸກມາກທີ່ໃຊ້ການບໍ່ໄດ້ Remove unreferenced named destinations ລຶບຈຸດໝາຍປາຍທາງທີ່ມີຊື່ເຊິ່ງບໍ່ມີການອ້າງອີງ Compress document structure (object streams) ບີບອັດໂຄງສ້າງເອກະສານ (ສະຕຣີມວັດຖຸ) diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index e2476f196..88d3ae50f 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -1278,6 +1278,7 @@ Update Atnaujinti Use 72 DPI (one point per pixel) Naudoti 72 DPI (vienas taškas vienam pikseliui) Use Flate to encode streams that are not encoded Nekoduotiems srautams koduoti naudoti „Flate“ + In streams that use LZW encoding, use Flate instead LZW koduotiems srautams naudoti „Flate“ Use a Digital ID from a file Naudoti faile esantį skaitmeninį ID Use custom DPI Naudoti pasirinktinį DPI Use each image's embedded DPI; use 72 DPI when it is absent. Naudoti paveikslėlyje įrašytą DPI; jei jo nėra, naudoti 72 DPI. diff --git a/crates/ui-egui/src/i18n/nb.tsv b/crates/ui-egui/src/i18n/nb.tsv index 27c1383ec..e8d4a02c7 100644 --- a/crates/ui-egui/src/i18n/nb.tsv +++ b/crates/ui-egui/src/i18n/nb.tsv @@ -1228,6 +1228,7 @@ Update Oppdater Use 72 DPI (one point per pixel) Bruk 72 DPI (ett punkt per piksel) Use Flate to encode streams that are not encoded Bruk Flate til å kode strømmer som ikke er kodet + In streams that use LZW encoding, use Flate instead Bruk Flate i stedet for LZW i strømmer som er LZW-kodet Use a Digital ID from a file Bruk en digital ID fra en fil Use custom DPI Bruk egendefinert DPI Use each image's embedded DPI; use 72 DPI when it is absent. Bruk hvert bildes innebygde DPI; bruk 72 DPI når den mangler. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 6836f3e0a..cf5ef5784 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -606,6 +606,7 @@ Discard private data of other applications Usuń prywatne dane innych aplikacji Discard hidden text Usuń ukryty tekst Use Flate to encode streams that are not encoded Zakoduj nieskompresowane strumienie metodą Flate + In streams that use LZW encoding, use Flate instead Zakoduj strumienie LZW metodą Flate Remove invalid links and bookmarks Usuń nieprawidłowe łącza i zakładki Remove unreferenced named destinations Usuń nieużywane nazwane miejsca docelowe Compress document structure (object streams) Kompresuj strukturę dokumentu (strumienie obiektów) diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index dcbebae95..55b1df669 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -238,6 +238,7 @@ Discard private data of other applications Descartar dados privados de outros aplicativos Discard hidden text Descartar texto oculto Use Flate to encode streams that are not encoded Usar Flate para codificar fluxos não codificados + In streams that use LZW encoding, use Flate instead Em fluxos que usam codificação LZW, usar Flate Remove invalid links and bookmarks Remover links e marcadores inválidos Remove unreferenced named destinations Remover destinos nomeados não referenciados Compress document structure (object streams) Compactar estrutura do documento (fluxos de objetos) diff --git a/crates/ui-egui/src/i18n/ro.tsv b/crates/ui-egui/src/i18n/ro.tsv index 2bd680f91..02da8aad8 100644 --- a/crates/ui-egui/src/i18n/ro.tsv +++ b/crates/ui-egui/src/i18n/ro.tsv @@ -1228,6 +1228,7 @@ Update Actualizare Use 72 DPI (one point per pixel) Folosire 72 DPI (un punct pe pixel) Use Flate to encode streams that are not encoded Folosire Flate pentru codarea fluxurilor necodate + In streams that use LZW encoding, use Flate instead Folosire Flate în locul LZW pentru fluxurile codate LZW Use a Digital ID from a file Folosire ID digital dintr-un fișier Use custom DPI Folosire DPI personalizat Use each image's embedded DPI; use 72 DPI when it is absent. Folosește DPI-ul încorporat al fiecărei imagini; 72 DPI când lipsește. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 0dc40fdba..6d0daae24 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -604,6 +604,7 @@ Discard private data of other applications Удалить личные данные других приложений Discard hidden text Удалить скрытый текст Use Flate to encode streams that are not encoded Использовать Flate для кодирования некодированных потоков + In streams that use LZW encoding, use Flate instead Использовать Flate вместо LZW в потоках с кодированием LZW Remove invalid links and bookmarks Удалить недействительные ссылки и закладки Remove unreferenced named destinations Удалить неиспользуемые именованные места назначения Compress document structure (object streams) Сжать структуру документа (потоки объектов) diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index f5e374ae7..6df01e98c 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -1964,6 +1964,7 @@ Update watermark… Posodobi vodni žig… Use 72 DPI (one point per pixel) Uporabi 72 DPI (ena točka na slikovno piko) Use Flate to encode streams that are not encoded Za kodiranje nekodiranih tokov uporabi Flate + In streams that use LZW encoding, use Flate instead V tokovih s kodiranjem LZW uporabi Flate Use JavaScript Uporabi JavaScript Use a Digital ID from a file Uporabi digitalno identiteto iz datoteke Use a certificate Uporabi potrdilo diff --git a/crates/ui-egui/src/i18n/sq.tsv b/crates/ui-egui/src/i18n/sq.tsv index c93efee5a..896264c96 100644 --- a/crates/ui-egui/src/i18n/sq.tsv +++ b/crates/ui-egui/src/i18n/sq.tsv @@ -1910,6 +1910,7 @@ Update watermark… Përditëso filigranin… Use 72 DPI (one point per pixel) Përdor 72 DPI (një pikë për piksel) Use Flate to encode streams that are not encoded Përdor Flate për të koduar rrjedhat që nuk janë të koduara + In streams that use LZW encoding, use Flate instead Në rrjedhat që përdorin kodimin LZW, përdor Flate në vend të tij Use JavaScript Përdor JavaScript Use a Digital ID from a file Përdor një ID dixhitale nga një skedar Use a certificate Përdor një certifikatë diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index fa3f6cec4..b48c9b52c 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -588,6 +588,7 @@ comment menu Edit సవరించు Discard private data of other applications ఇతర అప్లికేషన్‌ల ప్రైవేట్ డేటాను విస్మరించు Discard hidden text దాచిన టెక్స్ట్‌ను విస్మరించు Use Flate to encode streams that are not encoded ఎన్‌కోడ్ చేయని స్ట్రీమ్‌లను Flateతో ఎన్‌కోడ్ చేయి + In streams that use LZW encoding, use Flate instead LZW ఎన్‌కోడింగ్ ఉన్న స్ట్రీమ్‌లలో బదులుగా Flate ఉపయోగించు Remove invalid links and bookmarks చెల్లని లింక్‌లు, బుక్‌మార్క్‌లను తీసివేయి Remove unreferenced named destinations ఎక్కడా సూచించని పేరున్న గమ్యాలను తీసివేయి Compress document structure (object streams) పత్రం నిర్మాణాన్ని కుదించు (ఆబ్జెక్ట్ స్ట్రీమ్‌లు) diff --git a/crates/ui-egui/src/i18n/th.tsv b/crates/ui-egui/src/i18n/th.tsv index e56770757..a61546507 100644 --- a/crates/ui-egui/src/i18n/th.tsv +++ b/crates/ui-egui/src/i18n/th.tsv @@ -568,6 +568,7 @@ Discard private data of other applications ละทิ้งข้อมูลเฉพาะของแอปพลิเคชันอื่น Discard hidden text ละทิ้งข้อความที่ซ่อนอยู่ Use Flate to encode streams that are not encoded ใช้ Flate เข้ารหัสสตรีมที่ยังไม่ได้เข้ารหัส + In streams that use LZW encoding, use Flate instead ใช้ Flate แทนในสตรีมที่เข้ารหัสแบบ LZW Remove invalid links and bookmarks เอาลิงก์และบุ๊กมาร์กที่ใช้ไม่ได้ออก Remove unreferenced named destinations เอาปลายทางที่มีชื่อซึ่งไม่ถูกอ้างถึงออก Compress document structure (object streams) บีบอัดโครงสร้างเอกสาร (object streams) diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index c292b8863..8be0c67aa 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -569,6 +569,7 @@ Discard private data of other applications Diğer uygulamaların özel verilerini at Discard hidden text Gizli metni at Use Flate to encode streams that are not encoded Kodlanmamış akışları kodlamak için Flate kullan + In streams that use LZW encoding, use Flate instead LZW kodlamalı akışlarda bunun yerine Flate kullan Remove invalid links and bookmarks Geçersiz bağlantıları ve yer işaretlerini kaldır Remove unreferenced named destinations Referans verilmeyen adlandırılmış hedefleri kaldır Compress document structure (object streams) Belge yapısını sıkıştır (nesne akışları) diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index cc8f45765..f7cd48a1c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -609,6 +609,7 @@ Discard private data of other applications Видалити приватні дані інших застосунків Discard hidden text Видалити прихований текст Use Flate to encode streams that are not encoded Кодувати незакодовані потоки за допомогою Flate + In streams that use LZW encoding, use Flate instead Використовувати Flate замість LZW у потоках із кодуванням LZW Remove invalid links and bookmarks Видалити недійсні посилання й закладки Remove unreferenced named destinations Видалити іменовані призначення без посилань Compress document structure (object streams) Стиснути структуру документа (потоки об’єктів) diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 87abe5129..f9f8026ea 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1268,6 +1268,7 @@ theme Appearance Giao diện Update Cập nhật Use 72 DPI (one point per pixel) Dùng 72 DPI (một điểm cho mỗi điểm ảnh) Use Flate to encode streams that are not encoded Dùng Flate để mã hóa các luồng chưa được mã hóa + In streams that use LZW encoding, use Flate instead Dùng Flate thay cho LZW trong các luồng mã hóa LZW Use a Digital ID from a file Dùng Digital ID từ một tệp Use custom DPI Dùng DPI tùy chỉnh Use each image's embedded DPI; use 72 DPI when it is absent. Dùng DPI nhúng của từng hình ảnh; dùng 72 DPI khi không có. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 594f68df8..ce1f6fc55 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -288,6 +288,7 @@ Discard private data of other applications 丢弃其他应用程序的私有数据 Discard hidden text 丢弃隐藏文本 Use Flate to encode streams that are not encoded 使用 Flate 编码未编码的流 + In streams that use LZW encoding, use Flate instead 在使用 LZW 编码的流中改用 Flate Remove invalid links and bookmarks 删除无效链接和书签 Remove unreferenced named destinations 删除未引用的命名目标 Compress document structure (object streams) 压缩文档结构(对象流) diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index cb578ba26..6828fb7c2 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -244,6 +244,7 @@ Discard private data of other applications 捨棄其他應用程式的私有資料 Discard hidden text 捨棄隱藏文字 Use Flate to encode streams that are not encoded 使用 Flate 編碼未編碼的流 + In streams that use LZW encoding, use Flate instead 在使用 LZW 編碼的流中改用 Flate Remove invalid links and bookmarks 刪除無效連結和書籤 Remove unreferenced named destinations 刪除未引用的命名目標 Compress document structure (object streams) 壓縮文件結構(物件流) diff --git a/crates/ui-egui/src/optimize_ui.rs b/crates/ui-egui/src/optimize_ui.rs index 9660b0a92..6662bf997 100644 --- a/crates/ui-egui/src/optimize_ui.rs +++ b/crates/ui-egui/src/optimize_ui.rs @@ -230,6 +230,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo } OptimizeTab::CleanUp => { ui.add(widgets::checkbox(&mut s.flate_unencoded, tl!("Use Flate to encode streams that are not encoded"))); + ui.add(widgets::checkbox(&mut s.lzw_to_flate, tl!("In streams that use LZW encoding, use Flate instead"))); ui.add(widgets::checkbox(&mut s.remove_invalid_links, tl!("Remove invalid links and bookmarks"))); ui.add(widgets::checkbox(&mut s.remove_unreferenced_dests, tl!("Remove unreferenced named destinations"))); let mut object_streams = s.effective_object_streams(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 62fc5d84b..320c0963d 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -7589,8 +7589,8 @@ milestone = "M11" status = "partial" commands = ["optimize.advanced"] tools = ["doc_optimize"] -evidence = ["crates/optimize/tests/optimize.rs::discards_and_clean_up"] -notes = "unencoded streams; LZW not yet re-encoded" +evidence = ["crates/optimize/tests/optimize.rs::discards_and_clean_up", "crates/optimize/tests/optimize.rs::lzw_streams_become_flate_and_decode_to_the_same_bytes", "crates/optimize/tests/optimize.rs::a_page_drawn_from_lzw_streams_renders_the_same_after_conversion"] +notes = "Two Clean Up options, as in Acrobat: unencoded streams are Flate-compressed, and LZW streams (content and image XObjects; EarlyChange 0 and 1, with or without a predictor, LZW anywhere in a filter chain) are decoded and stored with Flate instead, keeping the predictor and the filters after the LZW one; LZW data that doesn't decode cleanly is left as it is. Not yet: LZW inline images inside content streams." [[feature]] id = "optimize.remove-invalid-links"