use std::fmt;
use rustc_hash::FxHashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId {
pub number: u32,
pub generation: u16,
}
impl ObjectId {
pub fn new(number: u32, generation: u16) -> Self {
Self { number, generation }
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} R", self.number, self.generation)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PdfName(pub String);
impl PdfName {
pub fn new(s: &str) -> Self {
Self(s.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for PdfName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "/{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PdfString(pub Vec<u8>);
impl PdfString {
pub fn from_literal(s: &str) -> Self {
Self(s.as_bytes().to_vec())
}
pub fn from_bytes(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
pub fn escape_literal_string_into(out: &mut Vec<u8>, bytes: &[u8]) {
for &b in bytes {
match b {
b'(' => out.extend_from_slice(b"\\("),
b')' => out.extend_from_slice(b"\\)"),
b'\\' => out.extend_from_slice(b"\\\\"),
b'\n' => out.extend_from_slice(b"\\n"),
b'\r' => out.extend_from_slice(b"\\r"),
b'\t' => out.extend_from_slice(b"\\t"),
8 => out.extend_from_slice(b"\\b"),
12 => out.extend_from_slice(b"\\f"),
0x20..=0x7e => out.push(b),
b => {
out.push(b'\\');
out.push(b'0' + (b >> 6));
out.push(b'0' + ((b >> 3) & 7));
out.push(b'0' + (b & 7));
}
}
}
}
pub fn escape_literal_string(bytes: &[u8]) -> String {
let mut out = Vec::with_capacity(bytes.len() + 8);
escape_literal_string_into(&mut out, bytes);
String::from_utf8(out).expect("escaped literal string is ASCII")
}
impl fmt::Display for PdfString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({})", escape_literal_string(&self.0))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PdfArray(pub Vec<PdfObject>);
impl PdfArray {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn push(&mut self, obj: PdfObject) {
self.0.push(obj);
}
pub fn get(&self, index: usize) -> Option<&PdfObject> {
self.0.get(index)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl Default for PdfArray {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PdfDictionary {
entries: FxHashMap<PdfName, PdfObject>,
}
impl PdfDictionary {
pub fn new() -> Self {
Self {
entries: FxHashMap::default(),
}
}
pub fn insert(&mut self, key: &str, value: PdfObject) {
self.entries.insert(PdfName::new(key), value);
}
pub fn get(&self, key: &str) -> Option<&PdfObject> {
self.entries.get(&PdfName::new(key))
}
pub fn get_name(&self, key: &str) -> Option<&PdfName> {
match self.get(key) {
Some(PdfObject::Name(n)) => Some(n),
_ => None,
}
}
pub fn get_integer(&self, key: &str) -> Option<i64> {
match self.get(key) {
Some(PdfObject::Integer(i)) => Some(*i),
_ => None,
}
}
pub fn get_array(&self, key: &str) -> Option<&PdfArray> {
match self.get(key) {
Some(PdfObject::Array(a)) => Some(a),
_ => None,
}
}
pub fn get_dict(&self, key: &str) -> Option<&PdfDictionary> {
match self.get(key) {
Some(PdfObject::Dictionary(d)) => Some(d),
_ => None,
}
}
pub fn get_string_bytes(&self, key: &str) -> Option<&[u8]> {
self.get(key)
.and_then(|o| o.as_string())
.map(PdfString::as_bytes)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&PdfName, &PdfObject)> {
self.entries.iter()
}
}
impl Default for PdfDictionary {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PdfStream {
pub dictionary: PdfDictionary,
pub data: Vec<u8>,
}
impl PdfStream {
pub fn new(data: Vec<u8>) -> Self {
Self {
dictionary: PdfDictionary::new(),
data,
}
}
pub fn with_dict(dictionary: PdfDictionary, data: Vec<u8>) -> Self {
Self { dictionary, data }
}
pub fn length(&self) -> usize {
self.data.len()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PdfObject {
Null,
Boolean(bool),
Integer(i64),
Real(f64),
Name(PdfName),
String(PdfString),
Array(PdfArray),
Dictionary(PdfDictionary),
Stream(PdfStream),
Reference(ObjectId),
}
impl PdfObject {
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
Self::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_real(&self) -> Option<f64> {
match self {
Self::Real(r) => Some(*r),
Self::Integer(i) => Some(*i as f64),
_ => None,
}
}
pub fn as_name(&self) -> Option<&PdfName> {
match self {
Self::Name(n) => Some(n),
_ => None,
}
}
pub fn as_string(&self) -> Option<&PdfString> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
pub fn as_array(&self) -> Option<&PdfArray> {
match self {
Self::Array(a) => Some(a),
_ => None,
}
}
pub fn as_dict(&self) -> Option<&PdfDictionary> {
match self {
Self::Dictionary(d) => Some(d),
_ => None,
}
}
pub fn as_stream(&self) -> Option<&PdfStream> {
match self {
Self::Stream(s) => Some(s),
_ => None,
}
}
pub fn as_reference(&self) -> Option<ObjectId> {
match self {
Self::Reference(id) => Some(*id),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_literal_handles_all_special_bytes() {
let input: &[u8] = b"a(b)c\\d\ne\rf\t";
let escaped = escape_literal_string(input);
assert_eq!(escaped, "a\\(b\\)c\\\\d\\ne\\rf\\t");
}
#[test]
fn escape_literal_uses_octal_for_control_bytes() {
assert_eq!(
escape_literal_string(&[0x01, 0x07, 0x0b]),
"\\001\\007\\013"
);
}
#[test]
fn escape_literal_octal_escapes_non_ascii() {
assert_eq!(escape_literal_string(&[0x80]), "\\200");
assert_eq!(escape_literal_string(&[0xc3, 0xa9]), "\\303\\251");
assert_eq!(escape_literal_string(&[0xff]), "\\377");
}
#[test]
fn string_display_escapes_parens_and_backslash() {
let s = PdfString::from_literal("(a) \\ b");
assert_eq!(s.to_string(), "(\\(a\\) \\\\ b)");
}
#[test]
fn string_display_escapes_line_breaks() {
let s = PdfString::from_bytes(b"line1\nline2\r\nline3");
assert_eq!(s.to_string(), "(line1\\nline2\\r\\nline3)");
}
}