use docling_core::redact::{CompositeDetector, PatternDetector, PiiDetector, Redactor};
use docling_core::{DoclingDocument, ImageRedaction, RedactionOptions, RedactionReport};
use crate::error::ConversionError;
pub(crate) fn detector(opts: &RedactionOptions) -> Result<CompositeDetector, ConversionError> {
let pattern = PatternDetector::new(opts).map_err(|e| ConversionError::Parse(e.0))?;
let mut detectors: Vec<Box<dyn PiiDetector + Send + Sync>> = vec![Box::new(pattern)];
if opts.wants_ner() {
if let Some(ner) = ner_detector() {
detectors.push(Box::new(ner));
}
}
Ok(CompositeDetector(detectors))
}
#[cfg(feature = "ner")]
fn ner_detector() -> Option<SharedNer> {
use std::sync::{Arc, OnceLock};
static SLOT: OnceLock<Option<Arc<docling_pdf::ner::NerDetector>>> = OnceLock::new();
SLOT.get_or_init(|| match docling_pdf::ner::NerDetector::load() {
Ok(d) => Some(Arc::new(d)),
Err(e) => {
eprintln!(
"warning: NER model unavailable ({e}); redacting by pattern only — names, \
organizations and locations stay. Put dslim/bert-base-NER's ONNX export under \
.models/ner/ (model.onnx, tokenizer.json, config.json) or set DOCLING_RS_NER_DIR"
);
None
}
})
.clone()
.map(SharedNer)
}
#[cfg(feature = "ner")]
struct SharedNer(std::sync::Arc<docling_pdf::ner::NerDetector>);
#[cfg(feature = "ner")]
impl PiiDetector for SharedNer {
fn detect(&self, text: &str) -> Vec<docling_core::redact::Span> {
self.0.detect(text)
}
}
#[cfg(not(feature = "ner"))]
fn ner_detector() -> Option<NoNer> {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"warning: this build has no NER detector (rebuild with the `ner` feature); \
redacting by pattern only — names, organizations and locations stay"
);
});
None
}
#[cfg(not(feature = "ner"))]
struct NoNer;
#[cfg(not(feature = "ner"))]
impl PiiDetector for NoNer {
fn detect(&self, _text: &str) -> Vec<docling_core::redact::Span> {
Vec::new()
}
}
#[cfg(feature = "pdf")]
pub(crate) fn box_out_images(
document: &mut DoclingDocument,
redactor: &Redactor<'_>,
ocr: Option<&mut docling_pdf::Pipeline>,
) {
let Some(pipeline) = ocr else {
warn_box_out_unavailable("no OCR pipeline");
drop_images(document);
return;
};
let mut failed: Option<String> = None;
let mut repaint = |img: &mut docling_core::PictureImage| {
if failed.is_some() {
return;
}
match pipeline.box_out(&img.data, &|t| redactor.has_pii(t)) {
Ok(Some(png)) => {
if let Some((w, h)) = png_size(&png) {
img.width = w;
img.height = h;
}
img.mimetype = "image/png".to_string();
img.data = png;
}
Ok(None) => {}
Err(e) => failed = Some(e.to_string()),
}
};
document.for_each_picture_mut(&mut |node| {
if let docling_core::Node::Picture {
image: Some(img), ..
} = node
{
repaint(img);
}
});
if let Some(tree) = document.tree.as_mut() {
for item in tree.items.iter_mut() {
if let docling_core::tree::TreeKind::Picture {
image: Some(img), ..
} = &mut item.kind
{
repaint(img);
}
}
}
for img in document.page_images.values_mut() {
repaint(img);
}
if let Some(e) = failed {
warn_box_out_unavailable(&e);
drop_images(document);
}
}
pub(crate) fn warn_box_out_unavailable(why: &str) {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"warning: redact_images=box_out needs the OCR models ({why}); dropping the images instead"
);
});
}
pub(crate) fn drop_images(document: &mut DoclingDocument) {
document.for_each_picture_mut(&mut |node| {
if let docling_core::Node::Picture { image, .. } = node {
*image = None;
}
});
if let Some(tree) = document.tree.as_mut() {
for item in tree.items.iter_mut() {
if let docling_core::tree::TreeKind::Picture { image, .. } = &mut item.kind {
*image = None;
}
}
}
document.page_images.clear();
}
#[cfg(feature = "pdf")]
fn png_size(png: &[u8]) -> Option<(u32, u32)> {
if png.len() < 24 || &png[..8] != b"\x89PNG\r\n\x1a\n" {
return None;
}
let be = |i: usize| u32::from_be_bytes([png[i], png[i + 1], png[i + 2], png[i + 3]]);
Some((be(16), be(20)))
}
#[cfg(feature = "pdf")]
pub(crate) fn run(
document: &mut DoclingDocument,
opts: &RedactionOptions,
detector: &dyn PiiDetector,
ocr: Option<&mut docling_pdf::Pipeline>,
) -> RedactionReport {
let mut redactor = Redactor::new(opts, detector);
if opts.images == ImageRedaction::BoxOut {
box_out_images(document, &redactor, ocr);
}
redactor.redact_document(document);
redactor.finish()
}
#[cfg(not(feature = "pdf"))]
pub(crate) fn run(
document: &mut DoclingDocument,
opts: &RedactionOptions,
detector: &dyn PiiDetector,
) -> RedactionReport {
let mut redactor = Redactor::new(opts, detector);
if opts.images == ImageRedaction::BoxOut {
warn_box_out_unavailable("built without the `pdf` feature");
drop_images(document);
}
redactor.redact_document(document);
redactor.finish()
}