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;