Repository: linebender/resvg (language: rust)

Issue:
Make image decoders (gif, jpeg, png) optional
Would you accept a PR that makes these dependencies optional?

I understand that the SVG spec includes jpeg and png. If I know that my SVGs do not make use of these it would still be nice to eliminate those crates from the dependency graph.


Discussion / hints:
I think we could. You can mimic `text` build feature.

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 07402646d..e06f3bdc0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -28,9 +28,12 @@ jobs:
       working-directory: usvg
       run: cargo test
 
+    - name: Build resvg without default support
+      run: cargo check --no-default-features
+
     - name: Build usvg without default support
       working-directory: usvg
-      run: cargo build --no-default-features
+      run: cargo check --no-default-features
 
   msrv:
     runs-on: ubuntu-18.04
diff --git a/Cargo.toml b/Cargo.toml
index 69bf9c57a..cda513c46 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,11 +17,11 @@ name = "resvg"
 required-features = ["filter", "text", "system-fonts", "memmap-fonts"]
 
 [dependencies]
-gif = "0.11"
-jpeg-decoder = { version = "0.2", default-features = false, features = ["platform_independent"] }
+gif = { version = "0.11", optional = true }
+jpeg-decoder = { version = "0.2", default-features = false, features = ["platform_independent"], optional = true }
 log = "0.4"
 pico-args =  { version = "0.5", features = ["eq-separator"] }
-png = "0.17"
+png = { version = "0.17", optional = true }
 rgb = "0.8"
 svgfilters = { path = "svgfilters", version = "0.4", optional = true }
 svgtypes = "0.8"
@@ -32,7 +32,7 @@ usvg = { path = "usvg", version = "0.23.0", default-features = false }
 once_cell = "1.5"
 
 [features]
-default = ["filter", "text", "system-fonts", "memmap-fonts"]
+default = ["filter", "text", "system-fonts", "memmap-fonts", "raster-images"]
 # enables SVG Filter support
 # adds around 100KiB to your binary
 filter = ["svgfilters", "usvg/filter"]
@@ -46,3 +46,6 @@ memmap-fonts = ["usvg/memmap-fonts"]
 # enables the --dump-svg flag in CLI
 # adds around 50KiB to your binary
 dump-svg = ["usvg/export"]
+# enables decoding and rendering of raster images
+# when disabled, `image` elements with SVG data will still be rendered
+raster-images = ["gif", "jpeg-decoder", "png"]
diff --git a/c-api/Cargo.toml b/c-api/Cargo.toml
index 0f9f70d77..17c4b837a 100644
--- a/c-api/Cargo.toml
+++ b/c-api/Cargo.toml
@@ -18,7 +18,7 @@ resvg = { path = "../", default-features = false }
 
 [features]
 # we do not provide the dump-svg/export feature here, because it's not available to C API anyway
-default = ["filter", "text", "system-fonts", "memmap-fonts"]
+default = ["filter", "text", "system-fonts", "memmap-fonts", "raster-images"]
 # enables SVG Filter support
 # adds around 100KiB to your binary
 filter = ["resvg/filter"]
@@ -29,3 +29,4 @@ text = ["resvg/text"]
 system-fonts = ["resvg/system-fonts"]
 # enables font files memmaping for faster loading (only for `text`)
 memmap-fonts = ["resvg/memmap-fonts"]
+raster-images = ["resvg/raster-images"]
diff --git a/src/image.rs b/src/image.rs
index 326f1cc5f..afd5014d6 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -20,111 +20,33 @@ pub fn draw_kind(
     canvas: &mut Canvas,
 ) {
     match kind {
+        usvg::ImageKind::SVG(ref subtree) => {
+            draw_svg(subtree, view_box, canvas);
+        }
+        #[cfg(feature = "raster-images")]
         usvg::ImageKind::JPEG(ref data) => {
-            match read_jpeg(data) {
-                Some(image) => { draw_raster(&image, view_box, rendering_mode, canvas); }
+            match raster_images::read_jpeg(data) {
+                Some(image) => { raster_images::draw_raster(&image, view_box, rendering_mode, canvas); }
                 None => log::warn!("Failed to decode a JPEG image."),
             }
         }
+        #[cfg(feature = "raster-images")]
         usvg::ImageKind::PNG(ref data) => {
-            match read_png(data) {
-                Some(image) => { draw_raster(&image, view_box, rendering_mode, canvas); }
+            match raster_images::read_png(data) {
+                Some(image) => { raster_images::draw_raster(&image, view_box, rendering_mode, canvas); }
                 None => log::warn!("Failed to decode a PNG image."),
             }
         }
+        #[cfg(feature = "raster-images")]
         usvg::ImageKind::GIF(ref data) => {
-            match read_gif(data) {
-                Some(image) => { draw_raster(&image, view_box, rendering_mode, canvas); }
+            match raster_images::read_gif(data) {
+                Some(image) => { raster_images::draw_raster(&image, view_box, rendering_mode, canvas); }
                 None => log::warn!("Failed to decode a GIF image."),
             }
         }
-        usvg::ImageKind::SVG(ref subtree) => {
-            draw_svg(subtree, view_box, canvas);
-        }
-    }
-}
-
-fn draw_raster(
-    img: &Image,
-    view_box: usvg::ViewBox,
-    rendering_mode: usvg::ImageRendering,
-    canvas: &mut Canvas,
-) -> Option<()> {
-    let (w, h) = img.size.dimensions();
-    let mut pixmap = tiny_skia::Pixmap::new(w, h)?;
-    image_to_pixmap(img, pixmap.data_mut());
-
-    let mut filter = tiny_skia::FilterQuality::Bicubic;
-    if rendering_mode == usvg::ImageRendering::OptimizeSpeed {
-        filter = tiny_skia::FilterQuality::Nearest;
-    }
-
-    let r = image_rect(&view_box, img.size);
-    let rect = tiny_skia::Rect::from_xywh(
-        r.x() as f32, r.y() as f32,
-        r.width() as f32, r.height() as f32,
-    )?;
-
-    let ts = tiny_skia::Transform::from_row(
-        rect.width() as f32 / pixmap.width() as f32,
-        0.0,
-        0.0,
-        rect.height() as f32 / pixmap.height() as f32,
-        r.x() as f32,
-        r.y() as f32,
-    );
-
-    let pattern = tiny_skia::Pattern::new(
-        pixmap.as_ref(),
-        tiny_skia::SpreadMode::Pad,
-        filter,
-        1.0,
-        ts,
-    );
-    let mut paint = tiny_skia::Paint::default();
-    paint.shader = pattern;
-
-    if view_box.aspect.slice {
-        let r = view_box.rect;
-        let rect = tiny_skia::Rect::from_xywh(
-            r.x() as f32, r.y() as f32,
-            r.width() as f32, r.height() as f32,
-        )?;
-
-        canvas.set_clip_rect(rect);
-    }
-
-    canvas.pixmap.fill_rect(rect, &paint, canvas.transform, canvas.clip.as_ref());
-    canvas.clip = None;
-
-    Some(())
-}
-
-fn image_to_pixmap(image: &Image, pixmap: &mut [u8]) {
-    use rgb::FromSlice;
-
-    let mut i = 0;
-    match &image.data {
-        ImageData::RGB(data) => {
-            for p in data.as_rgb() {
-                pixmap[i + 0] = p.r;
-                pixmap[i + 1] = p.g;
-                pixmap[i + 2] = p.b;
-                pixmap[i + 3] = 255;
-
-                i += tiny_skia::BYTES_PER_PIXEL;
-            }
-        }
-        ImageData::RGBA(data) => {
-            for p in data.as_rgba() {
-                let a = p.a as f64 / 255.0;
-                pixmap[i + 0] = (p.r as f64 * a + 0.5) as u8;
-                pixmap[i + 1] = (p.g as f64 * a + 0.5) as u8;
-                pixmap[i + 2] = (p.b as f64 * a + 0.5) as u8;
-                pixmap[i + 3] = p.a;
-
-                i += tiny_skia::BYTES_PER_PIXEL;
-            }
+        #[cfg(not(feature = "raster-images"))]
+        _ => {
+            log::warn!("Images decoding was disabled by a build feature.");
         }
     }
 }
@@ -166,118 +88,208 @@ fn draw_svg(
     Some(())
 }
 
-struct Image {
-    data: ImageData,
-    size: usvg::ScreenSize,
-}
-
-enum ImageData {
-    RGB(Vec<u8>),
-    RGBA(Vec<u8>),
-}
+#[cfg(feature = "raster-images")]
+mod raster_images {
+    use crate::render::Canvas;
+
+    pub fn draw_raster(
+        img: &Image,
+        view_box: usvg::ViewBox,
+        rendering_mode: usvg::ImageRendering,
+        canvas: &mut Canvas,
+    ) -> Option<()> {
+        let (w, h) = img.size.dimensions();
+        let mut pixmap = tiny_skia::Pixmap::new(w, h)?;
+        image_to_pixmap(img, pixmap.data_mut());
+
+        let mut filter = tiny_skia::FilterQuality::Bicubic;
+        if rendering_mode == usvg::ImageRendering::OptimizeSpeed {
+            filter = tiny_skia::FilterQuality::Nearest;
+        }
 
-fn read_png(data: &[u8]) -> Option<Image> {
-    let mut decoder = png::Decoder::new(data);
-    decoder.set_transformations(png::Transformations::normalize_to_color8());
-    let mut reader = decoder.read_info().ok()?;
-    let mut img_data = vec![0; reader.output_buffer_size()];
-    let info = reader.next_frame(&mut img_data).ok()?;
-
-    let size = usvg::ScreenSize::new(info.width, info.height)?;
-
-    let data = match info.color_type {
-        png::ColorType::Rgb => ImageData::RGB(img_data),
-        png::ColorType::Rgba => ImageData::RGBA(img_data),
-        png::ColorType::Grayscale => {
-            let mut rgb_data = Vec::with_capacity(img_data.len() * 3);
-            for gray in img_data {
-                rgb_data.push(gray);
-                rgb_data.push(gray);
-                rgb_data.push(gray);
-            }
+        let r = image_rect(&view_box, img.size);
+        let rect = tiny_skia::Rect::from_xywh(
+            r.x() as f32, r.y() as f32,
+            r.width() as f32, r.height() as f32,
+        )?;
 
-            ImageData::RGB(rgb_data)
+        let ts = tiny_skia::Transform::from_row(
+            rect.width() as f32 / pixmap.width() as f32,
+            0.0,
+            0.0,
+            rect.height() as f32 / pixmap.height() as f32,
+            r.x() as f32,
+            r.y() as f32,
+        );
+
+        let pattern = tiny_skia::Pattern::new(
+            pixmap.as_ref(),
+            tiny_skia::SpreadMode::Pad,
+            filter,
+            1.0,
+            ts,
+        );
+        let mut paint = tiny_skia::Paint::default();
+        paint.shader = pattern;
+
+        if view_box.aspect.slice {
+            let r = view_box.rect;
+            let rect = tiny_skia::Rect::from_xywh(
+                r.x() as f32, r.y() as f32,
+                r.width() as f32, r.height() as f32,
+            )?;
+
+            canvas.set_clip_rect(rect);
         }
-        png::ColorType::GrayscaleAlpha => {
-            let mut rgba_data = Vec::with_capacity(img_data.len() * 2);
-            for slice in img_data.chunks(2) {
-                let gray = slice[0];
-                let alpha = slice[1];
-                rgba_data.push(gray);
-                rgba_data.push(gray);
-                rgba_data.push(gray);
-                rgba_data.push(alpha);
-            }
 
-            ImageData::RGBA(rgba_data)
-        }
-        png::ColorType::Indexed => {
-            log::warn!("Indexed PNG is not supported.");
-            return None
+        canvas.pixmap.fill_rect(rect, &paint, canvas.transform, canvas.clip.as_ref());
+        canvas.clip = None;
+
+        Some(())
+    }
+
+    fn image_to_pixmap(image: &Image, pixmap: &mut [u8]) {
+        use rgb::FromSlice;
+
+        let mut i = 0;
+        match &image.data {
+            ImageData::RGB(data) => {
+                for p in data.as_rgb() {
+                    pixmap[i + 0] = p.r;
+                    pixmap[i + 1] = p.g;
+                    pixmap[i + 2] = p.b;
+                    pixmap[i + 3] = 255;
+
+                    i += tiny_skia::BYTES_PER_PIXEL;
+                }
+            }
+            ImageData::RGBA(data) => {
+                for p in data.as_rgba() {
+                    let a = p.a as f64 / 255.0;
+                    pixmap[i + 0] = (p.r as f64 * a + 0.5) as u8;
+                    pixmap[i + 1] = (p.g as f64 * a + 0.5) as u8;
+                    pixmap[i + 2] = (p.b as f64 * a + 0.5) as u8;
+                    pixmap[i + 3] = p.a;
+
+                    i += tiny_skia::BYTES_PER_PIXEL;
+                }
+            }
         }
-    };
+    }
 
-    Some(Image {
-        data,
-        size,
-    })
-}
+    pub struct Image {
+        data: ImageData,
+        size: usvg::ScreenSize,
+    }
 
-fn read_jpeg(data: &[u8]) -> Option<Image> {
-    let mut decoder = jpeg_decoder::Decoder::new(data);
-    let img_data = decoder.decode().ok()?;
-    let info = decoder.info()?;
-
-    let size = usvg::ScreenSize::new(info.width as u32, info.height as u32)?;
-
-    let data = match info.pixel_format {
-        jpeg_decoder::PixelFormat::RGB24 => ImageData::RGB(img_data),
-        jpeg_decoder::PixelFormat::L8 => {
-            let mut rgb_data = Vec::with_capacity(img_data.len() * 3);
-            for gray in img_data {
-                rgb_data.push(gray);
-                rgb_data.push(gray);
-                rgb_data.push(gray);
+    enum ImageData {
+        RGB(Vec<u8>),
+        RGBA(Vec<u8>),
+    }
+
+    pub fn read_png(data: &[u8]) -> Option<Image> {
+        let mut decoder = png::Decoder::new(data);
+        decoder.set_transformations(png::Transformations::normalize_to_color8());
+        let mut reader = decoder.read_info().ok()?;
+        let mut img_data = vec![0; reader.output_buffer_size()];
+        let info = reader.next_frame(&mut img_data).ok()?;
+
+        let size = usvg::ScreenSize::new(info.width, info.height)?;
+
+        let data = match info.color_type {
+            png::ColorType::Rgb => ImageData::RGB(img_data),
+            png::ColorType::Rgba => ImageData::RGBA(img_data),
+            png::ColorType::Grayscale => {
+                let mut rgb_data = Vec::with_capacity(img_data.len() * 3);
+                for gray in img_data {
+                    rgb_data.push(gray);
+                    rgb_data.push(gray);
+                    rgb_data.push(gray);
+                }
+
+                ImageData::RGB(rgb_data)
+            }
+            png::ColorType::GrayscaleAlpha => {
+                let mut rgba_data = Vec::with_capacity(img_data.len() * 2);
+                for slice in img_data.chunks(2) {
+                    let gray = slice[0];
+                    let alpha = slice[1];
+                    rgba_data.push(gray);
+                    rgba_data.push(gray);
+                    rgba_data.push(gray);
+                    rgba_data.push(alpha);
+                }
+
+                ImageData::RGBA(rgba_data)
             }
+            png::ColorType::Indexed => {
+                log::warn!("Indexed PNG is not supported.");
+                return None
+            }
+        };
 
-            ImageData::RGB(rgb_data)
-        }
-        _ => return None,
-    };
+        Some(Image {
+            data,
+            size,
+        })
+    }
 
-    Some(Image {
-        data,
-        size,
-    })
-}
+    pub fn read_jpeg(data: &[u8]) -> Option<Image> {
+        let mut decoder = jpeg_decoder::Decoder::new(data);
+        let img_data = decoder.decode().ok()?;
+        let info = decoder.info()?;
 
-fn read_gif(data: &[u8]) -> Option<Image> {
-    let mut decoder = gif::DecodeOptions::new();
-    decoder.set_color_output(gif::ColorOutput::RGBA);
-    let mut decoder = decoder.read_info(data).ok()?;
-    let first_frame = decoder.read_next_frame().ok()??;
+        let size = usvg::ScreenSize::new(info.width as u32, info.height as u32)?;
 
-    let size = usvg::ScreenSize::new(u32::from(first_frame.width), u32::from(first_frame.height))?;
+        let data = match info.pixel_format {
+            jpeg_decoder::PixelFormat::RGB24 => ImageData::RGB(img_data),
+            jpeg_decoder::PixelFormat::L8 => {
+                let mut rgb_data = Vec::with_capacity(img_data.len() * 3);
+                for gray in img_data {
+                    rgb_data.push(gray);
+                    rgb_data.push(gray);
+                    rgb_data.push(gray);
+                }
 
-    Some(Image {
-        data: ImageData::RGBA(first_frame.buffer.to_vec()),
-        size,
-    })
-}
+                ImageData::RGB(rgb_data)
+            }
+            _ => return None,
+        };
 
-/// Calculates an image rect depending on the provided view box.
-fn image_rect(
-    view_box: &usvg::ViewBox,
-    img_size: usvg::ScreenSize,
-) -> usvg::Rect {
-    let new_size = img_size.to_size().fit_view_box(view_box);
-    let (x, y) = usvg::utils::aligned_pos(
-        view_box.aspect.align,
-        view_box.rect.x(),
-        view_box.rect.y(),
-        view_box.rect.width() - new_size.width(),
-        view_box.rect.height() - new_size.height(),
-    );
+        Some(Image {
+            data,
+            size,
+        })
+    }
+
+    pub fn read_gif(data: &[u8]) -> Option<Image> {
+        let mut decoder = gif::DecodeOptions::new();
+        decoder.set_color_output(gif::ColorOutput::RGBA);
+        let mut decoder = decoder.read_info(data).ok()?;
+        let first_frame = decoder.read_next_frame().ok()??;
 
-    new_size.to_rect(x, y)
+        let size = usvg::ScreenSize::new(u32::from(first_frame.width), u32::from(first_frame.height))?;
+
+        Some(Image {
+            data: ImageData::RGBA(first_frame.buffer.to_vec()),
+            size,
+        })
+    }
+
+    /// Calculates an image rect depending on the provided view box.
+    fn image_rect(
+        view_box: &usvg::ViewBox,
+        img_size: usvg::ScreenSize,
+    ) -> usvg::Rect {
+        let new_size = img_size.to_size().fit_view_box(view_box);
+        let (x, y) = usvg::utils::aligned_pos(
+            view_box.aspect.align,
+            view_box.rect.x(),
+            view_box.rect.y(),
+            view_box.rect.width() - new_size.width(),
+            view_box.rect.height() - new_size.height(),
+        );
+
+        new_size.to_rect(x, y)
+    }
 }


Test patch: