use crate::FillRule;
use crate::color::ColorSpace;
use crate::context::Context;
use crate::convert::{convert_line_cap, convert_line_join};
use crate::device::Device;
use crate::font::{Font, FontData, FontQuery, StandardFont};
use crate::interpret::path::{
close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path,
};
use crate::interpret::state::{TextStateFont, handle_gs};
use crate::interpret::text::TextRenderingMode;
use crate::pattern::{Pattern, ShadingPattern};
use crate::shading::Shading;
use crate::util::{OptionLog, RectExt, decode_text_string};
use crate::x_object::{
FormXObject, ImageXObject, XObject, draw_form_xobject, draw_image_xobject, draw_xobject,
};
use kurbo::{Affine, Point, Shape};
use log::warn;
use pdf_syntax::content::ops::TypedInstruction;
use pdf_syntax::object::dict::keys::{
ACTUAL_TEXT, ANNOTS, AP, AS, BTN, CH, DV, F, FF, FT, KIDS, MCID, N, OC, OPT, PARENT, RECT,
SUBTYPE, TX, V, WIDGET,
};
use pdf_syntax::object::{
Array, Dict, Name, Object, Rect, Stream, String as PdfString, dict_or_stream,
};
use pdf_syntax::page::{Page, Resources};
use smallvec::smallvec;
use std::sync::{Arc, OnceLock};
pub(crate) mod path;
pub(crate) mod state;
pub(crate) mod text;
pub use state::ActiveTransferFunction;
pub type FontResolverFn = Arc<dyn Fn(&FontQuery) -> Option<(FontData, u32)> + Send + Sync>;
pub type CMapResolverFn =
Arc<dyn Fn(pdf_font::cmap::CMapName<'_>) -> Option<&'static [u8]> + Send + Sync>;
pub type WarningSinkFn = Arc<dyn Fn(InterpreterWarning) + Send + Sync>;
#[derive(Clone)]
pub struct InterpreterSettings {
pub font_resolver: FontResolverFn,
pub cmap_resolver: CMapResolverFn,
pub warning_sink: WarningSinkFn,
pub render_annotations: bool,
pub skip_signature_widgets: bool,
pub max_operator_count: Option<u64>,
pub shared_cache: Option<crate::Cache>,
}
#[cfg(feature = "embed-fonts")]
const CJK_FONT_CANDIDATE_PATHS: &[&str] = &[
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/arphic/uming.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
];
#[cfg(feature = "embed-fonts")]
static SYSTEM_CJK_FONT: OnceLock<Option<Arc<Vec<u8>>>> = OnceLock::new();
#[cfg(feature = "embed-fonts")]
fn system_cjk_font() -> Option<FontData> {
SYSTEM_CJK_FONT
.get_or_init(|| {
for path in CJK_FONT_CANDIDATE_PATHS {
if let Ok(bytes) = std::fs::read(path) {
log::debug!("CJK fallback font loaded from {path}");
return Some(Arc::new(bytes));
}
}
log::warn!(
"no system CJK font found; non-embedded CJK fonts will render with a Latin fallback"
);
None
})
.as_ref()
.map(|data| -> FontData { data.clone() })
}
impl Default for InterpreterSettings {
fn default() -> Self {
Self {
#[cfg(not(feature = "embed-fonts"))]
font_resolver: Arc::new(|_| None),
#[cfg(feature = "embed-fonts")]
font_resolver: Arc::new(|query| match query {
FontQuery::Standard(s) => Some(s.get_font_data()),
FontQuery::Fallback(f) => {
if f.character_collection
.as_ref()
.is_some_and(|cc| cc.family.is_cjk())
&& let Some(data) = system_cjk_font()
{
return Some((data, 0));
}
Some(f.pick_standard_font().get_font_data())
}
}),
#[cfg(feature = "embed-cmaps")]
cmap_resolver: Arc::new(pdf_font::cmap::load_embedded),
#[cfg(not(feature = "embed-cmaps"))]
cmap_resolver: Arc::new(|_| None),
warning_sink: Arc::new(|_| {}),
render_annotations: true,
skip_signature_widgets: true,
max_operator_count: None,
shared_cache: None,
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum InterpreterWarning {
UnsupportedFont,
ImageDecodeFailure,
StreamTooLarge {
observed: u64,
limit: u64,
},
NestingTooDeep {
limit: u32,
},
}
fn normal_appearance_stream<'a>(annot: &Dict<'a>) -> Option<Stream<'a>> {
let ap = annot.get::<Dict<'_>>(AP)?;
if let Some(stream) = ap.get::<Stream<'_>>(N) {
return Some(stream);
}
let states = ap.get::<Dict<'_>>(N)?;
if let Some(state) = annot.get::<Name>(AS) {
return states.get::<Stream<'_>>(state.as_ref());
}
let candidates = [
annot.get::<Name>(V),
annot.get::<Dict<'_>>(PARENT).and_then(|p| p.get::<Name>(V)),
];
candidates
.into_iter()
.flatten()
.find_map(|state| states.get::<Stream<'_>>(state.as_ref()))
}
const ANNOT_FLAG_HIDDEN: u32 = 1 << 1;
const ANNOT_FLAG_NOVIEW: u32 = 1 << 5;
const FIELD_INHERITANCE_DEPTH: usize = 32;
const FIELD_FLAG_PUSHBUTTON: u32 = 1 << 16;
const FIELD_FLAG_PASSWORD: u32 = 1 << 13;
fn inherited_field_entry<'a, T>(
annot: &Dict<'a>,
entry: impl Fn(&Dict<'a>) -> Option<T>,
) -> Option<T> {
let mut node = annot.clone();
for _ in 0..FIELD_INHERITANCE_DEPTH {
if let Some(value) = entry(&node) {
return Some(value);
}
node = node.get::<Dict<'a>>(PARENT)?;
}
None
}
fn name_as_text(name: &Name) -> Option<String> {
std::str::from_utf8(name.as_ref()).ok().map(str::to_string)
}
fn choice_option_label(annot: &Dict<'_>, value: &str) -> String {
let Some(options) = inherited_field_entry(annot, |d| d.get::<Array<'_>>(OPT)) else {
return value.to_string();
};
for option in options.iter::<Array<'_>>() {
let mut pair = option.iter::<PdfString>();
let Some(export) = pair.next().and_then(|s| decode_text_string(s.as_bytes())) else {
continue;
};
let Some(display) = pair.next().and_then(|s| decode_text_string(s.as_bytes())) else {
continue;
};
if export == value {
return display;
}
}
value.to_string()
}
fn choice_value_text<'a>(annot: &Dict<'a>, key: &'static [u8]) -> Option<String> {
if let Some(value) = inherited_field_entry(annot, |d| d.get::<PdfString>(key)) {
let value = decode_text_string(value.as_bytes())?;
return Some(choice_option_label(annot, &value));
}
let options = inherited_field_entry(annot, |d| d.get::<Array<'a>>(key))?;
let lines: Vec<String> = options
.iter::<PdfString>()
.filter_map(|option| decode_text_string(option.as_bytes()))
.map(|value| choice_option_label(annot, &value))
.collect();
(!lines.is_empty()).then(|| lines.join("\n"))
}
fn is_the_only_widget_of_its_field(annot: &Dict<'_>) -> bool {
let Some(parent) = annot.get::<Dict<'_>>(PARENT) else {
return true;
};
match parent.get::<Array<'_>>(KIDS) {
Some(kids) => kids.raw_iter().take(2).count() < 2,
None => true,
}
}
fn field_value_text(annot: &Dict<'_>) -> Option<String> {
let field_type = inherited_field_entry(annot, |d| d.get::<Name>(FT))?;
let flags = inherited_field_entry(annot, |d| d.get::<u32>(FF)).unwrap_or(0);
match field_type.as_ref() {
BTN => {
if flags & FIELD_FLAG_PUSHBUTTON != 0 {
return None;
}
let state = annot.get::<Name>(AS).or_else(|| {
is_the_only_widget_of_its_field(annot)
.then(|| {
inherited_field_entry(annot, |d| d.get::<Name>(V))
.or_else(|| inherited_field_entry(annot, |d| d.get::<Name>(DV)))
})
.flatten()
})?;
if state.as_ref() == b"Off" {
return None;
}
name_as_text(&state)
}
TX => {
if flags & FIELD_FLAG_PASSWORD != 0 {
return None;
}
let value = inherited_field_entry(annot, |d| d.get::<PdfString>(V))
.or_else(|| inherited_field_entry(annot, |d| d.get::<PdfString>(DV)))?;
let text = decode_text_string(value.as_bytes())?;
(!text.trim().is_empty()).then_some(text)
}
CH => choice_value_text(annot, V).or_else(|| choice_value_text(annot, DV)),
_ => None,
}
}
pub fn interpret_page<'a>(
page: &Page<'a>,
context: &mut Context<'a>,
device: &mut impl Device<'a>,
) {
let resources = page.resources();
interpret(page.typed_operations(), resources, context, device);
if context.settings.render_annotations
&& let Some(annot_arr) = page.raw().get::<Array<'_>>(ANNOTS)
{
for annot in annot_arr.iter::<Dict<'_>>() {
let flags = annot.get::<u32>(F).unwrap_or(0);
if flags & ANNOT_FLAG_HIDDEN != 0 {
continue;
}
if context.settings.skip_signature_widgets
&& annot
.get::<Name>(FT)
.as_deref()
.is_some_and(|n| n == b"Sig")
{
continue;
}
if let Some(apx) = normal_appearance_stream(&annot)
.and_then(|o| FormXObject::new(&o, &context.settings.warning_sink))
{
let Some(rect) = annot.get::<Rect>(RECT) else {
continue;
};
let annot_rect = rect.to_kurbo();
let transformed_rect = (apx.matrix
* kurbo::Rect::new(
apx.bbox[0] as f64,
apx.bbox[1] as f64,
apx.bbox[2] as f64,
apx.bbox[3] as f64,
)
.to_path(0.1))
.bounding_box();
let (tw, th) = (transformed_rect.width(), transformed_rect.height());
if !(tw.is_finite() && tw > 0.0 && th.is_finite() && th > 0.0) {
continue;
}
let affine = Affine::new([
annot_rect.width() / transformed_rect.width(),
0.0,
0.0,
annot_rect.height() / transformed_rect.height(),
annot_rect.x0 - transformed_rect.x0,
annot_rect.y0 - transformed_rect.y0,
]);
context.save_state();
context.pre_concat_affine(affine);
context.push_root_transform();
draw_form_xobject(resources, &apx, context, device);
context.pop_root_transform();
context.restore_state(device);
} else if annot.get::<Name>(SUBTYPE).as_deref() == Some(WIDGET)
&& flags & ANNOT_FLAG_NOVIEW == 0
&& let Some(text) = field_value_text(&annot)
&& let Some(rect) = annot.get::<Rect>(RECT)
{
device.draw_placed_text(
&text,
context.get().ctm.transform_rect_bbox(rect.to_kurbo()),
);
}
}
}
}
pub fn interpret<'a, 'b>(
ops: impl Iterator<Item = TypedInstruction<'b>>,
resources: &Resources<'a>,
context: &mut Context<'a>,
device: &mut impl Device<'a>,
) {
if !context.begin_nested_interpretation() {
let limit = crate::context::MAX_NESTED_INTERPRETATION_DEPTH;
warn!("content stream nesting exceeds {limit}, stopping interpretation");
(context.settings.warning_sink)(InterpreterWarning::NestingTooDeep { limit });
return;
}
let num_states = context.num_states();
let max_operator_count = context.settings.max_operator_count.unwrap_or(u64::MAX);
let mut operator_count = 0_u64;
context.save_state();
for op in ops {
operator_count = operator_count.saturating_add(1);
if operator_count > max_operator_count {
warn!(
"content stream operator count exceeds {max_operator_count}, stopping interpretation"
);
break;
}
match op {
TypedInstruction::SaveState(_) => context.save_state(),
TypedInstruction::StrokeColorDeviceRgb(s) => {
context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
context.get_mut().graphics_state.stroke_color =
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
}
TypedInstruction::StrokeColorDeviceGray(s) => {
context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
}
TypedInstruction::StrokeColorCmyk(s) => {
context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
context.get_mut().graphics_state.stroke_color =
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
}
TypedInstruction::LineWidth(w) => {
context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
}
TypedInstruction::LineCap(c) => {
context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
}
TypedInstruction::LineJoin(j) => {
context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
}
TypedInstruction::MiterLimit(l) => {
context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
}
TypedInstruction::Transform(t) => {
context.pre_concat_transform(t);
}
TypedInstruction::RectPath(r) => {
let rect = kurbo::Rect::new(
r.0.as_f64(),
r.1.as_f64(),
r.0.as_f64() + r.2.as_f64(),
r.1.as_f64() + r.3.as_f64(),
)
.to_path(0.1);
context.path_mut().extend(rect);
}
TypedInstruction::MoveTo(m) => {
let p = Point::new(m.0.as_f64(), m.1.as_f64());
*(context.last_point_mut()) = p;
*(context.sub_path_start_mut()) = p;
context.path_mut().move_to(p);
}
TypedInstruction::FillPathEvenOdd(_) => {
fill_path(context, device, FillRule::EvenOdd);
}
TypedInstruction::FillPathNonZero(_) => {
fill_path(context, device, FillRule::NonZero);
}
TypedInstruction::FillPathNonZeroCompatibility(_) => {
fill_path(context, device, FillRule::NonZero);
}
TypedInstruction::FillAndStrokeEvenOdd(_) => {
fill_stroke_path(context, device, FillRule::EvenOdd);
}
TypedInstruction::FillAndStrokeNonZero(_) => {
fill_stroke_path(context, device, FillRule::NonZero);
}
TypedInstruction::CloseAndStrokePath(_) => {
close_path(context);
stroke_path(context, device);
}
TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
close_path(context);
fill_stroke_path(context, device, FillRule::EvenOdd);
}
TypedInstruction::CloseFillAndStrokeNonZero(_) => {
close_path(context);
fill_stroke_path(context, device, FillRule::NonZero);
}
TypedInstruction::NonStrokeColorDeviceGray(s) => {
context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
}
TypedInstruction::NonStrokeColorDeviceRgb(s) => {
context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
context.get_mut().graphics_state.non_stroke_color =
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
}
TypedInstruction::NonStrokeColorCmyk(s) => {
context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
context.get_mut().graphics_state.non_stroke_color =
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
}
TypedInstruction::LineTo(m) => {
if !context.path().elements().is_empty() {
let last_point = *context.last_point();
let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
*(context.last_point_mut()) = p;
if last_point == p {
p.x += 0.0001;
}
context.path_mut().line_to(p);
}
}
TypedInstruction::CubicTo(c) => {
if !context.path().elements().is_empty() {
let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
*(context.last_point_mut()) = p3;
context.path_mut().curve_to(p1, p2, p3);
}
}
TypedInstruction::CubicStartTo(c) => {
if !context.path().elements().is_empty() {
let p1 = *context.last_point();
let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
*(context.last_point_mut()) = p3;
context.path_mut().curve_to(p1, p2, p3);
}
}
TypedInstruction::CubicEndTo(c) => {
if !context.path().elements().is_empty() {
let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
*(context.last_point_mut()) = p3;
context.path_mut().curve_to(p2, p3, p3);
}
}
TypedInstruction::ClosePath(_) => {
close_path(context);
}
TypedInstruction::SetGraphicsState(gs) => {
if let Some(gs) = resources
.get_ext_g_state(gs.0.clone())
.warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
{
handle_gs(&gs, context, resources);
}
}
TypedInstruction::StrokePath(_) => {
stroke_path(context, device);
}
TypedInstruction::EndPath(_) => {
if let Some(clip) = *context.clip()
&& !context.path().elements().is_empty()
{
let clip_path = context.get().ctm * context.path().clone();
context.push_clip_path(clip_path, clip, device);
*(context.clip_mut()) = None;
}
context.path_mut().truncate(0);
}
TypedInstruction::NonStrokeColor(c) => {
let fill_c = &mut context.get_mut().graphics_state.non_stroke_color;
fill_c.truncate(0);
for e in c.0 {
fill_c.push(e.as_f32());
}
}
TypedInstruction::StrokeColor(c) => {
let stroke_c = &mut context.get_mut().graphics_state.stroke_color;
stroke_c.truncate(0);
for e in c.0 {
stroke_c.push(e.as_f32());
}
}
TypedInstruction::ClipNonZero(_) => {
*(context.clip_mut()) = Some(FillRule::NonZero);
}
TypedInstruction::ClipEvenOdd(_) => {
*(context.clip_mut()) = Some(FillRule::EvenOdd);
}
TypedInstruction::RestoreState(_) => context.restore_state(device),
TypedInstruction::FlatnessTolerance(_) => {
}
TypedInstruction::ColorSpaceStroke(c) => {
let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
named
} else {
context
.get_color_space(resources, c.0)
.unwrap_or(ColorSpace::device_gray())
};
context.get_mut().graphics_state.stroke_color = cs.initial_color();
context.get_mut().graphics_state.stroke_cs = cs;
}
TypedInstruction::ColorSpaceNonStroke(c) => {
let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
named
} else {
context
.get_color_space(resources, c.0)
.unwrap_or(ColorSpace::device_gray())
};
context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
context.get_mut().graphics_state.none_stroke_cs = cs;
}
TypedInstruction::DashPattern(p) => {
context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
context.get_mut().graphics_state.stroke_props.dash_array =
p.0.iter::<f32>()
.map(|n| if n == 0.0 { 0.01 } else { n })
.collect();
}
TypedInstruction::RenderingIntent(_) => {
}
TypedInstruction::NonStrokeColorNamed(n) => {
context.get_mut().graphics_state.non_stroke_color =
n.0.into_iter().map(|n| n.as_f32()).collect();
context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
resources
.get_pattern(name)
.and_then(|d| Pattern::new(d, context, resources))
});
}
TypedInstruction::StrokeColorNamed(n) => {
context.get_mut().graphics_state.stroke_color =
n.0.into_iter().map(|n| n.as_f32()).collect();
context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
resources
.get_pattern(name)
.and_then(|d| Pattern::new(d, context, resources))
});
}
TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
let props = bdc
.1
.clone()
.into_name()
.and_then(|name| resources.properties.get::<Dict<'_>>(name))
.or_else(|| dict_or_stream(&bdc.1).map(|(props, _)| props));
let mcid = props.as_ref().and_then(|p| p.get::<i32>(MCID));
let actual_text = props
.as_ref()
.and_then(|p| p.get::<PdfString>(ACTUAL_TEXT))
.and_then(|s| decode_text_string(s.as_bytes()));
let oc = bdc
.1
.clone()
.into_name()
.and_then(|name| {
let r = resources.properties.get_ref(name.clone())?;
let d = resources
.properties
.get::<Dict<'_>>(name)
.unwrap_or_default();
Some((d, r))
})
.or_else(|| {
let (props, _) = dict_or_stream(&bdc.1)?;
let r = props.get_ref(OC)?;
let d = props.get::<Dict<'_>>(OC).unwrap_or_default();
Some((d, r))
});
if let Some((dict, oc_ref)) = oc {
context.ocg_state.begin_ocg(&dict, oc_ref.into());
} else {
context.ocg_state.begin_marked_content();
}
device.begin_marked_content(&bdc.0, mcid, actual_text.as_deref());
}
TypedInstruction::MarkedContentPointWithProperties(_) => {}
TypedInstruction::EndMarkedContent(_) => {
context.ocg_state.end_marked_content();
device.end_marked_content();
}
TypedInstruction::MarkedContentPoint(_) => {}
TypedInstruction::BeginMarkedContent(bmc) => {
context.ocg_state.begin_marked_content();
device.begin_marked_content(&bmc.0, None, None);
}
TypedInstruction::BeginText(_) => {
context.get_mut().text_state.text_matrix = Affine::IDENTITY;
context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
}
TypedInstruction::SetTextMatrix(m) => {
let m = Affine::new([
m.0.as_f64(),
m.1.as_f64(),
m.2.as_f64(),
m.3.as_f64(),
m.4.as_f64(),
m.5.as_f64(),
]);
context.get_mut().text_state.text_line_matrix = m;
context.get_mut().text_state.text_matrix = m;
}
TypedInstruction::EndText(_) => {
let has_outline = context
.get()
.text_state
.clip_paths
.segments()
.next()
.is_some();
if has_outline {
let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
context.push_clip_path(clip_path, FillRule::NonZero, device);
}
context.get_mut().text_state.clip_paths.truncate(0);
}
TypedInstruction::TextFont(t) => {
let name = t.0;
let font = if let Some(font_dict) = resources.get_font(name.clone()) {
context.resolve_font(&font_dict)
} else {
Font::new_standard(StandardFont::Helvetica, &context.settings.font_resolver)
.map(TextStateFont::Fallback)
};
context.get_mut().text_state.font_size = t.1.as_f32();
context.get_mut().text_state.font = font;
}
TypedInstruction::ShowText(s) => {
if context.get().text_state.font.is_none() {
context.get_mut().text_state.font = Font::new_standard(
StandardFont::Helvetica,
&context.settings.font_resolver,
)
.map(TextStateFont::Fallback);
}
text::show_text_string(context, device, resources, s.0);
}
TypedInstruction::ShowTexts(s) => {
if context.get().text_state.font.is_none() {
context.get_mut().text_state.font = Font::new_standard(
StandardFont::Helvetica,
&context.settings.font_resolver,
)
.map(TextStateFont::Fallback);
}
for obj in s.0.iter::<Object<'_>>() {
if let Some(adjustment) = obj.clone().into_f32() {
device.text_adjustment(adjustment);
context.get_mut().text_state.apply_adjustment(adjustment);
} else if let Some(text) = obj.into_string() {
text::show_text_string(context, device, resources, text);
}
}
}
TypedInstruction::HorizontalScaling(h) => {
context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
}
TypedInstruction::TextLeading(tl) => {
context.get_mut().text_state.leading = tl.0.as_f32();
}
TypedInstruction::CharacterSpacing(c) => {
context.get_mut().text_state.char_space = c.0.as_f32();
}
TypedInstruction::WordSpacing(w) => {
context.get_mut().text_state.word_space = w.0.as_f32();
}
TypedInstruction::NextLine(n) => {
let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
text::next_line(context, tx, ty);
}
TypedInstruction::NextLineUsingLeading(_) => {
text::next_line(context, 0.0, -context.get().text_state.leading as f64);
}
TypedInstruction::NextLineAndShowText(n) => {
text::next_line(context, 0.0, -context.get().text_state.leading as f64);
text::show_text_string(context, device, resources, n.0);
}
TypedInstruction::TextRenderingMode(r) => {
let mode = match r.0.as_i64() {
0 => TextRenderingMode::Fill,
1 => TextRenderingMode::Stroke,
2 => TextRenderingMode::FillStroke,
3 => TextRenderingMode::Invisible,
4 => TextRenderingMode::FillAndClip,
5 => TextRenderingMode::StrokeAndClip,
6 => TextRenderingMode::FillAndStrokeAndClip,
7 => TextRenderingMode::Clip,
_ => {
warn!("unknown text rendering mode {}", r.0.as_i64());
TextRenderingMode::Fill
}
};
context.get_mut().text_state.render_mode = mode;
}
TypedInstruction::NextLineAndSetLeading(n) => {
let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
context.get_mut().text_state.leading = -ty as f32;
text::next_line(context, tx, ty);
}
TypedInstruction::ShapeGlyph(_) => {}
TypedInstruction::XObject(x) => {
let cache = context.object_cache.clone();
let transfer_function = context.get().graphics_state.transfer_function.clone();
if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| {
XObject::new(
&s,
&context.settings.warning_sink,
&cache,
transfer_function.clone(),
)
}) {
draw_xobject(&x_object, resources, context, device);
}
}
TypedInstruction::InlineImage(i) => {
let warning_sink = context.settings.warning_sink.clone();
let transfer_function = context.get().graphics_state.transfer_function.clone();
let cache = context.object_cache.clone();
if let Some(x_object) = ImageXObject::new(
&i.0,
|name| context.get_color_space(resources, name.clone()),
&warning_sink,
&cache,
false,
transfer_function,
) {
draw_image_xobject(&x_object, context, device);
}
}
TypedInstruction::TextRise(t) => {
context.get_mut().text_state.rise = t.0.as_f32();
}
TypedInstruction::Shading(s) => {
if !context.ocg_state.is_visible() {
continue;
}
let transfer_function = context.get().graphics_state.transfer_function.clone();
if let Some(sp) = resources
.get_shading(s.0)
.and_then(|o| dict_or_stream(&o))
.and_then(|s| {
Shading::new(
&s.0,
s.1.as_ref(),
&context.object_cache,
&context.settings.warning_sink,
)
})
.map(|s| {
Pattern::Shading(ShadingPattern {
shading: Arc::new(s),
matrix: Affine::IDENTITY,
opacity: context.get().graphics_state.non_stroke_alpha,
transfer_function: transfer_function.clone(),
})
})
{
context.save_state();
context.push_root_transform();
let st = context.get_mut();
st.graphics_state.non_stroke_pattern = Some(sp);
st.graphics_state.none_stroke_cs = ColorSpace::pattern();
device.set_soft_mask(st.graphics_state.soft_mask.clone());
device.set_blend_mode(st.graphics_state.blend_mode);
let bbox = context.bbox().to_path(0.1);
let inverted_bbox = context.get().ctm.inverse() * bbox;
fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
context.pop_root_transform();
context.restore_state(device);
} else {
warn!("failed to process shading");
}
}
TypedInstruction::BeginCompatibility(_) => {}
TypedInstruction::EndCompatibility(_) => {}
TypedInstruction::ColorGlyph(_) => {}
TypedInstruction::ShowTextWithParameters(t) => {
context.get_mut().text_state.word_space = t.0.as_f32();
context.get_mut().text_state.char_space = t.1.as_f32();
text::next_line(context, 0.0, -context.get().text_state.leading as f64);
text::show_text_string(context, device, resources, t.2);
}
_ => {
warn!("failed to read an operator");
}
}
}
while context.num_states() > num_states {
context.restore_state(device);
}
context.end_nested_interpretation();
}
#[cfg(test)]
mod tests {
use crate::device::Device;
use crate::font::Glyph;
use crate::soft_mask::SoftMask;
use crate::util::PageExt;
use crate::{
BlendMode, ClipPath, Context, GlyphDrawMode, Image, InterpreterSettings, Paint,
PathDrawMode, interpret_page,
};
use kurbo::{Affine, BezPath, Shape};
use pdf_syntax::Pdf;
#[derive(Default)]
struct CountingDevice {
path_widths: Vec<f64>,
}
impl Device<'_> for CountingDevice {
fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
fn set_blend_mode(&mut self, _: BlendMode) {}
fn draw_path(&mut self, path: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {
self.path_widths.push(path.bounding_box().width());
}
fn push_clip_path(&mut self, _: &ClipPath) {}
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
fn draw_glyph(
&mut self,
_: &Glyph<'_>,
_: Affine,
_: Affine,
_: &Paint<'_>,
_: &GlyphDrawMode,
) {
}
fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {}
fn pop_clip_path(&mut self) {}
fn pop_transparency_group(&mut self) {}
}
fn build_pdf(objects: &[Vec<u8>]) -> Vec<u8> {
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::with_capacity(objects.len());
for (i, body) in objects.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(body);
out.extend_from_slice(b"\nendobj\n");
}
let xref_pos = out.len();
out.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
out.extend_from_slice(b"0000000000 65535 f \n");
for offset in offsets {
out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(
format!(
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",
objects.len() + 1
)
.as_bytes(),
);
out
}
fn form_stream(bbox: &str, content: &str) -> Vec<u8> {
format!(
"<< /Type /XObject /Subtype /Form /BBox {bbox} /Length {} >>\nstream\n{content}\nendstream",
content.len()
)
.into_bytes()
}
fn checkbox_pdf(annot_body: &[u8], on_bbox: &str, extra_objects: &[Vec<u8>]) -> Vec<u8> {
let mut objects = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
/Annots [4 0 R] /Contents 7 0 R >>"
.to_vec(),
annot_body.to_vec(),
form_stream(on_bbox, "0 0 10 10 re f\n12 12 4 4 re f"),
form_stream("[0 0 20 20]", "0 0 7 7 re f"),
b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
];
objects.extend_from_slice(extra_objects);
build_pdf(&objects)
}
fn interpret_widths(pdf_bytes: Vec<u8>) -> Vec<f64> {
let pdf = Pdf::new(pdf_bytes).expect("test PDF must parse");
let pages = pdf.pages();
let page = pages.first().expect("test PDF must have one page");
let settings = InterpreterSettings::default();
let initial_transform = page.initial_transform(true);
let bbox = kurbo::Rect::new(0.0, 0.0, 100.0, 100.0);
let mut context = Context::new(initial_transform, bbox, page.xref(), settings);
let mut device = CountingDevice::default();
interpret_page(page, &mut context, &mut device);
device.path_widths
}
fn assert_widths(widths: &[f64], expected: &[f64]) {
assert_eq!(
widths.len(),
expected.len(),
"expected {expected:?}, got {widths:?}"
);
for (got, want) in widths.iter().zip(expected) {
assert!(
(got - want).abs() < 1e-6,
"expected {expected:?}, got {widths:?}"
);
}
}
#[test]
fn widget_substate_as_on_state() {
let pdf = checkbox_pdf(
b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
"[0 0 20 20]",
&[],
);
assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
}
#[test]
fn widget_substate_as_off_state() {
let pdf = checkbox_pdf(
b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Off >>",
"[0 0 20 20]",
&[],
);
assert_widths(&interpret_widths(pdf), &[7.0]);
}
#[test]
fn widget_substate_as_off_without_off_entry() {
let pdf = checkbox_pdf(
b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N << /Yes 5 0 R >> >> /AS /Off >>",
"[0 0 20 20]",
&[],
);
assert_widths(&interpret_widths(pdf), &[]);
}
#[test]
fn widget_substate_v_fallback() {
let pdf = checkbox_pdf(
b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /V /Yes >>",
"[0 0 20 20]",
&[],
);
assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
}
#[test]
fn widget_substate_parent_v_fallback() {
let pdf = checkbox_pdf(
b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /Parent 8 0 R >>",
"[0 0 20 20]",
&[b"<< /FT /Btn /V /Yes >>".to_vec()],
);
assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
}
#[test]
fn widget_substate_non_ascii_state_name() {
let annot = b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N << /Stra\xf6m 5 0 R /Off 6 0 R >> >> /AS /Stra#F6m >>";
assert!(annot.contains(&0xf6));
let pdf = checkbox_pdf(annot, "[0 0 20 20]", &[]);
assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
}
#[test]
fn widget_degenerate_bbox_skipped() {
let pdf = checkbox_pdf(
b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
"[0 0 0 20]",
&[],
);
assert_widths(&interpret_widths(pdf), &[]);
}
#[test]
fn widget_direct_stream_still_renders() {
let pdf = checkbox_pdf(
b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
/AP << /N 6 0 R >> >>",
"[0 0 20 20]",
&[],
);
assert_widths(&interpret_widths(pdf), &[7.0]);
}
}