use super::content_geometry::{box_under, multiply, ClipText, Reach, TextPosition, UserBox};
use super::content_mask::{
mask_strings_comments_and_inline_images, name_operand_before, numeric_operands_before,
show_operand_text_bytes,
};
use super::content_resources::{
colour_space_bound, colour_space_components, components_of_colour_space, decoded_within,
numbers_of, pattern_type, resolve_pattern, resolve_xobject, stream_resources, XObjectDrawn,
};
use super::{
collect_text_chars_before, extract_font_name_before_tf, is_pdf_whitespace,
preceding_operand_closer,
};
use crate::extractor::{visible_page_box, PageBox};
use lopdf::{Document, Object, ObjectId};
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
const COVERING_IMAGE_MIN_PAGE_FRACTION: f64 = 0.5;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct ExecutedContent {
pub(super) text_ops: u32,
pub(super) hidden_text_ops: u32,
pub(super) draws_image: bool,
pub(super) covers_page: bool,
pub(super) shows_only_a_hidden_text_layer: bool,
pub(super) form_bytes: usize,
pub(super) form_bytes_exceeded: bool,
}
pub(super) fn scan_page_content(
doc: &Document,
page_id: ObjectId,
unique_chars: &mut HashSet<u8>,
used_font_names: &mut HashSet<Vec<u8>>,
) -> (ExecutedContent, ContentCounts) {
let (state, counts) = scan_page(doc, page_id, unique_chars, used_font_names);
(state.executed(), counts)
}
pub(super) fn page_executed_content(doc: &Document, page_id: ObjectId) -> ExecutedContent {
scan_page(doc, page_id, &mut HashSet::new(), &mut HashSet::new())
.0
.executed()
}
pub(super) fn scan_content_stream_alone<'a>(
doc: &'a Document,
content: &[u8],
unique_chars: &mut HashSet<u8>,
used_font_names: &mut HashSet<Vec<u8>>,
resources: &[&'a lopdf::Dictionary],
) -> ContentCounts {
let mut state = ContentScanState::new(doc, PageBox::LETTER, false);
state.follow_patterns = false;
scan_content_stream(
content,
unique_chars,
used_font_names,
&mut state,
resources,
)
}
fn resource_chain<'a>(
doc: &'a Document,
own: Option<&'a lopdf::Dictionary>,
ancestors: &[ObjectId],
) -> Vec<&'a lopdf::Dictionary> {
own.into_iter()
.chain(
ancestors
.iter()
.filter_map(|id| doc.get_dictionary(*id).ok()),
)
.collect()
}
fn scan_page<'a>(
doc: &'a Document,
page_id: ObjectId,
unique_chars: &mut HashSet<u8>,
used_font_names: &mut HashSet<Vec<u8>>,
) -> (ContentScanState<'a>, ContentCounts) {
let page_box = visible_page_box(doc, page_id).unwrap_or(PageBox::LETTER);
let page_resources = doc.get_page_resources(page_id).ok();
let resources = page_resources
.as_ref()
.map(|(own, ancestors)| resource_chain(doc, *own, ancestors))
.unwrap_or_default();
let mut state = ContentScanState::new(doc, page_box, true);
let mut counts = ContentCounts::default();
for content_id in doc.get_page_contents(page_id) {
if let Ok(Object::Stream(stream)) = doc.get_object(content_id) {
let content = stream
.decompressed_content()
.unwrap_or_else(|_| stream.content.clone());
counts.add(scan_content_stream(
&content,
unique_chars,
used_font_names,
&mut state,
&resources,
));
}
}
(state, counts)
}
#[derive(Clone, Copy, Default)]
pub(super) struct ContentCounts {
pub(super) text_ops: u32,
pub(super) image_count: u32,
pub(super) path_ops: u32,
pub(super) font_changes: u32,
}
impl ContentCounts {
pub(super) fn add(&mut self, other: ContentCounts) {
self.text_ops += other.text_ops;
self.image_count += other.image_count;
self.path_ops += other.path_ops;
self.font_changes += other.font_changes;
}
}
const SCAN_STATE_MAX_DEPTH: usize = 256;
const FORM_INVOCATIONS_MAX: usize = 1_000;
const EXECUTED_FORM_BYTES_MAX: usize = 32 << 20;
#[cfg(not(test))]
fn executed_form_bytes_budget() -> usize {
EXECUTED_FORM_BYTES_MAX
}
#[cfg(test)]
fn executed_form_bytes_budget() -> usize {
EXECUTED_FORM_BYTES_OVERRIDE
.with(std::cell::Cell::get)
.unwrap_or(EXECUTED_FORM_BYTES_MAX)
}
#[cfg(test)]
thread_local! {
static EXECUTED_FORM_BYTES_OVERRIDE: std::cell::Cell<Option<usize>> =
const { std::cell::Cell::new(None) };
}
const FORM_CONTENT_CACHE_MAX_BYTES: usize = 8 << 20;
const CLIP_TEXT_ENTRIES_MAX: usize = 256;
const IDENTITY: [f64; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
const COVERAGE_GRID: usize = 64;
struct FormContent {
content: Vec<u8>,
masked: Vec<u8>,
}
struct SavedScanState {
render_mode: u8,
ctm: [f64; 6],
clip: UserBox,
fill_paints_image: bool,
stroke_paints_image: bool,
font_size: Option<f64>,
leading: f64,
clip_text: Vec<ClipText>,
}
struct ContentScanState<'a> {
doc: &'a Document,
page: UserBox,
clip: UserBox,
follow_do: bool,
render_mode: u8,
ctm: [f64; 6],
path_box: Option<UserBox>,
clip_pending: bool,
fill_paints_image: bool,
stroke_paints_image: bool,
follow_patterns: bool,
pattern_verdicts: HashMap<(ObjectId, Vec<ObjectId>), bool>,
drew_image: bool,
drew_image_on_page: bool,
clip_text: Vec<ClipText>,
pending_clip_text_ops: u32,
clip_text_ops_open: u32,
clip_text_reach_open: Reach,
font_size: Option<f64>,
leading: f64,
text_position: Option<TextPosition>,
saved: Vec<SavedScanState>,
unsaved_depth: u32,
stack_floor: usize,
executed_text_ops: u32,
executed_hidden_text_ops: u32,
covered_cells: [u64; COVERAGE_GRID],
own_image_area: f64,
active_forms: Vec<ObjectId>,
form_invocations: usize,
executed_form_bytes: usize,
form_bytes_budget: usize,
form_bytes_exceeded: bool,
incomplete: bool,
form_content: HashMap<ObjectId, Rc<FormContent>>,
form_content_bytes: usize,
}
impl<'a> ContentScanState<'a> {
fn new(doc: &'a Document, page_box: PageBox, follow_do: bool) -> Self {
let page = UserBox {
x0: f64::from(page_box.x0),
y0: f64::from(page_box.y0),
x1: f64::from(page_box.x1),
y1: f64::from(page_box.y1),
};
Self {
doc,
page,
clip: page,
follow_do,
render_mode: 0,
ctm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
path_box: None,
clip_pending: false,
fill_paints_image: false,
stroke_paints_image: false,
follow_patterns: true,
pattern_verdicts: HashMap::new(),
drew_image: false,
drew_image_on_page: false,
clip_text: Vec::new(),
pending_clip_text_ops: 0,
clip_text_ops_open: 0,
clip_text_reach_open: Reach::Nowhere,
font_size: None,
leading: 0.0,
text_position: None,
saved: Vec::new(),
unsaved_depth: 0,
stack_floor: 0,
executed_text_ops: 0,
executed_hidden_text_ops: 0,
covered_cells: [0; COVERAGE_GRID],
own_image_area: 0.0,
active_forms: Vec::new(),
form_invocations: 0,
executed_form_bytes: 0,
form_bytes_budget: executed_form_bytes_budget(),
form_bytes_exceeded: false,
incomplete: false,
form_content: HashMap::new(),
form_content_bytes: 0,
}
}
fn save(&mut self) {
if self.saved.len() < SCAN_STATE_MAX_DEPTH {
self.saved.push(SavedScanState {
render_mode: self.render_mode,
ctm: self.ctm,
clip: self.clip,
fill_paints_image: self.fill_paints_image,
stroke_paints_image: self.stroke_paints_image,
font_size: self.font_size,
leading: self.leading,
clip_text: std::mem::take(&mut self.clip_text),
});
} else {
self.unsaved_depth += 1;
self.incomplete = true;
}
}
fn restore(&mut self) {
if self.unsaved_depth > 0 {
self.unsaved_depth -= 1;
} else if self.saved.len() > self.stack_floor {
if let Some(saved) = self.saved.pop() {
let discarded: u32 = self.clip_text.iter().map(|text| text.ops).sum();
self.pending_clip_text_ops -= discarded;
self.render_mode = saved.render_mode;
self.ctm = saved.ctm;
self.clip = saved.clip;
self.fill_paints_image = saved.fill_paints_image;
self.stroke_paints_image = saved.stroke_paints_image;
self.font_size = saved.font_size;
self.leading = saved.leading;
self.clip_text = saved.clip_text;
}
}
}
fn concat(&mut self, matrix: [f64; 6]) {
self.ctm = multiply(matrix, self.ctm);
}
fn transformed_box(&self, corners: [f64; 4]) -> Option<UserBox> {
box_under(self.ctm, corners)
}
fn text_object_began(&mut self) {
self.text_position = None;
}
fn text_moved(&mut self, tx: f64, ty: f64) {
let from = self
.text_position
.map_or(IDENTITY, |position| position.line);
let line = multiply([1.0, 0.0, 0.0, 1.0, tx, ty], from);
self.text_position = Some(TextPosition { matrix: line, line });
}
fn text_matrix_set(&mut self, matrix: [f64; 6]) {
self.text_position = Some(TextPosition {
matrix,
line: matrix,
});
}
fn next_line(&mut self) {
self.text_moved(0.0, -self.leading);
}
fn glyph_box(&self, bytes: usize) -> Option<UserBox> {
let size = self.font_size?.abs();
let position = self.text_position?;
let width = 0.5 * size * bytes as f64;
box_under(
multiply(position.matrix, self.ctm),
[0.0, -0.25 * size, width, size],
)
}
fn pen_advanced(&mut self, bytes: usize) {
let Some(size) = self.font_size else {
return;
};
let Some(position) = self.text_position.as_mut() else {
return;
};
let width = 0.5 * size.abs() * bytes as f64;
let [a, b, ..] = position.matrix;
position.matrix[4] += width * a;
position.matrix[5] += width * b;
}
fn text_shown(&mut self, bytes: usize) {
self.executed_text_ops += 1;
match self.render_mode {
3 => self.executed_hidden_text_ops += 1,
7 => {
self.executed_hidden_text_ops += 1;
self.clip_text_ops_open += 1;
let reach = match self.glyph_box(bytes) {
None => Reach::Anywhere,
Some(glyphs) => glyphs
.intersect(&self.clip)
.map_or(Reach::Nowhere, Reach::Within),
};
self.clip_text_reach_open = self.clip_text_reach_open.join(reach);
}
_ if self.pending_clip_text_ops > 0 => match self.glyph_box(bytes) {
None => self.painted(None),
Some(glyphs) => {
if let Some(landed) = glyphs.intersect(&self.clip) {
self.painted(Some(landed));
}
}
},
_ => {}
}
self.pen_advanced(bytes);
}
fn text_object_ended(&mut self) {
self.text_position = None;
if self.clip_text_ops_open == 0 {
return;
}
let text = ClipText {
ops: self.clip_text_ops_open,
reach: self.clip_text_reach_open,
};
self.clip_text_ops_open = 0;
self.clip_text_reach_open = Reach::Nowhere;
self.pending_clip_text_ops += text.ops;
let full = self.clip_text.len() >= CLIP_TEXT_ENTRIES_MAX;
match self.clip_text.last_mut() {
Some(last) if full => {
last.ops += text.ops;
last.reach = last.reach.join(text.reach);
}
_ => self.clip_text.push(text),
}
}
fn painted(&mut self, landed: Option<UserBox>) {
if self.pending_clip_text_ops == 0 {
return;
}
fn reveal(texts: &mut Vec<ClipText>, landed: Option<UserBox>) -> u32 {
let mut shown = 0;
texts.retain(|text| {
let seen = text.reach.shown_by(landed);
if seen {
shown += text.ops;
}
!seen
});
shown
}
let mut shown = reveal(&mut self.clip_text, landed);
for saved in &mut self.saved {
shown += reveal(&mut saved.clip_text, landed);
}
self.executed_hidden_text_ops -= shown;
self.pending_clip_text_ops -= shown;
}
fn path_point(&mut self, x: f64, y: f64) {
if let Some(point) = self.transformed_box([x, y, x, y]) {
self.path_box = Some(self.path_box.map_or(point, |path| path.union(&point)));
}
}
fn path_rect(&mut self, [x, y, w, h]: [f64; 4]) {
if let Some(rect) = self.transformed_box([x, y, x + w, y + h]) {
self.path_box = Some(self.path_box.map_or(rect, |path| path.union(&rect)));
}
}
fn clip_requested(&mut self) {
self.clip_pending = true;
}
fn path_ended(&mut self) {
if self.clip_pending {
self.clip = match self.path_box.and_then(|path| path.intersect(&self.clip)) {
Some(clip) => clip,
None => UserBox {
x0: self.clip.x0,
y0: self.clip.y0,
x1: self.clip.x0,
y1: self.clip.y0,
},
};
self.clip_pending = false;
}
self.path_box = None;
}
fn path_painted_with_image(&mut self) {
let Some(painted) = self.path_box.and_then(|path| path.intersect(&self.clip)) else {
return;
};
self.drew_image = true;
self.drew_image_on_page = true;
self.own_image_area += painted.area();
self.mark_cells(&painted);
}
fn clip_is_open(&self) -> bool {
self.clip.area() > 0.0
}
fn path_landed(&self) -> Option<UserBox> {
if !self.clip_is_open() {
return None;
}
self.path_box?.clamped(&self.clip)
}
fn pattern_paints_image(&mut self, name: &[u8], resources: &[&'a lopdf::Dictionary]) -> bool {
if !self.follow_patterns {
return false;
}
let Some((id, pattern)) = resolve_pattern(self.doc, resources, name) else {
return false;
};
let scope = (id, self.active_forms.clone());
if let Some(&verdict) = self.pattern_verdicts.get(&scope) {
return verdict;
}
if self.form_invocations >= FORM_INVOCATIONS_MAX {
self.incomplete = true;
return false;
}
self.form_invocations += 1;
let verdict = match pattern {
Object::Stream(cell) if pattern_type(self.doc, &cell.dict) == Some(1) => {
let Some(content) = self.form_bytes_admitted(cell) else {
return false;
};
let mut cell_resources = Vec::with_capacity(resources.len() + 1);
cell_resources.extend(stream_resources(self.doc, cell));
cell_resources.extend_from_slice(resources);
let mut cell_state = ContentScanState::new(self.doc, PageBox::LETTER, true);
cell_state.follow_patterns = false;
cell_state.executed_form_bytes = self.executed_form_bytes;
cell_state.form_bytes_budget = self.form_bytes_budget;
scan_content_stream(
&content,
&mut HashSet::new(),
&mut HashSet::new(),
&mut cell_state,
&cell_resources,
);
self.form_invocations += cell_state.form_invocations;
self.executed_form_bytes = cell_state.executed_form_bytes;
self.form_bytes_exceeded |= cell_state.form_bytes_exceeded;
self.incomplete |= cell_state.incomplete;
cell_state.drew_image
}
_ => false,
};
self.pattern_verdicts.insert(scope, verdict);
verdict
}
fn image_drawn(&mut self) {
self.drew_image = true;
let Some(drawn) = self.transformed_box([0.0, 0.0, 1.0, 1.0]) else {
return;
};
let Some(on_page) = drawn.intersect(&self.clip) else {
return;
};
self.painted(Some(on_page));
self.drew_image_on_page = true;
let [a, b, c, d, _, _] = self.ctm;
self.own_image_area += (a * d - b * c).abs().min(on_page.area());
self.mark_cells(&on_page);
}
fn mark_cells(&mut self, on_page: &UserBox) {
let cells = COVERAGE_GRID as f64;
let cell_w = (self.page.x1 - self.page.x0) / cells;
let cell_h = (self.page.y1 - self.page.y0) / cells;
let first_col = ((on_page.x0 - self.page.x0) / cell_w - 0.5).ceil().max(0.0);
let last_col = ((on_page.x1 - self.page.x0) / cell_w - 0.5)
.floor()
.min(cells - 1.0);
let first_row = ((on_page.y0 - self.page.y0) / cell_h - 0.5).ceil().max(0.0);
let last_row = ((on_page.y1 - self.page.y0) / cell_h - 0.5)
.floor()
.min(cells - 1.0);
if last_col < first_col || last_row < first_row {
return;
}
let (first_col, last_col) = (first_col as usize, last_col as usize);
let width = last_col - first_col + 1;
let mask = if width >= COVERAGE_GRID {
u64::MAX
} else {
((1u64 << width) - 1) << first_col
};
for row in first_row as usize..=last_row as usize {
self.covered_cells[row] |= mask;
}
}
fn form_drawn(
&mut self,
id: ObjectId,
form: &'a lopdf::Stream,
invoker_resources: &[&'a lopdf::Dictionary],
) {
if self.active_forms.contains(&id) {
return;
}
if self.form_invocations >= FORM_INVOCATIONS_MAX || self.saved.len() >= SCAN_STATE_MAX_DEPTH
{
self.incomplete = true;
return;
}
self.form_invocations += 1;
let outer_floor = self.stack_floor;
self.save();
let base = self.saved.len();
self.stack_floor = base;
if let Some(matrix) = numbers_of(self.doc, &form.dict, b"Matrix") {
self.concat(matrix);
}
let clip = match numbers_of(self.doc, &form.dict, b"BBox") {
Some(bbox) => self
.transformed_box(bbox)
.and_then(|drawn| drawn.intersect(&self.clip)),
None => Some(self.clip),
};
if let Some(clip) = clip {
self.clip = clip;
let mut resources = Vec::with_capacity(invoker_resources.len() + 1);
resources.extend(stream_resources(self.doc, form));
resources.extend_from_slice(invoker_resources);
if let Some(content) = self.form_content(id, form, &resources) {
self.active_forms.push(id);
scan_masked_content(
&content.content,
&content.masked,
&mut HashSet::new(),
&mut HashSet::new(),
self,
&resources,
);
self.active_forms.pop();
}
}
while self.unsaved_depth > 0 || self.saved.len() > base {
self.restore();
}
self.stack_floor = outer_floor;
self.restore();
}
fn form_content(
&mut self,
id: ObjectId,
form: &lopdf::Stream,
resources: &[&'a lopdf::Dictionary],
) -> Option<Rc<FormContent>> {
if let Some(content) = self.form_content.get(&id) {
let content = Rc::clone(content);
return self
.form_bytes_charged(content.content.len())
.then_some(content);
}
let content = self.form_bytes_admitted(form)?;
let doc = self.doc;
let own = stream_resources(doc, form);
let asked_the_invoker = Cell::new(false);
let masked = mask_strings_comments_and_inline_images(&content, &|name| {
match own.and_then(|scope| colour_space_bound(doc, scope, name)) {
Some(space) => components_of_colour_space(doc, space, 0),
None => {
asked_the_invoker.set(true);
colour_space_components(doc, resources, name)
}
}
});
let content = Rc::new(FormContent { content, masked });
let bytes = content.content.len() + content.masked.len();
if !asked_the_invoker.get()
&& self.form_content_bytes + bytes <= FORM_CONTENT_CACHE_MAX_BYTES
{
self.form_content_bytes += bytes;
self.form_content.insert(id, Rc::clone(&content));
}
Some(content)
}
fn form_bytes_admitted(&mut self, stream: &lopdf::Stream) -> Option<Vec<u8>> {
if self.form_bytes_exceeded {
self.form_bytes_refused();
return None;
}
let limit = self
.form_bytes_budget
.saturating_sub(self.executed_form_bytes)
.min(FORM_CONTENT_CACHE_MAX_BYTES);
let Some(content) = decoded_within(stream, limit) else {
self.form_bytes_refused();
return None;
};
self.executed_form_bytes += content.len();
Some(content)
}
fn form_bytes_charged(&mut self, bytes: usize) -> bool {
if self.form_bytes_exceeded || self.executed_form_bytes + bytes > self.form_bytes_budget {
self.form_bytes_refused();
return false;
}
self.executed_form_bytes += bytes;
true
}
fn form_bytes_refused(&mut self) {
self.form_bytes_exceeded = true;
self.incomplete = true;
}
fn covered_image_area(&self) -> f64 {
let cells: u32 = self.covered_cells.iter().map(|row| row.count_ones()).sum();
let cell_area = self.page.area() / (COVERAGE_GRID * COVERAGE_GRID) as f64;
(f64::from(cells) * cell_area).min(self.own_image_area)
}
fn covers_page(&self) -> bool {
self.covered_image_area() >= COVERING_IMAGE_MIN_PAGE_FRACTION * self.page.area()
}
fn shows_only_a_hidden_text_layer(&self) -> bool {
!self.incomplete
&& self.executed_text_ops > 0
&& self.executed_hidden_text_ops == self.executed_text_ops
&& self.covers_page()
}
fn executed(&self) -> ExecutedContent {
ExecutedContent {
text_ops: self.executed_text_ops,
hidden_text_ops: self.executed_hidden_text_ops,
draws_image: self.drew_image_on_page,
covers_page: self.covers_page(),
shows_only_a_hidden_text_layer: self.shows_only_a_hidden_text_layer(),
form_bytes: self.executed_form_bytes,
form_bytes_exceeded: self.form_bytes_exceeded,
}
}
}
fn scan_content_stream<'a>(
content: &[u8],
unique_chars: &mut HashSet<u8>,
used_font_names: &mut HashSet<Vec<u8>>,
state: &mut ContentScanState<'a>,
resources: &[&'a lopdf::Dictionary],
) -> ContentCounts {
let doc = state.doc;
let masked = mask_strings_comments_and_inline_images(content, &|name| {
colour_space_components(doc, resources, name)
});
scan_masked_content(
content,
&masked,
unique_chars,
used_font_names,
state,
resources,
)
}
fn scan_masked_content<'a>(
content: &[u8],
masked: &[u8],
unique_chars: &mut HashSet<u8>,
used_font_names: &mut HashSet<Vec<u8>>,
state: &mut ContentScanState<'a>,
resources: &[&'a lopdf::Dictionary],
) -> ContentCounts {
let mut counts = ContentCounts::default();
let ops: &[u8] = masked;
let is_word_start = |pos: usize| -> bool { pos == 0 || is_pdf_whitespace(ops[pos - 1]) };
let is_token_start = |pos: usize| -> bool {
pos == 0 || is_pdf_whitespace(ops[pos - 1]) || matches!(ops[pos - 1], b')' | b']' | b'>')
};
let is_token_end = |pos: usize| -> bool {
pos + 1 >= ops.len()
|| is_pdf_whitespace(ops[pos + 1])
|| matches!(ops[pos + 1], b'/' | b'[' | b'(' | b'<' | b'%')
};
let token_at = |pos: usize, token: &[u8]| -> bool {
ops[pos..].starts_with(token) && is_token_start(pos) && is_token_end(pos + token.len() - 1)
};
let mut operand_floor = 0usize;
let mut i = 0;
while i < ops.len() {
let b = ops[i];
if b == b'T' && i + 1 < ops.len() {
let next = ops[i + 1];
if (next == b'j' || next == b'J')
&& is_token_end(i + 1)
&& preceding_operand_closer(ops, i, operand_floor)
{
let bytes = show_operand_text_bytes(ops, content, i, operand_floor);
if bytes > 0 {
counts.text_ops += 1;
state.text_shown(bytes);
collect_text_chars_before(content, i, unique_chars, operand_floor);
}
operand_floor = i;
} else if next == b'f' && is_token_end(i + 1) {
if let Some([size]) = numeric_operands_before::<1>(ops, i, operand_floor) {
state.font_size = Some(size);
}
if let Some(name) = extract_font_name_before_tf(ops, i, operand_floor) {
used_font_names.insert(name);
counts.font_changes += 1;
operand_floor = i;
}
} else if next == b'r' && is_token_start(i) && is_token_end(i + 1) {
if let Some([mode]) = numeric_operands_before::<1>(ops, i, operand_floor) {
if mode.fract() == 0.0 && (0.0..=7.0).contains(&mode) {
state.render_mode = mode as u8;
}
operand_floor = i;
}
} else if (next == b'd' || next == b'D') && is_token_start(i) && is_token_end(i + 1) {
if let Some([tx, ty]) = numeric_operands_before::<2>(ops, i, operand_floor) {
if next == b'D' {
state.leading = -ty;
}
state.text_moved(tx, ty);
operand_floor = i;
}
} else if next == b'm' && is_token_start(i) && is_token_end(i + 1) {
if let Some(matrix) = numeric_operands_before::<6>(ops, i, operand_floor) {
state.text_matrix_set(matrix);
operand_floor = i;
}
} else if next == b'*' && is_token_start(i) && is_token_end(i + 1) {
state.next_line();
} else if next == b'L' && is_token_start(i) && is_token_end(i + 1) {
if let Some([leading]) = numeric_operands_before::<1>(ops, i, operand_floor) {
state.leading = leading;
operand_floor = i;
}
}
} else if (b == b'\'' || b == b'"')
&& is_token_start(i)
&& is_token_end(i)
&& preceding_operand_closer(ops, i, operand_floor)
{
state.next_line();
let bytes = show_operand_text_bytes(ops, content, i, operand_floor);
if bytes > 0 {
counts.text_ops += 1;
state.text_shown(bytes);
collect_text_chars_before(content, i, unique_chars, operand_floor);
}
operand_floor = i;
} else if matches!(
b,
b'c' | b'D'
| b's'
| b'B'
| b'E'
| b'S'
| b'g'
| b'r'
| b'k'
| b'C'
| b'G'
| b'R'
| b'K'
| b'q'
| b'Q'
) {
if token_at(i, b"cm") {
if let Some(matrix) = numeric_operands_before::<6>(ops, i, operand_floor) {
state.concat(matrix);
operand_floor = i;
}
} else if token_at(i, b"Do") {
if let Some(name) = name_operand_before(ops, i, operand_floor) {
operand_floor = i;
if state.follow_do {
match resolve_xobject(state.doc, resources, &name) {
Some(XObjectDrawn::Image) => state.image_drawn(),
Some(XObjectDrawn::Form(id, form)) => {
state.form_drawn(id, form, resources)
}
None => {}
}
}
}
} else if token_at(i, b"sh") {
if state.clip_is_open() {
state.painted(Some(state.clip));
}
} else if token_at(i, b"BI") {
if state.follow_do {
counts.image_count += 1;
state.image_drawn();
}
} else if token_at(i, b"BT") {
state.text_object_began();
} else if token_at(i, b"ET") {
state.text_object_ended();
} else if token_at(i, b"scn") || token_at(i, b"sc") {
let paints_image = name_operand_before(ops, i, operand_floor)
.is_some_and(|name| state.pattern_paints_image(&name, resources));
state.fill_paints_image = paints_image;
operand_floor = i;
} else if token_at(i, b"SCN") || token_at(i, b"SC") {
let paints_image = name_operand_before(ops, i, operand_floor)
.is_some_and(|name| state.pattern_paints_image(&name, resources));
state.stroke_paints_image = paints_image;
operand_floor = i;
} else if token_at(i, b"cs")
|| token_at(i, b"g")
|| token_at(i, b"rg")
|| token_at(i, b"k")
{
state.fill_paints_image = false;
} else if token_at(i, b"CS")
|| token_at(i, b"G")
|| token_at(i, b"RG")
|| token_at(i, b"K")
{
state.stroke_paints_image = false;
} else if token_at(i, b"q") {
state.save();
} else if token_at(i, b"Q") {
state.restore();
}
}
let mut painted = false;
let mut path_ended = false;
let mut fills = false;
let mut strokes = false;
match b {
b'm' | b'l' if is_word_start(i) && is_token_end(i) => {
counts.path_ops += 1;
if let Some([x, y]) = numeric_operands_before::<2>(ops, i, operand_floor) {
state.path_point(x, y);
}
}
b'c' if is_word_start(i) && is_token_end(i) => {
counts.path_ops += 1;
if let Some([x1, y1, x2, y2, x3, y3]) =
numeric_operands_before::<6>(ops, i, operand_floor)
{
state.path_point(x1, y1);
state.path_point(x2, y2);
state.path_point(x3, y3);
}
}
b'v' | b'y' if is_word_start(i) && is_token_end(i) => {
if let Some([x1, y1, x2, y2]) = numeric_operands_before::<4>(ops, i, operand_floor)
{
state.path_point(x1, y1);
state.path_point(x2, y2);
}
}
b'h' if is_word_start(i) && is_token_end(i) => {
counts.path_ops += 1;
}
b'f' | b'F' if is_word_start(i) && is_token_end(i) => {
counts.path_ops += 1;
painted = true;
path_ended = true;
fills = true;
}
b'S' | b's' if is_word_start(i) && is_token_end(i) => {
counts.path_ops += 1;
painted = true;
path_ended = true;
strokes = true;
}
b'B' if is_word_start(i) && is_token_end(i) => {
counts.path_ops += 1;
painted = true;
path_ended = true;
fills = true;
strokes = true;
}
b'b' if is_word_start(i) && is_token_end(i) => {
painted = true;
path_ended = true;
fills = true;
strokes = true;
}
b'n' if is_word_start(i) && is_token_end(i) => {
path_ended = true;
}
b'W' if is_word_start(i) && is_token_end(i) => {
state.clip_requested();
}
b'r' if ops.get(i + 1) == Some(&b'e') && is_word_start(i) && is_token_end(i + 1) => {
counts.path_ops += 1;
if let Some(rect) = numeric_operands_before::<4>(ops, i, operand_floor) {
state.path_rect(rect);
}
}
b'f' if ops.get(i + 1) == Some(&b'*') && is_word_start(i) && is_token_end(i + 1) => {
counts.path_ops += 1;
painted = true;
path_ended = true;
fills = true;
}
b'B' | b'b'
if ops.get(i + 1) == Some(&b'*') && is_word_start(i) && is_token_end(i + 1) =>
{
painted = true;
path_ended = true;
fills = true;
strokes = true;
}
b'W' if ops.get(i + 1) == Some(&b'*') && is_word_start(i) && is_token_end(i + 1) => {
state.clip_requested();
}
_ => {}
}
let landed = if painted { state.path_landed() } else { None };
if let Some(landed) = landed {
state.painted(Some(landed));
}
if (fills && state.fill_paints_image) || (strokes && state.stroke_paints_image) {
state.path_painted_with_image();
}
if path_ended {
state.path_ended();
}
i += 1;
}
counts
}
#[cfg(test)]
#[path = "content_scan_budget_tests.rs"]
mod budget_tests;
#[cfg(test)]
#[path = "content_scan_clip_tests.rs"]
mod clip_tests;
#[cfg(test)]
#[path = "content_scan_fixtures.rs"]
mod fixtures;
#[cfg(test)]
#[path = "content_scan_tests.rs"]
mod tests;