| 1 | // Copyright 2018 the Resvg Authors |
| 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
| 3 | |
| 4 | use crate::OptionLog; |
| 5 | |
| 6 | pub struct Context { |
| 7 | pub max_bbox: tiny_skia::IntRect, |
| 8 | } |
| 9 | |
| 10 | pub fn render_nodes( |
| 11 | parent: &usvg::Group, |
| 12 | ctx: &Context, |
| 13 | transform: tiny_skia::Transform, |
| 14 | pixmap: &mut tiny_skia::PixmapMut, |
| 15 | ) { |
| 16 | for node: &Node in parent.children() { |
| 17 | render_node(node, ctx, transform, pixmap); |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | pub fn render_node( |
| 22 | node: &usvg::Node, |
| 23 | ctx: &Context, |
| 24 | transform: tiny_skia::Transform, |
| 25 | pixmap: &mut tiny_skia::PixmapMut, |
| 26 | ) { |
| 27 | match node { |
| 28 | usvg::Node::Group(ref group: &Box) => { |
| 29 | render_group(group, ctx, transform, pixmap); |
| 30 | } |
| 31 | usvg::Node::Path(ref path: &Box) => { |
| 32 | crate::path::render( |
| 33 | path, |
| 34 | tiny_skia::BlendMode::SourceOver, |
| 35 | ctx, |
| 36 | transform, |
| 37 | pixmap, |
| 38 | ); |
| 39 | } |
| 40 | usvg::Node::Image(ref image: &Box) => { |
| 41 | crate::image::render(image, transform, pixmap); |
| 42 | } |
| 43 | usvg::Node::Text(ref text: &Box) => { |
| 44 | render_group(group:text.flattened(), ctx, transform, pixmap); |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | fn render_group( |
| 50 | group: &usvg::Group, |
| 51 | ctx: &Context, |
| 52 | transform: tiny_skia::Transform, |
| 53 | pixmap: &mut tiny_skia::PixmapMut, |
| 54 | ) -> Option<()> { |
| 55 | let transform = transform.pre_concat(group.transform()); |
| 56 | |
| 57 | if !group.should_isolate() { |
| 58 | render_nodes(group, ctx, transform, pixmap); |
| 59 | return Some(()); |
| 60 | } |
| 61 | |
| 62 | let bbox = group.layer_bounding_box().transform(transform)?; |
| 63 | |
| 64 | let mut ibbox = if group.filters().is_empty() { |
| 65 | // Convert group bbox into an integer one, expanding each side outwards by 2px |
| 66 | // to make sure that anti-aliased pixels would not be clipped. |
| 67 | tiny_skia::IntRect::from_xywh( |
| 68 | bbox.x().floor() as i32 - 2, |
| 69 | bbox.y().floor() as i32 - 2, |
| 70 | bbox.width().ceil() as u32 + 4, |
| 71 | bbox.height().ceil() as u32 + 4, |
| 72 | )? |
| 73 | } else { |
| 74 | // The bounding box for groups with filters is special and should not be expanded by 2px, |
| 75 | // because it's already acting as a clipping region. |
| 76 | let bbox = bbox.to_int_rect(); |
| 77 | // Make sure our filter region is not bigger than 4x the canvas size. |
| 78 | // This is required mainly to prevent huge filter regions that would tank the performance. |
| 79 | // It should not affect the final result in any way. |
| 80 | crate::geom::fit_to_rect(bbox, ctx.max_bbox)? |
| 81 | }; |
| 82 | |
| 83 | // Make sure our layer is not bigger than 4x the canvas size. |
| 84 | // This is required to prevent huge layers. |
| 85 | if group.filters().is_empty() { |
| 86 | ibbox = crate::geom::fit_to_rect(ibbox, ctx.max_bbox)?; |
| 87 | } |
| 88 | |
| 89 | let shift_ts = { |
| 90 | // Original shift. |
| 91 | let mut dx = bbox.x(); |
| 92 | let mut dy = bbox.y(); |
| 93 | |
| 94 | // Account for subpixel positioned layers. |
| 95 | dx -= bbox.x() - ibbox.x() as f32; |
| 96 | dy -= bbox.y() - ibbox.y() as f32; |
| 97 | |
| 98 | tiny_skia::Transform::from_translate(-dx, -dy) |
| 99 | }; |
| 100 | |
| 101 | let transform = shift_ts.pre_concat(transform); |
| 102 | |
| 103 | let mut sub_pixmap = tiny_skia::Pixmap::new(ibbox.width(), ibbox.height()) |
| 104 | .log_none(|| log::warn!("Failed to allocate a group layer for: {:?}." , ibbox))?; |
| 105 | |
| 106 | render_nodes(group, ctx, transform, &mut sub_pixmap.as_mut()); |
| 107 | |
| 108 | if !group.filters().is_empty() { |
| 109 | for filter in group.filters() { |
| 110 | crate::filter::apply(filter, transform, &mut sub_pixmap); |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | if let Some(clip_path) = group.clip_path() { |
| 115 | crate::clip::apply(clip_path, transform, &mut sub_pixmap); |
| 116 | } |
| 117 | |
| 118 | if let Some(mask) = group.mask() { |
| 119 | crate::mask::apply(mask, ctx, transform, &mut sub_pixmap); |
| 120 | } |
| 121 | |
| 122 | let paint = tiny_skia::PixmapPaint { |
| 123 | opacity: group.opacity().get(), |
| 124 | blend_mode: convert_blend_mode(group.blend_mode()), |
| 125 | quality: tiny_skia::FilterQuality::Nearest, |
| 126 | }; |
| 127 | |
| 128 | pixmap.draw_pixmap( |
| 129 | ibbox.x(), |
| 130 | ibbox.y(), |
| 131 | sub_pixmap.as_ref(), |
| 132 | &paint, |
| 133 | tiny_skia::Transform::identity(), |
| 134 | None, |
| 135 | ); |
| 136 | |
| 137 | Some(()) |
| 138 | } |
| 139 | |
| 140 | pub fn convert_blend_mode(mode: usvg::BlendMode) -> tiny_skia::BlendMode { |
| 141 | match mode { |
| 142 | usvg::BlendMode::Normal => tiny_skia::BlendMode::SourceOver, |
| 143 | usvg::BlendMode::Multiply => tiny_skia::BlendMode::Multiply, |
| 144 | usvg::BlendMode::Screen => tiny_skia::BlendMode::Screen, |
| 145 | usvg::BlendMode::Overlay => tiny_skia::BlendMode::Overlay, |
| 146 | usvg::BlendMode::Darken => tiny_skia::BlendMode::Darken, |
| 147 | usvg::BlendMode::Lighten => tiny_skia::BlendMode::Lighten, |
| 148 | usvg::BlendMode::ColorDodge => tiny_skia::BlendMode::ColorDodge, |
| 149 | usvg::BlendMode::ColorBurn => tiny_skia::BlendMode::ColorBurn, |
| 150 | usvg::BlendMode::HardLight => tiny_skia::BlendMode::HardLight, |
| 151 | usvg::BlendMode::SoftLight => tiny_skia::BlendMode::SoftLight, |
| 152 | usvg::BlendMode::Difference => tiny_skia::BlendMode::Difference, |
| 153 | usvg::BlendMode::Exclusion => tiny_skia::BlendMode::Exclusion, |
| 154 | usvg::BlendMode::Hue => tiny_skia::BlendMode::Hue, |
| 155 | usvg::BlendMode::Saturation => tiny_skia::BlendMode::Saturation, |
| 156 | usvg::BlendMode::Color => tiny_skia::BlendMode::Color, |
| 157 | usvg::BlendMode::Luminosity => tiny_skia::BlendMode::Luminosity, |
| 158 | } |
| 159 | } |
| 160 | |