Repository: linebender/resvg (language: rust)

Issue:
Incorrect dx/dy handling on 'text'
[e-tspan-027.svg](https://github.com/RazrFalcon/resvg-test-suite/blob/master/svg/e-tspan-027.svg)

Can be fixed only by a [new text layout](https://github.com/RazrFalcon/resvg/pull/117).

Depends on #109


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 466ba1c4f..1fb8cec68 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,4 +13,3 @@ env:
   matrix:
     - RESVG_QT_BACKEND=true
     - RESVG_CAIRO_BACKEND=true
-    - USVG_TESTING=true
diff --git a/BUILD.adoc b/BUILD.adoc
index 50d977a28..e97536574 100755
--- a/BUILD.adoc
+++ b/BUILD.adoc
@@ -5,7 +5,8 @@
 
 == Into
 
-*resvg* doesn't include a 2D graphics library and uses external ones via https://en.wikipedia.org/wiki/Foreign_function_interface[FFI].
+*resvg* doesn't include a 2D graphics library and uses external ones via
+https://en.wikipedia.org/wiki/Foreign_function_interface[FFI].
 Their support is implemented separately, therefore we call them _backends_.
 You can build them separately or together. +
 At the moment, there are only two backends: *Qt* and *cairo*.
@@ -26,25 +27,25 @@ cargo build --release --features="qt-backend cairo-backend"
 
 == Dependencies
 
-- The library requires the latest stable
+* The library requires the latest stable
   https://www.rust-lang.org/tools/install[Rust].
-- _Qt backend_ requires only `QtCore` and `QtGui` libraries
+* The _Qt backend_ requires only `QtCore` and `QtGui` libraries
   and the JPEG image format plugin (aka `plugins/imageformats/qjpeg`). +
   Technically, any Qt 5 version should work, but we only support Qt >= 5.6.
-
-- _cairo backend_ requires https://www.cairographics.org/[cairo] itself and https://www.pango.org/[pango] for text rendering (with the `pangocairo` glue library, which is an optional part of the `pango`). +
-  There is no specific minimal supported version at the moment.
+* The _cairo backend_ requires https://www.cairographics.org/[cairo] and `gdk-pixbuf` (part of GTK). +
+  cairo >= 1.12 and gdk-pixbuf >= 2.30
 
 == Windows
 
 === Qt backend via MSVC
 
-Install `stable-x86_64-pc-windows-msvc` https://www.rust-lang.org/tools/install[Rust] target.
+Install:
 
-Install Qt built with MSVC via an
-http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe[official installer].
+* `stable-x86_64-pc-windows-msvc` https://www.rust-lang.org/tools/install[Rust] target.
+* https://cmake.org/download/[CMake] (required to build harfbuzz).
+* Qt built with MSVC via an http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe[official installer].
 
-Build using `cmd.exe`:
+Build using MSVC `cmd.exe` shell:
 
 ```batch
 set PATH=%userprofile%\.cargo\bin;%PATH%
@@ -53,28 +54,28 @@ set QT_DIR=C:\Qt\5.12.0\msvc2015_64
 cargo.exe build --release --features "qt-backend"
 ```
 
-Instead of `msvc2015_64` you can use any other Qt MSVC build. Even 32-bit one. +
+Instead of `msvc2015_64` you can use any other Qt MSVC build. Even 32-bit one.
 We are using Qt 5.12.0 just for example.
 
 === Qt backend via MinGW
 
-Install `stable-i686-pc-windows-gnu` https://www.rust-lang.org/tools/install[Rust] target.
+Install:
 
-Install Qt built with MinGW 32-bit via an
-http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe[official installer]. +
-64-bit version, available since Qt 5.12, will not work due to https://github.com/rust-lang/rust/issues/47048[#47048].
+* `stable-x86_64-pc-windows-gnu` https://www.rust-lang.org/tools/install[Rust] target.
+* https://cmake.org/download/[CMake] (required to build harfbuzz).
+* Qt built with MinGW 64-bit via an http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe[official installer].
 
 Build using `cmd.exe`:
 
 ```batch
-set PATH=C:\Qt\5.11.3\mingw53_32\bin;C:\Qt\Tools\mingw530_32\bin;%userprofile%\.cargo\bin;%PATH%
-set QT_DIR=C:\Qt\5.11.3\mingw53_32
+set PATH=C:\Qt\5.12.0\mingw73_64\bin;C:\Qt\Tools\mingw730_64\bin;%userprofile%\.cargo\bin;%PATH%
+set QT_DIR=C:\Qt\5.12.0\mingw73_64
 
 cargo.exe build --release --features "qt-backend"
 ```
 
-Instead of `mingw53_32` you can use any other Qt mingw build. +
-We are using Qt 5.11.3 just for example.
+Instead of `mingw73_64` you can use any other Qt mingw build.
+We are using Qt 5.12.0 just for example.
 
 === cairo backend via MSYS2
 
@@ -83,11 +84,10 @@ Install `stable-x86_64-pc-windows-gnu` https://www.rust-lang.org/tools/install[R
 Install GTK+ dependencies using MSYS2 as explained
 http://gtk-rs.org/docs/requirements.html#windows[here].
 
-We do not need the whole GTK+, so we can install only `pango` (which will install
-`cairo` too) and `gdk-pixbuf2`:
+We do not need the whole GTK+, so we can install only `cargo` and `gdk-pixbuf2`:
 
 ```bash
-pacman -S mingw-w64-x86_64-pango mingw-w64-x86_64-gdk-pixbuf2
+pacman -S mingw-w64-x86_64-cairo mingw-w64-x86_64-gdk-pixbuf2
 
 cargo.exe build --release --features "cairo-backend"
 ```
@@ -112,9 +112,9 @@ PKG_CONFIG_PATH='/path_to_qt/lib/pkgconfig' cargo build --release --features "qt
 
 === cairo backend
 
-Install `cairo`, `pango` (with `pangocairo`) and `gdk-pixbuf` using your distributive package manager.
+Install `cairo` and `gdk-pixbuf` using your distributive package manager.
 
-For Ubuntu its `libpango1.0-dev` (will pull `cairo` too) and `libgdk-pixbuf2.0-dev`.
+For Ubuntu its `libcairo2-dev` and `libgdk-pixbuf2.0-dev`.
 
 ```bash
 cargo build --release --features "cairo-backend"
@@ -146,7 +146,7 @@ We are using Qt 5.12.0 just for example.
 Using https://brew.sh/[homebrew]:
 
 ```bash
-brew install cairo pango gdk-pixbuf
+brew install cairo gdk-pixbuf
 
 cargo build --release --features "cairo-backend"
 ```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31547fb9c..c03d779b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,20 +8,33 @@ This changelog also contains important changes in dependencies.
 
 ## [Unreleased]
 ### Added
+- New text layout implementation.
+- `textPath` support.
+- `writing-mode` support, aka vertical text.
+- [Text BIDI reordering](http://www.unicode.org/reports/tr9/).
+- Better text shaping.
+- Text will be converted into paths on the `usvg` side now.
+- `word-spacing` is supported for all backends now.
+- [`harfbuzz`](https://github.com/harfbuzz/harfbuzz) dependency.
 - `shape-rendering`, `text-rendering` and `image-rendering` support.
 - The `arithmetic` operator for `feComposite`.
 - (usvg) `--quiet` argument.
 
 ### Changed
 - (resvg) Do not rescale images before rendering. This is faster and better.
-- (qt-backend) Text will always be rendered as path now.
-  Previously, `QPainter::drawText` was used for simple text.
 - (usvg) An `image` element with a zero or negative size will be skipped now.
   Previously, a linked image size was used, which is incorrect.
 - Geometry primitives (`Rect`, `Size`, etc) are immutable and always valid now.
 - (usvg) The default `color-interpolation-filters` attribute will not be exported now.
 
+### Removed
+- (usvg) All text related structures and enums. Text will be converted into `Path` now.
+- `InitObject` and `init()` because they are no longer needed.
+- (c-api) `resvg_handle`, `resvg_init`, `resvg_destroy`.
+- (cairo-backend) `pango` dependency.
+
 ### Fixed
+- `letter-spacing` on cursive scripts (like Arabic).
 - (rctree) Prevent stack overflow on a huge, deeply nested SVG.
 
 ## [0.6.1] - 2019-03-16
diff --git a/Cargo.lock b/Cargo.lock
index 118b90796..79850ab60 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,6 +13,15 @@ dependencies = [
  "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "argon2rs"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
+ "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "arrayvec"
 version = "0.4.10"
@@ -47,8 +56,29 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "0.1.2"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "backtrace"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-demangle 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "backtrace-sys"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
 name = "base64"
@@ -63,6 +93,15 @@ name = "bitflags"
 version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "blake2-rfc"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "byteorder"
 version = "1.3.1"
@@ -113,6 +152,63 @@ name = "cfg-if"
 version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "core-foundation"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "core-graphics"
+version = "0.17.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "core-text"
+version = "13.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-graphics 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "crc32fast"
 version = "1.2.0"
@@ -135,13 +231,33 @@ name = "difference"
 version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "dirs"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "dwrote"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "euclid"
 version = "0.19.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "euclid_macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -154,6 +270,35 @@ dependencies = [
  "syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "expat-sys"
+version = "2.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cmake 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "failure"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "backtrace 0.3.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "failure_derive"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)",
+ "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "fern"
 version = "0.5.8"
@@ -167,14 +312,70 @@ name = "float-cmp"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "num-traits 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "float-ord"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "font-kit"
+version = "0.1.0"
+source = "git+https://github.com/pcwalton/font-kit?rev=45de005#45de005386a60a95b9889056f4080757474c9128"
+dependencies = [
+ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-graphics 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-text 13.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dwrote 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "euclid 0.19.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "freetype 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lyon_path 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memmap 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "servo-fontconfig 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "fragile"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "freetype"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "servo-freetype-sys 4.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "gdk"
 version = "0.10.0"
@@ -353,6 +554,28 @@ dependencies = [
  "syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "harfbuzz-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cmake 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-graphics 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-text 13.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "freetype 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "harfbuzz_rs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "harfbuzz-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.3.0"
@@ -388,7 +611,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.19.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "lyon_path"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lyon_geom 0.12.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "memmap"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -398,10 +643,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "num-traits"
-version = "0.2.7"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -437,35 +682,6 @@ dependencies = [
  "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "pangocairo"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "cairo-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "cairo-sys-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "glib 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "glib-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "gobject-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
- "pango 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pango-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pangocairo-sys 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "pangocairo-sys"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "cairo-sys-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "glib-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
- "pango-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "phf"
 version = "0.7.24"
@@ -514,16 +730,61 @@ dependencies = [
  "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rctree"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.1.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "redox_users"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rendersvg"
 version = "0.6.1"
@@ -542,8 +803,6 @@ dependencies = [
  "cairo-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gdk-pixbuf 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "pango 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pangocairo 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "resvg-qt 0.6.1",
  "rgb 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)",
  "usvg 0.6.1",
@@ -580,6 +839,52 @@ dependencies = [
  "xmlparser 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rustc-demangle"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "same-file"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "servo-fontconfig"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "servo-fontconfig-sys 4.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "servo-fontconfig-sys"
+version = "4.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "expat-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "servo-freetype-sys 4.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "servo-freetype-sys"
+version = "4.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cmake 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "simplecss"
 version = "0.1.0"
@@ -598,7 +903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 [[package]]
 name = "svgdom"
 version = "0.16.1"
-source = "git+https://github.com/RazrFalcon/svgdom?rev=f12882c#f12882cb05704d633af404fd09b41ec0127d73ee"
+source = "git+https://github.com/RazrFalcon/svgdom#3844582cbd11de698838deaf289be8d12548da2b"
 dependencies = [
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "roxmltree 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -636,6 +941,17 @@ dependencies = [
  "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "synstructure"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "time"
 version = "0.1.42"
@@ -646,6 +962,24 @@ dependencies = [
  "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "unicode-bidi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "unicode-script"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
+source = "git+https://github.com/RazrFalcon/unicode-vo#f163f6cc40436cf793742607048273fbb8834fbd"
+
 [[package]]
 name = "unicode-xid"
 version = "0.1.0"
@@ -656,12 +990,18 @@ name = "usvg"
 version = "0.6.1"
 dependencies = [
  "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "font-kit 0.1.0 (git+https://github.com/pcwalton/font-kit?rev=45de005)",
+ "harfbuzz_rs 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libflate 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "lyon_geom 0.12.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lyon_path 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "rctree 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "svgdom 0.16.1 (git+https://github.com/RazrFalcon/svgdom?rev=f12882c)",
+ "svgdom 0.16.1 (git+https://github.com/RazrFalcon/svgdom)",
+ "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-script 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-vo 0.1.0 (git+https://github.com/RazrFalcon/unicode-vo)",
 ]
 
 [[package]]
@@ -674,6 +1014,16 @@ dependencies = [
  "usvg 0.6.1",
 ]
 
+[[package]]
+name = "walkdir"
+version = "2.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.7"
@@ -688,6 +1038,14 @@ name = "winapi-i686-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
@@ -701,25 +1059,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 [metadata]
 "checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
 "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+"checksum argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392"
 "checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71"
 "checksum atk 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7ce8fd2055aa2ce617754c5a701682a06b4b57093cbbcbd8f068622a8debae96"
 "checksum atk-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83744274abbcc2242f845a25cf84b9adb5991eaecc8f7c67cffd69ea636f4ff8"
-"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
+"checksum autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf"
+"checksum backtrace 0.3.26 (registry+https://github.com/rust-lang/crates.io-index)" = "1a13fc43f04daf08ab4f71e3d27e1fc27fc437d3e95ac0063a796d92fb40f39b"
+"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
 "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
 "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
+"checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400"
 "checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
 "checksum cairo-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9e09d8a818b2ccc8983f04d95a9350c3cf8d24cc456cedca3b88fa3a81fdc0e2"
 "checksum cairo-sys-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3fa13914fdc013387afa771f554f2f71d6ae931f4e5be9246c337d60c3dc484"
 "checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d"
 "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
+"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+"checksum cmake 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "2ca4386c8954b76a8415b63959337d940d724b336cabd3afe189c2b51a7e1ff0"
+"checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e"
+"checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d"
+"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
+"checksum core-graphics 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)" = "56790968ab1c8a1202a102e6de05fc6e1ec87da99e4e93e9a7d13efbfc1e95a9"
+"checksum core-text 13.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d95a72b5e50e549969dd88eff3047495fe5b8c6f028635442c2b708be707e669"
 "checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
 "checksum ctor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3b4c17619643c1252b5f690084b82639dd7fac141c57c8e77a00e0148132092c"
 "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+"checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
+"checksum dwrote 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0bd1369e02db5e9b842a9b67bce8a2fcc043beafb2ae8a799dd482d46ea1ff0d"
 "checksum euclid 0.19.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7a4719a544a67ed3fc33784c2bd2c6581663dfe83b719a6ae05c6dabc3b51c73"
 "checksum euclid_macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdcb84c18ea5037a1c5a23039b4ff29403abce2e0d6b1daa11cf0bde2b30be15"
+"checksum expat-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "658f19728920138342f68408b7cf7644d90d4784353d8ebc32e7e8663dbe45fa"
+"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
+"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
 "checksum fern 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)" = "29d26fa0f4d433d1956746e66ec10d6bf4d6c8b93cd39965cceea7f7cc78c7dd"
 "checksum float-cmp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "134a8fa843d80a51a5b77d36d42bc2def9edcb0262c914861d08129fd1926600"
+"checksum float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
+"checksum font-kit 0.1.0 (git+https://github.com/pcwalton/font-kit?rev=45de005)" = "<none>"
+"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 "checksum fragile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f8140122fa0d5dcb9fc8627cfce2b37cc1500f752636d46ea28bc26785c2f9"
+"checksum freetype 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "11926b2b410b469d0e9399eca4cbbe237a9ef02176c485803b29216307e8e028"
+"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 "checksum gdk 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c229b642a3cf701f92246b131fda2eaabaa5f44f4a255dee65939ebd0ab0141"
 "checksum gdk-pixbuf 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9a1bcb11b461ff5638f6bf03c9f173d3dc250eabc8e57a60644b6f529d3a7c91"
 "checksum gdk-pixbuf-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "10e1115ca451907b6d775fc0392641d14c89277662d98828412fb68555e7e8c8"
@@ -733,38 +1113,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum gtk-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "849835f05d46f605b00ee9fefb6d5921238c570af406f35ee7426ed01cd6795c"
 "checksum gumdrop 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "395c7c79599167d04e7349ee60cc6cb14a28dfebaf922b4dc41603a7b18c3b28"
 "checksum gumdrop_derive 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "04c43663fd6604d9065f5cd26ae7a0301491fd941ad29b0a01665a6be3a1ddbd"
+"checksum harfbuzz-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e1042ab0b3e7bc1ff64f7f5935778b644ff2194a1cae5ec52167127d3fd23961"
+"checksum harfbuzz_rs 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "534c8e9b15d8db6e69654b07dad955f4132757194e7d2bba620d38cf08996088"
 "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
 "checksum libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "42914d39aad277d9e176efbdad68acb1d5443ab65afe0e0e4f0d49352a950880"
 "checksum libflate 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c52384aeb22d0ce82a10d8ddf35f7fb4717d1b23eac5b94cd38d2050fb53766a"
 "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
 "checksum lyon_geom 0.12.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0638070e85f0e8b5da3909d8c5c0a66785b18da96a1e8dc30acb0aea9770529a"
+"checksum lyon_path 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e9dc8e0746b7cca11960b602f7fe037bb067746a01eab4aa502fed1494544843"
+"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+"checksum memmap 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e2ffa2c986de11a9df78620c01eeaaf27d94d3ff02bf81bfcca953102dd0c6ff"
 "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
-"checksum num-traits 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "d9c79c952a4a139f44a0fe205c4ee66ce239c0e6ce72cd935f5f7e2f717549dd"
+"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
 "checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
 "checksum pango 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4e36b55d7cd522bd183efeb3dfabc547bda1f26eadf8a1241dac09ab3fd3242c"
 "checksum pango-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "747ab9cd4d537e6dc5ef0e4308c10dde8b706673b0237fed4e056b8d2c0b23c8"
-"checksum pangocairo 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "706e885b46a16ba96f46f3c8d880ca6082c2c62693b75f3d167bd3aa4e34b0d4"
-"checksum pangocairo-sys 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "868310e83da5269323c28bd47b641557d032426f396cddfa45e247ff44da9a12"
 "checksum phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
 "checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
 "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
 "checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
 "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
 "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
+"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0"
+"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
 "checksum rctree 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7365791774a0568ef20c86f872222c022ffd8efb27a58be074649b5342a38a30"
+"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
 "checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252"
+"checksum redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe5204c3a17e97dde73f285d49be585df59ed84b50a872baf416e73b62c3828"
 "checksum rgb 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)" = "4f089652ca87f5a82a62935ec6172a534066c7b97be003cc8f702ee9a7a59c92"
 "checksum roxmltree 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "53b0200cbfa8b3f6cfd6076592717d697a1ddc57cb2a8fbfd3d133c06011b579"
+"checksum rustc-demangle 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "ccc78bfd5acd7bf3e89cffcf899e5cb1a52d6fafa8dec2739ad70c9577a57288"
+"checksum same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8f20c4be53a8a1ff4c1f1b2bd14570d2f634628709752f0702ecdd2b3f9a5267"
+"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+"checksum servo-fontconfig 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a088f8d775a5c5314aae09bd77340bc9c67d72b9a45258be34c83548b4814cd9"
+"checksum servo-fontconfig-sys 4.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b46d201addcfbd25c1798ad1281d98c40743824e0b0f1e611bd3d5d0d31a7b8d"
+"checksum servo-freetype-sys 4.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4ccb6d0d32d277d3ef7dea86203d8210945eb7a45fba89dd445b3595dd0dfc"
 "checksum simplecss 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "135685097a85a64067df36e28a243e94a94f76d829087ce0be34eeb014260c0e"
 "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
 "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
-"checksum svgdom 0.16.1 (git+https://github.com/RazrFalcon/svgdom?rev=f12882c)" = "<none>"
+"checksum svgdom 0.16.1 (git+https://github.com/RazrFalcon/svgdom)" = "<none>"
 "checksum svgtypes 0.4.2 (git+https://github.com/RazrFalcon/svgtypes?rev=54eabc9)" = "<none>"
 "checksum syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)" = "261ae9ecaa397c42b960649561949d69311f08eeaea86a65696e6e46517cf741"
 "checksum syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)" = "a1393e4a97a19c01e900df2aec855a29f71cf02c402e2f443b8d2747c25c5dbe"
+"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f"
 "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
+"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+"checksum unicode-script 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "09f03ad95feb4fde244d79985bfd79eb34ff2702fedb441d2ba3f4ff813efd19"
+"checksum unicode-vo 0.1.0 (git+https://github.com/RazrFalcon/unicode-vo)" = "<none>"
 "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+"checksum walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d9d7ed3431229a144296213105a390676cc49c9b6a72bd19f3176c98e129fa1"
 "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
 "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
 "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 "checksum xmlparser 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ecec95f00fb0ff019153e64ea520f87d1409769db3e8f4db3ea588638a3e1cee"
diff --git a/Cargo.toml b/Cargo.toml
index bc3447f4f..dea6fa35f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,14 +32,12 @@ usvg = { path = "usvg", version = "0.6" }
 # cairo backend
 cairo-rs = { version = "0.6", features = ["png"], optional = true }
 gdk-pixbuf = { version = "0.6", optional = true }
-pango = { version = "0.6", optional = true }
-pangocairo = { version = "0.7", optional = true }
 
 # qt backend
 resvg-qt = { path = "resvg-qt", version = "0.6", optional = true }
 
 [features]
-cairo-backend = ["cairo-rs", "gdk-pixbuf", "pango", "pangocairo"]
+cairo-backend = ["cairo-rs", "gdk-pixbuf"]
 qt-backend = ["resvg-qt"]
 
 [package.metadata.docs.rs]
diff --git a/README.md b/README.md
index f856f7724..d4bfd1ccd 100644
--- a/README.md
+++ b/README.md
@@ -64,9 +64,7 @@ Also, it's [deprecated](https://wiki.qt.io/Qt_Modules_Maturity_Level).
 *resvg* is aiming to support only the [static](http://www.w3.org/TR/SVG11/feature#SVG-static)
 SVG subset; e.g. no `a`, `script`, `view` or `cursor` elements, no events and no animations.
 
-Also, `textPath` and
-[embedded fonts](https://www.w3.org/TR/SVG11/feature#Font) are not yet implemented.
-A full list can be found [here](docs/unsupported.md).
+A list of unsupported features can be found [here](docs/unsupported.md).
 
 [SVG Tiny 1.2](https://www.w3.org/TR/SVGTiny12/) and [SVG 2.0](https://www.w3.org/TR/SVG2/)
 are not supported and not planned.
diff --git a/capi/include/resvg.h b/capi/include/resvg.h
index 46329c043..1dddd6189 100644
--- a/capi/include/resvg.h
+++ b/capi/include/resvg.h
@@ -28,23 +28,6 @@
 #define RESVG_VERSION "0.6.1"
 
 
-/**
- * @brief An opaque pointer to the global library handle.
- *
- * Must be invoked before any other \b resvg code.
- *
- * Currently, handles \b QGuiApplication object which must be created
- * in order to draw text. If you don't plan to draw text - it's better to skip
- * the initialization.
- *
- * If you are using this library from an existing Qt application you can skip it.
- *
- * Does nothing when only \b cairo backend is enabled/used.
- *
- * \b Note: \b QGuiApplication initialization is pretty slow (up to 100ms).
- */
-typedef struct resvg_handle resvg_handle;
-
 /**
  * @brief An opaque pointer to the rendering tree.
  */
@@ -259,22 +242,6 @@ typedef struct resvg_transform {
     double f; /**< \b f value */
 } resvg_transform;
 
-/**
- * @brief Initializes the library.
- *
- * See #resvg_handle for details.
- *
- * @return Library handle.
- */
-resvg_handle* resvg_init();
-
-/**
- * @brief Destroys the #resvg_handle.
- *
- * @param handle Library handle.
- */
-void resvg_destroy(resvg_handle *handle);
-
 /**
  * @brief Initializes the library log.
  *
diff --git a/capi/src/lib.rs b/capi/src/lib.rs
index 315af0262..5ce2a4196 100644
--- a/capi/src/lib.rs
+++ b/capi/src/lib.rs
@@ -113,24 +113,6 @@ pub struct resvg_transform {
 #[repr(C)]
 pub struct resvg_render_tree(usvg::Tree);
 
-#[repr(C)]
-pub struct resvg_handle(resvg::InitObject);
-
-
-#[no_mangle]
-pub extern fn resvg_init() -> *mut resvg_handle {
-    let handle = Box::new(resvg_handle(resvg::init()));
-    Box::into_raw(handle)
-}
-
-#[no_mangle]
-pub extern fn resvg_destroy(handle: *mut resvg_handle) {
-    unsafe {
-        assert!(!handle.is_null());
-        Box::from_raw(handle)
-    };
-}
-
 #[no_mangle]
 pub extern fn resvg_init_log() {
     fern::Dispatch::new()
diff --git a/docs/backend_requirements.md b/docs/backend_requirements.md
index 8f021653a..684ff829e 100644
--- a/docs/backend_requirements.md
+++ b/docs/backend_requirements.md
@@ -40,23 +40,6 @@ List of features required from the 2D graphics library to implement a backend fo
     - LineTo
     - CurveTo (cubic)
     - ClosePath
-- Text
-  - Anti-aliasing
-  - Font properties:
-    - Family resolving (like `'Timer New Roman', monospace`)
-    - Style: *normal*, *italic* and *oblique*
-    - Weight
-    - Stretch
-    - Variant: *normal*, *small cap*
-    - Size
-    - Letter spacing
-    - Word spacing
-  - Font metrics:
-    - Text bounding box
-    - Ascent/baseline
-    - Overline position and thickness (optional)
-    - Strikeout position and thickness (optional)
-    - Underline position and thickness (optional)
 - Raster images:
   - Formats:
     - PNG
diff --git a/docs/unsupported.md b/docs/unsupported.md
index add786964..19b7bb485 100644
--- a/docs/unsupported.md
+++ b/docs/unsupported.md
@@ -28,7 +28,6 @@
   - `missing-glyph`
   - `vkern`
 - `color-profile`
-- `textPath`
 - `use` with a reference to an external SVG file
 
 ### Attributes
@@ -42,19 +41,17 @@
 - `dominant-baseline`
 - [`enable-background`](https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty) (deprecated in the SVG 2)
 - `font`
-- `font-variant` (unsupported only by cairo backend)
 - `font-size-adjust`
+- `font-stretch`
+- `font-variant`
 - `glyph-orientation-horizontal` (removed in the SVG 2)
 - `glyph-orientation-vertical` (deprecated in the SVG 2)
 - [`in`](https://www.w3.org/TR/SVG11/filters.html#FilterPrimitiveInAttribute)
   with `BackgroundImage`, `BackgroundAlpha`, `FillPaint`, `StrokePaint`
 - `kerning` (removed in the SVG 2)
 - `lengthAdjust`
-- `line-height`
 - `textLength`
 - `unicode-bidi`
-- `word-spacing` (unsupported only by cairo backend)
-- `writing-mode`
 
 **Note:** this list does not include elements and attributes outside the
 [static SVG](http://www.w3.org/TR/SVG11/feature#SVG-static) subset.
diff --git a/docs/usvg_spec.adoc b/docs/usvg_spec.adoc
index aaf742f24..331e1c546 100644
--- a/docs/usvg_spec.adoc
+++ b/docs/usvg_spec.adoc
@@ -26,10 +26,6 @@ You can use
 https://github.com/RazrFalcon/resvg/tree/master/usvg[usvg]
 to convert a random SVG into a SVG Micro almost losslessly.
 
-Note: the current version supports the `text` element,
-but eventually text will be converted into paths.
-To make SVG Micro even simpler.
-
 == Elements
 
 [[svg-element]]
@@ -44,7 +40,6 @@ It's defined only once and can't be nested, unlike by the SVG spec.
 * <<defs-element,defs>>
 * <<g-element,g>>
 * <<path-element,path>>
-* <<text-element,text>>
 * <<image-element,image>>
 
 *Attributes:*
@@ -152,7 +147,6 @@ Doesn't have a `xlink:href` attribute because all attributes and children will b
 
 * `g`
 * `path`
-* `text`
 * `image`
 
 *Attributes:*
@@ -178,7 +172,6 @@ Doesn't have a `xlink:href` attribute because all attributes and children will b
 *Children:*
 
 * `path`
-* `text`
 
 *Attributes:*
 
@@ -199,7 +192,6 @@ Doesn't have a `xlink:href` attribute because all attributes and children will b
 
 * `g`
 * `path`
-* `text`
 * `image`
 
 *Attributes:*
@@ -261,7 +253,6 @@ A group will have at least one of the attributes present.
 
 * <<g-element,g>>
 * <<path-element,path>>
-* <<text-element,text>>
 * <<image-element,image>>
 
 *Attributes:*
@@ -325,111 +316,6 @@ A group will have at least one of the attributes present.
   Default: visible
 * `transform` = <<transform-type,<transform> >>?
 
-[[text-element]]
-
-=== The `text` element
-
-The text is one of the most complex parts of the SVG.
-_usvg_ will modify the `text` element and its children a lot.
-The simplest `text` element like `<text>Text</text>` will be converted into:
-
-```xml
-<text>
-  <tspan
-     x="0"
-     y="0">
-    <tspan
-       fill="#000000"
-       font-family="Times New Roman"
-       font-size="12"
-       stroke="none">
-      Text
-    </tspan>
-  </tspan>
-</text>
-```
-
-In _usvg_, the `text` element is just a container for
-https://www.w3.org/TR/SVG11/text.html#TextChunk[text chunks],
-represented via `tspan`.
-So all `text` elements will have a three-level structure:
-
-* `text` - container
-** `tspan` - text chunk
-*** `tspan` - text container
-
-*Container attributes:*
-
-* `id` = <<string-type,<string> >>? +
-  An optional, but never empty, element ID.
-* `rotate` = `<list-of-numbers>`?
-* `xml:space` = `preserve`? +
-  Default: default
-* `clip-rule` = `evenodd`? +
-  Will be set only inside the <<clipPath-element,clipPath>>, instead of `fill-rule`.
-* `clip-path` = <<func-iri-type,<FuncIRI> >>? +
-  Available only inside the <<clipPath-element,clipPath>>.
-* `text-rendering` = `optimizeSpeed | geometricPrecision` +
-  Default: optimizeLegibility
-* `transform` = <<transform-type,<transform> >>?
-
-*Text chunk attributes:*
-
-* `x` = <<number-type,<number> >>?
-* `y` = <<number-type,<number> >>?
-* `dx` = <<number-type,<number> >>?
-* `dy` = <<number-type,<number> >>?
-* `text-anchor` = `middle | end`? +
-  Default: start
-
-*Text container attributes:*
-
-* `fill` = `none` | <<color-type,<color> >> | <<func-iri-type,<FuncIRI> >>? +
-  If not set, than all fill-* attributes will not be set too. +
-  Default: black
-* `fill-opacity` = <<opacity-type,<opacity> >>? +
-  Default: 1
-* `fill-rule` = `evenodd`? +
-  Default: nonzero
-* `stroke` = <<color-type,<color> >> | <<func-iri-type,<FuncIRI> >>? +
-  If not set, than all stroke-* attributes will not be set too. +
-  Default: none
-* `stroke-width` = <<positive-number-type,<positive-number> >>? +
-  Default: 1
-* `stroke-linecap` = `round | square`? +
-  Default: butt
-* `stroke-linejoin` = `round | bevel`? +
-  Default: miter
-* `stroke-miterlimit` = <<positive-number-type,<positive-number> >>? +
-  Guarantee to be > 1. +
-  Default: 4
-* `stroke-dasharray` = `<list-of-numbers>`? +
-  Guarantee to have even amount of numbers. +
-  Default: none
-* `stroke-dashoffset` = <<number-type,<number> >>?
-* `stroke-opacity` = <<opacity-type,<opacity> >>? +
-  Default: 1
-* `font-family` = <<string-type,<string> >>
-* `font-size` = <<positive-number-type,<positive-number> >>
-* `font-style` = `italic | oblique`? +
-  Default: normal
-* `font-variant` = `small-caps`? +
-  Default: normal
-* `font-weight` = `100 | 200 | 300 | 500 | 600 | 700 | 800 | 900`? +
-  Default: 400
-* `font-stretch` = `wider | narrower | ultra-condensed | extra-condensed` +
-  `| condensed | semi-condensed | semi-expanded | expanded` +
-  `| extra-expanded | ultra-expanded` ? +
-  Default: normal
-* `baseline-shift` = <<number-type,<number> >>? +
-  Default: baseline
-* `letter-spacing` = <<number-type,<number> >>? +
-  Default: normal
-* `word-spacing` = <<number-type,<number> >>? +
-  Default: normal
-* `visibility` = `hidden | collapse`? +
-  Default: visible
-
 [[image-element]]
 
 === The `image` element
diff --git a/examples/custom_rtree.rs b/examples/custom_rtree.rs
index 5e7eb97ae..e8ebb1899 100644
--- a/examples/custom_rtree.rs
+++ b/examples/custom_rtree.rs
@@ -5,7 +5,6 @@ use resvg::prelude::*;
 // TODO: write doc
 
 fn main() {
-    let _resvg = resvg::init();
     let backend = resvg::default_backend();
 
     let opt = Options::default();
diff --git a/examples/draw_bboxes.rs b/examples/draw_bboxes.rs
index 62f74e6ce..49a91887b 100644
--- a/examples/draw_bboxes.rs
+++ b/examples/draw_bboxes.rs
@@ -14,7 +14,6 @@ fn main() {
         return;
     }
 
-    let _resvg = resvg::init();
     let backend = resvg::default_backend();
 
     let zoom = if args.len() == 5 {
diff --git a/examples/minimal.rs b/examples/minimal.rs
index a861610e1..5ee37edb6 100644
--- a/examples/minimal.rs
+++ b/examples/minimal.rs
@@ -12,8 +12,6 @@ fn main() {
         return;
     }
 
-    let _resvg = resvg::init();
-
     let mut opt = resvg::Options::default();
     opt.usvg.path = Some(args[1].clone().into());
 
diff --git a/resvg-qt/cpp/qt_capi.cpp b/resvg-qt/cpp/qt_capi.cpp
index d0b5d2ab1..f955f0d46 100644
--- a/resvg-qt/cpp/qt_capi.cpp
+++ b/resvg-qt/cpp/qt_capi.cpp
@@ -13,27 +13,9 @@
 #define BRUSH_CAST reinterpret_cast<QBrush*>(c_brush)
 #define LG_CAST reinterpret_cast<QLinearGradient*>(c_lg)
 #define RG_CAST reinterpret_cast<QRadialGradient*>(c_rg)
-#define FONT_CAST reinterpret_cast<QFont*>(c_f)
-#define FM_CAST reinterpret_cast<QFontMetricsF*>(c_fm)
 
 extern "C" {
 
-qtc_qguiapp* qtc_create_gui(char *app_name)
-{
-    if (!QGuiApplication::instance()) {
-        int argc = 1;
-        char *argv[] = {app_name};
-        new QGuiApplication(argc, argv);
-    }
-
-    return reinterpret_cast<qtc_qguiapp*>(qGuiApp);
-}
-
-void qtc_destroy_gui(qtc_qguiapp *c_app)
-{
-    delete reinterpret_cast<QGuiApplication*>(c_app);
-}
-
 // QImage
 
 qtc_qimage * qtc_qimage_create_rgba_premultiplied(uint32_t width, uint32_t height)
@@ -167,16 +149,6 @@ void qtc_qpainter_set_smooth_pixmap_transform(qtc_qpainter *c_p, bool flag)
     PAINTER_CAST->setRenderHint(QPainter::SmoothPixmapTransform, flag);
 }
 
-qtc_qfont* qtc_qpainter_get_font(qtc_qpainter *c_p)
-{
-    return reinterpret_cast<qtc_qfont*>(new QFont(PAINTER_CAST->font()));
-}
-
-void qtc_qpainter_set_font(qtc_qpainter *c_p, qtc_qfont *c_f)
-{
-    PAINTER_CAST->setFont(*FONT_CAST);
-}
-
 void qtc_qpainter_set_pen(qtc_qpainter *c_p, qtc_qpen *c_pen)
 {
     PAINTER_CAST->setPen(*PEN_CAST);
@@ -275,11 +247,6 @@ void qtc_qpainter_set_composition_mode(qtc_qpainter *c_p, CompositionMode mode)
     PAINTER_CAST->setCompositionMode(QPainter::CompositionMode(mode));
 }
 
-qtc_qfontmetricsf* qtc_qpainter_get_fontmetricsf(qtc_qpainter *c_p)
-{
-    return reinterpret_cast<qtc_qfontmetricsf*>(new QFontMetricsF(PAINTER_CAST->font()));
-}
-
 void qtc_qpainter_end(qtc_qpainter *c_p)
 {
     PAINTER_CAST->end();
@@ -318,60 +285,11 @@ void qtc_qpainterpath_close_path(qtc_qpainterpath *c_pp)
     PATH_CAST->closeSubpath();
 }
 
-void qtc_qpainterpath_add_text(qtc_qpainterpath *c_pp, double x, double y, qtc_qfont *c_f,
-                               const char *text)
-{
-    PATH_CAST->addText(x, y, *FONT_CAST, QString::fromUtf8(text));
-}
-
 void qtc_qpainterpath_set_fill_rule(qtc_qpainterpath *c_pp, FillRule rule)
 {
     PATH_CAST->setFillRule(Qt::FillRule(rule));
 }
 
-int qtc_qpainterpath_element_count(qtc_qpainterpath *c_pp)
-{
-    return PATH_CAST->elementCount();
-}
-
-PathSegment qtc_qpainterpath_element_at(qtc_qpainterpath *c_pp, int i)
-{
-    if (i >= PATH_CAST->elementCount()) {
-        // TODO: qDebug produces a compilation error on Ubuntu 14.04...
-        fprintf(stderr, "%s", "resvg-qt: Trying to get a segment out of scope.\n");
-        abort();
-    }
-
-    const auto path_seg = PATH_CAST->elementAt(i);
-
-    PathSegment seg;
-    seg.x = path_seg.x;
-    seg.y = path_seg.y;
-
-    switch (path_seg.type) {
-        case QPainterPath::MoveToElement : {
-            seg.kind = PathSegmentType::MoveToSegment;
-        } break;
-
-        case QPainterPath::LineToElement : {
-            seg.kind = PathSegmentType::LineToSegment;
-        } break;
-
-        case QPainterPath::CurveToElement :
-        case QPainterPath::CurveToDataElement : {
-            seg.kind = PathSegmentType::CurveToSegment;
-        } break;
-    }
-
-    return seg;
-}
-
-qtc_rect_f qtc_qpainterpath_get_bbox(qtc_qpainterpath *c_pp)
-{
-    const auto bbox = PATH_CAST->boundingRect();
-    return qtc_rect_f { bbox.x(), bbox.y(), bbox.width(), bbox.height() };
-}
-
 void qtc_qpainterpath_destroy(qtc_qpainterpath *c_pp)
 {
     delete PATH_CAST;
@@ -564,119 +482,4 @@ void qtc_qradialgradient_destroy(qtc_qradialgradient *c_rg)
     delete RG_CAST;
 }
 
-// QFont
-
-qtc_qfont *qtc_qfont_create()
-{
-    return reinterpret_cast<qtc_qfont*>(new QFont());
-}
-
-qtc_qfont* qtc_qfont_clone(qtc_qfont *c_f)
-{
-    return reinterpret_cast<qtc_qfont*>(new QFont(*FONT_CAST));
-}
-
-void qtc_qfont_set_family(qtc_qfont *c_f, const char *family)
-{
-    FONT_CAST->setFamily(QString::fromUtf8(family));
-}
-
-void qtc_qfont_set_style(qtc_qfont *c_f, FontStyle style)
-{
-    FONT_CAST->setStyle(QFont::Style(style));
-}
-
-void qtc_qfont_set_small_caps(qtc_qfont *c_f, bool flag)
-{
-    FONT_CAST->setCapitalization(flag ? QFont::SmallCaps : QFont::MixedCase);
-}
-
-void qtc_qfont_set_weight(qtc_qfont *c_f, FontWeight weight)
-{
-    FONT_CAST->setWeight(QFont::Weight(weight));
-}
-
-void qtc_qfont_set_stretch(qtc_qfont *c_f, FontStretch stretch)
-{
-    FONT_CAST->setStretch(QFont::Stretch(stretch));
-}
-
-void qtc_qfont_set_size(qtc_qfont *c_f, double size)
-{
-    FONT_CAST->setPixelSize(size);
-}
-
-void qtc_qfont_set_letter_spacing(qtc_qfont *c_f, double size)
-{
-    FONT_CAST->setLetterSpacing(QFont::AbsoluteSpacing, size);
-}
-
-void qtc_qfont_set_word_spacing(qtc_qfont *c_f, double size)
-{
-    FONT_CAST->setWordSpacing(size);
-}
-
-void qtc_qfont_print_debug(qtc_qfont *c_f)
-{
-    qDebug() << *FONT_CAST;
-}
-
-void qtc_qfont_destroy(qtc_qfont *c_f)
-{
-    delete FONT_CAST;
-}
-
-// QFontMetricsF
-
-double qtc_qfontmetricsf_height(qtc_qfontmetricsf *c_fm)
-{
-    return FM_CAST->height();
-}
-
-double qtc_qfontmetricsf_width(qtc_qfontmetricsf *c_fm, const char *text)
-{
-    return FM_CAST->width(QString::fromUtf8(text));
-}
-
-double qtc_qfontmetricsf_full_width(qtc_qfontmetricsf *c_fm, const char *text)
-{
-    return FM_CAST->boundingRect(QString::fromUtf8(text)).width();
-}
-
-qtc_rect_f qtc_qfontmetricsf_get_bbox(qtc_qfontmetricsf *c_fm, const char *text)
-{
-    const auto bbox = FM_CAST->boundingRect(QString::fromUtf8(text));
-    return qtc_rect_f { bbox.x(), bbox.y(), bbox.width(), bbox.height() };
-}
-
-double qtc_qfontmetricsf_get_ascent(qtc_qfontmetricsf *c_fm)
-{
-    return FM_CAST->ascent();
-}
-
-double qtc_qfontmetricsf_get_underline_pos(qtc_qfontmetricsf *c_fm)
-{
-    return FM_CAST->underlinePos();
-}
-
-double qtc_qfontmetricsf_get_overline_pos(qtc_qfontmetricsf *c_fm)
-{
-    return FM_CAST->overlinePos();
-}
-
-double qtc_qfontmetricsf_get_strikeout_pos(qtc_qfontmetricsf *c_fm)
-{
-    return FM_CAST->strikeOutPos();
-}
-
-double qtc_qfontmetricsf_get_line_width(qtc_qfontmetricsf *c_fm)
-{
-    return FM_CAST->lineWidth();
-}
-
-void qtc_qfontmetricsf_destroy(qtc_qfontmetricsf *c_fm)
-{
-    delete FM_CAST;
-}
-
 }
diff --git a/resvg-qt/cpp/qt_capi.hpp b/resvg-qt/cpp/qt_capi.hpp
index fa5dfbc90..b89ea858d 100644
--- a/resvg-qt/cpp/qt_capi.hpp
+++ b/resvg-qt/cpp/qt_capi.hpp
@@ -7,7 +7,6 @@
     struct x; \
     typedef struct x x;
 
-INIT_STRUCT(qtc_qguiapp)
 INIT_STRUCT(qtc_qimage)
 INIT_STRUCT(qtc_qpainter)
 INIT_STRUCT(qtc_qpainterpath)
@@ -16,18 +15,9 @@ INIT_STRUCT(qtc_qpen)
 INIT_STRUCT(qtc_qbrush)
 INIT_STRUCT(qtc_qlineargradient)
 INIT_STRUCT(qtc_qradialgradient)
-INIT_STRUCT(qtc_qfont)
-INIT_STRUCT(qtc_qfontmetricsf)
 
 #undef INIT_STRUCT
 
-struct qtc_rect_f {
-    double x;
-    double y;
-    double w;
-    double h;
-};
-
 struct qtc_transform {
     double a;
     double b;
@@ -37,18 +27,6 @@ struct qtc_transform {
     double f;
 };
 
-enum PathSegmentType {
-    MoveToSegment,
-    LineToSegment,
-    CurveToSegment,
-};
-
-struct PathSegment {
-    PathSegmentType kind;
-    double x;
-    double y;
-};
-
 // A direct copy from qnamespace.h.
 enum PenCapStyle {
     FlatCap   = 0x00,
@@ -76,39 +54,6 @@ enum Spread {
     RepeatSpread,
 };
 
-// A direct copy from qfont.h.
-enum FontStyle {
-    StyleNormal,
-    StyleItalic,
-    StyleOblique,
-};
-
-// A direct copy from qfont.h.
-enum FontWeight {
-    Thin        = 0,  // 100
-    ExtraLight  = 12, // 200
-    Light       = 25, // 300
-    Normal      = 50, // 400
-    Medium      = 57, // 500
-    DemiBold    = 63, // 600
-    Bold        = 75, // 700
-    ExtraBold   = 81, // 800
-    Black       = 87, // 900
-};
-
-// A direct copy from qfont.h.
-enum FontStretch {
-    UltraCondensed =  50,
-    ExtraCondensed =  62,
-    Condensed      =  75,
-    SemiCondensed  =  87,
-    Unstretched    = 100,
-    SemiExpanded   = 112,
-    Expanded       = 125,
-    ExtraExpanded  = 150,
-    UltraExpanded  = 200,
-};
-
 // TODO: remove prefix somehow
 // A direct copy from qpainter.h.
 enum CompositionMode {
@@ -148,9 +93,6 @@ enum AspectRatioMode {
 
 extern "C" {
 
-qtc_qguiapp* qtc_create_gui(char *app_name);
-void qtc_destroy_gui(qtc_qguiapp *c_app);
-
 // QImage
 qtc_qimage* qtc_qimage_create_rgba_premultiplied(uint32_t width, uint32_t height);
 qtc_qimage* qtc_qimage_create_rgba(uint32_t width, uint32_t height);
@@ -174,8 +116,6 @@ void qtc_qimage_destroy(qtc_qimage *c_img);
 qtc_qpainter* qtc_qpainter_create(qtc_qimage *c_img);
 void qtc_qpainter_set_antialiasing(qtc_qpainter *c_p, bool flag);
 void qtc_qpainter_set_smooth_pixmap_transform(qtc_qpainter *c_p, bool flag);
-qtc_qfont* qtc_qpainter_get_font(qtc_qpainter *c_p);
-void qtc_qpainter_set_font(qtc_qpainter *c_p, qtc_qfont *c_f);
 void qtc_qpainter_set_pen(qtc_qpainter *c_p, qtc_qpen *c_pen);
 void qtc_qpainter_reset_pen(qtc_qpainter *c_p);
 void qtc_qpainter_set_brush(qtc_qpainter *c_p, qtc_qbrush *c_brush);
@@ -194,7 +134,6 @@ void qtc_qpainter_set_clip_rect(qtc_qpainter *c_p, double x, double y, double w,
 void qtc_qpainter_set_clip_path(qtc_qpainter *c_p, qtc_qpainterpath *c_pp);
 void qtc_qpainter_reset_clip_path(qtc_qpainter *c_p);
 void qtc_qpainter_set_composition_mode(qtc_qpainter *c_p, CompositionMode mode);
-qtc_qfontmetricsf* qtc_qpainter_get_fontmetricsf(qtc_qpainter *c_p);
 void qtc_qpainter_end(qtc_qpainter *c_p);
 void qtc_qpainter_destroy(qtc_qpainter *c_p);
 
@@ -206,12 +145,7 @@ void qtc_qpainterpath_line_to(qtc_qpainterpath *c_pp, double x, double y);
 void qtc_qpainterpath_curve_to(qtc_qpainterpath *c_pp, double x1, double y1, double x2, double y2,
                                double x, double y);
 void qtc_qpainterpath_close_path(qtc_qpainterpath *c_pp);
-void qtc_qpainterpath_add_text(qtc_qpainterpath *c_pp, double x, double y, qtc_qfont *c_f,
-                               const char *text);
 void qtc_qpainterpath_set_fill_rule(qtc_qpainterpath *c_pp, FillRule rule);
-int qtc_qpainterpath_element_count(qtc_qpainterpath *c_pp);
-PathSegment qtc_qpainterpath_element_at(qtc_qpainterpath *c_pp, int i);
-qtc_rect_f qtc_qpainterpath_get_bbox(qtc_qpainterpath *c_pp);
 void qtc_qpainterpath_destroy(qtc_qpainterpath *c_pp);
 
 
@@ -260,32 +194,6 @@ void qtc_qradialgradient_set_color_at(qtc_qradialgradient *c_rg, double offset,
                                       uint8_t r, uint8_t g, uint8_t b, uint8_t a);
 void qtc_qradialgradient_set_spread(qtc_qradialgradient *c_rg, Spread s);
 void qtc_qradialgradient_destroy(qtc_qradialgradient *c_rg);
-
-// QFont
-qtc_qfont* qtc_qfont_create();
-qtc_qfont* qtc_qfont_clone(qtc_qfont *c_f);
-void qtc_qfont_set_family(qtc_qfont *c_f, const char *family);
-void qtc_qfont_set_style(qtc_qfont *c_f, FontStyle style);
-void qtc_qfont_set_small_caps(qtc_qfont *c_f, bool flag);
-void qtc_qfont_set_weight(qtc_qfont *c_f, FontWeight weight);
-void qtc_qfont_set_stretch(qtc_qfont *c_f, FontStretch stretch);
-void qtc_qfont_set_size(qtc_qfont *c_f, double size);
-void qtc_qfont_set_letter_spacing(qtc_qfont *c_f, double size);
-void qtc_qfont_set_word_spacing(qtc_qfont *c_f, double size);
-void qtc_qfont_print_debug(qtc_qfont *c_f);
-void qtc_qfont_destroy(qtc_qfont *c_f);
-
-// QFontMetricsF
-double qtc_qfontmetricsf_height(qtc_qfontmetricsf *c_fm);
-double qtc_qfontmetricsf_width(qtc_qfontmetricsf *c_fm, const char *text);
-double qtc_qfontmetricsf_full_width(qtc_qfontmetricsf *c_fm, const char *text);
-qtc_rect_f qtc_qfontmetricsf_get_bbox(qtc_qfontmetricsf *c_fm, const char *text);
-double qtc_qfontmetricsf_get_ascent(qtc_qfontmetricsf *c_fm);
-double qtc_qfontmetricsf_get_underline_pos(qtc_qfontmetricsf *c_fm);
-double qtc_qfontmetricsf_get_overline_pos(qtc_qfontmetricsf *c_fm);
-double qtc_qfontmetricsf_get_strikeout_pos(qtc_qfontmetricsf *c_fm);
-double qtc_qfontmetricsf_get_line_width(qtc_qfontmetricsf *c_fm);
-void qtc_qfontmetricsf_destroy(qtc_qfontmetricsf *c_fm);
 }
 
 #endif // QT_CAPI_H
diff --git a/resvg-qt/src/ffi.rs b/resvg-qt/src/ffi.rs
index a670415b4..07c407cb9 100644
--- a/resvg-qt/src/ffi.rs
+++ b/resvg-qt/src/ffi.rs
@@ -35,13 +35,14 @@ pub const __USE_ATFILE: u32 = 1;
 pub const __USE_GNU: u32 = 1;
 pub const __USE_FORTIFY_LEVEL: u32 = 0;
 pub const __GLIBC_USE_DEPRECATED_GETS: u32 = 0;
+pub const __GLIBC_USE_DEPRECATED_SCANF: u32 = 0;
 pub const _STDC_PREDEF_H: u32 = 1;
 pub const __STDC_IEC_559__: u32 = 1;
 pub const __STDC_IEC_559_COMPLEX__: u32 = 1;
 pub const __STDC_ISO_10646__: u32 = 201706;
 pub const __GNU_LIBRARY__: u32 = 6;
 pub const __GLIBC__: u32 = 2;
-pub const __GLIBC_MINOR__: u32 = 28;
+pub const __GLIBC_MINOR__: u32 = 29;
 pub const _SYS_CDEFS_H: u32 = 1;
 pub const __glibc_c99_flexarr_available: u32 = 1;
 pub const __WORDSIZE: u32 = 64;
@@ -53,11 +54,13 @@ pub const __GLIBC_USE_IEC_60559_BFP_EXT: u32 = 1;
 pub const __GLIBC_USE_IEC_60559_FUNCS_EXT: u32 = 1;
 pub const __GLIBC_USE_IEC_60559_TYPES_EXT: u32 = 1;
 pub const _BITS_TYPES_H: u32 = 1;
+pub const __TIMESIZE: u32 = 64;
 pub const _BITS_TYPESIZES_H: u32 = 1;
 pub const __OFF_T_MATCHES_OFF64_T: u32 = 1;
 pub const __INO_T_MATCHES_INO64_T: u32 = 1;
 pub const __RLIM_T_MATCHES_RLIM64_T: u32 = 1;
 pub const __FD_SETSIZE: u32 = 1024;
+pub const _BITS_TIME64_H: u32 = 1;
 pub const _BITS_WCHAR_H: u32 = 1;
 pub const _BITS_STDINT_INTN_H: u32 = 1;
 pub const _BITS_STDINT_UINTN_H: u32 = 1;
@@ -240,11 +243,6 @@ pub type intmax_t = __intmax_t;
 pub type uintmax_t = __uintmax_t;
 #[repr(C)]
 #[derive(Debug, Copy, Clone)]
-pub struct qtc_qguiapp {
-    _unused: [u8; 0],
-}
-#[repr(C)]
-#[derive(Debug, Copy, Clone)]
 pub struct qtc_qimage {
     _unused: [u8; 0],
 }
@@ -285,77 +283,6 @@ pub struct qtc_qradialgradient {
 }
 #[repr(C)]
 #[derive(Debug, Copy, Clone)]
-pub struct qtc_qfont {
-    _unused: [u8; 0],
-}
-#[repr(C)]
-#[derive(Debug, Copy, Clone)]
-pub struct qtc_qfontmetricsf {
-    _unused: [u8; 0],
-}
-#[repr(C)]
-#[derive(Debug, Copy, Clone)]
-pub struct qtc_rect_f {
-    pub x: f64,
-    pub y: f64,
-    pub w: f64,
-    pub h: f64,
-}
-#[test]
-fn bindgen_test_layout_qtc_rect_f() {
-    assert_eq!(
-        ::std::mem::size_of::<qtc_rect_f>(),
-        32usize,
-        concat!("Size of: ", stringify!(qtc_rect_f))
-    );
-    assert_eq!(
-        ::std::mem::align_of::<qtc_rect_f>(),
-        8usize,
-        concat!("Alignment of ", stringify!(qtc_rect_f))
-    );
-    assert_eq!(
-        unsafe { &(*(::std::ptr::null::<qtc_rect_f>())).x as *const _ as usize },
-        0usize,
-        concat!(
-            "Offset of field: ",
-            stringify!(qtc_rect_f),
-            "::",
-            stringify!(x)
-        )
-    );
-    assert_eq!(
-        unsafe { &(*(::std::ptr::null::<qtc_rect_f>())).y as *const _ as usize },
-        8usize,
-        concat!(
-            "Offset of field: ",
-            stringify!(qtc_rect_f),
-            "::",
-            stringify!(y)
-        )
-    );
-    assert_eq!(
-        unsafe { &(*(::std::ptr::null::<qtc_rect_f>())).w as *const _ as usize },
-        16usize,
-        concat!(
-            "Offset of field: ",
-            stringify!(qtc_rect_f),
-            "::",
-            stringify!(w)
-        )
-    );
-    assert_eq!(
-        unsafe { &(*(::std::ptr::null::<qtc_rect_f>())).h as *const _ as usize },
-        24usize,
-        concat!(
-            "Offset of field: ",
-            stringify!(qtc_rect_f),
-            "::",
-            stringify!(h)
-        )
-    );
-}
-#[repr(C)]
-#[derive(Debug, Copy, Clone)]
 pub struct qtc_transform {
     pub a: f64,
     pub b: f64,
@@ -437,60 +364,6 @@ fn bindgen_test_layout_qtc_transform() {
         )
     );
 }
-pub const PathSegmentType_MoveToSegment: PathSegmentType = 0;
-pub const PathSegmentType_LineToSegment: PathSegmentType = 1;
-pub const PathSegmentType_CurveToSegment: PathSegmentType = 2;
-pub type PathSegmentType = u32;
-#[repr(C)]
-#[derive(Debug, Copy, Clone)]
-pub struct PathSegment {
-    pub kind: PathSegmentType,
-    pub x: f64,
-    pub y: f64,
-}
-#[test]
-fn bindgen_test_layout_PathSegment() {
-    assert_eq!(
-        ::std::mem::size_of::<PathSegment>(),
-        24usize,
-        concat!("Size of: ", stringify!(PathSegment))
-    );
-    assert_eq!(
-        ::std::mem::align_of::<PathSegment>(),
-        8usize,
-        concat!("Alignment of ", stringify!(PathSegment))
-    );
-    assert_eq!(
-        unsafe { &(*(::std::ptr::null::<PathSegment>())).kind as *const _ as usize },
-        0usize,
-        concat!(
-            "Offset of field: ",
-            stringify!(PathSegment),
-            "::",
-            stringify!(kind)
-        )
-    );
-    assert_eq!(
-        unsafe { &(*(::std::ptr::null::<PathSegment>())).x as *const _ as usize },
-        8usize,
-        concat!(
-            "Offset of field: ",
-            stringify!(PathSegment),
-            "::",
-            stringify!(x)
-        )
-    );
-    assert_eq!(
-        unsafe { &(*(::std::ptr::null::<PathSegment>())).y as *const _ as usize },
-        16usize,
-        concat!(
-            "Offset of field: ",
-            stringify!(PathSegment),
-            "::",
-            stringify!(y)
-        )
-    );
-}
 pub const PenCapStyle_FlatCap: PenCapStyle = 0;
 pub const PenCapStyle_SquareCap: PenCapStyle = 16;
 pub const PenCapStyle_RoundCap: PenCapStyle = 32;
@@ -506,30 +379,6 @@ pub const Spread_PadSpread: Spread = 0;
 pub const Spread_ReflectSpread: Spread = 1;
 pub const Spread_RepeatSpread: Spread = 2;
 pub type Spread = u32;
-pub const FontStyle_StyleNormal: FontStyle = 0;
-pub const FontStyle_StyleItalic: FontStyle = 1;
-pub const FontStyle_StyleOblique: FontStyle = 2;
-pub type FontStyle = u32;
-pub const FontWeight_Thin: FontWeight = 0;
-pub const FontWeight_ExtraLight: FontWeight = 12;
-pub const FontWeight_Light: FontWeight = 25;
-pub const FontWeight_Normal: FontWeight = 50;
-pub const FontWeight_Medium: FontWeight = 57;
-pub const FontWeight_DemiBold: FontWeight = 63;
-pub const FontWeight_Bold: FontWeight = 75;
-pub const FontWeight_ExtraBold: FontWeight = 81;
-pub const FontWeight_Black: FontWeight = 87;
-pub type FontWeight = u32;
-pub const FontStretch_UltraCondensed: FontStretch = 50;
-pub const FontStretch_ExtraCondensed: FontStretch = 62;
-pub const FontStretch_Condensed: FontStretch = 75;
-pub const FontStretch_SemiCondensed: FontStretch = 87;
-pub const FontStretch_Unstretched: FontStretch = 100;
-pub const FontStretch_SemiExpanded: FontStretch = 112;
-pub const FontStretch_Expanded: FontStretch = 125;
-pub const FontStretch_ExtraExpanded: FontStretch = 150;
-pub const FontStretch_UltraExpanded: FontStretch = 200;
-pub type FontStretch = u32;
 pub const CompositionMode_CompositionMode_SourceOver: CompositionMode = 0;
 pub const CompositionMode_CompositionMode_DestinationOver: CompositionMode = 1;
 pub const CompositionMode_CompositionMode_Clear: CompositionMode = 2;
@@ -559,12 +408,6 @@ pub const AspectRatioMode_IgnoreAspectRatio: AspectRatioMode = 0;
 pub const AspectRatioMode_KeepAspectRatio: AspectRatioMode = 1;
 pub const AspectRatioMode_KeepAspectRatioByExpanding: AspectRatioMode = 2;
 pub type AspectRatioMode = u32;
-extern "C" {
-    pub fn qtc_create_gui(app_name: *mut ::std::os::raw::c_char) -> *mut qtc_qguiapp;
-}
-extern "C" {
-    pub fn qtc_destroy_gui(c_app: *mut qtc_qguiapp);
-}
 extern "C" {
     pub fn qtc_qimage_create_rgba_premultiplied(width: u32, height: u32) -> *mut qtc_qimage;
 }
@@ -631,12 +474,6 @@ extern "C" {
 extern "C" {
     pub fn qtc_qpainter_set_smooth_pixmap_transform(c_p: *mut qtc_qpainter, flag: bool);
 }
-extern "C" {
-    pub fn qtc_qpainter_get_font(c_p: *mut qtc_qpainter) -> *mut qtc_qfont;
-}
-extern "C" {
-    pub fn qtc_qpainter_set_font(c_p: *mut qtc_qpainter, c_f: *mut qtc_qfont);
-}
 extern "C" {
     pub fn qtc_qpainter_set_pen(c_p: *mut qtc_qpainter, c_pen: *mut qtc_qpen);
 }
@@ -707,9 +544,6 @@ extern "C" {
 extern "C" {
     pub fn qtc_qpainter_set_composition_mode(c_p: *mut qtc_qpainter, mode: CompositionMode);
 }
-extern "C" {
-    pub fn qtc_qpainter_get_fontmetricsf(c_p: *mut qtc_qpainter) -> *mut qtc_qfontmetricsf;
-}
 extern "C" {
     pub fn qtc_qpainter_end(c_p: *mut qtc_qpainter);
 }
@@ -739,30 +573,9 @@ extern "C" {
 extern "C" {
     pub fn qtc_qpainterpath_close_path(c_pp: *mut qtc_qpainterpath);
 }
-extern "C" {
-    pub fn qtc_qpainterpath_add_text(
-        c_pp: *mut qtc_qpainterpath,
-        x: f64,
-        y: f64,
-        c_f: *mut qtc_qfont,
-        text: *const ::std::os::raw::c_char,
-    );
-}
 extern "C" {
     pub fn qtc_qpainterpath_set_fill_rule(c_pp: *mut qtc_qpainterpath, rule: FillRule);
 }
-extern "C" {
-    pub fn qtc_qpainterpath_element_count(c_pp: *mut qtc_qpainterpath) -> ::std::os::raw::c_int;
-}
-extern "C" {
-    pub fn qtc_qpainterpath_element_at(
-        c_pp: *mut qtc_qpainterpath,
-        i: ::std::os::raw::c_int,
-    ) -> PathSegment;
-}
-extern "C" {
-    pub fn qtc_qpainterpath_get_bbox(c_pp: *mut qtc_qpainterpath) -> qtc_rect_f;
-}
 extern "C" {
     pub fn qtc_qpainterpath_destroy(c_pp: *mut qtc_qpainterpath);
 }
@@ -889,78 +702,3 @@ extern "C" {
 extern "C" {
     pub fn qtc_qradialgradient_destroy(c_rg: *mut qtc_qradialgradient);
 }
-extern "C" {
-    pub fn qtc_qfont_create() -> *mut qtc_qfont;
-}
-extern "C" {
-    pub fn qtc_qfont_clone(c_f: *mut qtc_qfont) -> *mut qtc_qfont;
-}
-extern "C" {
-    pub fn qtc_qfont_set_family(c_f: *mut qtc_qfont, family: *const ::std::os::raw::c_char);
-}
-extern "C" {
-    pub fn qtc_qfont_set_style(c_f: *mut qtc_qfont, style: FontStyle);
-}
-extern "C" {
-    pub fn qtc_qfont_set_small_caps(c_f: *mut qtc_qfont, flag: bool);
-}
-extern "C" {
-    pub fn qtc_qfont_set_weight(c_f: *mut qtc_qfont, weight: FontWeight);
-}
-extern "C" {
-    pub fn qtc_qfont_set_stretch(c_f: *mut qtc_qfont, stretch: FontStretch);
-}
-extern "C" {
-    pub fn qtc_qfont_set_size(c_f: *mut qtc_qfont, size: f64);
-}
-extern "C" {
-    pub fn qtc_qfont_set_letter_spacing(c_f: *mut qtc_qfont, size: f64);
-}
-extern "C" {
-    pub fn qtc_qfont_set_word_spacing(c_f: *mut qtc_qfont, size: f64);
-}
-extern "C" {
-    pub fn qtc_qfont_print_debug(c_f: *mut qtc_qfont);
-}
-extern "C" {
-    pub fn qtc_qfont_destroy(c_f: *mut qtc_qfont);
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_height(c_fm: *mut qtc_qfontmetricsf) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_width(
-        c_fm: *mut qtc_qfontmetricsf,
-        text: *const ::std::os::raw::c_char,
-    ) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_full_width(
-        c_fm: *mut qtc_qfontmetricsf,
-        text: *const ::std::os::raw::c_char,
-    ) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_get_bbox(
-        c_fm: *mut qtc_qfontmetricsf,
-        text: *const ::std::os::raw::c_char,
-    ) -> qtc_rect_f;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_get_ascent(c_fm: *mut qtc_qfontmetricsf) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_get_underline_pos(c_fm: *mut qtc_qfontmetricsf) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_get_overline_pos(c_fm: *mut qtc_qfontmetricsf) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_get_strikeout_pos(c_fm: *mut qtc_qfontmetricsf) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_get_line_width(c_fm: *mut qtc_qfontmetricsf) -> f64;
-}
-extern "C" {
-    pub fn qtc_qfontmetricsf_destroy(c_fm: *mut qtc_qfontmetricsf);
-}
diff --git a/resvg-qt/src/lib.rs b/resvg-qt/src/lib.rs
index eda9e6880..4ad33eccf 100644
--- a/resvg-qt/src/lib.rs
+++ b/resvg-qt/src/lib.rs
@@ -32,39 +32,6 @@ pub enum LineJoin {
     Miter = ffi::PenJoinStyle_MiterJoin as isize,
 }
 
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
-pub enum FontStyle {
-    Normal = ffi::FontStyle_StyleNormal as isize,
-    Italic = ffi::FontStyle_StyleItalic as isize,
-    Oblique = ffi::FontStyle_StyleOblique as isize,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
-pub enum FontWeight {
-    Thin = ffi::FontWeight_Thin as isize,
-    ExtraLight = ffi::FontWeight_ExtraLight as isize,
-    Light = ffi::FontWeight_Light as isize,
-    Normal = ffi::FontWeight_Normal as isize,
-    Medium = ffi::FontWeight_Medium as isize,
-    DemiBold = ffi::FontWeight_DemiBold as isize,
-    Bold = ffi::FontWeight_Bold as isize,
-    ExtraBold = ffi::FontWeight_ExtraBold as isize,
-    Black = ffi::FontWeight_Black as isize,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
-pub enum FontStretch {
-    UltraCondensed = ffi::FontStretch_UltraCondensed as isize,
-    ExtraCondensed = ffi::FontStretch_ExtraCondensed as isize,
-    Condensed = ffi::FontStretch_Condensed as isize,
-    SemiCondensed = ffi::FontStretch_SemiCondensed as isize,
-    Unstretched = ffi::FontStretch_Unstretched as isize,
-    SemiExpanded = ffi::FontStretch_SemiExpanded as isize,
-    Expanded = ffi::FontStretch_Expanded as isize,
-    ExtraExpanded = ffi::FontStretch_ExtraExpanded as isize,
-    UltraExpanded = ffi::FontStretch_UltraExpanded as isize,
-}
-
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 pub enum CompositionMode {
     SourceOver = ffi::CompositionMode_CompositionMode_SourceOver as isize,
@@ -100,13 +67,6 @@ pub enum AspectRatioMode {
     KeepByExpanding = ffi::AspectRatioMode_KeepAspectRatioByExpanding as isize,
 }
 
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
-pub enum PathSegmentType {
-    MoveTo,
-    LineTo,
-    CurveTo,
-}
-
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 pub enum Spread {
     Pad = ffi::Spread_PadSpread as isize,
@@ -114,21 +74,6 @@ pub enum Spread {
     Repeat = ffi::Spread_RepeatSpread as isize,
 }
 
-pub struct GuiApp(*mut ffi::qtc_qguiapp);
-
-impl GuiApp {
-    pub fn new(app_name: &str) -> GuiApp {
-        let c_app_name = CString::new(app_name).unwrap();
-        unsafe { GuiApp(ffi::qtc_create_gui(c_app_name.as_ptr() as *mut _)) }
-    }
-}
-
-impl Drop for GuiApp {
-    fn drop(&mut self) {
-        unsafe { ffi::qtc_destroy_gui(self.0) }
-    }
-}
-
 
 pub struct Image(*mut ffi::qtc_qimage);
 
@@ -273,18 +218,6 @@ impl Painter {
         unsafe { ffi::qtc_qpainter_set_smooth_pixmap_transform(self.0, flag); }
     }
 
-    pub fn font(&self) -> Font {
-        unsafe { Font(ffi::qtc_qpainter_get_font(self.0)) }
-    }
-
-    pub fn set_font(&mut self, font: &Font) {
-        unsafe { ffi::qtc_qpainter_set_font(self.0, font.0) }
-    }
-
-    pub fn font_metrics(&self) -> FontMetricsF {
-        unsafe { FontMetricsF(ffi::qtc_qpainter_get_fontmetricsf(self.0)) }
-    }
-
     pub fn set_pen(&mut self, pen: Pen) {
         unsafe { ffi::qtc_qpainter_set_pen(self.0, pen.0) }
     }
@@ -317,11 +250,6 @@ impl Painter {
         unsafe { ffi::qtc_qpainter_draw_image_rect(self.0, x, y, w, h, img.0) }
     }
 
-    pub fn draw_text(&mut self, x: f64, y: f64, text: &str) {
-        let c_text = CString::new(text).unwrap();
-        unsafe { ffi::qtc_qpainter_draw_text(self.0, x, y, c_text.as_ptr()) }
-    }
-
     pub fn draw_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
         unsafe { ffi::qtc_qpainter_draw_rect(self.0, x, y, w, h) }
     }
@@ -399,34 +327,6 @@ impl PainterPath {
         unsafe { ffi::qtc_qpainterpath_close_path(self.0) }
     }
 
-    pub fn add_text(&mut self, x: f64, y: f64, font: &Font, text: &str) {
-        let c_text = CString::new(text).unwrap();
-        unsafe { ffi::qtc_qpainterpath_add_text(self.0, x, y, font.0, c_text.as_ptr()) }
-    }
-
-    pub fn len(&self) -> i32 {
-        unsafe { ffi::qtc_qpainterpath_element_count(self.0) }
-    }
-
-    pub fn get(&self, index: i32) -> (PathSegmentType, f64, f64) {
-        unsafe {
-            let seg = ffi::qtc_qpainterpath_element_at(self.0, index);
-            let kind = match seg.kind {
-                ffi::PathSegmentType_MoveToSegment => PathSegmentType::MoveTo,
-                ffi::PathSegmentType_LineToSegment => PathSegmentType::LineTo,
-                ffi::PathSegmentType_CurveToSegment => PathSegmentType::CurveTo,
-                _ => unreachable!(),
-            };
-            (kind, seg.x, seg.y)
-        }
-    }
-
-    pub fn bounding_box(&self) -> (f64, f64, f64, f64) {
-        let rect = unsafe { ffi::qtc_qpainterpath_get_bbox(self.0) };
-
-        (rect.x, rect.y, rect.w, rect.h)
-    }
-
     pub fn set_fill_rule(&mut self, rule: FillRule) {
         unsafe { ffi::qtc_qpainterpath_set_fill_rule(self.0, rule as ffi::FillRule) }
     }
@@ -602,113 +502,3 @@ impl Drop for RadialGradient {
         unsafe { ffi::qtc_qradialgradient_destroy(self.0) }
     }
 }
-
-
-pub struct Font(*mut ffi::qtc_qfont);
-
-impl Font {
-    pub fn new() -> Font {
-        unsafe { Font(ffi::qtc_qfont_create()) }
-    }
-
-    pub fn set_family(&mut self, family: &str) {
-        let c_family = CString::new(family).unwrap();
-        unsafe { ffi::qtc_qfont_set_family(self.0, c_family.as_ptr()); }
-    }
-
-    pub fn set_style(&mut self, style: FontStyle) {
-        unsafe { ffi::qtc_qfont_set_style(self.0, style as ffi::FontStyle); }
-    }
-
-    pub fn set_small_caps(&mut self, flag: bool) {
-        unsafe { ffi::qtc_qfont_set_small_caps(self.0, flag); }
-    }
-
-    pub fn set_weight(&mut self, weight: FontWeight) {
-        unsafe { ffi::qtc_qfont_set_weight(self.0, weight as ffi::FontWeight); }
-    }
-
-    pub fn set_stretch(&mut self, stretch: FontStretch) {
-        unsafe { ffi::qtc_qfont_set_stretch(self.0, stretch as ffi::FontStretch); }
-    }
-
-    pub fn set_size(&mut self, size: f64) {
-        unsafe { ffi::qtc_qfont_set_size(self.0, size); }
-    }
-
-    pub fn set_letter_spacing(&mut self, size: f64) {
-        unsafe { ffi::qtc_qfont_set_letter_spacing(self.0, size); }
-    }
-
-    pub fn set_word_spacing(&mut self, size: f64) {
-        unsafe { ffi::qtc_qfont_set_word_spacing(self.0, size); }
-    }
-
-    pub fn print_debug(&self) {
-        unsafe { ffi::qtc_qfont_print_debug(self.0); }
-    }
-}
-
-impl Clone for Font {
-    fn clone(&self) -> Self {
-        unsafe { Font(ffi::qtc_qfont_clone(self.0)) }
-    }
-}
-
-impl Drop for Font {
-    fn drop(&mut self) {
-        unsafe { ffi::qtc_qfont_destroy(self.0) }
-    }
-}
-
-
-pub struct FontMetricsF(*mut ffi::qtc_qfontmetricsf);
-
-impl FontMetricsF {
-    pub fn height(&self) -> f64 {
-        unsafe { ffi::qtc_qfontmetricsf_height(self.0) }
-    }
-
-    pub fn width(&self, text: &str) -> f64 {
-        let c_text = CString::new(text).unwrap();
-        unsafe { ffi::qtc_qfontmetricsf_width(self.0, c_text.as_ptr()) }
-    }
-
-    pub fn ascent(&self) -> f64 {
-        unsafe { ffi::qtc_qfontmetricsf_get_ascent(self.0) }
-    }
-
-    pub fn underline_pos(&self) -> f64 {
-        unsafe { ffi::qtc_qfontmetricsf_get_underline_pos(self.0) }
-    }
-
-    pub fn overline_pos(&self) -> f64 {
-        unsafe { ffi::qtc_qfontmetricsf_get_overline_pos(self.0) }
-    }
-
-    pub fn strikeout_pos(&self) -> f64 {
-        unsafe { ffi::qtc_qfontmetricsf_get_strikeout_pos(self.0) }
-    }
-
-    pub fn line_width(&self) -> f64 {
-        unsafe { ffi::qtc_qfontmetricsf_get_line_width(self.0) }
-    }
-
-    pub fn full_width(&self, text: &str) -> f64 {
-        let c_text = CString::new(text).unwrap();
-        unsafe { ffi::qtc_qfontmetricsf_full_width(self.0, c_text.as_ptr()) }
-    }
-
-    pub fn bounding_box(&self, text: &str) -> (f64, f64, f64, f64) {
-        let c_text = CString::new(text).unwrap();
-        let rect = unsafe { ffi::qtc_qfontmetricsf_get_bbox(self.0, c_text.as_ptr()) };
-
-        (rect.x, rect.y, rect.w, rect.h)
-    }
-}
-
-impl Drop for FontMetricsF {
-    fn drop(&mut self) {
-        unsafe { ffi::qtc_qfontmetricsf_destroy(self.0) }
-    }
-}
diff --git a/src/backend_cairo/clip_and_mask.rs b/src/backend_cairo/clip_and_mask.rs
index 5b6dcdb4d..0f731c2ba 100644
--- a/src/backend_cairo/clip_and_mask.rs
+++ b/src/backend_cairo/clip_and_mask.rs
@@ -11,10 +11,7 @@ use cairo::{
 // self
 use crate::backend_utils::mask;
 use super::prelude::*;
-use super::{
-    path,
-    text,
-};
+use super::path;
 
 
 pub fn clip(
@@ -35,7 +32,7 @@ pub fn clip(
     clip_cr.transform(cp.transform.to_native());
 
     if cp.units == usvg::Units::ObjectBoundingBox {
-        clip_cr.transform(cairo::Matrix::from_bbox(bbox));
+        clip_cr.transform(usvg::Transform::from_bbox(bbox).to_native());
     }
 
     clip_cr.set_operator(cairo::Operator::Clear);
@@ -48,9 +45,6 @@ pub fn clip(
             usvg::NodeKind::Path(ref p) => {
                 path::draw(&node.tree(), p, opt, &clip_cr);
             }
-            usvg::NodeKind::Text(ref text) => {
-                text::draw(&node.tree(), text, opt, &clip_cr);
-            }
             usvg::NodeKind::Group(ref g) => {
                 clip_group(&node, g, opt, bbox, layers, &clip_cr);
             }
@@ -129,9 +123,6 @@ fn draw_group_child(
             usvg::NodeKind::Path(ref path_node) => {
                 path::draw(&child.tree(), path_node, opt, cr);
             }
-            usvg::NodeKind::Text(ref text) => {
-                text::draw(&child.tree(), text, opt, cr);
-            }
             _ => {}
         }
     }
@@ -162,7 +153,7 @@ pub fn mask(
         mask_cr.clip();
 
         if mask.content_units == usvg::Units::ObjectBoundingBox {
-            mask_cr.transform(cairo::Matrix::from_bbox(bbox));
+            mask_cr.transform(usvg::Transform::from_bbox(bbox).to_native());
         }
 
         super::render_group(node, opt, layers, &mask_cr);
diff --git a/src/backend_cairo/mod.rs b/src/backend_cairo/mod.rs
index 59fa749eb..82e5d0039 100644
--- a/src/backend_cairo/mod.rs
+++ b/src/backend_cairo/mod.rs
@@ -9,14 +9,11 @@ use cairo::{
     self,
     MatrixTrait,
 };
-use pangocairo::functions as pc;
 
 // self
 use crate::prelude::*;
-use crate::{
-    backend_utils,
-    layers,
-};
+use crate::layers;
+use crate::backend_utils::ConvTransform;
 
 
 macro_rules! try_create_surface {
@@ -37,10 +34,11 @@ mod filter;
 mod image;
 mod path;
 mod style;
-mod text;
 
 mod prelude {
     pub use super::super::prelude::*;
+    pub use crate::backend_utils::ConvTransform;
+
     pub type CairoLayers = super::layers::Layers<super::cairo::ImageSurface>;
 
     // It's actually used. Rust bug?
@@ -62,11 +60,6 @@ impl ConvTransform<cairo::Matrix> for usvg::Transform {
     }
 }
 
-impl TransformFromBBox for cairo::Matrix {
-    fn from_bbox(bbox: Rect) -> Self {
-        Self::new(bbox.width(), 0.0, 0.0, bbox.height(), bbox.x(), bbox.y())
-    }
-}
 
 pub(crate) trait ReCairoContextExt {
     fn set_source_color(&self, color: usvg::Color, opacity: usvg::Opacity);
@@ -257,9 +250,6 @@ fn render_node(
         usvg::NodeKind::Path(ref path) => {
             path::draw(&node.tree(), path, opt, cr)
         }
-        usvg::NodeKind::Text(ref text) => {
-            Some(text::draw(&node.tree(), text, opt, cr))
-        }
         usvg::NodeKind::Image(ref img) => {
             Some(image::draw(img, opt, cr))
         }
@@ -402,36 +392,6 @@ fn _calc_node_bbox(
         usvg::NodeKind::Path(ref path) => {
             utils::path_bbox(&path.segments, path.stroke.as_ref(), Some(ts2))
         }
-        usvg::NodeKind::Text(ref text) => {
-            let mut bbox = Rect::new_bbox();
-            let mut fm = text::PangoFontMetrics::new(opt, cr);
-            let (blocks, _) = backend_utils::text::prepare_blocks(text, &mut fm);
-            backend_utils::text::draw_blocks(blocks, |block| {
-                cr.new_path();
-
-                let context = text::init_pango_context(opt, cr);
-                let layout = text::init_pango_layout(&block, &context);
-
-                pc::layout_path(cr, &layout);
-                let path = cr.copy_path();
-                let segments = from_cairo_path(&path);
-
-                let mut t = ts2;
-                if let Some(rotate) = block.rotate {
-                    t.rotate_at(rotate, block.bbox.x(), block.bbox.y() + block.font_ascent);
-                }
-                t.translate(block.bbox.x(), block.bbox.y());
-
-                if !segments.is_empty() {
-                    let c_bbox = utils::path_bbox(&segments, block.stroke.as_ref(), Some(t));
-                    if let Some(c_bbox) = c_bbox {
-                        bbox = bbox.expand(c_bbox);
-                    }
-                }
-            });
-
-            Some(bbox)
-        }
         usvg::NodeKind::Image(ref img) => {
             let segments = utils::rect_to_path(img.view_box.rect);
             utils::path_bbox(&segments, None, Some(ts2))
@@ -451,32 +411,6 @@ fn _calc_node_bbox(
     }
 }
 
-fn from_cairo_path(path: &cairo::Path) -> Vec<usvg::PathSegment> {
-    let mut segments = Vec::new();
-    for seg in path.iter() {
-        match seg {
-            cairo::PathSegment::MoveTo((x, y)) => {
-                segments.push(usvg::PathSegment::MoveTo { x, y });
-            }
-            cairo::PathSegment::LineTo((x, y)) => {
-                segments.push(usvg::PathSegment::LineTo { x, y });
-            }
-            cairo::PathSegment::CurveTo((x1, y1), (x2, y2), (x, y)) => {
-                segments.push(usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y });
-            }
-            cairo::PathSegment::ClosePath => {
-                segments.push(usvg::PathSegment::ClosePath);
-            }
-        }
-    }
-
-    if segments.len() == 1 {
-        segments.clear();
-    }
-
-    segments
-}
-
 fn create_layers(img_size: ScreenSize, opt: &Options) -> CairoLayers {
     layers::Layers::new(img_size, opt.usvg.dpi, create_subsurface, clear_subsurface)
 }
diff --git a/src/backend_cairo/style.rs b/src/backend_cairo/style.rs
index ecb937672..b8c15ca75 100644
--- a/src/backend_cairo/style.rs
+++ b/src/backend_cairo/style.rs
@@ -159,7 +159,7 @@ fn prepare_base_gradient<G: cairo::Gradient>(
     let mut matrix = g.transform.to_native();
 
     if g.units == usvg::Units::ObjectBoundingBox {
-        let m = cairo::Matrix::from_bbox(bbox);
+        let m = usvg::Transform::from_bbox(bbox).to_native();
         matrix = cairo::Matrix::multiply(&matrix, &m);
     }
 
diff --git a/src/backend_cairo/text.rs b/src/backend_cairo/text.rs
deleted file mode 100644
index 30c8e87c7..000000000
--- a/src/backend_cairo/text.rs
+++ /dev/null
@@ -1,290 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-use std::f64;
-
-// external
-use cairo;
-use pango::{
-    self,
-    LayoutExt,
-    ContextExt,
-};
-use pangocairo::functions as pc;
-
-// self
-use super::prelude::*;
-use crate::backend_utils;
-use crate::backend_utils::text::{
-    self,
-    FontMetrics,
-};
-use super::style;
-
-
-trait FromPangoScale {
-    fn from_pango(&self) -> f64;
-}
-
-impl FromPangoScale for i32 {
-    fn from_pango(&self) -> f64 {
-        *self as f64 / pango::SCALE as f64
-    }
-}
-
-
-trait ToPangoScale {
-    fn to_pango(&self) -> i32;
-}
-
-impl ToPangoScale for f64 {
-    fn to_pango(&self) -> i32 {
-        (*self * pango::SCALE as f64) as i32
-    }
-}
-
-
-pub struct PangoFontMetrics {
-    layout: pango::Layout,
-    dpi: f64,
-}
-
-impl PangoFontMetrics {
-    pub fn new(opt: &Options, cr: &cairo::Context) -> Self {
-        let context = init_pango_context(opt, cr);
-        let layout = pango::Layout::new(&context);
-        PangoFontMetrics { layout, dpi: opt.usvg.dpi }
-    }
-}
-
-impl FontMetrics<pango::FontDescription> for PangoFontMetrics {
-    fn set_font(&mut self, font: &usvg::Font) {
-        set_text_spacing(font.letter_spacing, &self.layout);
-        self.layout.set_font_description(&init_font(font, self.dpi));
-    }
-
-    fn font(&self) -> pango::FontDescription {
-        self.layout.get_font_description().unwrap()
-    }
-
-    fn width(&self, text: &str) -> f64 {
-        self.layout.set_text(text);
-        self.layout.get_size().0.from_pango()
-    }
-
-    fn ascent(&self, text: &str) -> f64 {
-        self.layout.set_text(text);
-        let mut layout_iter = self.layout.get_iter().unwrap();
-        layout_iter.get_baseline().from_pango()
-    }
-
-    fn height(&self) -> f64 {
-        self.layout.get_size().1.from_pango()
-    }
-}
-
-pub fn draw(
-    tree: &usvg::Tree,
-    text: &usvg::Text,
-    opt: &Options,
-    cr: &cairo::Context,
-) -> Rect {
-    let mut font_opt = cr.get_font_options();
-    if !backend_utils::use_text_antialiasing(text.rendering_mode) {
-        cr.set_antialias(cairo::Antialias::None);
-        font_opt.set_antialias(cairo::Antialias::None);
-        cr.set_font_options(&font_opt);
-    }
-
-    let mut fm = PangoFontMetrics::new(opt, cr);
-    let (blocks, text_bbox) = text::prepare_blocks(text, &mut fm);
-    text::draw_blocks(blocks, |block| draw_block(tree, block, text_bbox, opt, cr));
-
-    // Revert anti-aliasing.
-    cr.set_antialias(cairo::Antialias::Default);
-    font_opt.set_antialias(cairo::Antialias::Default);
-    cr.set_font_options(&font_opt);
-
-    text_bbox
-}
-
-pub fn init_pango_context(opt: &Options, cr: &cairo::Context) -> pango::Context {
-    let context = pc::create_context(cr).unwrap();
-    pc::update_context(cr, &context);
-    pc::context_set_resolution(&context, opt.usvg.dpi);
-    context
-}
-
-pub fn init_pango_layout(
-    block: &text::TextBlock<pango::FontDescription>,
-    context: &pango::Context,
-) -> pango::Layout {
-    let layout = pango::Layout::new(context);
-    layout.set_font_description(&block.font);
-    set_text_spacing(block.letter_spacing, &layout);
-    layout.set_text(&block.text);
-    layout
-}
-
-fn set_text_spacing(
-    letter_spacing: Option<f64>,
-    layout: &pango::Layout,
-) {
-    let attr_list = pango::AttrList::new();
-
-    if let Some(letter_spacing) = letter_spacing {
-        attr_list.insert(pango::Attribute::new_letter_spacing(letter_spacing.to_pango()).unwrap());
-    }
-
-    layout.set_attributes(&attr_list);
-}
-
-fn draw_block(
-    tree: &usvg::Tree,
-    block: &text::TextBlock<pango::FontDescription>,
-    text_bbox: Rect,
-    opt: &Options,
-    cr: &cairo::Context,
-) {
-    // `tspan` doesn't have a bbox by the SVG spec and should use the whole `text` bbox.
-    // That's why we are using `text_bbox` instead of `block.bbox`.
-
-    let context = init_pango_context(opt, cr);
-    let layout = init_pango_layout(&block, &context);
-
-    let fm = context.get_metrics(&block.font, None).unwrap();
-
-    let bbox = block.bbox;
-
-    let underline_height = fm.get_underline_thickness().from_pango();
-    let line_rect = try_opt!(Rect::new(bbox.x(), bbox.y(), bbox.width(), underline_height), ());
-
-    let old_ts = cr.get_matrix();
-    if let Some(rotate) = block.rotate {
-        let ts = usvg::Transform::new_rotate_at(rotate, bbox.x(), bbox.y() + block.font_ascent);
-        cr.transform(ts.to_native());
-    }
-
-    // Draw underline.
-    //
-    // Should be drawn before/under text.
-    if let Some(ref style) = block.decoration.underline {
-        let ty = block.font_ascent - fm.get_underline_position().from_pango();
-        draw_line(tree, line_rect.translate(0.0, ty), text_bbox, &style.fill, &style.stroke, opt, cr);
-    }
-
-    // Draw overline.
-    //
-    // Should be drawn before/under text.
-    if let Some(ref style) = block.decoration.overline {
-        let ty = underline_height;
-        draw_line(tree, line_rect.translate(0.0, ty), text_bbox, &style.fill, &style.stroke, opt, cr);
-    }
-
-    // Draw text.
-    cr.move_to(bbox.x(), bbox.y());
-
-    style::fill(tree, &block.fill, opt, text_bbox, cr);
-    pc::update_layout(cr, &layout);
-    pc::show_layout(cr, &layout);
-
-    style::stroke(tree, &block.stroke, opt, text_bbox, cr);
-    pc::layout_path(cr, &layout);
-    cr.stroke();
-
-    cr.move_to(-bbox.x(), -bbox.y());
-
-    // Draw line-through.
-    //
-    // Should be drawn after/over text.
-    if let Some(ref style) = block.decoration.line_through {
-        let line_rect = Rect::new(
-            bbox.x(),
-            bbox.y() + block.font_ascent - fm.get_strikethrough_position().from_pango(),
-            bbox.width(),
-            fm.get_strikethrough_thickness().from_pango(),
-        );
-        let line_rect = try_opt!(line_rect, ());
-
-        draw_line(tree, line_rect, text_bbox, &style.fill, &style.stroke, opt, cr);
-    }
-
-    cr.set_matrix(old_ts);
-}
-
-fn init_font(dom_font: &usvg::Font, dpi: f64) -> pango::FontDescription {
-    let mut font = pango::FontDescription::new();
-
-    // We have to remove quotes, because `pango` doesn't support them.
-    let font_family = dom_font.family.replace('\'', "");
-    font.set_family(&font_family);
-
-    let font_style = match dom_font.style {
-        usvg::FontStyle::Normal => pango::Style::Normal,
-        usvg::FontStyle::Italic => pango::Style::Italic,
-        usvg::FontStyle::Oblique => pango::Style::Oblique,
-    };
-    font.set_style(font_style);
-
-    let font_variant = match dom_font.variant {
-        usvg::FontVariant::Normal => pango::Variant::Normal,
-        usvg::FontVariant::SmallCaps => pango::Variant::SmallCaps,
-    };
-    font.set_variant(font_variant);
-
-    let font_weight = match dom_font.weight {
-        usvg::FontWeight::W100       => pango::Weight::Thin,
-        usvg::FontWeight::W200       => pango::Weight::Ultralight,
-        usvg::FontWeight::W300       => pango::Weight::Light,
-        usvg::FontWeight::W400       => pango::Weight::Normal,
-        usvg::FontWeight::W500       => pango::Weight::Medium,
-        usvg::FontWeight::W600       => pango::Weight::Semibold,
-        usvg::FontWeight::W700       => pango::Weight::Bold,
-        usvg::FontWeight::W800       => pango::Weight::Ultrabold,
-        usvg::FontWeight::W900       => pango::Weight::Heavy,
-    };
-    font.set_weight(font_weight);
-
-    let font_stretch = match dom_font.stretch {
-        usvg::FontStretch::Normal         => pango::Stretch::Normal,
-        usvg::FontStretch::Narrower |
-        usvg::FontStretch::Condensed      => pango::Stretch::Condensed,
-        usvg::FontStretch::UltraCondensed => pango::Stretch::UltraCondensed,
-        usvg::FontStretch::ExtraCondensed => pango::Stretch::ExtraCondensed,
-        usvg::FontStretch::SemiCondensed  => pango::Stretch::SemiCondensed,
-        usvg::FontStretch::SemiExpanded   => pango::Stretch::SemiExpanded,
-        usvg::FontStretch::Wider |
-        usvg::FontStretch::Expanded       => pango::Stretch::Expanded,
-        usvg::FontStretch::ExtraExpanded  => pango::Stretch::ExtraExpanded,
-        usvg::FontStretch::UltraExpanded  => pango::Stretch::UltraExpanded,
-    };
-    font.set_stretch(font_stretch);
-
-    let font_size = dom_font.size.value().to_pango() as f64 / dpi * 72.0;
-    font.set_size(font_size as i32);
-
-    font
-}
-
-fn draw_line(
-    tree: &usvg::Tree,
-    r: Rect,
-    text_bbox: Rect,
-    fill: &Option<usvg::Fill>,
-    stroke: &Option<usvg::Stroke>,
-    opt: &Options,
-    cr: &cairo::Context,
-) {
-    cr.rectangle(r.x(), r.y(), r.width(), r.height());
-
-    style::fill(tree, fill, opt, text_bbox, cr);
-    if stroke.is_some() {
-        cr.fill_preserve();
-
-        style::stroke(tree, &stroke, opt, text_bbox, cr);
-        cr.stroke();
-    } else {
-        cr.fill();
-    }
-}
diff --git a/src/backend_qt/clip_and_mask.rs b/src/backend_qt/clip_and_mask.rs
index c140240f4..fcb74312b 100644
--- a/src/backend_qt/clip_and_mask.rs
+++ b/src/backend_qt/clip_and_mask.rs
@@ -8,10 +8,7 @@ use crate::qt;
 // self
 use crate::backend_utils::mask;
 use super::prelude::*;
-use super::{
-    path,
-    text,
-};
+use super::path;
 
 
 pub fn clip(
@@ -31,7 +28,7 @@ pub fn clip(
     clip_p.apply_transform(&cp.transform.to_native());
 
     if cp.units == usvg::Units::ObjectBoundingBox {
-        clip_p.apply_transform(&qt::Transform::from_bbox(bbox));
+        clip_p.apply_transform(&usvg::Transform::from_bbox(bbox).to_native());
     }
 
     clip_p.set_composition_mode(qt::CompositionMode::Clear);
@@ -44,9 +41,6 @@ pub fn clip(
             usvg::NodeKind::Path(ref path_node) => {
                 path::draw(&node.tree(), path_node, opt, &mut clip_p);
             }
-            usvg::NodeKind::Text(ref text) => {
-                text::draw(&node.tree(), text, opt, &mut clip_p);
-            }
             usvg::NodeKind::Group(ref g) => {
                 clip_group(&node, g, opt, bbox, layers, &mut clip_p);
             }
@@ -116,9 +110,6 @@ fn draw_group_child(
             usvg::NodeKind::Path(ref path_node) => {
                 path::draw(&child.tree(), path_node, opt, p);
             }
-            usvg::NodeKind::Text(ref text) => {
-                text::draw(&child.tree(), text, opt, p);
-            }
             _ => {}
         }
     }
@@ -148,7 +139,7 @@ pub fn mask(
         mask_p.set_clip_rect(r.x(), r.y(), r.width(), r.height());
 
         if mask.content_units == usvg::Units::ObjectBoundingBox {
-            mask_p.apply_transform(&qt::Transform::from_bbox(bbox));
+            mask_p.apply_transform(&usvg::Transform::from_bbox(bbox).to_native());
         }
 
         super::render_group(node, opt, layers, &mut mask_p);
diff --git a/src/backend_qt/mod.rs b/src/backend_qt/mod.rs
index ac2024dc1..b4360f7c4 100644
--- a/src/backend_qt/mod.rs
+++ b/src/backend_qt/mod.rs
@@ -9,8 +9,8 @@ use crate::qt;
 
 // self
 use crate::prelude::*;
+use crate::backend_utils::ConvTransform;
 use crate::{
-    backend_utils,
     layers,
     OutputImage,
     Render,
@@ -33,10 +33,11 @@ mod filter;
 mod image;
 mod path;
 mod style;
-mod text;
 
 mod prelude {
     pub use super::super::prelude::*;
+    pub use crate::backend_utils::ConvTransform;
+
     pub type QtLayers = super::layers::Layers<super::qt::Image>;
 }
 
@@ -54,11 +55,6 @@ impl ConvTransform<qt::Transform> for usvg::Transform {
     }
 }
 
-impl TransformFromBBox for qt::Transform {
-    fn from_bbox(bbox: Rect) -> Self {
-        Self::new(bbox.width(), 0.0, 0.0, bbox.height(), bbox.x(), bbox.y())
-    }
-}
 
 /// Cairo backend handle.
 #[derive(Clone, Copy)]
@@ -213,9 +209,6 @@ fn render_node(
         usvg::NodeKind::Path(ref path) => {
             path::draw(&node.tree(), path, opt, p)
         }
-        usvg::NodeKind::Text(ref text) => {
-            Some(text::draw(&node.tree(), text, opt, p))
-        }
         usvg::NodeKind::Image(ref img) => {
             Some(image::draw(img, opt, p))
         }
@@ -347,32 +340,6 @@ fn _calc_node_bbox(
         usvg::NodeKind::Path(ref path) => {
             utils::path_bbox(&path.segments, path.stroke.as_ref(), Some(ts2))
         }
-        usvg::NodeKind::Text(ref text) => {
-            let mut bbox = Rect::new_bbox();
-            let mut fm = text::QtFontMetrics::new(p);
-            let (blocks, _) = backend_utils::text::prepare_blocks(text, &mut fm);
-            backend_utils::text::draw_blocks(blocks, |block| {
-                let mut p_path = qt::PainterPath::new();
-                p_path.add_text(0.0, 0.0, &block.font, &block.text);
-
-                let y = block.bbox.y() + block.font_ascent;
-
-                let mut t = ts2;
-                if let Some(rotate) = block.rotate {
-                    t.rotate_at(rotate, block.bbox.x(), y);
-                }
-                t.translate(block.bbox.x(), y);
-
-                let segments = from_qt_path(&p_path);
-                if !segments.is_empty() {
-                    if let Some(c_bbox) = utils::path_bbox(&segments, block.stroke.as_ref(), Some(t)) {
-                        bbox = bbox.expand(c_bbox);
-                    }
-                }
-            });
-
-            Some(bbox)
-        }
         usvg::NodeKind::Image(ref img) => {
             let segments = utils::rect_to_path(img.view_box.rect);
             utils::path_bbox(&segments, None, Some(ts2))
@@ -392,39 +359,6 @@ fn _calc_node_bbox(
     }
 }
 
-fn from_qt_path(p_path: &qt::PainterPath) -> Vec<usvg::PathSegment> {
-    let mut segments = Vec::with_capacity(p_path.len() as usize);
-    let p_path_len = p_path.len();
-    let mut i = 0;
-    while i < p_path_len {
-        let (kind, x, y) = p_path.get(i);
-        match kind {
-            qt::PathSegmentType::MoveTo => {
-                segments.push(usvg::PathSegment::MoveTo { x, y });
-            }
-            qt::PathSegmentType::LineTo => {
-                segments.push(usvg::PathSegment::LineTo { x, y });
-            }
-            qt::PathSegmentType::CurveTo => {
-                let (_, x1, y1) = p_path.get(i + 1);
-                let (_, x2, y2) = p_path.get(i + 2);
-
-                segments.push(usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y });
-
-                i += 2;
-            }
-        }
-
-        i += 1;
-    }
-
-    if segments.len() == 1 {
-        segments.clear();
-    }
-
-    segments
-}
-
 fn create_layers(img_size: ScreenSize, opt: &Options) -> QtLayers {
     layers::Layers::new(img_size, opt.usvg.dpi, create_subimage, clear_image)
 }
diff --git a/src/backend_qt/text.rs b/src/backend_qt/text.rs
deleted file mode 100644
index 19a6f9e76..000000000
--- a/src/backend_qt/text.rs
+++ /dev/null
@@ -1,195 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-// external
-use crate::qt;
-
-// self
-use super::prelude::*;
-use crate::backend_utils;
-use crate::backend_utils::text::{
-    self,
-    FontMetrics,
-};
-use super::style;
-
-
-pub struct QtFontMetrics<'a> {
-    p: &'a mut qt::Painter,
-}
-
-impl<'a> QtFontMetrics<'a> {
-    pub fn new(p: &'a mut qt::Painter) -> Self {
-        QtFontMetrics { p }
-    }
-}
-
-impl<'a> FontMetrics<qt::Font> for QtFontMetrics<'a> {
-    fn set_font(&mut self, font: &usvg::Font) {
-        let font = init_font(font);
-        self.p.set_font(&font);
-    }
-
-    fn font(&self) -> qt::Font {
-        self.p.font()
-    }
-
-    fn width(&self, text: &str) -> f64 {
-        self.p.font_metrics().width(text)
-    }
-
-    fn ascent(&self, _: &str) -> f64 {
-        self.p.font_metrics().ascent()
-    }
-
-    fn height(&self) -> f64 {
-        self.p.font_metrics().height()
-    }
-}
-
-pub fn draw(
-    tree: &usvg::Tree,
-    text: &usvg::Text,
-    opt: &Options,
-    p: &mut qt::Painter,
-) -> Rect {
-    p.set_antialiasing(backend_utils::use_text_antialiasing(text.rendering_mode));
-
-    let (blocks, text_bbox) = text::prepare_blocks(text, &mut QtFontMetrics::new(p));
-    text::draw_blocks(blocks, |block| draw_block(tree, block, text_bbox, opt, p));
-
-    // Revert anti-aliasing.
-    p.set_antialiasing(true);
-
-    text_bbox
-}
-
-fn draw_block(
-    tree: &usvg::Tree,
-    block: &text::TextBlock<qt::Font>,
-    text_bbox: Rect,
-    opt: &Options,
-    p: &mut qt::Painter,
-) {
-    // `tspan` doesn't have a bbox by the SVG spec and should use the whole `text` bbox.
-    // That's why we are using `text_bbox` instead of `block.bbox`.
-
-    p.set_font(&block.font);
-    let font_metrics = p.font_metrics();
-
-    let bbox = block.bbox;
-
-    let old_ts = p.get_transform();
-
-    if let Some(rotate) = block.rotate {
-        let ts = usvg::Transform::new_rotate_at(rotate, bbox.x(), bbox.y() + block.font_ascent);
-        p.apply_transform(&ts.to_native());
-    }
-
-    let line_rect = Rect::new(bbox.x(), bbox.y(), bbox.width(), font_metrics.line_width());
-    let line_rect = try_opt!(line_rect, ());
-
-    // Draw underline.
-    //
-    // Should be drawn before/under text.
-    if let Some(ref style) = block.decoration.underline {
-        let ty = font_metrics.height() - font_metrics.underline_pos();
-        draw_line(tree, line_rect.translate(0.0, ty), text_bbox, &style.fill, &style.stroke, opt, p);
-    }
-
-    // Draw overline.
-    //
-    // Should be drawn before/under text.
-    if let Some(ref style) = block.decoration.overline {
-        let ty = font_metrics.height() - font_metrics.overline_pos();
-        draw_line(tree, line_rect.translate(0.0, ty), text_bbox, &style.fill, &style.stroke, opt, p);
-    }
-
-    // Draw text.
-    style::fill(tree, &block.fill, opt, text_bbox, p);
-    style::stroke(tree, &block.stroke, opt, text_bbox, p);
-
-    p.draw_text(bbox.x(), bbox.y(), &block.text);
-
-    // Draw line-through.
-    //
-    // Should be drawn after/over text.
-    if let Some(ref style) = block.decoration.line_through {
-        let ty = font_metrics.ascent() - font_metrics.strikeout_pos();
-        draw_line(tree, line_rect.translate(0.0, ty), text_bbox, &style.fill, &style.stroke, opt, p);
-    }
-
-    p.set_transform(&old_ts);
-}
-
-fn init_font(dom_font: &usvg::Font) -> qt::Font {
-    let mut font = qt::Font::new();
-
-    font.set_family(&dom_font.family);
-
-    let font_style = match dom_font.style {
-        usvg::FontStyle::Normal => qt::FontStyle::Normal,
-        usvg::FontStyle::Italic => qt::FontStyle::Italic,
-        usvg::FontStyle::Oblique => qt::FontStyle::Oblique,
-    };
-    font.set_style(font_style);
-
-    if dom_font.variant == usvg::FontVariant::SmallCaps {
-        font.set_small_caps(true);
-    }
-
-    let font_weight = match dom_font.weight {
-        usvg::FontWeight::W100       => qt::FontWeight::Thin,
-        usvg::FontWeight::W200       => qt::FontWeight::ExtraLight,
-        usvg::FontWeight::W300       => qt::FontWeight::Light,
-        usvg::FontWeight::W400       => qt::FontWeight::Normal,
-        usvg::FontWeight::W500       => qt::FontWeight::Medium,
-        usvg::FontWeight::W600       => qt::FontWeight::DemiBold,
-        usvg::FontWeight::W700       => qt::FontWeight::Bold,
-        usvg::FontWeight::W800       => qt::FontWeight::ExtraBold,
-        usvg::FontWeight::W900       => qt::FontWeight::Black,
-    };
-    font.set_weight(font_weight);
-
-    let font_stretch = match dom_font.stretch {
-        usvg::FontStretch::Normal         => qt::FontStretch::Unstretched,
-        usvg::FontStretch::Narrower |
-        usvg::FontStretch::Condensed      => qt::FontStretch::Condensed,
-        usvg::FontStretch::UltraCondensed => qt::FontStretch::UltraCondensed,
-        usvg::FontStretch::ExtraCondensed => qt::FontStretch::ExtraCondensed,
-        usvg::FontStretch::SemiCondensed  => qt::FontStretch::SemiCondensed,
-        usvg::FontStretch::SemiExpanded   => qt::FontStretch::SemiExpanded,
-        usvg::FontStretch::Wider |
-        usvg::FontStretch::Expanded       => qt::FontStretch::Expanded,
-        usvg::FontStretch::ExtraExpanded  => qt::FontStretch::ExtraExpanded,
-        usvg::FontStretch::UltraExpanded  => qt::FontStretch::UltraExpanded,
-    };
-    font.set_stretch(font_stretch);
-
-    if let Some(letter_spacing) = dom_font.letter_spacing {
-        font.set_letter_spacing(letter_spacing);
-    }
-
-    if let Some(word_spacing) = dom_font.word_spacing {
-        font.set_word_spacing(word_spacing);
-    }
-
-    font.set_size(dom_font.size.value());
-
-    font
-}
-
-fn draw_line(
-    tree: &usvg::Tree,
-    r: Rect,
-    text_bbox: Rect,
-    fill: &Option<usvg::Fill>,
-    stroke: &Option<usvg::Stroke>,
-    opt: &Options,
-    p: &mut qt::Painter,
-) {
-    style::fill(tree, fill, opt, text_bbox, p);
-    style::stroke(tree, stroke, opt, text_bbox, p);
-    p.draw_rect(r.x(), r.y(), r.width() + 1.0, r.height());
-}
diff --git a/src/backend_utils/mod.rs b/src/backend_utils/mod.rs
index 71f5e8186..12697b3de 100644
--- a/src/backend_utils/mod.rs
+++ b/src/backend_utils/mod.rs
@@ -5,7 +5,6 @@
 pub mod filter;
 pub mod image;
 pub mod mask;
-pub mod text;
 
 mod prelude {
     pub use super::super::prelude::*;
@@ -21,12 +20,7 @@ pub fn use_shape_antialiasing(
     }
 }
 
-pub fn use_text_antialiasing(
-    mode: usvg::TextRendering,
-) -> bool {
-    match mode {
-        usvg::TextRendering::OptimizeSpeed         => false,
-        usvg::TextRendering::OptimizeLegibility    => true,
-        usvg::TextRendering::GeometricPrecision    => true,
-    }
+pub trait ConvTransform<T> {
+    fn to_native(&self) -> T;
+    fn from_native(_: &T) -> Self;
 }
diff --git a/src/backend_utils/text.rs b/src/backend_utils/text.rs
deleted file mode 100644
index 4c316a219..000000000
--- a/src/backend_utils/text.rs
+++ /dev/null
@@ -1,168 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-// self
-use super::prelude::*;
-
-
-pub struct TextBlock<Font> {
-    pub text: String,
-    pub is_visible: bool,
-    pub bbox: Rect,
-    pub rotate: Option<f64>,
-    pub fill: Option<usvg::Fill>,
-    pub stroke: Option<usvg::Stroke>,
-    pub font: Font,
-    pub font_ascent: f64,
-    pub letter_spacing: Option<f64>,
-    pub word_spacing: Option<f64>,
-    pub decoration: usvg::TextDecoration,
-}
-
-pub trait FontMetrics<Font> {
-    fn set_font(&mut self, font: &usvg::Font);
-    fn font(&self) -> Font;
-    fn width(&self, text: &str) -> f64;
-    fn ascent(&self, text: &str) -> f64;
-    fn height(&self) -> f64;
-}
-
-pub fn draw_blocks<Font, Draw>(
-    blocks: Vec<TextBlock<Font>>,
-    mut draw: Draw,
-)
-    where Draw: FnMut(&TextBlock<Font>)
-{
-    for block in blocks {
-        if block.is_visible {
-            draw(&block);
-        }
-    }
-}
-
-pub fn prepare_blocks<Font>(
-    text_kind: &usvg::Text,
-    font_metrics: &mut FontMetrics<Font>,
-) -> (Vec<TextBlock<Font>>, Rect) {
-    let mut buf_str = String::with_capacity(4);
-    let mut blocks: Vec<TextBlock<Font>> = Vec::new();
-    let mut last_x = 0.0;
-    let mut last_y = 0.0;
-    let mut char_idx = 0;
-    for chunk in &text_kind.chunks {
-        let chunk_x = chunk.x.unwrap_or(last_x) + chunk.dx.unwrap_or(0.0);
-        let mut x = chunk_x;
-        let y = chunk.y.unwrap_or(last_y) + chunk.dy.unwrap_or(0.0);
-        let start_idx = blocks.len();
-
-        for tspan in &chunk.spans {
-            font_metrics.set_font(&tspan.font);
-
-            for (i, c) in tspan.text.chars().enumerate() {
-                let mut has_custom_offset = i == 0;
-
-                if text_kind.rotate.is_some() {
-                    has_custom_offset = true;
-                }
-
-                let can_merge = !blocks.is_empty() && !has_custom_offset;
-                if can_merge {
-                    let prev_idx = blocks.len() - 1;
-                    blocks[prev_idx].text.push(c);
-
-                    let mut width = font_metrics.width(&blocks[prev_idx].text);
-
-                    // Width can be negative when letter-spacing is negative.
-                    // Not sure what should be done in this case.
-                    if !width.is_valid_length() {
-                        width = 1.0;
-                    }
-
-                    {
-                        let r = blocks[prev_idx].bbox;
-                        let r = Rect::new(r.x(), r.y(), width, r.height()).unwrap();
-                        blocks[prev_idx].bbox = r;
-                    }
-
-                    let mut new_width = chunk_x;
-                    for block in blocks.iter().skip(start_idx) {
-                        new_width += block.bbox.width();
-                    }
-
-                    x = new_width;
-                } else {
-                    buf_str.clear();
-                    buf_str.push(c);
-
-                    let mut width = font_metrics.width(&buf_str);
-
-                    // Width can be negative when letter-spacing is negative.
-                    // Not sure what should be done in this case.
-                    if !width.is_valid_length() {
-                        width = 1.0;
-                    }
-
-                    let font_ascent = font_metrics.ascent(&buf_str);
-                    let yy = y - font_ascent - tspan.baseline_shift;
-                    let height = font_metrics.height();
-                    let bbox = Rect::new(x, yy, width, height).unwrap();
-                    x += width;
-
-                    let mut rotate = None;
-                    if let Some(ref list) = text_kind.rotate {
-                        if let Some(angle) = list.get(char_idx) {
-                            if !angle.is_fuzzy_zero() {
-                                rotate = Some(*angle);
-                            }
-                        }
-                    }
-
-                    blocks.push(TextBlock {
-                        text: c.to_string(),
-                        is_visible: tspan.visibility == usvg::Visibility::Visible,
-                        bbox,
-                        rotate,
-                        fill: tspan.fill.clone(),
-                        stroke: tspan.stroke.clone(),
-                        font: font_metrics.font(),
-                        font_ascent,
-                        letter_spacing: tspan.font.letter_spacing,
-                        word_spacing: tspan.font.word_spacing,
-                        decoration: tspan.decoration.clone(),
-                    });
-                }
-
-                char_idx += 1;
-            }
-        }
-
-        let mut chunk_w = 0.0;
-        for block in blocks.iter().skip(start_idx) {
-            chunk_w += block.bbox.width();
-        }
-
-        let adx = process_text_anchor(chunk.anchor, chunk_w);
-        for block in blocks.iter_mut().skip(start_idx) {
-            block.bbox = block.bbox.translate(-adx, 0.0);
-        }
-
-        last_x = chunk_x + chunk_w - adx;
-        last_y = y;
-    }
-
-    let mut text_bbox = Rect::new_bbox();
-    for block in &blocks {
-        text_bbox = text_bbox.expand(block.bbox);
-    }
-
-    (blocks, text_bbox)
-}
-
-fn process_text_anchor(a: usvg::TextAnchor, text_width: f64) -> f64 {
-    match a {
-        usvg::TextAnchor::Start =>  0.0, // Nothing.
-        usvg::TextAnchor::Middle => text_width / 2.0,
-        usvg::TextAnchor::End =>    text_width,
-    }
-}
diff --git a/src/geom.rs b/src/geom.rs
index d0906adf8..e05e47e41 100644
--- a/src/geom.rs
+++ b/src/geom.rs
@@ -9,10 +9,7 @@ use std::f64;
 use std::fmt;
 
 // external
-use usvg::{
-    self,
-    FuzzyEq,
-};
+use usvg;
 pub use usvg::{
     Line,
     Rect,
@@ -116,14 +113,6 @@ fn size_scale(s1: ScreenSize, s2: ScreenSize, expand: bool) -> ScreenSize {
 
 /// Additional `Rect` methods.
 pub trait RectExt: Sized {
-    /// Creates a new `Rect` for bounding box calculation.
-    ///
-    /// Shorthand for `Rect::new(f64::MAX, f64::MAX, 1.0, 1.0)`.
-    fn new_bbox() -> Self;
-
-    /// Expands the `Rect` to the provided size.
-    fn expand(&self, r: Rect) -> Self;
-
     /// Transforms the `Rect` using the provided `bbox`.
     fn bbox_transform(&self, bbox: Rect) -> Self;
 
@@ -140,24 +129,6 @@ pub trait RectExt: Sized {
 }
 
 impl RectExt for Rect {
-    fn new_bbox() -> Self {
-        Rect::new(f64::MAX, f64::MAX, 1.0, 1.0).unwrap()
-    }
-
-    fn expand(&self, r: Rect) -> Self {
-        if self.fuzzy_eq(&Rect::new_bbox()) {
-            r
-        } else {
-            let x1 = f64_min(self.x(), r.x());
-            let y1 = f64_min(self.y(), r.y());
-
-            let x2 = f64_max(self.right(), r.right());
-            let y2 = f64_max(self.bottom(), r.bottom());
-
-            Rect::new(x1, y1, x2 - x1, y2 - y1).unwrap()
-        }
-    }
-
     fn bbox_transform(&self, bbox: Rect) -> Self {
         let x = self.x() * bbox.width() + bbox.x();
         let y = self.y() * bbox.height() + bbox.y();
@@ -344,17 +315,6 @@ impl fmt::Display for ScreenRect {
 }
 
 
-#[inline]
-fn f64_min(v1: f64, v2: f64) -> f64 {
-    if v1 < v2 { v1 } else { v2 }
-}
-
-#[inline]
-fn f64_max(v1: f64, v2: f64) -> f64 {
-    if v1 > v2 { v1 } else { v2 }
-}
-
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/lib.rs b/src/lib.rs
index 64f64e004..fcad524f4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -32,8 +32,6 @@ pub use usvg::{
     Error,
 };
 
-use usvg::lyon_geom;
-
 
 #[cfg(feature = "cairo-backend")] pub mod backend_cairo;
 #[cfg(feature = "qt-backend")] pub mod backend_qt;
@@ -43,7 +41,6 @@ mod backend_utils;
 mod geom;
 mod layers;
 mod options;
-mod traits;
 
 /// Commonly used types and traits.
 pub mod prelude {
@@ -52,7 +49,6 @@ pub mod prelude {
     pub use usvg::prelude::*;
     pub use usvg::{try_opt, try_opt_warn};
     pub use crate::geom::*;
-    pub(crate) use crate::traits::*;
     pub use crate::utils;
     pub use crate::Options;
     pub use crate::Render;
@@ -116,43 +112,6 @@ pub trait OutputImage {
 }
 
 
-/// A global library handle.
-pub struct InitObject {
-    #[cfg(feature = "qt-backend")]
-    #[allow(dead_code)]
-    handle: qt::GuiApp,
-}
-
-/// Creates a global library handle.
-///
-/// Must be invoked before any other `resvg` code.
-///
-/// Currently, handles `QGuiApplication` object which must be created
-/// in order to draw text. If you don't plan to draw text - it's better to skip
-/// the initialization.
-///
-/// Does nothing when only the `cairo` backend is enabled.
-///
-/// Note: `QGuiApplication` initialization is pretty slow (up to 100ms).
-///
-/// # Example
-///
-/// ```
-/// let _resvg = resvg::init();
-///
-/// // other code
-/// ```
-///
-/// Also, take a look at `examples/minimal.rs`.
-///
-/// **Warning**: this method is not thread-safe.
-pub fn init() -> InitObject {
-    InitObject {
-        #[cfg(feature = "qt-backend")]
-        handle: qt::GuiApp::new("resvg"),
-    }
-}
-
 /// Returns default backend.
 ///
 /// - If both backends are enabled - cairo backend will be returned.
diff --git a/src/traits.rs b/src/traits.rs
deleted file mode 100644
index a53bd2a28..000000000
--- a/src/traits.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-// external
-use usvg;
-
-// self
-use crate::geom::*;
-
-
-pub(crate) trait ConvTransform<T> {
-    fn to_native(&self) -> T;
-    fn from_native(_: &T) -> Self;
-}
-
-
-pub(crate) trait TransformFromBBox: Sized {
-    fn from_bbox(bbox: Rect) -> Self;
-}
-
-impl TransformFromBBox for usvg::Transform {
-    fn from_bbox(bbox: Rect) -> Self {
-        Self::new(bbox.width(), 0.0, 0.0, bbox.height(), bbox.x(), bbox.y())
-    }
-}
diff --git a/src/utils.rs b/src/utils.rs
index bbc5d3c68..46303d625 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -69,233 +69,3 @@ pub fn abs_transform(
 
     root_ts
 }
-
-/// Calculates path's bounding box.
-///
-/// Width and/or height can be zero.
-pub fn path_bbox(
-    segments: &[usvg::PathSegment],
-    stroke: Option<&usvg::Stroke>,
-    ts: Option<usvg::Transform>,
-) -> Option<Rect> {
-    debug_assert!(!segments.is_empty());
-
-    let ts = match ts {
-        Some(ts) => ts,
-        None => usvg::Transform::default(),
-    };
-
-    use crate::lyon_geom;
-
-    let mut prev_x = 0.0;
-    let mut prev_y = 0.0;
-    let mut minx = 0.0;
-    let mut miny = 0.0;
-    let mut maxx = 0.0;
-    let mut maxy = 0.0;
-
-    if let Some(usvg::PathSegment::MoveTo { x, y }) = TransformedPath::new(segments, ts).next() {
-        let x = x as f32;
-        let y = y as f32;
-
-        prev_x = x;
-        prev_y = y;
-        minx = x;
-        miny = y;
-        maxx = x;
-        maxy = y;
-    }
-
-    for seg in TransformedPath::new(segments, ts) {
-        match seg {
-              usvg::PathSegment::MoveTo { x, y }
-            | usvg::PathSegment::LineTo { x, y } => {
-                let x = x as f32;
-                let y = y as f32;
-                prev_x = x;
-                prev_y = y;
-
-                     if x > maxx { maxx = x; }
-                else if x < minx { minx = x; }
-
-                     if y > maxy { maxy = y; }
-                else if y < miny { miny = y; }
-            }
-            usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
-                let x = x as f32;
-                let y = y as f32;
-
-                let curve = lyon_geom::CubicBezierSegment {
-                    from: lyon_geom::math::Point::new(prev_x, prev_y),
-                    ctrl1: lyon_geom::math::Point::new(x1 as f32, y1 as f32),
-                    ctrl2: lyon_geom::math::Point::new(x2 as f32, y2 as f32),
-                    to: lyon_geom::math::Point::new(x, y),
-                };
-
-                prev_x = x;
-                prev_y = y;
-
-                let r = curve.bounding_rect();
-
-                let right = r.max_x();
-                let bottom = r.max_y();
-                if r.min_x() < minx { minx = r.min_x(); }
-                if right > maxx { maxx = right; }
-                if r.min_y() < miny { miny = r.min_y(); }
-                if bottom > maxy { maxy = bottom; }
-            }
-            usvg::PathSegment::ClosePath => {}
-        }
-    }
-
-    // TODO: find a better way
-    // It's an approximation, but it's better than nothing.
-    if let Some(ref stroke) = stroke {
-        let w = (stroke.width.value() / 2.0) as f32;
-        minx -= w;
-        miny -= w;
-        maxx += w;
-        maxy += w;
-    }
-
-    let width = maxx - minx;
-    let height = maxy - miny;
-
-    Rect::new(minx as f64, miny as f64, width as f64, height as f64)
-}
-
-/// Calculates path's length.
-///
-/// Length from the first segment to the first MoveTo, ClosePath or slice end.
-pub fn path_length(segments: &[usvg::PathSegment]) -> f64 {
-    debug_assert!(!segments.is_empty());
-
-    use crate::lyon_geom;
-
-    let (mut prev_x, mut prev_y) = {
-        if let usvg::PathSegment::MoveTo { x, y } = segments[0] {
-            (x as f32, y as f32)
-        } else {
-            panic!("first segment must be MoveTo");
-        }
-    };
-
-    let start_x = prev_x;
-    let start_y = prev_y;
-
-    let mut is_first_seg = true;
-    let mut length = 0.0f64;
-    for seg in segments {
-        match *seg {
-            usvg::PathSegment::MoveTo { .. } => {
-                if !is_first_seg {
-                    break;
-                }
-            }
-            usvg::PathSegment::LineTo { x, y } => {
-                length += Line::new(prev_x as f64, prev_y as f64, x, y).length();
-
-                prev_x = x as f32;
-                prev_y = y as f32;
-            }
-            usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
-                let x = x as f32;
-                let y = y as f32;
-
-                let curve = lyon_geom::CubicBezierSegment {
-                    from: lyon_geom::math::Point::new(prev_x, prev_y),
-                    ctrl1: lyon_geom::math::Point::new(x1 as f32, y1 as f32),
-                    ctrl2: lyon_geom::math::Point::new(x2 as f32, y2 as f32),
-                    to: lyon_geom::math::Point::new(x, y),
-                };
-
-                length += curve.approximate_length(1.0) as f64;
-
-                prev_x = x;
-                prev_y = y;
-            }
-            usvg::PathSegment::ClosePath => {
-                length += Line::new(prev_x as f64, prev_y as f64,
-                                    start_x as f64, start_y as f64).length();
-                break;
-            }
-        }
-
-        is_first_seg = false;
-    }
-
-    length
-}
-
-/// Applies the transform to the path segments.
-pub fn transform_path(segments: &mut [usvg::PathSegment], ts: &usvg::Transform) {
-    for seg in segments {
-        match *seg {
-            usvg::PathSegment::MoveTo { ref mut x, ref mut y } => {
-                ts.apply_to(x, y);
-            }
-            usvg::PathSegment::LineTo { ref mut x, ref mut y } => {
-                ts.apply_to(x, y);
-            }
-            usvg::PathSegment::CurveTo { ref mut x1, ref mut y1, ref mut x2,
-                                         ref mut y2, ref mut x, ref mut y } => {
-                ts.apply_to(x1, y1);
-                ts.apply_to(x2, y2);
-                ts.apply_to(x, y);
-            }
-            usvg::PathSegment::ClosePath => {}
-        }
-    }
-}
-
-
-/// An iterator over transformed path segments.
-pub struct TransformedPath<'a> {
-    segments: &'a [usvg::PathSegment],
-    ts: usvg::Transform,
-    idx: usize,
-}
-
-impl<'a> TransformedPath<'a> {
-    /// Creates a new `TransformedPath` iterator.
-    pub fn new(segments: &'a [usvg::PathSegment], ts: usvg::Transform) -> Self {
-        TransformedPath { segments, ts, idx: 0 }
-    }
-}
-
-impl<'a> Iterator for TransformedPath<'a> {
-    type Item = usvg::PathSegment;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.idx == self.segments.len() {
-            return None;
-        }
-
-        if self.ts.is_default() {
-            self.idx += 1;
-            return self.segments.get(self.idx - 1).cloned();
-        }
-
-        let seg = match self.segments[self.idx] {
-            usvg::PathSegment::MoveTo { x, y } => {
-                let (x, y) = self.ts.apply(x, y);
-                usvg::PathSegment::MoveTo { x, y }
-            }
-            usvg::PathSegment::LineTo { x, y } => {
-                let (x, y) = self.ts.apply(x, y);
-                usvg::PathSegment::LineTo { x, y }
-            }
-            usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
-                let (x1, y1) = self.ts.apply(x1, y1);
-                let (x2, y2) = self.ts.apply(x2, y2);
-                let (x,  y)  = self.ts.apply(x, y);
-                usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y }
-            }
-            usvg::PathSegment::ClosePath => usvg::PathSegment::ClosePath,
-        };
-
-        self.idx += 1;
-
-        Some(seg)
-    }
-}
diff --git a/tools/rendersvg/src/main.rs b/tools/rendersvg/src/main.rs
index 7937a6ef2..1a4abf8a1 100644
--- a/tools/rendersvg/src/main.rs
+++ b/tools/rendersvg/src/main.rs
@@ -75,10 +75,6 @@ fn process() -> Result<(), String> {
         usvg::Tree::from_file(&args.in_svg, &opt.usvg).map_err(|e| e.to_string())
     })?;
 
-    // We have to init only Qt backend.
-    #[cfg(feature = "qt-backend")]
-    let _resvg = timed!("Backend init", init_qt_gui(&tree, &args));
-
     if args.query_all {
         return query_all(backend, &tree, &opt);
     }
@@ -113,31 +109,6 @@ fn process() -> Result<(), String> {
     Ok(())
 }
 
-// Qt backend initialization is pretty slow
-// and needed only for files with text nodes.
-// So we skip it file doesn't have one.
-#[cfg(feature = "qt-backend")]
-fn init_qt_gui(
-    tree: &usvg::Tree,
-    args: &args::Args,
-) -> Option<resvg::InitObject> {
-    if args.backend_name != "qt" {
-        return None;
-    }
-
-    // Check that tree has any text nodes.
-    let has_text = tree.root().descendants().any(|n|
-        if let usvg::NodeKind::Text { .. } = *n.borrow() { true } else { false }
-    );
-
-    if has_text {
-        // Init Qt backend.
-        Some(resvg::init())
-    } else {
-        None
-    }
-}
-
 fn query_all(
     backend: Box<Render>,
     tree: &usvg::Tree,
diff --git a/usvg/Cargo.toml b/usvg/Cargo.toml
index 40517428d..cea62190d 100644
--- a/usvg/Cargo.toml
+++ b/usvg/Cargo.toml
@@ -19,11 +19,17 @@ travis-ci = { repository = "RazrFalcon/resvg" }
 
 [dependencies]
 base64 = "0.10"
+font-kit = { git = "https://github.com/pcwalton/font-kit", rev = "45de005" }
+harfbuzz_rs = "1.0"
 libflate = "0.1"
 log = "0.4"
 lyon_geom = "0.12"
+lyon_path = "0.12" # the same version as used by font-kit
 rctree = "0.3"
-svgdom = { git = "https://github.com/RazrFalcon/svgdom", rev = "f12882c" }
+svgdom = { git = "https://github.com/RazrFalcon/svgdom" }
+unicode-bidi = "0.3"
+unicode-script = "0.3"
+unicode-vo = { git = "https://github.com/RazrFalcon/unicode-vo" }
 
 [dev-dependencies]
 pretty_assertions = "0.6"
diff --git a/usvg/src/convert/mod.rs b/usvg/src/convert/mod.rs
index 69fafb4db..7091304d4 100644
--- a/usvg/src/convert/mod.rs
+++ b/usvg/src/convert/mod.rs
@@ -2,6 +2,9 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+use std::cell::RefCell;
+use std::rc::Rc;
+
 // external
 use svgdom::{
     self,
@@ -22,6 +25,7 @@ use crate::{
 };
 pub use self::preprocess::prepare_doc;
 
+
 mod clip_and_mask;
 mod filter;
 mod image;
@@ -38,6 +42,7 @@ mod units;
 mod use_node;
 
 mod prelude {
+    pub use log::warn;
     pub use svgdom::{
         AttributeType,
         ElementType,
@@ -53,7 +58,7 @@ mod prelude {
     pub use crate::Options;
     pub use super::svgdom_ext::*;
     pub use super::State;
-    pub use log::warn;
+    pub use super::FontCache;
 }
 
 use self::svgdom_ext::*;
@@ -64,6 +69,7 @@ pub struct State<'a> {
     current_root: svgdom::Node,
     size: Size,
     view_box: Rect,
+    font_cache: Rc<RefCell<FontCache>>,
     opt: &'a Options,
 }
 
@@ -74,6 +80,31 @@ impl<'a> State<'a> {
 }
 
 
+pub struct FontCache {
+    fonts: Vec<font_kit::handle::Handle>,
+}
+
+impl FontCache {
+    fn new() -> Self {
+        FontCache {
+            fonts: Vec::new(),
+        }
+    }
+
+    fn init(&mut self) {
+        if self.fonts.is_empty() {
+            if let Ok(v) = font_kit::source::SystemSource::new().all_fonts() {
+                self.fonts = v;
+            }
+        }
+    }
+
+    fn fonts(&self) -> &[font_kit::handle::Handle] {
+        &self.fonts
+    }
+}
+
+
 /// Converts an input `Document` into a `Tree`.
 ///
 /// # Errors
@@ -124,6 +155,7 @@ pub fn convert_doc(
         current_root: svg.clone(),
         size,
         view_box: view_box.rect,
+        font_cache: Rc::new(RefCell::new(FontCache::new())),
         opt: &opt,
     };
 
@@ -143,6 +175,7 @@ fn resolve_svg_size(svg: &svgdom::Node, opt: &Options) -> Result<Size, Error> {
         current_root: svg.clone(),
         size: Size::new(100.0, 100.0).unwrap(),
         view_box: Rect::new(0.0, 0.0, 100.0, 100.0).unwrap(),
+        font_cache: Rc::new(RefCell::new(FontCache::new())),
         opt,
     };
 
@@ -431,7 +464,7 @@ fn ungroup_groups(tree: &mut tree::Tree, opt: &Options) {
                 && g.clip_path.is_none()
                 && g.mask.is_none()
                 && g.filter.is_none()
-                && !opt.keep_named_groups
+                && !(opt.keep_named_groups && !g.id.is_empty())
             } else {
                 false
             };
@@ -446,9 +479,6 @@ fn ungroup_groups(tree: &mut tree::Tree, opt: &Options) {
                         tree::NodeKind::Path(ref mut path) => {
                             path.transform.prepend(&ts);
                         }
-                        tree::NodeKind::Text(ref mut text) => {
-                            text.transform.prepend(&ts);
-                        }
                         tree::NodeKind::Image(ref mut img) => {
                             img.transform.prepend(&ts);
                         }
@@ -513,14 +543,6 @@ fn remove_unused_defs(tree: &mut tree::Tree) {
                     check_paint_id!(path.fill, id);
                     check_paint_id!(path.stroke, id);
                 }
-                tree::NodeKind::Text(ref text) => {
-                    for chunk in &text.chunks {
-                        for span in &chunk.spans {
-                            check_paint_id!(span.fill, id);
-                            check_paint_id!(span.stroke, id);
-                        }
-                    }
-                }
                 tree::NodeKind::Group(ref g) => {
                     check_id!(g.clip_path, id);
                     check_id!(g.mask, id);
diff --git a/usvg/src/convert/path.rs b/usvg/src/convert/path.rs
index 30cfa6e2d..17d359999 100644
--- a/usvg/src/convert/path.rs
+++ b/usvg/src/convert/path.rs
@@ -13,7 +13,7 @@ use crate::tree;
 use super::prelude::*;
 
 
-pub fn convert_path(mut path: svgdom::Path) -> Vec<tree::PathSegment> {
+pub fn convert(mut path: svgdom::Path) -> Vec<tree::PathSegment> {
     let mut new_path = Vec::with_capacity(path.len());
 
     path.conv_to_absolute();
diff --git a/usvg/src/convert/shapes.rs b/usvg/src/convert/shapes.rs
index 69fb9d147..41611a4db 100644
--- a/usvg/src/convert/shapes.rs
+++ b/usvg/src/convert/shapes.rs
@@ -35,7 +35,7 @@ pub fn convert_path(
     node: &svgdom::Node,
 ) -> Option<Vec<tree::PathSegment>> {
     if let Some(AValue::Path(path)) = node.attributes().get_value(AId::D).cloned() {
-        let new_path = super::path::convert_path(path);
+        let new_path = super::path::convert(path);
         if new_path.len() >= 2 {
             return Some(new_path);
         }
@@ -117,7 +117,7 @@ pub fn convert_rect(
             .arc_to(rx, ry, 0.0, false, true, x + rx, y)
             .finalize();
 
-        path::convert_path(p)
+        path::convert(p)
     };
 
     Some(path)
@@ -231,7 +231,7 @@ pub fn convert_ellipse(
 }
 
 fn ellipse_to_path(cx: f64, cy: f64, rx: f64, ry: f64) -> Vec<tree::PathSegment> {
-    path::convert_path(svgdom::PathBuilder::with_capacity(6)
+    path::convert(svgdom::PathBuilder::with_capacity(6)
         .move_to(cx + rx, cy)
         .arc_to(rx, ry, 0.0, false, true, cx,      cy + ry)
         .arc_to(rx, ry, 0.0, false, true, cx - rx, cy)
diff --git a/usvg/src/convert/text.rs b/usvg/src/convert/text.rs
deleted file mode 100644
index d3160f20b..000000000
--- a/usvg/src/convert/text.rs
+++ /dev/null
@@ -1,497 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-use std::cmp;
-
-// external
-use svgdom;
-
-// self
-use crate::tree;
-use crate::tree::prelude::*;
-use super::prelude::*;
-use super::{
-    style,
-    units,
-};
-
-
-pub fn convert(
-    node: &svgdom::Node,
-    state: &State,
-    parent: &mut tree::Node,
-    tree: &mut tree::Tree,
-) {
-    let pos_list = resolve_positions_list(node, state);
-    let rotate_list = resolve_rotate_list(node);
-    let text_ts = node.attributes().get_transform(AId::Transform);
-
-    let chunks = collect_text_chunks(node, &pos_list, &rotate_list, state, tree);
-    if chunks.is_empty() {
-        return;
-    }
-
-    let rotate = if rotate_list.iter().any(|v| !v.is_fuzzy_zero()) {
-        Some(rotate_list)
-    } else {
-        None
-    };
-
-    let rendering_mode = node.try_find_enum(AId::TextRendering)
-                             .unwrap_or(state.opt.text_rendering);
-
-    parent.append_kind(tree::NodeKind::Text(tree::Text {
-        id: node.id().clone(),
-        transform: text_ts,
-        rotate,
-        rendering_mode,
-        chunks,
-    }));
-}
-
-fn collect_text_chunks(
-    text_elem: &svgdom::Node,
-    pos_list: &[CharacterPosition],
-    rotate_list: &[f64],
-    state: &State,
-    tree: &mut tree::Tree,
-) -> Vec<tree::TextChunk> {
-    // Update state for a proper fill-rule resolving.
-    let mut text_state = state.clone();
-    text_state.current_root = text_elem.clone();
-
-    let mut chunks = Vec::new();
-    let mut chars_count = 0;
-    for child in text_elem.descendants().filter(|n| n.is_text()) {
-        let ref parent = child.parent().unwrap();
-
-        if !style::is_visible_element(parent, state.opt) {
-            chars_count += child.text().chars().count();
-            continue;
-        }
-
-        let font = match resolve_font(parent, state) {
-            Some(v) => v,
-            None => {
-                chars_count += child.text().chars().count();
-                continue;
-            }
-        };
-
-        let anchor = parent.find_enum(AId::TextAnchor);
-        let span = tree::TextSpan {
-            visibility: parent.find_enum(AId::Visibility),
-            fill: style::resolve_fill(parent, true, &text_state, tree),
-            stroke: style::resolve_stroke(parent, true, state, tree),
-            font,
-            baseline_shift: resolve_baseline_shift(parent, state),
-            decoration: resolve_decoration(text_elem, parent, state, tree),
-            text: String::new(),
-        };
-
-        let mut is_new_span = true;
-        for c in child.text().chars() {
-            // Create a new chunk if:
-            // - this is the first span (yes, position can be None)
-            // - text character has an absolute coordinate assigned to it (via x/y attribute)
-            // TODO: technically, only x and y should affect text chunk creation, but
-            //       resvg doesn't support this yet.
-            let is_new_chunk =    pos_list[chars_count].x.is_some()
-                               || pos_list[chars_count].y.is_some()
-                               || pos_list[chars_count].dx.is_some()
-                               || pos_list[chars_count].dy.is_some()
-                               || !rotate_list[chars_count].is_fuzzy_zero()
-                               || chunks.is_empty();
-
-            if is_new_chunk {
-                let mut span2 = span.clone();
-                span2.text.push(c);
-
-                chunks.push(tree::TextChunk {
-                    x: pos_list[chars_count].x,
-                    y: pos_list[chars_count].y,
-                    dx: pos_list[chars_count].dx,
-                    dy: pos_list[chars_count].dy,
-                    anchor,
-                    spans: vec![span2],
-                });
-            } else if is_new_span {
-                // Add this span to the last text chunk.
-                if let Some(chunk) = chunks.last_mut() {
-                    let mut span2 = span.clone();
-                    span2.text.push(c);
-
-                    chunk.spans.push(span2);
-                }
-            } else {
-                // Extend the last span.
-                if let Some(chunk) = chunks.last_mut() {
-                    if let Some(span) = chunk.spans.last_mut() {
-                        span.text.push(c);
-                    }
-                }
-            }
-
-            is_new_span = false;
-            chars_count += 1;
-        }
-    }
-
-    chunks
-}
-
-fn resolve_font(
-    node: &svgdom::Node,
-    state: &State,
-) -> Option<tree::Font> {
-    let style = node.find_enum(AId::FontStyle);
-    let variant = node.find_enum(AId::FontVariant);
-    let stretch = node.find_enum(AId::FontStretch);
-    let weight = resolve_font_weight(node);
-
-    let letter_spacing = node.resolve_length(AId::LetterSpacing, state, 0.0);
-    let letter_spacing = if letter_spacing.is_fuzzy_zero() { None } else { Some(letter_spacing) };
-
-    let word_spacing = node.resolve_length(AId::WordSpacing, state, 0.0);
-    let word_spacing = if word_spacing.is_fuzzy_zero() { None } else { Some(word_spacing) };
-
-    // TODO: what to do when <= 0? UB?
-    let size = units::resolve_font_size(node, state);
-    if !(size > 0.0) {
-        return None;
-    }
-    let size = tree::FontSize::new(size);
-
-    let family = if let Some(n) = node.find_node_with_attribute(AId::FontFamily) {
-        n.attributes().get_str_or(AId::FontFamily, &state.opt.font_family).to_owned()
-    } else {
-        state.opt.font_family.to_owned()
-    };
-
-    Some(tree::Font {
-        family,
-        size,
-        style,
-        variant,
-        weight,
-        stretch,
-        letter_spacing,
-        word_spacing,
-    })
-}
-
-#[derive(Clone, Copy)]
-struct CharacterPosition {
-    x: Option<f64>,
-    y: Option<f64>,
-    dx: Option<f64>,
-    dy: Option<f64>,
-}
-
-/// Resolves text's character positions.
-///
-/// This includes: x, y, dx, dy.
-///
-/// # The character
-///
-/// The first problem with this task is that the *character* itself
-/// is basically undefined in the SVG spec. Sometimes it's an *XML character*,
-/// sometimes a *glyph*, and sometimes just a *character*.
-///
-/// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537)
-/// on the SVG working group that addresses this by stating that a character
-/// is a Unicode code point. But it's not final.
-///
-/// Also, according to the SVG 2 spec, *character* is *a Unicode code point*.
-///
-/// Anyway, we treat a character as a Unicode code point.
-///
-/// # Algorithm
-///
-/// To resolve positions, we have to iterate over descendant nodes and
-/// if the current node is a `tspan` and has x/y/dx/dy attribute,
-/// than the positions from this attribute should be assigned to the characters
-/// of this `tspan` and it's descendants.
-///
-/// Positions list can have more values than characters in the `tspan`,
-/// so we have to clamp it, because values should not overlap, e.g.:
-///
-/// (we ignore whitespaces for example purposes,
-/// so the `text` content is `Text` and not `T ex t`)
-///
-/// ```text
-/// <text>
-///   a
-///   <tspan x="10 20 30">
-///     bc
-///   </tspan>
-///   d
-/// </text>
-/// ```
-///
-/// In this example, the `d` position should not be set to `30`.
-/// And the result should be: `[None, 10, 20, None]`
-///
-/// Another example:
-///
-/// ```text
-/// <text>
-///   <tspan x="100 110 120 130">
-///     a
-///     <tspan x="50">
-///       bc
-///     </tspan>
-///   </tspan>
-///   d
-/// </text>
-/// ```
-///
-/// The result should be: `[100, 50, 120, None]`
-fn resolve_positions_list(
-    text_elem: &svgdom::Node,
-    state: &State,
-) -> Vec<CharacterPosition> {
-    // Allocate a list that has all characters positions set to `None`.
-    let total_chars = count_chars(text_elem);
-    let mut list = vec![CharacterPosition {
-        x: None,
-        y: None,
-        dx: None,
-        dy: None,
-    }; total_chars];
-
-    let mut offset = 0;
-    for child in text_elem.descendants() {
-        if child.is_element() {
-            let child_chars = count_chars(&child);
-            macro_rules! push_list {
-                ($aid:expr, $field:ident) => {
-                    if let Some(num_list) = units::convert_list(&child, $aid, state) {
-                        // Note that we are using not the total count,
-                        // but the amount of characters in the current `tspan` (with children).
-                        let len = cmp::min(num_list.len(), child_chars);
-                        for i in 0..len {
-                            list[offset + i].$field = Some(num_list[i]);
-                        }
-                    }
-                };
-            }
-
-            push_list!(AId::X, x);
-            push_list!(AId::Y, y);
-            push_list!(AId::Dx, dx);
-            push_list!(AId::Dy, dy);
-        } else if child.is_text() {
-            // Advance the offset.
-            offset += child.text().chars().count();
-        }
-    }
-
-    list
-}
-
-/// Resolves characters rotation.
-///
-/// The algorithm is well explained
-/// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit).
-///
-/// ![](https://www.w3.org/TR/SVG11/images/text/tspan05-diagram.png)
-///
-/// Note: this algorithm differs from the position resolving one.
-fn resolve_rotate_list(
-    text_elem: &svgdom::Node,
-) -> Vec<f64> {
-    // Allocate a list that has all characters angles set to `0.0`.
-    let mut list = vec![0.0; count_chars(text_elem)];
-    let mut last = 0.0;
-    let mut offset = 0;
-    for child in text_elem.descendants() {
-        if child.is_element() {
-            if let Some(num_list) = child.attributes().get_number_list(AId::Rotate) {
-                for i in 0..count_chars(&child) {
-                    if let Some(a) = num_list.get(i).cloned() {
-                        list[offset + i] = a;
-                        last = a;
-                    } else {
-                        // If the rotate list doesn't specify the rotation for
-                        // this character - use the last one.
-                        list[offset + i] = last;
-                    }
-                }
-            }
-        } else if child.is_text() {
-            // Advance the offset.
-            offset += child.text().chars().count();
-        }
-    }
-
-    list
-}
-
-fn count_chars(node: &svgdom::Node) -> usize {
-    let mut total = 0;
-    for child in node.descendants().filter(|n| n.is_text()) {
-        total += child.text().chars().count();
-    }
-
-    total
-}
-
-/// Resolves node's `text-decoration` property.
-///
-/// `text` and `tspan` can point to the same node.
-fn resolve_decoration(
-    text: &svgdom::Node,
-    tspan: &svgdom::Node,
-    state: &State,
-    tree: &mut tree::Tree,
-) -> tree::TextDecoration {
-    // TODO: explain the algorithm
-
-    let text_dec = conv_text_decoration(text);
-    let tspan_dec = conv_text_decoration2(tspan);
-
-    let mut gen_style = |in_tspan: bool, in_text: bool| {
-        let n = if in_tspan {
-            tspan.clone()
-        } else if in_text {
-            text.clone()
-        } else {
-            return None;
-        };
-
-        Some(tree::TextDecorationStyle {
-            fill: style::resolve_fill(&n, true, state, tree),
-            stroke: style::resolve_stroke(&n, true, state, tree),
-        })
-    };
-
-    tree::TextDecoration {
-        underline:    gen_style(tspan_dec.has_underline,    text_dec.has_underline),
-        overline:     gen_style(tspan_dec.has_overline,     text_dec.has_overline),
-        line_through: gen_style(tspan_dec.has_line_through, text_dec.has_line_through),
-    }
-}
-
-struct TextDecorationTypes {
-    has_underline: bool,
-    has_overline: bool,
-    has_line_through: bool,
-}
-
-/// Resolves the `text` node's `text-decoration` property.
-fn conv_text_decoration(node: &svgdom::Node) -> TextDecorationTypes {
-    debug_assert!(node.is_tag_name(EId::Text));
-
-    fn find_decoration(node: &svgdom::Node, value: &str) -> bool {
-        node.ancestors().any(|n| n.attributes().get_str(AId::TextDecoration) == Some(value))
-    }
-
-    TextDecorationTypes {
-        has_underline: find_decoration(node, "underline"),
-        has_overline: find_decoration(node, "overline"),
-        has_line_through: find_decoration(node, "line-through"),
-    }
-}
-
-/// Resolves the default `text-decoration` property.
-fn conv_text_decoration2(tspan: &svgdom::Node) -> TextDecorationTypes {
-    let attrs = tspan.attributes();
-    TextDecorationTypes {
-        has_underline:    attrs.get_str(AId::TextDecoration) == Some("underline"),
-        has_overline:     attrs.get_str(AId::TextDecoration) == Some("overline"),
-        has_line_through: attrs.get_str(AId::TextDecoration) == Some("line-through"),
-    }
-}
-
-fn resolve_baseline_shift(
-    node: &svgdom::Node,
-    state: &State,
-) -> f64 {
-    let mut shift = 0.0;
-    let nodes: Vec<_> = node.ancestors().take_while(|n| !n.is_tag_name(EId::Text)).collect();
-    for n in nodes.iter().rev() {
-        match n.attributes().get_value(AId::BaselineShift) {
-            Some(AValue::String(ref s)) => {
-                match s.as_str() {
-                    "baseline" => {}
-                    "sub" => shift += units::resolve_font_size(&n, state) * -0.2,
-                    "super" => shift += units::resolve_font_size(&n, state) * 0.4,
-                    _ => {}
-                }
-            }
-            Some(AValue::Length(len)) => {
-                if len.unit == Unit::Percent {
-                    shift += units::resolve_font_size(&n, state) * (len.num / 100.0);
-                } else {
-                    shift += units::convert_length(*len, &n, AId::BaselineShift,
-                                                   tree::Units::ObjectBoundingBox, state);
-                }
-            }
-            _ => {}
-        }
-    }
-
-    shift
-}
-
-fn resolve_font_weight(
-    node: &svgdom::Node,
-) -> tree::FontWeight {
-    fn bound(min: usize, val: usize, max: usize) -> usize {
-        cmp::max(min, cmp::min(max, val))
-    }
-
-    let nodes: Vec<_> = node.ancestors().collect();
-    let mut weight = 400;
-    for n in nodes.iter().rev().skip(1) { // skip Root
-        weight = match n.attributes().get_str_or(AId::FontWeight, "") {
-            "normal" => 400,
-            "bold" => 700,
-            "100" => 100,
-            "200" => 200,
-            "300" => 300,
-            "400" => 400,
-            "500" => 500,
-            "600" => 600,
-            "700" => 700,
-            "800" => 800,
-            "900" => 900,
-            "bolder" => {
-                // By the CSS2 spec the default value should be 400
-                // so `bolder` will result in 500.
-                // But Chrome and Inkscape will give us 700.
-                // Have no idea is it a bug or something, but
-                // we will follow such behavior for now.
-                let step = if weight == 400 { 300 } else { 100 };
-
-                bound(100, weight + step, 900)
-            }
-            "lighter" => {
-                // By the CSS2 spec the default value should be 400
-                // so `lighter` will result in 300.
-                // But Chrome and Inkscape will give us 200.
-                // Have no idea is it a bug or something, but
-                // we will follow such behavior for now.
-                let step = if weight == 400 { 200 } else { 100 };
-
-                bound(100, weight - step, 900)
-            }
-            _ => weight,
-        };
-    }
-
-    match weight {
-        100 => tree::FontWeight::W100,
-        200 => tree::FontWeight::W200,
-        300 => tree::FontWeight::W300,
-        400 => tree::FontWeight::W400,
-        500 => tree::FontWeight::W500,
-        600 => tree::FontWeight::W600,
-        700 => tree::FontWeight::W700,
-        800 => tree::FontWeight::W800,
-        900 => tree::FontWeight::W900,
-        _ => tree::FontWeight::W400,
-    }
-}
diff --git a/usvg/src/convert/text/convert.rs b/usvg/src/convert/text/convert.rs
new file mode 100644
index 000000000..fb27f0e1d
--- /dev/null
+++ b/usvg/src/convert/text/convert.rs
@@ -0,0 +1,902 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+use std::cmp;
+use std::path::PathBuf;
+use std::rc::Rc;
+
+// external
+use svgdom;
+
+mod fk {
+    pub use font_kit::family_name::FamilyName;
+    pub use font_kit::font::Font;
+    pub use font_kit::handle::Handle;
+    pub use font_kit::properties::*;
+    pub use font_kit::source::SystemSource;
+}
+
+// self
+use crate::tree;
+use crate::convert::prelude::*;
+use crate::convert::{
+    style,
+    units,
+};
+use super::TextNode;
+
+
+/// A read-only text index in bytes.
+///
+/// Guarantee to be on a char boundary and in text bounds.
+#[derive(Clone, Copy, PartialEq)]
+pub struct ByteIndex(usize);
+
+impl ByteIndex {
+    pub fn new(i: usize) -> Self {
+        ByteIndex(i)
+    }
+
+    pub fn value(&self) -> usize {
+        self.0
+    }
+
+    /// Converts byte position into a code point position.
+    pub fn code_point_at(&self, text: &str) -> usize {
+        text.char_indices().take_while(|(i, _)| *i != self.0).count()
+    }
+
+    /// Converts byte position into a character.
+    pub fn char_from(&self, text: &str) -> char {
+        text[self.0..].chars().next().unwrap()
+    }
+}
+
+
+#[derive(Clone, Copy, PartialEq)]
+pub enum TextAnchor {
+    Start,
+    Middle,
+    End,
+}
+
+pub type Font = Rc<FontData>;
+
+pub struct FontData {
+    pub handle: fk::Font,
+    pub path: PathBuf,
+    pub index: u32,
+
+    /// Guarantee to be > 0.
+    units_per_em: u32,
+
+    // All values below are in font units.
+    ascent: f32,
+    descent: f32,
+    x_height: f32,
+    underline_position: f32,
+    underline_thickness: f32,
+}
+
+impl FontData {
+    pub fn scale(&self, font_size: f64) -> f64 {
+        let s = font_size / self.units_per_em as f64;
+        debug_assert!(s.is_finite(), "units per em cannot be {}", self.units_per_em);
+        s
+    }
+
+    pub fn ascent(&self, font_size: f64) -> f64 {
+        self.ascent as f64 * self.scale(font_size)
+    }
+
+    pub fn descent(&self, font_size: f64) -> f64 {
+        self.descent as f64 * self.scale(font_size)
+    }
+
+    pub fn height(&self, font_size: f64) -> f64 {
+        self.ascent(font_size) - self.descent(font_size)
+    }
+
+    pub fn x_height(&self, font_size: f64) -> f64 {
+        self.x_height as f64 * self.scale(font_size)
+    }
+
+    pub fn underline_position(&self, font_size: f64) -> f64 {
+        self.underline_position as f64 * self.scale(font_size)
+    }
+
+    pub fn underline_thickness(&self, font_size: f64) -> f64 {
+        self.underline_thickness as f64 * self.scale(font_size)
+    }
+}
+
+impl std::fmt::Debug for FontData {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "FontData({}:{})", self.path.display(), self.index)
+    }
+}
+
+
+pub struct TextPath {
+    /// A text offset in SVG coordinates.
+    ///
+    /// Percentage values already resolved.
+    pub start_offset: f64,
+
+    pub segments: Vec<tree::PathSegment>,
+}
+
+
+#[derive(Clone)]
+pub enum TextFlow {
+    Horizontal,
+    Path(Rc<TextPath>),
+}
+
+
+/// A text chunk.
+///
+/// Text alignment and BIDI reordering can be done only inside a text chunk.
+pub struct TextChunk {
+    pub x: Option<f64>,
+    pub y: Option<f64>,
+    pub anchor: TextAnchor,
+    pub spans: Vec<TextSpan>,
+    pub text_flow: TextFlow,
+    pub text: String,
+}
+
+impl TextChunk {
+    pub fn span_at(&self, byte_offset: ByteIndex) -> Option<&TextSpan> {
+        for span in &self.spans {
+            if span.contains(byte_offset) {
+                return Some(span);
+            }
+        }
+
+        None
+    }
+}
+
+
+/// Spans do not overlap.
+#[derive(Clone)]
+pub struct TextSpan {
+    pub start: usize,
+    pub end: usize,
+    pub fill: Option<tree::Fill>,
+    pub stroke: Option<tree::Stroke>,
+    pub font: Font,
+    pub font_size: f64,
+    pub decoration: TextDecoration,
+    pub baseline_shift: f64,
+    pub visibility: tree::Visibility,
+    pub letter_spacing: f64,
+    pub word_spacing: f64,
+}
+
+impl TextSpan {
+    pub fn contains(&self, byte_offset: ByteIndex) -> bool {
+        byte_offset.value() >= self.start && byte_offset.value() < self.end
+    }
+}
+
+
+#[derive(Clone, Copy, PartialEq)]
+pub enum WritingMode {
+    LeftToRight,
+    TopToBottom,
+}
+
+
+struct IterState {
+    chars_count: usize,
+    chunk_bytes_count: usize,
+    split_chunk: bool,
+    text_flow: TextFlow,
+    chunks: Vec<TextChunk>,
+}
+
+pub fn collect_text_chunks(
+    text_node: &TextNode,
+    pos_list: &[CharacterPosition],
+    state: &State,
+    tree: &mut tree::Tree,
+) -> Vec<TextChunk> {
+    let mut iter_state = IterState {
+        chars_count: 0,
+        chunk_bytes_count: 0,
+        split_chunk: false,
+        text_flow: TextFlow::Horizontal,
+        chunks: Vec::new(),
+    };
+
+    collect_text_chunks_impl(text_node, text_node, pos_list, state, tree, &mut iter_state);
+
+    iter_state.chunks
+}
+
+fn collect_text_chunks_impl(
+    text_node: &TextNode,
+    parent: &svgdom::Node,
+    pos_list: &[CharacterPosition],
+    state: &State,
+    tree: &mut tree::Tree,
+    iter_state: &mut IterState,
+) {
+    for child in parent.children() {
+        if child.is_element() {
+            if child.is_tag_name(EId::TextPath) {
+                if !parent.is_tag_name(EId::Text) {
+                    // `textPath` can be set only as a direct `text` element child.
+                    iter_state.chars_count += count_chars(&child);
+                    continue;
+                }
+
+                match resolve_text_flow(child.clone(), state) {
+                    Some(v) => {
+                        iter_state.text_flow = v;
+                    }
+                    None => {
+                        // Skip an invalid text path and all it's children.
+                        // We have to update the chars count,
+                        // because `pos_list` was calculated including this text path.
+                        iter_state.chars_count += count_chars(&child);
+                        continue;
+                    }
+                }
+
+                iter_state.split_chunk = true;
+            }
+
+            collect_text_chunks_impl(text_node, &child, pos_list, state, tree, iter_state);
+
+            iter_state.text_flow = TextFlow::Horizontal;
+
+            // Next char after `textPath` should be split too.
+            if child.is_tag_name(EId::TextPath) {
+                iter_state.split_chunk = true;
+            }
+
+            continue;
+        }
+
+        if !style::is_visible_element(parent, state.opt) {
+            iter_state.chars_count += child.text().chars().count();
+            continue;
+        }
+
+        let anchor = conv_text_anchor(parent);
+
+        // TODO: what to do when <= 0? UB?
+        let font_size = units::resolve_font_size(parent, state);
+        if !(font_size > 0.0) {
+            // Skip this span.
+            iter_state.chars_count += child.text().chars().count();
+            continue;
+        }
+
+        let font = match resolve_font(parent, state) {
+            Some(v) => v,
+            None => {
+                // Skip this span.
+                iter_state.chars_count += child.text().chars().count();
+                continue;
+            }
+        };
+
+        let letter_spacing = parent.resolve_length(AId::LetterSpacing, state, 0.0);
+        let word_spacing = parent.resolve_length(AId::WordSpacing, state, 0.0);
+
+        let span = TextSpan {
+            start: 0,
+            end: 0,
+            fill: style::resolve_fill(parent, true, state, tree),
+            stroke: style::resolve_stroke(parent, true, state, tree),
+            font,
+            font_size,
+            decoration: resolve_decoration(text_node, parent, state, tree),
+            visibility: parent.find_enum(AId::Visibility),
+            baseline_shift: resolve_baseline_shift(parent, state),
+            letter_spacing,
+            word_spacing,
+        };
+
+        let mut is_new_span = true;
+        for c in child.text().chars() {
+            let char_len = c.len_utf8();
+
+            // Create a new chunk if:
+            // - this is the first span (yes, position can be None)
+            // - text character has an absolute coordinate assigned to it (via x/y attribute)
+            // - `c` is the first char of the `textPath`
+            // - `c` is the first char after `textPath`
+            let is_new_chunk =
+                   pos_list[iter_state.chars_count].x.is_some()
+                || pos_list[iter_state.chars_count].y.is_some()
+                || iter_state.split_chunk
+                || iter_state.chunks.is_empty();
+
+            iter_state.split_chunk = false;
+
+            if is_new_chunk {
+                iter_state.chunk_bytes_count = 0;
+
+                let mut span2 = span.clone();
+                span2.start = 0;
+                span2.end = char_len;
+
+                iter_state.chunks.push(TextChunk {
+                    x: pos_list[iter_state.chars_count].x,
+                    y: pos_list[iter_state.chars_count].y,
+                    anchor,
+                    spans: vec![span2],
+                    text_flow: iter_state.text_flow.clone(),
+                    text: c.to_string(),
+                });
+            } else if is_new_span {
+                // Add this span to the last text chunk.
+                let mut span2 = span.clone();
+                span2.start = iter_state.chunk_bytes_count;
+                span2.end = iter_state.chunk_bytes_count + char_len;
+
+                if let Some(chunk) = iter_state.chunks.last_mut() {
+                    chunk.text.push(c);
+                    chunk.spans.push(span2);
+                }
+            } else {
+                // Extend the last span.
+                if let Some(chunk) = iter_state.chunks.last_mut() {
+                    chunk.text.push(c);
+                    if let Some(span) = chunk.spans.last_mut() {
+                        debug_assert_ne!(span.end, 0);
+                        span.end += char_len;
+                    }
+                }
+            }
+
+            is_new_span = false;
+            iter_state.chars_count += 1;
+            iter_state.chunk_bytes_count += char_len;
+        }
+    }
+}
+
+fn resolve_text_flow(
+    node: svgdom::Node,
+    state: &State,
+) -> Option<TextFlow> {
+    let av = node.attributes().get_value(AId::Href).cloned();
+    let path_node = match av {
+        Some(AValue::Link(n)) => n.clone(),
+        _ => return None,
+    };
+
+    if !path_node.is_tag_name(EId::Path) {
+        return None;
+    }
+
+    let segments = match path_node.attributes().get_value(AId::D).cloned() {
+        Some(AValue::Path(path)) => crate::convert::path::convert(path),
+        _ => return None,
+    };
+
+    if segments.len() < 2 {
+        return None;
+    }
+
+    let start_offset = node.attributes().get_length_or(AId::StartOffset, Length::zero());
+    let start_offset = if start_offset.unit == Unit::Percent {
+        // 'If a percentage is given, then the `startOffset` represents
+        // a percentage distance along the entire path.'
+        let path_len = crate::utils::path_length(&segments);
+        path_len * (start_offset.num / 100.0)
+    } else {
+        node.resolve_length(AId::StartOffset, state, 0.0)
+    };
+
+    Some(TextFlow::Path(Rc::new(TextPath {
+        start_offset,
+        segments,
+    })))
+}
+
+pub fn resolve_rendering_mode(
+    text_node: &TextNode,
+    state: &State,
+) -> tree::ShapeRendering {
+    let mode: tree::TextRendering = text_node.try_find_enum(AId::TextRendering)
+                                             .unwrap_or(state.opt.text_rendering);
+
+    match mode {
+        tree::TextRendering::OptimizeSpeed => tree::ShapeRendering::CrispEdges,
+        tree::TextRendering::OptimizeLegibility => tree::ShapeRendering::GeometricPrecision,
+        tree::TextRendering::GeometricPrecision =>  tree::ShapeRendering::GeometricPrecision,
+    }
+}
+
+fn resolve_font(
+    node: &svgdom::Node,
+    state: &State,
+) -> Option<Font> {
+    let style = conv_font_style(node);
+    let stretch = conv_font_stretch(node);
+    let weight = resolve_font_weight(node);
+
+    let font_family = if let Some(n) = node.find_node_with_attribute(AId::FontFamily) {
+        n.attributes().get_str_or(AId::FontFamily, &state.opt.font_family).to_owned()
+    } else {
+        state.opt.font_family.to_owned()
+    };
+
+    let mut name_list = Vec::new();
+    for family in font_family.split(',') {
+        // TODO: to a proper parser
+        let family = family.replace('\'', "");
+        let family = family.trim();
+
+        let name = match family {
+            "serif"      => fk::FamilyName::Serif,
+            "sans-serif" => fk::FamilyName::SansSerif,
+            "monospace"  => fk::FamilyName::Monospace,
+            "cursive"    => fk::FamilyName::Cursive,
+            "fantasy"    => fk::FamilyName::Fantasy,
+            _            => fk::FamilyName::Title(family.to_string())
+        };
+
+        name_list.push(name);
+    }
+
+    // Use the default font as fallback.
+    name_list.push(fk::FamilyName::Title(state.opt.font_family.to_owned()));
+
+    let properties = fk::Properties { style, weight, stretch };
+    let handle = match fk::SystemSource::new().select_best_match(&name_list, &properties) {
+        Ok(v) => v,
+        Err(_) => {
+            let mut families = Vec::new();
+            for name in name_list {
+                families.push(match name {
+                    fk::FamilyName::Serif           => "serif".to_string(),
+                    fk::FamilyName::SansSerif       => "sans-serif".to_string(),
+                    fk::FamilyName::Monospace       => "monospace".to_string(),
+                    fk::FamilyName::Cursive         => "cursive".to_string(),
+                    fk::FamilyName::Fantasy         => "fantasy".to_string(),
+                    fk::FamilyName::Title(ref name) => name.clone(),
+                });
+            }
+
+            warn!("No match for '{}' font-family.", families.join(", "));
+            return None;
+        }
+    };
+
+    load_font(&handle)
+}
+
+pub fn load_font(handle: &fk::Handle) -> Option<Font> {
+    let (path, index) = match handle {
+        fk::Handle::Path { ref path, font_index } => {
+            (path.clone(), *font_index)
+        }
+        _ => return None,
+    };
+
+    let font = match handle.load() {
+        Ok(v) => v,
+        Err(_) => {
+            warn!("Failed to load '{}'.", path.display());
+            return None;
+        }
+    };
+
+    let metrics = font.metrics();
+
+    // Some fonts can have `units_per_em` set to zero, which will break out calculations.
+    if metrics.units_per_em == 0 {
+        return None;
+    }
+
+    let x_height = if metrics.x_height.is_fuzzy_zero() {
+        // If not set - fallback to height * 45%.
+        // 45% is what Firefox uses.
+        (metrics.ascent - metrics.descent) * 0.45
+    } else {
+        metrics.x_height
+    };
+
+    Some(Rc::new(FontData {
+        handle: font,
+        path,
+        index,
+        units_per_em: metrics.units_per_em,
+        ascent: metrics.ascent,
+        descent: metrics.descent,
+        x_height,
+        underline_position: metrics.underline_position,
+        underline_thickness: metrics.underline_thickness,
+    }))
+}
+
+fn conv_font_style(node: &svgdom::Node) -> fk::Style {
+    if let Some(n) = node.find_node_with_attribute(AId::FontStyle) {
+        match n.attributes().get_str_or(AId::FontStyle, "") {
+            "italic"  => fk::Style::Italic,
+            "oblique" => fk::Style::Oblique,
+            _         => fk::Style::Normal,
+        }
+    } else {
+        fk::Style::Normal
+    }
+}
+
+fn conv_font_stretch(node: &svgdom::Node) -> fk::Stretch {
+    if let Some(n) = node.find_node_with_attribute(AId::FontStretch) {
+        match n.attributes().get_str_or(AId::FontStretch, "") {
+            "narrower" | "condensed" => fk::Stretch::CONDENSED,
+            "ultra-condensed"        => fk::Stretch::ULTRA_CONDENSED,
+            "extra-condensed"        => fk::Stretch::EXTRA_CONDENSED,
+            "semi-condensed"         => fk::Stretch::SEMI_CONDENSED,
+            "semi-expanded"          => fk::Stretch::SEMI_EXPANDED,
+            "wider" | "expanded"     => fk::Stretch::EXPANDED,
+            "extra-expanded"         => fk::Stretch::EXTRA_EXPANDED,
+            "ultra-expanded"         => fk::Stretch::ULTRA_EXPANDED,
+            _                        => fk::Stretch::NORMAL,
+        }
+    } else {
+        fk::Stretch::NORMAL
+    }
+}
+
+fn conv_text_anchor(node: &svgdom::Node) -> TextAnchor {
+    if let Some(n) = node.find_node_with_attribute(AId::TextAnchor) {
+        match n.attributes().get_str_or(AId::TextAnchor, "") {
+            "middle" => TextAnchor::Middle,
+            "end"    => TextAnchor::End,
+            _        => TextAnchor::Start,
+        }
+    } else {
+        TextAnchor::Start
+    }
+}
+
+#[derive(Clone, Copy)]
+pub struct CharacterPosition {
+    pub x: Option<f64>,
+    pub y: Option<f64>,
+    pub dx: Option<f64>,
+    pub dy: Option<f64>,
+}
+
+/// Resolves text's character positions.
+///
+/// This includes: x, y, dx, dy.
+///
+/// # The character
+///
+/// The first problem with this task is that the *character* itself
+/// is basically undefined in the SVG spec. Sometimes it's an *XML character*,
+/// sometimes a *glyph*, and sometimes just a *character*.
+///
+/// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537)
+/// on the SVG working group that addresses this by stating that a character
+/// is a Unicode code point. But it's not final.
+///
+/// Also, according to the SVG 2 spec, *character* is *a Unicode code point*.
+///
+/// Anyway, we treat a character as a Unicode code point.
+///
+/// # Algorithm
+///
+/// To resolve positions, we have to iterate over descendant nodes and
+/// if the current node is a `tspan` and has x/y/dx/dy attribute,
+/// than the positions from this attribute should be assigned to the characters
+/// of this `tspan` and it's descendants.
+///
+/// Positions list can have more values than characters in the `tspan`,
+/// so we have to clamp it, because values should not overlap, e.g.:
+///
+/// (we ignore whitespaces for example purposes,
+/// so the `text` content is `Text` and not `T ex t`)
+///
+/// ```text
+/// <text>
+///   a
+///   <tspan x="10 20 30">
+///     bc
+///   </tspan>
+///   d
+/// </text>
+/// ```
+///
+/// In this example, the `d` position should not be set to `30`.
+/// And the result should be: `[None, 10, 20, None]`
+///
+/// Another example:
+///
+/// ```text
+/// <text>
+///   <tspan x="100 110 120 130">
+///     a
+///     <tspan x="50">
+///       bc
+///     </tspan>
+///   </tspan>
+///   d
+/// </text>
+/// ```
+///
+/// The result should be: `[100, 50, 120, None]`
+pub fn resolve_positions_list(
+    text_node: &TextNode,
+    state: &State,
+) -> Vec<CharacterPosition> {
+    // Allocate a list that has all characters positions set to `None`.
+    let total_chars = count_chars(text_node);
+    let mut list = vec![CharacterPosition {
+        x: None,
+        y: None,
+        dx: None,
+        dy: None,
+    }; total_chars];
+
+    let mut offset = 0;
+    for child in text_node.descendants() {
+        if child.is_element() {
+            let child_chars = count_chars(&child);
+            macro_rules! push_list {
+                ($aid:expr, $field:ident) => {
+                    if let Some(num_list) = units::convert_list(&child, $aid, state) {
+                        // Note that we are using not the total count,
+                        // but the amount of characters in the current `tspan` (with children).
+                        let len = cmp::min(num_list.len(), child_chars);
+                        for i in 0..len {
+                            list[offset + i].$field = Some(num_list[i]);
+                        }
+                    }
+                };
+            }
+
+            push_list!(AId::X, x);
+            push_list!(AId::Y, y);
+            push_list!(AId::Dx, dx);
+            push_list!(AId::Dy, dy);
+        } else if child.is_text() {
+            // Advance the offset.
+            offset += child.text().chars().count();
+        }
+    }
+
+    list
+}
+
+/// Resolves characters rotation.
+///
+/// The algorithm is well explained
+/// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit).
+///
+/// ![](https://www.w3.org/TR/SVG11/images/text/tspan05-diagram.png)
+///
+/// Note: this algorithm differs from the position resolving one.
+pub fn resolve_rotate_list(
+    text_node: &TextNode,
+) -> Vec<f64> {
+    // Allocate a list that has all characters angles set to `0.0`.
+    let mut list = vec![0.0; count_chars(text_node)];
+    let mut last = 0.0;
+    let mut offset = 0;
+    for child in text_node.descendants() {
+        if child.is_element() {
+            if let Some(num_list) = child.attributes().get_number_list(AId::Rotate) {
+                for i in 0..count_chars(&child) {
+                    if let Some(a) = num_list.get(i).cloned() {
+                        list[offset + i] = a;
+                        last = a;
+                    } else {
+                        // If the rotate list doesn't specify the rotation for
+                        // this character - use the last one.
+                        list[offset + i] = last;
+                    }
+                }
+            }
+        } else if child.is_text() {
+            // Advance the offset.
+            offset += child.text().chars().count();
+        }
+    }
+
+    list
+}
+
+#[derive(Clone)]
+pub struct TextDecorationStyle {
+    pub fill: Option<tree::Fill>,
+    pub stroke: Option<tree::Stroke>,
+}
+
+#[derive(Clone)]
+pub struct TextDecoration {
+    pub underline: Option<TextDecorationStyle>,
+    pub overline: Option<TextDecorationStyle>,
+    pub line_through: Option<TextDecorationStyle>,
+}
+
+/// Resolves node's `text-decoration` property.
+///
+/// `text` and `tspan` can point to the same node.
+fn resolve_decoration(
+    text_node: &TextNode,
+    tspan: &svgdom::Node,
+    state: &State,
+    tree: &mut tree::Tree,
+) -> TextDecoration {
+    // TODO: explain the algorithm
+
+    let text_dec = conv_text_decoration(text_node);
+    let tspan_dec = conv_text_decoration2(tspan);
+
+    let mut gen_style = |in_tspan: bool, in_text: bool| {
+        let n = if in_tspan {
+            tspan.clone()
+        } else if in_text {
+            (*text_node).clone()
+        } else {
+            return None;
+        };
+
+        Some(TextDecorationStyle {
+            fill: style::resolve_fill(&n, true, state, tree),
+            stroke: style::resolve_stroke(&n, true, state, tree),
+        })
+    };
+
+    TextDecoration {
+        underline:    gen_style(tspan_dec.has_underline,    text_dec.has_underline),
+        overline:     gen_style(tspan_dec.has_overline,     text_dec.has_overline),
+        line_through: gen_style(tspan_dec.has_line_through, text_dec.has_line_through),
+    }
+}
+
+struct TextDecorationTypes {
+    has_underline: bool,
+    has_overline: bool,
+    has_line_through: bool,
+}
+
+/// Resolves the `text` node's `text-decoration` property.
+fn conv_text_decoration(text_node: &TextNode) -> TextDecorationTypes {
+    fn find_decoration(node: &svgdom::Node, value: &str) -> bool {
+        node.ancestors().any(|n| n.attributes().get_str(AId::TextDecoration) == Some(value))
+    }
+
+    TextDecorationTypes {
+        has_underline: find_decoration(text_node, "underline"),
+        has_overline: find_decoration(text_node, "overline"),
+        has_line_through: find_decoration(text_node, "line-through"),
+    }
+}
+
+/// Resolves the default `text-decoration` property.
+fn conv_text_decoration2(tspan: &svgdom::Node) -> TextDecorationTypes {
+    let attrs = tspan.attributes();
+    TextDecorationTypes {
+        has_underline:    attrs.get_str(AId::TextDecoration) == Some("underline"),
+        has_overline:     attrs.get_str(AId::TextDecoration) == Some("overline"),
+        has_line_through: attrs.get_str(AId::TextDecoration) == Some("line-through"),
+    }
+}
+
+fn resolve_baseline_shift(
+    node: &svgdom::Node,
+    state: &State,
+) -> f64 {
+    let mut shift = 0.0;
+    let nodes: Vec<_> = node.ancestors().take_while(|n| !n.is_tag_name(EId::Text)).collect();
+    for n in nodes.iter().rev() {
+        match n.attributes().get_value(AId::BaselineShift) {
+            Some(AValue::String(ref s)) => {
+                // TODO: sub/super from font
+                match s.as_str() {
+                    "baseline" => {}
+                    "sub" => shift += units::resolve_font_size(&n, state) * -0.2,
+                    "super" => shift += units::resolve_font_size(&n, state) * 0.4,
+                    _ => {}
+                }
+            }
+            Some(AValue::Length(len)) => {
+                if len.unit == Unit::Percent {
+                    shift += units::resolve_font_size(&n, state) * (len.num / 100.0);
+                } else {
+                    shift += units::convert_length(*len, &n, AId::BaselineShift,
+                                                   tree::Units::ObjectBoundingBox, state);
+                }
+            }
+            _ => {}
+        }
+    }
+
+    shift
+}
+
+fn resolve_font_weight(
+    node: &svgdom::Node,
+) -> fk::Weight {
+    fn bound(min: usize, val: usize, max: usize) -> usize {
+        cmp::max(min, cmp::min(max, val))
+    }
+
+    let nodes: Vec<_> = node.ancestors().collect();
+    let mut weight = 400;
+    for n in nodes.iter().rev().skip(1) { // skip Root
+        weight = match n.attributes().get_str_or(AId::FontWeight, "") {
+            "normal" => 400,
+            "bold" => 700,
+            "100" => 100,
+            "200" => 200,
+            "300" => 300,
+            "400" => 400,
+            "500" => 500,
+            "600" => 600,
+            "700" => 700,
+            "800" => 800,
+            "900" => 900,
+            "bolder" => {
+                // By the CSS2 spec the default value should be 400
+                // so `bolder` will result in 500.
+                // But Chrome and Inkscape will give us 700.
+                // Have no idea is it a bug or something, but
+                // we will follow such behavior for now.
+                let step = if weight == 400 { 300 } else { 100 };
+
+                bound(100, weight + step, 900)
+            }
+            "lighter" => {
+                // By the CSS2 spec the default value should be 400
+                // so `lighter` will result in 300.
+                // But Chrome and Inkscape will give us 200.
+                // Have no idea is it a bug or something, but
+                // we will follow such behavior for now.
+                let step = if weight == 400 { 200 } else { 100 };
+
+                bound(100, weight - step, 900)
+            }
+            _ => weight,
+        };
+    }
+
+    fk::Weight(weight as f32)
+}
+
+fn count_chars(node: &svgdom::Node) -> usize {
+    node.descendants()
+        .filter(|n| n.is_text())
+        .fold(0, |w, n| w + n.text().chars().count())
+}
+
+/// Converts the writing mode.
+///
+/// According to the [SVG 2.0] spec, there are only two writing modes:
+/// horizontal left-to-right and vertical right-to-left.
+/// E.g:
+///
+/// - `lr`, `lr-tb`, `rl`, `rl-tb` => `horizontal-tb`
+/// - `tb`, `tb-rl` => `vertical-rl`
+///
+/// Also, looks like no one really supports the `rl` and `rl-tb`, except `Batik`.
+/// And I'm not sure if it's behaviour is correct.
+///
+/// So we will ignore it as well, mainly because I have no idea how exactly
+/// it should affect the rendering.
+///
+/// [SVG 2.0]: https://www.w3.org/TR/SVG2/text.html#WritingModeProperty
+pub fn convert_writing_mode(text_node: &TextNode) -> WritingMode {
+    if let Some(n) = text_node.find_node_with_attribute(AId::WritingMode) {
+        match n.attributes().get_str_or(AId::WritingMode, "lr-tb") {
+            "tb" | "tb-rl" => WritingMode::TopToBottom,
+            _ => WritingMode::LeftToRight,
+        }
+    } else {
+        WritingMode::LeftToRight
+    }
+}
diff --git a/usvg/src/convert/text/mod.rs b/usvg/src/convert/text/mod.rs
new file mode 100644
index 000000000..f6b03fd77
--- /dev/null
+++ b/usvg/src/convert/text/mod.rs
@@ -0,0 +1,496 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+use std::mem;
+
+// external
+use svgdom;
+
+mod fk {
+    pub use font_kit::handle::Handle;
+    pub use font_kit::hinting::HintingOptions as Hinting;
+    pub use font_kit::source::SystemSource;
+}
+
+// self
+use crate::tree;
+use crate::tree::prelude::*;
+use crate::utils;
+use super::prelude::*;
+
+mod convert;
+use self::convert::*;
+
+mod shaper;
+use self::shaper::OutlinedCluster;
+
+
+mod private {
+    use super::*;
+
+    /// A type-safe container for a `text` node.
+    ///
+    /// This way we can be sure that we are passing the `text` node and not just a random node.
+    pub struct TextNode(svgdom::Node);
+
+    impl TextNode {
+        pub fn new(node: svgdom::Node) -> Self {
+            debug_assert!(node.is_tag_name(EId::Text));
+            TextNode(node)
+        }
+    }
+
+    impl std::ops::Deref for TextNode {
+        type Target = svgdom::Node;
+
+        fn deref(&self) -> &Self::Target {
+            &self.0
+        }
+    }
+}
+use self::private::*;
+
+
+/// A text decoration span.
+///
+/// Basically a horizontal line, that will be used for underline, overline and line-through.
+/// It doesn't have a height, since it depends on the font metrics.
+#[derive(Clone, Copy)]
+struct DecorationSpan {
+    width: f64,
+    transform: tree::Transform,
+}
+
+
+pub fn convert(
+    node: &svgdom::Node,
+    state: &State,
+    parent: &mut tree::Node,
+    tree: &mut tree::Tree,
+) {
+    let text_node = &TextNode::new(node.clone());
+    let mut new_paths = text_to_paths(text_node, state, parent, tree);
+
+    let mut bbox = Rect::new_bbox();
+    for path in &new_paths {
+        if let Some(r) = utils::path_bbox(&path.segments, None, None) {
+            bbox = bbox.expand(r);
+        }
+    }
+
+    if new_paths.len() == 1 {
+        // Copy `text` id to the first path.
+        new_paths[0].id = node.id().clone();
+    }
+
+    let mut parent = if state.opt.keep_named_groups && new_paths.len() > 1 {
+        // Create a group will all paths that was created during text-to-path conversion.
+        parent.append_kind(tree::NodeKind::Group(tree::Group {
+            id: node.id().clone(),
+            .. tree::Group::default()
+        }))
+    } else {
+        parent.clone()
+    };
+
+    let rendering_mode = resolve_rendering_mode(text_node, state);
+    for mut path in new_paths {
+        fix_obj_bounding_box(&mut path, bbox, tree);
+        path.rendering_mode = rendering_mode;
+        parent.append_kind(tree::NodeKind::Path(path));
+    }
+}
+
+fn text_to_paths(
+    text_node: &TextNode,
+    state: &State,
+    parent: &mut tree::Node,
+    tree: &mut tree::Tree,
+) -> Vec<tree::Path> {
+    let pos_list = resolve_positions_list(text_node, state);
+    let rotate_list = resolve_rotate_list(text_node);
+    let writing_mode = convert_writing_mode(text_node);
+    let mut text_ts = text_node.attributes().get_transform(AId::Transform);
+
+    let mut chunks = collect_text_chunks(text_node, &pos_list, state, tree);
+    let mut char_offset = 0;
+    let mut last_x = 0.0;
+    let mut last_y = 0.0;
+    let mut new_paths = Vec::new();
+    for chunk in &mut chunks {
+        let (x, y) = match chunk.text_flow {
+            TextFlow::Horizontal => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)),
+            TextFlow::Path(_) => (0.0, 0.0),
+        };
+
+        let mut clusters = shaper::outline_chunk(&chunk, state);
+        if clusters.is_empty() {
+            char_offset += chunk.text.chars().count();
+            continue;
+        }
+
+        shaper::apply_writing_mode(writing_mode, &mut clusters);
+        shaper::apply_letter_spacing(&chunk, &mut clusters);
+        shaper::apply_word_spacing(&chunk, &mut clusters);
+        let curr_pos = shaper::resolve_clusters_positions(
+            chunk, char_offset, &pos_list, &rotate_list, &mut clusters
+        );
+
+        if let TextFlow::Path(_) = chunk.text_flow {
+            // Since a path flow can point clusters in any direction,
+            // we can't simply shift a global transform by Y axis.
+            // We have to shift each cluster individually.
+            shaper::shift_clusters_baseline(&chunk, &mut clusters);
+        }
+
+        if writing_mode == WritingMode::TopToBottom {
+            if let TextFlow::Horizontal = chunk.text_flow {
+                text_ts.rotate_at(90.0, x, y);
+            }
+        }
+
+        for span in &mut chunk.spans {
+            let decoration_spans = collect_decoration_spans(span, &clusters);
+
+            let mut span_ts = text_ts.clone();
+            span_ts.translate(x, y);
+            if let TextFlow::Horizontal = chunk.text_flow {
+                // In case of a horizontal flow, shift transform and not clusters,
+                // because clusters can be rotated and an additional shift will lead
+                // to invalid results.
+                span_ts.translate(0.0, -span.baseline_shift);
+            }
+
+            if let Some(decoration) = span.decoration.underline.take() {
+                // TODO: No idea what offset should be used for top-to-bottom layout.
+                // There is
+                // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property
+                // but it doesn't go into details.
+                let offset = match writing_mode {
+                    WritingMode::LeftToRight => -span.font.underline_position(span.font_size),
+                    WritingMode::TopToBottom => span.font.height(span.font_size) / 2.0,
+                };
+
+                new_paths.push(convert_decoration(
+                    offset, &span, decoration, &decoration_spans, span_ts,
+                ));
+            }
+
+            if let Some(decoration) = span.decoration.overline.take() {
+                let offset = match writing_mode {
+                    WritingMode::LeftToRight => -span.font.ascent(span.font_size),
+                    WritingMode::TopToBottom => -span.font.height(span.font_size) / 2.0,
+                };
+
+                new_paths.push(convert_decoration(
+                    offset, &span, decoration, &decoration_spans, span_ts,
+                ));
+            }
+
+            if let Some(path) = convert_span(span, &mut clusters, &span_ts, parent, false) {
+                new_paths.push(path);
+            }
+
+            if let Some(decoration) = span.decoration.line_through.take() {
+                let offset = match writing_mode {
+                    WritingMode::LeftToRight => -span.font.x_height(span.font_size) / 2.0,
+                    WritingMode::TopToBottom => 0.0,
+                };
+
+                new_paths.push(convert_decoration(
+                    offset, &span, decoration, &decoration_spans, span_ts,
+                ));
+            }
+        }
+
+        char_offset += chunk.text.chars().count();
+        last_x = x + curr_pos.0;
+        last_y = y + curr_pos.1;
+    }
+
+    new_paths
+}
+
+fn convert_span(
+    span: &mut TextSpan,
+    clusters: &mut [OutlinedCluster],
+    text_ts: &tree::Transform,
+    parent: &mut tree::Node,
+    dump_clusters: bool,
+) -> Option<tree::Path> {
+    let mut segments = Vec::new();
+
+    for cluster in clusters {
+        if !cluster.visible {
+            continue;
+        }
+
+        if span.contains(cluster.byte_idx) {
+            if dump_clusters {
+                let mut ts = *text_ts;
+                ts.append(&cluster.transform);
+                dump_cluster(cluster, ts, parent);
+            }
+
+            let mut path = mem::replace(&mut cluster.path, Vec::new());
+            crate::utils::transform_path(&mut path, &cluster.transform);
+
+            segments.extend_from_slice(&path);
+        }
+    }
+
+    if segments.is_empty() {
+        return None;
+    }
+
+    let mut fill = span.fill.take();
+    if let Some(ref mut fill) = fill {
+        // fill-rule on text must always be `nonzero`,
+        // otherwise overlapped characters will be clipped.
+        fill.rule = tree::FillRule::NonZero;
+    }
+
+    let path = tree::Path {
+        id: String::new(),
+        transform: *text_ts,
+        visibility: span.visibility,
+        fill,
+        stroke: span.stroke.take(),
+        rendering_mode: tree::ShapeRendering::default(),
+        segments,
+    };
+
+    Some(path)
+}
+
+// Only for debug purposes.
+fn dump_cluster(
+    cluster: &OutlinedCluster,
+    text_ts: tree::Transform,
+    parent: &mut tree::Node,
+) {
+    fn new_stroke(color: tree::Color) -> Option<tree::Stroke> {
+        Some(tree::Stroke {
+            paint: tree::Paint::Color(color),
+            width: tree::StrokeWidth::new(0.2),
+            .. tree::Stroke::default()
+        })
+    }
+
+    let mut base_path = tree::Path {
+        transform: text_ts,
+        .. tree::Path::default()
+    };
+
+    // Cluster bbox.
+    let r = Rect::new(0.0, -cluster.ascent,
+                      cluster.advance, cluster.height()).unwrap();
+    base_path.stroke = new_stroke(tree::Color::blue());
+    base_path.segments = utils::rect_to_path(r);
+    parent.append_kind(tree::NodeKind::Path(base_path.clone()));
+
+    // Baseline.
+    base_path.stroke = new_stroke(tree::Color::red());
+    base_path.segments = vec![
+        tree::PathSegment::MoveTo { x: 0.0,             y: 0.0 },
+        tree::PathSegment::LineTo { x: cluster.advance, y: 0.0 },
+    ];
+    parent.append_kind(tree::NodeKind::Path(base_path));
+}
+
+fn collect_decoration_spans(
+    span: &TextSpan,
+    clusters: &[OutlinedCluster],
+) -> Vec<DecorationSpan> {
+    let mut spans = Vec::new();
+
+    let mut started = false;
+    let mut width = 0.0;
+    let mut transform = tree::Transform::default();
+    for cluster in clusters {
+        if span.contains(cluster.byte_idx) {
+            if started && cluster.has_relative_shift {
+                started = false;
+                spans.push(DecorationSpan { width, transform });
+            }
+
+            if !started {
+                width = cluster.advance;
+                started = true;
+                transform = cluster.transform;
+            } else {
+                width += cluster.advance;
+            }
+        } else if started {
+            spans.push(DecorationSpan { width, transform });
+            started = false;
+        }
+    }
+
+    if started {
+        spans.push(DecorationSpan { width, transform });
+    }
+
+    spans
+}
+
+fn convert_decoration(
+    dy: f64,
+    span: &TextSpan,
+    mut decoration: TextDecorationStyle,
+    decoration_spans: &[DecorationSpan],
+    transform: tree::Transform,
+) -> tree::Path {
+    debug_assert!(!decoration_spans.is_empty());
+
+    let thickness = span.font.underline_thickness(span.font_size);
+
+    let mut segments = Vec::new();
+    for dec_span in decoration_spans {
+        let rect = Rect::new(
+            0.0,
+            -thickness / 2.0,
+            dec_span.width,
+            thickness,
+        ).unwrap();
+
+        let start_idx = segments.len();
+        add_rect_to_path(rect, &mut segments);
+
+        let mut ts = dec_span.transform;
+        ts.translate(0.0, dy);
+        crate::utils::transform_path(&mut segments[start_idx..], &ts);
+    }
+
+    tree::Path {
+        id: String::new(),
+        transform,
+        visibility: span.visibility,
+        fill: decoration.fill.take(),
+        stroke: decoration.stroke.take(),
+        rendering_mode: tree::ShapeRendering::default(),
+        segments,
+    }
+}
+
+fn add_rect_to_path(rect: Rect, path: &mut Vec<tree::PathSegment>) {
+    path.extend_from_slice(&[
+        tree::PathSegment::MoveTo { x: rect.x(),     y: rect.y() },
+        tree::PathSegment::LineTo { x: rect.right(), y: rect.y() },
+        tree::PathSegment::LineTo { x: rect.right(), y: rect.bottom() },
+        tree::PathSegment::LineTo { x: rect.x(),     y: rect.bottom() },
+        tree::PathSegment::ClosePath,
+    ]);
+}
+
+/// By the SVG spec, `tspan` doesn't have a bbox and uses the parent `text` bbox.
+/// Since we converted `text` and `tspan` to `path`, we have to update
+/// all linked paint servers (gradients and patterns) too.
+fn fix_obj_bounding_box(
+    path: &mut tree::Path,
+    bbox: Rect,
+    tree: &mut tree::Tree,
+) {
+    if let Some(ref mut fill) = path.fill {
+        if let tree::Paint::Link(ref mut id) = fill.paint {
+            if let Some(new_id) = paint_server_to_user_space_on_use(id, bbox, tree) {
+                *id = new_id;
+            }
+        }
+    }
+
+    if let Some(ref mut stroke) = path.stroke {
+        if let tree::Paint::Link(ref mut id) = stroke.paint {
+            if let Some(new_id) = paint_server_to_user_space_on_use(id, bbox, tree) {
+                *id = new_id;
+            }
+        }
+    }
+}
+
+/// Converts a selected paint server's units to `UserSpaceOnUse`.
+///
+/// Creates a deep copy of a selected paint server and returns its ID.
+///
+/// Returns `None` if a paint server already uses `UserSpaceOnUse`.
+fn paint_server_to_user_space_on_use(
+    id: &str,
+    bbox: Rect,
+    tree: &mut tree::Tree,
+) -> Option<String> {
+    if let Some(ps) = tree.defs_by_id(id) {
+        let is_obj_bbox = match *ps.borrow() {
+            tree::NodeKind::LinearGradient(ref lg) => {
+                lg.units == tree::Units::ObjectBoundingBox
+            }
+            tree::NodeKind::RadialGradient(ref rg) => {
+                rg.units == tree::Units::ObjectBoundingBox
+            }
+            tree::NodeKind::Pattern(ref patt) => {
+                patt.units == tree::Units::ObjectBoundingBox
+            }
+            _ => false,
+        };
+
+        // Do nothing.
+        if !is_obj_bbox {
+            return None;
+        }
+
+
+        // TODO: is `pattern` copying safe? Maybe we should reset id's on all `pattern` children.
+        // We have to clone a paint server, in case some other element is already using it.
+        // If not, the `convert` module will remove unused defs anyway.
+        let mut new_ps = ps.clone().make_deep_copy();
+        tree.defs().append(new_ps.clone());
+
+        let new_id = gen_paint_server_id(tree);
+
+        // Update id, transform and units.
+        match *new_ps.borrow_mut() {
+            tree::NodeKind::LinearGradient(ref mut lg) => {
+                if lg.units == tree::Units::ObjectBoundingBox {
+                    lg.id = new_id.clone();
+                    lg.base.transform.prepend(&tree::Transform::from_bbox(bbox));
+                    lg.base.units = tree::Units::UserSpaceOnUse;
+                }
+            }
+            tree::NodeKind::RadialGradient(ref mut rg) => {
+                if rg.units == tree::Units::ObjectBoundingBox {
+                    rg.id = new_id.clone();
+                    rg.base.transform.prepend(&tree::Transform::from_bbox(bbox));
+                    rg.base.units = tree::Units::UserSpaceOnUse;
+                }
+            }
+            tree::NodeKind::Pattern(ref mut patt) => {
+                if patt.units == tree::Units::ObjectBoundingBox {
+                    patt.id = new_id.clone();
+                    patt.transform.prepend(&tree::Transform::from_bbox(bbox));
+                    patt.units = tree::Units::UserSpaceOnUse;
+                }
+            }
+            _ => return None,
+        }
+
+        Some(new_id)
+    } else {
+        None
+    }
+}
+
+/// Creates a free id for a paint server.
+fn gen_paint_server_id(
+    tree: &tree::Tree,
+) -> String {
+    // TODO: speed up
+
+    let mut idx = 1;
+    let mut id = format!("usvg{}", idx);
+    while tree.defs().children().any(|n| *n.id() == id) {
+        idx += 1;
+        id = format!("usvg{}", idx);
+    }
+
+    id
+}
diff --git a/usvg/src/convert/text/shaper.rs b/usvg/src/convert/text/shaper.rs
new file mode 100644
index 000000000..734ccfed1
--- /dev/null
+++ b/usvg/src/convert/text/shaper.rs
@@ -0,0 +1,950 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// external
+use harfbuzz_rs as harfbuzz;
+use unicode_bidi;
+use unicode_script;
+use unicode_vo::{self, Orientation as CharOrientation};
+
+mod fk {
+    pub use font_kit::handle::Handle;
+    pub use font_kit::hinting::HintingOptions as Hinting;
+    pub use font_kit::source::SystemSource;
+}
+
+// self
+use crate::tree;
+use crate::convert::prelude::*;
+use super::convert::{
+    ByteIndex,
+    CharacterPosition,
+    Font,
+    TextAnchor,
+    TextChunk,
+    TextFlow,
+    TextPath,
+    WritingMode,
+};
+
+
+/// A glyph.
+///
+/// Basically, a glyph ID and it's metrics.
+#[derive(Clone)]
+struct Glyph {
+    /// The glyph ID in the font.
+    id: u32,
+
+    /// Position in bytes in the original string.
+    ///
+    /// We use it to match a glyph with a character in the text chunk and therefore with the style.
+    byte_idx: ByteIndex,
+
+    /// The glyph offset in font units.
+    dx: i32,
+
+    /// The glyph offset in font units.
+    dy: i32,
+
+    /// The glyph width / X-advance in font units.
+    width: i32,
+
+    /// Reference to the source font.
+    ///
+    /// Each glyph can have it's own source font.
+    font: Font,
+}
+
+impl Glyph {
+    fn is_missing(&self) -> bool {
+        self.id == 0
+    }
+}
+
+
+/// An outlined cluster.
+///
+/// Cluster/grapheme is a single, unbroken, renderable character.
+/// It can be positioned, rotated, spaced, etc.
+///
+/// Let's say we have `й` which is *CYRILLIC SMALL LETTER I* and *COMBINING BREVE*.
+/// It consists of two code points, will be shaped (via harfbuzz) as two glyphs into one cluster,
+/// and then will be combined into the one `OutlinedCluster`.
+#[derive(Clone)]
+pub struct OutlinedCluster {
+    /// Position in bytes in the original string.
+    ///
+    /// We use it to match a cluster with a character in the text chunk and therefore with the style.
+    pub byte_idx: ByteIndex,
+
+    /// Cluster's original codepoint.
+    ///
+    /// Technically, a cluster can contain multiple codepoints,
+    /// but we are storing only the first one.
+    pub codepoint: char,
+
+    /// An advance along the X axis.
+    ///
+    /// Can be negative.
+    pub advance: f64,
+
+    /// An ascent in SVG coordinates.
+    pub ascent: f64,
+
+    /// A descent in SVG coordinates.
+    pub descent: f64,
+
+    /// A x-height in SVG coordinates.
+    pub x_height: f64,
+
+    /// Indicates that this cluster was affected by the relative shift (via dx/dy attributes)
+    /// during the text layouting. Which breaks the `text-decoration` line.
+    ///
+    /// Used during the `text-decoration` processing.
+    pub has_relative_shift: bool,
+
+    /// An actual outline.
+    pub path: Vec<tree::PathSegment>,
+
+    /// A cluster's transform that contains it's position, rotation, etc.
+    pub transform: tree::Transform,
+
+    /// Not all clusters should be rendered.
+    ///
+    /// For example, if a cluster is outside the text path than it should not be rendered.
+    pub visible: bool,
+}
+
+impl OutlinedCluster {
+    pub fn height(&self) -> f64 {
+        self.ascent - self.descent
+    }
+}
+
+
+/// An iterator over glyph clusters.
+///
+/// Input:  0 2 2 2 3 4 4 5 5
+/// Result: 0 1     4 5   7
+struct GlyphClusters<'a> {
+    data: &'a [Glyph],
+    idx: usize,
+}
+
+impl<'a> GlyphClusters<'a> {
+    fn new(data: &'a [Glyph]) -> Self {
+        GlyphClusters { data, idx: 0 }
+    }
+}
+
+impl<'a> Iterator for GlyphClusters<'a> {
+    type Item = (std::ops::Range<usize>, ByteIndex);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.idx == self.data.len() {
+            return None;
+        }
+
+        let start = self.idx;
+        let cluster = self.data[self.idx].byte_idx;
+        for g in &self.data[self.idx..] {
+            if g.byte_idx != cluster {
+                break;
+            }
+
+            self.idx += 1;
+        }
+
+        Some((start..self.idx, cluster))
+    }
+}
+
+
+/// Converts a text chunk into a list of outlined clusters.
+///
+/// This function will do the BIDI reordering, text shaping and glyphs outlining,
+/// but not the text layouting. So all clusters are in the 0x0 position.
+pub fn outline_chunk(
+    chunk: &TextChunk,
+    state: &State,
+) -> Vec<OutlinedCluster> {
+    let mut glyphs = Vec::new();
+    for span in &chunk.spans {
+        let tmp_glyphs = shape_text(&chunk.text, &span.font, state);
+
+        // Do nothing with the first run.
+        if glyphs.is_empty() {
+            glyphs = tmp_glyphs;
+            continue;
+        }
+
+        // We assume, that shaping with an any font will produce the same amount of glyphs.
+        // Otherwise an error.
+        if glyphs.len() != tmp_glyphs.len() {
+            warn!("Text layouting failed.");
+            return Vec::new();
+        }
+
+        // Copy span's glyphs.
+        for (i, glyph) in tmp_glyphs.iter().enumerate() {
+            if span.contains(glyph.byte_idx) {
+                glyphs[i] = glyph.clone();
+            }
+        }
+    }
+
+    // Convert glyphs to clusters.
+    let mut clusters = Vec::new();
+    for (range, byte_idx) in GlyphClusters::new(&glyphs) {
+        if let Some(span) = chunk.span_at(byte_idx) {
+            clusters.push(outline_cluster(&glyphs[range], &chunk.text, span.font_size));
+        }
+    }
+
+    clusters
+}
+
+/// Text shaping with font fallback.
+fn shape_text(
+    text: &str,
+    font: &Font,
+    state: &State,
+) -> Vec<Glyph> {
+    let mut glyphs = shape_text_with_font(text, font);
+
+    // Remember all fonts used for shaping.
+    let mut used_fonts = vec![font.clone()];
+
+    // Loop until all glyphs become resolved or until no more fonts are left.
+    'outer: loop {
+        let mut missing = None;
+        for glyph in &glyphs {
+            if glyph.is_missing() {
+                missing = Some(glyph.byte_idx.char_from(text));
+                break;
+            }
+        }
+
+        if let Some(c) = missing {
+            let fallback_font = match find_font_for_char(c, &used_fonts, state) {
+                Some(v) => v,
+                None => break 'outer,
+            };
+
+            // Shape again, using a new font.
+            let fallback_glyphs = shape_text_with_font(text, &fallback_font);
+
+            // We assume, that shaping with an any font will produce the same amount of glyphs.
+            // Otherwise an error.
+            if glyphs.len() != fallback_glyphs.len() {
+                break 'outer;
+            }
+
+            // Copy new glyphs.
+            for i in 0..glyphs.len() {
+                if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
+                    glyphs[i] = fallback_glyphs[i].clone();
+                }
+            }
+
+            // Remember this font.
+            used_fonts.push(fallback_font);
+        } else {
+            break 'outer;
+        }
+    }
+
+    // Warn about missing glyphs.
+    for glyph in &glyphs {
+        if glyph.is_missing() {
+            let c = glyph.byte_idx.char_from(text);
+            warn!("No fonts with a {}/U+{:X} character were found.", c, c as u32);
+        }
+    }
+
+    glyphs
+}
+
+/// Converts a text into a list of glyph IDs.
+///
+/// This function will do the BIDI reordering and text shaping.
+fn shape_text_with_font(
+    text: &str,
+    font: &Font,
+) -> Vec<Glyph> {
+    let font_data = try_opt!(font.handle.copy_font_data(), Vec::new());
+    let hb_face = harfbuzz::Face::from_bytes(&font_data, font.index);
+    let hb_font = harfbuzz::Font::new(hb_face);
+
+    let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
+    let paragraph = &bidi_info.paragraphs[0];
+    let line = paragraph.range.clone();
+
+    let mut glyphs = Vec::new();
+
+    let (levels, runs) = bidi_info.visual_runs(&paragraph, line);
+    for run in runs.iter() {
+        let sub_text = &text[run.clone()];
+        if sub_text.is_empty() {
+            continue;
+        }
+
+        let hb_direction = if levels[run.start].is_rtl() {
+            harfbuzz::Direction::Rtl
+        } else {
+            harfbuzz::Direction::Ltr
+        };
+
+        let buffer = harfbuzz::UnicodeBuffer::new()
+            .add_str(sub_text)
+            .set_direction(hb_direction);
+
+        // TODO: feature smcp / small caps
+        //       simply setting the `smcp` doesn't work for some reasons
+
+        let output = harfbuzz::shape(&hb_font, buffer, &[]);
+
+        let positions = output.get_glyph_positions();
+        let infos = output.get_glyph_infos();
+
+        for (pos, info) in positions.iter().zip(infos) {
+            let idx = run.start + info.cluster as usize;
+            debug_assert!(text.get(idx..).is_some());
+
+            glyphs.push(Glyph {
+                byte_idx: ByteIndex::new(idx),
+                id: info.codepoint,
+                dx: pos.x_offset,
+                dy: pos.y_offset,
+                width: pos.x_advance,
+                font: font.clone(),
+            });
+        }
+    }
+
+    glyphs
+}
+
+/// Outlines a glyph cluster.
+///
+/// Uses one or more `Glyph`s to construct an `OutlinedCluster`.
+fn outline_cluster(
+    glyphs: &[Glyph],
+    text: &str,
+    font_size: f64,
+) -> OutlinedCluster {
+    debug_assert!(!glyphs.is_empty());
+
+    use lyon_path::builder::FlatPathBuilder;
+
+    let mut path = Vec::new();
+    let mut advance = 0.0;
+    let mut x = 0.0;
+
+    for glyph in glyphs {
+        let mut builder = svgdom_path_builder::Builder::new();
+        let mut outline = match glyph.font.handle.outline(glyph.id, fk::Hinting::None, &mut builder) {
+            Ok(_) => {
+                crate::convert::path::convert(builder.build())
+            }
+            Err(_) => {
+                // Technically unreachable.
+                warn!("Glyph {} not found in the font.", glyph.id);
+                Vec::new()
+            }
+        };
+
+        let sx = glyph.font.scale(font_size);
+
+        if !outline.is_empty() {
+            // By default, glyphs are upside-down, so we have to mirror them.
+            let mut ts = tree::Transform::new_scale(1.0, -1.0);
+
+            // Scale to font-size.
+            ts.scale(sx, sx);
+
+            // Apply offset.
+            //
+            // The first glyph in the cluster will have an offset from 0x0,
+            // but the later one will have an offset from the "current position".
+            // So we have to keep an advance.
+            // TODO: should be done only inside a single text span
+            ts.translate(x + glyph.dx as f64, glyph.dy as f64);
+
+            crate::utils::transform_path(&mut outline, &ts);
+
+            path.extend_from_slice(&outline);
+        }
+
+        x += glyph.width as f64;
+
+        let glyph_width = glyph.width as f64 * sx;
+        if glyph_width > advance {
+            advance = glyph_width;
+        }
+    }
+
+    OutlinedCluster {
+        byte_idx: glyphs[0].byte_idx,
+        codepoint: glyphs[0].byte_idx.char_from(text),
+        advance,
+        ascent: glyphs[0].font.ascent(font_size),
+        descent: glyphs[0].font.descent(font_size),
+        x_height: glyphs[0].font.x_height(font_size),
+        has_relative_shift: false,
+        path,
+        transform: tree::Transform::default(),
+        visible: true,
+    }
+}
+
+/// Finds a font with a specified char.
+///
+/// This is a rudimentary font fallback algorithm.
+fn find_font_for_char(
+    c: char,
+    exclude_fonts: &[Font],
+    state: &State,
+) -> Option<Font> {
+    let base_font = exclude_fonts[0].clone();
+
+    let mut cache = state.font_cache.borrow_mut();
+    cache.init();
+
+    // Iterate over fonts and check if any of them support the specified char.
+    for handle in cache.fonts() {
+        let (path, index) = match handle {
+            fk::Handle::Path { ref path, font_index } => {
+                (path, *font_index)
+            }
+            _ => continue,
+        };
+
+        // Ignore fonts, that were used for shaping already.
+        let exclude = exclude_fonts
+            .iter()
+            .find(|f| f.path == *path && f.index == index)
+            .is_some();
+
+        if exclude {
+            continue;
+        }
+
+        if let Some(font) = super::load_font(handle) {
+            if base_font.handle.properties() != font.handle.properties() {
+                continue;
+            }
+
+            if font.handle.glyph_for_char(c).is_none() {
+                continue;
+            }
+
+            warn!("Fallback from {} to {}.", exclude_fonts[0].path.display(), path.display());
+            return Some(font);
+        }
+    }
+
+    None
+}
+
+/// Resolves clusters positions.
+///
+/// Mainly sets the `transform` property.
+///
+/// Returns the last text position. The next text chunk should start from that position.
+pub fn resolve_clusters_positions(
+    chunk: &TextChunk,
+    offset: usize,
+    pos_list: &[CharacterPosition],
+    rotate_list: &[f64],
+    clusters: &mut [OutlinedCluster],
+) -> (f64, f64) {
+    match chunk.text_flow {
+        TextFlow::Horizontal => {
+            resolve_clusters_positions_horizontal(chunk, offset, pos_list, rotate_list, clusters)
+        }
+        TextFlow::Path(ref path) => {
+            resolve_clusters_positions_path(chunk, offset, path, pos_list, rotate_list, clusters)
+        }
+    }
+}
+
+fn resolve_clusters_positions_horizontal(
+    chunk: &TextChunk,
+    offset: usize,
+    pos_list: &[CharacterPosition],
+    rotate_list: &[f64],
+    clusters: &mut [OutlinedCluster],
+) -> (f64, f64) {
+    let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
+    let mut y = 0.0;
+
+    for cluster in clusters {
+        let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
+        if let Some(pos) = pos_list.get(cp) {
+            x += pos.dx.unwrap_or(0.0);
+            y += pos.dy.unwrap_or(0.0);
+            cluster.has_relative_shift = pos.dx.is_some() || pos.dy.is_some();
+        }
+
+        cluster.transform.translate(x, y);
+
+        if let Some(angle) = rotate_list.get(cp).cloned() {
+            if !angle.is_fuzzy_zero() {
+                cluster.transform.rotate(angle);
+                cluster.has_relative_shift = true;
+            }
+        }
+
+        x += cluster.advance;
+    }
+
+    (x, y)
+}
+
+fn resolve_clusters_positions_path(
+    chunk: &TextChunk,
+    offset: usize,
+    path: &TextPath,
+    pos_list: &[CharacterPosition],
+    rotate_list: &[f64],
+    clusters: &mut [OutlinedCluster],
+) -> (f64, f64) {
+    let mut last_x = 0.0;
+    let mut last_y = 0.0;
+    let mut last_dx = 0.0;
+    let mut last_dy = 0.0;
+
+    let start_offset = path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
+    let normals = collect_normals(clusters, &path.segments, start_offset);
+    for (cluster, normal) in clusters.iter_mut().zip(normals) {
+        let (mut x, mut y, angle) = match normal {
+            Some(normal) => {
+                (normal.x, normal.y, normal.angle)
+            }
+            None => {
+                // Hide clusters that are outside the text path.
+                cluster.visible = false;
+                continue;
+            }
+        };
+
+        // We have to break a decoration line for each cluster during text-on-path.
+        cluster.has_relative_shift = true;
+
+        let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
+        if let Some(pos) = pos_list.get(cp) {
+            x += pos.dx.unwrap_or(last_dx);
+            y += pos.dy.unwrap_or(last_dy);
+
+            last_dx = pos.dx.unwrap_or(last_dx);
+            last_dy = pos.dy.unwrap_or(last_dy);
+        }
+
+        // Clusters should be rotated by the x-midpoint x baseline position.
+        let half_advance = cluster.advance / 2.0;
+        cluster.transform.translate(x - half_advance, y);
+        cluster.transform.rotate_at(angle, half_advance, 0.0);
+
+        if let Some(angle) = rotate_list.get(cp).cloned() {
+            if !angle.is_fuzzy_zero() {
+                cluster.transform.rotate(angle);
+            }
+        }
+
+        last_x = x + cluster.advance;
+        last_y = y;
+    }
+
+    (last_x, last_y)
+}
+
+fn clusters_length(clusters: &[OutlinedCluster]) -> f64 {
+    clusters.iter().fold(0.0, |w, cluster| w + cluster.advance)
+}
+
+fn process_anchor(a: TextAnchor, text_width: f64) -> f64 {
+    match a {
+        TextAnchor::Start   => 0.0, // Nothing.
+        TextAnchor::Middle  => -text_width / 2.0,
+        TextAnchor::End     => -text_width,
+    }
+}
+
+struct PathNormal {
+    x: f64,
+    y: f64,
+    angle: f64,
+}
+
+fn collect_normals(
+    clusters: &[OutlinedCluster],
+    segments: &[tree::PathSegment],
+    offset: f64,
+) -> Vec<Option<PathNormal>> {
+    debug_assert!(!segments.is_empty());
+
+    let mut offsets = Vec::with_capacity(clusters.len());
+    let mut normals = Vec::with_capacity(clusters.len());
+    {
+        let mut advance = offset;
+        for cluster in clusters {
+            // Clusters should be rotated by the x-midpoint x baseline position.
+            let half_advance = cluster.advance / 2.0;
+
+            let offset = advance + half_advance;
+
+            // Clusters outside the path have no normals.
+            if offset < 0.0 {
+                normals.push(None);
+            }
+
+            offsets.push(offset);
+            advance += cluster.advance;
+        }
+    }
+
+    let (mut prev_mx, mut prev_my, mut prev_x, mut prev_y) = {
+        if let tree::PathSegment::MoveTo { x, y } = segments[0] {
+            (x, y, x, y)
+        } else {
+            unreachable!();
+        }
+    };
+
+    fn create_curve(px: f64, py: f64, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64)
+        -> lyon_geom::CubicBezierSegment<f64>
+    {
+        lyon_geom::CubicBezierSegment {
+            from:  lyon_geom::math::F64Point::new(px, py),
+            ctrl1: lyon_geom::math::F64Point::new(x1, y1),
+            ctrl2: lyon_geom::math::F64Point::new(x2, y2),
+            to:    lyon_geom::math::F64Point::new(x, y),
+        }
+    }
+
+    fn create_curve_from_line(px: f64, py: f64, x: f64, y: f64)
+        -> lyon_geom::CubicBezierSegment<f64>
+    {
+        let line = lyon_geom::LineSegment {
+            from:  lyon_geom::math::F64Point::new(px, py),
+            to:    lyon_geom::math::F64Point::new(x, y),
+        };
+
+        let p1 = line.sample(0.33);
+        let p2 = line.sample(0.66);
+        create_curve(px, py, p1.x, p1.y, p2.x, p2.y, x, y)
+    }
+
+    let mut length = 0.0;
+    for seg in segments {
+        let curve = match *seg {
+            tree::PathSegment::MoveTo { x, y } => {
+                prev_mx = x;
+                prev_my = y;
+                prev_x = x;
+                prev_y = y;
+                continue;
+            }
+            tree::PathSegment::LineTo { x, y } => {
+                create_curve_from_line(prev_x, prev_y, x, y)
+            }
+            tree::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
+                create_curve(prev_x, prev_y, x1, y1, x2, y2, x, y)
+            }
+            tree::PathSegment::ClosePath => {
+                create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
+            }
+        };
+
+        let curve_len = curve.approximate_length(1.0);
+
+        for offset in &offsets[normals.len()..] {
+            if *offset >= length && *offset <= length + curve_len {
+                let offset = (offset - length) / curve_len;
+                debug_assert!(offset >= 0.0 && offset <= 1.0);
+
+                let pos = curve.sample(offset);
+
+                let d = curve.derivative(offset);
+                let d = lyon_geom::utils::normalized_tangent(d);
+
+                let angle = d.angle_from_x_axis().to_degrees() - 90.0;
+
+                normals.push(Some(PathNormal {
+                    x: pos.x,
+                    y: pos.y,
+                    angle,
+                }));
+
+                if normals.len() == offsets.len() {
+                    break;
+                }
+            }
+        }
+
+        length += curve_len;
+        prev_x = curve.to.x;
+        prev_y = curve.to.y;
+    }
+
+    // If path ended and we still have unresolved normals - set them to `None`.
+    for _ in 0..(offsets.len() - normals.len()) {
+        normals.push(None);
+    }
+
+    normals
+}
+
+/// Applies the `letter-spacing` property to a text chunk clusters.
+///
+/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property).
+pub fn apply_letter_spacing(
+    chunk: &TextChunk,
+    clusters: &mut [OutlinedCluster],
+) {
+    // At least one span should have a non-zero spacing.
+    if !chunk.spans.iter().any(|span| !span.letter_spacing.is_fuzzy_zero()) {
+        return;
+    }
+
+    for cluster in clusters {
+        // Spacing must be applied only to characters that belongs to the script
+        // that supports spacing.
+        // We are checking only the first code point, since it should be enough.
+        let script = unicode_script::get_script(cluster.codepoint);
+        if script_supports_letter_spacing(script) {
+            if let Some(span) = chunk.span_at(cluster.byte_idx) {
+                // Technically, we should ignore spacing on the last character,
+                // but it doesn't affect us in any way, so we are ignoring this.
+                cluster.advance += span.letter_spacing;
+
+                // If the cluster advance became negative - clear it.
+                // This is an UB so we can do whatever we want, so we mimic the Chrome behavior.
+                if !(cluster.advance > 0.0) {
+                    cluster.advance = 0.0;
+                    cluster.path.clear();
+                }
+            }
+        }
+    }
+}
+
+/// Checks that selected script supports letter spacing.
+///
+/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking).
+///
+/// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64
+fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
+    use unicode_script::Script;
+
+    match script {
+          Script::Arabic
+        | Script::Syriac
+        | Script::Nko
+        | Script::Manichaean
+        | Script::Psalter_Pahlavi
+        | Script::Mandaic
+        | Script::Mongolian
+        | Script::Phags_Pa
+        | Script::Devanagari
+        | Script::Bengali
+        | Script::Gurmukhi
+        | Script::Modi
+        | Script::Sharada
+        | Script::Syloti_Nagri
+        | Script::Tirhuta
+        | Script::Ogham => false,
+        _ => true,
+    }
+}
+
+/// Applies the `word-spacing` property to a text chunk clusters.
+///
+/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing).
+pub fn apply_word_spacing(
+    chunk: &TextChunk,
+    clusters: &mut [OutlinedCluster],
+) {
+    // At least one span should have a non-zero spacing.
+    if !chunk.spans.iter().any(|span| !span.word_spacing.is_fuzzy_zero()) {
+        return;
+    }
+
+    for cluster in clusters {
+        if is_word_separator_characters(cluster.codepoint) {
+            if let Some(span) = chunk.span_at(cluster.byte_idx) {
+                // Technically, word spacing 'should be applied half on each
+                // side of the character', but it doesn't affect us in any way,
+                // so we are ignoring this.
+                cluster.advance += span.word_spacing;
+
+                // After word spacing, `advance` can be negative.
+            }
+        }
+    }
+}
+
+pub fn shift_clusters_baseline(
+    chunk: &TextChunk,
+    clusters: &mut [OutlinedCluster],
+) {
+    for span in &chunk.spans {
+        for cluster in clusters.iter_mut() {
+            if span.contains(cluster.byte_idx) {
+                cluster.transform.translate(0.0, -span.baseline_shift);
+            }
+        }
+    }
+}
+
+/// Checks that the selected character is a word separator.
+///
+/// According to: https://www.w3.org/TR/css-text-3/#word-separator
+fn is_word_separator_characters(c: char) -> bool {
+    match c as u32 {
+        0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F => true,
+        _ => false,
+    }
+}
+
+/// Rotates clusters according to
+/// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html).
+pub fn apply_writing_mode(
+    writing_mode: WritingMode,
+    clusters: &mut [OutlinedCluster],
+) {
+    if writing_mode != WritingMode::TopToBottom {
+        return;
+    }
+
+    for cluster in clusters {
+        let orientation = unicode_vo::char_orientation(cluster.codepoint);
+        if orientation == CharOrientation::Upright {
+            // Additional offset. Not sure why.
+            let dy = cluster.advance - cluster.height();
+
+            // Rotate a cluster 90deg counter clockwise by the center.
+            let mut ts = tree::Transform::default();
+            ts.translate(cluster.advance / 2.0, 0.0);
+            ts.rotate(-90.0);
+            ts.translate(-cluster.advance / 2.0, -dy);
+            crate::utils::transform_path(&mut cluster.path, &ts);
+
+            // Move "baseline" to the middle and make height equal to advance.
+            cluster.ascent = cluster.advance / 2.0;
+            cluster.descent = -cluster.advance / 2.0;
+        } else {
+            // Could not find a spec that explains this,
+            // but this is how other applications are shifting the "rotated" characters
+            // in the top-to-bottom mode.
+            cluster.transform.translate(0.0, cluster.x_height / 2.0);
+        }
+    }
+}
+
+
+/// Implements an ability to outline a glyph directly into the `svgdom::Path`.
+mod svgdom_path_builder {
+    use lyon_geom::math::*;
+    use lyon_path::builder::{FlatPathBuilder, PathBuilder};
+
+    pub struct Builder {
+        path: svgdom::Path,
+        current_position: Point,
+        first_position: Point,
+    }
+
+    impl Builder {
+        pub fn new() -> Self {
+            Builder {
+                path: svgdom::Path::new(),
+                current_position: Point::new(0.0, 0.0),
+                first_position: Point::new(0.0, 0.0),
+            }
+        }
+    }
+
+    impl FlatPathBuilder for Builder {
+        type PathType = svgdom::Path;
+
+        fn move_to(&mut self, to: Point) {
+            self.first_position = to;
+            self.current_position = to;
+            self.path.push(svgdom::PathSegment::MoveTo { abs: true, x: to.x as f64, y: to.y as f64 });
+        }
+
+        fn line_to(&mut self, to: Point) {
+            self.current_position = to;
+            self.path.push(svgdom::PathSegment::LineTo { abs: true, x: to.x as f64, y: to.y as f64 });
+        }
+
+        fn close(&mut self) {
+            self.current_position = self.first_position;
+            self.path.push(svgdom::PathSegment::ClosePath { abs: true });
+        }
+
+        fn build(self) -> Self::PathType {
+            self.path
+        }
+
+        fn build_and_reset(&mut self) -> Self::PathType {
+            let p = self.path.clone();
+            self.path.clear();
+            self.current_position = Point::new(0.0, 0.0);
+            self.first_position = Point::new(0.0, 0.0);
+            p
+        }
+
+        fn current_position(&self) -> Point {
+            self.current_position
+        }
+    }
+
+    impl PathBuilder for Builder {
+        fn quadratic_bezier_to(&mut self, ctrl: Point, to: Point) {
+            self.current_position = to;
+            self.path.push(svgdom::PathSegment::Quadratic {
+                abs: true,
+                x1: ctrl.x as f64,
+                y1: ctrl.y as f64,
+                x: to.x as f64,
+                y: to.y as f64,
+            });
+        }
+
+        fn cubic_bezier_to(&mut self, ctrl1: Point, ctrl2: Point, to: Point) {
+            self.current_position = to;
+            self.path.push(svgdom::PathSegment::CurveTo {
+                abs: true,
+                x1: ctrl1.x as f64,
+                y1: ctrl1.y as f64,
+                x2: ctrl2.x as f64,
+                y2: ctrl2.y as f64,
+                x: to.x as f64,
+                y: to.y as f64,
+            });
+        }
+
+        fn arc(&mut self, center: Point, radii: Vector, sweep_angle: Angle, x_rotation: Angle) {
+            let arc = lyon_geom::arc::Arc {
+                start_angle: (self.current_position() - center).angle_from_x_axis() - x_rotation,
+                center, radii, sweep_angle, x_rotation,
+            };
+            let arc = arc.to_svg_arc();
+
+            self.path.push(svgdom::PathSegment::EllipticalArc {
+                abs: true,
+                rx: arc.radii.x as f64,
+                ry: arc.radii.y as f64,
+                x_axis_rotation: arc.x_rotation.to_degrees() as f64,
+                large_arc: arc.flags.large_arc,
+                sweep: arc.flags.sweep,
+                x: arc.to.x as f64,
+                y: arc.to.y as f64,
+            });
+        }
+    }
+}
diff --git a/usvg/src/geom.rs b/usvg/src/geom.rs
index 28ebf9dc3..bb575e529 100644
--- a/usvg/src/geom.rs
+++ b/usvg/src/geom.rs
@@ -142,6 +142,13 @@ impl Rect {
         }
     }
 
+    /// Creates a new `Rect` for bounding box calculation.
+    ///
+    /// Shorthand for `Rect::new(f64::MAX, f64::MAX, 1.0, 1.0)`.
+    pub fn new_bbox() -> Self {
+        Rect::new(f64::MAX, f64::MAX, 1.0, 1.0).unwrap()
+    }
+
     /// Returns rect's size.
     pub fn size(&self) -> Size {
         Size::new(self.width, self.height).unwrap()
@@ -219,6 +226,31 @@ impl Rect {
 
         true
     }
+
+    /// Expands the `Rect` to the provided size.
+    pub fn expand(&self, r: Rect) -> Self {
+        #[inline]
+        fn f64_min(v1: f64, v2: f64) -> f64 {
+            if v1 < v2 { v1 } else { v2 }
+        }
+
+        #[inline]
+        fn f64_max(v1: f64, v2: f64) -> f64 {
+            if v1 > v2 { v1 } else { v2 }
+        }
+
+        if self.fuzzy_eq(&Rect::new_bbox()) {
+            r
+        } else {
+            let x1 = f64_min(self.x(), r.x());
+            let y1 = f64_min(self.y(), r.y());
+
+            let x2 = f64_max(self.right(), r.right());
+            let y2 = f64_max(self.bottom(), r.bottom());
+
+            Rect::new(x1, y1, x2 - x1, y2 - y1).unwrap()
+        }
+    }
 }
 
 impl FuzzyEq for Rect {
diff --git a/usvg/src/lib.rs b/usvg/src/lib.rs
index 785c53138..4751b18ad 100644
--- a/usvg/src/lib.rs
+++ b/usvg/src/lib.rs
@@ -163,3 +163,16 @@ impl IsValidLength for f64 {
         *self > 0.0
     }
 }
+
+
+/// Converts `Rect` into bbox `Transform`.
+pub trait TransformFromBBox: Sized {
+    /// Converts `Rect` into bbox `Transform`.
+    fn from_bbox(bbox: Rect) -> Self;
+}
+
+impl TransformFromBBox for tree::Transform {
+    fn from_bbox(bbox: Rect) -> Self {
+        Self::new(bbox.width(), 0.0, 0.0, bbox.height(), bbox.x(), bbox.y())
+    }
+}
diff --git a/usvg/src/tree/attributes.rs b/usvg/src/tree/attributes.rs
index 1a35e44f4..0e4797f65 100644
--- a/usvg/src/tree/attributes.rs
+++ b/usvg/src/tree/attributes.rs
@@ -194,194 +194,6 @@ enum_to_string!(Visibility,
 );
 
 
-/// A text decoration style.
-///
-/// Defines the style of the line that should be rendered.
-#[allow(missing_docs)]
-#[derive(Clone, Default, Debug)]
-pub struct TextDecorationStyle {
-    pub fill: Option<Fill>,
-    pub stroke: Option<Stroke>,
-}
-
-
-/// A text decoration.
-#[derive(Clone, Default, Debug)]
-pub struct TextDecoration {
-    /// Draw underline using specified style.
-    ///
-    /// Should be drawn before/under text.
-    pub underline: Option<TextDecorationStyle>,
-
-    /// Draw overline using specified style.
-    ///
-    /// Should be drawn before/under text.
-    pub overline: Option<TextDecorationStyle>,
-
-    /// Draw line-through using specified style.
-    ///
-    /// Should be drawn after/over text.
-    pub line_through: Option<TextDecorationStyle>,
-}
-
-
-/// A text anchor.
-///
-/// `text-anchor` attribute in the SVG.
-#[allow(missing_docs)]
-#[derive(Clone, Copy, PartialEq, Debug)]
-pub enum TextAnchor {
-    Start,
-    Middle,
-    End,
-}
-
-enum_default!(TextAnchor, Start);
-
-enum_from_str!(TextAnchor,
-    "start"     => TextAnchor::Start,
-    "middle"    => TextAnchor::Middle,
-    "end"       => TextAnchor::End
-);
-
-enum_to_string!(TextAnchor,
-    TextAnchor::Start   => "start",
-    TextAnchor::Middle  => "middle",
-    TextAnchor::End     => "end"
-);
-
-
-/// A font style.
-///
-/// `font-style` attribute in the SVG.
-#[allow(missing_docs)]
-#[derive(Clone, Copy, PartialEq, Debug)]
-pub enum FontStyle {
-    Normal,
-    Italic,
-    Oblique,
-}
-
-enum_default!(FontStyle, Normal);
-
-enum_from_str!(FontStyle,
-    "normal"    => FontStyle::Normal,
-    "italic"    => FontStyle::Italic,
-    "oblique"   => FontStyle::Oblique
-);
-
-enum_to_string!(FontStyle,
-    FontStyle::Normal   => "normal",
-    FontStyle::Italic   => "italic",
-    FontStyle::Oblique  => "oblique"
-);
-
-
-/// A font variant.
-///
-/// `font-variant` attribute in the SVG.
-#[allow(missing_docs)]
-#[derive(Clone, Copy, PartialEq, Debug)]
-pub enum FontVariant {
-    Normal,
-    SmallCaps,
-}
-
-enum_default!(FontVariant, Normal);
-
-enum_from_str!(FontVariant,
-    "normal"        => FontVariant::Normal,
-    "small-caps"    => FontVariant::SmallCaps
-);
-
-enum_to_string!(FontVariant,
-    FontVariant::Normal     => "normal",
-    FontVariant::SmallCaps  => "small-caps"
-);
-
-
-/// A font weight.
-///
-/// `font-weight` attribute in the SVG.
-#[allow(missing_docs)]
-#[derive(Clone, Copy, PartialEq, Debug)]
-pub enum FontWeight {
-    W100,
-    W200,
-    W300,
-    W400,
-    W500,
-    W600,
-    W700,
-    W800,
-    W900,
-}
-
-enum_default!(FontWeight, W400);
-
-enum_to_string!(FontWeight,
-    FontWeight::W100 => "100",
-    FontWeight::W200 => "200",
-    FontWeight::W300 => "300",
-    FontWeight::W400 => "400",
-    FontWeight::W500 => "500",
-    FontWeight::W600 => "600",
-    FontWeight::W700 => "700",
-    FontWeight::W800 => "800",
-    FontWeight::W900 => "900"
-);
-
-
-/// A font stretch.
-///
-/// `font-stretch` attribute in the SVG.
-#[allow(missing_docs)]
-#[derive(Clone, Copy, PartialEq, Debug)]
-pub enum FontStretch {
-    Normal,
-    Wider,
-    Narrower,
-    UltraCondensed,
-    ExtraCondensed,
-    Condensed,
-    SemiCondensed,
-    SemiExpanded,
-    Expanded,
-    ExtraExpanded,
-    UltraExpanded,
-}
-
-enum_default!(FontStretch, Normal);
-
-enum_from_str!(FontStretch,
-    "normal"            => FontStretch::Normal,
-    "wider"             => FontStretch::Wider,
-    "narrower"          => FontStretch::Narrower,
-    "ultra-condensed"   => FontStretch::UltraCondensed,
-    "extra-condensed"   => FontStretch::ExtraCondensed,
-    "condensed"         => FontStretch::Condensed,
-    "semi-condensed"    => FontStretch::SemiCondensed,
-    "semi-expanded"     => FontStretch::SemiExpanded,
-    "expanded"          => FontStretch::Expanded,
-    "extra-expanded"    => FontStretch::ExtraExpanded,
-    "ultra-expanded"    => FontStretch::UltraExpanded
-);
-
-enum_to_string!(FontStretch,
-    FontStretch::Normal         => "normal",
-    FontStretch::Wider          => "wider",
-    FontStretch::Narrower       => "narrower",
-    FontStretch::UltraCondensed => "ultra-condensed",
-    FontStretch::ExtraCondensed => "extra-condensed",
-    FontStretch::Condensed      => "condensed",
-    FontStretch::SemiCondensed  => "semi-condensed",
-    FontStretch::SemiExpanded   => "semi-expanded",
-    FontStretch::Expanded       => "expanded",
-    FontStretch::ExtraExpanded  => "extra-expanded",
-    FontStretch::UltraExpanded  => "ultra-expanded"
-);
-
-
 /// A paint style.
 ///
 /// `paint` value type in the SVG.
@@ -457,33 +269,6 @@ impl Default for Stroke {
 }
 
 
-/// A font description.
-#[allow(missing_docs)]
-#[derive(Clone, Debug)]
-pub struct Font {
-    /// Font family.
-    ///
-    /// Currently, is exactly the same as in the `font-family` attribute.
-    /// So it can look like `Verdana, 'Times New Roman', sans-serif`.
-    pub family: String,
-    pub size: FontSize,
-    pub style: FontStyle,
-    pub variant: FontVariant,
-    pub weight: FontWeight,
-    pub stretch: FontStretch,
-
-    /// Letter spacing.
-    ///
-    /// None == `normal`
-    pub letter_spacing: Option<f64>,
-
-    /// Word spacing.
-    ///
-    /// None == `normal`
-    pub word_spacing: Option<f64>,
-}
-
-
 /// View box.
 #[derive(Clone, Copy, Debug)]
 pub struct ViewBox {
diff --git a/usvg/src/tree/export.rs b/usvg/src/tree/export.rs
index 50e0b49be..8f2a5a542 100644
--- a/usvg/src/tree/export.rs
+++ b/usvg/src/tree/export.rs
@@ -319,75 +319,6 @@ fn conv_elements(
                 conv_fill(tree, &p.fill, defs, parent, &mut path_elem);
                 conv_stroke(tree, &p.stroke, defs, &mut path_elem);
             }
-            NodeKind::Text(ref text) => {
-                let mut text_elem = new_doc.create_element(EId::Text);
-                parent.append(text_elem.clone());
-
-                text_elem.set_id(text.id.clone());
-
-                conv_transform(AId::Transform, &text.transform, &mut text_elem);
-                text_elem.set_enum_attribute(AId::TextRendering, text.rendering_mode);
-
-                if let Some(ref rotate) = text.rotate {
-                    text_elem.set_attribute((AId::Rotate, NumberList(rotate.clone())));
-                }
-
-                let mut is_preserve_required = false;
-
-                for chunk in &text.chunks {
-                    let mut chunk_tspan_elem = new_doc.create_element(EId::Tspan);
-                    text_elem.append(chunk_tspan_elem.clone());
-
-                    if let Some(x) = chunk.x {
-                        chunk_tspan_elem.set_attribute((AId::X, x));
-                    }
-
-                    if let Some(y) = chunk.y {
-                        chunk_tspan_elem.set_attribute((AId::Y, y));
-                    }
-
-                    if let Some(dx) = chunk.dx {
-                        chunk_tspan_elem.set_attribute((AId::Dx, dx));
-                    }
-
-                    if let Some(dy) = chunk.dy {
-                        chunk_tspan_elem.set_attribute((AId::Dy, dy));
-                    }
-
-                    chunk_tspan_elem.set_enum_attribute(AId::TextAnchor, chunk.anchor);
-
-                    for tspan in &chunk.spans {
-                        let mut tspan_elem = new_doc.create_element(EId::Tspan);
-                        chunk_tspan_elem.append(tspan_elem.clone());
-
-                        let text_node = new_doc.create_node(
-                            svgdom::NodeType::Text,
-                            tspan.text.clone(),
-                        );
-                        tspan_elem.append(text_node.clone());
-
-                        tspan_elem.set_enum_attribute(AId::Visibility, tspan.visibility);
-
-                        conv_fill(tree, &tspan.fill, defs, parent, &mut tspan_elem);
-                        conv_stroke(tree, &tspan.stroke, defs, &mut tspan_elem);
-                        conv_font(&tspan.font, &mut tspan_elem);
-
-                        if !tspan.baseline_shift.is_fuzzy_zero() {
-                            tspan_elem.set_attribute((AId::BaselineShift, tspan.baseline_shift));
-                        }
-
-                        if tspan.text.contains("  ") {
-                            is_preserve_required = true;
-                        }
-
-                        // TODO: text-decoration
-                    }
-                }
-
-                if is_preserve_required {
-                    text_elem.set_attribute((AId::Space, "preserve"));
-                }
-            }
             NodeKind::Image(ref img) => {
                 let mut img_elem = new_doc.create_element(EId::Image);
                 parent.append(img_elem.clone());
@@ -587,30 +518,6 @@ fn conv_transform(
     }
 }
 
-fn conv_font(
-    font: &Font,
-    node: &mut svgdom::Node,
-) {
-    node.set_attribute((AId::FontFamily, font.family.clone()));
-    node.set_attribute((AId::FontSize, font.size.value()));
-    node.set_enum_attribute(AId::FontStyle, font.style);
-    node.set_enum_attribute(AId::FontVariant, font.variant);
-    node.set_enum_attribute(AId::FontWeight, font.weight);
-    node.set_enum_attribute(AId::FontStretch, font.stretch);
-    conv_text_spacing(font.letter_spacing, AId::LetterSpacing, node);
-    conv_text_spacing(font.word_spacing, AId::WordSpacing, node);
-}
-
-fn conv_text_spacing(
-    spacing: Option<f64>,
-    aid: AId,
-    node: &mut svgdom::Node,
-) {
-    if let Some(spacing) = spacing {
-        node.set_attribute((aid, spacing));
-    }
-}
-
 fn conv_opt_link(
     tree: &Tree,
     defs: &svgdom::Node,
diff --git a/usvg/src/tree/mod.rs b/usvg/src/tree/mod.rs
index c5eca694b..27e40643e 100644
--- a/usvg/src/tree/mod.rs
+++ b/usvg/src/tree/mod.rs
@@ -28,6 +28,7 @@ mod numbers;
 pub mod prelude {
     pub use crate::IsDefault;
     pub use crate::IsValidLength;
+    pub use crate::TransformFromBBox;
     pub use crate::tree::FuzzyEq;
     pub use crate::tree::FuzzyZero;
     pub use super::NodeExt;
diff --git a/usvg/src/tree/nodes.rs b/usvg/src/tree/nodes.rs
index 6a3aad8ab..25f65d704 100644
--- a/usvg/src/tree/nodes.rs
+++ b/usvg/src/tree/nodes.rs
@@ -24,7 +24,6 @@ pub enum NodeKind {
     Pattern(Pattern),
     Filter(Filter),
     Path(Path),
-    Text(Text),
     Image(Image),
     Group(Group),
 }
@@ -45,7 +44,6 @@ impl NodeKind {
             NodeKind::Pattern(ref e) => e.id.as_str(),
             NodeKind::Filter(ref e) => e.id.as_str(),
             NodeKind::Path(ref e) => e.id.as_str(),
-            NodeKind::Text(ref e) => e.id.as_str(),
             NodeKind::Image(ref e) => e.id.as_str(),
             NodeKind::Group(ref e) => e.id.as_str(),
         }
@@ -66,7 +64,6 @@ impl NodeKind {
             NodeKind::Pattern(ref e) => e.transform,
             NodeKind::Filter(_) => Transform::default(),
             NodeKind::Path(ref e) => e.transform,
-            NodeKind::Text(ref e) => e.transform,
             NodeKind::Image(ref e) => e.transform,
             NodeKind::Group(ref e) => e.transform,
         }
@@ -141,114 +138,6 @@ impl Default for Path {
 }
 
 
-/// A text element.
-///
-/// `text` element in SVG.
-#[derive(Clone, Debug)]
-pub struct Text {
-    /// Element's ID.
-    ///
-    /// Taken from the SVG itself.
-    /// Isn't automatically generated.
-    /// Can be empty.
-    pub id: String,
-
-    /// Element transform.
-    pub transform: Transform,
-
-    /// Rotate list.
-    ///
-    /// If set, contains a list of rotation angles for each *code point* in the `text`.
-    pub rotate: Option<Vec<f64>>,
-
-    /// Rendering mode.
-    ///
-    /// `text-rendering` in SVG.
-    pub rendering_mode: TextRendering,
-
-    /// A list of text chunks.
-    pub chunks: Vec<TextChunk>,
-}
-
-
-/// A text chunk.
-///
-/// Contains position and anchor of the next
-/// [text chunk](https://www.w3.org/TR/SVG11/text.html#TextChunk).
-///
-/// Doesn't represented in SVG directly. Usually, it's a first `tspan` or text node
-/// and any `tspan` that defines either `x` or `y` coordinates.
-///
-/// *Note:* `usvg` text chunk isn't strictly the same as an SVG one,
-/// because text chunk should be defined only by `x` or `y` coordinates.
-/// But since `resvg` backends doesn't have a direct access to glyphs/graphemes
-/// we have to create chunks on `dx`, `dy` and non-zero `rotate` to simulate this.
-/// This is incorrect, since it breaks text shaping
-/// and BIDI reordering (resvg doesn't support this one anyway).
-#[derive(Clone, Debug)]
-pub struct TextChunk {
-    /// An optional absolute positions along the X-axis.
-    pub x: Option<f64>,
-
-    /// An optional absolute positions along the Y-axis.
-    pub y: Option<f64>,
-
-    /// An optional relative positions along the X-axis.
-    pub dx: Option<f64>,
-
-    /// An optional relative positions along the Y-axis.
-    pub dy: Option<f64>,
-
-    /// A text anchor/align.
-    pub anchor: TextAnchor,
-
-    /// A list of text spans.
-    pub spans: Vec<TextSpan>,
-}
-
-
-/// A text span.
-///
-/// `tspan` element in SVG.
-#[derive(Clone, Debug)]
-pub struct TextSpan {
-    /// Element visibility.
-    pub visibility: Visibility,
-
-    /// Fill style.
-    pub fill: Option<Fill>,
-
-    /// Stroke style.
-    pub stroke: Option<Stroke>,
-
-    /// Font description.
-    pub font: Font,
-
-    /// Baseline shift.
-    pub baseline_shift: f64,
-
-    /// Text decoration.
-    ///
-    /// Unlike `text-decoration` attribute from the SVG, this one has all styles resolved.
-    /// Basically, by the SVG `text-decoration` attribute can be defined on `tspan` element
-    /// and on any parent element. And all definitions should be combined.
-    /// The one that was defined by `tspan` uses the `tspan` style itself.
-    /// The one that was defined by any parent node uses the `text` element style.
-    /// So it's pretty hard to resolve.
-    ///
-    /// This property has all this stuff resolved.
-    pub decoration: TextDecoration,
-
-    /// An actual text line.
-    ///
-    /// SVG doesn't support multiline text, so this property doesn't have a new line inside of it.
-    /// All the spaces are already trimmed or preserved, depending on the `xml:space` attribute.
-    /// All characters references are already resolved, so there is no `&gt;` or `&#x50;`.
-    /// So this text should be rendered as is, without any postprocessing.
-    pub text: String,
-}
-
-
 /// A raster image element.
 ///
 /// `image` element in SVG.
diff --git a/usvg/src/utils.rs b/usvg/src/utils.rs
index 0cbe7a293..80b2bee44 100644
--- a/usvg/src/utils.rs
+++ b/usvg/src/utils.rs
@@ -82,3 +82,228 @@ pub fn rect_to_path(rect: Rect) -> Vec<tree::PathSegment> {
         tree::PathSegment::ClosePath,
     ]
 }
+
+/// Calculates path's bounding box.
+///
+/// Width and/or height can be zero.
+pub fn path_bbox(
+    segments: &[tree::PathSegment],
+    stroke: Option<&tree::Stroke>,
+    ts: Option<tree::Transform>,
+) -> Option<Rect> {
+    debug_assert!(!segments.is_empty());
+
+    let ts = match ts {
+        Some(ts) => ts,
+        None => tree::Transform::default(),
+    };
+
+    let mut prev_x = 0.0;
+    let mut prev_y = 0.0;
+    let mut minx = 0.0;
+    let mut miny = 0.0;
+    let mut maxx = 0.0;
+    let mut maxy = 0.0;
+
+    if let Some(tree::PathSegment::MoveTo { x, y }) = TransformedPath::new(segments, ts).next() {
+        let x = x as f32;
+        let y = y as f32;
+
+        prev_x = x;
+        prev_y = y;
+        minx = x;
+        miny = y;
+        maxx = x;
+        maxy = y;
+    }
+
+    for seg in TransformedPath::new(segments, ts) {
+        match seg {
+              tree::PathSegment::MoveTo { x, y }
+            | tree::PathSegment::LineTo { x, y } => {
+                let x = x as f32;
+                let y = y as f32;
+                prev_x = x;
+                prev_y = y;
+
+                if x > maxx { maxx = x; }
+                else if x < minx { minx = x; }
+
+                if y > maxy { maxy = y; }
+                else if y < miny { miny = y; }
+            }
+            tree::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
+                let x = x as f32;
+                let y = y as f32;
+
+                let curve = lyon_geom::CubicBezierSegment {
+                    from: lyon_geom::math::Point::new(prev_x, prev_y),
+                    ctrl1: lyon_geom::math::Point::new(x1 as f32, y1 as f32),
+                    ctrl2: lyon_geom::math::Point::new(x2 as f32, y2 as f32),
+                    to: lyon_geom::math::Point::new(x, y),
+                };
+
+                prev_x = x;
+                prev_y = y;
+
+                let r = curve.bounding_rect();
+
+                let right = r.max_x();
+                let bottom = r.max_y();
+                if r.min_x() < minx { minx = r.min_x(); }
+                if right > maxx { maxx = right; }
+                if r.min_y() < miny { miny = r.min_y(); }
+                if bottom > maxy { maxy = bottom; }
+            }
+            tree::PathSegment::ClosePath => {}
+        }
+    }
+
+    // TODO: find a better way
+    // It's an approximation, but it's better than nothing.
+    if let Some(ref stroke) = stroke {
+        let w = (stroke.width.value() / 2.0) as f32;
+        minx -= w;
+        miny -= w;
+        maxx += w;
+        maxy += w;
+    }
+
+    let width = maxx - minx;
+    let height = maxy - miny;
+
+    Rect::new(minx as f64, miny as f64, width as f64, height as f64)
+}
+
+/// An iterator over transformed path segments.
+#[allow(missing_debug_implementations)]
+pub struct TransformedPath<'a> {
+    segments: &'a [tree::PathSegment],
+    ts: tree::Transform,
+    idx: usize,
+}
+
+impl<'a> TransformedPath<'a> {
+    /// Creates a new `TransformedPath` iterator.
+    pub fn new(segments: &'a [tree::PathSegment], ts: tree::Transform) -> Self {
+        TransformedPath { segments, ts, idx: 0 }
+    }
+}
+
+impl<'a> Iterator for TransformedPath<'a> {
+    type Item = tree::PathSegment;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.idx == self.segments.len() {
+            return None;
+        }
+
+        if self.ts.is_default() {
+            self.idx += 1;
+            return self.segments.get(self.idx - 1).cloned();
+        }
+
+        let seg = match self.segments[self.idx] {
+            tree::PathSegment::MoveTo { x, y } => {
+                let (x, y) = self.ts.apply(x, y);
+                tree::PathSegment::MoveTo { x, y }
+            }
+            tree::PathSegment::LineTo { x, y } => {
+                let (x, y) = self.ts.apply(x, y);
+                tree::PathSegment::LineTo { x, y }
+            }
+            tree::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
+                let (x1, y1) = self.ts.apply(x1, y1);
+                let (x2, y2) = self.ts.apply(x2, y2);
+                let (x,  y)  = self.ts.apply(x, y);
+                tree::PathSegment::CurveTo { x1, y1, x2, y2, x, y }
+            }
+            tree::PathSegment::ClosePath => tree::PathSegment::ClosePath,
+        };
+
+        self.idx += 1;
+
+        Some(seg)
+    }
+}
+
+/// Calculates path's length.
+///
+/// Length from the first segment to the first MoveTo, ClosePath or slice end.
+pub fn path_length(segments: &[tree::PathSegment]) -> f64 {
+    debug_assert!(!segments.is_empty());
+
+    let (mut prev_x, mut prev_y) = {
+        if let tree::PathSegment::MoveTo { x, y } = segments[0] {
+            (x as f32, y as f32)
+        } else {
+            panic!("first segment must be MoveTo");
+        }
+    };
+
+    let start_x = prev_x;
+    let start_y = prev_y;
+
+    let mut is_first_seg = true;
+    let mut length = 0.0f64;
+    for seg in segments {
+        match *seg {
+            tree::PathSegment::MoveTo { .. } => {
+                if !is_first_seg {
+                    break;
+                }
+            }
+            tree::PathSegment::LineTo { x, y } => {
+                length += Line::new(prev_x as f64, prev_y as f64, x, y).length();
+
+                prev_x = x as f32;
+                prev_y = y as f32;
+            }
+            tree::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
+                let x = x as f32;
+                let y = y as f32;
+
+                let curve = lyon_geom::CubicBezierSegment {
+                    from: lyon_geom::math::Point::new(prev_x, prev_y),
+                    ctrl1: lyon_geom::math::Point::new(x1 as f32, y1 as f32),
+                    ctrl2: lyon_geom::math::Point::new(x2 as f32, y2 as f32),
+                    to: lyon_geom::math::Point::new(x, y),
+                };
+
+                length += curve.approximate_length(1.0) as f64;
+
+                prev_x = x;
+                prev_y = y;
+            }
+            tree::PathSegment::ClosePath => {
+                length += Line::new(prev_x as f64, prev_y as f64,
+                                    start_x as f64, start_y as f64).length();
+                break;
+            }
+        }
+
+        is_first_seg = false;
+    }
+
+    length
+}
+
+/// Applies the transform to the path segments.
+pub fn transform_path(segments: &mut [tree::PathSegment], ts: &tree::Transform) {
+    for seg in segments {
+        match seg {
+            tree::PathSegment::MoveTo { x, y } => {
+                ts.apply_to(x, y);
+            }
+            tree::PathSegment::LineTo { x, y } => {
+                ts.apply_to(x, y);
+            }
+            tree::PathSegment::CurveTo { x1, y1, x2, y2, x,  y } => {
+                ts.apply_to(x1, y1);
+                ts.apply_to(x2, y2);
+                ts.apply_to(x, y);
+            }
+            tree::PathSegment::ClosePath => {}
+        }
+    }
+}


Test patch:
diff --git a/testing_tools/ci-install.sh b/testing_tools/ci-install.sh
index 086be5676..ee029a778 100755
--- a/testing_tools/ci-install.sh
+++ b/testing_tools/ci-install.sh
@@ -8,7 +8,7 @@ sudo apt-get update -qq
 sudo apt-get install python3.6
 
 if [ "$RESVG_CAIRO_BACKEND" = true ]; then
-    sudo apt-get install -y libpango1.0-dev
+    sudo apt-get install -y libcairo2-dev libgdk-pixbuf2.0-dev
     # for capi/examples/cairo-*
     sudo apt-get install -y libgtk-3-dev
 fi
diff --git a/testing_tools/run-tests.py b/testing_tools/run-tests.py
index 4fbf5fa6a..0a0ad2eac 100755
--- a/testing_tools/run-tests.py
+++ b/testing_tools/run-tests.py
@@ -62,6 +62,10 @@ def regression_testing(backend):
     run(['git', 'clone', TESTS_URL, '--depth', '1', './target/resvg-test-suite'], check=True)
 
 
+with cd('usvg'):
+    run(['cargo', 'test'], check=True)
+
+
 if 'RESVG_QT_BACKEND' in os.environ:
     # build qt backend
     with cd('tools/rendersvg'):
@@ -146,16 +150,3 @@ def regression_testing(backend):
     # build cairo-rs example
     with cd('examples/cairo-rs'):
         run(['cargo', 'build'], check=True)
-
-
-if 'USVG_TESTING' in os.environ:
-    with cd('usvg'):
-        run(['cargo', 'test'], check=True)
-
-    # usvg/testing_tools/regression.py uses tools/usvg
-    with cd('tools/usvg'):
-        run(['cargo', 'build'], check=True)
-
-    with cd('usvg/testing_tools'):
-        run(['./regression.py', '--ci-mode', '../../target/resvg-test-suite/svg',
-             '../../target/test-suite-temp'], check=True)
diff --git a/usvg/testing_tools/allow.csv b/usvg/testing_tools/allow.csv
deleted file mode 100644
index 872ed60dc..000000000
--- a/usvg/testing_tools/allow.csv
+++ /dev/null
@@ -1,263 +0,0 @@
-a-alignment-baseline-001,3064,unsupported
-a-baseline-shift-002,637
-a-baseline-shift-003,547
-a-baseline-shift-010,1254
-a-baseline-shift-011,1113
-a-baseline-shift-012,547
-a-baseline-shift-013,1397
-a-baseline-shift-014,3787
-a-baseline-shift-016,3794
-a-baseline-shift-019,1755
-a-baseline-shift-020,737
-a-baseline-shift-021,1105
-a-direction-001,906,unsupported
-a-direction-002,3257,unsupported
-a-display-002,25284,chrome bug
-a-display-003,25284,chrome bug
-a-display-004,31852,chrome bug
-a-display-005,2018,bug
-a-display-006,1997,chrome bug
-a-display-007,27184,chrome bug
-a-display-008,335
-a-dominant-baseline-001,3060,unsupported
-a-fill-025,25604,chrome bug
-a-fill-034,25604,chrome bug
-a-font-001,191,unsupported
-a-font-size-005,824,chrome uses different coefficients
-a-font-size-007,1538,UA dependent
-a-font-size-008,656,UA dependent
-a-font-size-013,189,UB #10
-a-font-size-016,1510,UA dependent
-a-font-size-017,1088,UA dependent
-a-font-size-019,649,bug
-a-font-weight-003,484,chrome bug
-a-font-weight-012,714,chrome bug
-a-glyph-orientation-vertical-001,1143,unsupported
-a-letter-spacing-005,2171,chrome bug
-a-word-spacing-005,730,chrome bug
-a-opacity-001,591
-a-opacity-002,137
-a-overflow-001,25
-a-overflow-003,25
-a-unicode-bidi-001,1144,unsupported
-a-shape-rendering-001,538
-a-shape-rendering-002,93
-a-shape-rendering-003,93
-a-shape-rendering-004,538
-a-shape-rendering-008,164
-a-stroke-dasharray-012,244
-a-stroke-width-004,964,chrome bug
-a-systemLanguage-001,25604,chrome bug
-a-systemLanguage-002,25604,chrome bug
-a-systemLanguage-003,25604,chrome bug
-a-systemLanguage-005,25604,chrome bug
-a-systemLanguage-006,25284,chrome bug
-a-systemLanguage-008,31852,chrome bug
-a-systemLanguage-009,2018,chrome bug
-a-systemLanguage-010,27184,chrome bug
-a-text-anchor-006,792,chrome or freetype bug
-a-text-anchor-008,846,bug #11
-a-text-anchor-009,272,bug
-a-text-anchor-012,901,bug
-a-text-decoration-001,363,unimplemented #4
-a-text-decoration-002,450,unimplemented #4
-a-text-decoration-003,361,unimplemented #4
-a-text-decoration-004,1166,unimplemented #4
-a-text-decoration-005,364,unimplemented #4
-a-text-decoration-006,364,unimplemented #4
-a-text-decoration-007,363,unimplemented #4
-a-text-decoration-008,242,unimplemented #4
-a-text-decoration-009,1476,unimplemented #4
-a-text-decoration-010,1474,unimplemented #4
-a-text-decoration-011,1881,unimplemented
-a-text-decoration-012,1535,unimplemented
-a-text-decoration-013,1947,unimplemented
-a-text-decoration-014,270,unimplemented
-a-text-rendering-005,313,#95
-a-visibility-005,137
-a-visibility-006,137
-a-visibility-007,29
-a-writing-mode-005,2939,unsupported
-a-writing-mode-006,2939,unsupported
-a-writing-mode-008,2939,unsupported
-a-writing-mode-009,3626,unsupported
-a-writing-mode-010,3487,unsupported
-a-writing-mode-012,3530,unsupported
-a-writing-mode-013,1882,unsupported
-a-writing-mode-014,1363,unsupported
-a-writing-mode-015,1600,unsupported
-e-circle-001,519
-e-circle-002,538
-e-circle-003,262
-e-circle-004,266
-e-circle-005,134
-e-circle-006,538
-e-clipPath-005,124
-e-clipPath-006,211
-e-clipPath-007,54
-e-clipPath-008,254
-e-clipPath-015,54
-e-clipPath-017,397
-e-clipPath-019,97
-e-clipPath-020,97
-e-clipPath-021,97
-e-clipPath-029,66
-e-clipPath-030,50
-e-clipPath-031,199
-e-clipPath-034,99
-e-clipPath-035,50
-e-clipPath-036,97
-e-clipPath-037,66
-e-clipPath-039,948,chrome bug
-e-clipPath-040,6059,chrome bug
-e-ellipse-001,464
-e-ellipse-002,11499,chrome bug
-e-ellipse-003,20329,chrome bug
-e-ellipse-005,223
-e-ellipse-006,246
-e-ellipse-007,123
-e-feImage-006,14408,unsupported
-e-feImage-011,119
-e-filter-017,17534,chrome bug
-e-filter-018,10131,chrome bug
-e-filter-019,21903,chrome bug
-e-image-006,15233,chrome quirks
-e-image-008,15169,chrome quirks
-e-image-025,19951,bug #13
-e-image-026,22684,bug #13
-e-image-027,22684,bug #13
-e-image-028,22684,bug #13
-e-image-029,24795,bug #13
-e-image-030,18844,bug #13
-e-image-031,18844,bug #13
-e-linearGradient-036,25364,UB
-e-linearGradient-037,25284,UB
-e-marker-008,48
-e-marker-015,846
-e-marker-030,704
-e-marker-032,144,UB
-e-marker-035,1183,bug
-e-marker-037,44
-e-marker-038,44
-e-marker-039,39
-e-marker-040,54
-e-marker-041,49
-e-marker-043,24
-e-marker-044,23
-e-marker-046,23
-e-marker-047,22
-e-marker-050,23
-e-marker-051,183
-e-marker-052,23
-e-marker-053,24
-e-marker-054,24
-e-marker-055,126,chrome bug
-e-marker-056,126,chrome bug
-e-mask-015,38
-e-mask-017,22324,bug #14
-e-mask-020,12804,bug
-e-mask-022,21564,UB
-e-path-010,42
-e-path-011,125
-e-path-032,132
-e-path-033,134
-e-path-034,131
-e-path-035,134
-e-path-036,130
-e-path-037,129
-e-path-038,129
-e-path-039,129
-e-pattern-001,170
-e-pattern-002,173
-e-pattern-003,170
-e-pattern-004,170
-e-pattern-005,3687
-e-pattern-006,168
-e-pattern-009,172
-e-pattern-011,173
-e-pattern-012,175
-e-pattern-013,175
-e-pattern-014,335
-e-pattern-015,335
-e-pattern-016,335
-e-pattern-017,175
-e-pattern-018,1262
-e-pattern-020,684
-e-pattern-021,2956,UA dependent
-e-pattern-022,174
-e-pattern-023,174
-e-pattern-024,174
-e-pattern-026,172
-e-pattern-027,171
-e-pattern-028,12763,UB
-e-radialGradient-031,38237,fixes chrome bug
-e-radialGradient-032,25604,fixes chrome bug
-e-radialGradient-033,25604,fixes chrome bug
-e-radialGradient-036,24340,fixes chrome bug
-e-radialGradient-037,12804,fixes chrome bug
-e-radialGradient-040,19573,UB
-e-rect-004,96
-e-rect-005,106
-e-rect-006,106
-e-rect-013,106
-e-rect-014,106
-e-rect-018,521
-e-rect-019,521
-e-rect-020,407
-e-rect-021,289
-e-rect-022,291
-e-rect-023,3322,chrome uses different coefficients
-e-rect-024,138
-e-style-004,39208,CSS support
-e-style-006,39208,CSS support
-e-svg-002,25604,svgdom bug
-e-svg-003,1284,svgdom bug
-e-svg-008,592
-e-svg-022,538
-e-svg-023,538
-e-svg-024,68
-e-svg-025,46
-e-svg-026,59
-e-svg-027,24
-e-svg-028,375
-e-svg-029,375
-e-svg-030,68
-e-switch-002,25604,unsupported #15
-e-switch-005,25604,chrome bug
-e-switch-006,25604,chrome bug
-e-switch-007,25604,chrome bug
-e-switch-009,25604,chrome bug
-e-switch-012,25604,chrome bug
-e-symbol-006,58
-e-text-003,972,invalid chunks
-e-text-006,1993,invalid chunks
-e-text-007,1960,invalid chunks
-e-text-008,1993,invalid chunks
-e-text-011,1974,invalid chunks
-e-text-012,2009,invalid chunks
-e-text-013,2097,invalid chunks
-e-text-014,2009,invalid chunks
-e-text-016,1551,chrome uses different coefficients
-e-text-024,1954,invalid chunks
-e-text-026,144,invalid chunks
-e-text-029,1772,invalid chunks
-e-text-030,198,invalid chunks
-e-text-032,552,unsupported #4
-e-text-033,1312,unsupported #4
-e-text-034,471,unsupported #4
-e-text-036,1513,?
-e-text-038,1959,chrome bug
-e-textPath-001,1286,unsupported
-e-tref-001,2005,chrome bug
-e-tref-002,1180,chrome bug
-e-tref-005,2005,chrome bug
-e-tref-006,2005,chrome bug
-e-tref-007,2005,chrome bug
-e-tref-008,720,bug
-e-tspan-015,3283,bug
-e-tspan-017,1019,invalid chunks
-e-tspan-018,280,chrome bug
-e-tspan-020,1989,chrome bug
-e-tspan-021,2005,chrome bug
-e-tspan-022,6588,chrome bug
-e-tspan-027,3572,bug #122
diff --git a/usvg/testing_tools/cache.csv b/usvg/testing_tools/cache.csv
deleted file mode 100644
index 1953cee55..000000000
--- a/usvg/testing_tools/cache.csv
+++ /dev/null
@@ -1,1072 +0,0 @@
-a-baseline-shift-001,8d059fca
-a-baseline-shift-002,f7912a2b
-a-baseline-shift-003,6c08606a
-a-baseline-shift-004,b0987e4f
-a-baseline-shift-005,83395a44
-a-baseline-shift-006,10af4d4a
-a-baseline-shift-007,ae69eb3a
-a-baseline-shift-008,8d059fca
-a-baseline-shift-009,c739953e
-a-baseline-shift-010,1d837f1c
-a-baseline-shift-011,ca7160b6
-a-baseline-shift-012,2b9d5801
-a-baseline-shift-013,203f1247
-a-baseline-shift-014,8aff4773
-a-baseline-shift-015,1f6c5684
-a-baseline-shift-016,1f6c5684
-a-baseline-shift-017,1f6c5684
-a-baseline-shift-018,1f6c5684
-a-baseline-shift-019,d62b6fdc
-a-baseline-shift-020,ad780460
-a-baseline-shift-021,67a1337c
-a-baseline-shift-022,8d059fca
-a-clip-path-001,07d56367
-a-clip-rule-001,0b8f2c0f
-a-color-001,5b8045ad
-a-color-interpolation-filters-001,68f98f3e
-a-display-001,5b8045ad
-a-display-002,6128e134
-a-display-003,6128e134
-a-display-004,07d56367
-a-display-005,232be037
-a-display-006,b4cf7b9c
-a-display-007,b3a6537f
-a-display-008,f99386f5
-a-display-009,44fb7ec2
-a-fill-001,5b8045ad
-a-fill-002,5b8045ad
-a-fill-003,934d7773
-a-fill-004,8d85b02e
-a-fill-005,5b8045ad
-a-fill-006,5b8045ad
-a-fill-007,5b8045ad
-a-fill-008,5b8045ad
-a-fill-009,5b8045ad
-a-fill-010,5b8045ad
-a-fill-011,a66f2b2b
-a-fill-012,176fda8a
-a-fill-013,cba9c3c0
-a-fill-014,a66f2b2b
-a-fill-015,90a1575b
-a-fill-016,120598a1
-a-fill-017,6aa2dd8b
-a-fill-018,09299d97
-a-fill-019,90a1575b
-a-fill-020,d81e6d04
-a-fill-021,5b8045ad
-a-fill-022,5b8045ad
-a-fill-023,8f23713e
-a-fill-024,120598a1
-a-fill-025,5b8045ad
-a-fill-026,5b8045ad
-a-fill-027,90a1575b
-a-fill-028,bed06148
-a-fill-029,90a1575b
-a-fill-030,90a1575b
-a-fill-031,20c32b2c
-a-fill-032,23dfb86b
-a-fill-033,252dd5ff
-a-fill-034,5b8045ad
-a-fill-035,5b8045ad
-a-fill-036,40ba48dd
-a-fill-037,40ba48dd
-a-fill-038,90a1575b
-a-fill-039,90a1575b
-a-fill-040,90a1575b
-a-fill-opacity-001,973017f0
-a-fill-opacity-002,1bf3e52b
-a-fill-opacity-003,19525403
-a-fill-opacity-004,098f0a83
-a-fill-opacity-005,973017f0
-a-fill-opacity-006,06afe2f5
-a-fill-rule-001,56624911
-a-fill-rule-002,81170992
-a-filter-001,6517edb5
-a-flood-color-001,bc283bfb
-a-flood-color-002,ca6250c1
-a-flood-color-003,ca6250c1
-a-flood-color-004,bc283bfb
-a-flood-color-005,ca6250c1
-a-flood-color-006,ca6250c1
-a-flood-opacity-001,1cb1d811
-a-font-family-001,bcd32679
-a-font-family-002,58425c51
-a-font-family-003,d2b62ca3
-a-font-family-004,f5a9913c
-a-font-family-005,51922a65
-a-font-family-006,300a3973
-a-font-family-007,e9e4dc8d
-a-font-family-008,946c2305
-a-font-family-009,d8b36210
-a-font-family-010,d1afb9ac
-a-font-size-001,78ea9c41
-a-font-size-002,78ea9c41
-a-font-size-003,0b2f6c61
-a-font-size-004,0b2f6c61
-a-font-size-005,2a810c72
-a-font-size-006,4b934d22
-a-font-size-007,9b867e66
-a-font-size-008,30baaa9f
-a-font-size-009,fff06ac6
-a-font-size-010,78ea9c41
-a-font-size-011,78ea9c41
-a-font-size-012,fff06ac6
-a-font-size-013,fff06ac6
-a-font-size-014,4981dea5
-a-font-size-015,6a43ea07
-a-font-size-016,4981dea5
-a-font-size-017,6a43ea07
-a-font-size-018,4981dea5
-a-font-size-019,f67b7ae8
-a-font-size-020,0b2f6c61
-a-font-stretch-001,c10ba469
-a-font-stretch-002,c10ba469
-a-font-style-001,2d75bb66
-a-font-style-002,99b3a82e
-a-font-style-003,2d75bb66
-a-font-variant-001,e0c11e14
-a-font-variant-002,e0c11e14
-a-font-weight-001,26486724
-a-font-weight-002,0e21aa5d
-a-font-weight-003,1b830879
-a-font-weight-004,a16f8192
-a-font-weight-005,0e21aa5d
-a-font-weight-006,0e21aa5d
-a-font-weight-007,533b89da
-a-font-weight-008,5be629a2
-a-font-weight-009,30ae195b
-a-font-weight-010,26486724
-a-font-weight-011,8ea189ab
-a-font-weight-012,8ea189ab
-a-letter-spacing-001,fa01c16a
-a-letter-spacing-002,815c24fd
-a-letter-spacing-003,67f99dd6
-a-letter-spacing-004,fa01c16a
-a-letter-spacing-005,40380977
-a-letter-spacing-006,2b0edbe6
-a-letter-spacing-007,1b5eabe5
-a-letter-spacing-008,03db18c6
-a-letter-spacing-009,502b130f
-a-letter-spacing-010,a806c5c4
-a-marker-end-001,c302a3e8
-a-marker-mid-001,ebf6bc62
-a-marker-start-001,1785141c
-a-mask-001,268e19a2
-a-opacity-001,408f8f50
-a-opacity-002,f16c0b71
-a-opacity-003,41dbe0f3
-a-opacity-004,4ae7b3c2
-a-opacity-005,109cd23c
-a-opacity-006,109cd23c
-a-opacity-007,d4e5c10d
-a-opacity-008,c6531ec7
-a-opacity-009,120598a1
-a-overflow-001,87730da9
-a-overflow-002,0d6a4c93
-a-overflow-003,87730da9
-a-stop-color-001,120598a1
-a-stop-opacity-001,46f9ac19
-a-stroke-001,227a0cf1
-a-stroke-002,c54b49cd
-a-stroke-003,b70460cf
-a-stroke-004,e184281f
-a-stroke-005,8f23713e
-a-stroke-006,40ba48dd
-a-stroke-007,ada1d3ea
-a-stroke-008,f0d126c4
-a-stroke-009,48760601
-a-stroke-010,040d7bf6
-a-stroke-011,59a786c4
-a-stroke-012,040d7bf6
-a-stroke-013,59a786c4
-a-stroke-014,9eac94c7
-a-stroke-015,36fb8fa3
-a-stroke-016,86b055fa
-a-stroke-017,b82df538
-a-stroke-018,2b073016
-a-stroke-019,d10d4a64
-a-stroke-dasharray-001,13c04c40
-a-stroke-dasharray-002,852077cc
-a-stroke-dasharray-003,852077cc
-a-stroke-dasharray-004,a3ec21fa
-a-stroke-dasharray-005,d474bff5
-a-stroke-dasharray-006,6bc6d4ac
-a-stroke-dasharray-007,13c04c40
-a-stroke-dasharray-008,13c04c40
-a-stroke-dasharray-009,13c04c40
-a-stroke-dasharray-010,bbfcd4ba
-a-stroke-dasharray-011,bbfcd4ba
-a-stroke-dasharray-012,5b60ab97
-a-stroke-dashoffset-001,852077cc
-a-stroke-dashoffset-002,e0a62cf6
-a-stroke-dashoffset-003,8acaa353
-a-stroke-dashoffset-004,ab32c6b7
-a-stroke-dashoffset-005,e2af4551
-a-stroke-dashoffset-006,9d3c5546
-a-stroke-linecap-001,8eb0777e
-a-stroke-linecap-002,420a1923
-a-stroke-linecap-003,b157c162
-a-stroke-linecap-004,026f2385
-a-stroke-linecap-005,8819dbd7
-a-stroke-linecap-006,678f66b0
-a-stroke-linecap-007,efa9dd15
-a-stroke-linecap-008,a8c66177
-a-stroke-linecap-009,4e470574
-a-stroke-linejoin-001,3bb12ae5
-a-stroke-linejoin-002,56f4d39f
-a-stroke-linejoin-003,fdbe9a28
-a-stroke-miterlimit-001,1ad8d605
-a-stroke-miterlimit-002,3d1af5dc
-a-stroke-miterlimit-003,981ff5e0
-a-stroke-miterlimit-004,1ad8d605
-a-stroke-miterlimit-005,1ad8d605
-a-stroke-opacity-001,62800f15
-a-stroke-opacity-002,a7fdc02d
-a-stroke-opacity-003,9f427e12
-a-stroke-opacity-004,d8a9121b
-a-stroke-opacity-005,62800f15
-a-stroke-opacity-006,b2b47bb0
-a-stroke-width-001,d936c898
-a-stroke-width-002,7683924b
-a-stroke-width-003,62135a0d
-a-stroke-width-004,62135a0d
-a-stroke-width-005,109fbbbe
-a-style-001,5b8045ad
-a-style-002,5b8045ad
-a-systemLanguage-001,a8d24448
-a-systemLanguage-002,a8d24448
-a-systemLanguage-003,a8d24448
-a-systemLanguage-004,5b8045ad
-a-systemLanguage-005,a8d24448
-a-systemLanguage-006,6128e134
-a-systemLanguage-007,6128e134
-a-systemLanguage-008,07d56367
-a-systemLanguage-009,232be037
-a-systemLanguage-010,b3a6537f
-a-text-anchor-001,7c500d6d
-a-text-anchor-002,5095c15e
-a-text-anchor-003,2ef99163
-a-text-anchor-004,7c500d6d
-a-text-anchor-005,de1cbc33
-a-text-anchor-006,3612404a
-a-text-anchor-007,57ec9cca
-a-text-anchor-008,b8bb2d47
-a-text-anchor-009,80331997
-a-text-anchor-010,e5d17c55
-a-text-anchor-011,f9a841d3
-a-text-anchor-012,a1f7b345
-a-text-decoration-001,8ccbb9a1
-a-text-decoration-002,8ccbb9a1
-a-text-decoration-003,8ccbb9a1
-a-text-decoration-004,8ccbb9a1
-a-text-decoration-005,7d1852e3
-a-text-decoration-006,7d1852e3
-a-text-decoration-007,8b30c130
-a-text-decoration-008,82c15900
-a-text-decoration-009,a3471e2f
-a-text-decoration-010,d60f3a28
-a-text-decoration-011,a9c4f667
-a-text-decoration-012,8ea80472
-a-text-decoration-013,0d1f8383
-a-text-decoration-014,111fab1d
-a-transform-001,6f5fa0c5
-a-transform-002,d3d90150
-a-transform-003,635ef9b2
-a-transform-004,dcf6f4cc
-a-transform-005,32a13fb9
-a-transform-006,b3c87638
-a-transform-007,a6808bc9
-a-transform-008,08bb3873
-a-transform-009,88b1aeef
-a-transform-010,88b1aeef
-a-transform-011,75c21ca2
-a-transform-012,75c21ca2
-a-transform-013,6f5fa0c5
-a-transform-014,6f5fa0c5
-a-transform-015,fff06ac6
-a-transform-016,5df1342f
-a-transform-017,5df1342f
-a-transform-018,5df1342f
-a-transform-019,2c3049f9
-a-visibility-001,7a5f95d7
-a-visibility-002,7a5f95d7
-a-visibility-003,ef0562d8
-a-visibility-004,8d5595cc
-a-visibility-005,5195380f
-a-visibility-006,09d53a9c
-a-visibility-007,07e25918
-a-word-spacing-001,346a5da2
-a-word-spacing-002,2c19ac3a
-a-word-spacing-003,7794bf5c
-a-word-spacing-004,346a5da2
-a-word-spacing-005,2c19ac3a
-a-word-spacing-006,77120d43
-a-word-spacing-007,4943dbb1
-e-a-001,5b8045ad
-e-a-002,e7dbb385
-e-a-003,e7dbb385
-e-a-004,8b6df993
-e-a-005,5957ab0a
-e-circle-001,c9e507f1
-e-circle-002,dcca929d
-e-circle-003,7df3974c
-e-circle-004,8f4dd18b
-e-circle-005,49afde8f
-e-circle-006,dcca929d
-e-clipPath-001,07d56367
-e-clipPath-002,07d56367
-e-clipPath-003,07d56367
-e-clipPath-004,07d56367
-e-clipPath-005,c0fc59c3
-e-clipPath-006,2a27a6ed
-e-clipPath-007,dc178e31
-e-clipPath-008,cca53e81
-e-clipPath-009,98bfb737
-e-clipPath-010,08bfda2b
-e-clipPath-011,40ac8e78
-e-clipPath-012,77182280
-e-clipPath-013,0b8f2c0f
-e-clipPath-014,29ea98c1
-e-clipPath-015,579a172d
-e-clipPath-016,0b8f2c0f
-e-clipPath-017,7be65bed
-e-clipPath-018,0b8f2c0f
-e-clipPath-019,38de6d43
-e-clipPath-020,9ec77244
-e-clipPath-021,1de15009
-e-clipPath-022,f80bcd35
-e-clipPath-023,f80bcd35
-e-clipPath-024,52488807
-e-clipPath-025,f80bcd35
-e-clipPath-026,f80bcd35
-e-clipPath-027,5b8045ad
-e-clipPath-028,5b8045ad
-e-clipPath-029,2a29b4ae
-e-clipPath-030,07d56367
-e-clipPath-031,e561641f
-e-clipPath-032,f80bcd35
-e-clipPath-033,fff06ac6
-e-clipPath-034,21e77fe2
-e-clipPath-035,07d56367
-e-clipPath-036,9ec77244
-e-clipPath-037,7efb422b
-e-clipPath-038,73440e74
-e-clipPath-039,07d56367
-e-clipPath-040,07d56367
-e-clipPath-041,c97e3219
-e-clipPath-042,c97e3219
-e-clipPath-043,fff06ac6
-e-clipPath-044,5498dbff
-e-defs-001,120598a1
-e-defs-002,120598a1
-e-defs-003,82c5fa1a
-e-defs-004,82c5fa1a
-e-defs-005,fff06ac6
-e-defs-006,01481c72
-e-defs-007,5734f360
-e-ellipse-001,ce578909
-e-ellipse-002,fff06ac6
-e-ellipse-003,fff06ac6
-e-ellipse-004,fff06ac6
-e-ellipse-005,fff8bccb
-e-ellipse-006,986d60b8
-e-ellipse-007,714ce75d
-e-ellipse-008,fff06ac6
-e-ellipse-009,fff06ac6
-e-ellipse-010,fff06ac6
-e-feBlend-001,84e9554f
-e-feBlend-002,a02933e8
-e-feBlend-003,9c4e0df2
-e-feBlend-004,07bd5551
-e-feBlend-005,0c751f7b
-e-feBlend-006,eaa1ee69
-e-feBlend-007,d727606f
-e-feBlend-008,c7f3647f
-e-feComposite-001,0c838331
-e-feComposite-002,6bda5d4c
-e-feComposite-003,28fd8137
-e-feComposite-004,26acebcc
-e-feComposite-005,a252e304
-e-feComposite-006,abd7be3d
-e-feComposite-007,4ad3a749
-e-feComposite-008,f6e128d5
-e-feComposite-009,36f364d8
-e-feComposite-010,c6192096
-e-feComposite-011,c5171c25
-e-feFlood-001,9a7fb76e
-e-feFlood-002,0ff92ecf
-e-feFlood-003,cdad45b1
-e-feFlood-004,3871209d
-e-feFlood-005,08cd1ec2
-e-feFlood-006,2a3c379b
-e-feGaussianBlur-001,574ec173
-e-feGaussianBlur-002,add444e6
-e-feGaussianBlur-003,978f0d49
-e-feGaussianBlur-004,978f0d49
-e-feGaussianBlur-005,978f0d49
-e-feGaussianBlur-006,978f0d49
-e-feGaussianBlur-007,574ec173
-e-feGaussianBlur-008,fc5b0e4c
-e-feGaussianBlur-009,c8fb09dd
-e-feGaussianBlur-010,b0424f19
-e-feImage-001,bb7f34bb
-e-feImage-002,edb2c792
-e-feImage-003,595c1294
-e-feImage-004,5ae644e6
-e-feImage-005,d8232da4
-e-feImage-006,0b7a4656
-e-feImage-007,c97a54f4
-e-feImage-008,35475f98
-e-feImage-009,980eab59
-e-feImage-010,ffdea742
-e-feImage-011,4692358e
-e-feMerge-001,4f8a9dd1
-e-feMerge-002,20971a7b
-e-feOffset-001,06a5cd00
-e-feOffset-002,1dd3bcce
-e-feOffset-003,a941e8df
-e-feOffset-004,9655ccf2
-e-feOffset-005,77bc2d5b
-e-feOffset-006,b6ab9871
-e-feOffset-007,c3564b3f
-e-feTile-001,c3b5c867
-e-feTile-002,5ae6bcfb
-e-feTile-003,1c423f4f
-e-feTile-004,1334c823
-e-feTile-005,08376149
-e-feTile-006,1334c823
-e-filter-001,574ec173
-e-filter-002,f1129e1e
-e-filter-003,9f325fa4
-e-filter-004,091a38f0
-e-filter-005,fff06ac6
-e-filter-006,30839b8f
-e-filter-007,962c373a
-e-filter-008,f82b79d7
-e-filter-009,b76b797e
-e-filter-010,022ed676
-e-filter-011,29d39469
-e-filter-012,6bedec14
-e-filter-013,9aebcf01
-e-filter-014,79348ee9
-e-filter-015,86716d90
-e-filter-016,a097511b
-e-filter-017,b15ab5fa
-e-filter-018,b15ab5fa
-e-filter-019,de1620f3
-e-filter-020,574ec173
-e-filter-021,ce97ff0d
-e-filter-022,574ec173
-e-filter-023,574ec173
-e-filter-024,fff06ac6
-e-filter-025,574ec173
-e-filter-026,c63e56c0
-e-filter-027,8ba41a0b
-e-filter-028,10443494
-e-filter-029,ac7bbc58
-e-filter-030,bb061c82
-e-filter-031,a89df57a
-e-filter-032,2423ac48
-e-filter-033,0c7f679b
-e-filter-034,76b843fe
-e-filter-035,e2e5c4b1
-e-filter-036,cf99ebf2
-e-filter-037,7cbf669b
-e-filter-038,182fad2e
-e-filter-039,c62f5891
-e-filter-040,36da3adc
-e-filter-041,0c613d49
-e-filter-042,d52414f3
-e-filter-043,574ec173
-e-filter-044,bbb601d0
-e-filter-045,d53df74d
-e-filter-046,68f98f3e
-e-filter-047,fee3fe7a
-e-filter-048,a0e27dad
-e-filter-049,574ec173
-e-filter-050,5bf58dff
-e-filter-051,fff06ac6
-e-filter-052,27a17c74
-e-filter-053,e97ed320
-e-filter-054,2743e1c2
-e-filter-055,6bedec14
-e-filter-056,dab5381f
-e-filter-057,52fcc964
-e-filter-058,b9fb1be3
-e-filter-059,c4a2fb8a
-e-filter-060,7240bdf7
-e-filter-061,11c8fa57
-e-filter-062,b353b219
-e-filter-063,5b8045ad
-e-g-001,5b8045ad
-e-g-002,1a070240
-e-image-001,901ad382
-e-image-002,bea6969d
-e-image-003,22ac5ecd
-e-image-004,87357c3a
-e-image-005,0c335e10
-e-image-006,a0a290e9
-e-image-007,2f7dbdf3
-e-image-008,219d0173
-e-image-009,87357c3a
-e-image-010,6a8cf99c
-e-image-011,92ed4aac
-e-image-012,90ddcb56
-e-image-013,e006d4f9
-e-image-014,9c1541a8
-e-image-015,e7b28149
-e-image-016,574cb020
-e-image-017,e5e398d2
-e-image-018,9e34c509
-e-image-019,e449bc60
-e-image-020,0ad73ece
-e-image-021,4d607a7f
-e-image-022,6a51d4ba
-e-image-023,bc903032
-e-image-024,f5b6168d
-e-image-025,a5e463a9
-e-image-026,0e35c49c
-e-image-027,0e35c49c
-e-image-028,0e35c49c
-e-image-029,0e35c49c
-e-image-030,0e35c49c
-e-image-031,0e35c49c
-e-image-032,fbf28886
-e-image-033,e06cfe39
-e-image-034,97d3dbe0
-e-line-001,9279d4ae
-e-line-002,47f9ac6a
-e-line-003,70d93ced
-e-line-004,c56c6765
-e-line-005,e7422226
-e-line-006,8ccc82cd
-e-line-007,16b57617
-e-line-008,251a9888
-e-line-009,b21cce51
-e-linearGradient-001,120598a1
-e-linearGradient-002,a60ade64
-e-linearGradient-003,12a0a560
-e-linearGradient-004,b54f83d8
-e-linearGradient-005,a60ade64
-e-linearGradient-006,7d128f5f
-e-linearGradient-007,ee59a36a
-e-linearGradient-008,120598a1
-e-linearGradient-009,1a070240
-e-linearGradient-010,e62524b8
-e-linearGradient-011,1ba31a11
-e-linearGradient-012,cb18e8a1
-e-linearGradient-013,0d22970b
-e-linearGradient-014,ab9df3fe
-e-linearGradient-015,d4834749
-e-linearGradient-016,ee59a36a
-e-linearGradient-017,ca88dae4
-e-linearGradient-018,120598a1
-e-linearGradient-019,ee59a36a
-e-linearGradient-020,d7737af2
-e-linearGradient-021,08ef00eb
-e-linearGradient-022,0ff8d9dd
-e-linearGradient-023,5b8045ad
-e-linearGradient-024,1a070240
-e-linearGradient-025,00980447
-e-linearGradient-026,446ebaf1
-e-linearGradient-027,0304178d
-e-linearGradient-028,6d1c1bd1
-e-linearGradient-029,6fa311f2
-e-linearGradient-030,751f2953
-e-linearGradient-031,120598a1
-e-linearGradient-032,1a070240
-e-linearGradient-033,1a070240
-e-linearGradient-034,5b8045ad
-e-linearGradient-035,1a070240
-e-linearGradient-036,6d1c1bd1
-e-linearGradient-037,6128e134
-e-marker-001,5d7884e1
-e-marker-002,c9cd3ee2
-e-marker-003,c9cd3ee2
-e-marker-004,c9cd3ee2
-e-marker-005,523c69f7
-e-marker-006,898edd5b
-e-marker-007,35064ca1
-e-marker-008,c33d0e9e
-e-marker-009,120813bc
-e-marker-010,56b8c6e9
-e-marker-011,11d7df35
-e-marker-012,d9426df9
-e-marker-013,096ad241
-e-marker-014,84ff56fa
-e-marker-015,563e9a8a
-e-marker-016,d81e6d04
-e-marker-017,ac095cb3
-e-marker-018,58401614
-e-marker-019,f3ec2cea
-e-marker-020,56b8c6e9
-e-marker-021,56b8c6e9
-e-marker-022,30cbc028
-e-marker-023,11d7df35
-e-marker-024,11d7df35
-e-marker-025,11d7df35
-e-marker-026,11d7df35
-e-marker-027,11d7df35
-e-marker-028,56b8c6e9
-e-marker-029,56b8c6e9
-e-marker-030,56b8c6e9
-e-marker-031,cc1ef2d0
-e-marker-032,bedfc8fd
-e-marker-033,634961ba
-e-marker-034,644e2508
-e-marker-035,faf94258
-e-marker-036,1e10f569
-e-marker-037,4a381d29
-e-marker-038,cac3f716
-e-marker-039,ba8d09fb
-e-marker-040,01826266
-e-marker-041,b3d44cf2
-e-marker-042,31d44ee8
-e-marker-043,1c97b201
-e-marker-044,ca41d2c1
-e-marker-045,f5834947
-e-marker-046,aad3c3ec
-e-marker-047,8c8b6566
-e-marker-048,3bc8dc8e
-e-marker-049,cd4dc1b7
-e-marker-050,e2554d92
-e-marker-051,4601f3c4
-e-marker-052,bf934620
-e-marker-053,fc21541b
-e-marker-054,8e184870
-e-marker-055,ed39de6a
-e-marker-056,55580aa3
-e-marker-057,11d7df35
-e-mask-001,268e19a2
-e-mask-002,6b6484bf
-e-mask-003,b3a76e22
-e-mask-004,64ec136e
-e-mask-005,b3973f91
-e-mask-006,268e19a2
-e-mask-007,feafb503
-e-mask-008,fff06ac6
-e-mask-009,fff06ac6
-e-mask-010,fff06ac6
-e-mask-011,01591486
-e-mask-012,109053d9
-e-mask-013,d29dd4b1
-e-mask-014,8ad26f3b
-e-mask-015,a100bf13
-e-mask-016,25e0ebf7
-e-mask-017,268e19a2
-e-mask-018,5b8045ad
-e-mask-019,5b8045ad
-e-mask-020,a9822b64
-e-mask-021,dcef6c47
-e-mask-022,81a4f1d0
-e-mask-023,c84efc90
-e-mask-024,c84efc90
-e-mask-025,dfcf3b90
-e-mask-026,f4432500
-e-mask-027,810e495c
-e-path-001,fff06ac6
-e-path-002,fff06ac6
-e-path-003,0fe2fbba
-e-path-004,a743630b
-e-path-005,08be0706
-e-path-006,447e1304
-e-path-007,18b6495a
-e-path-008,adab0953
-e-path-009,536b391b
-e-path-010,5433347b
-e-path-011,0f4a6d6d
-e-path-012,03bfa1db
-e-path-013,b4042ecf
-e-path-014,d2037525
-e-path-015,8c3cf450
-e-path-016,d9152841
-e-path-017,d9152841
-e-path-018,e929d5a9
-e-path-019,e929d5a9
-e-path-020,bd5c9379
-e-path-021,bb8278b8
-e-path-022,bb8278b8
-e-path-023,cf7f91c7
-e-path-024,00aea196
-e-path-025,cf7f91c7
-e-path-026,a0c961f0
-e-path-027,19699542
-e-path-028,ee69b5e8
-e-path-029,1c88a767
-e-path-030,a857de39
-e-path-031,f9d7beed
-e-path-032,f22a2724
-e-path-033,a760fc6c
-e-path-034,fd0a639b
-e-path-035,842e8263
-e-path-036,4e0c9792
-e-path-037,177f41ca
-e-path-038,f531c6c7
-e-path-039,177f41ca
-e-path-040,ac06c688
-e-path-041,ac06c688
-e-path-042,61a45626
-e-path-043,61a45626
-e-path-044,df89894d
-e-path-045,6d6a5e35
-e-path-046,97503d6d
-e-pattern-001,0d83dddc
-e-pattern-002,3fead5aa
-e-pattern-003,0d83dddc
-e-pattern-004,27e1496a
-e-pattern-005,90423171
-e-pattern-006,c4f51332
-e-pattern-007,44146b09
-e-pattern-008,ddde49e8
-e-pattern-009,028a7739
-e-pattern-010,41882c04
-e-pattern-011,7c062a18
-e-pattern-012,cd24cbb0
-e-pattern-013,cd24cbb0
-e-pattern-014,cea6e32a
-e-pattern-015,e6e1ca52
-e-pattern-016,e6e1ca52
-e-pattern-017,d7ee6ee8
-e-pattern-018,f7840d69
-e-pattern-019,3f720464
-e-pattern-020,3f40aa76
-e-pattern-021,31f151de
-e-pattern-022,54d210ca
-e-pattern-023,54d210ca
-e-pattern-024,c4c8b5de
-e-pattern-025,44146b09
-e-pattern-026,15c95521
-e-pattern-027,da9afb5c
-e-pattern-028,0d83dddc
-e-polygon-001,d7de0868
-e-polygon-002,fff06ac6
-e-polygon-003,6b1231c1
-e-polygon-004,6b1231c1
-e-polygon-005,fff06ac6
-e-polyline-001,52e4a23b
-e-polyline-002,fff06ac6
-e-polyline-003,52e4a23b
-e-polyline-004,52e4a23b
-e-polyline-005,fff06ac6
-e-radialGradient-001,6aa2dd8b
-e-radialGradient-002,4b1ee700
-e-radialGradient-003,0f56fe82
-e-radialGradient-004,0caf4125
-e-radialGradient-005,4b1ee700
-e-radialGradient-006,f47da7a1
-e-radialGradient-007,df9df97a
-e-radialGradient-008,88e5d82b
-e-radialGradient-009,6aa2dd8b
-e-radialGradient-010,1a070240
-e-radialGradient-011,c564659a
-e-radialGradient-012,c564659a
-e-radialGradient-013,1658cf10
-e-radialGradient-014,1ca5174a
-e-radialGradient-015,6aa2dd8b
-e-radialGradient-016,6aa2dd8b
-e-radialGradient-017,b28f0d56
-e-radialGradient-018,6aa2dd8b
-e-radialGradient-019,df9df97a
-e-radialGradient-020,0bec2e02
-e-radialGradient-021,527b7e9b
-e-radialGradient-022,c691ac42
-e-radialGradient-023,a8d24448
-e-radialGradient-024,1a070240
-e-radialGradient-025,21ea5746
-e-radialGradient-026,43312054
-e-radialGradient-027,a7deaf7d
-e-radialGradient-028,154570bb
-e-radialGradient-029,f42064d8
-e-radialGradient-030,5843a9ff
-e-radialGradient-031,8430f4e6
-e-radialGradient-032,5b8045ad
-e-radialGradient-033,00980447
-e-radialGradient-034,21ea5746
-e-radialGradient-035,db64a24e
-e-radialGradient-036,b36b302d
-e-radialGradient-037,06462952
-e-radialGradient-038,6aa2dd8b
-e-radialGradient-039,6aa2dd8b
-e-radialGradient-040,9a560f30
-e-rect-001,7a661386
-e-rect-002,913d63db
-e-rect-003,e35a39f1
-e-rect-004,568f4e3e
-e-rect-005,eb018552
-e-rect-006,eb018552
-e-rect-007,fff06ac6
-e-rect-008,fff06ac6
-e-rect-009,fff06ac6
-e-rect-010,fff06ac6
-e-rect-011,fff06ac6
-e-rect-012,fff06ac6
-e-rect-013,eb018552
-e-rect-014,eb018552
-e-rect-015,4ae7b3c2
-e-rect-016,5b8045ad
-e-rect-017,9cc9cf0b
-e-rect-018,c296f44d
-e-rect-019,c296f44d
-e-rect-020,fd4f39e3
-e-rect-021,58d410ba
-e-rect-022,ac9eeebe
-e-rect-023,ac9eeebe
-e-rect-024,321c480d
-e-rect-025,5b8045ad
-e-rect-026,5b8045ad
-e-rect-027,8ad7e4b8
-e-stop-001,0f130ac6
-e-stop-002,0f130ac6
-e-stop-003,6575167d
-e-stop-004,6575167d
-e-stop-005,6575167d
-e-stop-006,7cf1324d
-e-stop-007,0f130ac6
-e-stop-008,0f130ac6
-e-stop-009,0f130ac6
-e-stop-010,ef3c24ff
-e-stop-011,0f130ac6
-e-stop-012,ef3c24ff
-e-stop-013,ef3c24ff
-e-stop-014,ef3c24ff
-e-stop-015,f8d52eb3
-e-stop-016,b000de22
-e-stop-017,b000de22
-e-stop-018,2da925d8
-e-stop-019,0f130ac6
-e-stop-020,0f130ac6
-e-stop-021,532e28e0
-e-stop-022,6213bdf9
-e-stop-023,9bab2b72
-e-stop-024,4c0e42a6
-e-stop-025,0f130ac6
-e-stop-026,dc245d43
-e-stop-027,6936a62f
-e-stop-028,57e3fe56
-e-stop-029,ee35d2b5
-e-stop-030,d9a1931d
-e-stop-031,d65444de
-e-style-001,5b8045ad
-e-style-002,e11595cb
-e-style-003,5b8045ad
-e-style-004,90a1575b
-e-style-005,bebb0cc6
-e-style-006,90a1575b
-e-style-007,5b8045ad
-e-style-008,5b8045ad
-e-style-009,5b8045ad
-e-style-010,5b8045ad
-e-style-011,5b8045ad
-e-svg-001,5b8045ad
-e-svg-002,5b8045ad
-e-svg-003,34cef7c5
-e-svg-004,5b8045ad
-e-svg-005,5b8045ad
-e-svg-006,b3a6537f
-e-svg-008,bed7a3b7
-e-svg-009,cf16fc9f
-e-svg-010,8d2dd324
-e-svg-011,f8bbe01d
-e-svg-012,cd9054ee
-e-svg-013,2bfc9511
-e-svg-014,35cf18f9
-e-svg-015,d694ce1a
-e-svg-016,8bc84da5
-e-svg-017,b53714ed
-e-svg-018,01481c72
-e-svg-019,01481c72
-e-svg-020,120598a1
-e-svg-021,d97ed041
-e-svg-022,dcca929d
-e-svg-023,dcca929d
-e-svg-024,2355777b
-e-svg-025,63b6e308
-e-svg-026,9b658afc
-e-svg-027,25999d3a
-e-svg-028,0630e231
-e-svg-029,0630e231
-e-svg-030,973dee8d
-e-svg-031,b298acb5
-e-svg-032,fccdf53e
-e-svg-033,5b8045ad
-e-switch-001,a8d24448
-e-switch-002,a8d24448
-e-switch-003,5b8045ad
-e-switch-004,31177ab4
-e-switch-005,5b8045ad
-e-switch-006,5b8045ad
-e-switch-007,5b8045ad
-e-switch-008,a8d24448
-e-switch-009,5b8045ad
-e-switch-010,fff06ac6
-e-switch-011,5b8045ad
-e-switch-012,5b8045ad
-e-switch-013,5b8045ad
-e-symbol-001,01481c72
-e-symbol-002,fff06ac6
-e-symbol-003,257b5e82
-e-symbol-004,29fa0214
-e-symbol-005,05977980
-e-symbol-006,cb202fbe
-e-symbol-007,01481c72
-e-symbol-008,48847cd1
-e-symbol-009,4df2e6d9
-e-text-001,12732e5d
-e-text-002,ecaf5752
-e-text-003,deedf8bc
-e-text-004,ecaf5752
-e-text-005,aba48735
-e-text-006,53444d93
-e-text-007,98e37452
-e-text-008,53444d93
-e-text-009,7f3d5601
-e-text-010,61396c9a
-e-text-011,1b8b0995
-e-text-012,df24dd8d
-e-text-013,dd395264
-e-text-014,df24dd8d
-e-text-015,d4af03c8
-e-text-016,5d26a3e9
-e-text-017,b2d27bc2
-e-text-018,aa63f506
-e-text-019,d760f0f8
-e-text-020,ca5b9253
-e-text-021,5d27c4a7
-e-text-022,46b997b2
-e-text-023,f4050176
-e-text-024,42dab7fc
-e-text-025,ff492191
-e-text-026,bb9d379e
-e-text-027,60b384ed
-e-text-028,cceb6022
-e-text-029,73684d4b
-e-text-030,e22802f1
-e-text-031,7b046988
-e-text-033,ad03436c
-e-text-034,59207146
-e-text-035,14e2ef46
-e-text-036,d694e9eb
-e-text-037,079405e5
-e-text-038,1e291c48
-e-text-039,1647186b
-e-text-040,bf60adbd
-e-tref-001,157b2ebc
-e-tref-002,94a74399
-e-tref-003,079405e5
-e-tref-004,6eedd0eb
-e-tref-005,0763f418
-e-tref-006,157b2ebc
-e-tref-007,157b2ebc
-e-tref-008,5c682c08
-e-tspan-001,d4af03c8
-e-tspan-002,d4af03c8
-e-tspan-003,e7b0b339
-e-tspan-004,56f619a0
-e-tspan-005,22088cfb
-e-tspan-006,3c89627b
-e-tspan-007,1fefba12
-e-tspan-008,1fefba12
-e-tspan-009,08c2da18
-e-tspan-010,5641f861
-e-tspan-011,48f9b1ea
-e-tspan-012,d4af03c8
-e-tspan-013,2a68f66f
-e-tspan-014,897e8afb
-e-tspan-015,d4af03c8
-e-tspan-016,957e4f68
-e-tspan-017,28ef9281
-e-tspan-018,5d091f8c
-e-tspan-019,079405e5
-e-tspan-020,d4af03c8
-e-tspan-021,d4af03c8
-e-tspan-022,d4af03c8
-e-tspan-023,08e4bda1
-e-tspan-024,e16d7638
-e-tspan-025,fd57c72a
-e-tspan-026,bdfb0dff
-e-tspan-027,8850b17c
-e-tspan-028,612ec2d4
-e-tspan-029,8890424c
-e-use-001,fd5cfbb1
-e-use-002,01481c72
-e-use-003,01481c72
-e-use-004,01481c72
-e-use-005,b907252e
-e-use-006,e2c63aee
-e-use-007,e2c63aee
-e-use-008,fff06ac6
-e-use-009,0d0fc92b
-e-use-010,fff06ac6
-e-use-011,fff06ac6
-e-use-012,fff06ac6
-e-use-013,fff06ac6
-e-use-014,ba2e26fb
-e-use-015,5482a4f0
-e-use-016,195ef451
-e-use-017,3c81ca9e
-e-use-018,2db607b8
-e-use-019,fff06ac6
-e-use-020,01481c72
-e-use-021,e2c63aee
-e-use-022,006559b1
-e-use-023,e2c63aee
-e-use-024,e2c63aee
-e-use-025,4969b95a
-e-use-026,4969b95a
-e-use-027,fff06ac6
-e-use-028,fff06ac6
-e-use-029,fff06ac6
-e-use-030,fff06ac6
-a-shape-rendering-001,dcca929d
-a-shape-rendering-002,70fbb428
-a-shape-rendering-003,16db0fe7
-a-shape-rendering-004,dcca929d
-a-shape-rendering-005,ac095cb3
-a-shape-rendering-006,a741e2cd
-a-shape-rendering-007,c8af9dc1
-a-text-rendering-001,b9f5d09b
-a-text-rendering-002,da1c3d98
-a-text-rendering-003,afa836a0
-a-text-rendering-004,5095c15e
-a-text-rendering-005,0ee82461
-a-image-rendering-001,6770f6f0
-a-image-rendering-002,a88c42d0
-a-image-rendering-003,12d0239c
-a-shape-rendering-008,db6a7b2f
-e-image-035,0e35c49c
-e-image-036,0e35c49c
-e-image-037,0e35c49c
-e-feComposite-012,a609e1ba
-e-feComposite-013,353556b9
-e-feComposite-014,f5a1c2dd
-e-feComposite-015,14562e78
-e-feComposite-016,da0be2cd
-e-feComposite-017,f8b33c63
-a-alignment-baseline-001,eff261fe
-a-clip-001,0c335e10
-a-direction-001,5711540e
-a-direction-002,ed3032f2
-a-dominant-baseline-001,c6c3d362
-a-enable-background-001,2423ac48
-a-font-001,bb7c3aae
-a-font-size-adjust-001,adb65ac7
-a-glyph-orientation-horizontal-001,433fbb9d
-a-glyph-orientation-vertical-001,0b9fd2fa
-a-kerning-001,5095c15e
-a-unicode-bidi-001,15ef08d6
-a-writing-mode-001,8aff4773
-a-writing-mode-002,8aff4773
-a-writing-mode-003,8aff4773
-a-writing-mode-004,8aff4773
-a-writing-mode-005,cdbafb10
-a-writing-mode-006,cdbafb10
-a-writing-mode-007,8aff4773
-a-writing-mode-008,cdbafb10
-a-writing-mode-009,8aff4773
-a-writing-mode-010,56054ea4
-a-writing-mode-011,8fd0f596
-a-writing-mode-012,3d0c84b3
-e-textPath-001,f0b031b7
-a-writing-mode-013,508b3452
-a-writing-mode-014,0fbefd30
-a-writing-mode-015,6a8a387a
diff --git a/usvg/testing_tools/chrome-svgrender/.gitignore b/usvg/testing_tools/chrome-svgrender/.gitignore
deleted file mode 100644
index d50251241..000000000
--- a/usvg/testing_tools/chrome-svgrender/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/node_modules
-/package-lock.json
diff --git a/usvg/testing_tools/chrome-svgrender/README.md b/usvg/testing_tools/chrome-svgrender/README.md
deleted file mode 100644
index 53e31f13c..000000000
--- a/usvg/testing_tools/chrome-svgrender/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# svgrender
-
-Render SVG files using Headless Chrome.
-
-## Dependencies
-
-- Node.js
-
-## Build
-
-```
-npm i puppeteer
-```
-
-## Usage
-
-```
-node svgrender.js in.svg out.png 500
-```
diff --git a/usvg/testing_tools/chrome-svgrender/svgrender.js b/usvg/testing_tools/chrome-svgrender/svgrender.js
deleted file mode 100644
index 6c9007f54..000000000
--- a/usvg/testing_tools/chrome-svgrender/svgrender.js
+++ /dev/null
@@ -1,114 +0,0 @@
-'use strict';
-
-const puppeteer = require('puppeteer');
-const path = require('path');
-
-var argv = process.argv.slice(2);
-var svg_path = path.resolve(argv[0]);
-var png_path = argv[1];
-
-(async() => {
-
-try {
-    const browser = await puppeteer.launch({
-        args: ['--no-sandbox', '--disable-setuid-sandbox']
-    });
-    const page = await browser.newPage();
-
-    await page.goto("file://" + svg_path);
-
-    var svg_file_size = await page.evaluate(() => {
-        var svg = document.getElementsByTagNameNS("http://www.w3.org/2000/svg", "svg")[0];
-        return [
-            svg.getAttribute("width"),
-            svg.getAttribute("height")
-        ]
-    });
-
-    var is_dynamic =   (svg_file_size[0] == "100%" && svg_file_size[1] == "100%")
-                    || (svg_file_size[0] == null && svg_file_size[1] == null)
-
-    if (is_dynamic) {
-        var svg_width;
-        if (argv[2] == undefined) {
-            console.log("Error: width argument must be set")
-            process.exit()
-        } else {
-            svg_width = parseInt(argv[2])
-        }
-
-        var svg_view_box = await page.evaluate(() => {
-            var svg = document.querySelector('svg')
-            var box = svg.getAttribute('viewBox');
-            if (box == null) {
-                return null
-            }
-            return box.split(/\s+|,/)
-        });
-
-        if (svg_view_box == null) {
-            console.error("Error: no viewBox")
-            process.exit(1);
-        }
-
-        var view_size;
-        if (svg_width != undefined) {
-            view_size = [svg_width, svg_width]
-        } else {
-            view_size = [svg_view_box[2], svg_view_box[3]]
-        }
-
-        await page.setViewport({
-            width: parseInt(view_size[0]),
-            height: parseInt(view_size[1])
-        });
-
-        var y = 0;
-        var h = view_size[1];
-        if (svg_width != undefined) {
-            const scale = view_size[0] / svg_view_box[2]
-
-            h = Math.round(svg_view_box[3] * scale)
-            y = Math.round((view_size[1] - h) / 2)
-        }
-
-        await page.screenshot({
-            path: png_path,
-            clip: { x: 0, y: y, width: view_size[0], height: h }
-        });
-    } else {
-        var svg_rect = await page.evaluate(() => {
-            var root = document.rootElement;
-            return [
-                root.x.baseVal.value,
-                root.y.baseVal.value,
-                root.width.baseVal.value,
-                root.height.baseVal.value
-            ]
-        });
-
-        var svg_scale = 1
-        if (argv[2] != undefined) {
-            svg_scale = argv[2] / svg_rect[2]
-        }
-
-        await page.setViewport({
-            width: 2000,
-            height: 2000,
-            deviceScaleFactor: svg_scale
-        });
-
-        await page.screenshot({
-            path: png_path,
-            clip: { x: svg_rect[0], y: svg_rect[1], width: svg_rect[2], height: svg_rect[3] }
-        });
-    }
-
-    browser.close();
-
-} catch (e) {
-    console.log(e)
-    process.exit(1)
-}
-
-})();
diff --git a/usvg/testing_tools/regression.py b/usvg/testing_tools/regression.py
deleted file mode 100755
index 22583cba4..000000000
--- a/usvg/testing_tools/regression.py
+++ /dev/null
@@ -1,175 +0,0 @@
-#!/usr/bin/env python3.6
-
-import argparse
-import csv
-import fnmatch
-import hashlib
-import os
-import subprocess as proc
-from pathlib import Path
-from shutil import copyfile
-from subprocess import run
-
-
-# List of files that should be skipped.
-CRASH_ALLOWED = [
-    'e-svg-007.svg',  # non-UTF8 encoding
-    'e-svg-034.svg',  # invalid size
-    'e-svg-035.svg',  # invalid size
-    'e-svg-036.svg',  # invalid size
-]
-
-CACHE_FILENAME = 'cache.csv'
-
-
-def change_ext(path, suffix, new_ext):
-    return Path(path).stem + suffix + '.' + new_ext
-
-
-def render_svg(in_svg, out_png):
-    run(['node', 'svgrender.js', in_svg, out_png, '200'],
-        check=True, cwd='chrome-svgrender')
-
-
-def load_last_pos():
-    path = args.work_dir / 'pos.txt'
-    if path.exists():
-        with open(path, 'r') as f:
-            return int(f.read().splitlines()[0])
-    return 0
-
-
-def save_last_pos(pos):
-    path = args.work_dir / 'pos.txt'
-    with open(path, 'w') as out:
-        out.write(str(pos) + '\n')
-
-
-def rm_file(file_path):
-    if file_path.exists():
-        os.remove(file_path)
-
-
-def remove_artifacts():
-    rm_file(svg_copy_path)
-    rm_file(svg_path_usvg)
-    rm_file(png_path_orig)
-    rm_file(png_path_usvg)
-    rm_file(diff_path)
-
-
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser()
-    parser.add_argument('--ci-mode', action='store_true', help='Enables the CI mode')
-    parser.add_argument('--dpi', type=int, default=96, help='Sets the DPI')
-    parser.add_argument('svg_dir', type=Path, help='Sets an input directory with SVG files')
-    parser.add_argument('work_dir', type=Path, help='Sets the working directory')
-    args = parser.parse_args()
-
-    if not args.work_dir.exists():
-        os.mkdir(args.work_dir)
-
-    files = os.listdir(args.svg_dir)
-    files = fnmatch.filter(files, '*.svg')
-    files = sorted(files)
-
-    allowed_ae = {}
-    with open('allow.csv') as f:
-        for row in csv.reader(f):
-            allowed_ae[row[0]] = int(row[1])
-
-    cache = {}
-    if os.path.exists(CACHE_FILENAME):
-        with open(CACHE_FILENAME) as f:
-            for row in csv.reader(f):
-                cache[row[0]] = row[1]
-
-    start_idx = load_last_pos()
-    last_idx = 0  # will be set only on error
-    for idx, file in enumerate(files):
-        svg_path = args.svg_dir / file
-
-        # The output file must have the same name as an original.
-        # Otherwise tests like e-image-034.svg will break.
-        svg_path_usvg = args.work_dir / file
-
-        svg_copy_path = args.work_dir / change_ext(file, '_orig', 'svg')
-        png_path_orig = args.work_dir / change_ext(file, '_orig', 'png')
-        png_path_usvg = args.work_dir / change_ext(file, '_usvg', 'png')
-        diff_path = args.work_dir / change_ext(file, '_diff', 'png')
-
-        remove_artifacts()
-
-        if idx < start_idx:
-            continue
-
-        print('Test {} of {}: {}'.format(idx + 1, len(files), file))
-
-        if file in CRASH_ALLOWED:
-            continue
-
-        try:
-            run(['../../target/debug/usvg', svg_path, svg_path_usvg, '--dpi', str(args.dpi)],
-                check=True)
-        except proc.CalledProcessError as e:
-            print('Error: usvg crashed.')
-            last_idx = idx
-            break
-
-        with open(svg_path_usvg, 'rb') as f:
-            md5 = hashlib.md5()
-            md5.update(f.read())
-            md5hash = md5.hexdigest()
-            md5hash = md5hash[:8]  # 8 values is enough for us
-
-        # If the md5 hash of the simplified SVG was not changed
-        # that there is no need to render and compare raster images
-        # because it's very expensive.
-        if md5hash == cache.get(svg_path.stem, ''):
-            rm_file(svg_path_usvg)
-            continue
-        elif args.ci_mode:
-            # md5 check can't fail in the CI mode.
-            print('Error: md5 hash mismatch: {} != {}.'
-                  .format(md5hash, cache.get(svg_path.stem, '')))
-
-            # Print the simplified file content.
-            with open(svg_path_usvg) as f:
-                print(f.read())
-
-            last_idx = idx
-            break
-
-        render_svg(svg_path, png_path_orig)
-        render_svg(svg_path_usvg, png_path_usvg)
-
-        try:
-            run(['compare', '-metric', 'AE', '-fuzz', '1%',
-                 png_path_orig, png_path_usvg, diff_path],
-                check=True, stdout=proc.PIPE, stderr=proc.STDOUT)
-        except proc.CalledProcessError as e:
-            ae = int(e.stdout.decode('ascii'))
-            if ae > 20 and ae != allowed_ae.get(svg_path.stem, 0):
-                print('Error: images are different by {} pixels.'.format(ae))
-
-                # copy original svg on error
-                copyfile(svg_path, svg_copy_path)
-
-                last_idx = idx
-                break
-
-        # Update md5 hash on OK.
-        cache[svg_path.stem] = md5hash
-
-        remove_artifacts()
-
-    save_last_pos(last_idx)
-
-    # Update cache file.
-    with open(CACHE_FILENAME, 'w') as f:
-        writer = csv.writer(f)
-        for key, value in cache.items():
-            writer.writerow([key, value])
-
-    if last_idx != 0:
-        exit(1)
diff --git a/usvg/tests/test.rs b/usvg/tests/test.rs
index 9a1fab231..b87708c61 100644
--- a/usvg/tests/test.rs
+++ b/usvg/tests/test.rs
@@ -215,7 +215,7 @@ test!(pattern_without_children, false,
 // All supported elements should be listed.
 // We keep id's even if `keep_named_groups` is disabled.
 // ID on `svg`, `defs`, `stop` and `tspan` is ignored because they can't be rendered
-test!(preserve_id, false,
+test!(preserve_id, true,
 "<svg id='svg1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 1 1'>
     <defs id='defs1'>
         <linearGradient id='lg1'>
@@ -235,8 +235,8 @@ test!(preserve_id, false,
     </defs>
     <rect id='rect1' fill='url(#lg1)' stroke='url(#rg1)' clip-path='url(#clip1)' width='10' height='10'/>
     <path id='path1' fill='url(#patt1)' d='M 10 20 30 40'/>
-    <text id='text1'>Some text</text>
-    <text id='text2'><tspan id='tspan2'>Some text</tspan></text>
+    <text id='text1'>_</text>
+    <text id='text2' text-decoration='underline'>_</text>
     <image id='image1' width='1' height='1' xlink:href='data:image/png;base64,
         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAB3RJTUUH4gMLDwAjrsLbtwAAAAlw
         SFlzAAAuIwAALiMBeKU/dgAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAG
@@ -307,14 +307,16 @@ test!(preserve_id, false,
         id='path1'
         fill='url(#patt1)'
         d='M 10 20 L 30 40'/>
-    <text
-        id='text1'><tspan><tspan
-        font-family='Times New Roman'
-        font-size='12'>Some text</tspan></tspan></text>
-    <text
-        id='text2'><tspan><tspan
-        font-family='Times New Roman'
-        font-size='12'>Some text</tspan></tspan></text>
+    <path
+        id='text1'
+        d='M 6.10546875 2.58984375 L -0.099609375 2.58984375 L -0.099609375 2.09765625 L 6.10546875 2.09765625 Z'/>
+    <g
+        id='text2'>
+        <path
+            d='M 0 1.013671875 L 6 1.013671875 L 6 1.599609375 L 0 1.599609375 Z'/>
+        <path
+            d='M 6.10546875 2.58984375 L -0.099609375 2.58984375 L -0.099609375 2.09765625 L 6.10546875 2.09765625 Z'/>
+    </g>
     <image
         id='image1'
         x='0'
@@ -496,7 +498,7 @@ test!(switch_with_opacity, false,
 // `fill-rule` cannot be set on `text`.
 test!(fill_rule_on_text, false,
 "<svg viewBox='0 0 1 1' xmlns='http://www.w3.org/2000/svg'>
-    <text fill-rule='evenodd'>Text</text>
+    <text fill-rule='evenodd'>_</text>
 </svg>",
 "<svg
     xmlns='http://www.w3.org/2000/svg'
@@ -506,9 +508,8 @@ test!(fill_rule_on_text, false,
     xmlns:usvg='https://github.com/RazrFalcon/usvg'
     usvg:version='0.6.1'>
     <defs/>
-    <text><tspan><tspan
-        font-family='Times New Roman'
-        font-size='12'>Text</tspan></tspan></text>
+    <path
+        d='M 6.10546875 2.58984375 L -0.099609375 2.58984375 L -0.099609375 2.09765625 L 6.10546875 2.09765625 Z'/>
 </svg>
 ");