use std::str;
use oxideav_core::vector::{
DashPattern, FillRule, Group, LineCap, LineJoin, Node, Paint, Path, PathCommand, PathNode,
Point, Rgba, Stroke, Transform2D,
};
use crate::error::PdfError;
pub fn parse_content_stream(input: &[u8]) -> Result<Group, PdfError> {
let mut state = State::new();
state.parse(input)?;
Ok(state.finish())
}
struct State {
operands: Vec<Operand>,
stack: Vec<Frame>,
current_path: Option<Path>,
current_point: Point,
fill_paint: Option<Paint>,
stroke_paint: Option<Paint>,
stroke_width: f32,
line_cap: LineCap,
line_join: LineJoin,
miter_limit: f32,
dash: Option<DashPattern>,
}
struct Frame {
transform: Transform2D,
children: Vec<Node>,
clip: Option<Path>,
}
#[derive(Clone, Debug)]
enum Operand {
Number(f32),
Array(Vec<f32>),
#[allow(dead_code)]
Name(String),
}
impl State {
fn new() -> Self {
Self {
operands: Vec::new(),
stack: vec![Frame::new()],
current_path: None,
current_point: Point::default(),
fill_paint: None,
stroke_paint: None,
stroke_width: 1.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
miter_limit: 10.0,
dash: None,
}
}
fn finish(mut self) -> Group {
while self.stack.len() > 1 {
self.pop_q();
}
let root = self.stack.pop().expect("root frame present");
Group {
transform: root.transform,
opacity: 1.0,
clip: root.clip,
children: root.children,
..Group::default()
}
}
fn current(&mut self) -> &mut Frame {
self.stack.last_mut().expect("at least the root frame")
}
fn push_q(&mut self) {
self.stack.push(Frame::new());
}
fn pop_q(&mut self) {
if self.stack.len() <= 1 {
return;
}
let frame = self.stack.pop().unwrap();
if frame.is_effectively_empty() {
return;
}
let g = Group {
transform: frame.transform,
opacity: 1.0,
clip: frame.clip,
children: frame.children,
..Group::default()
};
self.current().children.push(Node::Group(g));
}
fn dispatch(&mut self, op: &[u8]) -> Result<(), PdfError> {
match op {
b"q" => {
self.push_q();
}
b"Q" => {
self.pop_q();
}
b"cm" => {
let nums = self.take_numbers(6)?;
let t = Transform2D {
a: nums[0],
b: nums[1],
c: nums[2],
d: nums[3],
e: nums[4],
f: nums[5],
};
let frame = self.current();
frame.transform = compose(frame.transform, t);
}
b"gs" => {
self.operands.clear();
}
b"m" => {
let p = self.take_point()?;
let path = self.path_mut();
path.commands.push(PathCommand::MoveTo(p));
self.current_point = p;
}
b"l" => {
let p = self.take_point()?;
let path = self.path_mut();
path.commands.push(PathCommand::LineTo(p));
self.current_point = p;
}
b"c" => {
let nums = self.take_numbers(6)?;
let c1 = Point::new(nums[0], nums[1]);
let c2 = Point::new(nums[2], nums[3]);
let end = Point::new(nums[4], nums[5]);
let path = self.path_mut();
path.commands
.push(PathCommand::CubicCurveTo { c1, c2, end });
self.current_point = end;
}
b"v" => {
let nums = self.take_numbers(4)?;
let c1 = self.current_point;
let c2 = Point::new(nums[0], nums[1]);
let end = Point::new(nums[2], nums[3]);
let path = self.path_mut();
path.commands
.push(PathCommand::CubicCurveTo { c1, c2, end });
self.current_point = end;
}
b"y" => {
let nums = self.take_numbers(4)?;
let c1 = Point::new(nums[0], nums[1]);
let end = Point::new(nums[2], nums[3]);
let c2 = end;
let path = self.path_mut();
path.commands
.push(PathCommand::CubicCurveTo { c1, c2, end });
self.current_point = end;
}
b"re" => {
let nums = self.take_numbers(4)?;
let (x, y, w, h) = (nums[0], nums[1], nums[2], nums[3]);
let path = self.path_mut();
path.commands.push(PathCommand::MoveTo(Point::new(x, y)));
path.commands
.push(PathCommand::LineTo(Point::new(x + w, y)));
path.commands
.push(PathCommand::LineTo(Point::new(x + w, y + h)));
path.commands
.push(PathCommand::LineTo(Point::new(x, y + h)));
path.commands.push(PathCommand::Close);
self.current_point = Point::new(x, y);
}
b"h" => {
let path = self.path_mut();
path.commands.push(PathCommand::Close);
}
b"f" | b"F" => self.commit_path(true, false, FillRule::NonZero),
b"f*" => self.commit_path(true, false, FillRule::EvenOdd),
b"S" => self.commit_path(false, true, FillRule::NonZero),
b"s" => {
if let Some(p) = &mut self.current_path {
p.commands.push(PathCommand::Close);
}
self.commit_path(false, true, FillRule::NonZero);
}
b"B" => self.commit_path(true, true, FillRule::NonZero),
b"B*" => self.commit_path(true, true, FillRule::EvenOdd),
b"b" => {
if let Some(p) = &mut self.current_path {
p.commands.push(PathCommand::Close);
}
self.commit_path(true, true, FillRule::NonZero);
}
b"b*" => {
if let Some(p) = &mut self.current_path {
p.commands.push(PathCommand::Close);
}
self.commit_path(true, true, FillRule::EvenOdd);
}
b"n" => {
self.current_path = None;
self.operands.clear();
}
b"W" | b"W*" => {
if let Some(p) = self.current_path.take() {
self.current().clip = Some(p);
}
self.operands.clear();
}
b"rg" => {
let nums = self.take_numbers(3)?;
self.fill_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[1], nums[2])));
}
b"RG" => {
let nums = self.take_numbers(3)?;
self.stroke_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[1], nums[2])));
}
b"g" => {
let nums = self.take_numbers(1)?;
self.fill_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[0], nums[0])));
}
b"G" => {
let nums = self.take_numbers(1)?;
self.stroke_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[0], nums[0])));
}
b"k" | b"K" => {
let _ = self.take_numbers(4);
let p = Some(Paint::Solid(Rgba::opaque(0, 0, 0)));
if op == b"K" {
self.stroke_paint = p;
} else {
self.fill_paint = p;
}
}
b"sc" | b"scn" => {
self.fill_paint = self
.fill_paint
.clone()
.or(Some(Paint::Solid(Rgba::opaque(0, 0, 0))));
self.operands.clear();
}
b"SC" | b"SCN" => {
self.stroke_paint = self
.stroke_paint
.clone()
.or(Some(Paint::Solid(Rgba::opaque(0, 0, 0))));
self.operands.clear();
}
b"cs" | b"CS" => {
self.operands.clear();
}
b"w" => {
let nums = self.take_numbers(1)?;
self.stroke_width = nums[0];
}
b"J" => {
let nums = self.take_numbers(1)?;
self.line_cap = match nums[0] as i32 {
0 => LineCap::Butt,
1 => LineCap::Round,
2 => LineCap::Square,
_ => LineCap::Butt,
};
}
b"j" => {
let nums = self.take_numbers(1)?;
self.line_join = match nums[0] as i32 {
0 => LineJoin::Miter,
1 => LineJoin::Round,
2 => LineJoin::Bevel,
_ => LineJoin::Miter,
};
}
b"M" => {
let nums = self.take_numbers(1)?;
self.miter_limit = nums[0];
}
b"d" => {
if self.operands.len() < 2 {
self.operands.clear();
return Ok(());
}
let offset = match self.operands.pop().unwrap() {
Operand::Number(n) => n,
_ => 0.0,
};
let array = match self.operands.pop().unwrap() {
Operand::Array(v) => v,
_ => Vec::new(),
};
self.dash = if array.is_empty() {
None
} else {
Some(DashPattern { array, offset })
};
self.operands.clear();
}
b"BT" | b"ET" | b"Tj" | b"TJ" | b"Tf" | b"Tc" | b"Tw" | b"Tz" | b"TL" | b"Tr"
| b"Ts" | b"Td" | b"TD" | b"Tm" | b"T*" | b"'" | b"\"" => {
self.operands.clear();
}
b"Do" => {
self.operands.clear();
}
_ => {
self.operands.clear();
}
}
Ok(())
}
fn commit_path(&mut self, fill: bool, stroke: bool, rule: FillRule) {
let Some(path) = self.current_path.take() else {
self.operands.clear();
return;
};
let fill_paint = if fill {
self.fill_paint
.clone()
.or(Some(Paint::Solid(Rgba::opaque(0, 0, 0))))
} else {
None
};
let stroke_obj = if stroke {
Some(Stroke {
width: self.stroke_width,
paint: self
.stroke_paint
.clone()
.unwrap_or(Paint::Solid(Rgba::opaque(0, 0, 0))),
cap: self.line_cap,
join: self.line_join,
miter_limit: self.miter_limit,
dash: self.dash.clone(),
})
} else {
None
};
let node = Node::Path(PathNode {
path,
fill: fill_paint,
stroke: stroke_obj,
fill_rule: rule,
});
self.current().children.push(node);
self.operands.clear();
}
fn path_mut(&mut self) -> &mut Path {
if self.current_path.is_none() {
self.current_path = Some(Path::new());
}
self.current_path.as_mut().unwrap()
}
fn take_numbers(&mut self, n: usize) -> Result<Vec<f32>, PdfError> {
if self.operands.len() < n {
return Err(PdfError::other(format!(
"PDF content parser: operator needed {n} numeric operands, got {}",
self.operands.len()
)));
}
let split = self.operands.len() - n;
let tail: Vec<Operand> = self.operands.drain(split..).collect();
let mut out = Vec::with_capacity(n);
for op in tail {
match op {
Operand::Number(f) => out.push(f),
other => {
return Err(PdfError::other(format!(
"PDF content parser: expected numeric operand, got {other:?}"
)));
}
}
}
Ok(out)
}
fn take_point(&mut self) -> Result<Point, PdfError> {
let nums = self.take_numbers(2)?;
Ok(Point::new(nums[0], nums[1]))
}
fn parse(&mut self, input: &[u8]) -> Result<(), PdfError> {
let mut i = 0;
while i < input.len() {
let b = input[i];
if is_whitespace(b) {
i += 1;
continue;
}
if b == b'%' {
while i < input.len() && input[i] != b'\n' && input[i] != b'\r' {
i += 1;
}
continue;
}
if b == b'(' {
let (end, _bytes) = read_literal_string(input, i)?;
i = end;
continue;
}
if b == b'<' && input.get(i + 1) != Some(&b'<') {
let end = read_hex_string(input, i)?;
i = end;
continue;
}
if b == b'[' {
let (end, nums) = read_number_array(input, i);
self.operands.push(Operand::Array(nums));
i = end;
continue;
}
if b == b'/' {
let mut end = i + 1;
while end < input.len() && !is_whitespace(input[end]) && !is_delimiter(input[end]) {
end += 1;
}
let name = String::from_utf8_lossy(&input[i + 1..end]).into_owned();
self.operands.push(Operand::Name(name));
i = end;
continue;
}
if matches!(b, b'+' | b'-' | b'.' | b'0'..=b'9') {
let mut end = i;
if matches!(input[end], b'+' | b'-') {
end += 1;
}
let mut saw_digit = false;
let mut saw_dot = false;
while end < input.len() {
let c = input[end];
if c.is_ascii_digit() {
end += 1;
saw_digit = true;
} else if c == b'.' && !saw_dot {
end += 1;
saw_dot = true;
} else {
break;
}
}
if !saw_digit {
let kw_end = scan_keyword_end(input, i);
let kw = &input[i..kw_end];
self.dispatch(kw)?;
i = kw_end;
continue;
}
let s = str::from_utf8(&input[i..end]).map_err(|_| {
PdfError::other(format!("PDF content parser: non-UTF-8 number at byte {i}"))
})?;
let f: f32 = s.parse().map_err(|_| {
PdfError::other(format!(
"PDF content parser: invalid number `{s}` at byte {i}"
))
})?;
self.operands.push(Operand::Number(f));
i = end;
continue;
}
let kw_end = scan_keyword_end(input, i);
if kw_end == i {
i += 1;
continue;
}
let kw = &input[i..kw_end];
self.dispatch(kw)?;
i = kw_end;
}
Ok(())
}
}
impl Frame {
fn new() -> Self {
Self {
transform: Transform2D::identity(),
children: Vec::new(),
clip: None,
}
}
fn is_effectively_empty(&self) -> bool {
self.children.is_empty() && self.clip.is_none() && self.transform.is_identity()
}
}
fn is_whitespace(b: u8) -> bool {
matches!(b, 0x00 | b'\t' | b'\n' | 0x0C | b'\r' | b' ')
}
fn is_delimiter(b: u8) -> bool {
matches!(
b,
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
)
}
fn rgb_from_unit(r: f32, g: f32, b: f32) -> Rgba {
Rgba::opaque(unit_to_byte(r), unit_to_byte(g), unit_to_byte(b))
}
fn unit_to_byte(f: f32) -> u8 {
(f.clamp(0.0, 1.0) * 255.0).round() as u8
}
fn compose(a: Transform2D, b: Transform2D) -> Transform2D {
Transform2D {
a: a.a * b.a + a.c * b.b,
b: a.b * b.a + a.d * b.b,
c: a.a * b.c + a.c * b.d,
d: a.b * b.c + a.d * b.d,
e: a.a * b.e + a.c * b.f + a.e,
f: a.b * b.e + a.d * b.f + a.f,
}
}
fn scan_keyword_end(input: &[u8], start: usize) -> usize {
let mut end = start;
while end < input.len() && !is_whitespace(input[end]) && !is_delimiter(input[end]) {
end += 1;
}
end
}
fn read_literal_string(input: &[u8], start: usize) -> Result<(usize, Vec<u8>), PdfError> {
let mut end = start + 1;
let mut depth = 1u32;
let mut decoded = Vec::new();
while end < input.len() {
let b = input[end];
if b == b'\\' {
end += 1;
if end < input.len() {
end += 1;
}
continue;
}
if b == b'(' {
depth += 1;
}
if b == b')' {
depth -= 1;
if depth == 0 {
end += 1;
return Ok((end, decoded));
}
}
decoded.push(b);
end += 1;
}
Err(PdfError::other(
"PDF content parser: unterminated literal string",
))
}
fn read_hex_string(input: &[u8], start: usize) -> Result<usize, PdfError> {
let mut end = start + 1;
while end < input.len() {
if input[end] == b'>' {
return Ok(end + 1);
}
end += 1;
}
Err(PdfError::other(
"PDF content parser: unterminated hex string",
))
}
fn read_number_array(input: &[u8], start: usize) -> (usize, Vec<f32>) {
let mut end = start + 1;
let mut nums = Vec::new();
while end < input.len() && input[end] != b']' {
if is_whitespace(input[end]) {
end += 1;
continue;
}
if matches!(input[end], b'+' | b'-' | b'.' | b'0'..=b'9') {
let nstart = end;
if matches!(input[end], b'+' | b'-') {
end += 1;
}
let mut saw_dot = false;
while end < input.len()
&& (input[end].is_ascii_digit() || (input[end] == b'.' && !saw_dot))
{
if input[end] == b'.' {
saw_dot = true;
}
end += 1;
}
if let Ok(s) = str::from_utf8(&input[nstart..end]) {
if let Ok(f) = s.parse::<f32>() {
nums.push(f);
}
}
} else {
end += 1;
}
}
if end < input.len() {
end += 1;
} (end, nums)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(input: &[u8]) -> Group {
parse_content_stream(input).unwrap()
}
#[test]
fn empty_content_yields_empty_group() {
let g = parse(b"");
assert!(g.children.is_empty());
assert!(g.clip.is_none());
}
#[test]
fn rect_fill_round_trips() {
let bytes = b"q 1 0 0 rg 10 10 m 110 10 l 110 60 l 10 60 l h f Q\n";
let root = parse(bytes);
assert_eq!(root.children.len(), 1);
let Node::Group(g) = &root.children[0] else {
panic!("expected group")
};
assert_eq!(g.children.len(), 1);
let Node::Path(pn) = &g.children[0] else {
panic!("expected path")
};
assert_eq!(pn.path.commands.len(), 5);
assert!(matches!(pn.path.commands[0], PathCommand::MoveTo(p) if (p.x - 10.0).abs() < 1e-3));
assert!(matches!(pn.path.commands[4], PathCommand::Close));
assert_eq!(pn.fill_rule, FillRule::NonZero);
match &pn.fill {
Some(Paint::Solid(r)) => assert_eq!((r.r, r.g, r.b), (255, 0, 0)),
other => panic!("unexpected fill: {other:?}"),
}
assert!(pn.stroke.is_none());
}
#[test]
fn nested_q_groups_are_promoted_to_node_groups() {
let bytes = b"q q 1 0 0 1 5 5 cm 0 0 m 10 10 l S Q Q\n";
let root = parse(bytes);
assert_eq!(root.children.len(), 1);
let Node::Group(outer) = &root.children[0] else {
panic!()
};
assert_eq!(outer.children.len(), 1);
let Node::Group(inner) = &outer.children[0] else {
panic!()
};
assert!(!inner.transform.is_identity());
assert_eq!(inner.children.len(), 1);
}
#[test]
fn rectangle_operator_re_expands_to_subpath() {
let bytes = b"q 0.5 0.5 0.5 rg 10 20 30 40 re f Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
assert_eq!(p.path.commands.len(), 5);
assert!(
matches!(p.path.commands[0], PathCommand::MoveTo(pp) if pp.x == 10.0 && pp.y == 20.0)
);
assert!(
matches!(p.path.commands[1], PathCommand::LineTo(pp) if pp.x == 40.0 && pp.y == 20.0)
);
assert!(
matches!(p.path.commands[2], PathCommand::LineTo(pp) if pp.x == 40.0 && pp.y == 60.0)
);
assert!(
matches!(p.path.commands[3], PathCommand::LineTo(pp) if pp.x == 10.0 && pp.y == 60.0)
);
assert!(matches!(p.path.commands[4], PathCommand::Close));
}
#[test]
fn cubic_curve_roundtrips() {
let bytes = b"q 0 0 m 1 1 2 1 3 0 c S Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
assert!(matches!(
p.path.commands[1],
PathCommand::CubicCurveTo { c1, c2, end }
if c1.x == 1.0 && c1.y == 1.0 && c2.x == 2.0 && c2.y == 1.0 && end.x == 3.0 && end.y == 0.0
));
}
#[test]
fn fill_rule_evenodd_recognised() {
let bytes = b"q 0 0 m 10 0 l 10 10 l h f* Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
assert_eq!(p.fill_rule, FillRule::EvenOdd);
}
#[test]
fn cm_translate_lands_on_group_transform() {
let bytes = b"q 1 0 0 1 100 200 cm 0 0 m 5 5 l S Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
assert!((g.transform.e - 100.0).abs() < 1e-3);
assert!((g.transform.f - 200.0).abs() < 1e-3);
}
#[test]
fn stroke_style_w_j_m_d_recorded() {
let bytes = b"q 2.5 w 1 J 2 j 8 M [5 3] 1 d 0 0 0 RG 0 0 m 10 10 l S Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke set");
assert!((s.width - 2.5).abs() < 1e-3);
assert!(matches!(s.cap, LineCap::Round));
assert!(matches!(s.join, LineJoin::Bevel));
assert!((s.miter_limit - 8.0).abs() < 1e-3);
let dash = s.dash.as_ref().expect("dash set");
assert_eq!(dash.array, vec![5.0, 3.0]);
assert!((dash.offset - 1.0).abs() < 1e-3);
}
#[test]
fn clip_w_assigns_to_group_clip() {
let bytes =
b"q 10 10 m 50 10 l 50 50 l 10 50 l h W n 0 0 0 rg 20 20 m 30 20 l 30 30 l h f Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
assert!(g.clip.is_some());
assert_eq!(g.children.len(), 1);
}
}