use std::path::Path;
use anyhow::Result;
use crate::finding::Report;
use crate::registry::FontRegistry;
pub mod docx;
#[cfg(feature = "html")]
pub mod html;
pub mod pdf;
pub fn scan_document(
doc: &lopdf::Document,
label: &str,
registry: Option<&FontRegistry>,
) -> Result<Report> {
let mut report = pdf::scan_doc(doc, label, registry)?;
report.finalize();
Ok(report)
}
pub fn scan_path(path: &Path, registry: Option<&FontRegistry>) -> Result<Report> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_ascii_lowercase();
let mut report = match ext.as_str() {
"pdf" => pdf::scan(path, registry),
"docx" => docx::scan(path, registry),
#[cfg(feature = "html")]
"html" | "htm" => html::scan(path, registry),
#[cfg(not(feature = "html"))]
"html" | "htm" => anyhow::bail!("HTML support is not compiled (enable the `html` feature)"),
other => anyhow::bail!("unsupported format: .{other} (supported: pdf, docx, html)"),
}?;
report.finalize(); Ok(report)
}
#[cfg(feature = "ocr-specimen")]
pub fn scan_path_with_ocr(
path: &Path,
registry: Option<&FontRegistry>,
ocr: &dyn crate::ocr::OcrEngine,
) -> Result<Report> {
let mut report = scan_path(path, registry)?;
let already = report.findings.iter().any(|f| f.rule.contains("GLYPH_SEMANTIC_REPLACEMENT"));
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase();
if matches!(ext.as_str(), "pdf" | "docx") {
if already {
if let Err(e) = crate::specimen::confirm_with_specimen(path, &mut report, ocr) {
eprintln!("[warn] specimen confirmation skipped: {e:#}");
}
} else if let Ok(extra) = crate::specimen::specimen_scan_path(path, ocr) {
report.findings.extend(extra);
}
}
report.finalize(); Ok(report)
}
fn is_symbol_family(name: &str) -> bool {
let n = name.to_ascii_lowercase();
["wingding", "webding", "symbol", "dingbat", "marlett", "bookshelf", "mt extra"]
.iter()
.any(|s| n.contains(s))
}
pub(crate) fn judge_embedded_font(
f: &crate::font::ParsedFont,
location: &str,
registry: Option<&FontRegistry>,
out: &mut Vec<crate::finding::Finding>,
) {
use crate::finding::{Category, Finding, Severity};
use crate::registry::Identification;
if let Some(reg) = registry {
if reg.identify(f) == Identification::Pristine {
return;
}
}
let is_symbol = f
.family
.as_deref()
.or(f.full_name.as_deref())
.or(f.postscript_name.as_deref())
.map(is_symbol_family)
.unwrap_or(false);
let pua = f.pua_ratio();
if !is_symbol && f.cmap.len() >= 16 && pua > 0.5 {
out.push(Finding::new(
"FONT.PUA_CMAP",
Severity::High,
Category::FontIntegrity,
location,
format!(
"internal cmap maps {:.0}% of codepoints into the Private Use Area on a non-symbol font: likely obfuscation (extracted text unreadable)",
pua * 100.0
),
0.75,
));
}
}