Skip to main content

scrybe_mermaid/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Shawn Hartsock and contributors
3
4//! scrybe-mermaid — standalone PNG iTXt codec.
5//!
6//! Embeds Mermaid diagram source as an iTXt metadata chunk inside a PNG.
7//! The PNG is fully valid; any viewer shows the rendered image. The source
8//! travels with the image and can be extracted later.
9//!
10//! # Codec format
11//!
12//! iTXt chunk key: `scrybe-mermaid`
13//! Value: JSON `{ "source": "<mermaid source>", "sha256": "<hex>" }`
14
15pub mod codec;
16pub mod error;
17
18pub use codec::{embed, extract};
19pub use error::MermaidError;
20
21/// The result of embedding or extracting Mermaid source.
22#[derive(Debug, Clone)]
23pub struct MermaidPayload {
24    /// The Mermaid diagram source text.
25    pub source: String,
26    /// SHA-256 of the source bytes (for integrity verification).
27    pub sha256: String,
28}
29
30// ── Python bindings ─────────────────────────────────────────────────────────
31//
32// Exposes `embed`, `extract`, and `MermaidPayload` to Python under the
33// `scrybe_mermaid._rust` module. Mirrors `scrybe-py`'s pyo3 v0.28 conventions.
34//
35// Python developers can:
36//
37//     >>> import scrybe_mermaid
38//     >>> with open("diagram.png", "rb") as f: png = f.read()
39//     >>> embedded = scrybe_mermaid.embed(png, "graph TD; A-->B")
40//     >>> payload = scrybe_mermaid.extract(embedded)
41//     >>> payload.source
42//     'graph TD; A-->B'
43//     >>> payload.sha256
44//     '83af36...'
45
46#[cfg(feature = "python")]
47mod python {
48    use pyo3::exceptions::PyValueError;
49    use pyo3::prelude::*;
50    use pyo3::types::PyBytes;
51
52    /// Embed Mermaid source as an iTXt metadata chunk in a PNG.
53    /// Returns the new PNG bytes; the original is unchanged.
54    #[pyfunction]
55    #[pyo3(name = "embed")]
56    fn py_embed<'py>(
57        py: Python<'py>,
58        png_bytes: &[u8],
59        source: &str,
60    ) -> PyResult<Bound<'py, PyBytes>> {
61        let out = crate::codec::embed(png_bytes, source)
62            .map_err(|e| PyValueError::new_err(e.to_string()))?;
63        Ok(PyBytes::new(py, &out))
64    }
65
66    /// Extract embedded Mermaid source from a PNG.
67    /// Raises `ValueError` if the PNG has no iTXt chunk with key `scrybe-mermaid`
68    /// or if the embedded sha256 doesn't match the source bytes.
69    #[pyfunction]
70    #[pyo3(name = "extract")]
71    fn py_extract(png_bytes: &[u8]) -> PyResult<PyMermaidPayload> {
72        crate::codec::extract(png_bytes)
73            .map(|p| PyMermaidPayload {
74                source: p.source,
75                sha256: p.sha256,
76            })
77            .map_err(|e| PyValueError::new_err(e.to_string()))
78    }
79
80    /// Result of `extract`. `source` is the Mermaid diagram text; `sha256`
81    /// is the hex digest of `source.encode('utf-8')` at embed time.
82    #[pyclass(name = "MermaidPayload", skip_from_py_object)]
83    #[derive(Clone)]
84    struct PyMermaidPayload {
85        #[pyo3(get)]
86        source: String,
87        #[pyo3(get)]
88        sha256: String,
89    }
90
91    #[pymethods]
92    impl PyMermaidPayload {
93        fn __repr__(&self) -> String {
94            format!(
95                "MermaidPayload(source={:?}, sha256={:?})",
96                self.source, self.sha256
97            )
98        }
99    }
100
101    #[pymodule]
102    pub fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
103        m.add_class::<PyMermaidPayload>()?;
104        m.add_function(wrap_pyfunction!(py_embed, m)?)?;
105        m.add_function(wrap_pyfunction!(py_extract, m)?)?;
106        Ok(())
107    }
108}
109
110#[cfg(feature = "python")]
111pub use python::_rust;