use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PdfString(Vec<u8>);
impl PdfString {
pub fn new(bytes: Vec<u8>) -> Self {
PdfString(bytes)
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
PdfString(s.as_bytes().to_vec())
}
pub fn from_text(s: &str) -> Self {
if s.is_ascii() {
PdfString(s.as_bytes().to_vec())
} else {
let mut bytes = vec![0xFE, 0xFF]; for ch in s.encode_utf16() {
bytes.extend_from_slice(&ch.to_be_bytes());
}
PdfString(bytes)
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.0).ok()
}
pub fn decode_text(&self) -> String {
if self.0.len() >= 2 && self.0[0] == 0xFE && self.0[1] == 0xFF {
let utf16: Vec<u16> = self.0[2..]
.chunks(2)
.filter_map(|chunk| {
if chunk.len() == 2 {
Some(u16::from_be_bytes([chunk[0], chunk[1]]))
} else {
None
}
})
.collect();
String::from_utf16_lossy(&utf16)
} else {
String::from_utf8_lossy(&self.0).into_owned()
}
}
pub fn encode(&self) -> String {
let mut result = String::with_capacity(self.0.len() * 2 + 2);
result.push('(');
for &byte in &self.0 {
match byte {
b'\n' => result.push_str("\\n"),
b'\r' => result.push_str("\\r"),
b'\t' => result.push_str("\\t"),
b'\x08' => result.push_str("\\b"),
b'\x0C' => result.push_str("\\f"),
b'(' => result.push_str("\\("),
b')' => result.push_str("\\)"),
b'\\' => result.push_str("\\\\"),
0x20..=0x7E => result.push(byte as char),
_ => {
result.push_str(&format!("\\{:03o}", byte));
}
}
}
result.push(')');
result
}
}
impl fmt::Display for PdfString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.encode())
}
}
impl From<&str> for PdfString {
fn from(s: &str) -> Self {
PdfString::from_str(s)
}
}
impl From<String> for PdfString {
fn from(s: String) -> Self {
PdfString::new(s.into_bytes())
}
}
impl From<Vec<u8>> for PdfString {
fn from(bytes: Vec<u8>) -> Self {
PdfString::new(bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PdfHexString(Vec<u8>);
impl PdfHexString {
pub fn new(bytes: Vec<u8>) -> Self {
PdfHexString(bytes)
}
pub fn from_text(text: &str) -> Self {
let mut bytes = vec![0xFE, 0xFF]; for ch in text.encode_utf16() {
bytes.extend_from_slice(&ch.to_be_bytes());
}
PdfHexString(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn decode_text(&self) -> String {
if self.0.len() >= 2 && self.0[0] == 0xFE && self.0[1] == 0xFF {
let utf16: Vec<u16> = self.0[2..]
.chunks(2)
.filter_map(|chunk| {
if chunk.len() == 2 {
Some(u16::from_be_bytes([chunk[0], chunk[1]]))
} else {
None
}
})
.collect();
String::from_utf16_lossy(&utf16)
} else {
String::from_utf8_lossy(&self.0).into_owned()
}
}
pub fn encode(&self) -> String {
let mut result = String::with_capacity(self.0.len() * 2 + 2);
result.push('<');
for byte in &self.0 {
result.push_str(&format!("{:02X}", byte));
}
result.push('>');
result
}
}
impl fmt::Display for PdfHexString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.encode())
}
}
impl From<Vec<u8>> for PdfHexString {
fn from(bytes: Vec<u8>) -> Self {
PdfHexString::new(bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_literal_string() {
let s = PdfString::from_str("Hello World");
assert_eq!(s.encode(), "(Hello World)");
}
#[test]
fn test_literal_string_escaping() {
let s = PdfString::from_str("Hello\nWorld");
assert_eq!(s.encode(), "(Hello\\nWorld)");
let s = PdfString::from_str("Test (parentheses)");
assert_eq!(s.encode(), "(Test \\(parentheses\\))");
}
#[test]
fn test_hex_string() {
let s = PdfHexString::new(vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]);
assert_eq!(s.encode(), "<48656C6C6F>");
}
#[test]
fn test_hex_string_from_text() {
let s = PdfHexString::from_text("Hi");
assert_eq!(s.as_bytes(), &[0xFE, 0xFF, 0x00, 0x48, 0x00, 0x69]);
}
#[test]
fn test_decode_text() {
let s = PdfHexString::from_text("Hello");
assert_eq!(s.decode_text(), "Hello");
}
#[test]
fn test_from_text_ascii() {
let s = PdfString::from_text("Hello World");
assert_eq!(s.as_bytes(), b"Hello World");
}
#[test]
fn test_from_text_unicode() {
let s = PdfString::from_text("你好");
assert_eq!(s.as_bytes()[0], 0xFE);
assert_eq!(s.as_bytes()[1], 0xFF);
assert_eq!(s.decode_text(), "你好");
}
#[test]
fn test_from_text_mixed() {
let s = PdfString::from_text("Hello 世界");
assert_eq!(s.as_bytes()[0], 0xFE);
assert_eq!(s.as_bytes()[1], 0xFF);
assert_eq!(s.decode_text(), "Hello 世界");
}
}