use crate::content::OperandRing;
use kurbo::{Affine, Point};
use pdfrum_object::{Name, Object, PdfString};
use smallvec::SmallVec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
impl LineCap {
#[must_use]
pub fn from_int(v: i64) -> Self {
match v {
1 => Self::Round,
2 => Self::Square,
_ => Self::Butt,
}
}
#[must_use]
pub fn is_exact(v: i64) -> bool {
(0..=2).contains(&v)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
impl LineJoin {
#[must_use]
pub fn from_int(v: i64) -> Self {
match v {
1 => Self::Round,
2 => Self::Bevel,
_ => Self::Miter,
}
}
#[must_use]
pub fn is_exact(v: i64) -> bool {
(0..=2).contains(&v)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum FillRule {
#[default]
None,
Winding,
EvenOdd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum TextRenderMode {
#[default]
Fill,
Stroke,
FillStroke,
Invisible,
FillClip,
StrokeClip,
FillStrokeClip,
Clip,
}
impl TextRenderMode {
#[must_use]
pub fn from_int(v: i64) -> Option<Self> {
Some(match v {
0 => Self::Fill,
1 => Self::Stroke,
2 => Self::FillStroke,
3 => Self::Invisible,
4 => Self::FillClip,
5 => Self::StrokeClip,
6 => Self::FillStrokeClip,
7 => Self::Clip,
_ => return None,
})
}
#[must_use]
pub fn fills(self) -> bool {
matches!(
self,
Self::Fill | Self::FillStroke | Self::FillClip | Self::FillStrokeClip
)
}
#[must_use]
pub fn strokes(self) -> bool {
matches!(
self,
Self::Stroke | Self::FillStroke | Self::StrokeClip | Self::FillStrokeClip
)
}
#[must_use]
pub fn clips(self) -> bool {
matches!(
self,
Self::FillClip | Self::StrokeClip | Self::FillStrokeClip | Self::Clip
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TextItem {
Show(Box<[u8]>),
Adjust(f32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum MarkProperties {
Named(Name),
Inline(Box<pdfrum_object::Dict>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct InlineImage {
pub dict: pdfrum_object::Dict,
pub data: Box<[u8]>,
}
trait FromOperands: Sized {
fn extract(ring: &OperandRing, index: usize) -> Self;
}
impl FromOperands for f32 {
fn extract(ring: &OperandRing, index: usize) -> Self {
ring.number(index)
}
}
impl FromOperands for i64 {
fn extract(ring: &OperandRing, index: usize) -> Self {
ring.integer(index).unwrap_or(0)
}
}
impl FromOperands for LineCap {
fn extract(ring: &OperandRing, index: usize) -> Self {
Self::from_int(ring.integer(index).unwrap_or(0))
}
}
impl FromOperands for LineJoin {
fn extract(ring: &OperandRing, index: usize) -> Self {
Self::from_int(ring.integer(index).unwrap_or(0))
}
}
impl FromOperands for Name {
fn extract(ring: &OperandRing, index: usize) -> Self {
Name::new(ring.string(index))
}
}
impl FromOperands for PdfString {
fn extract(ring: &OperandRing, index: usize) -> Self {
PdfString::literal(ring.string(index))
}
}
impl FromOperands for Object {
fn extract(ring: &OperandRing, index: usize) -> Self {
ring.object(index)
}
}
impl FromOperands for Point {
fn extract(ring: &OperandRing, index: usize) -> Self {
Self::new(
f64::from(ring.number(index + 1)),
f64::from(ring.number(index)),
)
}
}
impl FromOperands for Affine {
fn extract(ring: &OperandRing, index: usize) -> Self {
let n = |k: usize| f64::from(ring.number(index + k));
Self::new([n(5), n(4), n(3), n(2), n(1), n(0)])
}
}
macro_rules! ops {
($(
$(#[$meta:meta])*
$spelling:literal => $variant:ident ( $($ty:ty : $from:tt),* ) $($guard:literal)?;
)*) => {
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Op {
$(
$(#[$meta])*
$variant ( $($ty),* ),
)*
InlineImage(Box<InlineImage>),
Unknown(Box<[u8]>),
}
impl Op {
#[must_use]
pub fn keyword(&self) -> &[u8] {
match self {
$( Self::$variant { .. } => $spelling, )*
Self::InlineImage(_) => b"BI",
Self::Unknown(w) => w,
}
}
#[must_use]
pub fn is_operator(word: &[u8]) -> bool {
matches!(word, $( $spelling )|*)
}
pub(crate) fn from_ring(word: &[u8], ring: &OperandRing) -> Dispatch {
let _ = ring;
match word {
$(
$spelling => {
$(
if ring.len() != $guard {
return Dispatch::GuardFailed;
}
)?
Dispatch::Op(Op::$variant(
$( <$ty as FromOperands>::extract(ring, $from) ),*
))
}
)*
_ => Dispatch::NotAnOperator,
}
}
}
};
}
pub(crate) enum Dispatch {
Op(Op),
GuardFailed,
NotAnOperator,
}
include!("ops_table.rs");
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{LineCap, LineJoin, Op, TextRenderMode};
#[test]
fn every_operator_round_trips_its_spelling() {
for word in [
&b"q"[..],
b"Q",
b"cm",
b"w",
b"J",
b"j",
b"M",
b"d",
b"ri",
b"i",
b"gs",
b"m",
b"l",
b"c",
b"v",
b"y",
b"h",
b"re",
b"S",
b"s",
b"f",
b"F",
b"f*",
b"B",
b"B*",
b"b",
b"b*",
b"n",
b"W",
b"W*",
b"BT",
b"ET",
b"Td",
b"TD",
b"Tm",
b"T*",
b"TL",
b"Ts",
b"Tz",
b"Tc",
b"Tw",
b"Tf",
b"Tr",
b"Tj",
b"'",
b"\"",
b"TJ",
b"d0",
b"d1",
b"CS",
b"cs",
b"SC",
b"sc",
b"SCN",
b"scn",
b"G",
b"g",
b"RG",
b"rg",
b"K",
b"k",
b"Do",
b"sh",
b"BI",
b"ID",
b"EI",
b"BMC",
b"BDC",
b"EMC",
b"MP",
b"DP",
b"BX",
b"EX",
] {
assert!(Op::is_operator(word), "{word:?} is not registered");
}
assert!(!Op::is_operator(b"Tjxx"));
assert!(!Op::is_operator(b""));
assert!(!Op::is_operator(b"true"));
}
#[test]
fn caps_and_joins_clamp_out_of_range_operands() {
assert_eq!(LineCap::from_int(1), LineCap::Round);
assert_eq!(LineCap::from_int(5), LineCap::Butt);
assert_eq!(LineCap::from_int(-1), LineCap::Butt);
assert!(!LineCap::is_exact(3));
assert_eq!(LineJoin::from_int(2), LineJoin::Bevel);
assert_eq!(LineJoin::from_int(99), LineJoin::Miter);
assert!(LineJoin::is_exact(0));
}
#[test]
fn text_render_modes_outside_zero_to_seven_are_refused() {
assert_eq!(TextRenderMode::from_int(0), Some(TextRenderMode::Fill));
assert_eq!(TextRenderMode::from_int(7), Some(TextRenderMode::Clip));
assert_eq!(TextRenderMode::from_int(8), None);
assert_eq!(TextRenderMode::from_int(-1), None);
assert!(TextRenderMode::FillStrokeClip.fills());
assert!(TextRenderMode::FillStrokeClip.strokes());
assert!(TextRenderMode::FillStrokeClip.clips());
assert!(!TextRenderMode::Invisible.fills());
assert!(!TextRenderMode::Fill.clips());
}
}