diff --git a/crates/redact/src/interp.rs b/crates/redact/src/interp.rs index ffd59a26f6..8aa47059ff 100644 --- a/crates/redact/src/interp.rs +++ b/crates/redact/src/interp.rs @@ -477,6 +477,7 @@ fn content_draws(doc: &Document, content: &[Vec], 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(()); } @@ -1382,6 +1383,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)); diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index bdf1db873e..6f69151d89 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -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 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)); diff --git a/crates/redact/src/tests/surviving_references.rs b/crates/redact/src/tests/surviving_references.rs index a297ded2a4..3348cf7f89 100644 --- a/crates/redact/src/tests/surviving_references.rs +++ b/crates/redact/src/tests/surviving_references.rs @@ -105,3 +105,156 @@ 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") { + if let Some(res) = saved.resolve(res_val).as_dict() { + if 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:?}"); +} diff --git a/crates/redact/src/type3.rs b/crates/redact/src/type3.rs new file mode 100644 index 0000000000..9f547071f5 --- /dev/null +++ b/crates/redact/src/type3.rs @@ -0,0 +1,700 @@ +//! 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. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub(crate) enum FontKey { + Ref(ObjRef), + Inline(ObjRef, usize), +} + +/// 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>; 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>; + + 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>; 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, holder_ref: ObjRef, res: &Dict, font_name: &[u8]) -> Option<(FontKey, Dict)> { + let fonts = res_dict(doc, res, b"Font"); + let (idx, (_, font_obj)) = fonts.iter().enumerate().find(|(_, (k, _))| k.as_slice() == font_name)?; + let font_ref = font_obj.as_ref(); + let resolved = doc.resolve(font_obj); + let dict = resolved.as_dict()?.clone(); + if dict.name(b"Subtype") == Some(b"Type3") { + let key = match font_ref { + Some(r) => FontKey::Ref(r), + None => FontKey::Inline(holder_ref, idx), + }; + Some((key, dict)) + } else { + None + } +} + +fn resolve_extgstate_font(doc: &Document, holder_ref: ObjRef, res: &Dict, gs_name: &[u8]) -> Option<(FontKey, Dict)> { + let extg = res_dict(doc, res, b"ExtGState"); + let state_obj = extg.get(gs_name)?; + let state = doc.resolve(state_obj).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 resolved = doc.resolve(font_obj); + let dict = resolved.as_dict()?.clone(); + if dict.name(b"Subtype") == Some(b"Type3") { + let key = match font_ref { + Some(r) => FontKey::Ref(r), + None => FontKey::Inline(holder_ref, 0), + }; + Some((key, dict)) + } else { + None + } +} + +#[derive(Default)] +struct UsedInGlyphs { + xobjects: HashSet>, + colorspaces: HashSet>, + patterns: HashSet>, + shadings: HashSet>, + extgstates: HashSet>, + fonts: HashSet>, + properties: HashSet>, +} + +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> = 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>; 256]>, + used_glyphs: &'a mut HashMap>>, + seen_forms: &'a mut HashSet, +} + +fn scan_content_stream( + ctx: &mut ScanContext<'_>, + content: &[Vec], + holder_ref: ObjRef, + res: &Dict, + initial_font: Option, + 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> = 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" => current_font = stack.pop().unwrap_or(None), + b"Tf" => { + if let Some(name) = op.name(0) { + current_font = resolve_font_in_res(ctx.doc, holder_ref, res, name).map(|(k, _)| k); + } + } + b"gs" => { + if let Some(name) = op.name(0) + && let Some((k, _)) = resolve_extgstate_font(ctx.doc, holder_ref, res, name) + { + current_font = Some(k); + } + } + b"Tj" | b"'" => { + if let Some(fk) = current_font + && let Some(map) = ctx.font_maps.get(&fk) + && let Some(s) = op.operands.last().and_then(Object::as_string) + { + for b in &s.bytes { + if let Some(g) = &map[*b as usize] { + ctx.used_glyphs.entry(fk).or_default().insert(g.clone()); + } + } + } + } + b"TJ" => { + if let Some(fk) = current_font + && let Some(map) = ctx.font_maps.get(&fk) + && let Some(arr) = op.operands.last().and_then(Object::as_array) + { + for item in arr { + if let Some(s) = item.as_string() { + for b in &s.bytes { + if let Some(g) = &map[*b as usize] { + ctx.used_glyphs.entry(fk).or_default().insert(g.clone()); + } + } + } + } + } + } + b"\"" => { + if let Some(fk) = current_font + && let Some(map) = ctx.font_maps.get(&fk) + && let Some(s) = op.operands.last().and_then(Object::as_string) + { + for b in &s.bytes { + if let Some(g) = &map[*b as usize] { + ctx.used_glyphs.entry(fk).or_default().insert(g.clone()); + } + } + } + } + b"Do" => { + if let Some(name) = op.last_name() { + let xo = res_dict(ctx.doc, res, b"XObject"); + if let Some(r) = xo.get(name).and_then(Object::as_ref) + && ctx.seen_forms.insert(r) + { + let obj = ctx.doc.get(r); + if let Object::Stream(s) = &*obj + && s.dict.name(b"Subtype") == Some(b"Form") + { + let data = s.decoded()?; + let form_own_res = s.dict.get(b"Resources").and_then(|ro| ctx.doc.resolve(ro).as_dict().cloned()); + if let Some(form_res) = form_own_res { + scan_content_stream(ctx, std::slice::from_ref(&data), r, &form_res, current_font, depth + 1)?; + } else { + scan_content_stream(ctx, std::slice::from_ref(&data), holder_ref, res, current_font, depth + 1)?; + } + } + } + } + } + _ => {} + } + } + Ok(()) +} + +/// 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]) -> Result<(), CosError> { + let mut fonts = res_dict(doc, res, b"Font"); + if fonts.is_empty() { + return Ok(()); + } + + 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 drawn_font_names = HashSet::new(); + for op in &parsed.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(state_obj) = extg.get(name) + && let Some(state) = doc.resolve(state_obj).as_dict() + && let Some(font_entry) = doc.resolve(state.get(b"Font").unwrap_or(&Object::Null)).as_array() + && let Some(font_obj) = font_entry.first() + { + for (fn_name, fn_obj) in fonts.iter() { + if fn_obj == font_obj || (fn_obj.as_ref().is_some() && fn_obj.as_ref() == font_obj.as_ref()) { + drawn_font_names.insert(fn_name.to_vec()); + } + } + } + } + } + + let mut changed = false; + let font_names: Vec> = 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)> = Vec::new(); + let mut seen = HashSet::new(); + let mut stack: Vec<(ObjRef, Option)> = 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); + } + } + + // Find all reachable Form XObjects with resources of their own. + let mut form_holders: Vec<(ObjRef, Dict)> = Vec::new(); + let mut pending_res: Vec = 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() }.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); + } + } + } + + // Discover all Type 3 fonts in holders and forms. + let mut type3_fonts: HashMap = HashMap::new(); + let check_res_fonts = |doc: &Document, holder_ref: ObjRef, res: &Dict, t3_fonts: &mut HashMap| { + let fonts = res_dict(doc, res, b"Font"); + for (idx, (_, font_obj)) in fonts.iter().enumerate() { + let font_ref = font_obj.as_ref(); + if let Some(dict) = doc.resolve(font_obj).as_dict().cloned() + && dict.name(b"Subtype") == Some(b"Type3") + { + let key = match font_ref { + Some(r) => FontKey::Ref(r), + None => FontKey::Inline(holder_ref, idx), + }; + t3_fonts.entry(key).or_insert(dict); + } + } + let extg = res_dict(doc, res, b"ExtGState"); + for (idx, (_, state_obj)) in extg.iter().enumerate() { + if let Some(state) = doc.resolve(state_obj).as_dict() + && let Some(font_arr) = doc.resolve(state.get(b"Font").unwrap_or(&Object::Null)).as_array() + && let Some(font_obj) = font_arr.first() + { + let font_ref = font_obj.as_ref(); + if let Some(dict) = doc.resolve(font_obj).as_dict().cloned() + && dict.name(b"Subtype") == Some(b"Type3") + { + let key = match font_ref { + Some(r) => FontKey::Ref(r), + None => FontKey::Inline(holder_ref, idx), + }; + t3_fonts.entry(key).or_insert(dict); + } + } + } + }; + + 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()) { + check_res_fonts(doc, *node, &res, &mut type3_fonts); + } + } + for (form_ref, form_res) in &form_holders { + check_res_fonts(doc, *form_ref, 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>; 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>> = 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.contains(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(); + 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 char_procs_ref = char_procs_val.as_ref(); + 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> = 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; + } + } + + if char_procs.is_empty() { + // The font has no surviving glyphs: remove font from /Font across all holders. + remove_font_from_holders(doc, &holders, &form_holders, font_key)?; + } else { + if procs_changed { + if let Some(r) = char_procs_ref { + doc.update_dict(r, |d| *d = char_procs.clone())?; + } else if let FontKey::Ref(fr) = font_key { + 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. + if let Some(f_res_val) = font_dict.get(b"Resources") { + let f_res_ref = f_res_val.as_ref(); + 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) { + if let Some(rr) = f_res_ref { + doc.update_dict(rr, |d| *d = f_res.clone())?; + } else if let FontKey::Ref(fr) = font_key { + 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)], + 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> = fonts.iter().map(|(n, _)| n.to_vec()).collect(); + for (idx, n) in names.into_iter().enumerate() { + if let Some(val) = fonts.get(&n) { + let key = match val.as_ref() { + Some(r) => FontKey::Ref(r), + None => FontKey::Inline(ObjRef::new(0, 0), idx), // matched below if target is Ref + }; + let matches = match (target_key, key) { + (FontKey::Ref(target_r), FontKey::Ref(key_r)) => *target_r == key_r, + (FontKey::Inline(target_h, target_idx), _) => { + // Compare by value + if let Some(target_dict) = doc.get(*target_h).as_dict() { + fonts.get(&n) == target_dict.get(n.as_slice()) && *target_idx == idx + } else { + false + } + } + _ => false, + }; + if matches { + 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(()) +} diff --git a/crates/redact/src/type3_tables.rs b/crates/redact/src/type3_tables.rs new file mode 100644 index 0000000000..0ee744ab64 --- /dev/null +++ b/crates/redact/src/type3_tables.rs @@ -0,0 +1,1037 @@ +//! Predefined font encodings (ISO 32000 Annex D) mapping character codes to glyph names. + +pub(crate) static STANDARD: [Option<&'static [u8]>; 256] = [ + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(b"space"), + Some(b"exclam"), + Some(b"quotedbl"), + Some(b"numbersign"), + Some(b"dollar"), + Some(b"percent"), + Some(b"ampersand"), + Some(b"quoteright"), + Some(b"parenleft"), + Some(b"parenright"), + Some(b"asterisk"), + Some(b"plus"), + Some(b"comma"), + Some(b"hyphen"), + Some(b"period"), + Some(b"slash"), + Some(b"zero"), + Some(b"one"), + Some(b"two"), + Some(b"three"), + Some(b"four"), + Some(b"five"), + Some(b"six"), + Some(b"seven"), + Some(b"eight"), + Some(b"nine"), + Some(b"colon"), + Some(b"semicolon"), + Some(b"less"), + Some(b"equal"), + Some(b"greater"), + Some(b"question"), + Some(b"at"), + Some(b"A"), + Some(b"B"), + Some(b"C"), + Some(b"D"), + Some(b"E"), + Some(b"F"), + Some(b"G"), + Some(b"H"), + Some(b"I"), + Some(b"J"), + Some(b"K"), + Some(b"L"), + Some(b"M"), + Some(b"N"), + Some(b"O"), + Some(b"P"), + Some(b"Q"), + Some(b"R"), + Some(b"S"), + Some(b"T"), + Some(b"U"), + Some(b"V"), + Some(b"W"), + Some(b"X"), + Some(b"Y"), + Some(b"Z"), + Some(b"bracketleft"), + Some(b"backslash"), + Some(b"bracketright"), + Some(b"asciicircum"), + Some(b"underscore"), + Some(b"quoteleft"), + Some(b"a"), + Some(b"b"), + Some(b"c"), + Some(b"d"), + Some(b"e"), + Some(b"f"), + Some(b"g"), + Some(b"h"), + Some(b"i"), + Some(b"j"), + Some(b"k"), + Some(b"l"), + Some(b"m"), + Some(b"n"), + Some(b"o"), + Some(b"p"), + Some(b"q"), + Some(b"r"), + Some(b"s"), + Some(b"t"), + Some(b"u"), + Some(b"v"), + Some(b"w"), + Some(b"x"), + Some(b"y"), + Some(b"z"), + Some(b"braceleft"), + Some(b"bar"), + Some(b"braceright"), + Some(b"asciitilde"), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(b"exclamdown"), + Some(b"cent"), + Some(b"sterling"), + Some(b"fraction"), + Some(b"yen"), + Some(b"florin"), + Some(b"section"), + Some(b"currency"), + Some(b"quotesingle"), + Some(b"quotedblleft"), + Some(b"guillemotleft"), + Some(b"guilsinglleft"), + Some(b"guilsinglright"), + Some(b"fi"), + Some(b"fl"), + None, + Some(b"endash"), + Some(b"dagger"), + Some(b"daggerdbl"), + Some(b"periodcentered"), + None, + Some(b"paragraph"), + Some(b"bullet"), + Some(b"quotesinglbase"), + Some(b"quotedblbase"), + Some(b"quotedblright"), + Some(b"guillemotright"), + Some(b"ellipsis"), + Some(b"perthousand"), + None, + Some(b"questiondown"), + None, + Some(b"grave"), + Some(b"acute"), + Some(b"circumflex"), + Some(b"tilde"), + Some(b"macron"), + Some(b"breve"), + Some(b"dotaccent"), + Some(b"dieresis"), + None, + Some(b"ring"), + Some(b"cedilla"), + None, + Some(b"hungarumlaut"), + Some(b"ogonek"), + Some(b"caron"), + Some(b"emdash"), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(b"AE"), + None, + Some(b"ordfeminine"), + None, + None, + None, + None, + Some(b"Lslash"), + Some(b"Oslash"), + Some(b"OE"), + Some(b"ordmasculine"), + None, + None, + None, + None, + None, + Some(b"ae"), + None, + None, + None, + Some(b"dotlessi"), + None, + None, + Some(b"lslash"), + Some(b"oslash"), + Some(b"oe"), + Some(b"germandbls"), + None, + None, + None, + None, +]; + +pub(crate) static WIN_ANSI: [Option<&'static [u8]>; 256] = [ + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(b"space"), + Some(b"exclam"), + Some(b"quotedbl"), + Some(b"numbersign"), + Some(b"dollar"), + Some(b"percent"), + Some(b"ampersand"), + Some(b"quotesingle"), + Some(b"parenleft"), + Some(b"parenright"), + Some(b"asterisk"), + Some(b"plus"), + Some(b"comma"), + Some(b"hyphen"), + Some(b"period"), + Some(b"slash"), + Some(b"zero"), + Some(b"one"), + Some(b"two"), + Some(b"three"), + Some(b"four"), + Some(b"five"), + Some(b"six"), + Some(b"seven"), + Some(b"eight"), + Some(b"nine"), + Some(b"colon"), + Some(b"semicolon"), + Some(b"less"), + Some(b"equal"), + Some(b"greater"), + Some(b"question"), + Some(b"at"), + Some(b"A"), + Some(b"B"), + Some(b"C"), + Some(b"D"), + Some(b"E"), + Some(b"F"), + Some(b"G"), + Some(b"H"), + Some(b"I"), + Some(b"J"), + Some(b"K"), + Some(b"L"), + Some(b"M"), + Some(b"N"), + Some(b"O"), + Some(b"P"), + Some(b"Q"), + Some(b"R"), + Some(b"S"), + Some(b"T"), + Some(b"U"), + Some(b"V"), + Some(b"W"), + Some(b"X"), + Some(b"Y"), + Some(b"Z"), + Some(b"bracketleft"), + Some(b"backslash"), + Some(b"bracketright"), + Some(b"asciicircum"), + Some(b"underscore"), + Some(b"grave"), + Some(b"a"), + Some(b"b"), + Some(b"c"), + Some(b"d"), + Some(b"e"), + Some(b"f"), + Some(b"g"), + Some(b"h"), + Some(b"i"), + Some(b"j"), + Some(b"k"), + Some(b"l"), + Some(b"m"), + Some(b"n"), + Some(b"o"), + Some(b"p"), + Some(b"q"), + Some(b"r"), + Some(b"s"), + Some(b"t"), + Some(b"u"), + Some(b"v"), + Some(b"w"), + Some(b"x"), + Some(b"y"), + Some(b"z"), + Some(b"braceleft"), + Some(b"bar"), + Some(b"braceright"), + Some(b"asciitilde"), + None, + Some(b"Euro"), + None, + Some(b"quotesinglbase"), + Some(b"florin"), + Some(b"quotedblbase"), + Some(b"ellipsis"), + Some(b"dagger"), + Some(b"daggerdbl"), + Some(b"circumflex"), + Some(b"perthousand"), + Some(b"Scaron"), + Some(b"guilsinglleft"), + Some(b"OE"), + None, + Some(b"Zcaron"), + None, + None, + Some(b"quoteleft"), + Some(b"quoteright"), + Some(b"quotedblleft"), + Some(b"quotedblright"), + Some(b"bullet"), + Some(b"endash"), + Some(b"emdash"), + Some(b"tilde"), + Some(b"trademark"), + Some(b"scaron"), + Some(b"guilsinglright"), + Some(b"oe"), + None, + Some(b"zcaron"), + Some(b"Ydieresis"), + Some(b"nbspace"), + Some(b"exclamdown"), + Some(b"cent"), + Some(b"sterling"), + Some(b"currency"), + Some(b"yen"), + Some(b"brokenbar"), + Some(b"section"), + Some(b"dieresis"), + Some(b"copyright"), + Some(b"ordfeminine"), + Some(b"guillemotleft"), + Some(b"logicalnot"), + Some(b"sfthyphen"), + Some(b"registered"), + Some(b"macron"), + Some(b"degree"), + Some(b"plusminus"), + Some(b"twosuperior"), + Some(b"threesuperior"), + Some(b"acute"), + Some(b"mu"), + Some(b"paragraph"), + Some(b"periodcentered"), + Some(b"cedilla"), + Some(b"onesuperior"), + Some(b"ordmasculine"), + Some(b"guillemotright"), + Some(b"onequarter"), + Some(b"onehalf"), + Some(b"threequarters"), + Some(b"questiondown"), + Some(b"Agrave"), + Some(b"Aacute"), + Some(b"Acircumflex"), + Some(b"Atilde"), + Some(b"Adieresis"), + Some(b"Aring"), + Some(b"AE"), + Some(b"Ccedilla"), + Some(b"Egrave"), + Some(b"Eacute"), + Some(b"Ecircumflex"), + Some(b"Edieresis"), + Some(b"Igrave"), + Some(b"Iacute"), + Some(b"Icircumflex"), + Some(b"Idieresis"), + Some(b"Eth"), + Some(b"Ntilde"), + Some(b"Ograve"), + Some(b"Oacute"), + Some(b"Ocircumflex"), + Some(b"Otilde"), + Some(b"Odieresis"), + Some(b"multiply"), + Some(b"Oslash"), + Some(b"Ugrave"), + Some(b"Uacute"), + Some(b"Ucircumflex"), + Some(b"Udieresis"), + Some(b"Yacute"), + Some(b"Thorn"), + Some(b"germandbls"), + Some(b"agrave"), + Some(b"aacute"), + Some(b"acircumflex"), + Some(b"atilde"), + Some(b"adieresis"), + Some(b"aring"), + Some(b"ae"), + Some(b"ccedilla"), + Some(b"egrave"), + Some(b"eacute"), + Some(b"ecircumflex"), + Some(b"edieresis"), + Some(b"igrave"), + Some(b"iacute"), + Some(b"icircumflex"), + Some(b"idieresis"), + Some(b"eth"), + Some(b"ntilde"), + Some(b"ograve"), + Some(b"oacute"), + Some(b"ocircumflex"), + Some(b"otilde"), + Some(b"odieresis"), + Some(b"divide"), + Some(b"oslash"), + Some(b"ugrave"), + Some(b"uacute"), + Some(b"ucircumflex"), + Some(b"udieresis"), + Some(b"yacute"), + Some(b"thorn"), + Some(b"ydieresis"), +]; + +pub(crate) static MAC_ROMAN: [Option<&'static [u8]>; 256] = [ + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(b"space"), + Some(b"exclam"), + Some(b"quotedbl"), + Some(b"numbersign"), + Some(b"dollar"), + Some(b"percent"), + Some(b"ampersand"), + Some(b"quotesingle"), + Some(b"parenleft"), + Some(b"parenright"), + Some(b"asterisk"), + Some(b"plus"), + Some(b"comma"), + Some(b"hyphen"), + Some(b"period"), + Some(b"slash"), + Some(b"zero"), + Some(b"one"), + Some(b"two"), + Some(b"three"), + Some(b"four"), + Some(b"five"), + Some(b"six"), + Some(b"seven"), + Some(b"eight"), + Some(b"nine"), + Some(b"colon"), + Some(b"semicolon"), + Some(b"less"), + Some(b"equal"), + Some(b"greater"), + Some(b"question"), + Some(b"at"), + Some(b"A"), + Some(b"B"), + Some(b"C"), + Some(b"D"), + Some(b"E"), + Some(b"F"), + Some(b"G"), + Some(b"H"), + Some(b"I"), + Some(b"J"), + Some(b"K"), + Some(b"L"), + Some(b"M"), + Some(b"N"), + Some(b"O"), + Some(b"P"), + Some(b"Q"), + Some(b"R"), + Some(b"S"), + Some(b"T"), + Some(b"U"), + Some(b"V"), + Some(b"W"), + Some(b"X"), + Some(b"Y"), + Some(b"Z"), + Some(b"bracketleft"), + Some(b"backslash"), + Some(b"bracketright"), + Some(b"asciicircum"), + Some(b"underscore"), + Some(b"grave"), + Some(b"a"), + Some(b"b"), + Some(b"c"), + Some(b"d"), + Some(b"e"), + Some(b"f"), + Some(b"g"), + Some(b"h"), + Some(b"i"), + Some(b"j"), + Some(b"k"), + Some(b"l"), + Some(b"m"), + Some(b"n"), + Some(b"o"), + Some(b"p"), + Some(b"q"), + Some(b"r"), + Some(b"s"), + Some(b"t"), + Some(b"u"), + Some(b"v"), + Some(b"w"), + Some(b"x"), + Some(b"y"), + Some(b"z"), + Some(b"braceleft"), + Some(b"bar"), + Some(b"braceright"), + Some(b"asciitilde"), + None, + Some(b"Adieresis"), + Some(b"Aring"), + Some(b"Ccedilla"), + Some(b"Eacute"), + Some(b"Ntilde"), + Some(b"Odieresis"), + Some(b"Udieresis"), + Some(b"aacute"), + Some(b"agrave"), + Some(b"acircumflex"), + Some(b"adieresis"), + Some(b"atilde"), + Some(b"aring"), + Some(b"ccedilla"), + Some(b"eacute"), + Some(b"egrave"), + Some(b"ecircumflex"), + Some(b"edieresis"), + Some(b"iacute"), + Some(b"igrave"), + Some(b"icircumflex"), + Some(b"idieresis"), + Some(b"ntilde"), + Some(b"oacute"), + Some(b"ograve"), + Some(b"ocircumflex"), + Some(b"odieresis"), + Some(b"otilde"), + Some(b"uacute"), + Some(b"ugrave"), + Some(b"ucircumflex"), + Some(b"udieresis"), + Some(b"dagger"), + Some(b"degree"), + Some(b"cent"), + Some(b"sterling"), + Some(b"section"), + Some(b"bullet"), + Some(b"paragraph"), + Some(b"germandbls"), + Some(b"registered"), + Some(b"copyright"), + Some(b"trademark"), + Some(b"acute"), + Some(b"dieresis"), + None, + Some(b"AE"), + Some(b"Oslash"), + None, + Some(b"plusminus"), + None, + None, + Some(b"yen"), + Some(b"mu"), + None, + None, + None, + None, + None, + Some(b"ordfeminine"), + Some(b"ordmasculine"), + None, + Some(b"ae"), + Some(b"oslash"), + Some(b"questiondown"), + Some(b"exclamdown"), + Some(b"logicalnot"), + None, + Some(b"florin"), + None, + None, + Some(b"guillemotleft"), + Some(b"guillemotright"), + Some(b"ellipsis"), + Some(b"nbspace"), + Some(b"Agrave"), + Some(b"Atilde"), + Some(b"Otilde"), + Some(b"OE"), + Some(b"oe"), + Some(b"endash"), + Some(b"emdash"), + Some(b"quotedblleft"), + Some(b"quotedblright"), + Some(b"quoteleft"), + Some(b"quoteright"), + Some(b"divide"), + None, + Some(b"ydieresis"), + Some(b"Ydieresis"), + Some(b"fraction"), + Some(b"currency"), + Some(b"guilsinglleft"), + Some(b"guilsinglright"), + Some(b"fi"), + Some(b"fl"), + Some(b"daggerdbl"), + Some(b"periodcentered"), + Some(b"quotesinglbase"), + Some(b"quotedblbase"), + Some(b"perthousand"), + Some(b"Acircumflex"), + Some(b"Ecircumflex"), + Some(b"Aacute"), + Some(b"Edieresis"), + Some(b"Egrave"), + Some(b"Iacute"), + Some(b"Icircumflex"), + Some(b"Idieresis"), + Some(b"Igrave"), + Some(b"Oacute"), + Some(b"Ocircumflex"), + None, + Some(b"Ograve"), + Some(b"Uacute"), + Some(b"Ucircumflex"), + Some(b"Ugrave"), + Some(b"dotlessi"), + Some(b"circumflex"), + Some(b"tilde"), + Some(b"macron"), + Some(b"breve"), + Some(b"dotaccent"), + Some(b"ring"), + Some(b"cedilla"), + Some(b"hungarumlaut"), + Some(b"ogonek"), + Some(b"caron"), +]; + +pub(crate) static MAC_EXPERT: [Option<&'static [u8]>; 256] = [ + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(b"space"), + Some(b"exclamsmall"), + Some(b"Hungarumlautsmall"), + Some(b"centoldstyle"), + Some(b"dollaroldstyle"), + Some(b"dollarsuperior"), + Some(b"ampersandsmall"), + Some(b"Acutesmall"), + Some(b"parenleftsuperior"), + Some(b"parenrightsuperior"), + Some(b"twodotenleader"), + Some(b"onedotenleader"), + Some(b"comma"), + Some(b"hyphen"), + Some(b"period"), + Some(b"fraction"), + Some(b"zerooldstyle"), + Some(b"oneoldstyle"), + Some(b"twooldstyle"), + Some(b"threeoldstyle"), + Some(b"fouroldstyle"), + Some(b"fiveoldstyle"), + Some(b"sixoldstyle"), + Some(b"sevenoldstyle"), + Some(b"eightoldstyle"), + Some(b"nineoldstyle"), + Some(b"colon"), + Some(b"semicolon"), + None, + Some(b"threequartersemdash"), + None, + Some(b"questionsmall"), + None, + None, + None, + None, + Some(b"Ethsmall"), + None, + None, + Some(b"onequarter"), + Some(b"onehalf"), + Some(b"threequarters"), + Some(b"oneeighth"), + Some(b"threeeighths"), + Some(b"fiveeighths"), + Some(b"seveneighths"), + Some(b"onethird"), + Some(b"twothirds"), + None, + None, + None, + None, + None, + None, + Some(b"ff"), + Some(b"fi"), + Some(b"fl"), + Some(b"ffi"), + Some(b"ffl"), + Some(b"parenleftinferior"), + None, + Some(b"parenrightinferior"), + Some(b"Circumflexsmall"), + Some(b"hypheninferior"), + Some(b"Gravesmall"), + Some(b"Asmall"), + Some(b"Bsmall"), + Some(b"Csmall"), + Some(b"Dsmall"), + Some(b"Esmall"), + Some(b"Fsmall"), + Some(b"Gsmall"), + Some(b"Hsmall"), + Some(b"Ismall"), + Some(b"Jsmall"), + Some(b"Ksmall"), + Some(b"Lsmall"), + Some(b"Msmall"), + Some(b"Nsmall"), + Some(b"Osmall"), + Some(b"Psmall"), + Some(b"Qsmall"), + Some(b"Rsmall"), + Some(b"Ssmall"), + Some(b"Tsmall"), + Some(b"Usmall"), + Some(b"Vsmall"), + Some(b"Wsmall"), + Some(b"Xsmall"), + Some(b"Ysmall"), + Some(b"Zsmall"), + Some(b"colonmonetary"), + Some(b"onefitted"), + Some(b"rupiah"), + Some(b"Tildesmall"), + None, + None, + Some(b"asuperior"), + Some(b"centsuperior"), + None, + None, + None, + None, + Some(b"Aacutesmall"), + Some(b"Agravesmall"), + Some(b"Acircumflexsmall"), + Some(b"Adieresissmall"), + Some(b"Atildesmall"), + Some(b"Aringsmall"), + Some(b"Ccedillasmall"), + Some(b"Eacutesmall"), + Some(b"Egravesmall"), + Some(b"Ecircumflexsmall"), + Some(b"Edieresissmall"), + Some(b"Iacutesmall"), + Some(b"Igravesmall"), + Some(b"Icircumflexsmall"), + Some(b"Idieresissmall"), + Some(b"Ntildesmall"), + Some(b"Oacutesmall"), + Some(b"Ogravesmall"), + Some(b"Ocircumflexsmall"), + Some(b"Odieresissmall"), + Some(b"Otildesmall"), + Some(b"Uacutesmall"), + Some(b"Ugravesmall"), + Some(b"Ucircumflexsmall"), + Some(b"Udieresissmall"), + None, + Some(b"eightsuperior"), + Some(b"fourinferior"), + Some(b"threeinferior"), + Some(b"sixinferior"), + Some(b"eightinferior"), + Some(b"seveninferior"), + Some(b"Scaronsmall"), + None, + Some(b"centinferior"), + Some(b"twoinferior"), + None, + Some(b"Dieresissmall"), + None, + Some(b"Caronsmall"), + Some(b"osuperior"), + Some(b"fiveinferior"), + None, + Some(b"commainferior"), + Some(b"periodinferior"), + Some(b"Yacutesmall"), + None, + Some(b"dollarinferior"), + None, + None, + Some(b"Thornsmall"), + None, + Some(b"nineinferior"), + Some(b"zeroinferior"), + Some(b"Zcaronsmall"), + Some(b"AEsmall"), + Some(b"Oslashsmall"), + Some(b"questiondownsmall"), + Some(b"oneinferior"), + Some(b"Lslashsmall"), + None, + None, + None, + None, + None, + None, + Some(b"Cedillasmall"), + None, + None, + None, + None, + None, + Some(b"OEsmall"), + Some(b"figuredash"), + Some(b"hyphensuperior"), + None, + None, + None, + None, + Some(b"exclamdownsmall"), + None, + Some(b"Ydieresissmall"), + None, + Some(b"onesuperior"), + Some(b"twosuperior"), + Some(b"threesuperior"), + Some(b"foursuperior"), + Some(b"fivesuperior"), + Some(b"sixsuperior"), + Some(b"sevensuperior"), + Some(b"ninesuperior"), + Some(b"zerosuperior"), + None, + Some(b"esuperior"), + Some(b"rsuperior"), + Some(b"tsuperior"), + None, + None, + Some(b"isuperior"), + Some(b"ssuperior"), + Some(b"dsuperior"), + None, + None, + None, + None, + None, + Some(b"lsuperior"), + Some(b"Ogoneksmall"), + Some(b"Brevesmall"), + Some(b"Macronsmall"), + Some(b"bsuperior"), + Some(b"nsuperior"), + Some(b"msuperior"), + Some(b"commasuperior"), + Some(b"periodsuperior"), + Some(b"Dotaccentsmall"), + Some(b"Ringsmall"), + None, + None, + None, + None, +];