1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
//!
//! SVG image support.
//!
//! # Crate
//!
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]

use std::sync::Arc;

use zng_app::AppExtension;
use zng_ext_image::*;
use zng_txt::{formatx, Txt};
use zng_unit::{Px, PxSize};

/// Application extension that installs SVG handling.
///
/// This extension installs the [`SvgRenderCache`] in [`IMAGES`] on init.
#[derive(Default)]
pub struct SvgManager {}

impl AppExtension for SvgManager {
    fn init(&mut self) {
        IMAGES.install_proxy(Box::new(SvgRenderCache::default()));
    }
}

/// Image cache proxy that handlers SVG requests.
#[derive(Default)]
pub struct SvgRenderCache {}
impl ImageCacheProxy for SvgRenderCache {
    fn data(
        &mut self,
        key: &ImageHash,
        data: &[u8],
        format: &ImageDataFormat,
        mode: ImageCacheMode,
        downscale: Option<ImageDownscale>,
        mask: Option<ImageMaskMode>,
        is_loaded: bool,
    ) -> Option<ImageVar> {
        let data = match format {
            ImageDataFormat::FileExtension(txt) if txt == "svg" || txt == "svgz" => SvgData::Raw(data.to_vec()),
            ImageDataFormat::MimeType(txt) if txt == "image/svg+xml" => SvgData::Raw(data.to_vec()),
            ImageDataFormat::Unknown => SvgData::Str(svg_data_from_unknown(data)?),
            _ => return None,
        };
        let key = if is_loaded {
            None // already cached, return image is internal
        } else {
            Some(*key)
        };
        Some(IMAGES.image_task(async move { load(data, downscale) }, mode, key, None, None, mask))
    }

    fn is_data_proxy(&self) -> bool {
        true
    }
}

enum SvgData {
    Raw(Vec<u8>),
    Str(String),
}
fn load(data: SvgData, downscale: Option<ImageDownscale>) -> ImageSource {
    let options = resvg::usvg::Options::default();

    let tree = match data {
        SvgData::Raw(data) => resvg::usvg::Tree::from_data(&data, &options),
        SvgData::Str(data) => resvg::usvg::Tree::from_str(&data, &options),
    };
    match tree {
        Ok(tree) => {
            let mut size = tree.size().to_int_size();
            if let Some(d) = downscale {
                let s = d.resize_dimensions(PxSize::new(Px(size.width() as _), Px(size.height() as _)));
                match resvg::tiny_skia::IntSize::from_wh(s.width.0 as _, s.height.0 as _) {
                    Some(s) => size = s,
                    None => tracing::error!("cannot resize svg to zero size"),
                }
            }
            let mut pixmap = match resvg::tiny_skia::Pixmap::new(size.width(), size.height()) {
                Some(p) => p,
                None => return error(formatx!("can't allocate pixmap for {:?} svg", size)),
            };
            resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pixmap.as_mut());
            let size = PxSize::new(Px(pixmap.width() as _), Px(pixmap.height() as _));

            let mut data = pixmap.take();
            for pixel in data.chunks_exact_mut(4) {
                pixel.swap(0, 2);
            }

            ImageSource::Data(
                ImageHash::compute(&data),
                Arc::new(data),
                ImageDataFormat::Bgra8 {
                    size,
                    ppi: Some(ImagePpi::splat(options.dpi)),
                },
            )
        }
        Err(e) => error(formatx!("{e}")),
    }
}

fn error(error: Txt) -> ImageSource {
    ImageSource::Image(IMAGES.dummy(Some(error)))
}

fn svg_data_from_unknown(data: &[u8]) -> Option<String> {
    if data.starts_with(&[0x1f, 0x8b]) {
        // gzip magic number
        let data = resvg::usvg::decompress_svgz(data).ok()?;
        uncompressed_data_is_svg(&data)
    } else {
        uncompressed_data_is_svg(data)
    }
}
fn uncompressed_data_is_svg(data: &[u8]) -> Option<String> {
    let s = std::str::from_utf8(data).ok()?;
    if s.contains("http://www.w3.org/2000/svg") {
        Some(s.to_owned())
    } else {
        None
    }
}