Skip to main content

bcp_types/
image.rs

1use crate::enums::MediaType;
2use crate::error::TypeError;
3use crate::fields::{
4    decode_bytes_value, decode_field_header, decode_varint_value, encode_bytes_field,
5    encode_varint_field, skip_field,
6};
7
8/// IMAGE block — image content or reference.
9///
10/// Can carry either inline image bytes or a URI pointing to an external
11/// image. The `media_type` field tells the decoder how to interpret the
12/// `data` payload (PNG, JPEG, SVG, etc.).
13///
14/// Field layout within body:
15///
16/// ```text
17/// ┌──────────┬───────────┬────────────┬──────────────────────────┐
18/// │ Field ID │ Wire Type │ Name       │ Description              │
19/// ├──────────┼───────────┼────────────┼──────────────────────────┤
20/// │ 1        │ Varint    │ media_type │ MediaType enum byte      │
21/// │ 2        │ Bytes     │ alt_text   │ Alt text description     │
22/// │ 3        │ Bytes     │ data       │ Image bytes or URI       │
23/// └──────────┴───────────┴────────────┴──────────────────────────┘
24/// ```
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct ImageBlock {
27    pub media_type: MediaType,
28    pub alt_text: String,
29    /// Raw image bytes (inline) or a UTF-8 URI string (reference).
30    /// The block's `IS_REFERENCE` flag in `BlockFlags` distinguishes
31    /// between inline data and a URI reference.
32    pub data: Vec<u8>,
33}
34
35impl ImageBlock {
36    /// Serialize this block's fields into a TLV-encoded body.
37    pub fn encode_body(&self) -> Vec<u8> {
38        let mut buf = Vec::new();
39        encode_varint_field(&mut buf, 1, u64::from(self.media_type.to_wire_byte()));
40        encode_bytes_field(&mut buf, 2, self.alt_text.as_bytes());
41        encode_bytes_field(&mut buf, 3, &self.data);
42        buf
43    }
44
45    /// Deserialize an IMAGE block from a TLV-encoded body.
46    pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
47        let mut media_type: Option<MediaType> = None;
48        let mut alt_text: Option<String> = None;
49        let mut data: Option<Vec<u8>> = None;
50
51        while !buf.is_empty() {
52            let (header, n) = decode_field_header(buf)?;
53            buf = &buf[n..];
54
55            match header.field_id {
56                1 => {
57                    let (v, n) = decode_varint_value(buf)?;
58                    buf = &buf[n..];
59                    media_type = Some(MediaType::from_wire_byte(v as u8)?);
60                }
61                2 => {
62                    let (d, n) = decode_bytes_value(buf)?;
63                    buf = &buf[n..];
64                    alt_text = Some(String::from_utf8_lossy(d).into_owned());
65                }
66                3 => {
67                    let (d, n) = decode_bytes_value(buf)?;
68                    buf = &buf[n..];
69                    data = Some(d.to_vec());
70                }
71                _ => {
72                    let n = skip_field(buf, header.wire_type)?;
73                    buf = &buf[n..];
74                }
75            }
76        }
77
78        Ok(Self {
79            media_type: media_type.ok_or(TypeError::MissingRequiredField {
80                field: "media_type",
81            })?,
82            alt_text: alt_text.ok_or(TypeError::MissingRequiredField { field: "alt_text" })?,
83            data: data.ok_or(TypeError::MissingRequiredField { field: "data" })?,
84        })
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn roundtrip_inline_png() {
94        let block = ImageBlock {
95            media_type: MediaType::Png,
96            alt_text: "A screenshot of the app".to_string(),
97            data: vec![0x89, 0x50, 0x4E, 0x47], // PNG magic bytes (truncated)
98        };
99        let body = block.encode_body();
100        let decoded = ImageBlock::decode_body(&body).unwrap();
101        assert_eq!(decoded, block);
102    }
103
104    #[test]
105    fn roundtrip_svg_reference() {
106        let block = ImageBlock {
107            media_type: MediaType::Svg,
108            alt_text: "Architecture diagram".to_string(),
109            data: b"https://example.com/diagram.svg".to_vec(),
110        };
111        let body = block.encode_body();
112        let decoded = ImageBlock::decode_body(&body).unwrap();
113        assert_eq!(decoded, block);
114    }
115}