1use crate::Input;
2use typst::foundations::{Bytes, Dict, IntoValue, Str, Value};
3
4#[derive(Clone, Debug)]
6pub struct BlobInput {
7 pub key: Str,
11 pub value: Blob,
13}
14
15impl BlobInput {
16 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum ImageFormat {
46 Png,
48 Jpg,
50 Gif,
52 Svg,
54 Pdf,
56 Webp,
58}
59
60impl ImageFormat {
61 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#[derive(Clone, Debug)]
82pub struct Blob {
83 pub bytes: Bytes,
85 pub metadata: Dict,
87}
88
89impl Blob {
90 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 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}