Improve image generation perf

This commit is contained in:
Michal Vanko 2024-09-06 14:43:04 +02:00
parent b136f46a6f
commit 7f483af9cf
9 changed files with 82 additions and 49 deletions

View File

@ -3,21 +3,15 @@ pub struct Link {
pub label: String, pub label: String,
} }
#[derive(Default)]
pub struct HeaderProps { pub struct HeaderProps {
pub back_link: Option<Link>, pub back_link: Option<Link>,
} }
impl Default for HeaderProps {
fn default() -> Self {
Self { back_link: None }
}
}
impl HeaderProps { impl HeaderProps {
pub fn with_back_link(link: Link) -> Self { pub fn with_back_link(link: Link) -> Self {
Self { Self {
back_link: Some(link), back_link: Some(link),
..Default::default()
} }
} }
} }

View File

@ -48,7 +48,6 @@ async fn main() {
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }
// TODO footer
// TODO Display blog posts // TODO Display blog posts
// TODO responsive design // TODO responsive design
// TODO go live pipeline // TODO go live pipeline

View File

@ -21,7 +21,7 @@ pub struct BlogPostTemplate {
pub async fn render_blog_post(Path(post_id): Path<String>) -> Result<BlogPostTemplate, StatusCode> { pub async fn render_blog_post(Path(post_id): Path<String>) -> Result<BlogPostTemplate, StatusCode> {
let path = format!("../_posts/blog/{}.md", post_id); let path = format!("../_posts/blog/{}.md", post_id);
let parse_post = parse_post::<BlogPostMetadata>(&path); let parse_post = parse_post::<BlogPostMetadata>(&path, true);
let parsed = parse_post.await?; let parsed = parse_post.await?;
Ok(BlogPostTemplate { Ok(BlogPostTemplate {

View File

@ -24,6 +24,7 @@ pub fn generate_images(
.with_extension(format.get_extension()); .with_extension(format.get_extension());
if save_path.exists() { if save_path.exists() {
debug!("Skip generating {save_path:?} - Already exists");
return; return;
} }

View File

@ -1,12 +1,11 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr as _,
sync::Arc, sync::Arc,
}; };
use anyhow::Context; use anyhow::Context;
use image::{GenericImageView, ImageReader}; use image::{image_dimensions, ImageReader};
use super::{export_format::ExportFormat, image_generator::generate_images}; use super::{export_format::ExportFormat, image_generator::generate_images};
@ -17,23 +16,27 @@ pub fn generate_picture_markup(
width: u32, width: u32,
height: u32, height: u32,
alt_text: &str, alt_text: &str,
generate_image: bool,
) -> Result<String, anyhow::Error> { ) -> Result<String, anyhow::Error> {
if !orig_img_path.starts_with("/") {
return Ok(format!(
r#"<img
alt="{alt_text}"
src="{orig_img_path}"
/>"#
));
}
let exported_formats = get_export_formats(orig_img_path); let exported_formats = get_export_formats(orig_img_path);
let path_to_generated = get_generated_file_name(orig_img_path); let path_to_generated = get_generated_file_name(orig_img_path);
// TODO This should get removed when we move the project structure #move // TODO This should get removed when we move the project structure #move
let dev_only_img_path = Path::new("../static/").join(orig_img_path.strip_prefix("/").unwrap()); let dev_only_img_path =
Path::new("../static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
let orig_img = ImageReader::open(&dev_only_img_path) let orig_img_dimensions = image_dimensions(&dev_only_img_path).unwrap();
.with_context(|| format!("Failed to read instrs from {:?}", &dev_only_img_path))?
.decode()?;
let orig_img_dimensions = orig_img.dimensions();
let resolutions = get_resolutions(orig_img_dimensions, width, height); let resolutions = get_resolutions(orig_img_dimensions, width, height);
// TODO lets generate images
let orig_img_arc = Arc::new(orig_img);
let orig_img_clone = Arc::clone(&orig_img_arc);
let path_to_generated_arc = Arc::new(path_to_generated); let path_to_generated_arc = Arc::new(path_to_generated);
let path_to_generated_clone = Arc::clone(&path_to_generated_arc); let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
let resolutions_arc = Arc::new(resolutions); let resolutions_arc = Arc::new(resolutions);
@ -41,18 +44,25 @@ pub fn generate_picture_markup(
let exported_formats_arc = Arc::new(exported_formats); let exported_formats_arc = Arc::new(exported_formats);
let exported_formats_clone = Arc::clone(&exported_formats_arc); let exported_formats_clone = Arc::clone(&exported_formats_arc);
tokio::spawn(async move { if generate_image {
let orig_img = orig_img_clone.as_ref(); rayon::spawn(move || {
let path_to_generated = path_to_generated_clone.as_ref(); let orig_img = ImageReader::open(&dev_only_img_path)
let resolutions = resolutions_clone.as_ref(); .with_context(|| format!("Failed to read instrs from {:?}", &dev_only_img_path))
let exported_formats = exported_formats_clone.as_ref(); .unwrap()
.decode()
.unwrap();
let path_to_generated = path_to_generated_clone.as_ref();
let resolutions = resolutions_clone.as_ref();
let exported_formats = exported_formats_clone.as_ref();
let result = generate_images(orig_img, path_to_generated, resolutions, exported_formats) let result =
.with_context(|| "Failed to generate images".to_string()); generate_images(&orig_img, path_to_generated, resolutions, exported_formats)
if let Err(e) = result { .with_context(|| "Failed to generate images".to_string());
tracing::error!("Error: {}", e); if let Err(e) = result {
} tracing::error!("Error: {}", e);
}); }
});
}
let exported_formats = Arc::clone(&exported_formats_arc); let exported_formats = Arc::clone(&exported_formats_arc);
let path_to_generated = Arc::clone(&path_to_generated_arc); let path_to_generated = Arc::clone(&path_to_generated_arc);
@ -232,7 +242,11 @@ fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
.file_stem() .file_stem()
.expect("There should be a name for every img"); .expect("There should be a name for every img");
let result = Path::new("/generated_images/") let result = Path::new("/generated_images/")
.join(parent.strip_prefix("/").unwrap()) .join(
parent
.strip_prefix("/")
.unwrap_or(Path::new("/generated_images/")),
)
.join(file_name); .join(file_name);
result result
} }
@ -287,7 +301,9 @@ fn test_get_export_formats() {
} }
#[test] #[test]
fn test_generate_srcset() { fn test_generate_srcset() {
let orig_img_path = PathBuf::from_str("/generated_images/images/uploads/img_name").unwrap(); let orig_img_path =
<PathBuf as std::str::FromStr>::from_str("/generated_images/images/uploads/img_name")
.unwrap();
let export_format = ExportFormat::Avif; let export_format = ExportFormat::Avif;
let resolutions = vec![ let resolutions = vec![
(320, 200, 1.), (320, 200, 1.),
@ -325,7 +341,7 @@ fn test_generate_picture_markup() {
> >
</picture>"#; </picture>"#;
assert_eq!( assert_eq!(
generate_picture_markup(orig_img_path, width, height, "Testing image alt") generate_picture_markup(orig_img_path, width, height, "Testing image alt", false)
.expect("picture markup has to be generated"), .expect("picture markup has to be generated"),
result result
); );

View File

@ -22,7 +22,7 @@ pub async fn get_post_list<'de, Metadata: DeserializeOwned>(
let file_path = file.path(); let file_path = file.path();
let file_path_str = file_path.to_str().unwrap(); let file_path_str = file_path.to_str().unwrap();
info!(":{}", file_path_str); info!(":{}", file_path_str);
let post = parse_post::<Metadata>(file_path_str).await?; let post = parse_post::<Metadata>(file_path_str, false).await?;
posts.push(post); posts.push(post);
} }

View File

@ -6,6 +6,9 @@ use gray_matter::{engine::YAML, Matter};
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
use serde::{de::DeserializeOwned, Deserialize, Deserializer}; use serde::{de::DeserializeOwned, Deserialize, Deserializer};
use tokio::fs; use tokio::fs;
use tracing::debug;
use crate::picture_generator::picture_markup_generator::generate_picture_markup;
pub fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error> pub fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where where
@ -29,6 +32,7 @@ pub struct ParseResult<Metadata> {
pub async fn parse_post<'de, Metadata: DeserializeOwned>( pub async fn parse_post<'de, Metadata: DeserializeOwned>(
path: &str, path: &str,
generate_images: bool,
) -> Result<ParseResult<Metadata>, StatusCode> { ) -> Result<ParseResult<Metadata>, StatusCode> {
let file_contents = fs::read_to_string(path) let file_contents = fs::read_to_string(path)
.await .await
@ -43,7 +47,7 @@ pub async fn parse_post<'de, Metadata: DeserializeOwned>(
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
let body = parse_html(&metadata.content); let body = parse_html(&metadata.content, generate_images);
let filename = Path::new(path) let filename = Path::new(path)
.file_stem() .file_stem()
@ -59,7 +63,7 @@ pub async fn parse_post<'de, Metadata: DeserializeOwned>(
}) })
} }
pub fn parse_html(markdown: &str) -> String { pub fn parse_html(markdown: &str, generate_images: bool) -> String {
let mut options = Options::empty(); let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES); options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES); options.insert(Options::ENABLE_FOOTNOTES);
@ -80,22 +84,41 @@ pub fn parse_html(markdown: &str) -> String {
title, title,
id, id,
}) => { }) => {
println!( // TODO Get image resolution
// Place image into the content with scaled reso to a boundary
let picture_markup =
generate_picture_markup(&dest_url, 500, 500, &title, generate_images).unwrap_or(
format!(
r#"
<img
alt="{alt}"
src="{src}"
/>"#,
alt = title,
src = dest_url,
),
);
// let picture_markup = format!(
// r#"
// <img
// alt="{alt}"
// src="{src}"
// />"#,
// alt = title,
// src = dest_url,
// );
debug!(
"Image link_type: {:?} url: {} title: {} id: {}", "Image link_type: {:?} url: {} title: {} id: {}",
link_type, dest_url, title, id link_type, dest_url, title, id
); );
// TODO src set
Event::Html( Event::Html(
format!( format!(
r#"<figure> r#"<figure>
<img {picture_markup}
alt="{alt}" <figcaption>
src="{src}" "#,
/>
<figcaption>
"#,
alt = title,
src = dest_url,
) )
.into(), .into(),
) )

View File

@ -14,7 +14,7 @@ pub async fn get_featured_projects() -> Result<Vec<ParseResult<ProjectMetadata>>
.into_iter() .into_iter()
.filter(|post| post.metadata.featured) .filter(|post| post.metadata.featured)
.map(|mut post| { .map(|mut post| {
post.metadata.description = parse_html(&post.metadata.description); post.metadata.description = parse_html(&post.metadata.description, false);
post post
}) })
.collect(); .collect();

View File

@ -2,7 +2,7 @@
<aside class="row-span-3"> <aside class="row-span-3">
{% match post.metadata.thumbnail %} {% match post.metadata.thumbnail %}
{% when Some with (orig_path) %} {% when Some with (orig_path) %}
{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(orig_path, 180, 240, "Article thumbnail").unwrap()|safe }} {{ crate::picture_generator::picture_markup_generator::generate_picture_markup(orig_path, 180, 240, "Article thumbnail", true).unwrap()|safe }}
{% when None %} {% when None %}
<div> TODO default obrazok </div> <div> TODO default obrazok </div>
{% endmatch %} {% endmatch %}