favicon: scale the icons before saving them

Fixes #302
This commit is contained in:
Bilal Elmoussaoui 2022-04-18 02:01:50 +02:00
parent b7f39888bf
commit 11ba294816
5 changed files with 139 additions and 68 deletions

View file

@ -19,7 +19,7 @@
<child>
<object class="ProviderImage" id="image">
<property name="halign">start</property>
<property name="size">24</property>
<property name="size">32</property>
</object>
</child>
<child>

View file

@ -111,9 +111,8 @@ pub enum Favicon {
impl fmt::Debug for Favicon {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Data(bytes, metadata) => f
Self::Data(_, metadata) => f
.debug_struct("Favicon")
.field("data", bytes)
.field("type", &metadata.type_())
.field("size", &metadata.size())
.finish(),
@ -153,7 +152,7 @@ impl Favicon {
}
}
pub async fn cache(&self, icon_name: &str) -> anyhow::Result<PathBuf> {
pub async fn cache(&self, icon_name: &str) -> anyhow::Result<()> {
let body = match self {
Self::Data(bytes, _) => bytes.to_owned(),
Self::Url(url, _) => {
@ -161,30 +160,22 @@ impl Favicon {
res.bytes().await?.to_vec()
}
};
let cache_path = FAVICONS_PATH.join(icon_name);
let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
if self.metadata().type_() == &Type::Ico {
log::debug!("Found a .ico favicon, converting to PNG");
let cache_path = FAVICONS_PATH.join(format!("{}.png", icon_name));
let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
if let Ok(ico) = image::load_from_memory_with_format(&body, image::ImageFormat::Ico) {
let mut cursor = std::io::Cursor::new(vec![]);
ico.write_to(&mut cursor, image::ImageOutputFormat::Png)?;
dest.write_all(cursor.get_ref()).await?;
return Ok(());
} else {
log::debug!("It seems to not be a .ICO favicon, fallback to PNG");
dest.write_all(&body).await?;
};
Ok(cache_path)
} else {
let cache_path = FAVICONS_PATH.join(icon_name);
let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
dest.write_all(&body).await?;
Ok(cache_path)
}
dest.write_all(&body).await?;
Ok(())
}
async fn size(&self) -> Option<(u32, u32)> {
@ -267,7 +258,6 @@ impl std::fmt::Display for FaviconError {
}
}
#[derive(Debug)]
pub struct FaviconScrapper(Vec<Favicon>);
impl FaviconScrapper {
@ -315,6 +305,20 @@ impl FaviconScrapper {
self.0.is_empty()
}
pub async fn find_size(&self, size: u32) -> Option<&Favicon> {
let mut best = None;
for favicon in self.0.iter() {
if let Some(current_size) = favicon.size().await {
// Only store the width & assumes it has the same height here to simplify things
if current_size.0 == size {
best = Some(favicon);
break;
}
}
}
best
}
pub async fn find_best(&self) -> Option<&Favicon> {
let mut largest_size = 0;
let mut best = None;
@ -502,6 +506,12 @@ impl FaviconScrapper {
}
}
impl fmt::Debug for FaviconScrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(&self.0).finish()
}
}
impl std::ops::Index<usize> for FaviconScrapper {
type Output = Favicon;

View file

@ -1,13 +1,15 @@
use super::algorithm::{Algorithm, OTPMethod};
use crate::{
models::{database, otp, Account, AccountsModel, FaviconError, FaviconScrapper},
models::{
database, otp, Account, AccountsModel, FaviconError, FaviconScrapper, Type, FAVICONS_PATH,
},
schema::providers,
};
use anyhow::Result;
use core::cmp::Ordering;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use glib::{Cast, StaticType, ToValue};
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
use gtk::{gdk_pixbuf, gio, glib, prelude::*, subclass::prelude::*};
use std::{
cell::{Cell, RefCell},
str::FromStr,
@ -351,19 +353,77 @@ impl Provider {
website: String,
name: String,
id: u32,
) -> Result<gio::File, Box<dyn std::error::Error>> {
) -> Result<String, Box<dyn std::error::Error>> {
let website_url = Url::parse(&website)?;
let favicon = FaviconScrapper::from_url(website_url).await?;
log::debug!("Found the following icons {:#?} for {}", favicon, name);
let icon_name = format!("{}_{}", id, name.replace(' ', "_"));
let icon_name = glib::base64_encode(icon_name.as_bytes());
let small_icon_name = format!("{icon_name}_32x32");
let large_icon_name = format!("{icon_name}_96x96");
// We need two sizes:
// - 32x32 for the accounts lists
// - 96x96 elsewhere
// either we find them in the list of available icons
// or we scale down the "best" one we find.
let mut found_small = false;
let mut found_large = false;
match (favicon.find_size(32).await, favicon.find_size(96).await) {
(Some(small), Some(large)) => {
if small.cache(&small_icon_name).await.is_ok()
&& large.cache(&large_icon_name).await.is_ok()
{
return Ok(icon_name.to_string());
}
}
(Some(small), _) => {
log::debug!("Found a 32x32 variant with no 96x96 variant");
found_small = true;
small.cache(&small_icon_name).await?;
}
(_, Some(large)) => {
log::debug!("Found a 96x96 variant with no 32x32 variant");
found_large = true;
large.cache(&large_icon_name).await?;
}
// We found none, we fallback to getting the best icon
_ => (),
};
log::debug!("Trying to find the highest resolution favicon");
if let Some(best_favicon) = favicon.find_best().await {
log::debug!("Best favicon found is {:#?}", best_favicon);
let cache_path = best_favicon.cache(&icon_name).await?;
Ok(gio::File::for_path(cache_path))
log::debug!("Largest favicon found is {:#?}", best_favicon);
best_favicon.cache(&icon_name).await?;
let cache_path = FAVICONS_PATH.join(&*icon_name);
// Don't try to scale down svg variants
if best_favicon.metadata().type_() != &Type::Svg {
log::debug!("Creating scaled down variants for {:#?}", cache_path);
{
let pixbuf = gdk_pixbuf::Pixbuf::from_file(cache_path.clone())?;
if !found_small {
log::debug!("Creating a 32x32 variant of the favicon");
let small_pixbuf = pixbuf
.scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
let mut small_cache = cache_path.clone();
small_cache.set_file_name(small_icon_name);
small_pixbuf.savev(small_cache.clone(), "png", &[])?;
}
if !found_large {
log::debug!("Creating a 96x96 variant of the favicon");
let large_pixbuf = pixbuf
.scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
let mut large_cache = cache_path.clone();
large_cache.set_file_name(large_icon_name);
large_pixbuf.savev(large_cache.clone(), "png", &[])?;
}
};
tokio::fs::remove_file(cache_path).await?;
}
Ok(icon_name.to_string())
} else {
Err(Box::new(FaviconError::NoResults))
}

View file

@ -1,11 +1,11 @@
use crate::models::Provider;
use crate::models::RUNTIME;
use crate::models::{Provider, FAVICONS_PATH};
use glib::{clone, Receiver, Sender};
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk_macros::send;
pub enum ImageAction {
Ready(gio::File),
Ready(String),
Failed,
}
@ -79,7 +79,7 @@ mod imp {
"size",
"size",
"Image size",
24,
32,
96,
48,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT,
@ -154,13 +154,19 @@ impl ProviderImage {
return;
}
let file = gio::File::for_uri(&uri);
if !file.query_exists(gio::Cancellable::NONE) {
let small_file = gio::File::for_path(&FAVICONS_PATH.join(format!("{uri}_32x32")));
let large_file = gio::File::for_path(&FAVICONS_PATH.join(format!("{uri}_96x96")));
if !small_file.query_exists(gio::Cancellable::NONE)
|| !large_file.query_exists(gio::Cancellable::NONE)
{
self.fetch();
return;
}
imp.image.set_from_file(file.path());
if imp.size.get() == 32 {
imp.image.set_from_file(small_file.path());
} else {
imp.image.set_from_file(large_file.path());
}
imp.stack.set_visible_child_name("image");
}
_ => {
@ -181,8 +187,8 @@ impl ProviderImage {
let (sender, receiver) = futures::channel::oneshot::channel();
RUNTIME.spawn(async move {
match Provider::favicon(website, name, id).await {
Ok(file) => {
sender.send(Some(file)).unwrap();
Ok(cache_name) => {
sender.send(Some(cache_name)).unwrap();
}
Err(err) => {
log::error!("Failed to load favicon {}", err);
@ -193,8 +199,8 @@ impl ProviderImage {
glib::MainContext::default().spawn_local(clone!(@weak self as this => async move {
let imp = this.imp();
let response = receiver.await.unwrap();
if let Some(file) = response {
send!(imp.sender.clone(), ImageAction::Ready(file));
if let Some(cache_name) = response {
send!(imp.sender.clone(), ImageAction::Ready(cache_name));
} else {
send!(imp.sender.clone(), ImageAction::Failed);
}
@ -241,10 +247,15 @@ impl ProviderImage {
imp.image.set_from_icon_name(Some("provider-fallback"));
"invalid".to_string()
}
ImageAction::Ready(image) => {
imp.image.set_from_file(image.path());
let image_uri = image.uri();
image_uri.to_string()
ImageAction::Ready(cache_name) => {
if imp.size.get() == 32 {
imp.image
.set_from_file(Some(&FAVICONS_PATH.join(format!("{cache_name}_32x32"))));
} else {
imp.image
.set_from_file(Some(&FAVICONS_PATH.join(format!("{cache_name}_96x96"))));
}
cache_name
}
};
if let Some(provider) = self.provider() {

View file

@ -5,7 +5,7 @@ use crate::{
use adw::prelude::*;
use gettextrs::gettext;
use glib::{clone, translate::IntoGlib};
use gtk::{gio, glib, subclass::prelude::*, CompositeTemplate};
use gtk::{gio, gdk_pixbuf, glib, subclass::prelude::*, CompositeTemplate};
mod imp {
use super::*;
@ -254,36 +254,26 @@ impl ProviderPage {
let image_uri = if let Some(file) = imp.selected_image.borrow().clone() {
let basename = file.basename().unwrap();
let extension = basename
.to_str()
.unwrap()
.split('.')
.last()
.unwrap_or("png");
let icon_name = glib::base64_encode(basename.to_str().unwrap().as_bytes());
let small_icon_name = format!("{icon_name}_32x32");
let large_icon_name = format!("{icon_name}_96x96");
let icon_name = format!(
"{}.{}",
glib::base64_encode(basename.to_str().unwrap().as_bytes()),
extension
);
// Create a 96x96 & 32x32 variants
let stream = file.read(gio::Cancellable::NONE)?;
let pixbuf = gdk_pixbuf::Pixbuf::from_stream(&stream, gio::Cancellable::NONE)?;
log::debug!("Creating a 32x32 variant of the selected favicon");
let small_pixbuf = pixbuf
.scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
small_pixbuf.savev(FAVICONS_PATH.join(small_icon_name), "png", &[])?;
let image_dest = FAVICONS_PATH.join(icon_name.as_str());
log::debug!("Creating a 96x96 variant of the selected favicon");
let large_pixbuf = pixbuf
.scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
large_pixbuf.savev(FAVICONS_PATH.join(large_icon_name), "png", &[])?;
let dest_file = gio::File::for_path(image_dest);
dest_file
.create(
gio::FileCreateFlags::REPLACE_DESTINATION,
gio::Cancellable::NONE,
)
.ok();
file.copy(
&dest_file,
gio::FileCopyFlags::OVERWRITE,
gio::Cancellable::NONE,
None,
)?;
Some(dest_file.uri().to_string())
Some(icon_name.to_string())
} else {
None
};