use std::collections::{HashMap, HashSet};
#[allow(dead_code)] pub fn extract_font_references(content: &[u8]) -> HashSet<String> {
let mut font_names = HashSet::new();
let content_str = String::from_utf8_lossy(content);
for line in content_str.lines() {
let tokens: Vec<&str> = line.split_whitespace().collect();
for (i, token) in tokens.iter().enumerate() {
if token.starts_with('/') {
if i + 2 < tokens.len() {
if tokens[i + 2] == "Tf" {
let font_name = token[1..].to_string();
font_names.insert(font_name);
}
}
}
}
}
font_names
}
#[allow(dead_code)] pub fn rename_preserved_fonts(fonts: &crate::objects::Dictionary) -> crate::objects::Dictionary {
let mut renamed = crate::objects::Dictionary::new();
for (key, value) in fonts.iter() {
let new_name = format!("Orig{}", key);
renamed.set(new_name, value.clone());
}
renamed
}
#[allow(dead_code)] pub fn rewrite_font_references(content: &[u8], mappings: &HashMap<String, String>) -> Vec<u8> {
let content_str = String::from_utf8_lossy(content);
let mut result = String::new();
for line in content_str.lines() {
let tokens: Vec<&str> = line.split_whitespace().collect();
let mut rewritten_line = String::new();
let mut i = 0;
while i < tokens.len() {
let token = tokens[i];
if token.starts_with('/') && i + 2 < tokens.len() && tokens[i + 2] == "Tf" {
let font_name = &token[1..];
if let Some(new_name) = mappings.get(font_name) {
rewritten_line.push('/');
rewritten_line.push_str(new_name);
} else {
rewritten_line.push_str(token);
}
} else {
rewritten_line.push_str(token);
}
if i < tokens.len() - 1 {
rewritten_line.push(' ');
}
i += 1;
}
result.push_str(&rewritten_line);
result.push('\n');
}
if !content.ends_with(b"\n") && result.ends_with('\n') {
result.pop();
}
result.into_bytes()
}
#[allow(dead_code)] pub fn has_embedded_font_data(font_dict: &crate::objects::Dictionary) -> bool {
use crate::objects::Object;
if let Some(Object::Dictionary(descriptor)) = font_dict.get("FontDescriptor") {
descriptor.contains_key("FontFile")
|| descriptor.contains_key("FontFile2")
|| descriptor.contains_key("FontFile3")
} else if let Some(Object::Reference(_)) = font_dict.get("FontDescriptor") {
true
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_font_references_simple() {
let content = b"BT /F1 12 Tf (Hello) Tj ET";
let fonts = extract_font_references(content);
assert_eq!(fonts.len(), 1);
assert!(fonts.contains("F1"));
}
#[test]
fn test_extract_font_references_multiple() {
let content = b"BT /F1 12 Tf (Hello) Tj ET BT /F2 10 Tf (World) Tj ET";
let fonts = extract_font_references(content);
assert_eq!(fonts.len(), 2);
assert!(fonts.contains("F1"));
assert!(fonts.contains("F2"));
}
#[test]
fn test_extract_font_references_with_named_fonts() {
let content = b"BT /ArialBold 14 Tf (Test) Tj /Helvetica 10 Tf (More) Tj ET";
let fonts = extract_font_references(content);
assert_eq!(fonts.len(), 2);
assert!(fonts.contains("ArialBold"));
assert!(fonts.contains("Helvetica"));
}
#[test]
fn test_extract_font_references_multiline() {
let content = b"BT\n/F1 12 Tf\n(Line 1) Tj\nET\nBT\n/F2 10 Tf\n(Line 2) Tj\nET";
let fonts = extract_font_references(content);
assert_eq!(fonts.len(), 2);
assert!(fonts.contains("F1"));
assert!(fonts.contains("F2"));
}
#[test]
fn test_extract_font_references_no_fonts() {
let content = b"100 200 m 300 400 l S";
let fonts = extract_font_references(content);
assert_eq!(fonts.len(), 0);
}
#[test]
fn test_extract_font_references_ignore_false_positives() {
let content = b"/Pattern cs /P1 scn 100 100 m 200 200 l S";
let fonts = extract_font_references(content);
assert_eq!(fonts.len(), 0);
}
#[test]
fn test_rename_preserved_fonts_simple() {
use crate::objects::{Dictionary, Object};
let mut fonts = Dictionary::new();
fonts.set("F1", Object::Integer(1));
fonts.set("F2", Object::Integer(2));
let renamed = rename_preserved_fonts(&fonts);
assert_eq!(renamed.len(), 2);
assert!(renamed.contains_key("OrigF1"));
assert!(renamed.contains_key("OrigF2"));
assert!(!renamed.contains_key("F1")); assert!(!renamed.contains_key("F2"));
}
#[test]
fn test_rename_preserved_fonts_named_fonts() {
use crate::objects::{Dictionary, Object};
let mut fonts = Dictionary::new();
fonts.set("Arial", Object::Integer(10));
fonts.set("Helvetica", Object::Integer(20));
fonts.set("TimesNewRoman", Object::Integer(30));
let renamed = rename_preserved_fonts(&fonts);
assert_eq!(renamed.len(), 3);
assert!(renamed.contains_key("OrigArial"));
assert!(renamed.contains_key("OrigHelvetica"));
assert!(renamed.contains_key("OrigTimesNewRoman"));
}
#[test]
fn test_rename_preserved_fonts_preserves_values() {
use crate::objects::{Dictionary, Object};
let mut fonts = Dictionary::new();
fonts.set("F1", Object::Integer(42));
fonts.set("Arial", Object::String("test".to_string()));
let renamed = rename_preserved_fonts(&fonts);
assert_eq!(renamed.get("OrigF1"), Some(&Object::Integer(42)));
assert_eq!(
renamed.get("OrigArial"),
Some(&Object::String("test".to_string()))
);
}
#[test]
fn test_rename_preserved_fonts_empty_dictionary() {
use crate::objects::Dictionary;
let fonts = Dictionary::new();
let renamed = rename_preserved_fonts(&fonts);
assert_eq!(renamed.len(), 0);
}
#[test]
fn test_rename_preserved_fonts_complex_objects() {
use crate::objects::{Dictionary, Object};
let mut fonts = Dictionary::new();
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("Subtype", Object::Name("Type1".to_string()));
font_dict.set("BaseFont", Object::Name("Helvetica".to_string()));
fonts.set("F1", Object::Dictionary(font_dict.clone()));
let renamed = rename_preserved_fonts(&fonts);
assert_eq!(renamed.len(), 1);
assert!(renamed.contains_key("OrigF1"));
if let Some(Object::Dictionary(dict)) = renamed.get("OrigF1") {
assert_eq!(dict.get("Type"), Some(&Object::Name("Font".to_string())));
assert_eq!(
dict.get("Subtype"),
Some(&Object::Name("Type1".to_string()))
);
assert_eq!(
dict.get("BaseFont"),
Some(&Object::Name("Helvetica".to_string()))
);
} else {
panic!("Expected dictionary object");
}
}
#[test]
fn test_rename_preserved_fonts_all_keys_prefixed() {
use crate::objects::{Dictionary, Object};
let mut fonts = Dictionary::new();
fonts.set("F1", Object::Integer(1));
fonts.set("F2", Object::Integer(2));
fonts.set("Arial", Object::Integer(3));
fonts.set("Helvetica", Object::Integer(4));
let renamed = rename_preserved_fonts(&fonts);
for key in renamed.keys() {
assert!(
key.starts_with("Orig"),
"Key '{}' should start with 'Orig'",
key
);
}
}
#[test]
fn test_rewrite_font_references_simple() {
let content = b"BT /F1 12 Tf (Hello) Tj ET";
let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert_eq!(result, "BT /OrigF1 12 Tf (Hello) Tj ET");
}
#[test]
fn test_rewrite_font_references_multiple() {
let content = b"BT /F1 12 Tf (Hello) Tj ET BT /F2 10 Tf (World) Tj ET";
let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
mappings.insert("F2".to_string(), "OrigF2".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert_eq!(
result,
"BT /OrigF1 12 Tf (Hello) Tj ET BT /OrigF2 10 Tf (World) Tj ET"
);
}
#[test]
fn test_rewrite_font_references_named_fonts() {
let content = b"BT /Arial 14 Tf (Test) Tj /Helvetica 10 Tf (More) Tj ET";
let mut mappings = HashMap::new();
mappings.insert("Arial".to_string(), "OrigArial".to_string());
mappings.insert("Helvetica".to_string(), "OrigHelvetica".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert_eq!(
result,
"BT /OrigArial 14 Tf (Test) Tj /OrigHelvetica 10 Tf (More) Tj ET"
);
}
#[test]
fn test_rewrite_font_references_multiline() {
let content = b"BT\n/F1 12 Tf\n(Line 1) Tj\nET\nBT\n/F2 10 Tf\n(Line 2) Tj\nET";
let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
mappings.insert("F2".to_string(), "OrigF2".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/OrigF1 12 Tf"));
assert!(result.contains("/OrigF2 10 Tf"));
assert!(!result.contains("/F1 12 Tf"));
assert!(!result.contains("/F2 10 Tf"));
}
#[test]
fn test_rewrite_font_references_partial_mapping() {
let content = b"BT /F1 12 Tf (Hello) Tj /F2 10 Tf (World) Tj ET";
let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/OrigF1 12 Tf"));
assert!(result.contains("/F2 10 Tf")); assert!(!result.contains("/F1 12 Tf"));
}
#[test]
fn test_rewrite_font_references_no_mappings() {
let content = b"BT /F1 12 Tf (Hello) Tj ET";
let mappings = HashMap::new();
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert_eq!(result, "BT /F1 12 Tf (Hello) Tj ET");
}
#[test]
fn test_rewrite_font_references_non_font_operators() {
let content = b"/Pattern cs /P1 scn 100 100 m 200 200 l S";
let mut mappings = HashMap::new();
mappings.insert("Pattern".to_string(), "OrigPattern".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/Pattern cs"));
assert!(!result.contains("/OrigPattern"));
}
#[test]
fn test_rewrite_font_references_preserves_other_content() {
let content = b"100 200 m 300 400 l S BT /F1 12 Tf (Text) Tj ET q Q";
let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/OrigF1 12 Tf"));
assert!(result.contains("100 200 m"));
assert!(result.contains("300 400 l"));
assert!(result.contains("(Text) Tj"));
}
#[test]
fn test_rewrite_font_references_normalizes_whitespace() {
let content = b"BT /F1 12 Tf (Text) Tj ET"; let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/OrigF1 12 Tf"));
assert!(!result.contains(" /OrigF1"));
}
#[test]
fn test_rewrite_font_references_with_indentation() {
let content = b"BT\n /F1 12 Tf\n 100 700 Td\n (Text) Tj\nET";
let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/OrigF1 12 Tf"));
}
#[test]
fn test_rename_preserved_fonts_no_collision_detection() {
use crate::objects::{Dictionary, Object};
let mut fonts = Dictionary::new();
fonts.set("F1", Object::Integer(1));
fonts.set("OrigF1", Object::Integer(2));
let renamed = rename_preserved_fonts(&fonts);
assert!(renamed.contains_key("OrigF1")); assert!(renamed.contains_key("OrigOrigF1"));
}
#[test]
fn test_rewrite_font_references_with_tabs() {
let content = b"BT\t/F1\t12\tTf\t(Text)\tTj\tET";
let mut mappings = HashMap::new();
mappings.insert("F1".to_string(), "OrigF1".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/OrigF1 12 Tf"));
}
#[test]
fn test_rewrite_font_references_hyphenated_font_names() {
let content = b"BT /Arial-Bold 14 Tf (Text) Tj /Times-Italic 12 Tf (More) Tj ET";
let mut mappings = HashMap::new();
mappings.insert("Arial-Bold".to_string(), "OrigArial-Bold".to_string());
mappings.insert("Times-Italic".to_string(), "OrigTimes-Italic".to_string());
let rewritten = rewrite_font_references(content, &mappings);
let result = String::from_utf8(rewritten).unwrap();
assert!(result.contains("/OrigArial-Bold 14 Tf"));
assert!(result.contains("/OrigTimes-Italic 12 Tf"));
}
#[test]
fn test_has_embedded_font_data_with_fontfile() {
use crate::objects::{Dictionary, Object, ObjectId};
let mut descriptor = Dictionary::new();
descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
descriptor.set("FontFile", Object::Reference(ObjectId::new(10, 0)));
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("FontDescriptor", Object::Dictionary(descriptor));
assert!(has_embedded_font_data(&font_dict));
}
#[test]
fn test_has_embedded_font_data_with_fontfile2() {
use crate::objects::{Dictionary, Object, ObjectId};
let mut descriptor = Dictionary::new();
descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
descriptor.set("FontFile2", Object::Reference(ObjectId::new(20, 0)));
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("FontDescriptor", Object::Dictionary(descriptor));
assert!(has_embedded_font_data(&font_dict));
}
#[test]
fn test_has_embedded_font_data_with_fontfile3() {
use crate::objects::{Dictionary, Object, ObjectId};
let mut descriptor = Dictionary::new();
descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
descriptor.set("FontFile3", Object::Reference(ObjectId::new(30, 0)));
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("FontDescriptor", Object::Dictionary(descriptor));
assert!(has_embedded_font_data(&font_dict));
}
#[test]
fn test_has_embedded_font_data_descriptor_without_streams() {
use crate::objects::{Dictionary, Object};
let mut descriptor = Dictionary::new();
descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
descriptor.set("FontName", Object::Name("Arial".to_string()));
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("FontDescriptor", Object::Dictionary(descriptor));
assert!(!has_embedded_font_data(&font_dict));
}
#[test]
fn test_has_embedded_font_data_descriptor_as_reference() {
use crate::objects::{Dictionary, Object, ObjectId};
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("FontDescriptor", Object::Reference(ObjectId::new(100, 0)));
assert!(has_embedded_font_data(&font_dict));
}
#[test]
fn test_has_embedded_font_data_standard_font() {
use crate::objects::{Dictionary, Object};
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("Subtype", Object::Name("Type1".to_string()));
font_dict.set("BaseFont", Object::Name("Helvetica".to_string()));
assert!(!has_embedded_font_data(&font_dict));
}
#[test]
fn test_has_embedded_font_data_multiple_font_files() {
use crate::objects::{Dictionary, Object, ObjectId};
let mut descriptor = Dictionary::new();
descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
descriptor.set("FontFile2", Object::Reference(ObjectId::new(10, 0)));
descriptor.set("FontFile3", Object::Reference(ObjectId::new(11, 0)));
let mut font_dict = Dictionary::new();
font_dict.set("Type", Object::Name("Font".to_string()));
font_dict.set("FontDescriptor", Object::Dictionary(descriptor));
assert!(has_embedded_font_data(&font_dict));
}
#[test]
fn test_has_embedded_font_data_empty_dict() {
use crate::objects::Dictionary;
let font_dict = Dictionary::new();
assert!(!has_embedded_font_data(&font_dict));
}
}