proposal(redact): unlink unused Type 3 glyph programs and fonts on redaction (F-011)

This commit is contained in:
Dylan M. Taylor
2026-10-26 16:01:29 -04:00
committed by GitHub
parent d28d047a9e
commit 32a9af4d08
6 changed files with 1675 additions and 2 deletions
+6 -1
View File
@@ -477,6 +477,7 @@ fn content_draws(doc: &Document, content: &[Vec<u8>], xo: &Dict, name: &[u8], de
/// left as it is for them. Retired properties are likewise unlinked only when none of the
/// holder's pages or inheriting forms names them. Fails closed when use cannot be determined.
pub(crate) fn unlink_doomed(doc: &mut Document, doomed: &[ObjRef], retired_properties: &[Object]) -> Result<(), RedactError> {
crate::type3::unlink_type3(doc)?;
if doomed.is_empty() && retired_properties.is_empty() {
return Ok(());
}
@@ -677,7 +678,7 @@ fn clip_out(rects: &[[f64; 4]], ctm: &Matrix, around: [f64; 4]) -> Option<Vec<Op
Some(ops)
}
fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> Result<Vec<Op>, CosError> {
pub(crate) fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> Result<Vec<Op>, CosError> {
let parsed = Pieces::join(content).parse();
if parsed.skipped > 0 || parsed.ops.len() > 2_000_000 {
return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect property resources".into() });
@@ -1359,6 +1360,10 @@ fn xobject(
out.error = Some(error);
return None;
}
if let Err(error) = crate::type3::prune_type3_from_resources(doc, &mut cleaned_res, content) {
out.error = Some(error);
return None;
}
}
if cleaned_res != res {
dict.set(b"Resources".to_vec(), Object::Dict(cleaned_res));
+3
View File
@@ -27,6 +27,8 @@ pub mod sanitize;
mod tags;
#[cfg(test)]
mod tests;
pub(crate) mod type3;
pub(crate) mod type3_tables;
use interp::{Mode, Scope, process};
@@ -493,6 +495,7 @@ fn apply_to(doc: &mut Document, pages: Option<&[usize]>) -> Result<Report, Redac
pi,
)?;
retired_properties.extend(interp::prune_properties(doc, &mut res, &cleaned_data)?);
type3::prune_type3_from_resources(doc, &mut res, &cleaned_data)?;
doc.update_dict(page.obj, |d| {
d.set(b"Contents".to_vec(), Object::Array(new_list));
d.set(b"Resources".to_vec(), Object::Dict(res));
+6 -1
View File
@@ -424,6 +424,7 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re
res.set(b"XObject".to_vec(), Object::Dict(xo));
}
retired_properties.extend(crate::interp::prune_properties(doc, &mut res, &page_content)?);
crate::type3::prune_type3_from_resources(doc, &mut res, &page_content)?;
doc.update_dict(page.obj, |d| {
d.set(b"Contents".to_vec(), Object::Array(new_list));
d.set(b"Resources".to_vec(), Object::Dict(res));
@@ -443,7 +444,11 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re
}
// What a page no longer draws must not stay reachable through an inherited or shared
// /Resources either.
crate::interp::unlink_doomed(doc, &doomed, &retired_properties)?;
// Only a removal rewrites the document; a count on a clone has nothing to unlink, and a
// cleanup error there would be mistaken for "nothing to remove".
if write {
crate::interp::unlink_doomed(doc, &doomed, &retired_properties)?;
}
Ok((total, retained_off_layers))
}
@@ -105,3 +105,196 @@ fn an_image_another_page_draws_through_a_form_without_resources_is_kept() {
assert_eq!(apply(&mut doc, None).unwrap().images_removed, 1);
assert!(full_save_holds(&doc, b"IMAGESECRET"), "the second page still draws the image through its form");
}
/// A Type 3 font whose displayed glyphs are redacted has its unused glyph programs unlinked from
/// /CharProcs and exclusive graphical resources pruned, so they do not survive in a full save (F-011).
#[test]
fn redacted_type3_glyph_and_exclusive_resources_do_not_survive_full_save() {
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(),
// Text showing AB with Type 3 font /T3
stream("", b"BT /T3 10 Tf 10 100 Td (AB) Tj ET"),
b"<< /Font << /T3 6 0 R >> >>".to_vec(),
// Type 3 font with /CharProcs << /A 7 0 R /B 8 0 R >> and /Resources with images for both
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 -200 1000 800] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 66 /Widths [500 500] /Encoding << /Differences [65 /A /B] >> /CharProcs << /A 7 0 R /B 8 0 R >> /Resources << /XObject << /ImA 9 0 R /ImB 10 0 R >> >> >>".to_vec(),
stream("", b"/ImA Do % SECRET_GLYPH_A_PAYLOAD"),
stream("", b"/ImB Do % SURVIVING_GLYPH_B_PAYLOAD"),
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"CONFIDENTIAL_IMAGE_A"),
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"PUBLIC_IMAGE_B"),
]);
// Redaction mark covers only 'A' (x: 10..15, y: 95..110)
mark(&mut doc, 0, &[[9.0, 95.0, 14.9, 110.0]], "");
assert!(full_save_holds(&doc, b"SECRET_GLYPH_A_PAYLOAD"));
assert!(full_save_holds(&doc, b"CONFIDENTIAL_IMAGE_A"));
assert!(full_save_holds(&doc, b"SURVIVING_GLYPH_B_PAYLOAD"));
assert!(full_save_holds(&doc, b"PUBLIC_IMAGE_B"));
let report = apply(&mut doc, None).unwrap();
assert_eq!(report.glyphs, 1);
// After redaction, unreferenced glyph A and its exclusive image A must not survive full save
assert!(!full_save_holds(&doc, b"SECRET_GLYPH_A_PAYLOAD"), "redacted glyph A program survived");
assert!(!full_save_holds(&doc, b"CONFIDENTIAL_IMAGE_A"), "exclusive image for redacted glyph A survived");
// Surviving glyph B and its image B must survive
assert!(full_save_holds(&doc, b"SURVIVING_GLYPH_B_PAYLOAD"), "surviving glyph B was lost");
assert!(full_save_holds(&doc, b"PUBLIC_IMAGE_B"), "surviving glyph B image was lost");
// Reopen and inspect the Type 3 font dictionary
let bytes = write_full(&doc, &SaveOptions::default()).unwrap();
let saved = Document::open(Arc::new(bytes)).unwrap();
let page = pdfcraft_model::pages(&saved).swap_remove(0);
let res = saved.resolve(page.dict.get(b"Resources").unwrap()).as_dict().unwrap().clone();
let fonts = saved.resolve(res.get(b"Font").unwrap()).as_dict().unwrap().clone();
let t3 = saved.resolve(fonts.get(b"T3").unwrap()).as_dict().unwrap().clone();
let procs = saved.resolve(t3.get(b"CharProcs").unwrap()).as_dict().unwrap().clone();
assert!(!procs.contains(b"A"), "CharProcs still contains unreferenced glyph A");
assert!(procs.contains(b"B"), "CharProcs lost surviving glyph B");
let t3_res = saved.resolve(t3.get(b"Resources").unwrap()).as_dict().unwrap().clone();
let t3_xo = saved.resolve(t3_res.get(b"XObject").unwrap()).as_dict().unwrap().clone();
assert!(!t3_xo.contains(b"ImA"), "Type 3 font resources still contains exclusive image ImA");
assert!(t3_xo.contains(b"ImB"), "Type 3 font resources lost surviving image ImB");
}
/// When all glyphs of a Type 3 font are redacted, the entire font is removed from Resources (F-011).
#[test]
fn all_redacted_type3_glyphs_cause_font_removal_from_resources() {
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(),
stream("", b"BT /T3 10 Tf 10 100 Td (A) Tj ET"),
b"<< /Font << /T3 6 0 R >> >>".to_vec(),
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 -200 1000 800] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 65 /Widths [500] /Encoding << /Differences [65 /A] >> /CharProcs << /A 7 0 R >> /Resources << /XObject << /ImA 8 0 R >> >> >>".to_vec(),
stream("", b"/ImA Do % SECRET_GLYPH_ONLY_PAYLOAD"),
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"CONFIDENTIAL_IMAGE_ONLY"),
]);
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
let report = apply(&mut doc, None).unwrap();
assert_eq!(report.glyphs, 1);
assert!(!full_save_holds(&doc, b"SECRET_GLYPH_ONLY_PAYLOAD"));
assert!(!full_save_holds(&doc, b"CONFIDENTIAL_IMAGE_ONLY"));
let bytes = write_full(&doc, &SaveOptions::default()).unwrap();
let saved = Document::open(Arc::new(bytes)).unwrap();
let page = pdfcraft_model::pages(&saved).swap_remove(0);
if let Some(res_val) = page.dict.get(b"Resources")
&& let Some(res) = saved.resolve(res_val).as_dict()
&& let Some(fonts_val) = res.get(b"Font")
{
let fonts = saved.resolve(fonts_val).as_dict().unwrap().clone();
assert!(!fonts.contains(b"T3"), "Font /T3 was not removed from Resources");
}
}
/// In shared Type 3 fonts across multiple pages, glyphs used on another page are preserved (F-011).
#[test]
fn shared_type3_font_preserves_glyphs_used_by_another_page() {
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>".to_vec(),
// Page 1: draws 'A' (redacted)
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 5 0 R /Resources 7 0 R >>".to_vec(),
// Page 2: draws 'B' (survives)
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 6 0 R /Resources 7 0 R >>".to_vec(),
stream("", b"BT /T3 10 Tf 10 100 Td (A) Tj ET"),
stream("", b"BT /T3 10 Tf 10 100 Td (B) Tj ET"),
// Shared Resources 7 0 R
b"<< /Font << /T3 8 0 R >> >>".to_vec(),
// Shared Type 3 font 8 0 R
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 -200 1000 800] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 66 /Widths [500 500] /Encoding << /Differences [65 /A /B] >> /CharProcs << /A 9 0 R /B 10 0 R >> >>".to_vec(),
stream("", b"% SECRET_PAGE1_GLYPH_A"),
stream("", b"% PUBLIC_PAGE2_GLYPH_B"),
]);
// Redact only Page 1's 'A'
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
let report = apply(&mut doc, None).unwrap();
assert_eq!(report.glyphs, 1);
// Glyph A was removed from Page 1 and used nowhere else -> unlinked!
assert!(!full_save_holds(&doc, b"SECRET_PAGE1_GLYPH_A"));
// Glyph B is still used on Page 2 -> kept!
assert!(full_save_holds(&doc, b"PUBLIC_PAGE2_GLYPH_B"));
let bytes = write_full(&doc, &SaveOptions::default()).unwrap();
let saved = Document::open(Arc::new(bytes)).unwrap();
let pages = pdfcraft_model::pages(&saved);
let p2_res = saved.resolve(pages[1].dict.get(b"Resources").unwrap()).as_dict().unwrap().clone();
let fonts = saved.resolve(p2_res.get(b"Font").unwrap()).as_dict().unwrap().clone();
let t3 = saved.resolve(fonts.get(b"T3").unwrap()).as_dict().unwrap().clone();
let procs = saved.resolve(t3.get(b"CharProcs").unwrap()).as_dict().unwrap().clone();
assert!(!procs.contains(b"A"));
assert!(procs.contains(b"B"));
}
/// Fails closed when Type 3 font cleanup cannot be safely established (F-011).
#[test]
fn unparseable_type3_font_encoding_fails_closed() {
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(),
stream("", b"BT /T3 10 Tf 10 100 Td (A) Tj ET"),
b"<< /Font << /T3 6 0 R >> >>".to_vec(),
// Unsupported encoding /UnknownCustomEncoding without Differences
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 -200 1000 800] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 65 /Widths [500] /Encoding /UnknownCustomEncoding /CharProcs << /A 7 0 R >> >>".to_vec(),
stream("", b"% SECRET_PAYLOAD_UNPARSEABLE"),
]);
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
let res = apply(&mut doc, None);
assert!(res.is_err(), "redaction must fail closed when Type 3 cleanup cannot be established: {res:?}");
}
/// A Type 3 font selected through an ExtGState /Font entry is a use of the font for pruning:
/// its redacted glyph program does not survive a full save (F-011).
#[test]
fn type3_font_set_through_extgstate_has_its_redacted_glyph_removed() {
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(),
stream("", b"/GS1 gs BT 10 100 Td (A) Tj ET"),
b"<< /Font << /T3 6 0 R >> /ExtGState << /GS1 8 0 R >> >>".to_vec(),
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 -200 1000 800] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 65 /Widths [500] /Encoding << /Differences [65 /A] >> /CharProcs << /A 7 0 R >> >>".to_vec(),
stream("", b"% SECRET_EXTGSTATE_GLYPH_PAYLOAD"),
b"<< /Type /ExtGState /Font [6 0 R 10] >>".to_vec(),
]);
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
let report = apply(&mut doc, None).unwrap();
assert_eq!(report.glyphs, 1);
assert!(!full_save_holds(&doc, b"SECRET_EXTGSTATE_GLYPH_PAYLOAD"), "redacted glyph program survived");
}
/// A Type 3 font used only inside a Form XObject is found by the scan and its redacted glyph
/// program does not survive a full save (F-011).
#[test]
fn type3_font_used_in_a_form_xobject_has_its_redacted_glyph_removed() {
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(),
stream("", b"/Fm Do"),
b"<< /XObject << /Fm 8 0 R >> >>".to_vec(),
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 -200 1000 800] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 65 /Widths [500] /Encoding << /Differences [65 /A] >> /CharProcs << /A 7 0 R >> >>".to_vec(),
stream("", b"% SECRET_FORM_GLYPH_PAYLOAD"),
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Resources 9 0 R", b"BT /T3 10 Tf 10 100 Td (A) Tj ET"),
b"<< /Font << /T3 6 0 R >> >>".to_vec(),
]);
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
let report = apply(&mut doc, None).unwrap();
assert_eq!(report.glyphs, 1);
assert!(!full_save_holds(&doc, b"SECRET_FORM_GLYPH_PAYLOAD"), "redacted glyph program survived");
}
+692
View File
@@ -0,0 +1,692 @@
//! Unlink unused Type 3 font glyph programs and prune exclusive graphical resources (F-011).
use std::collections::{HashMap, HashSet};
use pdfcraft_content::Pieces;
use pdfcraft_cos::{CosError, Dict, Document, ObjRef, Object};
use crate::{RedactError, page_streams};
const MAX_DEPTH: usize = 64;
const MAX_HOLDERS: usize = 1_000_000;
fn res_dict(doc: &Document, res: &Dict, key: &[u8]) -> Dict {
res.get(key).and_then(|o| doc.resolve(o).as_dict().cloned()).unwrap_or_default()
}
/// Identifies a font in the document. Only fonts that are indirect objects are cleaned: a Type 3
/// font held directly in a resource dictionary fails closed (see [`unlink_type3`]), because its
/// glyph programs could not be told apart or rewritten reliably.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum FontKey {
Ref(ObjRef),
}
/// Build the mapping from character code (0..=255) to glyph name in a Type 3 font.
/// Fails closed if the encoding cannot be safely resolved or parsed.
pub(crate) fn type3_code_to_glyph(doc: &Document, font_dict: &Dict) -> Result<[Option<Vec<u8>>; 256], CosError> {
let Some(enc_obj) = font_dict.get(b"Encoding") else {
return Err(CosError::Syntax { offset: 0, detail: "redaction could not establish safe Type 3 font cleanup: missing Encoding".into() });
};
let resolved_enc = doc.resolve(enc_obj);
let mut differences = HashMap::new();
let base_name: Option<Vec<u8>>;
match &*resolved_enc {
Object::Dict(enc_dict) => {
if let Some(diffs) = enc_dict.get(b"Differences").and_then(|d| doc.resolve(d).as_array().cloned()) {
let mut cur_code = 0i64;
for item in &diffs {
match &*doc.resolve(item) {
Object::Int(n) => cur_code = *n,
Object::Name(name) => {
if let Ok(c) = u8::try_from(cur_code) {
differences.insert(c, name.clone());
}
cur_code = cur_code.saturating_add(1);
}
_ => {}
}
}
}
if let Some(base) = enc_dict.name(b"BaseEncoding") {
base_name = Some(base.to_vec());
} else {
// ISO 32000 §9.6.6.1: default base encoding for Type 3 fonts is StandardEncoding.
base_name = Some(b"StandardEncoding".to_vec());
}
}
Object::Name(name) => {
base_name = Some(name.clone());
}
_ => {
return Err(CosError::Syntax {
offset: 0,
detail: "redaction could not establish safe Type 3 font cleanup: invalid Encoding object".into(),
});
}
}
let base_table: Option<&'static [Option<&'static [u8]>; 256]> = match base_name.as_deref() {
Some(b"StandardEncoding") => Some(&crate::type3_tables::STANDARD),
Some(b"WinAnsiEncoding") => Some(&crate::type3_tables::WIN_ANSI),
Some(b"MacRomanEncoding") => Some(&crate::type3_tables::MAC_ROMAN),
Some(b"MacExpertEncoding") => Some(&crate::type3_tables::MAC_EXPERT),
Some(other) => {
return Err(CosError::Syntax {
offset: 0,
detail: format!(
"redaction could not establish safe Type 3 font cleanup: unsupported BaseEncoding: {}",
String::from_utf8_lossy(other)
),
});
}
None => None,
};
let mut map: [Option<Vec<u8>>; 256] = std::array::from_fn(|_| None);
for (code, entry) in map.iter_mut().enumerate() {
let c = code as u8;
if let Some(diff_glyph) = differences.get(&c) {
*entry = Some(diff_glyph.clone());
} else if let Some(table) = base_table
&& let Some(base_glyph) = table[code]
{
*entry = Some(base_glyph.to_vec());
}
}
Ok(map)
}
fn resolve_font_in_res(doc: &Document, res: &Dict, font_name: &[u8]) -> Option<(FontKey, Dict)> {
let fonts = res_dict(doc, res, b"Font");
let font_obj = fonts.get(font_name)?;
// A direct Type 3 font is refused during discovery, so none reaches the scan.
let font_ref = font_obj.as_ref()?;
let dict = doc.resolve(font_obj).as_dict()?.clone();
(dict.name(b"Subtype") == Some(b"Type3")).then_some((FontKey::Ref(font_ref), dict))
}
fn resolve_extgstate_font(doc: &Document, res: &Dict, gs_name: &[u8]) -> Option<(FontKey, Dict)> {
let extg = res_dict(doc, res, b"ExtGState");
let state = doc.resolve(extg.get(gs_name)?).as_dict().cloned()?;
let font_entry = state.get(b"Font").and_then(|f| doc.resolve(f).as_array().cloned())?;
let font_obj = font_entry.first()?;
let font_ref = font_obj.as_ref()?;
let dict = doc.resolve(font_obj).as_dict()?.clone();
(dict.name(b"Subtype") == Some(b"Type3")).then_some((FontKey::Ref(font_ref), dict))
}
#[derive(Default)]
struct UsedInGlyphs {
xobjects: HashSet<Vec<u8>>,
colorspaces: HashSet<Vec<u8>>,
patterns: HashSet<Vec<u8>>,
shadings: HashSet<Vec<u8>>,
extgstates: HashSet<Vec<u8>>,
fonts: HashSet<Vec<u8>>,
properties: HashSet<Vec<u8>>,
}
fn collect_used_resources_in_stream(data: &[u8], used: &mut UsedInGlyphs) -> Result<(), CosError> {
let parsed = pdfcraft_content::parse(data);
if parsed.skipped > 0 {
return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect glyph stream syntax".into() });
}
for op in parsed.ops {
match op.op.as_slice() {
b"Do" => {
if let Some(n) = op.last_name() {
used.xobjects.insert(n.to_vec());
}
}
b"cs" | b"CS" => {
if let Some(n) = op.name(0) {
used.colorspaces.insert(n.to_vec());
}
}
b"scn" | b"SCN" => {
if let Some(n) = op.last_name() {
used.patterns.insert(n.to_vec());
}
}
b"sh" => {
if let Some(n) = op.last_name() {
used.shadings.insert(n.to_vec());
}
}
b"gs" => {
if let Some(n) = op.name(0) {
used.extgstates.insert(n.to_vec());
}
}
b"Tf" => {
if let Some(n) = op.name(0) {
used.fonts.insert(n.to_vec());
}
}
b"BDC" | b"DP" => {
if let Some(n) = op.name(1) {
used.properties.insert(n.to_vec());
}
}
_ => {}
}
}
Ok(())
}
fn prune_type3_font_resources(doc: &Document, font_res: &mut Dict, used: &UsedInGlyphs) -> bool {
let mut changed = false;
macro_rules! prune_category {
($cat:expr, $used_set:expr) => {
if let Some(sub_obj) = font_res.get($cat) {
let mut sub = doc.resolve(sub_obj).as_dict().cloned().unwrap_or_default();
let names: Vec<Vec<u8>> = sub.iter().map(|(n, _)| n.to_vec()).collect();
let mut cat_changed = false;
for n in names {
if !$used_set.contains(&n) {
sub.remove(&n);
cat_changed = true;
}
}
if cat_changed {
changed = true;
if sub.is_empty() {
font_res.remove($cat);
} else {
font_res.set($cat.to_vec(), Object::Dict(sub));
}
}
}
};
}
prune_category!(b"XObject", used.xobjects);
prune_category!(b"ColorSpace", used.colorspaces);
prune_category!(b"Pattern", used.patterns);
prune_category!(b"Shading", used.shadings);
prune_category!(b"ExtGState", used.extgstates);
prune_category!(b"Font", used.fonts);
prune_category!(b"Properties", used.properties);
changed
}
struct ScanContext<'a> {
doc: &'a Document,
font_maps: &'a HashMap<FontKey, [Option<Vec<u8>>; 256]>,
used_glyphs: &'a mut HashMap<FontKey, HashSet<Vec<u8>>>,
/// A form scanned with its inherited font and the resources it draws through: the same form
/// drawn in another context is scanned again.
seen_forms: &'a mut HashSet<(ObjRef, Option<FontKey>, ObjRef)>,
}
fn scan_content_stream(
ctx: &mut ScanContext<'_>,
content: &[Vec<u8>],
holder_ref: ObjRef,
res: &Dict,
initial_font: Option<FontKey>,
depth: usize,
) -> Result<(), CosError> {
if depth > MAX_DEPTH {
return Err(CosError::Syntax { offset: 0, detail: "redaction exceeded max depth inspecting Type 3 font content".into() });
}
let joined = Pieces::join(content);
let parsed = joined.parse();
if parsed.skipped > 0 {
return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect content stream syntax".into() });
}
let mut stack: Vec<Option<FontKey>> = Vec::new();
let mut current_font = initial_font;
for op in parsed.ops {
match op.op.as_slice() {
b"q" => stack.push(current_font),
b"Q" => {
if let Some(saved) = stack.pop() {
current_font = saved;
}
}
b"Do" => {
if let Some(name) = op.last_name() {
scan_xobject(ctx, res, holder_ref, name, current_font, depth)?;
}
}
_ => apply_text_op(ctx, res, &op, &mut current_font),
}
}
Ok(())
}
/// Apply an operator that changes or uses the current font: `Tf`, `gs`, and the text-showing
/// operators, which record the glyphs they draw.
fn apply_text_op(ctx: &mut ScanContext<'_>, res: &Dict, op: &pdfcraft_content::Op, current_font: &mut Option<FontKey>) {
match op.op.as_slice() {
b"Tf" => {
if let Some(name) = op.name(0) {
*current_font = resolve_font_in_res(ctx.doc, res, name).map(|(k, _)| k);
}
}
b"gs" => {
if let Some(name) = op.name(0)
&& let Some((k, _)) = resolve_extgstate_font(ctx.doc, res, name)
{
*current_font = Some(k);
}
}
b"Tj" | b"'" | b"\"" => {
if let Some(s) = op.operands.last().and_then(Object::as_string) {
mark_shown(ctx, *current_font, &s.bytes);
}
}
b"TJ" => {
if let Some(arr) = op.operands.last().and_then(Object::as_array) {
for item in arr {
if let Some(s) = item.as_string() {
mark_shown(ctx, *current_font, &s.bytes);
}
}
}
}
_ => {}
}
}
/// Record every glyph of `font` that the shown string `bytes` uses.
fn mark_shown(ctx: &mut ScanContext<'_>, font: Option<FontKey>, bytes: &[u8]) {
let Some(fk) = font else { return };
let Some(map) = ctx.font_maps.get(&fk) else { return };
for b in bytes {
if let Some(g) = &map[*b as usize] {
ctx.used_glyphs.entry(fk).or_default().insert(g.clone());
}
}
}
/// Scan the Form XObject `name` drawn by a `Do` operator, once per (form, font, drawing holder).
fn scan_xobject(
ctx: &mut ScanContext<'_>,
res: &Dict,
holder_ref: ObjRef,
name: &[u8],
current_font: Option<FontKey>,
depth: usize,
) -> Result<(), CosError> {
let xo = res_dict(ctx.doc, res, b"XObject");
let Some(r) = xo.get(name).and_then(Object::as_ref) else { return Ok(()) };
let obj = ctx.doc.get(r);
let Object::Stream(s) = &*obj else { return Ok(()) };
if s.dict.name(b"Subtype") != Some(b"Form") {
return Ok(());
}
let form_own_res = s.dict.get(b"Resources").and_then(|ro| ctx.doc.resolve(ro).as_dict().cloned());
// A form with resources of its own draws through them; one without draws through its caller's.
let through = if form_own_res.is_some() { r } else { holder_ref };
if !ctx.seen_forms.insert((r, current_font, through)) {
return Ok(());
}
let data = s.decoded()?;
match form_own_res {
Some(form_res) => scan_content_stream(ctx, std::slice::from_ref(&data), r, &form_res, current_font, depth + 1),
None => scan_content_stream(ctx, std::slice::from_ref(&data), holder_ref, res, current_font, depth + 1),
}
}
/// The font object an ExtGState's `/Font` entry names (its first element), if any.
fn gs_font_object(doc: &Document, state_obj: &Object) -> Option<Object> {
let state = doc.resolve(state_obj).as_dict()?.clone();
let font_entry = doc.resolve(state.get(b"Font")?).as_array()?.clone();
font_entry.into_iter().next()
}
/// Prune Type 3 fonts from a rewritten page or form's `/Resources` dictionary if the rewritten
/// content no longer draws any text using that font name.
pub(crate) fn prune_type3_from_resources(doc: &Document, res: &mut Dict, content: &[Vec<u8>]) -> Result<(), CosError> {
let mut fonts = res_dict(doc, res, b"Font");
if fonts.is_empty() {
return Ok(());
}
// The operators of forms that draw through these resources count too: dropping a font a child
// form still shows would leave its text without one.
let ops = crate::interp::resource_ops(doc, res, content)?;
let mut drawn_font_names = HashSet::new();
for op in &ops {
if op.is("Tf") {
if let Some(name) = op.name(0) {
drawn_font_names.insert(name.to_vec());
}
} else if op.is("gs")
&& let Some(name) = op.name(0)
{
let extg = res_dict(doc, res, b"ExtGState");
if let Some(font_obj) = extg.get(name).and_then(|state_obj| gs_font_object(doc, state_obj)) {
drawn_font_names.extend(
fonts
.iter()
.filter(|(_, fn_obj)| fn_obj.as_ref().is_some() && fn_obj.as_ref() == font_obj.as_ref())
.map(|(fn_name, _)| fn_name.to_vec()),
);
}
}
}
let mut changed = false;
let font_names: Vec<Vec<u8>> = fonts.iter().map(|(n, _)| n.to_vec()).collect();
for name in font_names {
if let Some(font_obj) = fonts.get(&name) {
let resolved = doc.resolve(font_obj);
if let Some(d) = resolved.as_dict()
&& d.name(b"Subtype") == Some(b"Type3")
&& !drawn_font_names.contains(&name)
{
fonts.remove(&name);
changed = true;
}
}
}
if changed {
if fonts.is_empty() {
res.remove(b"Font");
} else {
res.set(b"Font".to_vec(), Object::Dict(fonts));
}
}
Ok(())
}
/// Unlink unused Type 3 glyph programs from `CharProcs`, prune their exclusive resources from the
/// font's `/Resources`, and remove completely unused Type 3 fonts across all reachable resource
/// holders in the document. Fails closed if safe cleanup cannot be established.
pub(crate) fn unlink_type3(doc: &mut Document) -> Result<(), RedactError> {
let Some(top) = doc.root().and_then(|root| doc.get(root).as_dict().and_then(|d| d.reference(b"Pages"))) else {
return Ok(());
};
// Find all page-tree nodes with /Resources of their own, and the pages that draw through them.
let mut holders: Vec<(ObjRef, Vec<ObjRef>)> = Vec::new();
let mut seen = HashSet::new();
let mut stack: Vec<(ObjRef, Option<usize>)> = vec![(top, None)];
while let Some((node, mut holder)) = stack.pop() {
if !seen.insert(node) || seen.len() > MAX_HOLDERS {
continue;
}
let obj = doc.get(node);
let Some(d) = obj.as_dict() else { continue };
if d.contains(b"Resources") {
holders.push((node, Vec::new()));
holder = Some(holders.len() - 1);
}
let is_pages = d.name(b"Type") == Some(b"Pages") || (d.contains(b"Kids") && d.name(b"Type") != Some(b"Page"));
if is_pages {
let kids = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()).unwrap_or_default();
stack.extend(kids.iter().filter_map(Object::as_ref).map(|k| (k, holder)));
} else if let Some(h) = holder.and_then(|i| holders.get_mut(i)) {
h.1.push(node);
}
}
let form_holders = reachable_form_holders(doc, &holders)?;
// Discover all Type 3 fonts in holders and forms.
let mut type3_fonts: HashMap<FontKey, Dict> = HashMap::new();
for (node, _) in &holders {
if let Some(res) = doc.get(*node).as_dict().and_then(|d| d.get(b"Resources")).and_then(|r| doc.resolve(r).as_dict().cloned()) {
collect_type3_fonts(doc, &res, &mut type3_fonts)?;
}
}
for (_, form_res) in &form_holders {
collect_type3_fonts(doc, form_res, &mut type3_fonts)?;
}
if type3_fonts.is_empty() {
return Ok(());
}
// Build code-to-glyph mappings for each Type 3 font. Fails closed on error.
let mut font_maps: HashMap<FontKey, [Option<Vec<u8>>; 256]> = HashMap::new();
for (key, font_dict) in &type3_fonts {
let map = type3_code_to_glyph(doc, font_dict)?;
font_maps.insert(*key, map);
}
// Scan all page contents and reachable forms to discover surviving glyph usage.
let page_list = pdfcraft_model::pages(doc);
let mut used_glyphs: HashMap<FontKey, HashSet<Vec<u8>>> = HashMap::new();
let mut seen_drawn_forms = HashSet::new();
let mut ctx = ScanContext { doc, font_maps: &font_maps, used_glyphs: &mut used_glyphs, seen_forms: &mut seen_drawn_forms };
for (pi, p) in page_list.iter().enumerate() {
let (_, content) = page_streams(ctx.doc, &p.dict, pi)?;
let page_res = p
.dict
.get(b"Resources")
.and_then(|r| ctx.doc.resolve(r).as_dict().cloned())
.or_else(|| {
holders.iter().find(|(_, users)| users.contains(&p.obj)).and_then(|(h, _)| {
ctx.doc.get(*h).as_dict().and_then(|d| d.get(b"Resources")).and_then(|ro| ctx.doc.resolve(ro).as_dict().cloned())
})
})
.unwrap_or_default();
scan_content_stream(&mut ctx, &content, p.obj, &page_res, None, 0)?;
}
// Also scan any surviving form that wasn't reached by page streams directly.
for (form_ref, form_res) in &form_holders {
if !ctx.seen_forms.iter().any(|(form, _, _)| form == form_ref) {
let Object::Stream(s) = &*ctx.doc.get(*form_ref) else { continue };
let data = s.decoded()?;
scan_content_stream(&mut ctx, std::slice::from_ref(&data), *form_ref, form_res, None, 0)?;
}
}
// For each Type 3 font: unlink unused glyphs from CharProcs and prune exclusive resources.
for (font_key, font_dict) in &type3_fonts {
let surviving = used_glyphs.get(font_key).cloned().unwrap_or_default();
clean_type3_font(doc, &holders, &form_holders, font_key, font_dict, &surviving)?;
}
Ok(())
}
/// Every reachable Form XObject with resources of its own, with those resources, found by
/// walking the resources of the holders and of the forms they draw.
fn reachable_form_holders(doc: &Document, holders: &[(ObjRef, Vec<ObjRef>)]) -> Result<Vec<(ObjRef, Dict)>, CosError> {
let mut form_holders: Vec<(ObjRef, Dict)> = Vec::new();
let mut pending_res: Vec<Dict> = holders
.iter()
.filter_map(|(r, _)| doc.get(*r).as_dict().and_then(|d| d.get(b"Resources")).and_then(|ro| doc.resolve(ro).as_dict().cloned()))
.collect();
let mut seen_forms = HashSet::new();
while let Some(res) = pending_res.pop() {
for (_, value) in res_dict(doc, &res, b"XObject").iter() {
let Some(reference) = value.as_ref() else { continue };
if !seen_forms.insert(reference) {
continue;
}
if seen_forms.len() > MAX_HOLDERS {
return Err(CosError::Syntax { offset: 0, detail: "redaction exceeded max depth inspecting form resources".into() });
}
let object = doc.get(reference);
let Object::Stream(form) = &*object else { continue };
if form.dict.name(b"Subtype") != Some(b"Form") {
continue;
}
if let Some(r) = form.dict.get(b"Resources").and_then(|ro| doc.resolve(ro).as_dict().cloned()) {
form_holders.push((reference, r.clone()));
pending_res.push(r);
}
}
}
Ok(form_holders)
}
/// Record every Type 3 font named by a resource dictionary's `/Font` and `/ExtGState` entries.
/// A Type 3 font held directly in a resource dictionary can neither be named reliably nor have its
/// glyph programs rewritten, so redaction fails closed rather than leaving its glyphs unchecked.
fn collect_type3_fonts(doc: &Document, res: &Dict, t3_fonts: &mut HashMap<FontKey, Dict>) -> Result<(), CosError> {
let direct = || CosError::Syntax {
offset: 0,
detail: "redaction could not establish safe Type 3 font cleanup: a Type 3 font is not an indirect object".into(),
};
let mut consider = |font_obj: &Object| -> Result<(), CosError> {
if let Some(dict) = doc.resolve(font_obj).as_dict().cloned()
&& dict.name(b"Subtype") == Some(b"Type3")
{
let font_ref = font_obj.as_ref().ok_or_else(direct)?;
t3_fonts.entry(FontKey::Ref(font_ref)).or_insert(dict);
}
Ok(())
};
for (_, font_obj) in res_dict(doc, res, b"Font").iter() {
consider(font_obj)?;
}
for (_, state_obj) in res_dict(doc, res, b"ExtGState").iter() {
if let Some(font_obj) = gs_font_object(doc, state_obj) {
consider(&font_obj)?;
}
}
Ok(())
}
/// Unlink the glyph programs of one Type 3 font that no longer draw, then either remove the font
/// (when no glyph survives) or prune the resources only its surviving glyphs used.
fn clean_type3_font(
doc: &mut Document,
holders: &[(ObjRef, Vec<ObjRef>)],
form_holders: &[(ObjRef, Dict)],
font_key: &FontKey,
font_dict: &Dict,
surviving: &HashSet<Vec<u8>>,
) -> Result<(), RedactError> {
let Some(char_procs_val) = font_dict.get(b"CharProcs") else {
return Err(CosError::Syntax { offset: 0, detail: "redaction could not establish safe Type 3 font cleanup: missing CharProcs".into() }.into());
};
let mut char_procs = doc.resolve(char_procs_val).as_dict().cloned().ok_or_else(|| CosError::Syntax {
offset: 0,
detail: "redaction could not establish safe Type 3 font cleanup: CharProcs is not a dict".into(),
})?;
let mut procs_changed = false;
let glyph_names: Vec<Vec<u8>> = char_procs.iter().map(|(n, _)| n.to_vec()).collect();
for g in glyph_names {
if !surviving.contains(&g) {
char_procs.remove(&g);
procs_changed = true;
}
}
let FontKey::Ref(fr) = font_key;
if char_procs.is_empty() {
// The font has no surviving glyphs: clear its own glyph programs and resources, then
// remove font from /Font across all holders.
doc.update_dict(*fr, |d| {
d.remove(b"CharProcs");
d.remove(b"Resources");
})?;
return remove_font_from_holders(doc, holders, form_holders, font_key);
}
// A CharProcs object can be shared with another Type 3 font, so never rewrite it in place:
// give this font its own filtered dictionary instead.
if procs_changed {
doc.update_dict(*fr, |d| {
d.set(b"CharProcs".to_vec(), Object::Dict(char_procs.clone()));
})?;
}
// Clean exclusive resources from the Type 3 font's /Resources.
let Some(f_res_val) = font_dict.get(b"Resources") else { return Ok(()) };
let mut f_res = doc.resolve(f_res_val).as_dict().cloned().unwrap_or_default();
let mut used_in_glyphs = UsedInGlyphs::default();
for (_, proc_val) in char_procs.iter() {
let resolved = doc.resolve(proc_val);
let Object::Stream(s) = &*resolved else {
return Err(CosError::Syntax {
offset: 0,
detail: "redaction could not establish safe Type 3 font cleanup: CharProc is not a stream".into(),
}
.into());
};
let data = s.decoded()?;
collect_used_resources_in_stream(&data, &mut used_in_glyphs)?;
}
if prune_type3_font_resources(doc, &mut f_res, &used_in_glyphs) {
// The pruned dictionary goes into the font object: a font's /Resources object is
// often the page's own, and rewriting that would strip the page of its resources.
doc.update_dict(*fr, |d| {
if f_res.is_empty() {
d.remove(b"Resources");
} else {
d.set(b"Resources".to_vec(), Object::Dict(f_res.clone()));
}
})?;
}
Ok(())
}
fn remove_font_from_holders(
doc: &mut Document,
holders: &[(ObjRef, Vec<ObjRef>)],
form_holders: &[(ObjRef, Dict)],
target_key: &FontKey,
) -> Result<(), RedactError> {
let clean_res = |doc: &Document, res: &mut Dict| -> bool {
let mut fonts = res_dict(doc, res, b"Font");
let mut changed = false;
let names: Vec<Vec<u8>> = fonts.iter().map(|(n, _)| n.to_vec()).collect();
for n in names {
if let Some(val) = fonts.get(&n)
&& val.as_ref().map(FontKey::Ref) == Some(*target_key)
{
fonts.remove(&n);
changed = true;
}
}
if changed {
if fonts.is_empty() {
res.remove(b"Font");
} else {
res.set(b"Font".to_vec(), Object::Dict(fonts));
}
}
changed
};
for (node, _) in holders {
if let Some(mut res) = doc.get(*node).as_dict().and_then(|d| d.get(b"Resources")).and_then(|r| doc.resolve(r).as_dict().cloned())
&& clean_res(doc, &mut res)
{
doc.update_dict(*node, |d| {
d.set(b"Resources".to_vec(), Object::Dict(res));
})?;
}
}
for (form_ref, _) in form_holders {
let obj = doc.get(*form_ref);
if let Object::Stream(s) = &*obj
&& let Some(mut res) = s.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned())
&& clean_res(doc, &mut res)
{
doc.update_dict(*form_ref, |d| {
d.set(b"Resources".to_vec(), Object::Dict(res));
})?;
}
}
Ok(())
}
+775
View File
@@ -0,0 +1,775 @@
//! Predefined font encodings (ISO 32000 Annex D) mapping character codes to glyph names.
//! Each encoding lists only the codes it maps; every other code is unmapped.
type Entries = &'static [(u8, &'static [u8])];
const fn build(entries: Entries) -> [Option<&'static [u8]>; 256] {
let mut out: [Option<&'static [u8]>; 256] = [None; 256];
let mut i = 0;
while i < entries.len() {
out[entries[i].0 as usize] = Some(entries[i].1);
i += 1;
}
out
}
const STANDARD_ENTRIES: Entries = &[
(32, b"space"),
(33, b"exclam"),
(34, b"quotedbl"),
(35, b"numbersign"),
(36, b"dollar"),
(37, b"percent"),
(38, b"ampersand"),
(39, b"quoteright"),
(40, b"parenleft"),
(41, b"parenright"),
(42, b"asterisk"),
(43, b"plus"),
(44, b"comma"),
(45, b"hyphen"),
(46, b"period"),
(47, b"slash"),
(48, b"zero"),
(49, b"one"),
(50, b"two"),
(51, b"three"),
(52, b"four"),
(53, b"five"),
(54, b"six"),
(55, b"seven"),
(56, b"eight"),
(57, b"nine"),
(58, b"colon"),
(59, b"semicolon"),
(60, b"less"),
(61, b"equal"),
(62, b"greater"),
(63, b"question"),
(64, b"at"),
(65, b"A"),
(66, b"B"),
(67, b"C"),
(68, b"D"),
(69, b"E"),
(70, b"F"),
(71, b"G"),
(72, b"H"),
(73, b"I"),
(74, b"J"),
(75, b"K"),
(76, b"L"),
(77, b"M"),
(78, b"N"),
(79, b"O"),
(80, b"P"),
(81, b"Q"),
(82, b"R"),
(83, b"S"),
(84, b"T"),
(85, b"U"),
(86, b"V"),
(87, b"W"),
(88, b"X"),
(89, b"Y"),
(90, b"Z"),
(91, b"bracketleft"),
(92, b"backslash"),
(93, b"bracketright"),
(94, b"asciicircum"),
(95, b"underscore"),
(96, b"quoteleft"),
(97, b"a"),
(98, b"b"),
(99, b"c"),
(100, b"d"),
(101, b"e"),
(102, b"f"),
(103, b"g"),
(104, b"h"),
(105, b"i"),
(106, b"j"),
(107, b"k"),
(108, b"l"),
(109, b"m"),
(110, b"n"),
(111, b"o"),
(112, b"p"),
(113, b"q"),
(114, b"r"),
(115, b"s"),
(116, b"t"),
(117, b"u"),
(118, b"v"),
(119, b"w"),
(120, b"x"),
(121, b"y"),
(122, b"z"),
(123, b"braceleft"),
(124, b"bar"),
(125, b"braceright"),
(126, b"asciitilde"),
(161, b"exclamdown"),
(162, b"cent"),
(163, b"sterling"),
(164, b"fraction"),
(165, b"yen"),
(166, b"florin"),
(167, b"section"),
(168, b"currency"),
(169, b"quotesingle"),
(170, b"quotedblleft"),
(171, b"guillemotleft"),
(172, b"guilsinglleft"),
(173, b"guilsinglright"),
(174, b"fi"),
(175, b"fl"),
(177, b"endash"),
(178, b"dagger"),
(179, b"daggerdbl"),
(180, b"periodcentered"),
(182, b"paragraph"),
(183, b"bullet"),
(184, b"quotesinglbase"),
(185, b"quotedblbase"),
(186, b"quotedblright"),
(187, b"guillemotright"),
(188, b"ellipsis"),
(189, b"perthousand"),
(191, b"questiondown"),
(193, b"grave"),
(194, b"acute"),
(195, b"circumflex"),
(196, b"tilde"),
(197, b"macron"),
(198, b"breve"),
(199, b"dotaccent"),
(200, b"dieresis"),
(202, b"ring"),
(203, b"cedilla"),
(205, b"hungarumlaut"),
(206, b"ogonek"),
(207, b"caron"),
(208, b"emdash"),
(225, b"AE"),
(227, b"ordfeminine"),
(232, b"Lslash"),
(233, b"Oslash"),
(234, b"OE"),
(235, b"ordmasculine"),
(241, b"ae"),
(245, b"dotlessi"),
(248, b"lslash"),
(249, b"oslash"),
(250, b"oe"),
(251, b"germandbls"),
];
pub(crate) static STANDARD: [Option<&[u8]>; 256] = build(STANDARD_ENTRIES);
const WIN_ANSI_ENTRIES: Entries = &[
(32, b"space"),
(33, b"exclam"),
(34, b"quotedbl"),
(35, b"numbersign"),
(36, b"dollar"),
(37, b"percent"),
(38, b"ampersand"),
(39, b"quotesingle"),
(40, b"parenleft"),
(41, b"parenright"),
(42, b"asterisk"),
(43, b"plus"),
(44, b"comma"),
(45, b"hyphen"),
(46, b"period"),
(47, b"slash"),
(48, b"zero"),
(49, b"one"),
(50, b"two"),
(51, b"three"),
(52, b"four"),
(53, b"five"),
(54, b"six"),
(55, b"seven"),
(56, b"eight"),
(57, b"nine"),
(58, b"colon"),
(59, b"semicolon"),
(60, b"less"),
(61, b"equal"),
(62, b"greater"),
(63, b"question"),
(64, b"at"),
(65, b"A"),
(66, b"B"),
(67, b"C"),
(68, b"D"),
(69, b"E"),
(70, b"F"),
(71, b"G"),
(72, b"H"),
(73, b"I"),
(74, b"J"),
(75, b"K"),
(76, b"L"),
(77, b"M"),
(78, b"N"),
(79, b"O"),
(80, b"P"),
(81, b"Q"),
(82, b"R"),
(83, b"S"),
(84, b"T"),
(85, b"U"),
(86, b"V"),
(87, b"W"),
(88, b"X"),
(89, b"Y"),
(90, b"Z"),
(91, b"bracketleft"),
(92, b"backslash"),
(93, b"bracketright"),
(94, b"asciicircum"),
(95, b"underscore"),
(96, b"grave"),
(97, b"a"),
(98, b"b"),
(99, b"c"),
(100, b"d"),
(101, b"e"),
(102, b"f"),
(103, b"g"),
(104, b"h"),
(105, b"i"),
(106, b"j"),
(107, b"k"),
(108, b"l"),
(109, b"m"),
(110, b"n"),
(111, b"o"),
(112, b"p"),
(113, b"q"),
(114, b"r"),
(115, b"s"),
(116, b"t"),
(117, b"u"),
(118, b"v"),
(119, b"w"),
(120, b"x"),
(121, b"y"),
(122, b"z"),
(123, b"braceleft"),
(124, b"bar"),
(125, b"braceright"),
(126, b"asciitilde"),
(128, b"Euro"),
(130, b"quotesinglbase"),
(131, b"florin"),
(132, b"quotedblbase"),
(133, b"ellipsis"),
(134, b"dagger"),
(135, b"daggerdbl"),
(136, b"circumflex"),
(137, b"perthousand"),
(138, b"Scaron"),
(139, b"guilsinglleft"),
(140, b"OE"),
(142, b"Zcaron"),
(145, b"quoteleft"),
(146, b"quoteright"),
(147, b"quotedblleft"),
(148, b"quotedblright"),
(149, b"bullet"),
(150, b"endash"),
(151, b"emdash"),
(152, b"tilde"),
(153, b"trademark"),
(154, b"scaron"),
(155, b"guilsinglright"),
(156, b"oe"),
(158, b"zcaron"),
(159, b"Ydieresis"),
(160, b"space"),
(161, b"exclamdown"),
(162, b"cent"),
(163, b"sterling"),
(164, b"currency"),
(165, b"yen"),
(166, b"brokenbar"),
(167, b"section"),
(168, b"dieresis"),
(169, b"copyright"),
(170, b"ordfeminine"),
(171, b"guillemotleft"),
(172, b"logicalnot"),
(173, b"hyphen"),
(174, b"registered"),
(175, b"macron"),
(176, b"degree"),
(177, b"plusminus"),
(178, b"twosuperior"),
(179, b"threesuperior"),
(180, b"acute"),
(181, b"mu"),
(182, b"paragraph"),
(183, b"periodcentered"),
(184, b"cedilla"),
(185, b"onesuperior"),
(186, b"ordmasculine"),
(187, b"guillemotright"),
(188, b"onequarter"),
(189, b"onehalf"),
(190, b"threequarters"),
(191, b"questiondown"),
(192, b"Agrave"),
(193, b"Aacute"),
(194, b"Acircumflex"),
(195, b"Atilde"),
(196, b"Adieresis"),
(197, b"Aring"),
(198, b"AE"),
(199, b"Ccedilla"),
(200, b"Egrave"),
(201, b"Eacute"),
(202, b"Ecircumflex"),
(203, b"Edieresis"),
(204, b"Igrave"),
(205, b"Iacute"),
(206, b"Icircumflex"),
(207, b"Idieresis"),
(208, b"Eth"),
(209, b"Ntilde"),
(210, b"Ograve"),
(211, b"Oacute"),
(212, b"Ocircumflex"),
(213, b"Otilde"),
(214, b"Odieresis"),
(215, b"multiply"),
(216, b"Oslash"),
(217, b"Ugrave"),
(218, b"Uacute"),
(219, b"Ucircumflex"),
(220, b"Udieresis"),
(221, b"Yacute"),
(222, b"Thorn"),
(223, b"germandbls"),
(224, b"agrave"),
(225, b"aacute"),
(226, b"acircumflex"),
(227, b"atilde"),
(228, b"adieresis"),
(229, b"aring"),
(230, b"ae"),
(231, b"ccedilla"),
(232, b"egrave"),
(233, b"eacute"),
(234, b"ecircumflex"),
(235, b"edieresis"),
(236, b"igrave"),
(237, b"iacute"),
(238, b"icircumflex"),
(239, b"idieresis"),
(240, b"eth"),
(241, b"ntilde"),
(242, b"ograve"),
(243, b"oacute"),
(244, b"ocircumflex"),
(245, b"otilde"),
(246, b"odieresis"),
(247, b"divide"),
(248, b"oslash"),
(249, b"ugrave"),
(250, b"uacute"),
(251, b"ucircumflex"),
(252, b"udieresis"),
(253, b"yacute"),
(254, b"thorn"),
(255, b"ydieresis"),
];
pub(crate) static WIN_ANSI: [Option<&[u8]>; 256] = build(WIN_ANSI_ENTRIES);
const MAC_ROMAN_ENTRIES: Entries = &[
(32, b"space"),
(33, b"exclam"),
(34, b"quotedbl"),
(35, b"numbersign"),
(36, b"dollar"),
(37, b"percent"),
(38, b"ampersand"),
(39, b"quotesingle"),
(40, b"parenleft"),
(41, b"parenright"),
(42, b"asterisk"),
(43, b"plus"),
(44, b"comma"),
(45, b"hyphen"),
(46, b"period"),
(47, b"slash"),
(48, b"zero"),
(49, b"one"),
(50, b"two"),
(51, b"three"),
(52, b"four"),
(53, b"five"),
(54, b"six"),
(55, b"seven"),
(56, b"eight"),
(57, b"nine"),
(58, b"colon"),
(59, b"semicolon"),
(60, b"less"),
(61, b"equal"),
(62, b"greater"),
(63, b"question"),
(64, b"at"),
(65, b"A"),
(66, b"B"),
(67, b"C"),
(68, b"D"),
(69, b"E"),
(70, b"F"),
(71, b"G"),
(72, b"H"),
(73, b"I"),
(74, b"J"),
(75, b"K"),
(76, b"L"),
(77, b"M"),
(78, b"N"),
(79, b"O"),
(80, b"P"),
(81, b"Q"),
(82, b"R"),
(83, b"S"),
(84, b"T"),
(85, b"U"),
(86, b"V"),
(87, b"W"),
(88, b"X"),
(89, b"Y"),
(90, b"Z"),
(91, b"bracketleft"),
(92, b"backslash"),
(93, b"bracketright"),
(94, b"asciicircum"),
(95, b"underscore"),
(96, b"grave"),
(97, b"a"),
(98, b"b"),
(99, b"c"),
(100, b"d"),
(101, b"e"),
(102, b"f"),
(103, b"g"),
(104, b"h"),
(105, b"i"),
(106, b"j"),
(107, b"k"),
(108, b"l"),
(109, b"m"),
(110, b"n"),
(111, b"o"),
(112, b"p"),
(113, b"q"),
(114, b"r"),
(115, b"s"),
(116, b"t"),
(117, b"u"),
(118, b"v"),
(119, b"w"),
(120, b"x"),
(121, b"y"),
(122, b"z"),
(123, b"braceleft"),
(124, b"bar"),
(125, b"braceright"),
(126, b"asciitilde"),
(128, b"Adieresis"),
(129, b"Aring"),
(130, b"Ccedilla"),
(131, b"Eacute"),
(132, b"Ntilde"),
(133, b"Odieresis"),
(134, b"Udieresis"),
(135, b"aacute"),
(136, b"agrave"),
(137, b"acircumflex"),
(138, b"adieresis"),
(139, b"atilde"),
(140, b"aring"),
(141, b"ccedilla"),
(142, b"eacute"),
(143, b"egrave"),
(144, b"ecircumflex"),
(145, b"edieresis"),
(146, b"iacute"),
(147, b"igrave"),
(148, b"icircumflex"),
(149, b"idieresis"),
(150, b"ntilde"),
(151, b"oacute"),
(152, b"ograve"),
(153, b"ocircumflex"),
(154, b"odieresis"),
(155, b"otilde"),
(156, b"uacute"),
(157, b"ugrave"),
(158, b"ucircumflex"),
(159, b"udieresis"),
(160, b"dagger"),
(161, b"degree"),
(162, b"cent"),
(163, b"sterling"),
(164, b"section"),
(165, b"bullet"),
(166, b"paragraph"),
(167, b"germandbls"),
(168, b"registered"),
(169, b"copyright"),
(170, b"trademark"),
(171, b"acute"),
(172, b"dieresis"),
(174, b"AE"),
(175, b"Oslash"),
(177, b"plusminus"),
(180, b"yen"),
(181, b"mu"),
(187, b"ordfeminine"),
(188, b"ordmasculine"),
(190, b"ae"),
(191, b"oslash"),
(192, b"questiondown"),
(193, b"exclamdown"),
(194, b"logicalnot"),
(196, b"florin"),
(199, b"guillemotleft"),
(200, b"guillemotright"),
(201, b"ellipsis"),
(202, b"space"),
(203, b"Agrave"),
(204, b"Atilde"),
(205, b"Otilde"),
(206, b"OE"),
(207, b"oe"),
(208, b"endash"),
(209, b"emdash"),
(210, b"quotedblleft"),
(211, b"quotedblright"),
(212, b"quoteleft"),
(213, b"quoteright"),
(214, b"divide"),
(216, b"ydieresis"),
(217, b"Ydieresis"),
(218, b"fraction"),
(219, b"currency"),
(220, b"guilsinglleft"),
(221, b"guilsinglright"),
(222, b"fi"),
(223, b"fl"),
(224, b"daggerdbl"),
(225, b"periodcentered"),
(226, b"quotesinglbase"),
(227, b"quotedblbase"),
(228, b"perthousand"),
(229, b"Acircumflex"),
(230, b"Ecircumflex"),
(231, b"Aacute"),
(232, b"Edieresis"),
(233, b"Egrave"),
(234, b"Iacute"),
(235, b"Icircumflex"),
(236, b"Idieresis"),
(237, b"Igrave"),
(238, b"Oacute"),
(239, b"Ocircumflex"),
(241, b"Ograve"),
(242, b"Uacute"),
(243, b"Ucircumflex"),
(244, b"Ugrave"),
(245, b"dotlessi"),
(246, b"circumflex"),
(247, b"tilde"),
(248, b"macron"),
(249, b"breve"),
(250, b"dotaccent"),
(251, b"ring"),
(252, b"cedilla"),
(253, b"hungarumlaut"),
(254, b"ogonek"),
(255, b"caron"),
];
pub(crate) static MAC_ROMAN: [Option<&[u8]>; 256] = build(MAC_ROMAN_ENTRIES);
const MAC_EXPERT_ENTRIES: Entries = &[
(32, b"space"),
(33, b"exclamsmall"),
(34, b"Hungarumlautsmall"),
(35, b"centoldstyle"),
(36, b"dollaroldstyle"),
(37, b"dollarsuperior"),
(38, b"ampersandsmall"),
(39, b"Acutesmall"),
(40, b"parenleftsuperior"),
(41, b"parenrightsuperior"),
(42, b"twodotenleader"),
(43, b"onedotenleader"),
(44, b"comma"),
(45, b"hyphen"),
(46, b"period"),
(47, b"fraction"),
(48, b"zerooldstyle"),
(49, b"oneoldstyle"),
(50, b"twooldstyle"),
(51, b"threeoldstyle"),
(52, b"fouroldstyle"),
(53, b"fiveoldstyle"),
(54, b"sixoldstyle"),
(55, b"sevenoldstyle"),
(56, b"eightoldstyle"),
(57, b"nineoldstyle"),
(58, b"colon"),
(59, b"semicolon"),
(61, b"threequartersemdash"),
(63, b"questionsmall"),
(68, b"Ethsmall"),
(71, b"onequarter"),
(72, b"onehalf"),
(73, b"threequarters"),
(74, b"oneeighth"),
(75, b"threeeighths"),
(76, b"fiveeighths"),
(77, b"seveneighths"),
(78, b"onethird"),
(79, b"twothirds"),
(86, b"ff"),
(87, b"fi"),
(88, b"fl"),
(89, b"ffi"),
(90, b"ffl"),
(91, b"parenleftinferior"),
(93, b"parenrightinferior"),
(94, b"Circumflexsmall"),
(95, b"hypheninferior"),
(96, b"Gravesmall"),
(97, b"Asmall"),
(98, b"Bsmall"),
(99, b"Csmall"),
(100, b"Dsmall"),
(101, b"Esmall"),
(102, b"Fsmall"),
(103, b"Gsmall"),
(104, b"Hsmall"),
(105, b"Ismall"),
(106, b"Jsmall"),
(107, b"Ksmall"),
(108, b"Lsmall"),
(109, b"Msmall"),
(110, b"Nsmall"),
(111, b"Osmall"),
(112, b"Psmall"),
(113, b"Qsmall"),
(114, b"Rsmall"),
(115, b"Ssmall"),
(116, b"Tsmall"),
(117, b"Usmall"),
(118, b"Vsmall"),
(119, b"Wsmall"),
(120, b"Xsmall"),
(121, b"Ysmall"),
(122, b"Zsmall"),
(123, b"colonmonetary"),
(124, b"onefitted"),
(125, b"rupiah"),
(126, b"Tildesmall"),
(129, b"asuperior"),
(130, b"centsuperior"),
(135, b"Aacutesmall"),
(136, b"Agravesmall"),
(137, b"Acircumflexsmall"),
(138, b"Adieresissmall"),
(139, b"Atildesmall"),
(140, b"Aringsmall"),
(141, b"Ccedillasmall"),
(142, b"Eacutesmall"),
(143, b"Egravesmall"),
(144, b"Ecircumflexsmall"),
(145, b"Edieresissmall"),
(146, b"Iacutesmall"),
(147, b"Igravesmall"),
(148, b"Icircumflexsmall"),
(149, b"Idieresissmall"),
(150, b"Ntildesmall"),
(151, b"Oacutesmall"),
(152, b"Ogravesmall"),
(153, b"Ocircumflexsmall"),
(154, b"Odieresissmall"),
(155, b"Otildesmall"),
(156, b"Uacutesmall"),
(157, b"Ugravesmall"),
(158, b"Ucircumflexsmall"),
(159, b"Udieresissmall"),
(161, b"eightsuperior"),
(162, b"fourinferior"),
(163, b"threeinferior"),
(164, b"sixinferior"),
(165, b"eightinferior"),
(166, b"seveninferior"),
(167, b"Scaronsmall"),
(169, b"centinferior"),
(170, b"twoinferior"),
(172, b"Dieresissmall"),
(174, b"Caronsmall"),
(175, b"osuperior"),
(176, b"fiveinferior"),
(178, b"commainferior"),
(179, b"periodinferior"),
(180, b"Yacutesmall"),
(182, b"dollarinferior"),
(185, b"Thornsmall"),
(187, b"nineinferior"),
(188, b"zeroinferior"),
(189, b"Zcaronsmall"),
(190, b"AEsmall"),
(191, b"Oslashsmall"),
(192, b"questiondownsmall"),
(193, b"oneinferior"),
(194, b"Lslashsmall"),
(201, b"Cedillasmall"),
(207, b"OEsmall"),
(208, b"figuredash"),
(209, b"hyphensuperior"),
(214, b"exclamdownsmall"),
(216, b"Ydieresissmall"),
(218, b"onesuperior"),
(219, b"twosuperior"),
(220, b"threesuperior"),
(221, b"foursuperior"),
(222, b"fivesuperior"),
(223, b"sixsuperior"),
(224, b"sevensuperior"),
(225, b"ninesuperior"),
(226, b"zerosuperior"),
(228, b"esuperior"),
(229, b"rsuperior"),
(230, b"tsuperior"),
(233, b"isuperior"),
(234, b"ssuperior"),
(235, b"dsuperior"),
(241, b"lsuperior"),
(242, b"Ogoneksmall"),
(243, b"Brevesmall"),
(244, b"Macronsmall"),
(245, b"bsuperior"),
(246, b"nsuperior"),
(247, b"msuperior"),
(248, b"commasuperior"),
(249, b"periodsuperior"),
(250, b"Dotaccentsmall"),
(251, b"Ringsmall"),
];
pub(crate) static MAC_EXPERT: [Option<&[u8]>; 256] = build(MAC_EXPERT_ENTRIES);