mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 19:48:14 -06:00
Merge PR #3310: Keep text rows in other layers, gs states or Tz out of a paragraph
crates/edit/src/text.rs: interpret takes the page resources (#3310) and the named property lists for /ActualText (main's #3301); every call site passes both. In BDC handling, #3310's layer push runs before main's property-list lookup; carry.layers is saved before positioned_tracking. crates/edit/src/tests.rs: kept both sides' new tests.
This commit is contained in:
@@ -5372,3 +5372,109 @@ fn a_text_session_commits_what_a_rewrite_writes_whatever_it_laid_out_before() {
|
||||
assert_eq!(pdfcraft_cos::write_full(&by_session, &SaveOptions::default()).unwrap(), before);
|
||||
assert!(session.is_current(&doc) && !session.is_current(&by_session));
|
||||
}
|
||||
|
||||
/// A one-page document whose resources have `/GS0` (30 % opacity) and `/L1`, a layer that is OFF.
|
||||
fn row_state_page(content: &str) -> Document {
|
||||
let objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R] /D << /OFF [5 0 R] /Order [5 0 R] >> >> >>".into(),
|
||||
"<< /Type /Pages /Kids [6 0 R] /Count 1 >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
"<< /Type /ExtGState /ca 0.3 /CA 0.3 >>".into(),
|
||||
"<< /Type /OCG /Name (Hidden layer) >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 7 0 R /Resources << /Font << /F1 3 0 R >> /ExtGState << /GS0 4 0 R /GS1 << /Type /ExtGState /ca 0.3 /CA 0.3 /BM /Normal >> /GS2 << /Type /ExtGState /ca 1 /CA 1 >> >> /Properties << /L1 5 0 R >> >> >>".into(),
|
||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offs = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offs.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let x = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offs {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
Document::open(Arc::new(out)).unwrap()
|
||||
}
|
||||
|
||||
/// Each shown string on page 0 with the state it is drawn in: inside a `/OC` marked-content
|
||||
/// sequence, after `/GS0 gs`, and its horizontal scale (percent).
|
||||
fn shown_states(doc: &Document) -> Vec<(String, bool, bool, f64)> {
|
||||
let joined = streams(doc, 0).join("\n");
|
||||
let (mut oc, mut marks) = (0usize, Vec::new());
|
||||
let (mut gs, mut tz, mut stack) = (false, 100.0, Vec::new());
|
||||
let mut out = Vec::new();
|
||||
for op in pdfcraft_content::parse(joined.as_bytes()).ops {
|
||||
match op.op.as_slice() {
|
||||
b"BDC" | b"BMC" => {
|
||||
let layer = op.name(0) == Some(b"OC".as_slice());
|
||||
oc += usize::from(layer);
|
||||
marks.push(layer);
|
||||
}
|
||||
b"EMC" => oc -= usize::from(marks.pop().unwrap()),
|
||||
b"q" => stack.push((gs, tz)),
|
||||
b"Q" => (gs, tz) = stack.pop().unwrap(),
|
||||
b"gs" => gs |= op.name(0) == Some(b"GS0".as_slice()),
|
||||
b"Tz" => tz = op.num(0).unwrap(),
|
||||
b"Tj" | b"'" => {
|
||||
let Some(Object::String(s)) = op.operands.last() else { continue };
|
||||
if !s.bytes.is_empty() {
|
||||
out.push((String::from_utf8_lossy(&s.bytes).into_owned(), oc > 0, gs, tz));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// #3115: a row drawn in a hidden layer, at 30 % opacity or at 80 % horizontal scale keeps that
|
||||
/// state when another row of the same paragraph is edited; the edited row doesn't take it on.
|
||||
#[test]
|
||||
fn editing_a_row_keeps_the_layer_opacity_and_scale_of_the_rows_around_it() {
|
||||
let row1 = "BT /F1 12 Tf 72 700 Td (Hello world, this is the first line) Tj ET\n";
|
||||
let row2 = "BT /F1 12 Tf 72 686 Td (and the second) Tj ET";
|
||||
let pages = [
|
||||
(format!("{row1}/OC /L1 BDC {row2} EMC"), (true, false, 100.0)),
|
||||
(format!("{row1}q /GS0 gs {row2} Q"), (false, true, 100.0)),
|
||||
("BT /F1 12 Tf 72 700 Td (Hello world, this is the first line) Tj 80 Tz 0 -14 Td (and the second) Tj ET".to_string(), (false, false, 80.0)),
|
||||
(format!("{row1}{row2}"), (false, false, 100.0)),
|
||||
];
|
||||
for (n, (content, (oc, gs, tz))) in pages.iter().enumerate() {
|
||||
let mut doc = row_state_page(content);
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
let block = blocks.iter().position(|b| b.text.starts_with("Hello")).unwrap();
|
||||
let text = blocks[block].text.replacen("Hello", "Jello", 1);
|
||||
text::replace_block(&mut doc, 0, block, &text).unwrap();
|
||||
let shown = shown_states(&reopen(&doc));
|
||||
let find = |needle: &str| shown.iter().filter(|s| s.0.contains(needle)).cloned().collect::<Vec<_>>();
|
||||
let first = find("Jello");
|
||||
assert_eq!(first.len(), 1, "page {}: {shown:?}", n + 1);
|
||||
assert_eq!((first[0].1, first[0].2, first[0].3), (false, false, 100.0), "page {}: the edited row keeps its own state: {shown:?}", n + 1);
|
||||
let second = find("and the second");
|
||||
assert_eq!(second.len(), 1, "page {}: {shown:?}", n + 1);
|
||||
assert_eq!((second[0].1, second[0].2, second[0].3), (*oc, *gs, *tz), "page {}: the other row keeps its state: {shown:?}", n + 1);
|
||||
}
|
||||
// The control: rows in the same state are still one paragraph.
|
||||
let blocks = text::text_blocks(&row_state_page(&pages[3].0), 0).unwrap();
|
||||
assert_eq!(blocks.len(), 1);
|
||||
assert_eq!(blocks[0].text, "Hello world, this is the first line and the second");
|
||||
}
|
||||
|
||||
/// What keeps rows apart is the graphics state they are drawn in, not the `gs` operators
|
||||
/// before them: a repeated `gs` without q/Q, or another name with the same settings, is still one
|
||||
/// paragraph, and a `gs` that changes the opacity or puts it back is not.
|
||||
#[test]
|
||||
fn rows_under_repeated_or_equal_gs_states_stay_one_paragraph() {
|
||||
let row1 = "BT /F1 12 Tf 72 700 Td (Hello world, this is the first line) Tj ET\n";
|
||||
let row2 = "BT /F1 12 Tf 72 686 Td (and the second) Tj ET";
|
||||
let paragraphs = |content: String| text::text_blocks(&row_state_page(&content), 0).unwrap().len();
|
||||
assert_eq!(paragraphs(format!("/GS0 gs {row1}/GS0 gs {row2}")), 1, "the same name twice");
|
||||
assert_eq!(paragraphs(format!("/GS0 gs {row1}/GS0 gs /GS0 gs {row2}")), 1, "the same name three times");
|
||||
assert_eq!(paragraphs(format!("/GS0 gs {row1}/GS1 gs {row2}")), 1, "two names, the same settings");
|
||||
assert_eq!(paragraphs(format!("/GS2 gs {row1}{row2}")), 1, "settings that are the defaults");
|
||||
assert_eq!(paragraphs(format!("{row1}/GS0 gs {row2}")), 2, "another opacity");
|
||||
assert_eq!(paragraphs(format!("/GS0 gs {row1}/GS2 gs {row2}")), 2, "the opacity put back");
|
||||
}
|
||||
|
||||
+95
-12
@@ -65,6 +65,8 @@ pub struct TextLine {
|
||||
origin: Origin,
|
||||
/// Explicit marked-content scope (when present); distinct scopes must not be guessed into one paragraph.
|
||||
scope: Option<usize>,
|
||||
/// The layers and graphics state parameters it is drawn in; see [`Paint`].
|
||||
paint: Paint,
|
||||
/// Right-to-left `/ActualText` already supplies typing order; glyph text still needs bidi
|
||||
/// recovery. Left-to-right ActualText (e.g. a Latin ligature) joins its line as usual.
|
||||
logical_order: bool,
|
||||
@@ -244,6 +246,66 @@ impl TextState {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a run is drawn in beyond its text state: the optional content groups of the `/OC`
|
||||
/// marked-content sequences around it, and the effective graphics state parameters (`gs`) in
|
||||
/// force. A rewrite writes a paragraph in its first run's place, so runs that differ here are
|
||||
/// never joined: one in a hidden layer or at another opacity would lose it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
struct Paint {
|
||||
layers: Vec<Vec<u8>>,
|
||||
gs: Gs,
|
||||
}
|
||||
|
||||
/// The transparency parameters of the graphics state: what the `gs` operators have set, resolved,
|
||||
/// so a repeated `gs` or two names with the same settings leave it as it was.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Gs {
|
||||
/// `ca`, the fill opacity.
|
||||
fill_alpha: f64,
|
||||
/// `CA`, the stroke opacity.
|
||||
stroke_alpha: f64,
|
||||
/// `BM`, the blend mode (`Compatible` is `Normal`).
|
||||
blend: Vec<u8>,
|
||||
/// `SMask`, when it is not `None`: the reference of an indirect soft mask, else the dictionary.
|
||||
soft_mask: Option<Object>,
|
||||
}
|
||||
|
||||
impl Default for Gs {
|
||||
fn default() -> Self {
|
||||
Gs { fill_alpha: 1.0, stroke_alpha: 1.0, blend: b"Normal".to_vec(), soft_mask: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Gs {
|
||||
/// Applies the parameters a graphics state parameter dictionary sets; the others stay.
|
||||
fn apply(&mut self, doc: &Document, params: &Dict) {
|
||||
let num = |key: &[u8]| params.get(key).and_then(|o| doc.resolve(o).as_f64()).filter(|v| v.is_finite());
|
||||
if let Some(v) = num(b"ca") {
|
||||
self.fill_alpha = v;
|
||||
}
|
||||
if let Some(v) = num(b"CA") {
|
||||
self.stroke_alpha = v;
|
||||
}
|
||||
if let Some(bm) = params.get(b"BM").map(|o| doc.resolve(o)) {
|
||||
let first = bm
|
||||
.as_name()
|
||||
.map(<[u8]>::to_vec)
|
||||
.or_else(|| bm.as_array().and_then(|a| a.iter().find_map(|o| doc.resolve(o).as_name().map(<[u8]>::to_vec))));
|
||||
if let Some(name) = first {
|
||||
self.blend = if name == b"Compatible" { b"Normal".to_vec() } else { name };
|
||||
}
|
||||
}
|
||||
if let Some(mask) = params.get(b"SMask") {
|
||||
self.soft_mask = match (mask.as_ref(), &*doc.resolve(mask)) {
|
||||
(_, Object::Name(_)) => None,
|
||||
(Some(r), _) => Some(Object::Ref(r)),
|
||||
(None, Object::Dict(d)) => Some(Object::Dict(d.clone())),
|
||||
(None, _) => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Origin {
|
||||
tm: [f64; 6],
|
||||
@@ -535,6 +597,8 @@ struct Ts {
|
||||
leading: f64,
|
||||
rise: f64,
|
||||
render_mode: i64,
|
||||
/// The transparency parameters the `gs` operators have set so far (see [`Paint`]).
|
||||
gs: Gs,
|
||||
}
|
||||
|
||||
fn content_streams(doc: &Document, page: &Dict) -> Vec<(Object, Vec<u8>)> {
|
||||
@@ -678,6 +742,7 @@ struct Shown {
|
||||
visual_order: bool,
|
||||
decodable: bool,
|
||||
scope: Option<usize>,
|
||||
paint: Paint,
|
||||
logical_order: bool,
|
||||
actual_text: bool,
|
||||
}
|
||||
@@ -797,6 +862,8 @@ struct Carry {
|
||||
ts: Ts,
|
||||
stack: Vec<Ts>,
|
||||
marked: Vec<Option<usize>>,
|
||||
/// For each open marked-content sequence, its optional content group when it is `/OC`.
|
||||
layers: Vec<Option<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl Carry {
|
||||
@@ -813,8 +880,9 @@ impl Carry {
|
||||
leading: 0.0,
|
||||
rise: 0.0,
|
||||
render_mode: 0,
|
||||
gs: Gs::default(),
|
||||
};
|
||||
Carry { ts, stack: Vec::new(), marked: Vec::new() }
|
||||
Carry { ts, stack: Vec::new(), marked: Vec::new(), layers: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,13 +892,15 @@ type Invocation = (usize, Vec<u8>, Ts);
|
||||
fn interpret(
|
||||
doc: &Document,
|
||||
ops: &[Op],
|
||||
fonts_res: &Dict,
|
||||
resources: &Dict,
|
||||
cache: &mut HashMap<Vec<u8>, (Rc<Metrics>, Option<String>)>,
|
||||
carry: &mut Carry,
|
||||
mut invoked: Option<&mut Vec<Invocation>>,
|
||||
// `Some(properties)`: read `/ActualText`, looking named property lists up in `properties`.
|
||||
logical_text: Option<&Dict>,
|
||||
) -> Vec<Shown> {
|
||||
let fonts_res = resources.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default();
|
||||
let states = resources.get(b"ExtGState").map(|g| doc.resolve(g)).and_then(|g| g.as_dict().cloned()).unwrap_or_default();
|
||||
let mut out: Vec<Shown> = Vec::new();
|
||||
let mut ts = carry.ts.clone();
|
||||
let mut at_bt = (0usize, ts.clone());
|
||||
@@ -838,6 +908,7 @@ fn interpret(
|
||||
let (mut tm, mut tlm) = (Matrix::IDENTITY, Matrix::IDENTITY);
|
||||
let mut bt = 0usize;
|
||||
let mut marked = std::mem::take(&mut carry.marked);
|
||||
let mut layers = std::mem::take(&mut carry.layers);
|
||||
let mut actual: Vec<Option<(usize, usize, String)>> = Vec::new();
|
||||
for (i, op) in ops.iter().enumerate() {
|
||||
match op.op.as_slice() {
|
||||
@@ -846,6 +917,9 @@ fn interpret(
|
||||
// artifacts and inner spans, which inherit their enclosing region.
|
||||
let region = matches!(op.name(0), Some(b"P" | b"H" | b"H1" | b"H2" | b"H3" | b"H4" | b"H5" | b"H6" | b"LI" | b"TD" | b"TH"));
|
||||
marked.push(if region { Some(i) } else { marked.last().copied().flatten() });
|
||||
// A layer named in the resources is told apart by its name, an inline one by where it opens.
|
||||
let layer = op.is("BDC") && op.name(0) == Some(b"OC".as_slice());
|
||||
layers.push(layer.then(|| op.operands.get(1).and_then(Object::as_name).map_or_else(|| format!("@{i}").into_bytes(), <[u8]>::to_vec)));
|
||||
// The property list is inline or named in the resources' /Properties (§14.6.2).
|
||||
let list = logical_text.and_then(|properties| match op.operands.get(1) {
|
||||
Some(Object::Name(n)) => properties.get(n).and_then(|o| doc.dict(o)),
|
||||
@@ -859,6 +933,7 @@ fn interpret(
|
||||
}
|
||||
b"EMC" => {
|
||||
marked.pop();
|
||||
layers.pop();
|
||||
if let Some(Some((start, open, logical))) = actual.pop() {
|
||||
let tail = out.split_off(start.min(out.len()));
|
||||
let mut items = tail.into_iter();
|
||||
@@ -932,6 +1007,12 @@ fn interpret(
|
||||
b"TL" => ts.leading = op.num(0).unwrap_or(0.0),
|
||||
b"Ts" => ts.rise = op.num(0).unwrap_or(0.0),
|
||||
b"Tr" => ts.render_mode = op.operands.first().and_then(Object::as_int).unwrap_or(-1),
|
||||
b"gs" => {
|
||||
let params = op.name(0).and_then(|name| states.get(name)).map(|p| doc.resolve(p));
|
||||
if let Some(params) = params.as_deref().and_then(Object::as_dict) {
|
||||
ts.gs.apply(doc, params);
|
||||
}
|
||||
}
|
||||
b"Do" => {
|
||||
if let (Some(list), Some(name)) = (invoked.as_deref_mut(), op.name(0)) {
|
||||
list.push((i, name.to_vec(), ts.clone()));
|
||||
@@ -1090,6 +1171,7 @@ fn interpret(
|
||||
visual_order: m.visual_order,
|
||||
decodable,
|
||||
scope: marked.last().copied().flatten(),
|
||||
paint: Paint { layers: layers.iter().flatten().cloned().collect(), gs: ts.gs.clone() },
|
||||
logical_order: false,
|
||||
actual_text: false,
|
||||
});
|
||||
@@ -1100,6 +1182,7 @@ fn interpret(
|
||||
carry.ts = ts;
|
||||
carry.stack = stack;
|
||||
carry.marked = marked;
|
||||
carry.layers = layers;
|
||||
positioned_tracking(&mut out);
|
||||
out
|
||||
}
|
||||
@@ -1108,7 +1191,6 @@ fn interpret(
|
||||
pub fn text_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditError> {
|
||||
let p = page_dict(doc, page)?;
|
||||
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 = res.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default();
|
||||
let mut cache = HashMap::new();
|
||||
let mut lines: Vec<TextLine> = Vec::new();
|
||||
let mut carry = Carry::new();
|
||||
@@ -1117,7 +1199,7 @@ pub fn text_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditErro
|
||||
let pieces = Pieces::join(&content_streams(doc, &p.dict).into_iter().map(|(_, d)| d).collect::<Vec<_>>());
|
||||
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, &fonts_res, &mut cache, &mut carry, None, Some(&properties));
|
||||
let shown = interpret(doc, &ops, &res, &mut cache, &mut carry, None, Some(&properties));
|
||||
let mut last: Option<(usize, Pen)> = None; // the stream piece and where its last run ended
|
||||
for mut s in shown {
|
||||
// A single RTL glyph's ActualText (e.g. lam-alef) is an inline ligature, not a
|
||||
@@ -1135,7 +1217,7 @@ pub fn text_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditErro
|
||||
let bt = pen.bt;
|
||||
let (near, gap) = pen.continues(&s, if bt == s.bt { 3.0 } else { 1.5 });
|
||||
piece == si
|
||||
&& lines.last().is_some_and(|l| l.scope == s.scope && l.logical_order == s.logical_order)
|
||||
&& lines.last().is_some_and(|l| l.scope == s.scope && l.paint == s.paint && l.logical_order == s.logical_order)
|
||||
&& (bt == s.bt || lines.last().is_some_and(|l| compatible_word(l, &s)))
|
||||
&& (s.text.trim().is_empty() || lines.last().is_some_and(|l| same_font(l, &s.font, &s.base_font, s.face.as_deref())))
|
||||
&& (s.text.trim().is_empty()
|
||||
@@ -1180,6 +1262,7 @@ pub fn text_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditErro
|
||||
face: s.face.clone(),
|
||||
stream: si,
|
||||
scope: s.scope,
|
||||
paint: s.paint.clone(),
|
||||
logical_order: s.logical_order,
|
||||
actual_text: s.actual_text,
|
||||
clips: s.state.render_mode >= 4,
|
||||
@@ -1218,6 +1301,7 @@ fn compatible_word(line: &TextLine, shown: &Shown) -> bool {
|
||||
let same = |a: &[f64], b: &[f64]| a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-6);
|
||||
let (a, b) = (&line.origin.state, TextState::of(&shown.state));
|
||||
line.scope == shown.scope
|
||||
&& line.paint == shown.paint
|
||||
&& same(&line.origin.tm[..4], &shown.tm.0[..4])
|
||||
&& same(&line.origin.ctm, &shown.state.ctm.0)
|
||||
&& same(&[a.char_spacing, a.word_spacing, a.scale, a.rise], &[b.char_spacing, b.word_spacing, b.scale, b.rise])
|
||||
@@ -1230,7 +1314,7 @@ fn compatible_word(line: &TextLine, shown: &Shown) -> bool {
|
||||
fn add_shown(lines: &mut Vec<TextLine>, last: &mut Option<Pen>, s: Shown, stream: usize) {
|
||||
let joins = last.is_some_and(|pen| {
|
||||
let bt = pen.bt;
|
||||
lines.last().is_some_and(|l| l.scope == s.scope && l.logical_order == s.logical_order)
|
||||
lines.last().is_some_and(|l| l.scope == s.scope && l.paint == s.paint && l.logical_order == s.logical_order)
|
||||
&& (bt == s.bt || lines.last().is_some_and(|l| compatible_word(l, &s)))
|
||||
&& (s.text.trim().is_empty() || lines.last().is_some_and(|l| same_font(l, &s.font, &s.base_font, s.face.as_deref())))
|
||||
&& (s.text.trim().is_empty()
|
||||
@@ -1271,6 +1355,7 @@ fn add_shown(lines: &mut Vec<TextLine>, last: &mut Option<Pen>, s: Shown, stream
|
||||
face: s.face.clone(),
|
||||
stream,
|
||||
scope: s.scope,
|
||||
paint: s.paint.clone(),
|
||||
logical_order: s.logical_order,
|
||||
actual_text: s.actual_text,
|
||||
clips: s.state.render_mode >= 4,
|
||||
@@ -1383,11 +1468,10 @@ fn reading_lines(doc: &Document, page: usize) -> Result<(Vec<TextLine>, bool), E
|
||||
}
|
||||
/// `stream` gives the stream number of the operator at an index of `ops`.
|
||||
fn walk(w: &mut Walk, ops: &[Op], resources: &Dict, carry: &mut Carry, stream: &dyn Fn(usize) -> usize) {
|
||||
let fonts_res = resources.get(b"Font").map(|f| w.doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default();
|
||||
let mut cache = HashMap::new();
|
||||
let mut invoked = Vec::new();
|
||||
let properties = resources.get(b"Properties").and_then(|o| w.doc.dict(o)).unwrap_or_default();
|
||||
let shown = interpret(w.doc, ops, &fonts_res, &mut cache, carry, Some(&mut invoked), Some(&properties));
|
||||
let shown = interpret(w.doc, ops, resources, &mut cache, carry, Some(&mut invoked), Some(&properties));
|
||||
let mut last = None;
|
||||
let mut last_stream = None;
|
||||
let mut forms = invoked.into_iter().peekable();
|
||||
@@ -1412,7 +1496,7 @@ fn reading_lines(doc: &Document, page: usize) -> Result<(Vec<TextLine>, bool), E
|
||||
let Some(form) = form_call(w.doc, resources, name, ts.ctm, &w.path, &mut w.budget) else { return };
|
||||
let ops = parse(&form.data).ops;
|
||||
// The form starts with the graphics state at its `Do`, its own matrix applied.
|
||||
let mut carry = Carry { ts: Ts { ctm: form.ctm, ..ts }, stack: Vec::new(), marked: Vec::new() };
|
||||
let mut carry = Carry { ts: Ts { ctm: form.ctm, ..ts }, stack: Vec::new(), marked: Vec::new(), layers: Vec::new() };
|
||||
let stream = w.next_stream;
|
||||
w.next_stream = w.next_stream.saturating_add(1);
|
||||
if let Some(r) = form.obj {
|
||||
@@ -2108,11 +2192,10 @@ pub fn clear_block(doc: &mut Document, page: usize, block: usize) -> Result<(),
|
||||
drops.extend(target.lines.iter().filter_map(|i| lines.get(*i)).flat_map(|l| l.ops.iter().copied()));
|
||||
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 fonts = res.get(b"Font").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 ops = pieces.parse().ops;
|
||||
let shown = interpret(doc, &ops, &fonts, &mut HashMap::new(), &mut Carry::new(), None, None);
|
||||
let shown = interpret(doc, &ops, &res, &mut HashMap::new(), &mut Carry::new(), None, None);
|
||||
let mut patches = HashMap::new();
|
||||
for s in shown.into_iter().filter(|s| drops.contains(&s.op)) {
|
||||
if s.state.render_mode >= 4 {
|
||||
@@ -2790,7 +2873,7 @@ pub(crate) fn translate_blocks(doc: &mut Document, page: usize, indexes: &[usize
|
||||
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 ops = pieces.parse().ops;
|
||||
let shown = interpret(doc, &ops, &fonts, &mut HashMap::new(), &mut Carry::new(), None, None);
|
||||
let shown = interpret(doc, &ops, &res, &mut HashMap::new(), &mut Carry::new(), None, None);
|
||||
let mut patches = HashMap::new();
|
||||
// Keep full precision: a tiny rounded text-space change can be magnified by a CTM,
|
||||
// and restoring rise/spacing must not alter following unselected text.
|
||||
|
||||
@@ -6,6 +6,7 @@ fn same_plane(a: &TextLine, b: &TextLine) -> bool {
|
||||
let at = Matrix(a.origin.tm).then(&Matrix(a.origin.ctm));
|
||||
let bt = Matrix(b.origin.tm).then(&Matrix(b.origin.ctm));
|
||||
a.scope == b.scope
|
||||
&& a.paint == b.paint
|
||||
&& [0, 2].iter().all(|axis| {
|
||||
let ax = at.0.get(*axis).copied().unwrap_or(0.0);
|
||||
let ay = at.0.get(axis + 1).copied().unwrap_or(0.0);
|
||||
@@ -108,6 +109,8 @@ fn group_layout(lines: &[TextLine], mixed: bool) -> Vec<TextBlock> {
|
||||
(!mixed || !(invisible(prev) || invisible(first)))
|
||||
&& (mixed || same_style(prev, first))
|
||||
&& same_plane(prev, first)
|
||||
// The rewrite sets every row in the first one's horizontal scale.
|
||||
&& (prev.origin.state.scale - first.origin.state.scale).abs() < 1e-6
|
||||
&& aligned
|
||||
&& first.size.min(prev.size) >= first.size.max(prev.size) * 0.4
|
||||
&& gap > first.size.min(prev.size) * 0.8
|
||||
|
||||
Reference in New Issue
Block a user