mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
M2.6: an edit never rewrites a content stream from the part before its damage
A Flate stream damaged partway decodes to what came before the damage, and `DecodeBudget::decode` gave that back as the stream's data: the decoder's partial flag was dropped in `Stream::decoded_within_counted`. Editing a text line, a block, an image or an added item in such a stream wrote it again from that part, so everything after the damage was lost on save, though other readers may still read it. - `DecodeBudget::decode_with_damage` decodes as `decode_within` does and says whether the stream is damaged partway. A missing checksum or end-of-data marker is no damage, as before. - Edit reads the page's streams with it (`ContentStream`), and `rewritten_contents` refuses, before adding anything, a change to a damaged stream. Changes in the page's other streams go on, and the damaged one stays as stored. - Selecting objects (`editable_objects`, moving added items) refuses a page with a damaged stream, as it refuses one that doesn't decode: what follows the damage could change every later item's graphics state. - Marking a tag as an artifact or a figure as decorative refuses when the tag's content is in a damaged stream (`ArtifactError::Damaged`, `AltError::Damaged`), and changes nothing, the structure tree included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
f42fda7342
commit
712345d7a1
13 files changed
+308
-112
No files matched your search
+47
-18
@@ -25,6 +25,8 @@ pub enum AltError {
|
||||
NotAFigure(u32),
|
||||
#[error("the page has more content than can be edited")]
|
||||
TooLarge,
|
||||
#[error("the figure's content is in a content stream damaged partway, which can't be rewritten without losing what follows the damage")]
|
||||
Damaged,
|
||||
#[error("{0}")]
|
||||
Cos(#[from] pdfcraft_cos::CosError),
|
||||
}
|
||||
@@ -52,6 +54,8 @@ pub enum ArtifactError {
|
||||
ParentTree(String),
|
||||
#[error("the page has more content than can be edited")]
|
||||
TooLarge,
|
||||
#[error("the tag's marked content is in a content stream damaged partway, which can't be rewritten without losing what follows the damage")]
|
||||
Damaged,
|
||||
#[error(transparent)]
|
||||
Cos(#[from] pdfcraft_cos::CosError),
|
||||
}
|
||||
@@ -372,7 +376,9 @@ fn content_entries(doc: &Document, page: &Dict) -> Vec<Object> {
|
||||
}
|
||||
}
|
||||
|
||||
type DecodedStream = (Object, Option<(Stream, Vec<u8>)>);
|
||||
/// A `/Contents` entry and, for a stream, the stream, its decoded data and whether it is damaged
|
||||
/// partway (the data is then what decoded before the damage).
|
||||
type DecodedStream = (Object, Option<(Stream, Vec<u8>, bool)>);
|
||||
|
||||
/// Decode the page's content streams within the page decode budget. `None` when a stream is
|
||||
/// rejected because the budget is exhausted.
|
||||
@@ -383,14 +389,14 @@ fn decode_page_streams(doc: &Document, page: &Dict) -> Option<Vec<DecodedStream>
|
||||
for entry in entries {
|
||||
let decoded = match &*doc.resolve(&entry) {
|
||||
Object::Stream(s) => {
|
||||
let data = match budget.decode(s) {
|
||||
Ok(data) => data,
|
||||
let (data, damaged) = match budget.decode_with_damage(s, usize::MAX) {
|
||||
Ok(read) => read,
|
||||
// Content past the budget may hold more of the tag: editing the rest
|
||||
// would leave that part marked as a tag the tree no longer has.
|
||||
Err(_) if budget.exhausted() => return None,
|
||||
Err(_) => Vec::new(),
|
||||
Err(_) => (Vec::new(), false),
|
||||
};
|
||||
Some((s.clone(), data))
|
||||
Some((s.clone(), data, damaged))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -403,6 +409,18 @@ fn marked_id(doc: &Document, props: &Dict, op: &pdfcraft_content::Op, ids: &Hash
|
||||
(op.op == b"BDC").then(|| bdc_mcid(doc, props, op)).flatten().filter(|n| ids.contains(n))
|
||||
}
|
||||
|
||||
/// What [`artifact_marked_content`] did.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Marked {
|
||||
/// The streams holding the content were rewritten.
|
||||
Done,
|
||||
/// None of the content is on the page, or with `require_all` not all of it: nothing changed.
|
||||
NotFound,
|
||||
/// Some of it is in a stream damaged partway, which can't be rewritten without losing what
|
||||
/// follows the damage: nothing changed.
|
||||
Damaged,
|
||||
}
|
||||
|
||||
/// Turn the tag's marked-content blocks into artifacts, rewriting only the streams that
|
||||
/// contain them. Other content streams, and unknown entries on a rewritten stream, stay.
|
||||
/// With `require_all`, every id in `ids` must appear on the page before any stream is rewritten.
|
||||
@@ -412,9 +430,9 @@ fn artifact_marked_content(
|
||||
streams: Vec<DecodedStream>,
|
||||
ids: &HashSet<i64>,
|
||||
require_all: bool,
|
||||
) -> Result<bool, pdfcraft_cos::CosError> {
|
||||
) -> Result<Marked, pdfcraft_cos::CosError> {
|
||||
let props = page_properties(doc, &page.dict);
|
||||
let decoded: Vec<&[u8]> = streams.iter().filter_map(|(_, s)| s.as_ref().map(|(_, bytes)| bytes.as_slice())).collect();
|
||||
let decoded: Vec<&[u8]> = streams.iter().filter_map(|(_, s)| s.as_ref().map(|(_, bytes, _)| bytes.as_slice())).collect();
|
||||
let pieces = pdfcraft_content::Pieces::join(&decoded);
|
||||
let mut found = HashSet::new();
|
||||
let mut edits = Vec::new();
|
||||
@@ -425,12 +443,17 @@ fn artifact_marked_content(
|
||||
}
|
||||
}
|
||||
if if require_all { found != *ids } else { found.is_empty() } {
|
||||
return Ok(false);
|
||||
return Ok(Marked::NotFound);
|
||||
}
|
||||
let mut spliced = pieces.splice(edits).into_iter();
|
||||
let spliced = pieces.splice(edits);
|
||||
// The pieces are the streams that decoded, in order.
|
||||
if streams.iter().filter_map(|(_, s)| s.as_ref()).zip(&spliced).any(|((_, old, damaged), new)| *damaged && old != new) {
|
||||
return Ok(Marked::Damaged);
|
||||
}
|
||||
let mut spliced = spliced.into_iter();
|
||||
let mut next = Vec::with_capacity(streams.len());
|
||||
for (entry, stream) in streams {
|
||||
let Some((stream, old)) = stream else {
|
||||
let Some((stream, old, _)) = stream else {
|
||||
next.push(entry);
|
||||
continue;
|
||||
};
|
||||
@@ -456,7 +479,7 @@ fn artifact_marked_content(
|
||||
_ => Object::Array(next),
|
||||
};
|
||||
doc.update_dict(page.obj, |p| p.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(true)
|
||||
Ok(Marked::Done)
|
||||
}
|
||||
|
||||
/// Decorative figure: its marked content on the page becomes an artifact, and the element
|
||||
@@ -468,11 +491,15 @@ pub fn mark_decorative(doc: &mut Document, obj: ObjRef) -> Result<(), AltError>
|
||||
let pg = d.get(b"Pg").and_then(Object::as_ref).or_else(|| elements(doc).into_iter().find(|e| e.object == obj).and_then(|e| e.page));
|
||||
if let Some(page) = pg.and_then(|p| pages.iter().find(|x| x.obj == p)).filter(|_| !ids.is_empty()) {
|
||||
let streams = decode_page_streams(doc, &page.dict).ok_or(AltError::TooLarge)?;
|
||||
// The parent tree no longer points those ids at the figure.
|
||||
if artifact_marked_content(doc, page, streams, &ids, false)?
|
||||
&& let Some(key) = page.dict.get(b"StructParents").and_then(Object::as_int)
|
||||
{
|
||||
clear_parent_tree(doc, key, obj)?;
|
||||
match artifact_marked_content(doc, page, streams, &ids, false)? {
|
||||
// The parent tree no longer points those ids at the figure.
|
||||
Marked::Done => {
|
||||
if let Some(key) = page.dict.get(b"StructParents").and_then(Object::as_int) {
|
||||
clear_parent_tree(doc, key, obj)?;
|
||||
}
|
||||
}
|
||||
Marked::NotFound => {}
|
||||
Marked::Damaged => return Err(AltError::Damaged),
|
||||
}
|
||||
}
|
||||
// Out of its parent's kids.
|
||||
@@ -513,8 +540,10 @@ pub fn mark_tag_artifact(doc: &mut Document, obj: ObjRef) -> Result<(), Artifact
|
||||
let parent = element.dict.get(b"P").and_then(Object::as_ref).ok_or(ArtifactError::NoParent)?;
|
||||
let replacement = replacement_kids(doc, parent, obj)?;
|
||||
let streams = decode_page_streams(doc, &page.dict).ok_or(ArtifactError::TooLarge)?;
|
||||
if !artifact_marked_content(doc, page, streams, &ids, true)? {
|
||||
return Err(ArtifactError::ContentNotFound);
|
||||
match artifact_marked_content(doc, page, streams, &ids, true)? {
|
||||
Marked::Done => {}
|
||||
Marked::NotFound => return Err(ArtifactError::ContentNotFound),
|
||||
Marked::Damaged => return Err(ArtifactError::Damaged),
|
||||
}
|
||||
if let Some(key) = page.dict.get(b"StructParents").and_then(Object::as_int) {
|
||||
clear_parent_tree(doc, key, obj).map_err(|e| ArtifactError::ParentTree(e.to_string()))?;
|
||||
|
||||
@@ -1087,6 +1087,48 @@ fn mark_tag_artifact_validates_parent_and_all_mcids_before_mutating_document() {
|
||||
assert_eq!(*absent.get(ObjRef::new(9, 0)), before_pt);
|
||||
}
|
||||
|
||||
/// zlib data that reads as `readable` and is then damaged, with more of the stream after the
|
||||
/// damage: a stored block, then a block of the reserved type (RFC 1951 §3.2.3), then `rest`.
|
||||
fn damaged_flate(readable: &[u8], rest: &[u8]) -> pdfcraft_cos::Stream {
|
||||
let len = u16::try_from(readable.len()).unwrap();
|
||||
let mut raw = vec![0x78, 0x01, 0x00];
|
||||
raw.extend_from_slice(&len.to_le_bytes());
|
||||
raw.extend_from_slice(&(!len).to_le_bytes());
|
||||
raw.extend_from_slice(readable);
|
||||
raw.push(0x07);
|
||||
raw.extend_from_slice(rest);
|
||||
let mut dict = pdfcraft_cos::Dict::new();
|
||||
dict.set(b"Filter".to_vec(), pdfcraft_cos::Object::name("FlateDecode"));
|
||||
pdfcraft_cos::Stream::from_raw(dict, raw)
|
||||
}
|
||||
|
||||
/// A tag's content in a stream damaged partway stays tagged: rewriting the stream from what
|
||||
/// decoded would lose what follows the damage. Nothing changes, the structure tree included.
|
||||
/// Content in a whole stream beside a damaged one is marked, and the damaged one kept as stored.
|
||||
#[test]
|
||||
fn a_tags_content_in_a_stream_damaged_partway_is_not_rewritten() {
|
||||
use pdfcraft_cos::{ObjRef, Object};
|
||||
let content = b"/Figure << /MCID 0 >> BDC q 40 0 0 20 10 150 cm /Im Do Q EMC /Photo << /MCID 1 >> BDC 100 100 50 30 re f EMC\n";
|
||||
let damaged = damaged_flate(content, b"0 0 1 rg 0 0 10 10 re f");
|
||||
let mut doc = figures_doc();
|
||||
doc.set(ObjRef::new(4, 0), damaged.clone());
|
||||
let contents = |doc: &Document| doc.get(ObjRef::new(3, 0)).as_dict().unwrap().get(b"Contents").cloned();
|
||||
let (before_contents, before_pt, before_kids) = (contents(&doc), doc.get(ObjRef::new(9, 0)), doc.get(ObjRef::new(6, 0)));
|
||||
assert_eq!(figures(&doc).len(), 2, "read up to the damage");
|
||||
assert_eq!(mark_tag_artifact(&mut doc, ObjRef::new(7, 0)), Err(ArtifactError::Damaged));
|
||||
assert_eq!(mark_decorative(&mut doc, ObjRef::new(7, 0)), Err(AltError::Damaged));
|
||||
assert_eq!((contents(&doc), doc.get(ObjRef::new(9, 0)), doc.get(ObjRef::new(6, 0))), (before_contents, before_pt, before_kids));
|
||||
|
||||
let mut doc = figures_doc();
|
||||
let other = doc.add(damaged_flate(b"0 g\n", b"0 0 1 rg 0 0 10 10 re f"));
|
||||
doc.update_dict(ObjRef::new(3, 0), |p| p.set(b"Contents".to_vec(), Object::Array(vec![Object::Ref(other), Object::Ref(ObjRef::new(4, 0))])))
|
||||
.unwrap();
|
||||
mark_tag_artifact(&mut doc, ObjRef::new(7, 0)).unwrap();
|
||||
let Some(Object::Array(list)) = contents(&doc) else { panic!("contents {:?}", contents(&doc)) };
|
||||
assert_eq!(list.first(), Some(&Object::Ref(other)), "kept as stored");
|
||||
assert_ne!(list.get(1), Some(&Object::Ref(ObjRef::new(4, 0))), "the tag's stream is rewritten");
|
||||
}
|
||||
|
||||
fn ref_(n: u32) -> Option<pdfcraft_cos::ObjRef> {
|
||||
Some(pdfcraft_cos::ObjRef::new(n, 0))
|
||||
}
|
||||
|
||||
+52
-11
@@ -38,9 +38,9 @@ const JPEG_WORK: u64 = 2;
|
||||
/// and images can share one, so reading it again for each has to count.
|
||||
const JPEG_READ_WORK: u64 = 4;
|
||||
|
||||
/// A stream decode within a byte bound, with the bytes its filter stages decoded: the data or
|
||||
/// the error, each with that count.
|
||||
type Decoder = fn(&Stream, usize) -> Result<(Vec<u8>, usize), (FilterError, usize)>;
|
||||
/// A stream decode within a byte bound, with the bytes its filter stages decoded: the data and
|
||||
/// whether it stopped at damage, or the error, each with that count.
|
||||
type Decoder = fn(&Stream, usize) -> Result<(Vec<u8>, bool, usize), (FilterError, usize)>;
|
||||
|
||||
/// What one operation over a document's images may still spend decoding them: exporting them,
|
||||
/// optimizing them, and storing LZW streams with Flate as it does, clearing them for redaction.
|
||||
@@ -263,45 +263,56 @@ impl DecodeBudget {
|
||||
}
|
||||
|
||||
/// `stream` decoded like [`Stream::decoded`], charged to the allowance. Fails, without
|
||||
/// decoding, once the allowance is spent.
|
||||
/// decoding, once the allowance is spent. A stream damaged partway gives what decoded before
|
||||
/// the damage, so code that writes the stream back reads it with
|
||||
/// [`DecodeBudget::decode_with_damage`] instead.
|
||||
pub fn decode(&mut self, stream: &Stream) -> Result<Vec<u8>, CosError> {
|
||||
self.charge(stream, usize::MAX, Stream::decoded_within_counted)
|
||||
self.charge(stream, usize::MAX, Stream::decoded_within_counted).map(|(data, _)| data)
|
||||
}
|
||||
|
||||
/// Like [`DecodeBudget::decode`], for a stream with a tighter bound of its own: decoding
|
||||
/// past `max` bytes is an error that charges `max`, the work it took to find out.
|
||||
pub fn decode_within(&mut self, stream: &Stream, max: usize) -> Result<Vec<u8>, CosError> {
|
||||
self.charge(stream, max, Stream::decoded_within_counted).map(|(data, _)| data)
|
||||
}
|
||||
|
||||
/// [`DecodeBudget::decode_within`] (`usize::MAX` for no bound of its own), also saying
|
||||
/// whether the stream is damaged: `true` when a filter stopped at corrupt data, so the bytes
|
||||
/// are only what decoded before it. A stream written back from them loses everything after
|
||||
/// the damage, which other readers may still read.
|
||||
pub fn decode_with_damage(&mut self, stream: &Stream, max: usize) -> Result<(Vec<u8>, bool), CosError> {
|
||||
self.charge(stream, max, Stream::decoded_within_counted)
|
||||
}
|
||||
|
||||
/// `stream` decoded like [`Stream::decoded_strict`] (any corruption is an error), charged to
|
||||
/// the allowance.
|
||||
pub fn decode_strict(&mut self, stream: &Stream) -> Result<Vec<u8>, CosError> {
|
||||
self.charge(stream, usize::MAX, Stream::decoded_strict_within_counted)
|
||||
let strict: Decoder = |s, max| s.decoded_strict_within_counted(max).map(|(data, spent)| (data, false, spent));
|
||||
self.charge(stream, usize::MAX, strict).map(|(data, _)| data)
|
||||
}
|
||||
|
||||
/// Decodes `stream` within `max` and the allowance, charging what every filter stage
|
||||
/// decoded: a stage's output counts even when a later stage shrinks it, and a corrupt stream
|
||||
/// pays for the bytes decoded before the error, so referencing a stream again and again
|
||||
/// spends the allowance by the work each decode does.
|
||||
fn charge(&mut self, stream: &Stream, max: usize, decode: Decoder) -> Result<Vec<u8>, CosError> {
|
||||
fn charge(&mut self, stream: &Stream, max: usize, decode: Decoder) -> Result<(Vec<u8>, bool), CosError> {
|
||||
if self.bytes == 0 && !stream.raw.is_empty() {
|
||||
return Err(CosError::Filter(FilterError::LimitExceeded(self.limit).to_string()));
|
||||
}
|
||||
if max < self.bytes {
|
||||
let result = decode(stream, max);
|
||||
let spent = match &result {
|
||||
Ok((_, spent)) => *spent,
|
||||
Ok((_, _, spent)) => *spent,
|
||||
Err((FilterError::LimitExceeded(_), _)) => max,
|
||||
Err((_, spent)) => *spent,
|
||||
};
|
||||
self.bytes = self.bytes.saturating_sub(spent);
|
||||
return result.map(|(data, _)| data).map_err(|(e, _)| CosError::Filter(e.to_string()));
|
||||
return result.map(|(data, damaged, _)| (data, damaged)).map_err(|(e, _)| CosError::Filter(e.to_string()));
|
||||
}
|
||||
match decode(stream, self.bytes) {
|
||||
Ok((data, spent)) => {
|
||||
Ok((data, damaged, spent)) => {
|
||||
self.bytes = self.bytes.saturating_sub(spent);
|
||||
Ok(data)
|
||||
Ok((data, damaged))
|
||||
}
|
||||
Err((FilterError::LimitExceeded(_), _)) => {
|
||||
self.bytes = 0;
|
||||
@@ -801,6 +812,36 @@ mod tests {
|
||||
assert_eq!(budget.remaining(), (10 << 20) - partial);
|
||||
}
|
||||
|
||||
/// A stream damaged partway decodes to what came before the damage, and says so, so that code
|
||||
/// writing it back can leave it as it is. What every reader takes is no damage: a missing
|
||||
/// checksum, no filter at all.
|
||||
#[test]
|
||||
fn a_stream_damaged_partway_says_so() {
|
||||
let read = |budget: &mut DecodeBudget, s: &Stream, max| budget.decode_with_damage(s, max).map(|(d, damaged)| (d.len(), damaged)).ok();
|
||||
let whole = Stream::flate(Dict::new(), &vec![b' '; 1 << 20]);
|
||||
let cut = Stream::from_raw(whole.dict.clone(), whole.raw.get(..whole.raw.len() / 2).unwrap_or_default().to_vec());
|
||||
let partial = cut.decoded().map(|d| d.len()).unwrap_or(0);
|
||||
assert!(partial > 0, "a cut flate stream decodes in part");
|
||||
let mut budget = DecodeBudget::new(10 << 20, 1);
|
||||
assert_eq!(read(&mut budget, &cut, usize::MAX), Some((partial, true)));
|
||||
assert_eq!(budget.remaining(), (10 << 20) - partial);
|
||||
// Within a bound of its own, under what is left.
|
||||
assert_eq!(read(&mut budget, &cut, 4 << 20), Some((partial, true)));
|
||||
assert_eq!(budget.decode(&cut).map(|d| d.len()).ok(), Some(partial));
|
||||
|
||||
let mut budget = DecodeBudget::new(10 << 20, 1);
|
||||
assert_eq!(read(&mut budget, &whole, usize::MAX), Some((1 << 20, false)));
|
||||
assert_eq!(read(&mut budget, &whole, 4 << 20), Some((1 << 20, false)));
|
||||
let unchecked = Stream::from_raw(whole.dict.clone(), whole.raw.get(..whole.raw.len() - 4).unwrap_or_default().to_vec());
|
||||
assert_eq!(read(&mut budget, &unchecked, usize::MAX), Some((1 << 20, false)));
|
||||
assert_eq!(read(&mut budget, &Stream::from_raw(Dict::new(), b"0 0 m".to_vec()), usize::MAX), Some((5, false)));
|
||||
// A later stage's damage is the stream's.
|
||||
let mut dict = Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::Array(vec![Object::name("FlateDecode"), Object::name("ASCIIHexDecode")]));
|
||||
let hex = Stream::from_raw(dict, Stream::flate(Dict::new(), b"4142zz4344>").raw.to_vec());
|
||||
assert_eq!(budget.decode_with_damage(&hex, usize::MAX).ok(), Some((b"AB".to_vec(), true)));
|
||||
}
|
||||
|
||||
/// A stream that fails its reading as zlib, and then as raw deflate after decoding all of it,
|
||||
/// gives nothing back, and each reference still pays for what it decoded.
|
||||
#[test]
|
||||
|
||||
@@ -297,13 +297,18 @@ impl Stream {
|
||||
|
||||
/// [`Stream::decoded_within`] for a caller charging an allowance: the filter error is kept
|
||||
/// typed, so a caller can tell a spent limit from corrupt data, `max` bounds all filter
|
||||
/// stages together, and the result gives the bytes they decoded (the work done, on success
|
||||
/// and on failure).
|
||||
pub(crate) fn decoded_within_counted(&self, max: usize) -> Result<(Vec<u8>, usize), (pdfcraft_filters::FilterError, usize)> {
|
||||
/// stages together, and the result gives whether the data stopped at damage (it is then what
|
||||
/// decoded before it) and the bytes the stages decoded (the work done, on success and on
|
||||
/// failure).
|
||||
pub(crate) fn decoded_within_counted(&self, max: usize) -> Result<(Vec<u8>, bool, usize), (pdfcraft_filters::FilterError, usize)> {
|
||||
let mut damaged = false;
|
||||
let decode = |chain: &[(Filter, Params)], data: &[u8], max| {
|
||||
pdfcraft_filters::decode_tolerant_counted(chain, data, max).map(|(v, _, spent)| (v, spent))
|
||||
pdfcraft_filters::decode_tolerant_counted(chain, data, max).map(|(v, partial, spent)| {
|
||||
damaged = partial;
|
||||
(v, spent)
|
||||
})
|
||||
};
|
||||
self.decode_chain(max, decode)
|
||||
self.decode_chain(max, decode).map(|(v, spent)| (v, damaged, spent))
|
||||
}
|
||||
|
||||
/// [`Stream::decoded_within`] spending from `work`, what an operation may still do across a
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! A page's background colour: the colour the page is painted with before anything else, as
|
||||
//! Edit a PDF ▸ Background and many producers do it, for exports that keep a page colour (#2992).
|
||||
|
||||
use pdfcraft_content::{Matrix, Op, Pieces};
|
||||
use pdfcraft_content::{Matrix, Op};
|
||||
use pdfcraft_cos::{Dict, Document};
|
||||
|
||||
use crate::{EditError, Rgb};
|
||||
@@ -51,7 +51,7 @@ pub fn page_background(doc: &Document, page: usize) -> Result<Option<Rgb>, EditE
|
||||
let resources = p.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
let gstates = resources.get(b"ExtGState").map(|g| doc.resolve(g)).and_then(|g| g.as_dict().cloned()).unwrap_or_default();
|
||||
let streams = crate::text::content_streams(doc, &p.dict)?;
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let pieces = crate::text::joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
|
||||
let mut st = State { ctm: Matrix::IDENTITY, space: Space::Gray, fill: Some([0.0; 3]), alpha: 1.0, plain: true, clip: Some(Vec::new()) };
|
||||
|
||||
+17
-28
@@ -4,12 +4,12 @@
|
||||
//! or delete. Only the stream that draws it is rewritten, as a new object.
|
||||
|
||||
use kurbo::{BezPath, Point, Shape};
|
||||
use pdfcraft_content::{Matrix, Op, Pieces, parse, serialize_ops};
|
||||
use pdfcraft_content::{Matrix, Op, parse, serialize_ops};
|
||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::EditError;
|
||||
use crate::text::content_streams;
|
||||
use crate::text::{ContentStream, content_streams, joined};
|
||||
|
||||
/// The identity of a soft mask (`/SMask`) in the PDF graphics state.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -47,7 +47,7 @@ pub struct PageImage {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// The content stream its `Do` (its first token) is in, and the `Do`'s index among the
|
||||
/// operators of the page's streams joined ([`Pieces`]).
|
||||
/// operators of the page's streams joined ([`pdfcraft_content::Pieces`]).
|
||||
pub stream: usize,
|
||||
op: usize,
|
||||
/// CTM before `Do`, excluding a Form's own Matrix.
|
||||
@@ -103,11 +103,6 @@ fn too_many_images() -> EditError {
|
||||
EditError::Invalid(format!("the page draws more than {MAX_PAGE_IMAGES} images"))
|
||||
}
|
||||
|
||||
/// The page's streams joined into the one stream they are: an operator may span two of them.
|
||||
fn joined(streams: &[(Object, Vec<u8>)]) -> Pieces {
|
||||
Pieces::join(&streams.iter().map(|(_, data)| data.as_slice()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
fn page_of(doc: &Document, page: usize) -> Result<pdfcraft_model::Page, EditError> {
|
||||
pdfcraft_model::pages(doc).into_iter().nth(page).ok_or(EditError::NoSuchPage(page))
|
||||
}
|
||||
@@ -1191,13 +1186,7 @@ fn build_relocated_unit_ops(
|
||||
}
|
||||
|
||||
/// Change the stacking order of `img`, drawn on page `p` by its content streams `all`.
|
||||
fn arrange_image(
|
||||
doc: &mut Document,
|
||||
p: &pdfcraft_model::Page,
|
||||
all: &[(Object, Vec<u8>)],
|
||||
img: &PageImage,
|
||||
order: ArrangeOrder,
|
||||
) -> Result<(), EditError> {
|
||||
fn arrange_image(doc: &mut Document, p: &pdfcraft_model::Page, all: &[ContentStream], img: &PageImage, order: ArrangeOrder) -> Result<(), EditError> {
|
||||
let pieces = joined(all);
|
||||
let ops = pieces.parse().ops;
|
||||
let target = img.op;
|
||||
@@ -1264,7 +1253,7 @@ fn arrange_image(
|
||||
ops.get(dest_unit.start).map(|op| pieces.piece_of(op.span.start))
|
||||
};
|
||||
let owned_destination = destination_piece
|
||||
.filter(|piece| all.get(*piece).is_some_and(|(obj, _)| doc.resolve(obj).as_dict().is_some_and(|d| d.get(b"PCAdded").is_some())));
|
||||
.filter(|piece| all.get(*piece).is_some_and(|s| doc.resolve(&s.entry).as_dict().is_some_and(|d| d.get(b"PCAdded").is_some())));
|
||||
if let Some(piece) = owned_destination {
|
||||
let whole_unit = ops.iter().enumerate().all(|(index, op)| {
|
||||
let (first, last) = pieces.pieces_of(op);
|
||||
@@ -1440,7 +1429,7 @@ fn arrange_image(
|
||||
|
||||
if let Some(piece) = owned_destination {
|
||||
let changed = pieces.splice([(target_span, removal_replacement)]);
|
||||
let contents = crate::text::rewritten_contents(doc, all, changed);
|
||||
let contents = crate::text::rewritten_contents(doc, all, changed)?;
|
||||
let mut list = match contents {
|
||||
Object::Array(list) => list,
|
||||
one => vec![one],
|
||||
@@ -1467,7 +1456,7 @@ fn arrange_image(
|
||||
}
|
||||
|
||||
let changed = pieces.splice(edits);
|
||||
let contents = crate::text::rewritten_contents(doc, all, changed);
|
||||
let contents = crate::text::rewritten_contents(doc, all, changed)?;
|
||||
doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1606,7 +1595,7 @@ fn app_crop_ranges(ops: &[Op], start: usize, target: usize) -> Vec<std::ops::Ran
|
||||
}
|
||||
|
||||
/// Remove only crops written by PdfCraft, preserving the document author's clipping paths.
|
||||
fn remove_image_crop(doc: &mut Document, p: &pdfcraft_model::Page, all: &[(Object, Vec<u8>)], img: &PageImage) -> Result<(), EditError> {
|
||||
fn remove_image_crop(doc: &mut Document, p: &pdfcraft_model::Page, all: &[ContentStream], img: &PageImage) -> Result<(), EditError> {
|
||||
let pieces = joined(all);
|
||||
let ops = pieces.parse().ops;
|
||||
let target = img.op;
|
||||
@@ -1619,7 +1608,7 @@ fn remove_image_crop(doc: &mut Document, p: &pdfcraft_model::Page, all: &[(Objec
|
||||
return Err(EditError::Invalid("the image has no PdfCraft crop to remove".into()));
|
||||
}
|
||||
let changed = crate::text::splice(&pieces, &ops, |index| (Vec::new(), !crops.iter().any(|r| r.contains(&index))));
|
||||
let contents = crate::text::rewritten_contents(doc, all, changed);
|
||||
let contents = crate::text::rewritten_contents(doc, all, changed)?;
|
||||
doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1666,7 +1655,7 @@ pub fn change_image(doc: &mut Document, page: usize, index: usize, change: &Imag
|
||||
}
|
||||
};
|
||||
let changed = crate::text::splice(&pieces, &ops, |index| if index == img.op { (replacement.clone(), false) } else { (Vec::new(), true) });
|
||||
let contents = crate::text::rewritten_contents(doc, &all, changed);
|
||||
let contents = crate::text::rewritten_contents(doc, &all, changed)?;
|
||||
doc.update_dict(p.obj, |d| {
|
||||
d.set(b"Contents".to_vec(), contents);
|
||||
if let Some(res) = resources {
|
||||
@@ -1687,7 +1676,7 @@ pub(crate) fn selectable_images(
|
||||
Ok(page_images(doc, page)?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(_, image)| !all.get(image.stream).and_then(|(o, _)| o.as_ref()).is_some_and(|r| excluded.contains(&r)))
|
||||
.filter(|(_, image)| !all.get(image.stream).and_then(|s| s.entry.as_ref()).is_some_and(|r| excluded.contains(&r)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -1700,7 +1689,7 @@ pub(crate) fn painted_over(doc: &Document, page: usize, rect: [f64; 4], from: us
|
||||
fn transform_image(
|
||||
doc: &mut Document,
|
||||
page: &pdfcraft_model::Page,
|
||||
streams: &[(Object, Vec<u8>)],
|
||||
streams: &[ContentStream],
|
||||
image: &PageImage,
|
||||
transform: [f64; 6],
|
||||
) -> Result<(), EditError> {
|
||||
@@ -1733,7 +1722,7 @@ fn transform_image(
|
||||
let changed = crate::text::splice(&pieces, &ops, |index| {
|
||||
if index == at { (vec![Op::new("cm", folded.0.map(Object::Real).to_vec())], false) } else { (Vec::new(), true) }
|
||||
});
|
||||
let contents = crate::text::rewritten_contents(doc, streams, changed);
|
||||
let contents = crate::text::rewritten_contents(doc, streams, changed)?;
|
||||
doc.update_dict(page.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1754,7 +1743,7 @@ fn transform_image(
|
||||
(Vec::new(), true)
|
||||
}
|
||||
});
|
||||
let contents = crate::text::rewritten_contents(doc, streams, changed);
|
||||
let contents = crate::text::rewritten_contents(doc, streams, changed)?;
|
||||
doc.update_dict(page.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1766,7 +1755,7 @@ fn transform_image(
|
||||
fn crop_image_impl(
|
||||
doc: &mut Document,
|
||||
page: &pdfcraft_model::Page,
|
||||
streams: &[(Object, Vec<u8>)],
|
||||
streams: &[ContentStream],
|
||||
image: &PageImage,
|
||||
keep: [f64; 4],
|
||||
) -> Result<(), EditError> {
|
||||
@@ -1844,7 +1833,7 @@ fn crop_image_impl(
|
||||
(Vec::new(), true)
|
||||
}
|
||||
});
|
||||
let contents = crate::text::rewritten_contents(doc, streams, changed);
|
||||
let contents = crate::text::rewritten_contents(doc, streams, changed)?;
|
||||
doc.update_dict(page.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1977,7 +1966,7 @@ pub(crate) fn translate_images(doc: &mut Document, page: usize, indexes: &[usize
|
||||
out.push(Op::new("Q", vec![]));
|
||||
(out, false)
|
||||
});
|
||||
let contents = crate::text::rewritten_contents(doc, &all, data);
|
||||
let contents = crate::text::rewritten_contents(doc, &all, data)?;
|
||||
doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use pdfcraft_content::{Matrix, Op, Pieces};
|
||||
use pdfcraft_cos::{Dict, Document, Object};
|
||||
use pdfcraft_cos::{DecodeBudget, Dict, Document, Object};
|
||||
|
||||
use crate::text::ContentStream;
|
||||
use crate::{Content, EditError};
|
||||
|
||||
pub const MAX_MOVE_OBJECTS: usize = 1_000;
|
||||
@@ -152,7 +153,7 @@ fn added_geometry(doc: &Document, page: usize) -> Result<(Pieces, Vec<AddedGeome
|
||||
return Ok((Pieces::default(), Vec::new()));
|
||||
}
|
||||
let streams = checked_streams(doc, page)?;
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, bytes)| bytes.as_slice()).collect::<Vec<_>>());
|
||||
let pieces = crate::text::joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
let owned: BTreeSet<_> = added.iter().map(|a| a.obj).collect();
|
||||
let mut geometry = BTreeMap::new();
|
||||
@@ -161,7 +162,7 @@ fn added_geometry(doc: &Document, page: usize) -> Result<(Pieces, Vec<AddedGeome
|
||||
let mut stack = Vec::new();
|
||||
for (index, op) in ops.iter().enumerate() {
|
||||
let (stream, end) = pieces.pieces_of(op);
|
||||
let object = streams.get(stream).and_then(|(r, _)| r.as_ref());
|
||||
let object = streams.get(stream).and_then(|s| s.entry.as_ref());
|
||||
if object.is_some_and(|r| owned.contains(&r)) {
|
||||
if let std::collections::btree_map::Entry::Vacant(entry) = first.entry(stream) {
|
||||
if stream != end || !op.is("q") {
|
||||
@@ -275,7 +276,7 @@ fn translate_added(doc: &mut Document, page: usize, indexes: &[usize], offset: [
|
||||
})
|
||||
.collect::<Result<Vec<_>, EditError>>()?;
|
||||
let data = pieces.splice(edits);
|
||||
let contents = crate::text::rewritten_contents(doc, &streams, data);
|
||||
let contents = crate::text::rewritten_contents(doc, &streams, data)?;
|
||||
let refs = match &contents {
|
||||
Object::Array(a) => a.clone(),
|
||||
one => vec![one.clone()],
|
||||
@@ -288,9 +289,9 @@ fn translate_added(doc: &mut Document, page: usize, indexes: &[usize], offset: [
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A missing or undecodable preceding stream can hide graphics state that affects every
|
||||
/// A missing, undecodable or damaged preceding stream can hide graphics state that affects every
|
||||
/// later item. Refuse group editing rather than inventing an inventory from partial data.
|
||||
fn checked_streams(doc: &Document, page: usize) -> Result<Vec<(Object, Vec<u8>)>, EditError> {
|
||||
fn checked_streams(doc: &Document, page: usize) -> Result<Vec<ContentStream>, EditError> {
|
||||
let p = pdfcraft_model::pages(doc).into_iter().nth(page).ok_or(EditError::NoSuchPage(page))?;
|
||||
let contents = match p.dict.get(b"Contents") {
|
||||
None => Vec::new(),
|
||||
@@ -305,6 +306,7 @@ fn checked_streams(doc: &Document, page: usize) -> Result<Vec<(Object, Vec<u8>)>
|
||||
},
|
||||
};
|
||||
let mut remaining = MAX_CONTENT_BYTES;
|
||||
let mut decode = DecodeBudget::new(usize::MAX, 0);
|
||||
let mut depth = 0usize;
|
||||
let mut operations = 0usize;
|
||||
let mut streams = Vec::with_capacity(contents.len());
|
||||
@@ -313,9 +315,12 @@ fn checked_streams(doc: &Document, page: usize) -> Result<Vec<(Object, Vec<u8>)>
|
||||
let Object::Stream(stream) = &*resolved else {
|
||||
return Err(EditError::Invalid("a page content stream is missing".into()));
|
||||
};
|
||||
let bytes = stream
|
||||
.decoded_within(remaining.min(64 * 1024 * 1024))
|
||||
let (bytes, damaged) = decode
|
||||
.decode_with_damage(stream, remaining.min(64 * 1024 * 1024))
|
||||
.map_err(|_| EditError::Invalid("the page content cannot be decoded within its size limit".into()))?;
|
||||
if damaged {
|
||||
return Err(EditError::Invalid("a page content stream is damaged partway".into()));
|
||||
}
|
||||
remaining =
|
||||
remaining.checked_sub(bytes.len()).ok_or_else(|| EditError::Invalid("the page content is too large for group selection".into()))?;
|
||||
let parsed = pdfcraft_content::parse(&bytes);
|
||||
@@ -333,7 +338,7 @@ fn checked_streams(doc: &Document, page: usize) -> Result<Vec<(Object, Vec<u8>)>
|
||||
return Err(EditError::Invalid("the page graphics state is nested too deeply".into()));
|
||||
}
|
||||
}
|
||||
streams.push((object, bytes));
|
||||
streams.push(ContentStream { entry: object, data: bytes, damaged: false });
|
||||
}
|
||||
Ok(streams)
|
||||
}
|
||||
@@ -5148,6 +5148,66 @@ fn the_graphics_state_carries_over_between_content_streams() {
|
||||
assert!(close(l.rect[0], 100.0) && close(l.size, 10.0), "{l:?}");
|
||||
}
|
||||
|
||||
/// zlib data that reads as `readable` and is then damaged, with more of the stream after the
|
||||
/// damage: a stored block, then a block of the reserved type (RFC 1951 §3.2.3), then `rest`.
|
||||
fn damaged_flate(readable: &[u8], rest: &[u8]) -> pdfcraft_cos::Stream {
|
||||
let len = u16::try_from(readable.len()).unwrap();
|
||||
let mut raw = vec![0x78, 0x01, 0x00];
|
||||
raw.extend_from_slice(&len.to_le_bytes());
|
||||
raw.extend_from_slice(&(!len).to_le_bytes());
|
||||
raw.extend_from_slice(readable);
|
||||
raw.push(0x07);
|
||||
raw.extend_from_slice(rest);
|
||||
let mut dict = pdfcraft_cos::Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::name("FlateDecode"));
|
||||
pdfcraft_cos::Stream::from_raw(dict, raw)
|
||||
}
|
||||
|
||||
/// A page whose first content stream is damaged after its text, and whose second is whole.
|
||||
fn damaged_page() -> (Document, pdfcraft_cos::Stream) {
|
||||
let mut doc = split_page("", "BT /F1 12 Tf 72 600 Td (Whole) Tj ET");
|
||||
let damaged = damaged_flate(b"BT /F1 12 Tf 72 700 Td (Damaged) Tj ET\n", b"BT /F1 12 Tf 72 500 Td (After the damage) Tj ET");
|
||||
doc.set(pdfcraft_cos::ObjRef::new(4, 0), damaged.clone());
|
||||
(doc, damaged)
|
||||
}
|
||||
|
||||
/// An edit that would rewrite a content stream damaged partway is refused: written from what
|
||||
/// decoded, it would lose what follows the damage, which other readers may still read. An edit
|
||||
/// in the page's other stream goes on, and leaves the damaged one as it is stored.
|
||||
#[test]
|
||||
fn an_edit_never_rewrites_a_content_stream_damaged_partway() {
|
||||
let (mut doc, damaged) = damaged_page();
|
||||
let texts = |doc: &Document| text::text_lines(doc, 0).unwrap().into_iter().map(|l| l.text).collect::<Vec<_>>();
|
||||
assert_eq!(texts(&doc), ["Damaged", "Whole"], "read up to the damage");
|
||||
let contents = |doc: &Document| pdfcraft_model::pages(doc)[0].dict.get(b"Contents").cloned();
|
||||
let listed = contents(&doc);
|
||||
|
||||
let refused = text::replace_line(&mut doc, 0, 0, "Edited").unwrap_err();
|
||||
assert!(refused.to_string().contains("damaged partway"), "{refused}");
|
||||
assert_eq!(contents(&doc), listed);
|
||||
let refused = text::rewrite_block(&mut doc, 0, 0, Some("Edited"), &text::BlockStyle::default()).unwrap_err();
|
||||
assert!(refused.to_string().contains("damaged partway"), "{refused}");
|
||||
assert_eq!(contents(&doc), listed);
|
||||
|
||||
text::replace_line(&mut doc, 0, 1, "Edited").unwrap();
|
||||
let doc = reopen(&doc);
|
||||
assert_eq!(texts(&doc), ["Damaged", "Edited"]);
|
||||
let Some(Object::Array(list)) = contents(&doc) else { panic!("contents {:?}", contents(&doc)) };
|
||||
assert_eq!(list.first(), Some(&Object::Ref(pdfcraft_cos::ObjRef::new(4, 0))));
|
||||
let Object::Stream(kept) = &*doc.get(pdfcraft_cos::ObjRef::new(4, 0)) else { panic!("not a stream") };
|
||||
assert_eq!(kept.raw, damaged.raw, "stored as it was");
|
||||
}
|
||||
|
||||
/// Objects are selected only from content read whole: what a damaged stream hides past the
|
||||
/// damage could change the graphics state of every later item.
|
||||
#[test]
|
||||
fn objects_are_not_listed_from_a_content_stream_damaged_partway() {
|
||||
let (doc, _) = damaged_page();
|
||||
let refused = editable_objects(&doc, 0).unwrap_err();
|
||||
assert!(refused.to_string().contains("damaged partway"), "{refused}");
|
||||
assert!(editable_objects(&split_page("", "BT /F1 12 Tf 72 600 Td (Whole) Tj ET"), 0).is_ok());
|
||||
}
|
||||
|
||||
fn two_stream_image_page(first: &str, second: &str) -> Document {
|
||||
let objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
||||
|
||||
@@ -55,7 +55,9 @@ use fallback::{add_type3_font, is_type3_fallback};
|
||||
pub(crate) use fallback::{glyph_path, type3_font_dict, unique_font_name};
|
||||
use interp::{Origin, Paint, union};
|
||||
use lines::page_dict;
|
||||
pub(crate) use lines::{FormBudget, content_streams, content_streams_within, dict_in_place, form_call, resource_dict, rewritten_contents, splice};
|
||||
pub(crate) use lines::{
|
||||
ContentStream, FormBudget, content_streams, content_streams_within, dict_in_place, form_call, joined, resource_dict, rewritten_contents, splice,
|
||||
};
|
||||
pub use lines::{MAX_FORM_DEPTH, MAX_FORM_VISITS, glyph_text_lines, text_lines};
|
||||
pub use rewrite::{LineEdit, replace_line, rewrite_block};
|
||||
use rewrite::{coincident_ops, rewrite_block_from};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::interp::{Carry, interpret};
|
||||
use super::lines::{content_streams, page_dict, reading_lines, rewritten_contents, splice, text_lines};
|
||||
use super::lines::{content_streams, joined, page_dict, reading_lines, rewritten_contents, splice, text_lines};
|
||||
use super::rewrite::{LineEdit, coincident_ops, rewrite_block};
|
||||
use super::{Document, EditError, HashMap, Object, Op, Pieces, TextLine, group_blocks, regions};
|
||||
use super::{Document, EditError, HashMap, Object, Op, TextLine, group_blocks, regions};
|
||||
|
||||
/// An editable text region: adjacent runs on visual rows, followed by aligned rows.
|
||||
/// A region may contain several source fonts, sizes, colours and weights.
|
||||
@@ -270,7 +270,7 @@ pub fn clear_block(doc: &mut Document, page: usize, block: usize) -> Result<(),
|
||||
let p = page_dict(doc, page)?;
|
||||
let res = p.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||
let streams = content_streams(doc, &p.dict)?;
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let pieces = joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
let shown = interpret(doc, &ops, &res, &mut HashMap::new(), &mut Carry::new(), None, None)?;
|
||||
let mut patches = HashMap::new();
|
||||
@@ -297,7 +297,7 @@ pub fn clear_block(doc: &mut Document, page: usize, block: usize) -> Result<(),
|
||||
patches.insert(s.op, replacement);
|
||||
}
|
||||
let data = splice(&pieces, &ops, |i| patches.remove(&i).map_or((Vec::new(), true), |v| (v, false)));
|
||||
let contents = rewritten_contents(doc, &streams, data);
|
||||
let contents = rewritten_contents(doc, &streams, data)?;
|
||||
doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -61,16 +61,27 @@ impl TextLine {
|
||||
}
|
||||
}
|
||||
|
||||
/// The page's content streams as listed, each with its decoded data (empty when it doesn't
|
||||
/// decode), within one page's [`DecodeBudget`]. An error when the content runs past it: reading
|
||||
/// or rewriting a page from part of its content would lose the rest.
|
||||
pub(crate) fn content_streams(doc: &Document, page: &Dict) -> Result<Vec<(Object, Vec<u8>)>, EditError> {
|
||||
/// One entry of a page's `/Contents`, and what it decoded to.
|
||||
pub(crate) struct ContentStream {
|
||||
/// The entry as listed.
|
||||
pub(crate) entry: Object,
|
||||
/// Its decoded data, empty when it doesn't decode.
|
||||
pub(crate) data: Vec<u8>,
|
||||
/// The stream is damaged partway: `data` is what decoded before the damage, so the stream
|
||||
/// can't be written again from it without losing the rest ([`rewritten_contents`]).
|
||||
pub(crate) damaged: bool,
|
||||
}
|
||||
|
||||
/// The page's content streams as listed, each with its decoded data, within one page's
|
||||
/// [`DecodeBudget`]. An error when the content runs past it: reading or rewriting a page from
|
||||
/// part of its content would lose the rest.
|
||||
pub(crate) fn content_streams(doc: &Document, page: &Dict) -> Result<Vec<ContentStream>, EditError> {
|
||||
content_streams_within(doc, page, &mut DecodeBudget::page())
|
||||
}
|
||||
|
||||
/// [`content_streams`], decoded against `budget`, which a walk of the page's forms then goes on
|
||||
/// spending.
|
||||
pub(crate) fn content_streams_within(doc: &Document, page: &Dict, budget: &mut DecodeBudget) -> Result<Vec<(Object, Vec<u8>)>, EditError> {
|
||||
pub(crate) fn content_streams_within(doc: &Document, page: &Dict, budget: &mut DecodeBudget) -> Result<Vec<ContentStream>, EditError> {
|
||||
let list: Vec<Object> = match page.get(b"Contents") {
|
||||
None => Vec::new(),
|
||||
Some(c) => match &*doc.resolve(c) {
|
||||
@@ -79,20 +90,25 @@ pub(crate) fn content_streams_within(doc: &Document, page: &Dict, budget: &mut D
|
||||
},
|
||||
};
|
||||
let mut out = Vec::with_capacity(list.len());
|
||||
for o in list {
|
||||
let data = match &*doc.resolve(&o) {
|
||||
Object::Stream(s) => match budget.decode(s) {
|
||||
Ok(data) => data,
|
||||
for entry in list {
|
||||
let (data, damaged) = match &*doc.resolve(&entry) {
|
||||
Object::Stream(s) => match budget.decode_with_damage(s, usize::MAX) {
|
||||
Ok(read) => read,
|
||||
Err(e) if budget.remaining() == 0 => return Err(EditError::Invalid(format!("the page content is too large to read: {e}"))),
|
||||
Err(_) => Vec::new(),
|
||||
Err(_) => (Vec::new(), false),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
_ => (Vec::new(), false),
|
||||
};
|
||||
out.push((o, data));
|
||||
out.push(ContentStream { entry, data, damaged });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The page's streams joined into the one stream they are: an operator may span two of them.
|
||||
pub(crate) fn joined(streams: &[ContentStream]) -> Pieces {
|
||||
Pieces::join(&streams.iter().map(|s| s.data.as_slice()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Rewrite a page's content (its streams joined, see [`Pieces`]): `edit(i)` gives the operators
|
||||
/// to insert before operator `i` of `ops` (parsed from the joined pieces) and whether to keep it.
|
||||
/// Everything else is copied byte for byte. A page's streams are one stream in pieces, split
|
||||
@@ -121,25 +137,32 @@ pub(crate) fn splice(pieces: &Pieces, ops: &[Op], mut edit: impl FnMut(usize) ->
|
||||
}
|
||||
|
||||
/// The page's `/Contents` after a [`splice`]: pieces whose bytes changed become new streams
|
||||
/// (keeping their dictionaries), the others stay as they were.
|
||||
pub(crate) fn rewritten_contents(doc: &mut Document, streams: &[(Object, Vec<u8>)], new: Vec<Vec<u8>>) -> Object {
|
||||
/// (keeping their dictionaries), the others stay as they were. An error, before anything is
|
||||
/// added, when a piece that changed is a damaged stream: written from what decoded, it would
|
||||
/// lose everything after the damage.
|
||||
pub(crate) fn rewritten_contents(doc: &mut Document, streams: &[ContentStream], new: Vec<Vec<u8>>) -> Result<Object, EditError> {
|
||||
if streams.iter().zip(&new).any(|(s, data)| s.damaged && s.data != *data) {
|
||||
return Err(EditError::Invalid(
|
||||
"this change is in a content stream that is damaged partway, and rewriting it would lose what follows the damage".into(),
|
||||
));
|
||||
}
|
||||
let mut contents: Vec<Object> = Vec::with_capacity(streams.len());
|
||||
for ((obj, old), data) in streams.iter().zip(new) {
|
||||
if *old == data {
|
||||
contents.push(obj.clone());
|
||||
for (s, data) in streams.iter().zip(new) {
|
||||
if s.data == data {
|
||||
contents.push(s.entry.clone());
|
||||
continue;
|
||||
}
|
||||
let mut dict = match &*doc.resolve(obj) {
|
||||
let mut dict = match &*doc.resolve(&s.entry) {
|
||||
Object::Stream(s) => s.dict.clone(),
|
||||
_ => Dict::new(),
|
||||
};
|
||||
dict.remove(b"Length");
|
||||
contents.push(Object::Ref(doc.add(Object::Stream(Stream::flate(dict, &data)))));
|
||||
}
|
||||
match <[Object; 1]>::try_from(contents) {
|
||||
Ok(match <[Object; 1]>::try_from(contents) {
|
||||
Ok([one]) => one,
|
||||
Err(many) => Object::Array(many),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn page_dict(doc: &Document, page: usize) -> Result<pdfcraft_model::Page, EditError> {
|
||||
@@ -201,7 +224,7 @@ fn text_lines_in(doc: &Document, p: &pdfcraft_model::Page, actual_text: bool) ->
|
||||
let mut carry = Carry::new();
|
||||
// The streams are one stream in pieces (§7.8.2), split between any two tokens: parse them
|
||||
// joined, so an operator whose operands end one piece still has them.
|
||||
let pieces = Pieces::join(&content_streams(doc, &p.dict)?.into_iter().map(|(_, d)| d).collect::<Vec<_>>());
|
||||
let pieces = joined(&content_streams(doc, &p.dict)?);
|
||||
let ops = pieces.parse().ops;
|
||||
let properties = res.get(b"Properties").and_then(|o| doc.dict(o)).unwrap_or_default();
|
||||
let shown = interpret(doc, &ops, &res, &mut cache, &mut carry, None, actual_text.then_some(&properties))?;
|
||||
@@ -608,7 +631,7 @@ pub(super) fn reading_lines(doc: &Document, page: usize) -> Result<(Vec<TextLine
|
||||
// do: a comment, an operator's operands or a marked-content sequence (its /ActualText too)
|
||||
// can run on from one stream into the next. Text still counts as the stream its operator
|
||||
// starts in, which keeps lines from different streams apart.
|
||||
let pieces = Pieces::join(&streams.into_iter().map(|(_, data)| data).collect::<Vec<_>>());
|
||||
let pieces = joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
let stream_of = |i: usize| ops.get(i).map_or(0, |op| pieces.pieces_of(op).0);
|
||||
walk(&mut w, &ops, &res, &mut carry, &stream_of)?;
|
||||
|
||||
@@ -4,10 +4,10 @@ use super::fallback::{
|
||||
source_program_completes, source_show, source_space, source_width, type3_encode, type3_font,
|
||||
};
|
||||
use super::interp::{Carry, Origin, TextState, interpret};
|
||||
use super::lines::{content_streams, page_dict, rewritten_contents, splice, text_lines};
|
||||
use super::lines::{content_streams, joined, page_dict, rewritten_contents, splice, text_lines};
|
||||
use super::{
|
||||
Dict, Document, EditError, HashMap, Matrix, Metrics, Object, Op, PdfString, Pieces, TextLine, TextSession, alignment_frame, group_blocks,
|
||||
is_centred, parse, rewrite_styled_block,
|
||||
Dict, Document, EditError, HashMap, Matrix, Metrics, Object, Op, PdfString, TextLine, TextSession, alignment_frame, group_blocks, is_centred,
|
||||
parse, rewrite_styled_block,
|
||||
};
|
||||
|
||||
/// What replacing a line did.
|
||||
@@ -56,7 +56,7 @@ pub(crate) fn replace_line_with_limit(doc: &mut Document, page: usize, line: usi
|
||||
let mut res = p.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
let mut fonts_res = res.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default();
|
||||
let streams = content_streams(doc, &p.dict)?;
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let pieces = joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
let first = target.ops.first().copied().filter(|i| *i < ops.len()).ok_or_else(|| EditError::Invalid("the page's content changed".into()))?;
|
||||
// The line's own font, when it can show every character.
|
||||
@@ -127,7 +127,7 @@ pub(crate) fn replace_line_with_limit(doc: &mut Document, page: usize, line: usi
|
||||
}
|
||||
// Rebuild: the line's first operator becomes the replacement, its others go.
|
||||
let new_data = splice(&pieces, &ops, |i| if i == first { (std::mem::take(&mut replacement), false) } else { (Vec::new(), !drops.contains(&i)) });
|
||||
let contents = rewritten_contents(doc, &streams, new_data);
|
||||
let contents = rewritten_contents(doc, &streams, new_data)?;
|
||||
let page_ref = p.obj;
|
||||
if substituted.is_some() {
|
||||
res.set(b"Font".to_vec(), Object::Dict(fonts_res));
|
||||
@@ -834,7 +834,7 @@ pub(crate) fn rewrite_block_from(
|
||||
}
|
||||
let upright = dispatch.upright;
|
||||
let streams = content_streams(doc, &p.dict)?;
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let pieces = joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
let text = paragraph_text(text.unwrap_or(&b.text));
|
||||
// The characters to show, for choosing fonts: the breaks are moves, not glyphs.
|
||||
@@ -970,7 +970,7 @@ pub(crate) fn rewrite_block_from(
|
||||
let insert = if i == at { std::mem::take(&mut block_ops) } else { Vec::new() };
|
||||
(insert, !dropped.contains(&i))
|
||||
});
|
||||
let contents = rewritten_contents(doc, &streams, new_data);
|
||||
let contents = rewritten_contents(doc, &streams, new_data)?;
|
||||
let new_font = !reuse;
|
||||
if new_font {
|
||||
res.set(b"Font".to_vec(), Object::Dict(fonts_res));
|
||||
@@ -1040,7 +1040,7 @@ pub(crate) fn selectable_blocks(
|
||||
!b.lines
|
||||
.iter()
|
||||
.filter_map(|i| lines.get(*i))
|
||||
.any(|l| streams.get(l.stream).and_then(|(o, _)| o.as_ref()).is_some_and(|r| excluded.contains(&r)))
|
||||
.any(|l| streams.get(l.stream).and_then(|s| s.entry.as_ref()).is_some_and(|r| excluded.contains(&r)))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -1134,7 +1134,7 @@ pub(crate) fn translate_blocks(doc: &mut Document, page: usize, indexes: &[usize
|
||||
}
|
||||
let res = p.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
let fonts = res.get(b"Font").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let pieces = joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
let shown = interpret(doc, &ops, &res, &mut HashMap::new(), &mut Carry::new(), None, None)?;
|
||||
let mut patches = HashMap::new();
|
||||
@@ -1205,7 +1205,7 @@ pub(crate) fn translate_blocks(doc: &mut Document, page: usize, indexes: &[usize
|
||||
return Err(EditError::Invalid("a selected text operation cannot be moved".into()));
|
||||
}
|
||||
let data = splice(&pieces, &ops, |i| patches.remove(&i).map_or((Vec::new(), true), |v| (v, false)));
|
||||
let contents = rewritten_contents(doc, &streams, data);
|
||||
let contents = rewritten_contents(doc, &streams, data)?;
|
||||
doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), contents))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -962,7 +962,7 @@ pub(crate) fn rewrite_styled_block(
|
||||
};
|
||||
let (rows, row_ranges, row_lines, row_soft, row_gap) = wrap_styled_rows(&chars, width, measure)?;
|
||||
let streams = content_streams(doc, &p.dict)?;
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let pieces = crate::text::joined(&streams);
|
||||
let ops = pieces.parse().ops;
|
||||
// A row the edit doesn't reach keeps its source operators, so its kerning, TJ word gaps and
|
||||
// glyph widths stay exactly as they were instead of being rebuilt from the text state. A row
|
||||
@@ -1206,7 +1206,7 @@ pub(crate) fn rewrite_styled_block(
|
||||
// when the rewrite splits one, and a kept row's operators come later in the stream.
|
||||
let marker = if marked { None } else { paragraph_marker(block, lines, &ops) };
|
||||
let data = splice_with_marker(&pieces, &ops, marker, at, block_ops, &drop);
|
||||
let contents = rewritten_contents(doc, &streams, data);
|
||||
let contents = rewritten_contents(doc, &streams, data)?;
|
||||
res.set(b"Font".to_vec(), Object::Dict(fonts));
|
||||
doc.update_dict(p.obj, |d| {
|
||||
d.set(b"Contents".to_vec(), contents);
|
||||
|
||||
Reference in new issue
Block a user