mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
proposal(redact): unlink unused Type 3 glyph programs and fonts on redaction (F-011)
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user