fullbleed 2.5.4

Deterministic HTML/CSS-to-PDF engine in Rust for transactional document generation and AI agent workflows.
Documentation
use super::*;
use crate::python_abi::IntoPyValue;

fn value_to_py(py: Python<'_>, value: &crate::DpmValue) -> PyResult<PyObject> {
    use crate::DpmValue;
    match value {
        DpmValue::String(value) => value.clone().into_py_value(),
        DpmValue::Boolean(value) => value.into_py_value(),
        DpmValue::Integer(value) => value.into_py_value(),
        DpmValue::Real(value) => value.into_py_value(),
        DpmValue::Array(values) => {
            let items = PyList::empty(py);
            for value in values {
                items.append(value_to_py(py, value)?)?;
            }
            Ok(items.unbind().into_any())
        }
        DpmValue::Dictionary(values) => {
            let result = PyDict::new(py);
            for (key, value) in values {
                result.set_item(key, value_to_py(py, value)?)?;
            }
            Ok(result.unbind().into_any())
        }
    }
}

pub(super) fn parts_to_py(
    py: Python<'_>,
    parts: &[crate::pdfinspect::PdfVtPartInspection],
) -> PyResult<PyObject> {
    let result = PyList::empty(py);
    for part in parts {
        let item = PyDict::new(py);
        item.set_item("object_id", part.object_id)?;
        item.set_item("depth", part.depth)?;
        item.set_item("name", part.name.clone())?;
        item.set_item("id", part.id.clone())?;
        item.set_item("first_page", part.first_page)?;
        item.set_item("last_page", part.last_page)?;
        item.set_item(
            "metadata",
            value_to_py(py, &crate::DpmValue::Dictionary(part.metadata.clone()))?,
        )?;
        result.append(item)?;
    }
    Ok(result.unbind().into_any())
}

fn parse_metadata(value: Option<&Bound<'_, PyAny>>) -> PyResult<crate::DpmMetadata> {
    fn parse(
        value: &Bound<'_, PyAny>,
        depth: usize,
        count: &mut usize,
    ) -> PyResult<crate::DpmValue> {
        use crate::DpmValue;
        *count += 1;
        if depth > 16 || *count > 10000 {
            return Err(PyValueError::new_err(
                "PDF_VT_JOB_INVALID: DPM exceeds depth 16 or 10000 values per node",
            ));
        }
        if let Ok(value) = value.extract::<String>() {
            return Ok(DpmValue::String(value));
        }
        if let Ok(value) = value.extract::<bool>() {
            return Ok(DpmValue::Boolean(value));
        }
        if let Ok(dict) = value.downcast::<PyDict>() {
            let mut result = BTreeMap::new();
            for (key, value) in dict.iter() {
                result.insert(key.extract::<String>()?, parse(&value, depth + 1, count)?);
            }
            return Ok(DpmValue::Dictionary(result));
        }
        if let Ok(items) = value.downcast::<PyList>() {
            return Ok(DpmValue::Array(
                items
                    .iter()
                    .map(|item| parse(&item, depth + 1, count))
                    .collect::<PyResult<_>>()?,
            ));
        }
        if value.is_builtin_instance("int")? {
            return value.extract::<i64>().map(DpmValue::Integer).map_err(|_| {
                PyValueError::new_err(
                    "PDF_VT_JOB_INVALID: DPM integers must fit signed 64-bit values",
                )
            });
        }
        if value.is_builtin_instance("float")? {
            return value.extract::<f64>().map(DpmValue::Real);
        }
        Err(PyValueError::new_err(
            "PDF_VT_JOB_INVALID: DPM values must be strings, booleans, numbers, lists or dictionaries",
        ))
    }
    let Some(value) = value else {
        return Ok(BTreeMap::new());
    };
    match parse(value, 0, &mut 0)? {
        crate::DpmValue::Dictionary(value) => Ok(value),
        _ => Err(PyValueError::new_err(
            "PDF_VT_JOB_INVALID: metadata must be a dictionary",
        )),
    }
}

pub(super) fn parse_job(value: Option<&Bound<'_, PyAny>>) -> PyResult<Option<crate::PdfVtJob>> {
    fn fields(
        value: &Bound<'_, PyAny>,
        allowed: &[&str],
    ) -> PyResult<(String, crate::DpmMetadata)> {
        let dict = value.downcast::<PyDict>()?;
        for (key, _) in dict.iter() {
            let key = key.extract::<String>()?;
            if !allowed.contains(&key.as_str()) {
                return Err(PyValueError::new_err(format!(
                    "PDF_VT_JOB_INVALID: unknown field {key:?}"
                )));
            }
        }
        let id = dict
            .get_item("id")?
            .ok_or_else(|| PyValueError::new_err("PDF_VT_JOB_INVALID: id is required"))?
            .extract::<String>()?;
        let metadata = parse_metadata(dict.get_item("metadata")?.as_ref())?;
        Ok((id, metadata))
    }
    let Some(value) = value else {
        return Ok(None);
    };
    if value.is_none() {
        return Ok(None);
    }
    let (id, metadata) = fields(value, &["id", "metadata", "records"])?;
    let mut job = crate::PdfVtJob {
        id,
        metadata,
        records: Vec::new(),
    };
    if let Some(records) = value.downcast::<PyDict>()?.get_item("records")? {
        for record in records.downcast::<PyList>()?.iter() {
            let (id, metadata) = fields(&record, &["id", "metadata", "documents"])?;
            let mut result = crate::PdfVtRecord {
                id,
                metadata,
                documents: Vec::new(),
            };
            let documents = record
                .downcast::<PyDict>()?
                .get_item("documents")?
                .ok_or_else(|| {
                    PyValueError::new_err("PDF_VT_JOB_INVALID: record documents are required")
                })?;
            for document in documents.downcast::<PyList>()?.iter() {
                let (id, metadata) = fields(&document, &["id", "metadata"])?;
                result.documents.push(crate::PdfVtDocument { id, metadata });
            }
            job.records.push(result);
        }
    }
    job.validate()
        .map_err(|error| PyValueError::new_err(error.to_string()))?;
    Ok(Some(job))
}