summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenys Séguret <cano.petrole@gmail.com>2023-02-02 17:31:13 +0100
committerGitHub <noreply@github.com>2023-02-02 17:31:13 +0100
commit4b279733b65784dd73d991bf837bf0f35f32ccd6 (patch)
treebc1e6181cd14b271fb137481628bedc3b1d3abaa
parent1cb4d37f7aed946db98e278626df811c3a27ad52 (diff)
Render SVG files as images in preview (#660)
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock279
-rw-r--r--Cargo.toml4
-rw-r--r--src/errors.rs6
-rw-r--r--src/image/image_view.rs3
-rw-r--r--src/image/mod.rs26
-rw-r--r--src/image/svg.rs71
7 files changed, 383 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 642fc97..d32d0ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
### next
- unless overriden, `/proc` is now no-enter - See #639
+- SVG files now rendered as images in the preview panel
- preview can now be opened on directories, showing their first level - Fix #405
- new version of the nushell function. You should be prompted for an update
- "no-hide" special paths - Thanks @Avlllo
diff --git a/Cargo.lock b/Cargo.lock
index c66eed3..478dbbd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -89,6 +89,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "149f75bbec1827618262e0855a68f0f9a7f2edc13faebf33c4f16d6725edb6a9"
[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+
+[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -175,6 +187,7 @@ dependencies = [
"phf",
"rayon",
"regex",
+ "resvg",
"secular",
"serde",
"smallvec",
@@ -185,10 +198,13 @@ dependencies = [
"termimad",
"terminal-clipboard",
"terminal-light",
+ "tiny-skia",
"toml",
"umask",
"unicode-width",
"users",
+ "usvg",
+ "usvg-text-layout",
"xterm-query 0.1.0",
]
@@ -585,6 +601,12 @@ dependencies = [
]
[[package]]
+name = "data-url"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
+
+[[package]]
name = "deser-hjson"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -666,7 +688,7 @@ dependencies = [
"flume",
"half",
"lebe",
- "miniz_oxide",
+ "miniz_oxide 0.6.2",
"smallvec",
"threadpool",
]
@@ -705,10 +727,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
"crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.6.2",
]
[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+
+[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -728,6 +756,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
+name = "fontconfig-parser"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2be17a530a842f8a7a60f4397a08e8f08872849a5e31b20c7bd7301dac483296"
+dependencies = [
+ "roxmltree",
+]
+
+[[package]]
+name = "fontdb"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8131752b3f3b876a20f42b3d08233ad177d6e7ec6d18aaa6954489a201071be5"
+dependencies = [
+ "fontconfig-parser",
+ "log",
+ "memmap2",
+ "ttf-parser",
+]
+
+[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -928,6 +977,12 @@ dependencies = [
]
[[package]]
+name = "imagesize"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df19da1e92fbfec043ca97d622955381b1f3ee72a180ec999912df31b1ccd951"
+
+[[package]]
name = "include_dir"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1014,6 +1069,15 @@ dependencies = [
]
[[package]]
+name = "kurbo"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
name = "lazy-regex"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1187,6 +1251,15 @@ dependencies = [
[[package]]
name = "miniz_oxide"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
@@ -1402,6 +1475,12 @@ dependencies = [
]
[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
+[[package]]
name = "pin-project"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1443,14 +1522,14 @@ dependencies = [
[[package]]
name = "png"
-version = "0.17.7"
+version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c"
dependencies = [
"bitflags",
"crc32fast",
"flate2",
- "miniz_oxide",
+ "miniz_oxide 0.5.4",
]
[[package]]
@@ -1578,6 +1657,12 @@ dependencies = [
]
[[package]]
+name = "rctree"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
+
+[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1630,6 +1715,25 @@ dependencies = [
]
[[package]]
+name = "resvg"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c115863f2d3621999cf187e318bc92b16402dfeff6a48c74df700d77381394c1"
+dependencies = [
+ "gif",
+ "jpeg-decoder",
+ "log",
+ "pico-args",
+ "png",
+ "rgb",
+ "svgfilters",
+ "svgtypes",
+ "tiny-skia",
+ "usvg",
+ "usvg-text-layout",
+]
+
+[[package]]
name = "rgb"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1639,6 +1743,15 @@ dependencies = [
]
[[package]]
+name = "roxmltree"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b9de9831a129b122e7e61f242db509fa9d0838008bf0b29bb0624669edfe48a"
+dependencies = [
+ "xmlparser",
+]
+
+[[package]]
name = "rusqlite"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1654,6 +1767,22 @@ dependencies = [
]
[[package]]
+name = "rustybuzz"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab9e34ecf6900625412355a61bda0bd68099fe674de707c67e5e4aed2c05e489"
+dependencies = [
+ "bitflags",
+ "bytemuck",
+ "smallvec",
+ "ttf-parser",
+ "unicode-bidi-mirroring",
+ "unicode-ccc",
+ "unicode-general-category",
+ "unicode-script",
+]
+
+[[package]]
name = "ryu"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1763,6 +1892,15 @@ dependencies = [
]
[[package]]
+name = "simplecss"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
+dependencies = [
+ "log",
+]
+
+[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1824,6 +1962,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "991af58f8bd0512b0c76abc87f8f6a8a492c314ebcd13189b426c00c95f6f0ee"
[[package]]
+name = "strict-num"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df65f20698aeed245efdde3628a6b559ea1239bbb871af1b6e3b58c413b2bd1"
+dependencies = [
+ "float-cmp",
+]
+
+[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1842,6 +1989,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e72d8b19ab05827afefcca66bf47040c1e66a0901eb814784c77d4ec118bd309"
[[package]]
+name = "svgfilters"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "639abcebc15fdc2df179f37d6f5463d660c1c79cd552c12343a4600827a04bce"
+dependencies = [
+ "float-cmp",
+ "rgb",
+]
+
+[[package]]
+name = "svgtypes"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22975e8a2bac6a76bb54f898a6b18764633b00e780330f0b689f65afb3975564"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
name = "syn"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2026,6 +2192,31 @@ dependencies = [
]
[[package]]
+name = "tiny-skia"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae12c22601b6853f4d93abb178e13bf0e1cc8e2454100c85d4d3a59ac71b3f7"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if",
+ "png",
+ "tiny-skia-path",
+]
+
+[[package]]
+name = "tiny-skia-path"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd665853ce64402daabef6edda442dbb4f8ee93ea80957b66ba1af419f11a104"
+dependencies = [
+ "arrayref",
+ "bytemuck",
+ "strict-num",
+]
+
+[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2050,6 +2241,12 @@ dependencies = [
]
[[package]]
+name = "ttf-parser"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375812fa44dab6df41c195cd2f7fecb488f6c09fbaafb62807488cefab642bff"
+
+[[package]]
name = "umask"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2062,6 +2259,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1"
+
+[[package]]
+name = "unicode-general-category"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7"
+
+[[package]]
name = "unicode-ident"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2077,6 +2292,18 @@ dependencies = [
]
[[package]]
+name = "unicode-script"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc"
+
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
+
+[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2104,6 +2331,42 @@ dependencies = [
]
[[package]]
+name = "usvg"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b5b7c2b30845b3348c067ca3d09e20cc6e327c288f0ca4c48698712abf432e9"
+dependencies = [
+ "base64",
+ "data-url",
+ "flate2",
+ "imagesize",
+ "kurbo",
+ "log",
+ "rctree",
+ "roxmltree",
+ "simplecss",
+ "siphasher",
+ "strict-num",
+ "svgtypes",
+]
+
+[[package]]
+name = "usvg-text-layout"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c9550670848028641bf976b06f5c520ffdcd6f00ee7ee7eb0853f78e2c249d7"
+dependencies = [
+ "fontdb",
+ "kurbo",
+ "log",
+ "rustybuzz",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "usvg",
+]
+
+[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2313,6 +2576,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
+name = "xmlparser"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
+
+[[package]]
name = "xterm-query"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index d640652..02cfc2c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -50,6 +50,7 @@ pathdiff = "0.2"
phf = { version = "0.10", features = ["macros"] }
rayon = "1.5"
regex = "1.5"
+resvg = "0.28.0"
secular = { version = "1.0", features = ["normalization"] }
serde = { version = "1.0", features = ["derive"] }
smallvec = "1.9"
@@ -60,9 +61,12 @@ tempfile = "3.2"
termimad = "0.20.6"
terminal-clipboard = { version = "0.3.1", optional = true }
terminal-light = "1.1"
+tiny-skia = "0.8.2"
toml = "0.5"
umask = "2.0.0"
unicode-width = "0.1.8"
+usvg = "0.28.0"
+usvg-text-layout = { version = "0.28.0", default-features = false }
xterm-query = { version = "0.1", optional = true }
[dev-dependencies]
diff --git a/src/errors.rs b/src/errors.rs
index 5ae94d5..eb475b9 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -21,6 +21,7 @@ custom_error! {pub ProgramError
NetError {source: NetError} = "{source}",
OpenError { source: opener::OpenError } = "Open error: {source}",
ShelInstall { source: ShellInstallError } = "{source}",
+ Svg {source: SvgError} = "SVG error: {source}",
SyntectCrashed { details: String } = "Syntect crashed on {details:?}",
Termimad {source: termimad::Error} = "Termimad Error : {source}",
TreeBuild {source: TreeBuildError} = "{source}",
@@ -115,3 +116,8 @@ custom_error! {pub NetError
InvalidMessage = "invalid message received",
}
+custom_error! {pub SvgError
+ Io {source: io::Error} = "IO Error : {source}",
+ Internal { message: &'static str } = "Internal error : {message}",
+ Svg {source: usvg::Error} = "SVG Error: {source}",
+}
diff --git a/src/image/image_view.rs b/src/image/image_view.rs
index 9a5c793..21e4285 100644
--- a/src/image/image_view.rs
+++ b/src/image/image_view.rs
@@ -16,7 +16,6 @@ use {
QueueableCommand,
},
image::{
- io::Reader,
DynamicImage,
GenericImageView,
imageops::FilterType,
@@ -62,7 +61,7 @@ impl ImageView {
let source_img = time!(
"decode image",
path,
- Reader::open(path)?.decode()?
+ super::load(path)?
);
Ok(Self {
path: path.to_path_buf(),
diff --git a/src/image/mod.rs b/src/image/mod.rs
index caf5c13..7672ec1 100644
--- a/src/image/mod.rs
+++ b/src/image/mod.rs
@@ -1,7 +1,33 @@
mod double_line;
mod image_view;
+mod svg;
pub use {
image_view::ImageView,
};
+
+use {
+ crate::errors::ProgramError,
+ image::{
+ io::Reader,
+ DynamicImage,
+ },
+ std::path::Path,
+};
+
+// Max dimensions of the SVG image to render. A bigger size just makes it need
+// a little more memory and takes more time to render. There's no quality gain
+// in having this bigger than your screen
+pub const MAX_SVG_BITMAP_WIDTH: u32 = 1000;
+pub const MAX_SVG_BITMAP_HEIGHT: u32 = 1000;
+
+pub fn load(path: &Path) -> Result<DynamicImage, ProgramError> {
+ let is_svg = matches!(path.extension(), Some(ext) if ext == "svg" || ext == "SVG");
+ let img = if is_svg {
+ svg::render(path, MAX_SVG_BITMAP_WIDTH, MAX_SVG_BITMAP_HEIGHT)?
+ } else {
+ Reader::open(path)?.decode()?
+ };
+ Ok(img)
+}
diff --git a/src/image/svg.rs b/src/image/svg.rs
new file mode 100644
index 0000000..c2347a0
--- /dev/null
+++ b/src/image/svg.rs
@@ -0,0 +1,71 @@
+use {
+ crate::{
+ errors::SvgError,
+ },
+ image::{
+ DynamicImage,
+ RgbaImage,
+ },
+ std::path::PathBuf,
+ usvg::ScreenSize,
+ usvg_text_layout::{fontdb, TreeTextToPath},
+};
+
+fn compute_zoom(width:u32, height:u32, max_width:u32, max_height:u32) -> Result<f32, SvgError> {
+ let w: f32 = width as f32;
+ let h: f32 = height as f32;
+ let mw: f32 = max_width.max(2) as f32;
+ let mh: f32 = max_height.max(2) as f32;
+ let zoom = 1.0f32
+ .min(mw / w)
+ .min(mh / h);
+ if zoom > 0.0f32 {
+ Ok(zoom)
+ } else {
+ Err(SvgError::Internal { message: "invalid SVG dimensions" })
+ }
+}
+
+/// Generate a bitmap at the natural dimensions of the SVG unless it's too big
+/// in which case a smaller one is generated to fit into (max_width x max_height).
+pub fn render<P: Into<PathBuf>>(
+ path: P,
+ max_width: u32,
+ max_height: u32,
+) -> Result<DynamicImage, SvgError> {
+ let path: PathBuf = path.into();
+ let mut opt = usvg::Options::default();
+ opt.resources_dir = Some(path.clone());
+ let mut fontdb = fontdb::Database::new();
+ fontdb.load_system_fonts();
+ let svg_data = std::fs::read(path)?;
+ let mut tree = usvg::Tree::from_data(&svg_data, &opt)?;
+ debug!("SVG natural size: {} x {}", tree.size.width(), tree.size.height());
+ let px_size = tree.size.to_screen_size();
+ let zoom = compute_zoom(px_size.width(), px_size.height(), max_width, max_height)?;
+ debug!("svg rendering zoom: {zoom}");
+ let Some(px_size) = ScreenSize::new(
+ (px_size.width() as f32 * zoom) as u32,
+ (px_size.height() as f32 * zoom) as u32,
+ ) else {
+ return Err(SvgError::Internal { message: "invalid SVG dimensions" });
+ };
+ debug!("px_size: {px_size:?}");
+ tree.convert_text(&fontdb, opt.keep_named_groups);
+ let mut pixmap = tiny_skia::Pixmap::new(
+ px_size.width(),
+ px_size.height(),
+ ).ok_or(SvgError::Internal { message: "unable to create pixmap buffer" })?;
+ resvg::render(
+ &tree,
+ usvg::FitTo::Zoom(zoom),
+ tiny_skia::Transform::default(),
+ pixmap.as_mut(),
+ ).ok_or(SvgError::Internal { message: "resvg doesn't look happy (not sure)" })?;
+ let image_buffer = RgbaImage::from_vec(
+ pixmap.width(),
+ pixmap.height(),
+ pixmap.take(),
+ ).ok_or(SvgError::Internal { message: "wrong image buffer size" })?;
+ Ok(DynamicImage::ImageRgba8(image_buffer))
+}