Skip to main content

oicana_input/input/
blob.rs

1use crate::Input;
2use typst::foundations::{Bytes, Dict, IntoValue, Str, Value};
3
4/// A blob input with its key and value.
5#[derive(Clone, Debug)]
6pub struct BlobInput {
7    /// The key of the input.
8    ///
9    /// This corresponds to the identifier of an input definition in the manifest.
10    pub key: Str,
11    /// The blob value.
12    pub value: Blob,
13}
14
15impl BlobInput {
16    /// Create a new blob input.
17    pub fn new(key: impl Into<Str>, value: impl Into<Blob>) -> Self {
18        BlobInput {
19            key: key.into(),
20            value: value.into(),
21        }
22    }
23
24    /// Create a blob input for an image, setting the `image_format` metadata
25    /// entry that the `oicana-image` helper uses to pick a decoder.
26    ///
27    /// For raw pixel data, build the metadata manually with [`Blob::with_metadata`]
28    /// and an `image_format` dictionary containing `encoding`, `width`, and `height`.
29    pub fn image<B>(key: impl Into<Str>, bytes: B, format: ImageFormat) -> Self
30    where
31        B: AsRef<[u8]> + Send + Sync + 'static,
32    {
33        BlobInput::new(
34            key,
35            Blob::with_metadata(bytes, [("image_format", Str::from(format))]),
36        )
37    }
38}
39
40/// Encoded image formats supported by Typst's `image` function.
41///
42/// Used with [`BlobInput::image`] to set the `image_format` metadata entry
43/// that the `oicana-image` helper forwards to Typst's `image` function.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum ImageFormat {
46    /// PNG image.
47    Png,
48    /// JPEG image.
49    Jpg,
50    /// GIF image.
51    Gif,
52    /// SVG image.
53    Svg,
54    /// PDF image.
55    Pdf,
56    /// WebP image.
57    Webp,
58}
59
60impl ImageFormat {
61    /// The string representation Typst's `image` function expects.
62    pub fn as_str(&self) -> &'static str {
63        match self {
64            ImageFormat::Png => "png",
65            ImageFormat::Jpg => "jpg",
66            ImageFormat::Gif => "gif",
67            ImageFormat::Svg => "svg",
68            ImageFormat::Pdf => "pdf",
69            ImageFormat::Webp => "webp",
70        }
71    }
72}
73
74impl From<ImageFormat> for Str {
75    fn from(format: ImageFormat) -> Self {
76        Str::from(format.as_str())
77    }
78}
79
80/// A blob with metadata.
81#[derive(Clone, Debug)]
82pub struct Blob {
83    /// The bytes of the Blob.
84    pub bytes: Bytes,
85    /// Metadata containing mostly optional info like an image format.
86    pub metadata: Dict,
87}
88
89impl Blob {
90    /// Create a blob with the given bytes and metadata entries.
91    pub fn with_metadata<B, K, V, I>(bytes: B, metadata: I) -> Self
92    where
93        B: AsRef<[u8]> + Send + Sync + 'static,
94        K: Into<Str>,
95        V: IntoValue,
96        I: IntoIterator<Item = (K, V)>,
97    {
98        let mut dict = Dict::new();
99        for (key, value) in metadata {
100            dict.insert(key.into(), value.into_value());
101        }
102        Blob {
103            bytes: Bytes::new(bytes),
104            metadata: dict,
105        }
106    }
107}
108
109impl From<Bytes> for Blob {
110    fn from(bytes: Bytes) -> Self {
111        Blob {
112            bytes,
113            metadata: Dict::new(),
114        }
115    }
116}
117
118impl From<Vec<u8>> for Blob {
119    fn from(bytes: Vec<u8>) -> Self {
120        Blob {
121            bytes: Bytes::new(bytes),
122            metadata: Dict::new(),
123        }
124    }
125}
126
127impl From<Blob> for Dict {
128    fn from(value: Blob) -> Self {
129        let mut dict = Dict::new();
130        dict.insert("bytes".into(), Value::Bytes(value.bytes));
131        dict.insert("meta".into(), Value::Dict(value.metadata));
132
133        dict
134    }
135}
136
137impl Input for BlobInput {
138    fn key(&self) -> Str {
139        self.key.clone()
140    }
141
142    fn to_value(self) -> Value {
143        Value::Dict(self.value.into())
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use typst::foundations::{Array, IndexMap};
151
152    #[test]
153    fn build_blob_input() {
154        let blob_input = BlobInput::new("blob", Bytes::new([4u8].as_slice()));
155
156        let blob = blob_input.to_value();
157        let Value::Dict(mut blob) = blob else {
158            panic!("blob is not a dict");
159        };
160
161        assert_eq!(blob.len(), 2);
162        assert_eq!(
163            blob.remove("bytes".into(), None).unwrap(),
164            Value::Bytes(Bytes::new([4u8].as_slice()))
165        );
166        assert_eq!(
167            blob.remove("meta".into(), None).unwrap(),
168            Value::Dict(Dict::new())
169        );
170    }
171
172    #[test]
173    fn blob_with_metadata_collects_string_entries() {
174        let blob = Blob::with_metadata(
175            vec![1u8, 2, 3],
176            [
177                ("image_format", Str::from("png")),
178                ("variant", Str::from("dark")),
179            ],
180        );
181
182        assert_eq!(blob.bytes, Bytes::new([1u8, 2, 3].as_slice()));
183        assert_eq!(blob.metadata.len(), 2);
184        assert_eq!(
185            blob.metadata
186                .at("image_format".into(), None)
187                .expect("image_format missing"),
188            Value::Str("png".into())
189        );
190        assert_eq!(
191            blob.metadata
192                .at("variant".into(), None)
193                .expect("variant missing"),
194            Value::Str("dark".into())
195        );
196    }
197
198    #[test]
199    fn blob_with_metadata_accepts_bool() {
200        let blob = Blob::with_metadata(vec![1u8], [("flag", true)]);
201
202        assert_eq!(
203            blob.metadata.at("flag".into(), None).expect("flag missing"),
204            Value::Bool(true)
205        );
206    }
207
208    #[test]
209    fn blob_with_metadata_mixed_value_types() {
210        let blob = Blob::with_metadata(
211            vec![1u8, 2, 3],
212            [
213                ("image_format", Value::Str("png".into())),
214                ("flag", Value::Bool(true)),
215            ],
216        );
217
218        assert_eq!(blob.metadata.len(), 2);
219        assert_eq!(
220            blob.metadata
221                .at("image_format".into(), None)
222                .expect("image_format missing"),
223            Value::Str("png".into())
224        );
225        assert_eq!(
226            blob.metadata.at("flag".into(), None).expect("flag missing"),
227            Value::Bool(true)
228        );
229    }
230
231    #[test]
232    fn blob_input_image_sets_format_metadata() {
233        let blob_input = BlobInput::image("logo", vec![9u8, 8, 7], ImageFormat::Png);
234
235        assert_eq!(blob_input.key, Str::from("logo"));
236        let blob = blob_input.to_value();
237        let Value::Dict(mut blob) = blob else {
238            panic!("blob is not a dict");
239        };
240        let Value::Dict(meta) = blob.remove("meta".into(), None).unwrap() else {
241            panic!("meta is not a dict");
242        };
243        assert_eq!(meta.len(), 1);
244        assert_eq!(
245            meta.at("image_format".into(), None)
246                .expect("image_format missing"),
247            Value::Str("png".into())
248        );
249    }
250
251    #[test]
252    fn blob_with_metadata_supports_dict_image_format_for_raw_pixels() {
253        // Raw pixel data needs `image_format` to be a dict with encoding/width/height.
254        // 2x2 rgb8 image = 4 pixels * 3 channels = 12 bytes.
255        let pixels = vec![255u8, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255];
256
257        let mut format_dict = Dict::new();
258        format_dict.insert("encoding".into(), Value::Str("rgb8".into()));
259        format_dict.insert("width".into(), Value::Int(2));
260        format_dict.insert("height".into(), Value::Int(2));
261
262        let blob = Blob::with_metadata(pixels, [("image_format", Value::Dict(format_dict))]);
263
264        let Value::Dict(format) = blob
265            .metadata
266            .at("image_format".into(), None)
267            .expect("image_format missing")
268        else {
269            panic!("image_format should be a dict for raw pixel data");
270        };
271        assert_eq!(
272            format.at("encoding".into(), None).unwrap(),
273            Value::Str("rgb8".into())
274        );
275        assert_eq!(format.at("width".into(), None).unwrap(), Value::Int(2));
276        assert_eq!(format.at("height".into(), None).unwrap(), Value::Int(2));
277    }
278
279    #[test]
280    fn build_blob_input_with_meta() {
281        let blob_input = BlobInput::new(
282            "blob",
283            Blob {
284                bytes: Bytes::new([1u8, 2, 3].as_slice()),
285                metadata: {
286                    let mut meta = Dict::new();
287                    meta.insert("format".into(), Value::Str("png".into()));
288                    meta.insert(
289                        "custom".into(),
290                        Value::Array(Array::from_iter(vec![
291                            Value::Str("value1".into()),
292                            Value::Str("value2".into()),
293                        ])),
294                    );
295
296                    meta
297                },
298            },
299        );
300
301        let blob = blob_input.to_value();
302        let Value::Dict(mut blob) = blob else {
303            panic!("blob is not a dict");
304        };
305
306        assert_eq!(blob.len(), 2);
307        assert_eq!(
308            blob.remove("bytes".into(), None).unwrap(),
309            Value::Bytes(Bytes::new([1u8, 2, 3].as_slice()))
310        );
311        assert_eq!(
312            blob.remove("meta".into(), None).unwrap(),
313            Value::Dict(Dict::from(IndexMap::from_iter(vec![
314                ("format".into(), Value::Str("png".into())),
315                (
316                    "custom".into(),
317                    Value::Array(Array::from_iter(vec![
318                        Value::Str("value1".into()),
319                        Value::Str("value2".into())
320                    ]))
321                )
322            ])))
323        );
324    }
325}