use crate::models::{ArticleID, Url};
use crate::schema::thumbnails;
use chrono::{DateTime, Utc};
use error::ThumbnailError;
use image::codecs::png::PngEncoder;
use image::{GenericImageView, ImageEncoder, ImageReader, imageops};
use reqwest::{Client, header};
use std::io::Cursor;
const THUMB_DEST_HEIGHT: u32 = 128;
#[derive(Identifiable, Queryable, Clone, Debug, Insertable, Eq)]
#[diesel(primary_key(article_id))]
#[diesel(table_name = thumbnails)]
#[diesel(check_for_backend(SQLite))]
pub struct Thumbnail {
pub article_id: ArticleID,
#[diesel(column_name = "date")]
pub last_try: DateTime<Utc>,
pub format: Option<String>,
pub etag: Option<String>,
pub source_url: Option<Url>,
pub data: Option<Vec<u8>>,
pub width: Option<i32>,
pub height: Option<i32>,
}
impl PartialEq for Thumbnail {
fn eq(&self, other: &Thumbnail) -> bool {
self.article_id == other.article_id
}
}
impl Thumbnail {
pub fn empty(article_id: &ArticleID) -> Self {
Thumbnail {
article_id: article_id.clone(),
last_try: Utc::now(),
format: None,
etag: None,
source_url: None,
data: None,
width: None,
height: None,
}
}
pub async fn from_url(url: &str, article_id: &ArticleID, client: &Client) -> Result<Self, ThumbnailError> {
let head_res = client.head(url).send().await?;
if let Some(Ok(content_type)) = head_res.headers().get(header::CONTENT_TYPE).map(|hval| hval.to_str()) {
if !content_type.starts_with("image") {
tracing::debug!(%url, mime = content_type, "Thumnail URL doesn't point to an image");
return Err(ThumbnailError::NotAnImage);
}
} else {
tracing::debug!(%url, "No content type header value set for image URL");
return Err(ThumbnailError::NotAnImage);
}
let res = client.get(url).send().await?;
let etag = res
.headers()
.get(reqwest::header::ETAG)
.and_then(|hval| hval.to_str().ok())
.map(|etag| etag.into());
let image_data = res.bytes().await?;
let image = ImageReader::new(Cursor::new(image_data))
.with_guessed_format()
.map_err(|_| ThumbnailError::GuessFormat)?
.decode()
.map_err(|_| ThumbnailError::Decode)?;
let (original_width, original_height) = image.dimensions();
let (thumb_width, thumb_height) = Self::calc_thumb_dimensions(original_width, original_height);
let thumbnail = imageops::thumbnail(&image, thumb_width, thumb_height);
let (width, height) = thumbnail.dimensions();
let thumbnail_data = thumbnail.into_vec();
let mut dest = Cursor::new(Vec::new());
let encoder = PngEncoder::new(&mut dest);
encoder
.write_image(&thumbnail_data, width, height, image::ExtendedColorType::Rgba8)
.map_err(|_| ThumbnailError::Encode)?;
Ok(Thumbnail {
article_id: article_id.clone(),
last_try: Utc::now(),
format: Some("image/png".into()),
etag,
source_url: Some(Url::parse(url).unwrap()),
data: Some(dest.into_inner()),
width: Some(width as i32),
height: Some(height as i32),
})
}
fn calc_thumb_dimensions(original_width: u32, original_height: u32) -> (u32, u32) {
if original_height <= THUMB_DEST_HEIGHT {
return (original_width, original_height);
}
let ratio = (original_width as f64) / (original_height as f64);
((THUMB_DEST_HEIGHT as f64 * ratio) as u32, THUMB_DEST_HEIGHT)
}
}
mod error {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ThumbnailError {
#[error("Failed to decode image")]
Decode,
#[error("Failed to guess image format")]
GuessFormat,
#[error("Failed to encode image")]
Encode,
#[error("Http request failed")]
Http(#[from] reqwest::Error),
#[error("Url doesn't point to an image")]
NotAnImage,
#[error("Unknown Error")]
Unknown,
}
}
#[cfg(test)]
mod test {
use super::Thumbnail;
use crate::models::ArticleID;
use once_cell::sync::Lazy;
use reqwest::Client;
use test_log::test;
#[test(tokio::test)]
async fn golem_bitcoin() {
let url = "https://www.golem.de/2102/154364-260020-260017_rc.jpg";
let client: Lazy<Client> = Lazy::new(Client::new);
let _thumb = Thumbnail::from_url(url, &ArticleID::new("asf"), &client).await.unwrap();
}
#[test(tokio::test)]
async fn feaneron() {
let url = "https://feaneron.files.wordpress.com/2019/05/captura-de-tela-de-2019-05-31-17-48-43.png?w=1200";
let client: Lazy<Client> = Lazy::new(Client::new);
let _thumb = Thumbnail::from_url(url, &ArticleID::new("asf"), &client).await.unwrap();
}
}