Repository: linebender/resvg (language: rust)

Issue:
Make `usvg::Tree` read-only
Right now, `usvg::Tree` can be modified in pretty much any way and can be even constructed from scratch. It might have some benefits, but also introduces a lot of issues:

1.  We do not control the content. `resvg` basically assumes that `usvg::Tree` was parsed from SVG. There are no guarantee it would work with a manually created tree. By having a mutable tree the user assumes that everything can be changed, but in reality almost nothing can be safely changed.
2. It complicates a tree writing back to SVG, because again, we don't know if it was modified. Someone could remove all `id` values, which we need during writing. Or even introduce recursive references - we cannot check for that either.
3. We can optimize a lot of things knowing that tree structure before-hand. For example, right now to get a list of clipPaths we need to traverse the tree. But with a read-only tree we can collect them before-hand.
4. All the internals are public. Ideally, the caller shouldn't know if we're using shared references for some data or not. That should be an implementation detail and not part of the public API.

@LaurenzV @yisibl I hope this change would not break your use case. Or anyone else.


Discussion / hints:
@laurmaedje IIRC Typst also manually pushes fonts to the SVG tree, no?
Typst starts out with an empty fontdb, traverses the tree and inspects the `TextSpan::font::families` to load fonts through its own mechanisms and to perform font fallback. It then loads only the used once into the fontdb and overwrites the text span's font families with the chosen font. It is somewhat hacky, but the best way we found.
I see, so if the tree were to become read-only, we could probably still traverse it to find all fonts, but we can't define our own fallback fonts anymore for each text span.
@laurmaedje this feature would be removed as well... The problem is that currently parsing and text-to-path conversion are two separate steps which leads to a lot of complexity and issues as well. For one, to do a lot of things with SVG we must know the layer bbox, which we don't know before text layout is finished. A chicken and egg problem.

Will a callback approach work for you? The way we handle images resolving right now. usvg will call a callback for each font not present in fontdb and you could either add it to it or ignore it. I haven't tried implementing it yet, but that's the idea.

In general, the issue here is that usvg is too flexible for me to maintain. Therefore I plan to remove all the features not used by resvg. Ideally, there should be only one way an SVG can be parsed.
> The problem is that currently parsing and text-to-path conversion are two separate steps which leads to a lot of complexity and issues as well. For one, to do a lot of things with SVG we must know the layer bbox, which we don't know before text layout is finished.

So the idea is that text is still preserved in the usvg tree, but you already need to supply a `fontdb` when parsing the SVG file, so that when you parse it you already know for each text node which font will be used, and thus you can calculate the bounding boxes appropriately at the parse stage? That might indeed make things simpler, and might even allow to simplify the structure of the tree (for example, iirc one of the main blockers for removing the "ObjectBoundingBox" property was that we don't know the text bbox while parsing)?
So for `svg2pdf`, the only time I am creating a tree by myself is for filters. But IIRC I only did this because I needed to calculate the bounding boxes, which are currently only exposed via the tree. If it is possible to calculate bounding boxes just from a node (and also render a node directly, which I think is already possible), I wouldn't need anything else, I think.
> Will a callback approach work for you? The way we handle images resolving right now. usvg will call a callback for each font not present in fontdb and you could either add it to it or ignore it. I haven't tried implementing it yet, but that's the idea.

As far as font selection goes, this could work. What's also relevant to note here is how the family names are resolved. Typst and fontdb parse family metadata somewhat differently and currently we parse the SVG family names in the Typst way and then, after loading the font into fontdb, patch the name in the SVG to the fontdb variant. 

I'd like to bring up one other thing, too: A current problem with Typst's SVG stack is that text isn't selectable. In the current usvg design, I think it would be possible to layout the text with Typst's text stack in the common, simple case (no crazy SVG features on the text), which would make it selectable. Forcing text-to-path in the parsing stage would remove this capability as far as I can tell, which would be unfortunate. 

I really liked that usvg offered more control over the text in recent versions. Of course, I can't judge the complexity this forces upon usvg, but I wonder whether it's possible to have some middle ground where consumers can interact with the text beyond just paths.
> Forcing text-to-path in the parsing stage would remove this capability as far as I can tell, which would be unfortunate.

As far as I understood, text will still be preserved, but the used font will be decided on at parsing stage so that the bbox is known in advance? If I'm wrong and text would also be converted automatically into paths again, that would be very unfortunate indeed. :(
These changes may be more impactful, so I also cc @zimond 
@LaurenzV 

>So the idea is that text is still preserved in the usvg tree, but you already need to supply a fontdb when parsing the SVG file

Yes. Ideally, all you have to do is too call `usvg::Tree::parse` and thats it. No `tree.postprocess()` and stuff.

Yes, it will simplify `objectBoundingBox` as well. But we can implement it already, if we want to. 
The bigger problem is that right now we have to parse SVG first and them post-process it via a separate function. Meaning that we have to store some metadata in-between we don't need.
>If it is possible to calculate bounding boxes just from a node (and also render a node directly, which I think is already possible), I wouldn't need anything else, I think.

It's not possible, because we have to know all parent transforms. And you would not be able to create individual Nodes anyway. Just a tree.

That's the problem. To properly parse SVG I need access to the whole tree, not just part of it.
@laurmaedje 

>A current problem with Typst's SVG stack is that text isn't selectable.

There are plans to preserve text layout as well. So a `Text` node would store SVG properties, text layout (a list of glyph + position + style structs) and a flattened (text-to-paths) output.
I'm not sure it would help much. SVG Text layout is pretty complicated. I'm not sure how `usvg` can help you here.

>Forcing text-to-path in the parsing stage would remove this capability as far as I can tell, which would be unfortunate.

The `Text` node isn't going anywhere. So it's fine.
Turns out text cannot be flattened by design and the current text-as-paths SVG writer is technically incorrect.

>after loading the font into fontdb, patch the name in the SVG to the fontdb variant.

This would not be possible anymore. And I'm not sure how can we handle this.
Let's be hones, that's a pretty rare edge case. I don't mind having a callback for such cases, but I'm not really sure what are you doing and why.

>Of course, I can't judge the complexity this forces upon usvg, but I wonder whether it's possible to have some middle ground where consumers can interact with the text beyond just paths.

The complexity comes from the fact that the whole reason `usvg` exists is to serve `resvg`. It's not designed to be used on its own. And by adding more flexibility we're making `resvg` more complicated as well.
For example, right now text-to-paths conversion and bbox calculation are optional. But `resvg` doesn't understand this. The `usvg::Tree` `resvg` expects must have all of those properties already.

The fact that `usvg::Tree` is mutable makes SVG writing pretty hard, if not impossible. Because, as mentioned above, one can modify the `Tree` in a way that it would have recursive references and so on. And right now both `resvg` and SVG writer expect that `Tree` was parsed from a string without any modifications.

To put it bluntly, any modifications to `usvg::Tree` are undefined behaviour. As simple as that. And by having a read-only tree we can avoid it.
> It's not possible, because we have to know all parent transforms. And you would not be able to create individual Nodes anyway. Just a tree.

Hm okay... This might mean that it might break stuff for me then. :/ But it's hard to say, I would have to see the concrete implementation.
Why do you create filters yourself? To rasterize them?

Also, I will sound like a broken record, but you should not be creating filters by yourself as well. `resvg` is able to handle only filters created during parsing. All the validation is done on the parser side, not resvg. Meaning your code could break any time.
> There are plans to preserve text layout as well. So a Text node would store SVG properties, text layout (a list of glyph + position + style structs) and a flattened (text-to-paths) output.
I'm not sure it would help much. SVG Text layout is pretty complicated. I'm not sure how usvg can help you here.

Then, it should be fine I think.

> The complexity comes from the fact that the whole reason usvg exists is to serve resvg. It's not designed to be used on its own.

I think I'm not alone in saying that it _is_ very useful on its own and that's actually a great feature! I just remembered [this message](https://www.reddit.com/r/rust/comments/rb4vvn/comment/hnmh2kp/?utm_source=share&utm_medium=web2x&context=3) from you on reddit and I wholeheartedly agree with it.
@laurmaedje `usvg` isn't going anywhere. It will only get better. But to do so we have to make it read-only.
> Also, I will sound like a broken record, but you should not be creating filters by yourself as well. resvg is able to handle only filters created during parsing. All the validation is done on the parser side, not resvg. Meaning your code could break any time.

I'm not creating filters myself. Basically, in general, I traverse the `usvg` tree and convert all of the nodes into PDF objects successively. However, filters are not supported in PDF, so they need to be rasterized. So whenever I encounter a node that has filters, instead of traversing it, I create a new resvg tree (with an empty group root node) and append that node, and then convert it using resvg into an image. And then I just embed the image into the PDF instead.

And it works really well, there are a few edge cases that don't work yet, but all other test cases pass successfully.
@LaurenzV couldn't you simply render the node itself? `resvg` has an API for that. I don't see why would you need a new tree for that.
~~Yes that's what I tried, but I need to know the bounding boxes of the node so that I can set it inside the PDF file (I don't "care" about the parent transforms in this case, I just need the bounding box of the node as if it were the root node). And when I implemented it, the bounding box calculation was private to usvg and only exposed via `usvg::Tree`. Not sure if that changed.~~

EDIT: Nevermind, looks like the method on the group is public... Maybe I missed something, I will look into it later. 😅 
Okay, I rewrote it using the node render method, and except for two edge cases that break, it works fine otherwise. So I think read-only tree would be fine in my case, as long as you can still create nodes manually and work with those!
>as long as you can still create nodes manually and work with those

This wouldn't be allowed. That's the whole point. Why do you want this?
Okay, nevermind, I guess I don't strictly need to be able to construct a image node by myself. But I can still at least read the attributes of an image node (such as `viewbox`, `visibility`, etc.), right? 

I think for me it's just a bit difficult to visualize what the exact API you had in mind would look like, so I guess it's just best to try it out once it's ready.  So feel free to write here once it happens and I'll try it out (although I presume that this will probably a long time). :p
No worries, I will make a pull request soon.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfee1755d..4ac9cae4c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 This changelog also contains important changes in dependencies.
 
 ## [Unreleased]
+### Changed
+- All types in `usvg` are immutable now. Meaning that `usvg::Tree` cannot be modified
+  after creation anymore.
+- All struct fields in `usvg` are private now. Use getters instead.
 
 ## [0.39.0] - 2024-02-06
 ### Added
diff --git a/crates/c-api/lib.rs b/crates/c-api/lib.rs
index 5ad250918..de6494f56 100644
--- a/crates/c-api/lib.rs
+++ b/crates/c-api/lib.rs
@@ -612,7 +612,7 @@ pub extern "C" fn resvg_is_image_empty(tree: *const resvg_render_tree) -> bool {
         &*tree
     };
 
-    !tree.0.root.has_children()
+    !tree.0.root().has_children()
 }
 
 /// @brief Returns an image size.
@@ -630,7 +630,7 @@ pub extern "C" fn resvg_get_image_size(tree: *const resvg_render_tree) -> resvg_
         &*tree
     };
 
-    let size = tree.0.size;
+    let size = tree.0.size();
 
     resvg_size {
         width: size.width(),
@@ -651,7 +651,7 @@ pub extern "C" fn resvg_get_image_viewbox(tree: *const resvg_render_tree) -> res
         &*tree
     };
 
-    let r = tree.0.view_box.rect;
+    let r = tree.0.view_box().rect;
 
     resvg_rect {
         x: r.x(),
@@ -680,8 +680,8 @@ pub extern "C" fn resvg_get_image_bbox(
 
     if let Some(r) = tree
         .0
-        .root
-        .abs_bounding_box
+        .root()
+        .abs_bounding_box()
         .and_then(|r| r.to_non_zero_rect())
     {
         unsafe {
diff --git a/crates/resvg/examples/custom_href_resolver.rs b/crates/resvg/examples/custom_href_resolver.rs
index 0073291fe..7c6e39605 100644
--- a/crates/resvg/examples/custom_href_resolver.rs
+++ b/crates/resvg/examples/custom_href_resolver.rs
@@ -22,7 +22,7 @@ fn main() {
     let svg_data = std::fs::read("./examples/custom_href_resolver.svg").unwrap();
     let tree = usvg::Tree::from_data(&svg_data, &opt).unwrap();
 
-    let pixmap_size = tree.size.to_int_size();
+    let pixmap_size = tree.size().to_int_size();
     let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
 
     resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
diff --git a/crates/resvg/examples/custom_usvg_tree.rs b/crates/resvg/examples/custom_usvg_tree.rs
deleted file mode 100644
index 432e026c2..000000000
--- a/crates/resvg/examples/custom_usvg_tree.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use std::rc::Rc;
-
-use usvg::fontdb;
-
-fn main() {
-    let size = usvg::Size::from_wh(200.0, 200.0).unwrap();
-    let mut tree = usvg::Tree {
-        size,
-        view_box: usvg::ViewBox {
-            rect: size.to_non_zero_rect(0.0, 0.0),
-            aspect: usvg::AspectRatio::default(),
-        },
-        root: usvg::Group::default(),
-    };
-
-    let gradient = usvg::LinearGradient {
-        x1: 0.0,
-        y1: 0.0,
-        x2: 1.0,
-        y2: 0.0,
-        base: usvg::BaseGradient {
-            id: "lg1".into(),
-            units: usvg::Units::ObjectBoundingBox,
-            transform: usvg::Transform::default(),
-            spread_method: usvg::SpreadMethod::Pad,
-            stops: vec![
-                usvg::Stop {
-                    offset: usvg::StopOffset::ZERO,
-                    color: usvg::Color::new_rgb(0, 255, 0),
-                    opacity: usvg::Opacity::ONE,
-                },
-                usvg::Stop {
-                    offset: usvg::StopOffset::ONE,
-                    color: usvg::Color::new_rgb(0, 255, 0),
-                    opacity: usvg::Opacity::ZERO,
-                },
-            ],
-        },
-    };
-
-    let fill = Some(usvg::Fill {
-        paint: usvg::Paint::LinearGradient(Rc::new(gradient)),
-        ..usvg::Fill::default()
-    });
-
-    let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect(
-        tiny_skia::Rect::from_xywh(20.0, 20.0, 160.0, 160.0).unwrap(),
-    )));
-    path.fill = fill;
-    tree.root.children.push(usvg::Node::Path(Box::new(path)));
-    tree.postprocess(
-        usvg::PostProcessingSteps::default(),
-        &fontdb::Database::new(),
-    );
-
-    let pixmap_size = tree.size.to_int_size();
-    let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
-    resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
-    pixmap.save_png("out.png").unwrap();
-}
diff --git a/crates/resvg/examples/draw_bboxes.rs b/crates/resvg/examples/draw_bboxes.rs
index 2424f06df..4e5bf56a6 100644
--- a/crates/resvg/examples/draw_bboxes.rs
+++ b/crates/resvg/examples/draw_bboxes.rs
@@ -1,5 +1,3 @@
-use std::rc::Rc;
-
 use usvg::fontdb;
 
 fn main() {
@@ -34,39 +32,31 @@ fn main() {
 
     let mut bboxes = Vec::new();
     let mut stroke_bboxes = Vec::new();
-    collect_bboxes(&tree.root, &mut bboxes, &mut stroke_bboxes);
+    collect_bboxes(tree.root(), &mut bboxes, &mut stroke_bboxes);
+
+    let pixmap_size = tree.size().to_int_size().scale_by(zoom).unwrap();
+    let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
+    let render_ts = tiny_skia::Transform::from_scale(zoom, zoom);
+    resvg::render(&tree, render_ts, &mut pixmap.as_mut());
 
-    let stroke1 = Some(usvg::Stroke {
-        paint: usvg::Paint::Color(usvg::Color::new_rgb(255, 0, 0)),
-        opacity: usvg::Opacity::new_clamped(0.5),
-        ..usvg::Stroke::default()
-    });
+    let stroke = tiny_skia::Stroke::default();
 
-    let stroke2 = Some(usvg::Stroke {
-        paint: usvg::Paint::Color(usvg::Color::new_rgb(0, 200, 0)),
-        opacity: usvg::Opacity::new_clamped(0.5),
-        ..usvg::Stroke::default()
-    });
+    let mut paint1 = tiny_skia::Paint::default();
+    paint1.set_color_rgba8(255, 0, 0, 127);
+
+    let mut paint2 = tiny_skia::Paint::default();
+    paint2.set_color_rgba8(0, 200, 0, 127);
 
     for bbox in bboxes {
-        let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect(bbox)));
-        path.stroke = stroke1.clone();
-        tree.root.children.push(usvg::Node::Path(Box::new(path)));
+        let path = tiny_skia::PathBuilder::from_rect(bbox);
+        pixmap.stroke_path(&path, &paint1, &stroke, render_ts, None);
     }
 
     for bbox in stroke_bboxes {
-        let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect(bbox)));
-        path.stroke = stroke2.clone();
-        tree.root.children.push(usvg::Node::Path(Box::new(path)));
+        let path = tiny_skia::PathBuilder::from_rect(bbox);
+        pixmap.stroke_path(&path, &paint2, &stroke, render_ts, None);
     }
 
-    // Calculate bboxes of newly added path.
-    tree.calculate_bounding_boxes();
-
-    let pixmap_size = tree.size.to_int_size().scale_by(zoom).unwrap();
-    let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
-    let render_ts = tiny_skia::Transform::from_scale(zoom, zoom);
-    resvg::render(&tree, render_ts, &mut pixmap.as_mut());
     pixmap.save_png(&args[2]).unwrap();
 }
 
@@ -75,7 +65,7 @@ fn collect_bboxes(
     bboxes: &mut Vec<usvg::Rect>,
     stroke_bboxes: &mut Vec<usvg::Rect>,
 ) {
-    for node in &parent.children {
+    for node in parent.children() {
         if let usvg::Node::Group(ref group) = node {
             collect_bboxes(group, bboxes, stroke_bboxes);
         }
diff --git a/crates/resvg/examples/minimal.rs b/crates/resvg/examples/minimal.rs
index 53e1478dd..ac06a5f70 100644
--- a/crates/resvg/examples/minimal.rs
+++ b/crates/resvg/examples/minimal.rs
@@ -27,7 +27,7 @@ fn main() {
         tree
     };
 
-    let pixmap_size = tree.size.to_int_size();
+    let pixmap_size = tree.size().to_int_size();
     let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
     resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
     pixmap.save_png(&args[2]).unwrap();
diff --git a/crates/resvg/src/clip.rs b/crates/resvg/src/clip.rs
index ed0b4e9eb..89d163982 100644
--- a/crates/resvg/src/clip.rs
+++ b/crates/resvg/src/clip.rs
@@ -10,8 +10,8 @@ pub fn apply(
     transform: tiny_skia::Transform,
     pixmap: &mut tiny_skia::Pixmap,
 ) {
-    let mut clip_transform = clip.transform;
-    if clip.units == usvg::Units::ObjectBoundingBox {
+    let mut clip_transform = clip.transform();
+    if clip.units() == usvg::Units::ObjectBoundingBox {
         let object_bbox = match object_bbox.to_non_zero_rect() {
             Some(v) => v,
             None => {
@@ -28,14 +28,14 @@ pub fn apply(
     clip_pixmap.fill(tiny_skia::Color::BLACK);
 
     draw_children(
-        &clip.root,
+        clip.root(),
         tiny_skia::BlendMode::Clear,
         object_bbox,
         transform.pre_concat(clip_transform),
         &mut clip_pixmap.as_mut(),
     );
 
-    if let Some(ref clip) = clip.clip_path {
+    if let Some(ref clip) = clip.clip_path() {
         apply(&clip.borrow(), object_bbox, transform, pixmap);
     }
 
@@ -51,10 +51,10 @@ fn draw_children(
     transform: tiny_skia::Transform,
     pixmap: &mut tiny_skia::PixmapMut,
 ) {
-    for child in &parent.children {
+    for child in parent.children() {
         match child {
             usvg::Node::Path(ref path) => {
-                if path.visibility != usvg::Visibility::Visible {
+                if path.visibility() != usvg::Visibility::Visible {
                     continue;
                 }
 
@@ -66,14 +66,14 @@ fn draw_children(
                 crate::path::fill_path(path, mode, &ctx, object_bbox, transform, pixmap);
             }
             usvg::Node::Text(ref text) => {
-                if let (Some(flattened), Some(bbox)) = (&text.flattened, text.bounding_box) {
+                if let (Some(flattened), Some(bbox)) = (&text.flattened(), text.bounding_box()) {
                     draw_children(flattened, mode, bbox.to_rect(), transform, pixmap);
                 }
             }
             usvg::Node::Group(ref group) => {
-                let transform = transform.pre_concat(group.transform);
+                let transform = transform.pre_concat(group.transform());
 
-                if let Some(ref clip) = group.clip_path {
+                if let Some(ref clip) = group.clip_path() {
                     // If a `clipPath` child also has a `clip-path`
                     // then we should render this child on a new canvas,
                     // clip it, and only then draw it to the `clipPath`.
diff --git a/crates/resvg/src/filter/component_transfer.rs b/crates/resvg/src/filter/component_transfer.rs
index b10a5335c..b56af6437 100644
--- a/crates/resvg/src/filter/component_transfer.rs
+++ b/crates/resvg/src/filter/component_transfer.rs
@@ -10,20 +10,20 @@ use usvg::filter::{ComponentTransfer, TransferFunction};
 /// Input image pixels should have an **unpremultiplied alpha**.
 pub fn apply(fe: &ComponentTransfer, src: ImageRefMut) {
     for pixel in src.data {
-        if !is_dummy(&fe.func_r) {
-            pixel.r = transfer(&fe.func_r, pixel.r);
+        if !is_dummy(fe.func_r()) {
+            pixel.r = transfer(fe.func_r(), pixel.r);
         }
 
-        if !is_dummy(&fe.func_b) {
-            pixel.b = transfer(&fe.func_b, pixel.b);
+        if !is_dummy(fe.func_b()) {
+            pixel.b = transfer(fe.func_b(), pixel.b);
         }
 
-        if !is_dummy(&fe.func_g) {
-            pixel.g = transfer(&fe.func_g, pixel.g);
+        if !is_dummy(fe.func_g()) {
+            pixel.g = transfer(fe.func_g(), pixel.g);
         }
 
-        if !is_dummy(&fe.func_a) {
-            pixel.a = transfer(&fe.func_a, pixel.a);
+        if !is_dummy(fe.func_a()) {
+            pixel.a = transfer(fe.func_a(), pixel.a);
         }
     }
 }
diff --git a/crates/resvg/src/filter/convolve_matrix.rs b/crates/resvg/src/filter/convolve_matrix.rs
index 49695ecfc..fca9e9c5f 100644
--- a/crates/resvg/src/filter/convolve_matrix.rs
+++ b/crates/resvg/src/filter/convolve_matrix.rs
@@ -30,12 +30,12 @@ pub fn apply(matrix: &ConvolveMatrix, src: ImageRefMut) {
         let mut new_g = 0.0;
         let mut new_b = 0.0;
         let mut new_a = 0.0;
-        for oy in 0..matrix.matrix.rows {
-            for ox in 0..matrix.matrix.columns {
-                let mut tx = x as i32 - matrix.matrix.target_x as i32 + ox as i32;
-                let mut ty = y as i32 - matrix.matrix.target_y as i32 + oy as i32;
+        for oy in 0..matrix.matrix().rows() {
+            for ox in 0..matrix.matrix().columns() {
+                let mut tx = x as i32 - matrix.matrix().target_x() as i32 + ox as i32;
+                let mut ty = y as i32 - matrix.matrix().target_y() as i32 + oy as i32;
 
-                match matrix.edge_mode {
+                match matrix.edge_mode() {
                     EdgeMode::None => {
                         if tx < 0 || tx > width_max || ty < 0 || ty > height_max {
                             continue;
@@ -58,33 +58,34 @@ pub fn apply(matrix: &ConvolveMatrix, src: ImageRefMut) {
                     }
                 }
 
-                let k = matrix
-                    .matrix
-                    .get(matrix.matrix.columns - ox - 1, matrix.matrix.rows - oy - 1);
+                let k = matrix.matrix().get(
+                    matrix.matrix().columns() - ox - 1,
+                    matrix.matrix().rows() - oy - 1,
+                );
 
                 let p = src.pixel_at(tx as u32, ty as u32);
                 new_r += (p.r as f32) / 255.0 * k;
                 new_g += (p.g as f32) / 255.0 * k;
                 new_b += (p.b as f32) / 255.0 * k;
 
-                if !matrix.preserve_alpha {
+                if !matrix.preserve_alpha() {
                     new_a += (p.a as f32) / 255.0 * k;
                 }
             }
         }
 
-        if matrix.preserve_alpha {
+        if matrix.preserve_alpha() {
             new_a = in_p.a as f32 / 255.0;
         } else {
-            new_a = new_a / matrix.divisor.get() + matrix.bias;
+            new_a = new_a / matrix.divisor().get() + matrix.bias();
         }
 
         let bounded_new_a = f32_bound(0.0, new_a, 1.0);
 
         let calc = |x| {
-            let x = x / matrix.divisor.get() + matrix.bias * new_a;
+            let x = x / matrix.divisor().get() + matrix.bias() * new_a;
 
-            let x = if matrix.preserve_alpha {
+            let x = if matrix.preserve_alpha() {
                 f32_bound(0.0, x, 1.0) * bounded_new_a
             } else {
                 f32_bound(0.0, x, bounded_new_a)
diff --git a/crates/resvg/src/filter/displacement_map.rs b/crates/resvg/src/filter/displacement_map.rs
index 64dce3195..545a54418 100644
--- a/crates/resvg/src/filter/displacement_map.rs
+++ b/crates/resvg/src/filter/displacement_map.rs
@@ -43,10 +43,10 @@ pub fn apply(
             c as f32 / 255.0 - 0.5
         };
 
-        let dx = calc_offset(fe.x_channel_selector);
-        let dy = calc_offset(fe.y_channel_selector);
-        let ox = (x as f32 + dx * sx * fe.scale).round() as i32;
-        let oy = (y as f32 + dy * sy * fe.scale).round() as i32;
+        let dx = calc_offset(fe.x_channel_selector());
+        let dy = calc_offset(fe.y_channel_selector());
+        let ox = (x as f32 + dx * sx * fe.scale()).round() as i32;
+        let oy = (y as f32 + dy * sy * fe.scale()).round() as i32;
 
         // TODO: we should use some kind of anti-aliasing when offset is on a pixel border
 
diff --git a/crates/resvg/src/filter/lighting.rs b/crates/resvg/src/filter/lighting.rs
index a55192ba3..d6be6b71a 100644
--- a/crates/resvg/src/filter/lighting.rs
+++ b/crates/resvg/src/filter/lighting.rs
@@ -144,7 +144,7 @@ pub fn diffuse_lighting(
         let k = if normal.normal.approx_zero() {
             light_vector.z
         } else {
-            let mut n = normal.normal * (fe.surface_scale / 255.0);
+            let mut n = normal.normal * (fe.surface_scale() / 255.0);
             n.x *= normal.factor.x;
             n.y *= normal.factor.y;
 
@@ -153,13 +153,13 @@ pub fn diffuse_lighting(
             normal.dot(&light_vector) / normal.length()
         };
 
-        fe.diffuse_constant * k
+        fe.diffuse_constant() * k
     };
 
     apply(
         light_source,
-        fe.surface_scale,
-        fe.lighting_color,
+        fe.surface_scale(),
+        fe.lighting_color(),
         &light_factor,
         calc_diffuse_alpha,
         src,
@@ -195,33 +195,33 @@ pub fn specular_lighting(
 
         let k = if normal.normal.approx_zero() {
             let n_dot_h = h.z / h_length;
-            if fe.specular_exponent.approx_eq_ulps(&1.0, 4) {
+            if fe.specular_exponent().approx_eq_ulps(&1.0, 4) {
                 n_dot_h
             } else {
-                n_dot_h.powf(fe.specular_exponent)
+                n_dot_h.powf(fe.specular_exponent())
             }
         } else {
-            let mut n = normal.normal * (fe.surface_scale / 255.0);
+            let mut n = normal.normal * (fe.surface_scale() / 255.0);
             n.x *= normal.factor.x;
             n.y *= normal.factor.y;
 
             let normal = Vector3::new(n.x, n.y, 1.0);
 
             let n_dot_h = normal.dot(&h) / normal.length() / h_length;
-            if fe.specular_exponent.approx_eq_ulps(&1.0, 4) {
+            if fe.specular_exponent().approx_eq_ulps(&1.0, 4) {
                 n_dot_h
             } else {
-                n_dot_h.powf(fe.specular_exponent)
+                n_dot_h.powf(fe.specular_exponent())
             }
         };
 
-        fe.specular_constant * k
+        fe.specular_constant() * k
     };
 
     apply(
         light_source,
-        fe.surface_scale,
-        fe.lighting_color,
+        fe.surface_scale(),
+        fe.lighting_color(),
         &light_factor,
         calc_specular_alpha,
         src,
diff --git a/crates/resvg/src/filter/mod.rs b/crates/resvg/src/filter/mod.rs
index ebeef5058..7c3eb73ae 100644
--- a/crates/resvg/src/filter/mod.rs
+++ b/crates/resvg/src/filter/mod.rs
@@ -362,7 +362,7 @@ fn apply_inner(
     ts: usvg::Transform,
     source: &mut tiny_skia::Pixmap,
 ) -> Result<Image, Error> {
-    let bbox = group.bounding_box;
+    let bbox = group.bounding_box();
     let filter_region = group.filters_bounding_box().ok_or(Error::InvalidRegion)?;
     let region = filter_region
         .transform(ts)
@@ -371,7 +371,7 @@ fn apply_inner(
 
     let mut results: Vec<FilterResult> = Vec::new();
 
-    for primitive in &filter.primitives {
+    for primitive in filter.primitives() {
         let subregion = match calc_subregion(filter, primitive, bbox, filter_region) {
             Some(v) => v,
             None => {
@@ -386,69 +386,69 @@ fn apply_inner(
             .ok_or(Error::InvalidRegion)?;
 
         // `feOffset` inherits its region from the input.
-        if let usvg::filter::Kind::Offset(ref fe) = primitive.kind {
-            if let usvg::filter::Input::Reference(ref name) = fe.input {
+        if let usvg::filter::Kind::Offset(ref fe) = primitive.kind() {
+            if let usvg::filter::Input::Reference(ref name) = fe.input() {
                 if let Some(res) = results.iter().rev().find(|v| v.name == *name) {
                     subregion = res.image.region;
                 }
             }
         }
 
-        let cs = primitive.color_interpolation;
+        let cs = primitive.color_interpolation();
 
-        let mut result = match primitive.kind {
+        let mut result = match primitive.kind() {
             usvg::filter::Kind::Blend(ref fe) => {
-                let input1 = get_input(&fe.input1, region, source, &results)?;
-                let input2 = get_input(&fe.input2, region, source, &results)?;
+                let input1 = get_input(fe.input1(), region, source, &results)?;
+                let input2 = get_input(fe.input2(), region, source, &results)?;
                 apply_blend(fe, cs, region, input1, input2)
             }
             usvg::filter::Kind::DropShadow(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
-                apply_drop_shadow(fe, filter.primitive_units, cs, bbox, ts, input)
+                let input = get_input(fe.input(), region, source, &results)?;
+                apply_drop_shadow(fe, filter.primitive_units(), cs, bbox, ts, input)
             }
             usvg::filter::Kind::Flood(ref fe) => apply_flood(fe, region),
             usvg::filter::Kind::GaussianBlur(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
-                apply_blur(fe, filter.primitive_units, cs, bbox, ts, input)
+                let input = get_input(fe.input(), region, source, &results)?;
+                apply_blur(fe, filter.primitive_units(), cs, bbox, ts, input)
             }
             usvg::filter::Kind::Offset(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
-                apply_offset(fe, filter.primitive_units, bbox, ts, input)
+                let input = get_input(fe.input(), region, source, &results)?;
+                apply_offset(fe, filter.primitive_units(), bbox, ts, input)
             }
             usvg::filter::Kind::Composite(ref fe) => {
-                let input1 = get_input(&fe.input1, region, source, &results)?;
-                let input2 = get_input(&fe.input2, region, source, &results)?;
+                let input1 = get_input(fe.input1(), region, source, &results)?;
+                let input2 = get_input(fe.input2(), region, source, &results)?;
                 apply_composite(fe, cs, region, input1, input2)
             }
             usvg::filter::Kind::Merge(ref fe) => apply_merge(fe, cs, region, source, &results),
             usvg::filter::Kind::Tile(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
+                let input = get_input(fe.input(), region, source, &results)?;
                 apply_tile(input, region)
             }
             usvg::filter::Kind::Image(ref fe) => apply_image(fe, region, subregion, ts),
             usvg::filter::Kind::ComponentTransfer(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
+                let input = get_input(fe.input(), region, source, &results)?;
                 apply_component_transfer(fe, cs, input)
             }
             usvg::filter::Kind::ColorMatrix(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
+                let input = get_input(fe.input(), region, source, &results)?;
                 apply_color_matrix(fe, cs, input)
             }
             usvg::filter::Kind::ConvolveMatrix(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
+                let input = get_input(fe.input(), region, source, &results)?;
                 apply_convolve_matrix(fe, cs, input)
             }
             usvg::filter::Kind::Morphology(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
-                apply_morphology(fe, filter.primitive_units, cs, bbox, ts, input)
+                let input = get_input(fe.input(), region, source, &results)?;
+                apply_morphology(fe, filter.primitive_units(), cs, bbox, ts, input)
             }
             usvg::filter::Kind::DisplacementMap(ref fe) => {
-                let input1 = get_input(&fe.input1, region, source, &results)?;
-                let input2 = get_input(&fe.input2, region, source, &results)?;
+                let input1 = get_input(fe.input1(), region, source, &results)?;
+                let input2 = get_input(fe.input2(), region, source, &results)?;
                 apply_displacement_map(
                     fe,
                     region,
-                    filter.primitive_units,
+                    filter.primitive_units(),
                     cs,
                     bbox,
                     ts,
@@ -458,11 +458,11 @@ fn apply_inner(
             }
             usvg::filter::Kind::Turbulence(ref fe) => apply_turbulence(fe, region, cs, ts),
             usvg::filter::Kind::DiffuseLighting(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
+                let input = get_input(fe.input(), region, source, &results)?;
                 apply_diffuse_lighting(fe, region, cs, ts, input)
             }
             usvg::filter::Kind::SpecularLighting(ref fe) => {
-                let input = get_input(&fe.input, region, source, &results)?;
+                let input = get_input(fe.input(), region, source, &results)?;
                 apply_specular_lighting(fe, region, cs, ts, input)
             }
         }?;
@@ -471,7 +471,7 @@ fn apply_inner(
             // Clip result.
 
             // TODO: explain
-            let subregion2 = if let usvg::filter::Kind::Offset(..) = primitive.kind {
+            let subregion2 = if let usvg::filter::Kind::Offset(..) = primitive.kind() {
                 // We do not support clipping on feOffset.
                 region.translate_to(0, 0)
             } else {
@@ -521,7 +521,7 @@ fn apply_inner(
         }
 
         results.push(FilterResult {
-            name: primitive.result.clone(),
+            name: primitive.result().to_string(),
             image: result,
         });
     }
@@ -541,20 +541,20 @@ fn calc_subregion(
 ) -> Option<tiny_skia::NonZeroRect> {
     // TODO: rewrite/simplify/explain/whatever
 
-    let region = match primitive.kind {
+    let region = match primitive.kind() {
         usvg::filter::Kind::Flood(..) | usvg::filter::Kind::Image(..) => {
             // `feImage` uses the object bbox.
-            if filter.primitive_units == usvg::Units::ObjectBoundingBox {
+            if filter.primitive_units() == usvg::Units::ObjectBoundingBox {
                 let bbox = bbox?.to_non_zero_rect()?;
 
                 // TODO: wrong
                 // let ts_bbox = tiny_skia::Rect::new(ts.e, ts.f, ts.a, ts.d).unwrap();
 
                 let r = tiny_skia::NonZeroRect::from_xywh(
-                    primitive.x.unwrap_or(0.0),
-                    primitive.y.unwrap_or(0.0),
-                    primitive.width.unwrap_or(1.0),
-                    primitive.height.unwrap_or(1.0),
+                    primitive.x().unwrap_or(0.0),
+                    primitive.y().unwrap_or(0.0),
+                    primitive.width().unwrap_or(1.0),
+                    primitive.height().unwrap_or(1.0),
                 )?;
 
                 let r = r.bbox_transform(bbox);
@@ -569,21 +569,21 @@ fn calc_subregion(
     };
 
     // TODO: Wrong! Does not account rotate and skew.
-    let subregion = if filter.primitive_units == usvg::Units::ObjectBoundingBox {
+    let subregion = if filter.primitive_units() == usvg::Units::ObjectBoundingBox {
         let subregion_bbox = tiny_skia::NonZeroRect::from_xywh(
-            primitive.x.unwrap_or(0.0),
-            primitive.y.unwrap_or(0.0),
-            primitive.width.unwrap_or(1.0),
-            primitive.height.unwrap_or(1.0),
+            primitive.x().unwrap_or(0.0),
+            primitive.y().unwrap_or(0.0),
+            primitive.width().unwrap_or(1.0),
+            primitive.height().unwrap_or(1.0),
         )?;
 
         region.bbox_transform(subregion_bbox)
     } else {
         tiny_skia::NonZeroRect::from_xywh(
-            primitive.x.unwrap_or(region.x()),
-            primitive.y.unwrap_or(region.y()),
-            primitive.width.unwrap_or(region.width()),
-            primitive.height.unwrap_or(region.height()),
+            primitive.x().unwrap_or(region.x()),
+            primitive.y().unwrap_or(region.y()),
+            primitive.width().unwrap_or(region.width()),
+            primitive.height().unwrap_or(region.height()),
         )?
     };
 
@@ -656,7 +656,7 @@ fn apply_drop_shadow(
     ts: usvg::Transform,
     input: Image,
 ) -> Result<Image, Error> {
-    let (dx, dy) = match scale_coordinates(fe.dx, fe.dy, units, bbox, ts) {
+    let (dx, dy) = match scale_coordinates(fe.dx(), fe.dy(), units, bbox, ts) {
         Some(v) => v,
         None => return Ok(input),
     };
@@ -666,7 +666,7 @@ fn apply_drop_shadow(
     let mut shadow_pixmap = input_pixmap.clone();
 
     if let Some((std_dx, std_dy, use_box_blur)) =
-        resolve_std_dev(fe.std_dev_x.get(), fe.std_dev_y.get(), units, bbox, ts)
+        resolve_std_dev(fe.std_dev_x().get(), fe.std_dev_y().get(), units, bbox, ts)
     {
         if use_box_blur {
             box_blur::apply(std_dx, std_dy, shadow_pixmap.as_image_ref_mut());
@@ -677,10 +677,10 @@ fn apply_drop_shadow(
 
     // flood
     let color = tiny_skia::Color::from_rgba8(
-        fe.color.red,
-        fe.color.green,
-        fe.color.blue,
-        fe.opacity.to_u8(),
+        fe.color().red,
+        fe.color().green,
+        fe.color().blue,
+        fe.opacity().to_u8(),
     );
     for p in shadow_pixmap.pixels_mut() {
         let mut color = color;
@@ -723,7 +723,7 @@ fn apply_blur(
     input: Image,
 ) -> Result<Image, Error> {
     let (std_dx, std_dy, use_box_blur) =
-        match resolve_std_dev(fe.std_dev_x.get(), fe.std_dev_y.get(), units, bbox, ts) {
+        match resolve_std_dev(fe.std_dev_x().get(), fe.std_dev_y().get(), units, bbox, ts) {
             Some(v) => v,
             None => return Ok(input),
         };
@@ -746,7 +746,7 @@ fn apply_offset(
     ts: usvg::Transform,
     input: Image,
 ) -> Result<Image, Error> {
-    let (dx, dy) = match scale_coordinates(fe.dx, fe.dy, units, bbox, ts) {
+    let (dx, dy) = match scale_coordinates(fe.dx(), fe.dy(), units, bbox, ts) {
         Some(v) => v,
         None => return Ok(input),
     };
@@ -794,7 +794,7 @@ fn apply_blend(
         0,
         input1.as_ref().as_ref(),
         &tiny_skia::PixmapPaint {
-            blend_mode: crate::render::convert_blend_mode(fe.mode),
+            blend_mode: crate::render::convert_blend_mode(fe.mode()),
             ..tiny_skia::PixmapPaint::default()
         },
         tiny_skia::Transform::identity(),
@@ -818,7 +818,7 @@ fn apply_composite(
 
     let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;
 
-    if let Operator::Arithmetic { k1, k2, k3, k4 } = fe.operator {
+    if let Operator::Arithmetic { k1, k2, k3, k4 } = fe.operator() {
         let pixmap1 = input1.take()?;
         let pixmap2 = input2.take()?;
 
@@ -844,7 +844,7 @@ fn apply_composite(
         None,
     );
 
-    let blend_mode = match fe.operator {
+    let blend_mode = match fe.operator() {
         Operator::Over => tiny_skia::BlendMode::SourceOver,
         Operator::In => tiny_skia::BlendMode::SourceIn,
         Operator::Out => tiny_skia::BlendMode::SourceOut,
@@ -877,7 +877,7 @@ fn apply_merge(
 ) -> Result<Image, Error> {
     let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;
 
-    for input in &fe.inputs {
+    for input in fe.inputs() {
         let input = get_input(input, region, source, results)?;
         let input = input.into_color_space(cs)?;
         pixmap.draw_pixmap(
@@ -894,14 +894,14 @@ fn apply_merge(
 }
 
 fn apply_flood(fe: &usvg::filter::Flood, region: IntRect) -> Result<Image, Error> {
-    let c = fe.color;
+    let c = fe.color();
 
     let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;
     pixmap.fill(tiny_skia::Color::from_rgba8(
         c.red,
         c.green,
         c.blue,
-        fe.opacity.to_u8(),
+        fe.opacity().to_u8(),
     ));
 
     Ok(Image::from_image(
@@ -942,7 +942,7 @@ fn apply_image(
 ) -> Result<Image, Error> {
     let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;
 
-    match fe.data {
+    match fe.data() {
         usvg::filter::ImageKind::Image(ref kind) => {
             let dx = (subregion.x() - region.x()) as f32;
             let dy = (subregion.y() - region.y()) as f32;
@@ -955,20 +955,16 @@ fn apply_image(
                     .to_rect()
                     .to_non_zero_rect()
                     .unwrap(),
-                aspect: fe.aspect,
+                aspect: fe.aspect(),
             };
 
-            let image = usvg::Image {
-                id: String::new(),
-                visibility: usvg::Visibility::Visible,
+            crate::image::render_inner(
+                kind,
                 view_box,
-                rendering_mode: fe.rendering_mode,
-                kind: kind.clone(),
-                abs_transform: usvg::Transform::default(),
-                bounding_box: Some(view_box.rect),
-            };
-
-            crate::image::render(&image, transform, &mut pixmap.as_mut());
+                transform,
+                fe.rendering_mode(),
+                &mut pixmap.as_mut(),
+            );
         }
         usvg::filter::ImageKind::Use(ref node) => {
             let (sx, sy) = ts.get_scale();
@@ -1019,7 +1015,7 @@ fn apply_color_matrix(
     let mut pixmap = input.into_color_space(cs)?.take()?;
 
     demultiply_alpha(pixmap.data_mut().as_rgba_mut());
-    color_matrix::apply(&fe.kind, pixmap.as_image_ref_mut());
+    color_matrix::apply(fe.kind(), pixmap.as_image_ref_mut());
     multiply_alpha(pixmap.data_mut().as_rgba_mut());
 
     Ok(Image::from_image(pixmap, cs))
@@ -1032,7 +1028,7 @@ fn apply_convolve_matrix(
 ) -> Result<Image, Error> {
     let mut pixmap = input.into_color_space(cs)?.take()?;
 
-    if fe.preserve_alpha {
+    if fe.preserve_alpha() {
         demultiply_alpha(pixmap.data_mut().as_rgba_mut());
     }
 
@@ -1051,17 +1047,18 @@ fn apply_morphology(
 ) -> Result<Image, Error> {
     let mut pixmap = input.into_color_space(cs)?.take()?;
 
-    let (rx, ry) = match scale_coordinates(fe.radius_x.get(), fe.radius_y.get(), units, bbox, ts) {
-        Some(v) => v,
-        None => return Ok(Image::from_image(pixmap, cs)),
-    };
+    let (rx, ry) =
+        match scale_coordinates(fe.radius_x().get(), fe.radius_y().get(), units, bbox, ts) {
+            Some(v) => v,
+            None => return Ok(Image::from_image(pixmap, cs)),
+        };
 
     if !(rx > 0.0 && ry > 0.0) {
         pixmap.clear();
         return Ok(Image::from_image(pixmap, cs));
     }
 
-    morphology::apply(fe.operator, rx, ry, pixmap.as_image_ref_mut());
+    morphology::apply(fe.operator(), rx, ry, pixmap.as_image_ref_mut());
 
     Ok(Image::from_image(pixmap, cs))
 }
@@ -1081,7 +1078,7 @@ fn apply_displacement_map(
 
     let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;
 
-    let (sx, sy) = match scale_coordinates(fe.scale, fe.scale, units, bbox, ts) {
+    let (sx, sy) = match scale_coordinates(fe.scale(), fe.scale(), units, bbox, ts) {
         Some(v) => v,
         None => return Ok(Image::from_image(pixmap1, cs)),
     };
@@ -1116,12 +1113,12 @@ fn apply_turbulence(
         region.y() as f64 - ts.ty as f64,
         sx as f64,
         sy as f64,
-        fe.base_frequency_x.get() as f64,
-        fe.base_frequency_y.get() as f64,
-        fe.num_octaves,
-        fe.seed,
-        fe.stitch_tiles,
-        fe.kind == usvg::filter::TurbulenceKind::FractalNoise,
+        fe.base_frequency_x().get() as f64,
+        fe.base_frequency_y().get() as f64,
+        fe.num_octaves(),
+        fe.seed(),
+        fe.stitch_tiles(),
+        fe.kind() == usvg::filter::TurbulenceKind::FractalNoise,
         pixmap.as_image_ref_mut(),
     );
 
@@ -1139,7 +1136,7 @@ fn apply_diffuse_lighting(
 ) -> Result<Image, Error> {
     let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;
 
-    let light_source = transform_light_source(fe.light_source, region, ts);
+    let light_source = transform_light_source(fe.light_source(), region, ts);
 
     lighting::diffuse_lighting(
         fe,
@@ -1160,7 +1157,7 @@ fn apply_specular_lighting(
 ) -> Result<Image, Error> {
     let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;
 
-    let light_source = transform_light_source(fe.light_source, region, ts);
+    let light_source = transform_light_source(fe.light_source(), region, ts);
 
     lighting::specular_lighting(
         fe,
@@ -1172,6 +1169,7 @@ fn apply_specular_lighting(
     Ok(Image::from_image(pixmap, cs))
 }
 
+// TODO: do not modify LightSource
 fn transform_light_source(
     mut source: usvg::filter::LightSource,
     region: IntRect,
diff --git a/crates/resvg/src/image.rs b/crates/resvg/src/image.rs
index af5422a6d..78b555140 100644
--- a/crates/resvg/src/image.rs
+++ b/crates/resvg/src/image.rs
@@ -9,17 +9,33 @@ pub fn render(
     transform: tiny_skia::Transform,
     pixmap: &mut tiny_skia::PixmapMut,
 ) {
-    if image.visibility != usvg::Visibility::Visible {
+    if image.visibility() != usvg::Visibility::Visible {
         return;
     }
 
-    match image.kind {
+    render_inner(
+        image.kind(),
+        image.view_box(),
+        transform,
+        image.rendering_mode(),
+        pixmap,
+    );
+}
+
+pub fn render_inner(
+    image_kind: &usvg::ImageKind,
+    view_box: usvg::ViewBox,
+    transform: tiny_skia::Transform,
+    rendering_mode: usvg::ImageRendering,
+    pixmap: &mut tiny_skia::PixmapMut,
+) {
+    match image_kind {
         usvg::ImageKind::SVG(ref tree) => {
-            render_vector(image, tree, transform, pixmap);
+            render_vector(tree, &view_box, transform, pixmap);
         }
         #[cfg(feature = "raster-images")]
         _ => {
-            raster_images::render_raster(image, transform, pixmap);
+            raster_images::render_raster(image_kind, view_box, transform, rendering_mode, pixmap);
         }
         #[cfg(not(feature = "raster-images"))]
         _ => {
@@ -29,13 +45,13 @@ pub fn render(
 }
 
 fn render_vector(
-    image: &usvg::Image,
     tree: &usvg::Tree,
+    view_box: &usvg::ViewBox,
     transform: tiny_skia::Transform,
     pixmap: &mut tiny_skia::PixmapMut,
 ) -> Option<()> {
-    let img_size = tree.size.to_int_size();
-    let (ts, clip) = crate::geom::view_box_to_transform_with_clip(&image.view_box, img_size);
+    let img_size = tree.size().to_int_size();
+    let (ts, clip) = crate::geom::view_box_to_transform_with_clip(&view_box, img_size);
 
     let mut sub_pixmap = tiny_skia::Pixmap::new(pixmap.width(), pixmap.height()).unwrap();
 
@@ -67,8 +83,8 @@ mod raster_images {
     use crate::render::TinySkiaPixmapMutExt;
     use crate::OptionLog;
 
-    fn decode_raster(image: &usvg::Image) -> Option<tiny_skia::Pixmap> {
-        match image.kind {
+    fn decode_raster(image: &usvg::ImageKind) -> Option<tiny_skia::Pixmap> {
+        match image {
             usvg::ImageKind::SVG(_) => None,
             usvg::ImageKind::JPEG(ref data) => {
                 decode_jpeg(data).log_none(|| log::warn!("Failed to decode a JPEG image."))
@@ -163,14 +179,16 @@ mod raster_images {
     }
 
     pub(crate) fn render_raster(
-        image: &usvg::Image,
+        image: &usvg::ImageKind,
+        view_box: usvg::ViewBox,
         transform: tiny_skia::Transform,
+        rendering_mode: usvg::ImageRendering,
         pixmap: &mut tiny_skia::PixmapMut,
     ) -> Option<()> {
         let raster = decode_raster(image)?;
 
         let img_size = tiny_skia::IntSize::from_wh(raster.width(), raster.height())?;
-        let rect = image_rect(&image.view_box, img_size);
+        let rect = image_rect(&view_box, img_size);
 
         let ts = tiny_skia::Transform::from_row(
             rect.width() / raster.width() as f32,
@@ -182,7 +200,7 @@ mod raster_images {
         );
 
         let mut quality = tiny_skia::FilterQuality::Bicubic;
-        if image.rendering_mode == usvg::ImageRendering::OptimizeSpeed {
+        if rendering_mode == usvg::ImageRendering::OptimizeSpeed {
             quality = tiny_skia::FilterQuality::Nearest;
         }
 
@@ -196,8 +214,8 @@ mod raster_images {
         let mut paint = tiny_skia::Paint::default();
         paint.shader = pattern;
 
-        let mask = if image.view_box.aspect.slice {
-            pixmap.create_rect_mask(transform, image.view_box.rect.to_rect())
+        let mask = if view_box.aspect.slice {
+            pixmap.create_rect_mask(transform, view_box.rect.to_rect())
         } else {
             None
         };
diff --git a/crates/resvg/src/lib.rs b/crates/resvg/src/lib.rs
index f513ee86a..8363d91d8 100644
--- a/crates/resvg/src/lib.rs
+++ b/crates/resvg/src/lib.rs
@@ -49,13 +49,16 @@ pub fn render(
     )
     .unwrap();
 
-    let ts =
-        usvg::utils::view_box_to_transform(tree.view_box.rect, tree.view_box.aspect, tree.size);
+    let ts = usvg::utils::view_box_to_transform(
+        tree.view_box().rect,
+        tree.view_box().aspect,
+        tree.size(),
+    );
 
     let root_transform = transform.pre_concat(ts);
 
     let ctx = render::Context { max_bbox };
-    render::render_nodes(&tree.root, &ctx, root_transform, None, pixmap);
+    render::render_nodes(tree.root(), &ctx, root_transform, None, pixmap);
 }
 
 /// Renders a node onto the pixmap.
diff --git a/crates/resvg/src/main.rs b/crates/resvg/src/main.rs
index e3d42809b..25407f2bb 100644
--- a/crates/resvg/src/main.rs
+++ b/crates/resvg/src/main.rs
@@ -600,7 +600,7 @@ fn load_fonts(args: &mut Args, fontdb: &mut fontdb::Database) {
 }
 
 fn query_all(tree: &usvg::Tree) -> Result<(), String> {
-    let count = query_all_impl(&tree.root);
+    let count = query_all_impl(tree.root());
 
     if count == 0 {
         return Err("the file has no valid ID's".to_string());
@@ -611,7 +611,7 @@ fn query_all(tree: &usvg::Tree) -> Result<(), String> {
 
 fn query_all_impl(parent: &usvg::Group) -> usize {
     let mut count = 0;
-    for node in &parent.children {
+    for node in parent.children() {
         if node.id().is_empty() {
             if let usvg::Node::Group(ref group) = node {
                 count += query_all_impl(group);
@@ -672,7 +672,7 @@ fn render_svg(args: &Args, tree: &usvg::Tree) -> Result<tiny_skia::Pixmap, Strin
             }
         }
 
-        let ts = args.fit_to.fit_to_transform(tree.size.to_int_size());
+        let ts = args.fit_to.fit_to_transform(tree.size().to_int_size());
 
         resvg::render_node(node, ts, &mut pixmap.as_mut());
 
@@ -681,7 +681,7 @@ fn render_svg(args: &Args, tree: &usvg::Tree) -> Result<tiny_skia::Pixmap, Strin
 
             let size = args
                 .fit_to
-                .fit_to_size(tree.size.to_int_size())
+                .fit_to_size(tree.size().to_int_size())
                 .ok_or_else(|| "target size is zero".to_string())?;
 
             // Unwrap is safe, because `size` is already valid.
@@ -706,7 +706,7 @@ fn render_svg(args: &Args, tree: &usvg::Tree) -> Result<tiny_skia::Pixmap, Strin
     } else {
         let size = args
             .fit_to
-            .fit_to_size(tree.size.to_int_size())
+            .fit_to_size(tree.size().to_int_size())
             .ok_or_else(|| "target size is zero".to_string())?;
 
         // Unwrap is safe, because `size` is already valid.
@@ -716,7 +716,7 @@ fn render_svg(args: &Args, tree: &usvg::Tree) -> Result<tiny_skia::Pixmap, Strin
             pixmap.fill(svg_to_skia_color(background));
         }
 
-        let ts = args.fit_to.fit_to_transform(tree.size.to_int_size());
+        let ts = args.fit_to.fit_to_transform(tree.size().to_int_size());
 
         resvg::render(tree, ts, &mut pixmap.as_mut());
 
@@ -740,7 +740,7 @@ fn trim_pixmap(
     transform: tiny_skia::Transform,
     pixmap: &tiny_skia::Pixmap,
 ) -> Option<tiny_skia::Pixmap> {
-    let content_area = tree.root.layer_bounding_box?;
+    let content_area = tree.root().layer_bounding_box()?;
 
     let limit = tiny_skia::IntRect::from_xywh(0, 0, pixmap.width(), pixmap.height()).unwrap();
 
diff --git a/crates/resvg/src/mask.rs b/crates/resvg/src/mask.rs
index c45067b27..fad120200 100644
--- a/crates/resvg/src/mask.rs
+++ b/crates/resvg/src/mask.rs
@@ -12,7 +12,7 @@ pub fn apply(
     pixmap: &mut tiny_skia::Pixmap,
 ) {
     let mut content_transform = tiny_skia::Transform::default();
-    if mask.content_units == usvg::Units::ObjectBoundingBox {
+    if mask.content_units() == usvg::Units::ObjectBoundingBox {
         let object_bbox = match object_bbox.to_non_zero_rect() {
             Some(v) => v,
             None => {
@@ -25,22 +25,22 @@ pub fn apply(
         content_transform = ts;
     }
 
-    if mask.units == usvg::Units::ObjectBoundingBox && object_bbox.to_non_zero_rect().is_none() {
+    if mask.units() == usvg::Units::ObjectBoundingBox && object_bbox.to_non_zero_rect().is_none() {
         // `objectBoundingBox` units and zero-sized bbox? Clear the canvas and return.
         // Technically a UB, but this is what Chrome and Firefox do.
         pixmap.fill(tiny_skia::Color::TRANSPARENT);
         return;
     }
 
-    let region = if mask.units == usvg::Units::ObjectBoundingBox {
+    let region = if mask.units() == usvg::Units::ObjectBoundingBox {
         if let Some(bbox) = object_bbox.to_non_zero_rect() {
-            mask.rect.bbox_transform(bbox)
+            mask.rect().bbox_transform(bbox)
         } else {
             // The actual values does not matter. Will not be used anyway.
             tiny_skia::NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()
         }
     } else {
-        mask.rect
+        mask.rect()
     };
 
     let mut mask_pixmap = tiny_skia::Pixmap::new(pixmap.width(), pixmap.height()).unwrap();
@@ -58,7 +58,7 @@ pub fn apply(
 
         let content_transform = transform.pre_concat(content_transform);
         crate::render::render_nodes(
-            &mask.root,
+            mask.root(),
             ctx,
             content_transform,
             None,
@@ -68,11 +68,11 @@ pub fn apply(
         mask_pixmap.apply_mask(&alpha_mask);
     }
 
-    if let Some(ref mask) = mask.mask {
+    if let Some(ref mask) = mask.mask() {
         self::apply(&mask.borrow(), ctx, object_bbox, transform, pixmap);
     }
 
-    let mask_type = match mask.kind {
+    let mask_type = match mask.kind() {
         usvg::MaskType::Luminance => tiny_skia::MaskType::Luminance,
         usvg::MaskType::Alpha => tiny_skia::MaskType::Alpha,
     };
diff --git a/crates/resvg/src/path.rs b/crates/resvg/src/path.rs
index e5a1197a6..e77f67162 100644
--- a/crates/resvg/src/path.rs
+++ b/crates/resvg/src/path.rs
@@ -13,11 +13,11 @@ pub fn render(
     transform: tiny_skia::Transform,
     pixmap: &mut tiny_skia::PixmapMut,
 ) {
-    if path.visibility != usvg::Visibility::Visible {
+    if path.visibility() != usvg::Visibility::Visible {
         return;
     }
 
-    let mut object_bbox = match path.bounding_box {
+    let mut object_bbox = match path.bounding_box() {
         Some(v) => v,
         None => {
             log::warn!(
@@ -32,7 +32,7 @@ pub fn render(
         object_bbox = text_bbox.to_rect();
     }
 
-    if path.paint_order == usvg::PaintOrder::FillAndStroke {
+    if path.paint_order() == usvg::PaintOrder::FillAndStroke {
         fill_path(path, blend_mode, ctx, object_bbox, transform, pixmap);
         stroke_path(path, blend_mode, ctx, object_bbox, transform, pixmap);
     } else {
@@ -49,31 +49,31 @@ pub fn fill_path(
     transform: tiny_skia::Transform,
     pixmap: &mut tiny_skia::PixmapMut,
 ) -> Option<()> {
-    let fill = path.fill.as_ref()?;
+    let fill = path.fill()?;
 
     // Horizontal and vertical lines cannot be filled. Skip.
-    if path.data.bounds().width() == 0.0 || path.data.bounds().height() == 0.0 {
+    if path.data().bounds().width() == 0.0 || path.data().bounds().height() == 0.0 {
         return None;
     }
 
     let object_bbox = object_bbox.to_non_zero_rect();
 
-    let rule = match fill.rule {
+    let rule = match fill.rule() {
         usvg::FillRule::NonZero => tiny_skia::FillRule::Winding,
         usvg::FillRule::EvenOdd => tiny_skia::FillRule::EvenOdd,
     };
 
     let pattern_pixmap;
     let mut paint = tiny_skia::Paint::default();
-    match fill.paint {
+    match fill.paint() {
         usvg::Paint::Color(c) => {
-            paint.set_color_rgba8(c.red, c.green, c.blue, fill.opacity.to_u8());
+            paint.set_color_rgba8(c.red, c.green, c.blue, fill.opacity().to_u8());
         }
         usvg::Paint::LinearGradient(ref lg) => {
-            paint.shader = convert_linear_gradient(lg, fill.opacity, object_bbox)?;
+            paint.shader = convert_linear_gradient(lg, fill.opacity(), object_bbox)?;
         }
         usvg::Paint::RadialGradient(ref rg) => {
-            paint.shader = convert_radial_gradient(rg, fill.opacity, object_bbox)?;
+            paint.shader = convert_radial_gradient(rg, fill.opacity(), object_bbox)?;
         }
         usvg::Paint::Pattern(ref pattern) => {
             let (patt_pix, patt_ts) =
@@ -84,15 +84,15 @@ pub fn fill_path(
                 pattern_pixmap.as_ref(),
                 tiny_skia::SpreadMode::Repeat,
                 tiny_skia::FilterQuality::Bicubic,
-                fill.opacity.get(),
+                fill.opacity().get(),
                 patt_ts,
             )
         }
     }
-    paint.anti_alias = path.rendering_mode.use_shape_antialiasing();
+    paint.anti_alias = path.rendering_mode().use_shape_antialiasing();
     paint.blend_mode = blend_mode;
 
-    pixmap.fill_path(&path.data, &paint, rule, transform, None);
+    pixmap.fill_path(path.data(), &paint, rule, transform, None);
 
     Some(())
 }
@@ -105,20 +105,20 @@ fn stroke_path(
     transform: tiny_skia::Transform,
     pixmap: &mut tiny_skia::PixmapMut,
 ) -> Option<()> {
-    let stroke = path.stroke.as_ref()?;
+    let stroke = path.stroke()?;
     let object_bbox = object_bbox.to_non_zero_rect();
 
     let pattern_pixmap;
     let mut paint = tiny_skia::Paint::default();
-    match stroke.paint {
+    match stroke.paint() {
         usvg::Paint::Color(c) => {
-            paint.set_color_rgba8(c.red, c.green, c.blue, stroke.opacity.to_u8());
+            paint.set_color_rgba8(c.red, c.green, c.blue, stroke.opacity().to_u8());
         }
         usvg::Paint::LinearGradient(ref lg) => {
-            paint.shader = convert_linear_gradient(lg, stroke.opacity, object_bbox)?;
+            paint.shader = convert_linear_gradient(lg, stroke.opacity(), object_bbox)?;
         }
         usvg::Paint::RadialGradient(ref rg) => {
-            paint.shader = convert_radial_gradient(rg, stroke.opacity, object_bbox)?;
+            paint.shader = convert_radial_gradient(rg, stroke.opacity(), object_bbox)?;
         }
         usvg::Paint::Pattern(ref pattern) => {
             let (patt_pix, patt_ts) =
@@ -129,15 +129,15 @@ fn stroke_path(
                 pattern_pixmap.as_ref(),
                 tiny_skia::SpreadMode::Repeat,
                 tiny_skia::FilterQuality::Bicubic,
-                stroke.opacity.get(),
+                stroke.opacity().get(),
                 patt_ts,
             )
         }
     }
-    paint.anti_alias = path.rendering_mode.use_shape_antialiasing();
+    paint.anti_alias = path.rendering_mode().use_shape_antialiasing();
     paint.blend_mode = blend_mode;
 
-    pixmap.stroke_path(&path.data, &paint, &stroke.to_tiny_skia(), transform, None);
+    pixmap.stroke_path(path.data(), &paint, &stroke.to_tiny_skia(), transform, None);
 
     Some(())
 }
@@ -150,8 +150,8 @@ fn convert_linear_gradient(
     let (mode, transform, points) = convert_base_gradient(gradient, opacity, object_bbox)?;
 
     let shader = tiny_skia::LinearGradient::new(
-        (gradient.x1, gradient.y1).into(),
-        (gradient.x2, gradient.y2).into(),
+        (gradient.x1(), gradient.y1()).into(),
+        (gradient.x2(), gradient.y2()).into(),
         points,
         mode,
         transform,
@@ -168,9 +168,9 @@ fn convert_radial_gradient(
     let (mode, transform, points) = convert_base_gradient(gradient, opacity, object_bbox)?;
 
     let shader = tiny_skia::RadialGradient::new(
-        (gradient.fx, gradient.fy).into(),
-        (gradient.cx, gradient.cy).into(),
-        gradient.r.get(),
+        (gradient.fx(), gradient.fy()).into(),
+        (gradient.cx(), gradient.cy()).into(),
+        gradient.r().get(),
         points,
         mode,
         transform,
@@ -188,31 +188,31 @@ fn convert_base_gradient(
     tiny_skia::Transform,
     Vec<tiny_skia::GradientStop>,
 )> {
-    let mode = match gradient.spread_method {
+    let mode = match gradient.spread_method() {
         usvg::SpreadMethod::Pad => tiny_skia::SpreadMode::Pad,
         usvg::SpreadMethod::Reflect => tiny_skia::SpreadMode::Reflect,
         usvg::SpreadMethod::Repeat => tiny_skia::SpreadMode::Repeat,
     };
 
-    let transform = if gradient.units == usvg::Units::ObjectBoundingBox {
+    let transform = if gradient.units() == usvg::Units::ObjectBoundingBox {
         let bbox =
             object_bbox.log_none(|| log::warn!("Gradient on zero-sized shapes is not allowed."))?;
         let ts = tiny_skia::Transform::from_bbox(bbox);
-        ts.pre_concat(gradient.transform)
+        ts.pre_concat(gradient.transform())
     } else {
-        gradient.transform
+        gradient.transform()
     };
 
-    let mut points = Vec::with_capacity(gradient.stops.len());
-    for stop in &gradient.stops {
-        let alpha = stop.opacity * opacity;
+    let mut points = Vec::with_capacity(gradient.stops().len());
+    for stop in gradient.stops() {
+        let alpha = stop.opacity() * opacity;
         let color = tiny_skia::Color::from_rgba8(
-            stop.color.red,
-            stop.color.green,
-            stop.color.blue,
+            stop.color().red,
+            stop.color().green,
+            stop.color().blue,
             alpha.to_u8(),
         );
-        points.push(tiny_skia::GradientStop::new(stop.offset.get(), color))
+        points.push(tiny_skia::GradientStop::new(stop.offset().get(), color))
     }
 
     Some((mode, transform, points))
@@ -224,28 +224,29 @@ fn render_pattern_pixmap(
     transform: tiny_skia::Transform,
     object_bbox: Option<tiny_skia::NonZeroRect>,
 ) -> Option<(tiny_skia::Pixmap, tiny_skia::Transform)> {
-    let content_transform =
-        if pattern.content_units == usvg::Units::ObjectBoundingBox && pattern.view_box.is_none() {
-            let bbox = object_bbox
-                .log_none(|| log::warn!("Pattern on zero-sized shapes is not allowed."))?;
-
-            // No need to shift patterns.
-            tiny_skia::Transform::from_scale(bbox.width(), bbox.height())
-        } else {
-            tiny_skia::Transform::default()
-        };
-
-    let rect = if pattern.units == usvg::Units::ObjectBoundingBox {
+    let content_transform = if pattern.content_units() == usvg::Units::ObjectBoundingBox
+        && pattern.view_box().is_none()
+    {
         let bbox =
             object_bbox.log_none(|| log::warn!("Pattern on zero-sized shapes is not allowed."))?;
 
-        pattern.rect.bbox_transform(bbox)
+        // No need to shift patterns.
+        tiny_skia::Transform::from_scale(bbox.width(), bbox.height())
     } else {
-        pattern.rect
+        tiny_skia::Transform::default()
+    };
+
+    let rect = if pattern.units() == usvg::Units::ObjectBoundingBox {
+        let bbox =
+            object_bbox.log_none(|| log::warn!("Pattern on zero-sized shapes is not allowed."))?;
+
+        pattern.rect().bbox_transform(bbox)
+    } else {
+        pattern.rect()
     };
 
     let (sx, sy) = {
-        let ts2 = transform.pre_concat(pattern.transform);
+        let ts2 = transform.pre_concat(pattern.transform());
         ts2.get_scale()
     };
 
@@ -256,17 +257,17 @@ fn render_pattern_pixmap(
     let mut pixmap = tiny_skia::Pixmap::new(img_size.width(), img_size.height())?;
 
     let mut transform = tiny_skia::Transform::from_scale(sx, sy);
-    if let Some(vbox) = pattern.view_box {
+    if let Some(vbox) = pattern.view_box() {
         let ts = usvg::utils::view_box_to_transform(vbox.rect, vbox.aspect, rect.size());
         transform = transform.pre_concat(ts);
     }
 
     transform = transform.pre_concat(content_transform);
 
-    crate::render::render_nodes(&pattern.root, ctx, transform, None, &mut pixmap.as_mut());
+    crate::render::render_nodes(pattern.root(), ctx, transform, None, &mut pixmap.as_mut());
 
     let mut ts = tiny_skia::Transform::default();
-    ts = ts.pre_concat(pattern.transform);
+    ts = ts.pre_concat(pattern.transform());
     ts = ts.pre_translate(rect.x(), rect.y());
     ts = ts.pre_scale(1.0 / sx, 1.0 / sy);
 
diff --git a/crates/resvg/src/render.rs b/crates/resvg/src/render.rs
index 7b9078c48..501c26f72 100644
--- a/crates/resvg/src/render.rs
+++ b/crates/resvg/src/render.rs
@@ -15,7 +15,7 @@ pub fn render_nodes(
     text_bbox: Option<tiny_skia::NonZeroRect>,
     pixmap: &mut tiny_skia::PixmapMut,
 ) {
-    for node in &parent.children {
+    for node in parent.children() {
         render_node(node, ctx, transform, text_bbox, pixmap);
     }
 }
@@ -45,7 +45,7 @@ pub fn render_node(
             crate::image::render(image, transform, pixmap);
         }
         usvg::Node::Text(ref text) => {
-            if let (Some(bbox), Some(ref flattened)) = (text.bounding_box, &text.flattened) {
+            if let (Some(bbox), Some(ref flattened)) = (text.bounding_box(), &text.flattened()) {
                 render_group(flattened, ctx, transform, Some(bbox), pixmap);
             } else {
                 log::warn!("Text nodes should be flattened before rendering.");
@@ -61,16 +61,16 @@ fn render_group(
     text_bbox: Option<tiny_skia::NonZeroRect>,
     pixmap: &mut tiny_skia::PixmapMut,
 ) -> Option<()> {
-    let transform = transform.pre_concat(group.transform);
+    let transform = transform.pre_concat(group.transform());
 
     if !group.should_isolate() {
         render_nodes(group, ctx, transform, text_bbox, pixmap);
         return Some(());
     }
 
-    let bbox = group.layer_bounding_box?.transform(transform)?;
+    let bbox = group.layer_bounding_box()?.transform(transform)?;
 
-    let mut ibbox = if group.filters.is_empty() {
+    let mut ibbox = if group.filters().is_empty() {
         // Convert group bbox into an integer one, expanding each side outwards by 2px
         // to make sure that anti-aliased pixels would not be clipped.
         tiny_skia::IntRect::from_xywh(
@@ -91,7 +91,7 @@ fn render_group(
 
     // Make sure our layer is not bigger than 4x the canvas size.
     // This is required to prevent huge layers.
-    if group.filters.is_empty() {
+    if group.filters().is_empty() {
         ibbox = crate::geom::fit_to_rect(ibbox, ctx.max_bbox)?;
     }
 
@@ -114,23 +114,23 @@ fn render_group(
 
     render_nodes(group, ctx, transform, text_bbox, &mut sub_pixmap.as_mut());
 
-    if !group.filters.is_empty() {
-        for filter in &group.filters {
+    if !group.filters().is_empty() {
+        for filter in group.filters() {
             crate::filter::apply(group, &filter.borrow(), transform, &mut sub_pixmap);
         }
     }
 
-    if let (Some(ref clip_path), Some(object_bbox)) = (&group.clip_path, group.bounding_box) {
+    if let (Some(ref clip_path), Some(object_bbox)) = (&group.clip_path(), group.bounding_box()) {
         crate::clip::apply(&clip_path.borrow(), object_bbox, transform, &mut sub_pixmap);
     }
 
-    if let (Some(ref mask), Some(object_bbox)) = (&group.mask, group.bounding_box) {
+    if let (Some(ref mask), Some(object_bbox)) = (&group.mask(), group.bounding_box()) {
         crate::mask::apply(&mask.borrow(), ctx, object_bbox, transform, &mut sub_pixmap);
     }
 
     let paint = tiny_skia::PixmapPaint {
-        opacity: group.opacity.get(),
-        blend_mode: convert_blend_mode(group.blend_mode),
+        opacity: group.opacity().get(),
+        blend_mode: convert_blend_mode(group.blend_mode()),
         quality: tiny_skia::FilterQuality::Nearest,
     };
 
diff --git a/crates/usvg/src/parser/clippath.rs b/crates/usvg/src/parser/clippath.rs
index 033629944..715e8a68a 100644
--- a/crates/usvg/src/parser/clippath.rs
+++ b/crates/usvg/src/parser/clippath.rs
@@ -48,7 +48,7 @@ pub(crate) fn convert(
         units,
         transform,
         clip_path,
-        root: Group::default(),
+        root: Group::empty(),
     };
 
     let mut clip_state = state.clone();
diff --git a/crates/usvg/src/parser/converter.rs b/crates/usvg/src/parser/converter.rs
index 9a73f60a9..9439e97ff 100644
--- a/crates/usvg/src/parser/converter.rs
+++ b/crates/usvg/src/parser/converter.rs
@@ -156,7 +156,7 @@ pub(crate) fn convert_doc(svg_doc: &svgtree::Document, opt: &Options) -> Result<
     let mut tree = Tree {
         size,
         view_box,
-        root: Group::default(),
+        root: Group::empty(),
     };
 
     if !svg.is_visible_element(opt) {
@@ -623,7 +623,7 @@ fn convert_path(
 
     let mut markers_node = None;
     if super::marker::is_valid(node) && visibility == Visibility::Visible {
-        let mut marker = Group::default();
+        let mut marker = Group::empty();
         super::marker::convert(node, &path, state, cache, &mut marker);
         markers_node = Some(marker);
     }
diff --git a/crates/usvg/src/parser/filter.rs b/crates/usvg/src/parser/filter.rs
index 682d765d0..14ffe2dc2 100644
--- a/crates/usvg/src/parser/filter.rs
+++ b/crates/usvg/src/parser/filter.rs
@@ -696,7 +696,7 @@ fn convert_image(fe: SvgNode, state: &converter::State, cache: &mut converter::C
     if let Some(node) = fe.try_attribute::<SvgNode>(AId::Href) {
         let mut state = state.clone();
         state.fe_image_link = true;
-        let mut root = Group::default();
+        let mut root = Group::empty();
         super::converter::convert_element(node, &state, cache, &mut root);
         return if root.has_children() {
             // Transfer node id from group's child to the group itself if needed.
diff --git a/crates/usvg/src/parser/marker.rs b/crates/usvg/src/parser/marker.rs
index aa88f978a..e27cda528 100644
--- a/crates/usvg/src/parser/marker.rs
+++ b/crates/usvg/src/parser/marker.rs
@@ -120,9 +120,9 @@ fn resolve(
             r.size().to_non_zero_rect(0.0, 0.0)
         };
 
-        let mut clip_path = ClipPath::default();
+        let mut clip_path = ClipPath::empty();
 
-        let mut path = Path::new(Rc::new(tiny_skia_path::PathBuilder::from_rect(
+        let mut path = Path::with_path(Rc::new(tiny_skia_path::PathBuilder::from_rect(
             clip_rect.to_rect(),
         )));
         path.fill = Some(Fill::default());
@@ -197,7 +197,7 @@ fn resolve(
         let mut g = Group {
             transform: ts,
             clip_path: clip_path.clone(),
-            ..Group::default()
+            ..Group::empty()
         };
 
         let mut marker_state = state.clone();
diff --git a/crates/usvg/src/parser/mask.rs b/crates/usvg/src/parser/mask.rs
index af165fcb9..392c374a7 100644
--- a/crates/usvg/src/parser/mask.rs
+++ b/crates/usvg/src/parser/mask.rs
@@ -67,7 +67,7 @@ pub(crate) fn convert(
         rect,
         kind,
         mask,
-        root: Group::default(),
+        root: Group::empty(),
     };
 
     converter::convert_children(node, state, cache, &mut mask.root);
diff --git a/crates/usvg/src/parser/paint_server.rs b/crates/usvg/src/parser/paint_server.rs
index 9f3695f0d..7240ef06e 100644
--- a/crates/usvg/src/parser/paint_server.rs
+++ b/crates/usvg/src/parser/paint_server.rs
@@ -184,7 +184,7 @@ fn convert_pattern(
         transform,
         rect,
         view_box,
-        root: Group::default(),
+        root: Group::empty(),
     };
 
     converter::convert_children(node_with_children, state, cache, &mut patt.root);
diff --git a/crates/usvg/src/parser/use_node.rs b/crates/usvg/src/parser/use_node.rs
index fd615d540..08ecfa1ef 100644
--- a/crates/usvg/src/parser/use_node.rs
+++ b/crates/usvg/src/parser/use_node.rs
@@ -201,9 +201,9 @@ fn clip_element(
     //   <elem/>
     // </g>
 
-    let mut clip_path = crate::ClipPath::default();
+    let mut clip_path = crate::ClipPath::empty();
 
-    let mut path = Path::new(Rc::new(tiny_skia_path::PathBuilder::from_rect(
+    let mut path = Path::with_path(Rc::new(tiny_skia_path::PathBuilder::from_rect(
         clip_rect.to_rect(),
     )));
     path.fill = Some(crate::Fill::default());
@@ -220,7 +220,7 @@ fn clip_element(
         id,
         transform,
         clip_path: Some(Rc::new(RefCell::new(clip_path))),
-        ..Group::default()
+        ..Group::empty()
     }
 }
 
diff --git a/crates/usvg/src/text_to_paths.rs b/crates/usvg/src/text_to_paths.rs
index 7eef7ca39..2b519d029 100644
--- a/crates/usvg/src/text_to_paths.rs
+++ b/crates/usvg/src/text_to_paths.rs
@@ -48,7 +48,7 @@ fn convert_node(
 
     let mut group = Group {
         id: text.id.clone(),
-        ..Group::default()
+        ..Group::empty()
     };
 
     let rendering_mode = resolve_rendering_mode(text);
@@ -760,7 +760,7 @@ fn convert_decoration(
     let mut path_data = builder.finish()?;
     path_data = path_data.transform(transform)?;
 
-    let mut path = Path::new(Rc::new(path_data));
+    let mut path = Path::with_path(Rc::new(path_data));
     path.visibility = span.visibility;
     path.fill = decoration.fill.take();
     path.stroke = decoration.stroke.take();
diff --git a/crates/usvg/src/tree/filter.rs b/crates/usvg/src/tree/filter.rs
index ac5ddbcfd..3f9d9fcd1 100644
--- a/crates/usvg/src/tree/filter.rs
+++ b/crates/usvg/src/tree/filter.rs
@@ -17,29 +17,47 @@ use crate::{BlendMode, Color, Group, ImageRendering, NonZeroF32, NonZeroRect, Op
 /// `filter` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Filter {
+    pub(crate) id: String,
+    pub(crate) units: Units,
+    pub(crate) primitive_units: Units,
+    pub(crate) rect: NonZeroRect,
+    pub(crate) primitives: Vec<Primitive>,
+}
+
+impl Filter {
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Used only during SVG writing. `resvg` doesn't rely on this property.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Region coordinate system units.
     ///
     /// `filterUnits` in the SVG.
-    pub units: Units,
+    pub fn units(&self) -> Units {
+        self.units
+    }
 
     /// Content coordinate system units.
     ///
     /// `primitiveUnits` in the SVG.
-    pub primitive_units: Units,
+    pub fn primitive_units(&self) -> Units {
+        self.primitive_units
+    }
 
     /// Filter region.
     ///
     /// `x`, `y`, `width` and `height` in the SVG.
-    pub rect: NonZeroRect,
+    pub fn rect(&self) -> NonZeroRect {
+        self.rect
+    }
 
     /// A list of filter primitives.
-    pub primitives: Vec<Primitive>,
+    pub fn primitives(&self) -> &[Primitive] {
+        &self.primitives
+    }
 }
 
 /// An alias for a shared `Filter`.
@@ -48,30 +66,54 @@ pub type SharedFilter = Rc<RefCell<Filter>>;
 /// A filter primitive element.
 #[derive(Clone, Debug)]
 pub struct Primitive {
+    pub(crate) x: Option<f32>,
+    pub(crate) y: Option<f32>,
+    pub(crate) width: Option<f32>,
+    pub(crate) height: Option<f32>,
+    pub(crate) color_interpolation: ColorInterpolation,
+    pub(crate) result: String,
+    pub(crate) kind: Kind,
+}
+
+impl Primitive {
     /// `x` coordinate of the filter subregion.
-    pub x: Option<f32>,
+    pub fn x(&self) -> Option<f32> {
+        self.x
+    }
 
     /// `y` coordinate of the filter subregion.
-    pub y: Option<f32>,
+    pub fn y(&self) -> Option<f32> {
+        self.y
+    }
 
     /// The filter subregion width.
-    pub width: Option<f32>,
+    pub fn width(&self) -> Option<f32> {
+        self.width
+    }
 
     /// The filter subregion height.
-    pub height: Option<f32>,
+    pub fn height(&self) -> Option<f32> {
+        self.height
+    }
 
     /// Color interpolation mode.
     ///
     /// `color-interpolation-filters` in the SVG.
-    pub color_interpolation: ColorInterpolation,
+    pub fn color_interpolation(&self) -> ColorInterpolation {
+        self.color_interpolation
+    }
 
     /// Assigned name for this filter primitive.
     ///
     /// `result` in the SVG.
-    pub result: String,
+    pub fn result(&self) -> &str {
+        &self.result
+    }
 
     /// Filter primitive kind.
-    pub kind: Kind,
+    pub fn kind(&self) -> &Kind {
+        &self.kind
+    }
 }
 
 /// A filter kind.
@@ -150,20 +192,32 @@ impl Default for ColorInterpolation {
 /// `feBlend` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Blend {
+    pub(crate) input1: Input,
+    pub(crate) input2: Input,
+    pub(crate) mode: BlendMode,
+}
+
+impl Blend {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input1: Input,
+    pub fn input1(&self) -> &Input {
+        &self.input1
+    }
 
     /// Identifies input for the given filter primitive.
     ///
     /// `in2` in the SVG.
-    pub input2: Input,
+    pub fn input2(&self) -> &Input {
+        &self.input2
+    }
 
     /// A blending mode.
     ///
     /// `mode` in the SVG.
-    pub mode: BlendMode,
+    pub fn mode(&self) -> BlendMode {
+        self.mode
+    }
 }
 
 /// A color matrix filter primitive.
@@ -171,15 +225,24 @@ pub struct Blend {
 /// `feColorMatrix` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct ColorMatrix {
+    pub(crate) input: Input,
+    pub(crate) kind: ColorMatrixKind,
+}
+
+impl ColorMatrix {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// A matrix kind.
     ///
     /// `type` in the SVG.
-    pub kind: ColorMatrixKind,
+    pub fn kind(&self) -> &ColorMatrixKind {
+        &self.kind
+    }
 }
 
 /// A color matrix filter primitive kind.
@@ -206,22 +269,40 @@ impl Default for ColorMatrixKind {
 /// `feComponentTransfer` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct ComponentTransfer {
+    pub(crate) input: Input,
+    pub(crate) func_r: TransferFunction,
+    pub(crate) func_g: TransferFunction,
+    pub(crate) func_b: TransferFunction,
+    pub(crate) func_a: TransferFunction,
+}
+
+impl ComponentTransfer {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// `feFuncR` in the SVG.
-    pub func_r: TransferFunction,
+    pub fn func_r(&self) -> &TransferFunction {
+        &self.func_r
+    }
 
     /// `feFuncG` in the SVG.
-    pub func_g: TransferFunction,
+    pub fn func_g(&self) -> &TransferFunction {
+        &self.func_g
+    }
 
     /// `feFuncB` in the SVG.
-    pub func_b: TransferFunction,
+    pub fn func_b(&self) -> &TransferFunction {
+        &self.func_b
+    }
 
     /// `feFuncA` in the SVG.
-    pub func_a: TransferFunction,
+    pub fn func_a(&self) -> &TransferFunction {
+        &self.func_a
+    }
 }
 
 /// A transfer function used by `FeComponentTransfer`.
@@ -260,20 +341,32 @@ pub enum TransferFunction {
 /// `feComposite` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Composite {
+    pub(crate) input1: Input,
+    pub(crate) input2: Input,
+    pub(crate) operator: CompositeOperator,
+}
+
+impl Composite {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input1: Input,
+    pub fn input1(&self) -> &Input {
+        &self.input1
+    }
 
     /// Identifies input for the given filter primitive.
     ///
     /// `in2` in the SVG.
-    pub input2: Input,
+    pub fn input2(&self) -> &Input {
+        &self.input2
+    }
 
     /// A compositing operation.
     ///
     /// `operator` in the SVG.
-    pub operator: CompositeOperator,
+    pub fn operator(&self) -> CompositeOperator {
+        self.operator
+    }
 }
 
 /// An images compositing operation.
@@ -293,33 +386,54 @@ pub enum CompositeOperator {
 /// `feConvolveMatrix` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct ConvolveMatrix {
+    pub(crate) input: Input,
+    pub(crate) matrix: ConvolveMatrixData,
+    pub(crate) divisor: NonZeroF32,
+    pub(crate) bias: f32,
+    pub(crate) edge_mode: EdgeMode,
+    pub(crate) preserve_alpha: bool,
+}
+
+impl ConvolveMatrix {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// A convolve matrix.
-    pub matrix: ConvolveMatrixData,
+    pub fn matrix(&self) -> &ConvolveMatrixData {
+        &self.matrix
+    }
 
     /// A matrix divisor.
     ///
     /// `divisor` in the SVG.
-    pub divisor: NonZeroF32,
+    pub fn divisor(&self) -> NonZeroF32 {
+        self.divisor
+    }
 
     /// A kernel matrix bias.
     ///
     /// `bias` in the SVG.
-    pub bias: f32,
+    pub fn bias(&self) -> f32 {
+        self.bias
+    }
 
     /// An edges processing mode.
     ///
     /// `edgeMode` in the SVG.
-    pub edge_mode: EdgeMode,
+    pub fn edge_mode(&self) -> EdgeMode {
+        self.edge_mode
+    }
 
     /// An alpha preserving flag.
     ///
     /// `preserveAlpha` in the SVG.
-    pub preserve_alpha: bool,
+    pub fn preserve_alpha(&self) -> bool {
+        self.preserve_alpha
+    }
 }
 
 /// A convolve matrix representation.
@@ -327,28 +441,46 @@ pub struct ConvolveMatrix {
 /// Used primarily by [`ConvolveMatrix`].
 #[derive(Clone, Debug)]
 pub struct ConvolveMatrixData {
+    pub(crate) target_x: u32,
+    pub(crate) target_y: u32,
+    pub(crate) columns: u32,
+    pub(crate) rows: u32,
+    pub(crate) data: Vec<f32>,
+}
+
+impl ConvolveMatrixData {
     /// Returns a matrix's X target.
     ///
     /// `targetX` in the SVG.
-    pub target_x: u32,
+    pub fn target_x(&self) -> u32 {
+        self.target_x
+    }
 
     /// Returns a matrix's Y target.
     ///
     /// `targetY` in the SVG.
-    pub target_y: u32,
+    pub fn target_y(&self) -> u32 {
+        self.target_y
+    }
 
     /// Returns a number of columns in the matrix.
     ///
     /// Part of the `order` attribute in the SVG.
-    pub columns: u32,
+    pub fn columns(&self) -> u32 {
+        self.columns
+    }
 
     /// Returns a number of rows in the matrix.
     ///
     /// Part of the `order` attribute in the SVG.
-    pub rows: u32,
+    pub fn rows(&self) -> u32 {
+        self.rows
+    }
 
     /// The actual matrix.
-    pub data: Vec<f32>,
+    pub fn data(&self) -> &[f32] {
+        &self.data
+    }
 }
 
 impl ConvolveMatrixData {
@@ -359,7 +491,7 @@ impl ConvolveMatrixData {
     /// - `columns` * `rows` != `data.len()`
     /// - `target_x` >= `columns`
     /// - `target_y` >= `rows`
-    pub fn new(
+    pub(crate) fn new(
         target_x: u32,
         target_y: u32,
         columns: u32,
@@ -403,30 +535,48 @@ pub enum EdgeMode {
 /// `feDisplacementMap` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct DisplacementMap {
+    pub(crate) input1: Input,
+    pub(crate) input2: Input,
+    pub(crate) scale: f32,
+    pub(crate) x_channel_selector: ColorChannel,
+    pub(crate) y_channel_selector: ColorChannel,
+}
+
+impl DisplacementMap {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input1: Input,
+    pub fn input1(&self) -> &Input {
+        &self.input1
+    }
 
     /// Identifies input for the given filter primitive.
     ///
     /// `in2` in the SVG.
-    pub input2: Input,
+    pub fn input2(&self) -> &Input {
+        &self.input2
+    }
 
     /// Scale factor.
     ///
     /// `scale` in the SVG.
-    pub scale: f32,
+    pub fn scale(&self) -> f32 {
+        self.scale
+    }
 
     /// Indicates a source color channel along the X-axis.
     ///
     /// `xChannelSelector` in the SVG.
-    pub x_channel_selector: ColorChannel,
+    pub fn x_channel_selector(&self) -> ColorChannel {
+        self.x_channel_selector
+    }
 
     /// Indicates a source color channel along the Y-axis.
     ///
     /// `yChannelSelector` in the SVG.
-    pub y_channel_selector: ColorChannel,
+    pub fn y_channel_selector(&self) -> ColorChannel {
+        self.y_channel_selector
+    }
 }
 
 /// A color channel.
@@ -446,36 +596,60 @@ pub enum ColorChannel {
 /// `feDropShadow` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct DropShadow {
+    pub(crate) input: Input,
+    pub(crate) dx: f32,
+    pub(crate) dy: f32,
+    pub(crate) std_dev_x: PositiveF32,
+    pub(crate) std_dev_y: PositiveF32,
+    pub(crate) color: Color,
+    pub(crate) opacity: Opacity,
+}
+
+impl DropShadow {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// The amount to offset the input graphic along the X-axis.
-    pub dx: f32,
+    pub fn dx(&self) -> f32 {
+        self.dx
+    }
 
     /// The amount to offset the input graphic along the Y-axis.
-    pub dy: f32,
+    pub fn dy(&self) -> f32 {
+        self.dy
+    }
 
     /// A standard deviation along the X-axis.
     ///
     /// `stdDeviation` in the SVG.
-    pub std_dev_x: PositiveF32,
+    pub fn std_dev_x(&self) -> PositiveF32 {
+        self.std_dev_x
+    }
 
     /// A standard deviation along the Y-axis.
     ///
     /// `stdDeviation` in the SVG.
-    pub std_dev_y: PositiveF32,
+    pub fn std_dev_y(&self) -> PositiveF32 {
+        self.std_dev_y
+    }
 
     /// A flood color.
     ///
     /// `flood-color` in the SVG.
-    pub color: Color,
+    pub fn color(&self) -> Color {
+        self.color
+    }
 
     /// A flood opacity.
     ///
     /// `flood-opacity` in the SVG.
-    pub opacity: Opacity,
+    pub fn opacity(&self) -> Opacity {
+        self.opacity
+    }
 }
 
 /// A flood filter primitive.
@@ -483,15 +657,24 @@ pub struct DropShadow {
 /// `feFlood` element in the SVG.
 #[derive(Clone, Copy, Debug)]
 pub struct Flood {
+    pub(crate) color: Color,
+    pub(crate) opacity: Opacity,
+}
+
+impl Flood {
     /// A flood color.
     ///
     /// `flood-color` in the SVG.
-    pub color: Color,
+    pub fn color(&self) -> Color {
+        self.color
+    }
 
     /// A flood opacity.
     ///
     /// `flood-opacity` in the SVG.
-    pub opacity: Opacity,
+    pub fn opacity(&self) -> Opacity {
+        self.opacity
+    }
 }
 
 /// A Gaussian blur filter primitive.
@@ -499,20 +682,32 @@ pub struct Flood {
 /// `feGaussianBlur` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct GaussianBlur {
+    pub(crate) input: Input,
+    pub(crate) std_dev_x: PositiveF32,
+    pub(crate) std_dev_y: PositiveF32,
+}
+
+impl GaussianBlur {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// A standard deviation along the X-axis.
     ///
     /// `stdDeviation` in the SVG.
-    pub std_dev_x: PositiveF32,
+    pub fn std_dev_x(&self) -> PositiveF32 {
+        self.std_dev_x
+    }
 
     /// A standard deviation along the Y-axis.
     ///
     /// `stdDeviation` in the SVG.
-    pub std_dev_y: PositiveF32,
+    pub fn std_dev_y(&self) -> PositiveF32 {
+        self.std_dev_y
+    }
 }
 
 /// An image filter primitive.
@@ -520,16 +715,28 @@ pub struct GaussianBlur {
 /// `feImage` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Image {
+    pub(crate) aspect: AspectRatio,
+    pub(crate) rendering_mode: ImageRendering,
+    pub(crate) data: ImageKind,
+}
+
+impl Image {
     /// Value of the `preserveAspectRatio` attribute.
-    pub aspect: AspectRatio,
+    pub fn aspect(&self) -> AspectRatio {
+        self.aspect
+    }
 
     /// Rendering method.
     ///
     /// `image-rendering` in SVG.
-    pub rendering_mode: ImageRendering,
+    pub fn rendering_mode(&self) -> ImageRendering {
+        self.rendering_mode
+    }
 
     /// Image data.
-    pub data: ImageKind,
+    pub fn data(&self) -> &ImageKind {
+        &self.data
+    }
 }
 
 /// Kind of the `feImage` data.
@@ -547,28 +754,46 @@ pub enum ImageKind {
 /// `feDiffuseLighting` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct DiffuseLighting {
+    pub(crate) input: Input,
+    pub(crate) surface_scale: f32,
+    pub(crate) diffuse_constant: f32,
+    pub(crate) lighting_color: Color,
+    pub(crate) light_source: LightSource,
+}
+
+impl DiffuseLighting {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// A surface scale.
     ///
     /// `surfaceScale` in the SVG.
-    pub surface_scale: f32,
+    pub fn surface_scale(&self) -> f32 {
+        self.surface_scale
+    }
 
     /// A diffuse constant.
     ///
     /// `diffuseConstant` in the SVG.
-    pub diffuse_constant: f32,
+    pub fn diffuse_constant(&self) -> f32 {
+        self.diffuse_constant
+    }
 
     /// A lighting color.
     ///
     /// `lighting-color` in the SVG.
-    pub lighting_color: Color,
+    pub fn lighting_color(&self) -> Color {
+        self.lighting_color
+    }
 
     /// A light source.
-    pub light_source: LightSource,
+    pub fn light_source(&self) -> LightSource {
+        self.light_source
+    }
 }
 
 /// A specular lighting filter primitive.
@@ -576,35 +801,56 @@ pub struct DiffuseLighting {
 /// `feSpecularLighting` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct SpecularLighting {
+    pub(crate) input: Input,
+    pub(crate) surface_scale: f32,
+    pub(crate) specular_constant: f32,
+    pub(crate) specular_exponent: f32,
+    pub(crate) lighting_color: Color,
+    pub(crate) light_source: LightSource,
+}
+
+impl SpecularLighting {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// A surface scale.
     ///
     /// `surfaceScale` in the SVG.
-    pub surface_scale: f32,
+    pub fn surface_scale(&self) -> f32 {
+        self.surface_scale
+    }
 
     /// A specular constant.
     ///
     /// `specularConstant` in the SVG.
-    pub specular_constant: f32,
+    pub fn specular_constant(&self) -> f32 {
+        self.specular_constant
+    }
 
     /// A specular exponent.
     ///
     /// Should be in 1..128 range.
     ///
     /// `specularExponent` in the SVG.
-    pub specular_exponent: f32,
+    pub fn specular_exponent(&self) -> f32 {
+        self.specular_exponent
+    }
 
     /// A lighting color.
     ///
     /// `lighting-color` in the SVG.
-    pub lighting_color: Color,
+    pub fn lighting_color(&self) -> Color {
+        self.lighting_color
+    }
 
     /// A light source.
-    pub light_source: LightSource,
+    pub fn light_source(&self) -> LightSource {
+        self.light_source
+    }
 }
 
 /// A light source kind.
@@ -705,10 +951,16 @@ pub struct SpotLight {
 /// `feMerge` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Merge {
+    pub(crate) inputs: Vec<Input>,
+}
+
+impl Merge {
     /// List of input layers that should be merged.
     ///
     /// List of `feMergeNode`'s in the SVG.
-    pub inputs: Vec<Input>,
+    pub fn inputs(&self) -> &[Input] {
+        &self.inputs
+    }
 }
 
 /// A morphology filter primitive.
@@ -716,29 +968,44 @@ pub struct Merge {
 /// `feMorphology` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Morphology {
+    pub(crate) input: Input,
+    pub(crate) operator: MorphologyOperator,
+    pub(crate) radius_x: PositiveF32,
+    pub(crate) radius_y: PositiveF32,
+}
+
+impl Morphology {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// A filter operator.
     ///
     /// `operator` in the SVG.
-    pub operator: MorphologyOperator,
+    pub fn operator(&self) -> MorphologyOperator {
+        self.operator
+    }
 
     /// A filter radius along the X-axis.
     ///
     /// A value of zero disables the effect of the given filter primitive.
     ///
     /// `radius` in the SVG.
-    pub radius_x: PositiveF32,
+    pub fn radius_x(&self) -> PositiveF32 {
+        self.radius_x
+    }
 
     /// A filter radius along the Y-axis.
     ///
     /// A value of zero disables the effect of the given filter primitive.
     ///
     /// `radius` in the SVG.
-    pub radius_y: PositiveF32,
+    pub fn radius_y(&self) -> PositiveF32 {
+        self.radius_y
+    }
 }
 
 /// A morphology operation.
@@ -754,16 +1021,28 @@ pub enum MorphologyOperator {
 /// `feOffset` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Offset {
+    pub(crate) input: Input,
+    pub(crate) dx: f32,
+    pub(crate) dy: f32,
+}
+
+impl Offset {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 
     /// The amount to offset the input graphic along the X-axis.
-    pub dx: f32,
+    pub fn dx(&self) -> f32 {
+        self.dx
+    }
 
     /// The amount to offset the input graphic along the Y-axis.
-    pub dy: f32,
+    pub fn dy(&self) -> f32 {
+        self.dy
+    }
 }
 
 /// A tile filter primitive.
@@ -771,10 +1050,16 @@ pub struct Offset {
 /// `feTile` element in the SVG.
 #[derive(Clone, Debug)]
 pub struct Tile {
+    pub(crate) input: Input,
+}
+
+impl Tile {
     /// Identifies input for the given filter primitive.
     ///
     /// `in` in the SVG.
-    pub input: Input,
+    pub fn input(&self) -> &Input {
+        &self.input
+    }
 }
 
 /// A turbulence generation filter primitive.
@@ -782,35 +1067,56 @@ pub struct Tile {
 /// `feTurbulence` element in the SVG.
 #[derive(Clone, Copy, Debug)]
 pub struct Turbulence {
+    pub(crate) base_frequency_x: PositiveF32,
+    pub(crate) base_frequency_y: PositiveF32,
+    pub(crate) num_octaves: u32,
+    pub(crate) seed: i32,
+    pub(crate) stitch_tiles: bool,
+    pub(crate) kind: TurbulenceKind,
+}
+
+impl Turbulence {
     /// Identifies the base frequency for the noise function.
     ///
     /// `baseFrequency` in the SVG.
-    pub base_frequency_x: PositiveF32,
+    pub fn base_frequency_x(&self) -> PositiveF32 {
+        self.base_frequency_x
+    }
 
     /// Identifies the base frequency for the noise function.
     ///
     /// `baseFrequency` in the SVG.
-    pub base_frequency_y: PositiveF32,
+    pub fn base_frequency_y(&self) -> PositiveF32 {
+        self.base_frequency_y
+    }
 
     /// Identifies the number of octaves for the noise function.
     ///
     /// `numOctaves` in the SVG.
-    pub num_octaves: u32,
+    pub fn num_octaves(&self) -> u32 {
+        self.num_octaves
+    }
 
     /// The starting number for the pseudo random number generator.
     ///
     /// `seed` in the SVG.
-    pub seed: i32,
+    pub fn seed(&self) -> i32 {
+        self.seed
+    }
 
     /// Smooth transitions at the border of tiles.
     ///
     /// `stitchTiles` in the SVG.
-    pub stitch_tiles: bool,
+    pub fn stitch_tiles(&self) -> bool {
+        self.stitch_tiles
+    }
 
     /// Indicates whether the filter primitive should perform a noise or turbulence function.
     ///
     /// `type` in the SVG.
-    pub kind: TurbulenceKind,
+    pub fn kind(&self) -> TurbulenceKind {
+        self.kind
+    }
 }
 
 /// A turbulence kind for the `feTurbulence` filter.
diff --git a/crates/usvg/src/tree/geom.rs b/crates/usvg/src/tree/geom.rs
index 0e0737d02..a54cef792 100644
--- a/crates/usvg/src/tree/geom.rs
+++ b/crates/usvg/src/tree/geom.rs
@@ -57,7 +57,7 @@ pub struct ViewBox {
 
 /// A bounding box calculator.
 #[derive(Clone, Copy, Debug)]
-pub struct BBox {
+pub(crate) struct BBox {
     left: f32,
     top: f32,
     right: f32,
@@ -121,11 +121,6 @@ impl BBox {
         }
     }
 
-    /// Transforms the bounding box.
-    pub fn transform(&self, ts: tiny_skia_path::Transform) -> Option<Self> {
-        self.to_rect()?.transform(ts).map(Self::from)
-    }
-
     /// Converts a bounding box into [`Rect`].
     pub fn to_rect(&self) -> Option<Rect> {
         if !self.is_default() {
diff --git a/crates/usvg/src/tree/mod.rs b/crates/usvg/src/tree/mod.rs
index d1db585e5..2dd43177f 100644
--- a/crates/usvg/src/tree/mod.rs
+++ b/crates/usvg/src/tree/mod.rs
@@ -101,7 +101,6 @@ impl Default for ShapeRendering {
     }
 }
 
-// TODO: remove?
 impl std::str::FromStr for ShapeRendering {
     type Err = &'static str;
 
@@ -223,44 +222,81 @@ impl Default for SpreadMethod {
 /// A generic gradient.
 #[derive(Clone, Debug)]
 pub struct BaseGradient {
+    pub(crate) id: String,
+    pub(crate) units: Units,
+    pub(crate) transform: Transform,
+    pub(crate) spread_method: SpreadMethod,
+    pub(crate) stops: Vec<Stop>,
+}
+
+impl BaseGradient {
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Used only during SVG writing. `resvg` doesn't rely on this property.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Coordinate system units.
     ///
     /// `gradientUnits` in SVG.
-    pub units: Units,
+    pub fn units(&self) -> Units {
+        self.units
+    }
 
     /// Gradient transform.
     ///
     /// `gradientTransform` in SVG.
-    pub transform: Transform,
+    pub fn transform(&self) -> Transform {
+        self.transform
+    }
 
     /// Gradient spreading method.
     ///
     /// `spreadMethod` in SVG.
-    pub spread_method: SpreadMethod,
+    pub fn spread_method(&self) -> SpreadMethod {
+        self.spread_method
+    }
 
     /// A list of `stop` elements.
-    pub stops: Vec<Stop>,
+    pub fn stops(&self) -> &[Stop] {
+        &self.stops
+    }
 }
 
 /// A linear gradient.
 ///
 /// `linearGradient` element in SVG.
-#[allow(missing_docs)]
 #[derive(Clone, Debug)]
 pub struct LinearGradient {
-    /// Base gradient data.
-    pub base: BaseGradient,
+    pub(crate) base: BaseGradient,
+    pub(crate) x1: f32,
+    pub(crate) y1: f32,
+    pub(crate) x2: f32,
+    pub(crate) y2: f32,
+}
+
+impl LinearGradient {
+    /// `x1` coordinate.
+    pub fn x1(&self) -> f32 {
+        self.x1
+    }
+
+    /// `y1` coordinate.
+    pub fn y1(&self) -> f32 {
+        self.y1
+    }
+
+    /// `x2` coordinate.
+    pub fn x2(&self) -> f32 {
+        self.x2
+    }
 
-    pub x1: f32,
-    pub y1: f32,
-    pub x2: f32,
-    pub y2: f32,
+    /// `y2` coordinate.
+    pub fn y2(&self) -> f32 {
+        self.y2
+    }
 }
 
 impl std::ops::Deref for LinearGradient {
@@ -274,17 +310,41 @@ impl std::ops::Deref for LinearGradient {
 /// A radial gradient.
 ///
 /// `radialGradient` element in SVG.
-#[allow(missing_docs)]
 #[derive(Clone, Debug)]
 pub struct RadialGradient {
-    /// Base gradient data.
-    pub base: BaseGradient,
+    pub(crate) base: BaseGradient,
+    pub(crate) cx: f32,
+    pub(crate) cy: f32,
+    pub(crate) r: PositiveF32,
+    pub(crate) fx: f32,
+    pub(crate) fy: f32,
+}
+
+impl RadialGradient {
+    /// `cx` coordinate.
+    pub fn cx(&self) -> f32 {
+        self.cx
+    }
+
+    /// `cy` coordinate.
+    pub fn cy(&self) -> f32 {
+        self.cy
+    }
+
+    /// Gradient radius.
+    pub fn r(&self) -> PositiveF32 {
+        self.r
+    }
+
+    /// `fx` coordinate.
+    pub fn fx(&self) -> f32 {
+        self.fx
+    }
 
-    pub cx: f32,
-    pub cy: f32,
-    pub r: PositiveF32,
-    pub fx: f32,
-    pub fy: f32,
+    /// `fy` coordinate.
+    pub fn fy(&self) -> f32 {
+        self.fy
+    }
 }
 
 impl std::ops::Deref for RadialGradient {
@@ -303,20 +363,32 @@ pub type StopOffset = NormalizedF32;
 /// `stop` element in SVG.
 #[derive(Clone, Copy, Debug)]
 pub struct Stop {
+    pub(crate) offset: StopOffset,
+    pub(crate) color: Color,
+    pub(crate) opacity: Opacity,
+}
+
+impl Stop {
     /// Gradient stop offset.
     ///
     /// `offset` in SVG.
-    pub offset: StopOffset,
+    pub fn offset(&self) -> StopOffset {
+        self.offset
+    }
 
     /// Gradient stop color.
     ///
     /// `stop-color` in SVG.
-    pub color: Color,
+    pub fn color(&self) -> Color {
+        self.color
+    }
 
     /// Gradient stop opacity.
     ///
     /// `stop-opacity` in SVG.
-    pub opacity: Opacity,
+    pub fn opacity(&self) -> Opacity {
+        self.opacity
+    }
 }
 
 /// A pattern element.
@@ -324,38 +396,62 @@ pub struct Stop {
 /// `pattern` element in SVG.
 #[derive(Clone, Debug)]
 pub struct Pattern {
+    pub(crate) id: String,
+    pub(crate) units: Units,
+    pub(crate) content_units: Units,
+    pub(crate) transform: Transform,
+    pub(crate) rect: NonZeroRect,
+    pub(crate) view_box: Option<ViewBox>,
+    pub(crate) root: Group,
+}
+
+impl Pattern {
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Used only during SVG writing. `resvg` doesn't rely on this property.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Coordinate system units.
     ///
     /// `patternUnits` in SVG.
-    pub units: Units,
+    pub fn units(&self) -> Units {
+        self.units
+    }
 
     // TODO: should not be accessible when `viewBox` is present.
     /// Content coordinate system units.
     ///
     /// `patternContentUnits` in SVG.
-    pub content_units: Units,
+    pub fn content_units(&self) -> Units {
+        self.content_units
+    }
 
     /// Pattern transform.
     ///
     /// `patternTransform` in SVG.
-    pub transform: Transform,
+    pub fn transform(&self) -> Transform {
+        self.transform
+    }
 
     /// Pattern rectangle.
     ///
     /// `x`, `y`, `width` and `height` in SVG.
-    pub rect: NonZeroRect,
+    pub fn rect(&self) -> NonZeroRect {
+        self.rect
+    }
 
     /// Pattern viewbox.
-    pub view_box: Option<ViewBox>,
+    pub fn view_box(&self) -> Option<ViewBox> {
+        self.view_box
+    }
 
     /// Pattern children.
-    pub root: Group,
+    pub fn root(&self) -> &Group {
+        &self.root
+    }
 }
 
 /// An alias to `NonZeroPositiveF32`.
@@ -443,37 +539,59 @@ impl Default for LineJoin {
 }
 
 /// A stroke style.
-#[allow(missing_docs)]
 #[derive(Clone, Debug)]
 pub struct Stroke {
-    pub paint: Paint,
-    pub dasharray: Option<Vec<f32>>,
-    pub dashoffset: f32,
-    pub miterlimit: StrokeMiterlimit,
-    pub opacity: Opacity,
-    pub width: StrokeWidth,
-    pub linecap: LineCap,
-    pub linejoin: LineJoin,
+    pub(crate) paint: Paint,
+    pub(crate) dasharray: Option<Vec<f32>>,
+    pub(crate) dashoffset: f32,
+    pub(crate) miterlimit: StrokeMiterlimit,
+    pub(crate) opacity: Opacity,
+    pub(crate) width: StrokeWidth,
+    pub(crate) linecap: LineCap,
+    pub(crate) linejoin: LineJoin,
 }
 
-impl Default for Stroke {
-    fn default() -> Self {
-        Stroke {
-            // The actual default color is `none`,
-            // but to simplify the `Stroke` object creation we use `black`.
-            paint: Paint::Color(Color::black()),
-            dasharray: None,
-            dashoffset: 0.0,
-            miterlimit: StrokeMiterlimit::default(),
-            opacity: Opacity::ONE,
-            width: StrokeWidth::new(1.0).unwrap(),
-            linecap: LineCap::default(),
-            linejoin: LineJoin::default(),
-        }
+impl Stroke {
+    /// Stroke paint.
+    pub fn paint(&self) -> &Paint {
+        &self.paint
+    }
+
+    /// Stroke dash array.
+    pub fn dasharray(&self) -> Option<&[f32]> {
+        self.dasharray.as_deref()
+    }
+
+    /// Stroke dash offset.
+    pub fn dashoffset(&self) -> f32 {
+        self.dashoffset
+    }
+
+    /// Stroke miter limit.
+    pub fn miterlimit(&self) -> StrokeMiterlimit {
+        self.miterlimit
+    }
+
+    /// Stroke opacity.
+    pub fn opacity(&self) -> Opacity {
+        self.opacity
+    }
+
+    /// Stroke width.
+    pub fn width(&self) -> StrokeWidth {
+        self.width
+    }
+
+    /// Stroke linecap.
+    pub fn linecap(&self) -> LineCap {
+        self.linecap
+    }
+
+    /// Stroke linejoin.
+    pub fn linejoin(&self) -> LineJoin {
+        self.linejoin
     }
-}
 
-impl Stroke {
     /// Converts into a `tiny_skia_path::Stroke` type.
     pub fn to_tiny_skia(&self) -> tiny_skia_path::Stroke {
         let mut stroke = tiny_skia_path::Stroke {
@@ -520,23 +638,27 @@ impl Default for FillRule {
 }
 
 /// A fill style.
-#[allow(missing_docs)]
 #[derive(Clone, Debug)]
 pub struct Fill {
-    pub paint: Paint,
-    pub opacity: Opacity,
-    pub rule: FillRule,
+    pub(crate) paint: Paint,
+    pub(crate) opacity: Opacity,
+    pub(crate) rule: FillRule,
 }
 
 impl Fill {
-    /// Creates a `Fill` from `Paint`.
-    ///
-    /// `opacity` and `rule` will be set to default values.
-    pub fn from_paint(paint: Paint) -> Self {
-        Fill {
-            paint,
-            ..Fill::default()
-        }
+    /// Fill paint.
+    pub fn paint(&self) -> &Paint {
+        &self.paint
+    }
+
+    /// Fill opacity.
+    pub fn opacity(&self) -> Opacity {
+        self.opacity
+    }
+
+    /// Fill rule.
+    pub fn rule(&self) -> FillRule {
+        self.rule
     }
 }
 
@@ -624,46 +746,62 @@ impl PartialEq for Paint {
 /// `clipPath` element in SVG.
 #[derive(Clone, Debug)]
 pub struct ClipPath {
+    pub(crate) id: String,
+    pub(crate) units: Units,
+    pub(crate) transform: Transform,
+    pub(crate) clip_path: Option<SharedClipPath>,
+    pub(crate) root: Group,
+}
+
+impl ClipPath {
+    pub(crate) fn empty() -> Self {
+        ClipPath {
+            id: String::new(),
+            units: Units::UserSpaceOnUse,
+            transform: Transform::default(),
+            clip_path: None,
+            root: Group::empty(),
+        }
+    }
+
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Used only during SVG writing. `resvg` doesn't rely on this property.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Coordinate system units.
     ///
     /// `clipPathUnits` in SVG.
-    pub units: Units,
+    pub fn units(&self) -> Units {
+        self.units
+    }
 
     /// Clip path transform.
     ///
     /// `transform` in SVG.
-    pub transform: Transform,
+    pub fn transform(&self) -> Transform {
+        self.transform
+    }
 
     /// Additional clip path.
     ///
     /// `clip-path` in SVG.
-    pub clip_path: Option<SharedClipPath>,
+    pub fn clip_path(&self) -> Option<SharedClipPath> {
+        self.clip_path.clone()
+    }
 
     /// Clip path children.
-    pub root: Group,
+    pub fn root(&self) -> &Group {
+        &self.root
+    }
 }
 
 /// An alias for a shared `ClipPath`.
 pub type SharedClipPath = Rc<RefCell<ClipPath>>;
 
-impl Default for ClipPath {
-    fn default() -> Self {
-        ClipPath {
-            id: String::new(),
-            units: Units::UserSpaceOnUse,
-            transform: Transform::default(),
-            clip_path: None,
-            root: Group::default(),
-        }
-    }
-}
-
 /// A mask type.
 #[derive(Clone, Copy, PartialEq, Debug)]
 pub enum MaskType {
@@ -684,39 +822,63 @@ impl Default for MaskType {
 /// `mask` element in SVG.
 #[derive(Clone, Debug)]
 pub struct Mask {
+    pub(crate) id: String,
+    pub(crate) units: Units,
+    pub(crate) content_units: Units,
+    pub(crate) rect: NonZeroRect,
+    pub(crate) kind: MaskType,
+    pub(crate) mask: Option<SharedMask>,
+    pub(crate) root: Group,
+}
+
+impl Mask {
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Used only during SVG writing. `resvg` doesn't rely on this property.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Coordinate system units.
     ///
     /// `maskUnits` in SVG.
-    pub units: Units,
+    pub fn units(&self) -> Units {
+        self.units
+    }
 
     /// Content coordinate system units.
     ///
     /// `maskContentUnits` in SVG.
-    pub content_units: Units,
+    pub fn content_units(&self) -> Units {
+        self.content_units
+    }
 
     /// Mask rectangle.
     ///
     /// `x`, `y`, `width` and `height` in SVG.
-    pub rect: NonZeroRect,
+    pub fn rect(&self) -> NonZeroRect {
+        self.rect
+    }
 
     /// Mask type.
     ///
     /// `mask-type` in SVG.
-    pub kind: MaskType,
+    pub fn kind(&self) -> MaskType {
+        self.kind
+    }
 
     /// Additional mask.
     ///
     /// `mask` in SVG.
-    pub mask: Option<SharedMask>,
+    pub fn mask(&self) -> Option<SharedMask> {
+        self.mask.clone()
+    }
 
     /// Mask children.
-    pub root: Group,
+    pub fn root(&self) -> &Group {
+        &self.root
+    }
 }
 
 /// An alias for a shared `Mask`.
@@ -820,7 +982,7 @@ impl Node {
     ///
     /// ```no_run
     /// fn all_nodes(parent: &usvg::Group) {
-    ///     for node in &parent.children {
+    ///     for node in parent.children() {
     ///         // do stuff...
     ///
     ///         if let usvg::Node::Group(ref g) = node {
@@ -862,17 +1024,59 @@ impl Node {
 /// `g` element in SVG.
 #[derive(Clone, Debug)]
 pub struct Group {
+    pub(crate) id: String,
+    pub(crate) transform: Transform,
+    pub(crate) abs_transform: Transform,
+    pub(crate) opacity: Opacity,
+    pub(crate) blend_mode: BlendMode,
+    pub(crate) isolate: bool,
+    pub(crate) clip_path: Option<SharedClipPath>,
+    pub(crate) mask: Option<SharedMask>,
+    pub(crate) filters: Vec<filter::SharedFilter>,
+    pub(crate) bounding_box: Option<Rect>,
+    pub(crate) abs_bounding_box: Option<Rect>,
+    pub(crate) stroke_bounding_box: Option<NonZeroRect>,
+    pub(crate) abs_stroke_bounding_box: Option<NonZeroRect>,
+    pub(crate) layer_bounding_box: Option<NonZeroRect>,
+    pub(crate) children: Vec<Node>,
+}
+
+impl Group {
+    pub(crate) fn empty() -> Self {
+        Group {
+            id: String::new(),
+            transform: Transform::default(),
+            abs_transform: Transform::default(),
+            opacity: Opacity::ONE,
+            blend_mode: BlendMode::Normal,
+            isolate: false,
+            clip_path: None,
+            mask: None,
+            filters: Vec::new(),
+            bounding_box: None,
+            abs_bounding_box: None,
+            stroke_bounding_box: None,
+            abs_stroke_bounding_box: None,
+            layer_bounding_box: None,
+            children: Vec::new(),
+        }
+    }
+
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Isn't automatically generated.
     /// Can be empty.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Element's transform.
     ///
     /// This is a relative transform. The one that is set via the `transform` attribute in SVG.
-    pub transform: Transform,
+    pub fn transform(&self) -> Transform {
+        self.transform
+    }
 
     /// Element's absolute transform.
     ///
@@ -882,32 +1086,46 @@ pub struct Group {
     ///
     /// Note that subroots, like clipPaths, masks and patterns, have their own root transform,
     /// which isn't affected by the node that references this subroot.
-    pub abs_transform: Transform,
+    pub fn abs_transform(&self) -> Transform {
+        self.abs_transform
+    }
 
     /// Group opacity.
     ///
     /// After the group is rendered we should combine
     /// it with a parent group using the specified opacity.
-    pub opacity: Opacity,
+    pub fn opacity(&self) -> Opacity {
+        self.opacity
+    }
 
     /// Group blend mode.
     ///
     /// `mix-blend-mode` in SVG.
-    pub blend_mode: BlendMode,
+    pub fn blend_mode(&self) -> BlendMode {
+        self.blend_mode
+    }
 
     /// Group isolation.
     ///
     /// `isolation` in SVG.
-    pub isolate: bool,
+    pub fn isolate(&self) -> bool {
+        self.isolate
+    }
 
     /// Element's clip path.
-    pub clip_path: Option<SharedClipPath>,
+    pub fn clip_path(&self) -> Option<SharedClipPath> {
+        self.clip_path.clone()
+    }
 
     /// Element's mask.
-    pub mask: Option<SharedMask>,
+    pub fn mask(&self) -> Option<SharedMask> {
+        self.mask.clone()
+    }
 
     /// Element's filters.
-    pub filters: Vec<filter::SharedFilter>,
+    pub fn filters(&self) -> &[filter::SharedFilter] {
+        &self.filters
+    }
 
     /// Element's object bounding box.
     ///
@@ -916,24 +1134,32 @@ pub struct Group {
     /// Can be set to `None` in case of an empty group.
     ///
     /// Will be set after calling `usvg::Tree::postprocess`.
-    pub bounding_box: Option<Rect>,
+    pub fn bounding_box(&self) -> Option<Rect> {
+        self.bounding_box
+    }
 
     /// Element's bounding box in canvas coordinates.
     ///
     /// `userSpaceOnUse` in SVG terms.
     ///
     /// Will be set after calling `usvg::Tree::postprocess`.
-    pub abs_bounding_box: Option<Rect>,
+    pub fn abs_bounding_box(&self) -> Option<Rect> {
+        self.abs_bounding_box
+    }
 
     /// Element's object bounding box including stroke.
     ///
     /// Similar to `bounding_box`, but includes stroke.
-    pub stroke_bounding_box: Option<NonZeroRect>,
+    pub fn stroke_bounding_box(&self) -> Option<NonZeroRect> {
+        self.stroke_bounding_box
+    }
 
     /// Element's bounding box including stroke in user coordinates.
     ///
     /// Similar to `abs_bounding_box`, but includes stroke.
-    pub abs_stroke_bounding_box: Option<NonZeroRect>,
+    pub fn abs_stroke_bounding_box(&self) -> Option<NonZeroRect> {
+        self.abs_stroke_bounding_box
+    }
 
     /// Element's "layer" bounding box in object units.
     ///
@@ -948,35 +1174,15 @@ pub struct Group {
     /// Unlike other bounding boxes, cannot have zero size.
     ///
     /// Will be set after calling `usvg::Tree::postprocess`.
-    pub layer_bounding_box: Option<NonZeroRect>,
+    pub fn layer_bounding_box(&self) -> Option<NonZeroRect> {
+        self.layer_bounding_box
+    }
 
     /// Group's children.
-    pub children: Vec<Node>,
-}
-
-impl Default for Group {
-    fn default() -> Self {
-        Group {
-            id: String::new(),
-            transform: Transform::default(),
-            abs_transform: Transform::default(),
-            opacity: Opacity::ONE,
-            blend_mode: BlendMode::Normal,
-            isolate: false,
-            clip_path: None,
-            mask: None,
-            filters: Vec::new(),
-            bounding_box: None,
-            abs_bounding_box: None,
-            stroke_bounding_box: None,
-            abs_stroke_bounding_box: None,
-            layer_bounding_box: None,
-            children: Vec::new(),
-        }
+    pub fn children(&self) -> &[Node] {
+        &self.children
     }
-}
 
-impl Group {
     /// Checks if this group should be isolated during rendering.
     pub fn should_isolate(&self) -> bool {
         self.isolate
@@ -1103,21 +1309,62 @@ impl Default for PaintOrder {
 /// A path element.
 #[derive(Clone, Debug)]
 pub struct Path {
+    pub(crate) id: String,
+    pub(crate) visibility: Visibility,
+    pub(crate) fill: Option<Fill>,
+    pub(crate) stroke: Option<Stroke>,
+    pub(crate) paint_order: PaintOrder,
+    pub(crate) rendering_mode: ShapeRendering,
+    pub(crate) data: Rc<tiny_skia_path::Path>,
+    pub(crate) abs_transform: Transform,
+    pub(crate) bounding_box: Option<Rect>,
+    pub(crate) abs_bounding_box: Option<Rect>,
+    pub(crate) stroke_bounding_box: Option<NonZeroRect>,
+    pub(crate) abs_stroke_bounding_box: Option<NonZeroRect>,
+}
+
+impl Path {
+    /// Creates a new `Path` with default values.
+    pub(crate) fn with_path(data: Rc<tiny_skia_path::Path>) -> Self {
+        Path {
+            id: String::new(),
+            visibility: Visibility::Visible,
+            fill: None,
+            stroke: None,
+            paint_order: PaintOrder::default(),
+            rendering_mode: ShapeRendering::default(),
+            data,
+            abs_transform: Transform::default(),
+            bounding_box: None,
+            abs_bounding_box: None,
+            stroke_bounding_box: None,
+            abs_stroke_bounding_box: None,
+        }
+    }
+
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Isn't automatically generated.
     /// Can be empty.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Element visibility.
-    pub visibility: Visibility,
+    pub fn visibility(&self) -> Visibility {
+        self.visibility
+    }
 
     /// Fill style.
-    pub fill: Option<Fill>,
+    pub fn fill(&self) -> Option<&Fill> {
+        self.fill.as_ref()
+    }
 
     /// Stroke style.
-    pub stroke: Option<Stroke>,
+    pub fn stroke(&self) -> Option<&Stroke> {
+        self.stroke.as_ref()
+    }
 
     /// Fill and stroke paint order.
     ///
@@ -1125,17 +1372,24 @@ pub struct Path {
     /// `usvg` doesn't provide the `markers` order type. It's was already done.
     ///
     /// `paint-order` in SVG.
-    pub paint_order: PaintOrder,
+    pub fn paint_order(&self) -> PaintOrder {
+        self.paint_order
+    }
 
     /// Rendering mode.
     ///
     /// `shape-rendering` in SVG.
-    pub rendering_mode: ShapeRendering,
+    pub fn rendering_mode(&self) -> ShapeRendering {
+        self.rendering_mode
+    }
 
+    // TODO: find a better name
     /// Segments list.
     ///
     /// All segments are in absolute coordinates.
-    pub data: Rc<tiny_skia_path::Path>,
+    pub fn data(&self) -> &tiny_skia_path::Path {
+        self.data.as_ref()
+    }
 
     /// Element's absolute transform.
     ///
@@ -1145,60 +1399,50 @@ pub struct Path {
     ///
     /// Note that this is not the relative transform present in SVG.
     /// The SVG one would be set only on groups.
-    pub abs_transform: Transform,
+    pub fn abs_transform(&self) -> Transform {
+        self.abs_transform
+    }
 
     /// Element's object bounding box.
     ///
     /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms.
     ///
     /// Will be set after calling `usvg::Tree::postprocess`.
-    pub bounding_box: Option<Rect>,
+    pub fn bounding_box(&self) -> Option<Rect> {
+        self.bounding_box
+    }
 
     /// Element's bounding box in canvas coordinates.
     ///
     /// `userSpaceOnUse` in SVG terms.
     ///
     /// Will be set after calling `usvg::Tree::postprocess`.
-    pub abs_bounding_box: Option<Rect>,
+    pub fn abs_bounding_box(&self) -> Option<Rect> {
+        self.abs_bounding_box
+    }
 
     /// Element's object bounding box including stroke.
     ///
     /// Similar to `bounding_box`, but includes stroke.
     ///
     /// Will have the same value as `bounding_box` when path has no stroke.
-    pub stroke_bounding_box: Option<NonZeroRect>,
+    pub fn stroke_bounding_box(&self) -> Option<NonZeroRect> {
+        self.stroke_bounding_box
+    }
 
     /// Element's bounding box including stroke in canvas coordinates.
     ///
     /// Similar to `abs_bounding_box`, but includes stroke.
     ///
     /// Will have the same value as `abs_bounding_box` when path has no stroke.
-    pub abs_stroke_bounding_box: Option<NonZeroRect>,
-}
-
-impl Path {
-    /// Creates a new `Path` with default values.
-    pub fn new(data: Rc<tiny_skia_path::Path>) -> Self {
-        Path {
-            id: String::new(),
-            visibility: Visibility::Visible,
-            fill: None,
-            stroke: None,
-            paint_order: PaintOrder::default(),
-            rendering_mode: ShapeRendering::default(),
-            data,
-            abs_transform: Transform::default(),
-            bounding_box: None,
-            abs_bounding_box: None,
-            stroke_bounding_box: None,
-            abs_stroke_bounding_box: None,
-        }
+    pub fn abs_stroke_bounding_box(&self) -> Option<NonZeroRect> {
+        self.abs_stroke_bounding_box
     }
 
     /// Calculates and sets path's stroke bounding box.
     ///
     /// This operation is expensive.
-    pub fn calculate_stroke_bounding_box(&self) -> Option<NonZeroRect> {
+    pub(crate) fn calculate_stroke_bounding_box(&self) -> Option<NonZeroRect> {
         Self::calculate_stroke_bounding_box_inner(self.stroke.as_ref(), &self.data)
     }
 
@@ -1272,29 +1516,49 @@ impl std::fmt::Debug for ImageKind {
 /// `image` element in SVG.
 #[derive(Clone, Debug)]
 pub struct Image {
+    pub(crate) id: String,
+    pub(crate) visibility: Visibility,
+    pub(crate) view_box: ViewBox,
+    pub(crate) rendering_mode: ImageRendering,
+    pub(crate) kind: ImageKind,
+    pub(crate) abs_transform: Transform,
+    pub(crate) bounding_box: Option<NonZeroRect>,
+}
+
+impl Image {
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Isn't automatically generated.
     /// Can be empty.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Element visibility.
-    pub visibility: Visibility,
+    pub fn visibility(&self) -> Visibility {
+        self.visibility
+    }
 
     /// An image rectangle in which it should be fit.
     ///
     /// Combination of the `x`, `y`, `width`, `height` and `preserveAspectRatio`
     /// attributes.
-    pub view_box: ViewBox,
+    pub fn view_box(&self) -> ViewBox {
+        self.view_box
+    }
 
     /// Rendering mode.
     ///
     /// `image-rendering` in SVG.
-    pub rendering_mode: ImageRendering,
+    pub fn rendering_mode(&self) -> ImageRendering {
+        self.rendering_mode
+    }
 
     /// Image data.
-    pub kind: ImageKind,
+    pub fn kind(&self) -> &ImageKind {
+        &self.kind
+    }
 
     /// Element's absolute transform.
     ///
@@ -1304,17 +1568,19 @@ pub struct Image {
     ///
     /// Note that this is not the relative transform present in SVG.
     /// The SVG one would be set only on groups.
-    pub abs_transform: Transform,
+    pub fn abs_transform(&self) -> Transform {
+        self.abs_transform
+    }
 
     /// Element's object bounding box.
     ///
     /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms.
     ///
     /// Will be set after calling `usvg::Tree::postprocess`.
-    pub bounding_box: Option<NonZeroRect>,
-}
+    pub fn bounding_box(&self) -> Option<NonZeroRect> {
+        self.bounding_box
+    }
 
-impl Image {
     fn abs_bounding_box(&self) -> Option<NonZeroRect> {
         self.bounding_box
             .and_then(|r| r.transform(self.abs_transform))
@@ -1337,25 +1603,35 @@ impl Image {
 #[allow(missing_debug_implementations)]
 #[derive(Clone, Debug)]
 pub struct Tree {
+    pub(crate) size: Size,
+    pub(crate) view_box: ViewBox,
+    pub(crate) root: Group,
+}
+
+impl Tree {
     /// Image size.
     ///
     /// Size of an image that should be created to fit the SVG.
     ///
     /// `width` and `height` in SVG.
-    pub size: Size,
+    pub fn size(&self) -> Size {
+        self.size
+    }
 
     /// SVG viewbox.
     ///
     /// Specifies which part of the SVG image should be rendered.
     ///
     /// `viewBox` and `preserveAspectRatio` in SVG.
-    pub view_box: ViewBox,
+    pub fn view_box(&self) -> ViewBox {
+        self.view_box
+    }
 
     /// The root element of the SVG tree.
-    pub root: Group,
-}
+    pub fn root(&self) -> &Group {
+        &self.root
+    }
 
-impl Tree {
     /// Returns a renderable node by ID.
     ///
     /// If an empty ID is provided, than this method will always return `None`.
@@ -1403,14 +1679,14 @@ impl Tree {
     /// Calculates absolute transforms for all nodes in the tree.
     ///
     /// A low-level method. Prefer `usvg::Tree::postprocess` instead.
-    pub fn calculate_abs_transforms(&mut self) {
+    pub(crate) fn calculate_abs_transforms(&mut self) {
         self.root.calculate_abs_transforms(Transform::identity());
     }
 
     /// Calculates bounding boxes for all nodes in the tree.
     ///
     /// A low-level method. Prefer `usvg::Tree::postprocess` instead.
-    pub fn calculate_bounding_boxes(&mut self) {
+    pub(crate) fn calculate_bounding_boxes(&mut self) {
         self.root.calculate_bounding_boxes();
     }
 
@@ -1592,7 +1868,7 @@ impl Group {
     /// Calculates absolute transforms for all children of this group.
     ///
     /// A low-level method. Prefer `usvg::Tree::postprocess` instead.
-    pub fn calculate_abs_transforms(&mut self, transform: Transform) {
+    pub(crate) fn calculate_abs_transforms(&mut self, transform: Transform) {
         for node in &mut self.children {
             match node {
                 Node::Group(ref mut group) => {
@@ -1613,7 +1889,7 @@ impl Group {
     /// Calculates bounding boxes for all children of this group.
     ///
     /// A low-level method. Prefer `usvg::Tree::postprocess` instead.
-    pub fn calculate_bounding_boxes(&mut self) {
+    pub(crate) fn calculate_bounding_boxes(&mut self) {
         for node in &mut self.children {
             match node {
                 Node::Path(ref mut path) => {
diff --git a/crates/usvg/src/tree/text.rs b/crates/usvg/src/tree/text.rs
index b4e487663..ebcf330c1 100644
--- a/crates/usvg/src/tree/text.rs
+++ b/crates/usvg/src/tree/text.rs
@@ -53,16 +53,34 @@ impl Default for FontStyle {
 /// Text font properties.
 #[derive(Clone, Eq, PartialEq, Hash, Debug)]
 pub struct Font {
+    pub(crate) families: Vec<FontFamily>,
+    pub(crate) style: FontStyle,
+    pub(crate) stretch: FontStretch,
+    pub(crate) weight: u16,
+}
+
+impl Font {
     /// A list of family names.
     ///
     /// Never empty. Uses `usvg::Options::font_family` as fallback.
-    pub families: Vec<FontFamily>,
+    pub fn families(&self) -> &[FontFamily] {
+        &self.families
+    }
+
     /// A font style.
-    pub style: FontStyle,
+    pub fn style(&self) -> FontStyle {
+        self.style
+    }
+
     /// A font stretch.
-    pub stretch: FontStretch,
+    pub fn stretch(&self) -> FontStretch {
+        self.stretch
+    }
+
     /// A font width.
-    pub weight: u16,
+    pub fn weight(&self) -> u16 {
+        self.weight
+    }
 }
 
 /// A dominant baseline property.
@@ -152,21 +170,45 @@ impl Default for LengthAdjust {
 /// Also, in SVG you can specify text decoration stroking.
 #[derive(Clone, Debug)]
 pub struct TextDecorationStyle {
+    pub(crate) fill: Option<Fill>,
+    pub(crate) stroke: Option<Stroke>,
+}
+
+impl TextDecorationStyle {
     /// A fill style.
-    pub fill: Option<Fill>,
+    pub fn fill(&self) -> Option<&Fill> {
+        self.fill.as_ref()
+    }
+
     /// A stroke style.
-    pub stroke: Option<Stroke>,
+    pub fn stroke(&self) -> Option<&Stroke> {
+        self.stroke.as_ref()
+    }
 }
 
 /// A text span decoration.
 #[derive(Clone, Debug)]
 pub struct TextDecoration {
+    pub(crate) underline: Option<TextDecorationStyle>,
+    pub(crate) overline: Option<TextDecorationStyle>,
+    pub(crate) line_through: Option<TextDecorationStyle>,
+}
+
+impl TextDecoration {
     /// An optional underline and its style.
-    pub underline: Option<TextDecorationStyle>,
+    pub fn underline(&self) -> Option<&TextDecorationStyle> {
+        self.underline.as_ref()
+    }
+
     /// An optional overline and its style.
-    pub overline: Option<TextDecorationStyle>,
+    pub fn overline(&self) -> Option<&TextDecorationStyle> {
+        self.overline.as_ref()
+    }
+
     /// An optional line-through and its style.
-    pub line_through: Option<TextDecorationStyle>,
+    pub fn line_through(&self) -> Option<&TextDecorationStyle> {
+        self.line_through.as_ref()
+    }
 }
 
 /// A text style span.
@@ -174,52 +216,126 @@ pub struct TextDecoration {
 /// Spans do not overlap inside a text chunk.
 #[derive(Clone, Debug)]
 pub struct TextSpan {
+    pub(crate) start: usize,
+    pub(crate) end: usize,
+    pub(crate) fill: Option<Fill>,
+    pub(crate) stroke: Option<Stroke>,
+    pub(crate) paint_order: PaintOrder,
+    pub(crate) font: Font,
+    pub(crate) font_size: NonZeroPositiveF32,
+    pub(crate) small_caps: bool,
+    pub(crate) apply_kerning: bool,
+    pub(crate) decoration: TextDecoration,
+    pub(crate) dominant_baseline: DominantBaseline,
+    pub(crate) alignment_baseline: AlignmentBaseline,
+    pub(crate) baseline_shift: Vec<BaselineShift>,
+    pub(crate) visibility: Visibility,
+    pub(crate) letter_spacing: f32,
+    pub(crate) word_spacing: f32,
+    pub(crate) text_length: Option<f32>,
+    pub(crate) length_adjust: LengthAdjust,
+}
+
+impl TextSpan {
     /// A span start in bytes.
     ///
     /// Offset is relative to the parent text chunk and not the parent text element.
-    pub start: usize,
+    pub fn start(&self) -> usize {
+        self.start
+    }
+
     /// A span end in bytes.
     ///
     /// Offset is relative to the parent text chunk and not the parent text element.
-    pub end: usize,
+    pub fn end(&self) -> usize {
+        self.end
+    }
+
     /// A fill style.
-    pub fill: Option<Fill>,
+    pub fn fill(&self) -> Option<&Fill> {
+        self.fill.as_ref()
+    }
+
     /// A stroke style.
-    pub stroke: Option<Stroke>,
+    pub fn stroke(&self) -> Option<&Stroke> {
+        self.stroke.as_ref()
+    }
+
     /// A paint order style.
-    pub paint_order: PaintOrder,
+    pub fn paint_order(&self) -> PaintOrder {
+        self.paint_order
+    }
+
     /// A font.
-    pub font: Font,
+    pub fn font(&self) -> &Font {
+        &self.font
+    }
+
     /// A font size.
-    pub font_size: NonZeroPositiveF32,
+    pub fn font_size(&self) -> NonZeroPositiveF32 {
+        self.font_size
+    }
+
     /// Indicates that small caps should be used.
     ///
     /// Set by `font-variant="small-caps"`
-    pub small_caps: bool,
+    pub fn small_caps(&self) -> bool {
+        self.small_caps
+    }
+
     /// Indicates that a kerning should be applied.
     ///
     /// Supports both `kerning` and `font-kerning` properties.
-    pub apply_kerning: bool,
+    pub fn apply_kerning(&self) -> bool {
+        self.apply_kerning
+    }
+
     /// A span decorations.
-    pub decoration: TextDecoration,
+    pub fn decoration(&self) -> &TextDecoration {
+        &self.decoration
+    }
+
     /// A span dominant baseline.
-    pub dominant_baseline: DominantBaseline,
+    pub fn dominant_baseline(&self) -> DominantBaseline {
+        self.dominant_baseline
+    }
+
     /// A span alignment baseline.
-    pub alignment_baseline: AlignmentBaseline,
+    pub fn alignment_baseline(&self) -> AlignmentBaseline {
+        self.alignment_baseline
+    }
+
     /// A list of all baseline shift that should be applied to this span.
     ///
     /// Ordered from `text` element down to the actual `span` element.
-    pub baseline_shift: Vec<BaselineShift>,
+    pub fn baseline_shift(&self) -> &[BaselineShift] {
+        &self.baseline_shift
+    }
+
     /// A visibility property.
-    pub visibility: Visibility,
+    pub fn visibility(&self) -> Visibility {
+        self.visibility
+    }
+
     /// A letter spacing property.
-    pub letter_spacing: f32,
+    pub fn letter_spacing(&self) -> f32 {
+        self.letter_spacing
+    }
+
     /// A word spacing property.
-    pub word_spacing: f32,
+    pub fn word_spacing(&self) -> f32 {
+        self.word_spacing
+    }
+
     /// A text length property.
-    pub text_length: Option<f32>,
+    pub fn text_length(&self) -> Option<f32> {
+        self.text_length
+    }
+
     /// A length adjust property.
-    pub length_adjust: LengthAdjust,
+    pub fn length_adjust(&self) -> LengthAdjust {
+        self.length_adjust
+    }
 }
 
 /// A text chunk anchor property.
@@ -240,18 +356,30 @@ impl Default for TextAnchor {
 /// A path used by text-on-path.
 #[derive(Clone, Debug)]
 pub struct TextPath {
+    pub(crate) id: String,
+    pub(crate) start_offset: f32,
+    pub(crate) path: Rc<tiny_skia_path::Path>,
+}
+
+impl TextPath {
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// A text offset in SVG coordinates.
     ///
     /// Percentage values already resolved.
-    pub start_offset: f32,
+    pub fn start_offset(&self) -> f32 {
+        self.start_offset
+    }
 
     /// A path.
-    pub path: Rc<tiny_skia_path::Path>,
+    pub fn path(&self) -> &tiny_skia_path::Path {
+        &self.path
+    }
 }
 
 /// A text chunk flow property.
@@ -270,18 +398,44 @@ pub enum TextFlow {
 /// Text alignment and BIDI reordering can only be done inside a text chunk.
 #[derive(Clone, Debug)]
 pub struct TextChunk {
+    pub(crate) x: Option<f32>,
+    pub(crate) y: Option<f32>,
+    pub(crate) anchor: TextAnchor,
+    pub(crate) spans: Vec<TextSpan>,
+    pub(crate) text_flow: TextFlow,
+    pub(crate) text: String,
+}
+
+impl TextChunk {
     /// An absolute X axis offset.
-    pub x: Option<f32>,
+    pub fn x(&self) -> Option<f32> {
+        self.x
+    }
+
     /// An absolute Y axis offset.
-    pub y: Option<f32>,
+    pub fn y(&self) -> Option<f32> {
+        self.y
+    }
+
     /// A text anchor.
-    pub anchor: TextAnchor,
+    pub fn anchor(&self) -> TextAnchor {
+        self.anchor
+    }
+
     /// A list of text chunk style spans.
-    pub spans: Vec<TextSpan>,
+    pub fn spans(&self) -> &[TextSpan] {
+        &self.spans
+    }
+
     /// A text chunk flow.
-    pub text_flow: TextFlow,
+    pub fn text_flow(&self) -> TextFlow {
+        self.text_flow.clone()
+    }
+
     /// A text chunk actual text.
-    pub text: String,
+    pub fn text(&self) -> &str {
+        &self.text
+    }
 }
 
 /// A writing mode.
@@ -297,38 +451,68 @@ pub enum WritingMode {
 /// `text` element in SVG.
 #[derive(Clone, Debug)]
 pub struct Text {
+    pub(crate) id: String,
+    pub(crate) rendering_mode: TextRendering,
+    pub(crate) dx: Vec<f32>,
+    pub(crate) dy: Vec<f32>,
+    pub(crate) rotate: Vec<f32>,
+    pub(crate) writing_mode: WritingMode,
+    pub(crate) chunks: Vec<TextChunk>,
+    pub(crate) abs_transform: Transform,
+    pub(crate) bounding_box: Option<NonZeroRect>,
+    pub(crate) abs_bounding_box: Option<NonZeroRect>,
+    pub(crate) stroke_bounding_box: Option<NonZeroRect>,
+    pub(crate) abs_stroke_bounding_box: Option<NonZeroRect>,
+    pub(crate) flattened: Option<Box<Group>>,
+}
+
+impl Text {
     /// Element's ID.
     ///
     /// Taken from the SVG itself.
     /// Isn't automatically generated.
     /// Can be empty.
-    pub id: String,
+    pub fn id(&self) -> &str {
+        &self.id
+    }
 
     /// Rendering mode.
     ///
     /// `text-rendering` in SVG.
-    pub rendering_mode: TextRendering,
+    pub fn rendering_mode(&self) -> TextRendering {
+        self.rendering_mode
+    }
 
     /// A relative X axis offsets.
     ///
     /// One offset for each Unicode codepoint. Aka `char` in Rust.
-    pub dx: Vec<f32>,
+    pub fn dx(&self) -> &[f32] {
+        &self.dx
+    }
 
     /// A relative Y axis offsets.
     ///
     /// One offset for each Unicode codepoint. Aka `char` in Rust.
-    pub dy: Vec<f32>,
+    pub fn dy(&self) -> &[f32] {
+        &self.dy
+    }
 
     /// A list of rotation angles.
     ///
     /// One angle for each Unicode codepoint. Aka `char` in Rust.
-    pub rotate: Vec<f32>,
+    pub fn rotate(&self) -> &[f32] {
+        &self.rotate
+    }
 
     /// A writing mode.
-    pub writing_mode: WritingMode,
+    pub fn writing_mode(&self) -> WritingMode {
+        self.writing_mode
+    }
 
     /// A list of text chunks.
-    pub chunks: Vec<TextChunk>,
+    pub fn chunks(&self) -> &[TextChunk] {
+        &self.chunks
+    }
 
     /// Element's absolute transform.
     ///
@@ -338,7 +522,9 @@ pub struct Text {
     ///
     /// Note that this is not the relative transform present in SVG.
     /// The SVG one would be set only on groups.
-    pub abs_transform: Transform,
+    pub fn abs_transform(&self) -> Transform {
+        self.abs_transform
+    }
 
     /// Element's text bounding box.
     ///
@@ -353,32 +539,40 @@ pub struct Text {
     /// `usvg::PostProcessingSteps::convert_text_into_paths`.
     /// Assuming the `text` build feature of `usvg` was enabled.
     /// This is because we have to perform a text layout before calculating a bounding box.
-    pub bounding_box: Option<NonZeroRect>,
+    pub fn bounding_box(&self) -> Option<NonZeroRect> {
+        self.bounding_box
+    }
 
     /// Element's text bounding box in canvas coordinates.
     ///
     /// `userSpaceOnUse` in SVG terms.
-    pub abs_bounding_box: Option<NonZeroRect>,
+    pub fn abs_bounding_box(&self) -> Option<NonZeroRect> {
+        self.abs_bounding_box
+    }
 
     /// Element's object bounding box including stroke.
     ///
     /// Similar to `bounding_box`, but includes stroke.
     ///
     /// Will have the same value as `bounding_box` when path has no stroke.
-    pub stroke_bounding_box: Option<NonZeroRect>,
+    pub fn stroke_bounding_box(&self) -> Option<NonZeroRect> {
+        self.stroke_bounding_box
+    }
 
     /// Element's bounding box including stroke in canvas coordinates.
-    pub abs_stroke_bounding_box: Option<NonZeroRect>,
+    pub fn abs_stroke_bounding_box(&self) -> Option<NonZeroRect> {
+        self.abs_stroke_bounding_box
+    }
 
     /// Text converted into paths, ready to render.
     ///
     /// Will be set only after calling `usvg::Tree::postprocess` with
     /// `usvg::PostProcessingSteps::convert_text_into_paths`.
     /// Assuming the `text` build feature of `usvg` was enabled.
-    pub flattened: Option<Box<Group>>,
-}
+    pub fn flattened(&self) -> Option<&Group> {
+        self.flattened.as_deref()
+    }
 
-impl Text {
     pub(crate) fn subroots(&self, f: &mut dyn FnMut(&Group)) {
         if let Some(ref flattened) = self.flattened {
             f(flattened);
diff --git a/tools/explorer-thumbnailer/src/utils.rs b/tools/explorer-thumbnailer/src/utils.rs
index cf3b2835f..f3b43fb7f 100644
--- a/tools/explorer-thumbnailer/src/utils.rs
+++ b/tools/explorer-thumbnailer/src/utils.rs
@@ -43,15 +43,15 @@ pub fn render_thumbnail(tree: &Option<usvg::Tree>, cx: u32) -> Result<tiny_skia:
 
     let tree = tree.as_ref().ok_or(Error::TreeEmpty)?;
 
-    let size = if tree.size.width() > tree.size.height() {
-        tree.size.to_int_size().scale_to_width(cx)
+    let size = if tree.size().width() > tree.size().height() {
+        tree.size().to_int_size().scale_to_width(cx)
     } else {
-        tree.size.to_int_size().scale_to_height(cx)
+        tree.size().to_int_size().scale_to_height(cx)
     }.ok_or(Error::RenderError)?;
 
     let transform = tiny_skia::Transform::from_scale(
-        size.width() as f32 / tree.size.width() as f32,
-        size.height() as f32 / tree.size.height() as f32,
+        size.width() as f32 / tree.size().width() as f32,
+        size.height() as f32 / tree.size().height() as f32,
     );
 
     let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap();


Test patch:
diff --git a/crates/resvg/tests/integration/main.rs b/crates/resvg/tests/integration/main.rs
index f05077da2..c34016b87 100644
--- a/crates/resvg/tests/integration/main.rs
+++ b/crates/resvg/tests/integration/main.rs
@@ -44,11 +44,15 @@ pub fn render(name: &str) -> usize {
         tree
     };
 
-    let size = tree.size.to_int_size().scale_to_width(IMAGE_SIZE).unwrap();
+    let size = tree
+        .size()
+        .to_int_size()
+        .scale_to_width(IMAGE_SIZE)
+        .unwrap();
     let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap();
     let render_ts = tiny_skia::Transform::from_scale(
-        size.width() as f32 / tree.size.width() as f32,
-        size.height() as f32 / tree.size.height() as f32,
+        size.width() as f32 / tree.size().width() as f32,
+        size.height() as f32 / tree.size().height() as f32,
     );
     resvg::render(&tree, render_ts, &mut pixmap.as_mut());
 
@@ -94,7 +98,7 @@ pub fn render_extra_with_scale(name: &str, scale: f32) -> usize {
         tree
     };
 
-    let size = tree.size.to_int_size().scale_by(scale).unwrap();
+    let size = tree.size().to_int_size().scale_by(scale).unwrap();
     let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap();
 
     let render_ts = tiny_skia::Transform::from_scale(scale, scale);
diff --git a/crates/usvg/tests/parser.rs b/crates/usvg/tests/parser.rs
index 6339a24f7..9602f571f 100644
--- a/crates/usvg/tests/parser.rs
+++ b/crates/usvg/tests/parser.rs
@@ -11,7 +11,7 @@ fn clippath_with_invalid_child() {
 
     let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
     // clipPath is invalid and should be removed together with rect.
-    assert_eq!(tree.root.has_children(), false);
+    assert_eq!(tree.root().has_children(), false);
 }
 
 #[test]
@@ -23,11 +23,11 @@ fn simplify_paths() {
     ";
 
     let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
-    let path = &tree.root.children[0];
+    let path = &tree.root().children()[0];
     match path {
         usvg::Node::Path(ref path) => {
             // Make sure we have MLZ and not MLZZZ
-            assert_eq!(path.data.verbs().len(), 3);
+            assert_eq!(path.data().verbs().len(), 3);
         }
         _ => unreachable!(),
     };
@@ -37,7 +37,7 @@ fn simplify_paths() {
 fn size_detection_1() {
     let svg = "<svg viewBox='0 0 10 20' xmlns='http://www.w3.org/2000/svg'/>";
     let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
-    assert_eq!(tree.size, usvg::Size::from_wh(10.0, 20.0).unwrap());
+    assert_eq!(tree.size(), usvg::Size::from_wh(10.0, 20.0).unwrap());
 }
 
 #[test]
@@ -45,7 +45,7 @@ fn size_detection_2() {
     let svg =
         "<svg width='30' height='40' viewBox='0 0 10 20' xmlns='http://www.w3.org/2000/svg'/>";
     let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
-    assert_eq!(tree.size, usvg::Size::from_wh(30.0, 40.0).unwrap());
+    assert_eq!(tree.size(), usvg::Size::from_wh(30.0, 40.0).unwrap());
 }
 
 #[test]
@@ -53,7 +53,7 @@ fn size_detection_3() {
     let svg =
         "<svg width='50%' height='100%' viewBox='0 0 10 20' xmlns='http://www.w3.org/2000/svg'/>";
     let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
-    assert_eq!(tree.size, usvg::Size::from_wh(5.0, 20.0).unwrap());
+    assert_eq!(tree.size(), usvg::Size::from_wh(5.0, 20.0).unwrap());
 }
 
 #[test]
@@ -64,9 +64,9 @@ fn size_detection_4() {
     </svg>
     ";
     let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
-    assert_eq!(tree.size, usvg::Size::from_wh(36.0, 36.0).unwrap());
+    assert_eq!(tree.size(), usvg::Size::from_wh(36.0, 36.0).unwrap());
     assert_eq!(
-        tree.view_box.rect,
+        tree.view_box().rect,
         usvg::NonZeroRect::from_xywh(0.0, 0.0, 36.0, 36.0).unwrap()
     );
 }
@@ -75,7 +75,7 @@ fn size_detection_4() {
 fn size_detection_5() {
     let svg = "<svg xmlns='http://www.w3.org/2000/svg'/>";
     let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
-    assert_eq!(tree.size, usvg::Size::from_wh(100.0, 100.0).unwrap());
+    assert_eq!(tree.size(), usvg::Size::from_wh(100.0, 100.0).unwrap());
 }
 
 #[test]