mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-01-12 23:36:29 +01:00
feat: reimplement desktop collection w/ better icon collection, file-cache png creations
This commit is contained in:
parent
2894bb257d
commit
0a6a741f58
5 changed files with 513 additions and 16 deletions
19
plugin-desktop2/Cargo.toml
Normal file
19
plugin-desktop2/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "desktop2"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
freedesktop-desktop-entry = "0.5.0"
|
||||
freedesktop-icons = "0.2.3"
|
||||
log = "0.4.19"
|
||||
once_cell = "1.18.0"
|
||||
regex = "1.9.1"
|
||||
rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
|
||||
rust-ini = "0.19.0"
|
||||
serde_json = "1.0.104"
|
||||
shellexpand = "3.1.0"
|
||||
thiserror = "1.0.44"
|
||||
walkdir = "2.3.3"
|
306
plugin-desktop2/src/icons.rs
Normal file
306
plugin-desktop2/src/icons.rs
Normal file
|
@ -0,0 +1,306 @@
|
|||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs::{read_dir, read_to_string};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use freedesktop_desktop_entry::DesktopEntry;
|
||||
use ini::Ini;
|
||||
use once_cell::sync::Lazy;
|
||||
use thiserror::Error;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
type ThemeSource<'a> = (&'a str, &'a str, &'a str);
|
||||
|
||||
static INDEX_MAIN: &'static str = "Icon Theme";
|
||||
static INDEX_NAME: &'static str = "Name";
|
||||
static INDEX_SIZE: &'static str = "Size";
|
||||
static INDEX_DIRS: &'static str = "Directories";
|
||||
static INDEX_FILE: &'static str = "index.theme";
|
||||
|
||||
static DEFAULT_INDEX: &'static str = "default/index.theme";
|
||||
static DEFAULT_THEME: &'static str = "Hicolor";
|
||||
|
||||
static PIXMAPS: Lazy<PathBuf> = Lazy::new(|| PathBuf::from("/usr/share/pixmaps/"));
|
||||
static THEME_SOURCES: Lazy<Vec<ThemeSource>> = Lazy::new(|| {
|
||||
vec![
|
||||
("kdeglobals", "Icons", "Theme"),
|
||||
("gtk-4.0/settings.ini", "Settings", "gtk-icon-theme-name"),
|
||||
("gtk-3.0/settings.ini", "Settings", "gtk-icon-theme-name"),
|
||||
]
|
||||
});
|
||||
|
||||
/// Title String
|
||||
#[inline]
|
||||
fn title(s: &str) -> String {
|
||||
let mut c = s.chars();
|
||||
match c.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect Theme Definitions in Common GUI Configurations
|
||||
fn theme_inis(cfgdir: &PathBuf) -> Vec<String> {
|
||||
THEME_SOURCES
|
||||
.iter()
|
||||
.filter_map(|(path, sec, key)| {
|
||||
let path = cfgdir.join(path);
|
||||
let ini = Ini::load_from_file(path).ok()?;
|
||||
ini.get_from(Some(sec.to_owned()), key).map(|s| title(s))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse FreeDesktop Theme-Name from Index File
|
||||
fn get_theme_name(path: &PathBuf) -> Option<String> {
|
||||
let content = read_to_string(path).ok()?;
|
||||
let config = DesktopEntry::decode(&path, &content).ok()?;
|
||||
config
|
||||
.groups
|
||||
.get(INDEX_MAIN)
|
||||
.and_then(|g| g.get(INDEX_NAME))
|
||||
.map(|key| key.0.to_owned())
|
||||
}
|
||||
|
||||
/// Determine XDG Icon Theme based on Preexisting Configuration Files
|
||||
pub fn active_themes(cfgdir: &PathBuf, icondirs: &Vec<PathBuf>) -> Vec<String> {
|
||||
let mut themes: Vec<String> = icondirs
|
||||
.iter()
|
||||
.map(|d| d.join(DEFAULT_INDEX))
|
||||
.filter(|p| p.exists())
|
||||
.filter_map(|p| get_theme_name(&p))
|
||||
.collect();
|
||||
themes.extend(theme_inis(cfgdir));
|
||||
let default = DEFAULT_THEME.to_string();
|
||||
if !themes.contains(&default) {
|
||||
themes.push(default);
|
||||
}
|
||||
themes
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ThemeError {
|
||||
#[error("Failed to Read Index")]
|
||||
FileError(#[from] std::io::Error),
|
||||
#[error("Failed to Parse Index")]
|
||||
IndexError(#[from] freedesktop_desktop_entry::DecodeError),
|
||||
#[error("No Such Group")]
|
||||
NoSuchGroup(&'static str),
|
||||
#[error("No Such Key")]
|
||||
NoSuchKey(&'static str),
|
||||
#[error("Unselected Theme")]
|
||||
UnselectedTheme,
|
||||
#[error("Invalid Path Name")]
|
||||
BadPathName(PathBuf),
|
||||
}
|
||||
|
||||
/// Track Paths and their Priority according to Sizes preference
|
||||
struct PathPriority {
|
||||
path: PathBuf,
|
||||
priority: usize,
|
||||
}
|
||||
|
||||
impl PathPriority {
|
||||
fn new(path: PathBuf, priority: usize) -> Self {
|
||||
Self { path, priority }
|
||||
}
|
||||
}
|
||||
|
||||
/// Track Theme Information w/ Name/Priority/SubPaths
|
||||
struct ThemeInfo {
|
||||
name: String,
|
||||
priority: usize,
|
||||
paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Single Theme Specification
|
||||
struct ThemeSpec<'a> {
|
||||
root: &'a PathBuf,
|
||||
themes: &'a Vec<String>,
|
||||
sizes: &'a Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> ThemeSpec<'a> {
|
||||
fn new(root: &'a PathBuf, themes: &'a Vec<String>, sizes: &'a Vec<String>) -> Self {
|
||||
Self {
|
||||
root,
|
||||
themes,
|
||||
sizes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort Theme Directories by Priority, Append Root, and Collect Names Only
|
||||
#[inline]
|
||||
fn sort_dirs(base: &PathBuf, dirs: &mut Vec<PathPriority>) -> Vec<PathBuf> {
|
||||
dirs.sort_by_key(|p| p.priority);
|
||||
dirs.push(PathPriority::new("".into(), 0));
|
||||
dirs.into_iter().map(|p| p.path.to_owned()).collect()
|
||||
}
|
||||
|
||||
/// Parse Theme Index and Sort Directories based on Size Preference
|
||||
fn parse_index(spec: &ThemeSpec) -> Result<ThemeInfo, ThemeError> {
|
||||
// parse file content
|
||||
let index = spec.root.join(INDEX_FILE);
|
||||
let content = read_to_string(&index)?;
|
||||
let config = DesktopEntry::decode(&index, &content)?;
|
||||
let main = config
|
||||
.groups
|
||||
.get(INDEX_MAIN)
|
||||
.ok_or_else(|| ThemeError::NoSuchGroup(INDEX_MAIN))?;
|
||||
// retrieve name and directories
|
||||
let name = main
|
||||
.get(INDEX_NAME)
|
||||
.ok_or_else(|| ThemeError::NoSuchKey(INDEX_NAME))?
|
||||
.0;
|
||||
// check if name in supported themes
|
||||
let index = spec
|
||||
.themes
|
||||
.iter()
|
||||
.position(|t| t == &name)
|
||||
.ok_or_else(|| ThemeError::UnselectedTheme)?;
|
||||
// sort directories based on size preference
|
||||
let mut directories = main
|
||||
.get(INDEX_DIRS)
|
||||
.ok_or_else(|| ThemeError::NoSuchKey(INDEX_DIRS))?
|
||||
.0
|
||||
.split(',')
|
||||
.into_iter()
|
||||
.filter_map(|dir| {
|
||||
let group = config.groups.get(dir)?;
|
||||
let size = group
|
||||
.get(INDEX_SIZE)
|
||||
.and_then(|e| Some(e.0.to_owned()))
|
||||
.and_then(|s| spec.sizes.iter().position(|is| &s == is));
|
||||
Some(match size {
|
||||
Some(num) => PathPriority::new(spec.root.join(dir), num),
|
||||
None => PathPriority::new(spec.root.join(dir), 99),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(ThemeInfo {
|
||||
priority: index,
|
||||
name: name.to_owned(),
|
||||
paths: sort_dirs(spec.root, &mut directories),
|
||||
})
|
||||
}
|
||||
|
||||
/// Guess Theme when Index is Missing
|
||||
fn guess_index(spec: &ThemeSpec) -> Result<ThemeInfo, ThemeError> {
|
||||
// parse name and confirm active theme
|
||||
let name = title(
|
||||
spec.root
|
||||
.file_name()
|
||||
.ok_or_else(|| ThemeError::BadPathName(spec.root.to_owned()))?
|
||||
.to_str()
|
||||
.ok_or_else(|| ThemeError::BadPathName(spec.root.to_owned()))?,
|
||||
);
|
||||
let index = spec
|
||||
.themes
|
||||
.iter()
|
||||
.position(|t| t == &name)
|
||||
.ok_or_else(|| ThemeError::UnselectedTheme)?;
|
||||
// retrieve directories and include priority
|
||||
let mut directories: Vec<PathPriority> = read_dir(spec.root)?
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name().to_str().map(|n| n.to_owned())?;
|
||||
Some(match name.split_once("x") {
|
||||
Some((size, _)) => {
|
||||
let index = spec.sizes.iter().position(|is| &size == is);
|
||||
PathPriority::new(e.path(), index.unwrap_or(99))
|
||||
}
|
||||
None => PathPriority::new(e.path(), 99),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// sort by priorty and only include matches
|
||||
Ok(ThemeInfo {
|
||||
name,
|
||||
priority: index,
|
||||
paths: sort_dirs(spec.root, &mut directories),
|
||||
})
|
||||
}
|
||||
|
||||
/// Specification for a Single Theme Path
|
||||
pub struct IconSpec {
|
||||
paths: Vec<PathBuf>,
|
||||
themes: Vec<String>,
|
||||
sizes: Vec<String>,
|
||||
}
|
||||
|
||||
impl IconSpec {
|
||||
pub fn new(paths: Vec<PathBuf>, themes: Vec<String>, sizes: Vec<usize>) -> Self {
|
||||
Self {
|
||||
paths,
|
||||
themes,
|
||||
sizes: sizes.into_iter().map(|i| i.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn standard(cfg: &PathBuf, sizes: Vec<usize>) -> Self {
|
||||
let mut icon_paths = crate::data_dirs("icons");
|
||||
let themes = active_themes(cfg, &icon_paths);
|
||||
Self::new(icon_paths, themes, sizes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and Collect a list of Directories to Find Icons in Order of Preference
|
||||
fn parse_themes(icons: IconSpec) -> Vec<PathBuf> {
|
||||
// retrieve supported theme information
|
||||
let mut infos: Vec<ThemeInfo> = icons
|
||||
.paths
|
||||
// retrieve icon directories within main icon data paths
|
||||
.into_iter()
|
||||
.filter_map(|p| Some(read_dir(&p).ok()?.into_iter().filter_map(|d| d.ok())))
|
||||
.flatten()
|
||||
.map(|readdir| readdir.path())
|
||||
// parse or guess index themes
|
||||
.filter_map(|icondir| {
|
||||
let spec = ThemeSpec::new(&icondir, &icons.themes, &icons.sizes);
|
||||
parse_index(&spec)
|
||||
.map(|r| Ok(r))
|
||||
.unwrap_or_else(|_| guess_index(&spec))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
// sort results by theme index
|
||||
infos.sort_by_key(|i| i.priority);
|
||||
// combine results from multiple directories for the same theme
|
||||
let mut map = BTreeMap::new();
|
||||
for info in infos.into_iter() {
|
||||
map.entry(info.name).or_insert(vec![]).extend(info.paths);
|
||||
}
|
||||
// finalize results from values
|
||||
map.insert("pixmaps".to_owned(), vec![PIXMAPS.to_owned()]);
|
||||
map.into_values().flatten().collect()
|
||||
}
|
||||
|
||||
pub type IconMap = HashMap<String, PathBuf>;
|
||||
|
||||
#[inline]
|
||||
fn is_icon(fname: &str) -> bool {
|
||||
fname.ends_with("png") || fname.ends_with("svg") || fname.ends_with("xpm")
|
||||
}
|
||||
|
||||
/// Collect Unique Icon Map based on Preffered Paths
|
||||
pub fn collect_icons(spec: IconSpec) -> IconMap {
|
||||
let mut map = HashMap::new();
|
||||
for path in parse_themes(spec).into_iter() {
|
||||
let icons = WalkDir::new(path)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_file());
|
||||
for icon in icons {
|
||||
let Some(fname) = icon.file_name().to_str() else { continue };
|
||||
if !is_icon(&fname) {
|
||||
continue;
|
||||
}
|
||||
let Some((name, _)) = fname.rsplit_once(".") else { continue };
|
||||
map.entry(name.to_owned())
|
||||
.or_insert_with(|| icon.path().to_owned());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
142
plugin-desktop2/src/main.rs
Normal file
142
plugin-desktop2/src/main.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use std::fs::read_to_string;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use freedesktop_desktop_entry::{DesktopEntry, Iter};
|
||||
use freedesktop_icons::lookup;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rmenu_plugin::{Action, Entry, Method};
|
||||
|
||||
mod icons;
|
||||
|
||||
static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS";
|
||||
static XDG_CONFIG_ENV: &'static str = "XDG_CONFIG_HOME";
|
||||
static XDG_DATA_DEFAULT: &'static str = "/usr/share:/usr/local/share";
|
||||
static XDG_CONFIG_DEFAULT: &'static str = "~/.config";
|
||||
|
||||
static EXEC_RGX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"%\w").expect("Failed to Build Exec Regex"));
|
||||
|
||||
/// Retrieve XDG-CONFIG-HOME Directory
|
||||
#[inline]
|
||||
fn config_dir() -> PathBuf {
|
||||
let path = std::env::var(XDG_CONFIG_ENV).unwrap_or_else(|_| XDG_CONFIG_DEFAULT.to_string());
|
||||
PathBuf::from(shellexpand::tilde(&path).to_string())
|
||||
}
|
||||
|
||||
/// Retrieve XDG-DATA Directories
|
||||
fn data_dirs(dir: &str) -> Vec<PathBuf> {
|
||||
std::env::var(XDG_DATA_ENV)
|
||||
.unwrap_or_else(|_| XDG_DATA_DEFAULT.to_string())
|
||||
.split(":")
|
||||
.map(|p| shellexpand::tilde(p).to_string())
|
||||
.map(PathBuf::from)
|
||||
.map(|p| p.join(dir.to_owned()))
|
||||
.filter(|p| p.exists())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find Freedesktop Default Theme
|
||||
fn default_theme() -> String {
|
||||
data_dirs("icons")
|
||||
.into_iter()
|
||||
.map(|p| p.join("default/index.theme"))
|
||||
.filter(|p| p.exists())
|
||||
.find_map(|p| {
|
||||
let content = read_to_string(&p).ok()?;
|
||||
let config = DesktopEntry::decode(&p, &content).ok()?;
|
||||
config
|
||||
.groups
|
||||
.get("Icon Theme")
|
||||
.and_then(|g| g.get("Name"))
|
||||
.map(|key| key.0.to_owned())
|
||||
})
|
||||
.unwrap_or_else(|| "Hicolor".to_string())
|
||||
}
|
||||
|
||||
/// Modify Exec Statements to Remove %u/%f/etc...
|
||||
#[inline(always)]
|
||||
fn fix_exec(exec: &str) -> String {
|
||||
EXEC_RGX.replace_all(exec, "").trim().to_string()
|
||||
}
|
||||
|
||||
/// Parse XDG Desktop Entry into RMenu Entry
|
||||
fn parse_desktop(path: &PathBuf, locale: Option<&str>) -> Option<Entry> {
|
||||
let bytes = read_to_string(path).ok()?;
|
||||
let entry = DesktopEntry::decode(&path, &bytes).ok()?;
|
||||
let name = entry.name(locale)?.to_string();
|
||||
let icon = entry.icon().map(|i| i.to_string());
|
||||
let comment = entry.comment(locale).map(|s| s.to_string());
|
||||
let terminal = entry.terminal();
|
||||
let mut actions = match entry.exec() {
|
||||
Some(exec) => vec![Action {
|
||||
name: "main".to_string(),
|
||||
exec: Method::new(fix_exec(exec), terminal),
|
||||
comment: None,
|
||||
}],
|
||||
None => vec![],
|
||||
};
|
||||
actions.extend(
|
||||
entry
|
||||
.actions()
|
||||
.unwrap_or("")
|
||||
.split(";")
|
||||
.into_iter()
|
||||
.filter(|a| a.len() > 0)
|
||||
.filter_map(|a| {
|
||||
let name = entry.action_name(a, locale)?;
|
||||
let exec = entry.action_exec(a)?;
|
||||
Some(Action {
|
||||
name: name.to_string(),
|
||||
exec: Method::new(fix_exec(exec), terminal),
|
||||
comment: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
Some(Entry {
|
||||
name,
|
||||
actions,
|
||||
comment,
|
||||
icon,
|
||||
})
|
||||
}
|
||||
|
||||
/// Assign XDG Icon based on Desktop-Entry
|
||||
fn assign_icon(icon: String, map: &icons::IconMap) -> Option<String> {
|
||||
if !icon.contains("/") {
|
||||
if let Some(icon) = map.get(&icon) {
|
||||
if let Some(path) = icon.to_str() {
|
||||
return Some(path.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(icon)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let locale = Some("en");
|
||||
let sizes = vec![64, 32, 96, 22, 128];
|
||||
|
||||
// collect icons
|
||||
let cfg = config_dir();
|
||||
let spec = icons::IconSpec::standard(&cfg, sizes);
|
||||
let icons = icons::collect_icons(spec);
|
||||
|
||||
// collect applications
|
||||
let app_paths = data_dirs("applications");
|
||||
let mut desktops: Vec<Entry> = Iter::new(app_paths)
|
||||
.into_iter()
|
||||
.filter_map(|f| parse_desktop(&f, locale))
|
||||
.map(|mut e| {
|
||||
e.icon = e.icon.and_then(|s| assign_icon(s, &icons));
|
||||
e
|
||||
})
|
||||
.collect();
|
||||
|
||||
desktops.sort_by_cached_key(|e| e.name.to_owned());
|
||||
desktops
|
||||
.into_iter()
|
||||
.filter_map(|e| serde_json::to_string(&e).ok())
|
||||
.map(|s| println!("{}", s))
|
||||
.last();
|
||||
}
|
|
@ -6,7 +6,6 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21.2"
|
||||
cached = "0.44.0"
|
||||
clap = { version = "4.3.15", features = ["derive"] }
|
||||
dioxus = "0.3.2"
|
||||
|
@ -15,6 +14,7 @@ env_logger = "0.10.0"
|
|||
heck = "0.4.1"
|
||||
keyboard-types = "0.6.2"
|
||||
log = "0.4.19"
|
||||
once_cell = "1.18.0"
|
||||
png = "0.17.9"
|
||||
quick-xml = "0.30.0"
|
||||
regex = { version = "1.9.1" }
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
//! GUI Image Processing
|
||||
use std::fs::read_to_string;
|
||||
use std::fs::{create_dir_all, read_to_string, write};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use cached::proc_macro::cached;
|
||||
use once_cell::sync::Lazy;
|
||||
use resvg::usvg::TreeParsing;
|
||||
use thiserror::Error;
|
||||
|
||||
static TEMP_EXISTS: Lazy<Mutex<Vec<bool>>> = Lazy::new(|| Mutex::new(vec![]));
|
||||
static TEMP_DIR: Lazy<PathBuf> = Lazy::new(|| PathBuf::from("/tmp/rmenu"));
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum SvgError {
|
||||
#[error("Invalid SVG Filepath")]
|
||||
|
@ -13,12 +19,23 @@ enum SvgError {
|
|||
#[error("Invalid Document")]
|
||||
InvalidTree(#[from] resvg::usvg::Error),
|
||||
#[error("Failed to Alloc PixBuf")]
|
||||
NoPixBuf,
|
||||
NoPixBuf(u32, u32, u32),
|
||||
#[error("Failed to Convert SVG to PNG")]
|
||||
PngError(#[from] png::EncodingError),
|
||||
}
|
||||
|
||||
fn svg_to_png(path: &str, pixels: u32) -> Result<String, SvgError> {
|
||||
/// Make Temporary Directory for Generated PNGs
|
||||
fn make_temp() -> Result<(), io::Error> {
|
||||
let mut temp = TEMP_EXISTS.lock().expect("Failed to Access Global Mutex");
|
||||
if temp.len() == 0 {
|
||||
create_dir_all(TEMP_DIR.to_owned())?;
|
||||
temp.push(true);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert SVG to PNG Image
|
||||
fn svg_to_png(path: &str, dest: &PathBuf, pixels: u32) -> Result<(), SvgError> {
|
||||
// read and convert to resvg document tree
|
||||
let xml = read_to_string(path)?;
|
||||
let opt = resvg::usvg::Options::default();
|
||||
|
@ -26,22 +43,35 @@ fn svg_to_png(path: &str, pixels: u32) -> Result<String, SvgError> {
|
|||
let rtree = resvg::Tree::from_usvg(&tree);
|
||||
// generate pixel-buffer and scale according to size preference
|
||||
let size = rtree.size.to_int_size();
|
||||
let scale = pixels / size.width();
|
||||
let width = size.width() * scale;
|
||||
let height = size.height() * scale;
|
||||
let fscale = scale as f32;
|
||||
let mut pixmap =
|
||||
resvg::tiny_skia::Pixmap::new(width, height).ok_or_else(|| SvgError::NoPixBuf)?;
|
||||
let form = resvg::tiny_skia::Transform::from_scale(fscale, fscale);
|
||||
let scale = pixels as f32 / size.width() as f32;
|
||||
let width = (size.width() as f32 * scale) as u32;
|
||||
let height = (size.height() as f32 * scale) as u32;
|
||||
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height)
|
||||
.ok_or_else(|| SvgError::NoPixBuf(width, height, pixels))?;
|
||||
let form = resvg::tiny_skia::Transform::from_scale(scale, scale);
|
||||
// render as png to memory
|
||||
rtree.render(form, &mut pixmap.as_mut());
|
||||
let mut png = pixmap.encode_png()?;
|
||||
let png = pixmap.encode_png()?;
|
||||
// base64 encode png
|
||||
let encoded = general_purpose::STANDARD.encode(&mut png);
|
||||
Ok(format!("data:image/png;base64, {encoded}"))
|
||||
Ok(write(dest, png)?)
|
||||
}
|
||||
|
||||
#[cached]
|
||||
pub fn convert_svg(path: String) -> Option<String> {
|
||||
svg_to_png(&path, 64).ok()
|
||||
// ensure temporary directory exists
|
||||
let _ = make_temp();
|
||||
// convert path to new temporary png filepath
|
||||
let (_, fname) = path.rsplit_once('/')?;
|
||||
let (name, _) = fname.rsplit_once(".")?;
|
||||
let name = format!("{name}.png");
|
||||
let new_path = TEMP_DIR.join(name);
|
||||
// generate png if it doesnt already exist
|
||||
if !new_path.exists() {
|
||||
log::debug!("generating png {new_path:?}");
|
||||
match svg_to_png(&path, &new_path, 64) {
|
||||
Err(err) => log::error!("failed svg->png: {err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(new_path.to_str()?.to_string())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue