remove old svelte web source

This commit is contained in:
2024-09-30 21:13:20 +02:00
parent f42ebc7044
commit 1ce8ccfdd5
149 changed files with 20 additions and 10198 deletions

View File

@ -0,0 +1,36 @@
use image::ImageFormat;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ExportFormat {
Jpeg,
Avif,
Svg,
Png,
}
impl ExportFormat {
pub fn get_extension(&self) -> &str {
match self {
ExportFormat::Jpeg => "jpg",
ExportFormat::Avif => "avif",
ExportFormat::Svg => "svg",
ExportFormat::Png => "png",
}
}
pub fn get_type(&self) -> &str {
match self {
ExportFormat::Jpeg => "image/jpeg",
ExportFormat::Avif => "image/avif",
ExportFormat::Svg => "image/svg+xml",
ExportFormat::Png => "image/png",
}
}
pub fn get_image_format(&self) -> ImageFormat {
match self {
ExportFormat::Jpeg => ImageFormat::Jpeg,
ExportFormat::Avif => ImageFormat::Avif,
ExportFormat::Svg => ImageFormat::Jpeg, // TODO what now?
ExportFormat::Png => ImageFormat::Png,
}
}
}

View File

@ -0,0 +1,49 @@
use std::{fs::create_dir_all, path::Path};
use image::{imageops::FilterType, DynamicImage};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{debug, error};
use super::export_format::ExportFormat;
pub fn generate_images(
image: &DynamicImage,
path_to_generated: &Path,
resolutions: &[(u32, u32, f32)],
formats: &[ExportFormat],
) -> Result<(), anyhow::Error> {
formats.par_iter().for_each(|format| {
resolutions.par_iter().for_each(|resolution| {
let (width, height, _) = *resolution;
// let image = image.clone();
let resized = image.resize_to_fill(width, height, FilterType::Triangle);
let file_name = path_to_generated.file_name().unwrap().to_str().unwrap();
let save_path = Path::new("./")
.join(path_to_generated.strip_prefix("/").unwrap())
.with_file_name(format!("{file_name}_{width}x{height}"))
.with_extension(format.get_extension());
if save_path.exists() {
debug!("Skip generating {save_path:?} - Already exists");
return;
}
let parent_dir = save_path.parent().unwrap();
if !parent_dir.exists() {
create_dir_all(parent_dir).unwrap();
}
let result = resized.save_with_format(&save_path, format.get_image_format());
match result {
Err(err) => {
error!("Failed to generate {:?} - {:?}", &save_path, err);
}
_ => {
debug!("Generated image {:?}", &save_path);
}
}
});
});
Ok(())
}

View File

@ -0,0 +1,27 @@
/*!
This is going to be an attempt for creating HTML markup for serving and generating images
for the most common PIXEL_DENSITIES.
It should create `<picture>` elements with following features:
- least amount of needed arguments
- for each pixel density it should have a definition in `srcset`
- create a `avif` type for the image for each pixel_density
- create an image in the original format for each pixel_density
- support case of `svg` therefore not doing any of the pixel_density logic
These features might be considered later:
- support case for art direction (different pictures for different screen sizes)
TODO: figure wether `height` or `width` have to be known ahead of time
## Usage
It can be used from the rust code
It should be used from the templates as well
*/
pub mod export_format;
pub mod image_generator;
pub mod picture_markup_generator;
pub mod resolutions;

View File

@ -0,0 +1,329 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::Context;
use image::{image_dimensions, ImageReader};
use indoc::formatdoc;
use super::{
export_format::ExportFormat, image_generator::generate_images,
resolutions::get_max_resolution_with_crop,
};
pub const PIXEL_DENSITIES: [f32; 5] = [1., 1.5, 2., 3., 4.];
pub fn generate_picture_markup(
orig_img_path: &str,
width: u32,
height: u32,
alt_text: &str,
class_name: Option<&str>,
generate_image: bool,
) -> Result<String, anyhow::Error> {
let exported_formats = get_export_formats(orig_img_path);
let class_attr = if let Some(class) = class_name {
format!(r#"class="{class}""#)
} else {
"".to_string()
};
if exported_formats.is_empty() {
return Ok(formatdoc!(
r#"<img
src="{orig_img_path}"
width="{width}"
height="{height}"
{class_attr}
alt="{alt_text}"
>"#
));
}
let path_to_generated = get_generated_file_name(orig_img_path);
// TODO This should get removed when we move the project structure #directory-swap
let disk_img_path =
Path::new("../static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
let orig_img_dimensions = image_dimensions(&disk_img_path)?;
let resolutions = get_resolutions(orig_img_dimensions, width, height);
let path_to_generated_arc = Arc::new(path_to_generated);
let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
let resolutions_arc = Arc::new(resolutions);
let resolutions_clone = Arc::clone(&resolutions_arc);
let exported_formats_arc = Arc::new(exported_formats);
let exported_formats_clone = Arc::clone(&exported_formats_arc);
if generate_image {
rayon::spawn(move || {
let orig_img = ImageReader::open(&disk_img_path)
.with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path))
.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)
.with_context(|| "Failed to generate images".to_string());
if let Err(e) = result {
tracing::error!("Error: {}", e);
}
});
}
let exported_formats = Arc::clone(&exported_formats_arc);
let path_to_generated = Arc::clone(&path_to_generated_arc);
let resolutions = Arc::clone(&resolutions_arc);
let source_tags = exported_formats
.iter()
.map(|format| {
let srcset = generate_srcset(&path_to_generated, format, &resolutions);
let format_type = format.get_type();
formatdoc!(
r#"<source
srcset="{srcset}"
type="{format_type}"
>"#
)
})
.collect::<Vec<String>>()
.join("\n");
let image_path = get_image_path(
&path_to_generated,
resolutions.first().expect("Should this error ever happen?"),
exported_formats.last().expect("Can this one ever happen?"),
);
let image_tag = formatdoc!(
r#"<img
src="{image_path}"
width="{width}"
height="{height}"
alt="{alt_text}"
{class_attr}
>"#
);
let result = formatdoc!(
r#"<picture>
{source_tags}
{image_tag}
</picture>"#
);
Ok(result)
}
fn get_image_path(path: &Path, resolution: &(u32, u32, f32), format: &ExportFormat) -> String {
let path_name = path.to_str().expect("Image has to have a valid path");
let (width, height, _) = resolution;
let extension = format.get_extension();
format!("{path_name}_{width}x{height}.{extension}")
}
fn get_resolutions(
(orig_width, orig_height): (u32, u32),
width: u32,
height: u32,
) -> Vec<(u32, u32, f32)> {
let mut resolutions: Vec<(u32, u32, f32)> = vec![];
for pixel_density in PIXEL_DENSITIES {
let (density_width, density_height) = (
(pixel_density * width as f32) as u32,
(pixel_density * height as f32) as u32,
);
// The equal sign `=` was added just to prevent next occurence of the loop
// In case of the `orig_width` and `orig_height` being the same as `width` and `height`
// See test case #1
if density_width >= orig_width || density_height >= orig_height {
let (max_width, max_height) =
get_max_resolution_with_crop((orig_width, orig_height), width, height);
resolutions.push((max_width, max_height, pixel_density));
break;
}
resolutions.push((density_width, density_height, pixel_density));
}
resolutions
}
#[test]
fn test_get_resolutions() {
assert_eq!(
get_resolutions((320, 200), 320, 200),
vec![(320, 200, 1.)],
"Only original size fits"
);
assert_eq!(
get_resolutions((500, 400), 320, 200),
vec![(320, 200, 1.), (480, 300, 1.5), (500, 312, 2.)],
"Should only create sizes that fits and fill the max possible for the last one - width"
);
assert_eq!(
get_resolutions((400, 600), 300, 200),
vec![(300, 200, 1.), (400, 266, 1.5)],
"Should only create sizes that fits and fill the max possible for the last one - height"
);
assert_eq!(
get_resolutions((1200, 900), 320, 200),
vec![
(320, 200, 1.),
(480, 300, 1.5),
(640, 400, 2.),
(960, 600, 3.),
(1200, 750, 4.)
],
"Should create all possible sizes, with the last one maxed"
);
}
fn strip_prefixes(path: &Path) -> &Path {
// Loop to remove all leading "../" components
let mut parent_path = path
.parent()
.expect("there should be a parent route to an image");
while let Ok(stripped) = parent_path
.strip_prefix("..")
.or(parent_path.strip_prefix("."))
{
parent_path = stripped;
}
parent_path
}
fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
let path = Path::new(&orig_img_path);
// let parent = path
// .parent()
// .expect("There should be a parent route to an image")
// .strip_prefix(".")
// .unwrap();
let parent = strip_prefixes(path);
let file_name = path
.file_stem()
.expect("There should be a name for every img");
let result = Path::new("/generated_images/")
.join(
parent
.strip_prefix("/")
.unwrap_or(Path::new("/generated_images/")),
)
.join(file_name);
result
}
#[test]
fn test_get_generated_paths() {
let orig_img_path = "/images/uploads/img_name.jpg";
assert_eq!(
get_generated_file_name(orig_img_path)
.to_str()
.unwrap_or(""),
"/generated_images/images/uploads/img_name"
);
}
fn generate_srcset(path: &Path, format: &ExportFormat, resolutions: &[(u32, u32, f32)]) -> String {
resolutions
.iter()
.map(|resolution| {
let extension = format.get_extension();
let (width, height, density) = resolution;
let path_name = path.to_str().expect("Path to an image has to be valid");
let formatted_density = if density.fract() == 0.0 {
format!("{}", density) // Convert to integer if there is no decimal part
} else {
format!("{:.1}", density) // Format to 1 decimal place if there is a fractional part
};
format!("{path_name}_{width}x{height}.{extension} {formatted_density}x")
})
.collect::<Vec<String>>()
.join(", ")
}
fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
let path = Path::new(&orig_img_path)
.extension()
.and_then(|ext| ext.to_str());
match path {
// THINK: Do we want to enable avif? It's very expensive to encode
// Some("jpg" | "jpeg") => vec![ExportFormat::Avif, ExportFormat::Jpeg],
// Some("png") => vec![ExportFormat::Avif, ExportFormat::Png],
Some("jpg" | "jpeg") => vec![ExportFormat::Jpeg],
Some("png") => vec![ExportFormat::Png],
Some(_) | None => vec![],
}
}
#[test]
fn test_get_export_formats() {
assert_eq!(
get_export_formats("/images/uploads/img_name.jpg"),
vec![ExportFormat::Avif, ExportFormat::Jpeg]
)
}
#[test]
fn test_generate_srcset() {
let orig_img_path =
<PathBuf as std::str::FromStr>::from_str("/generated_images/images/uploads/img_name")
.unwrap();
let export_format = ExportFormat::Avif;
let resolutions = vec![
(320, 200, 1.),
(480, 300, 1.5),
(640, 400, 2.),
(960, 600, 3.),
(1200, 750, 4.),
];
let result = "/generated_images/images/uploads/img_name_320x200.avif 1x, /generated_images/images/uploads/img_name_480x300.avif 1.5x, /generated_images/images/uploads/img_name_640x400.avif 2x, /generated_images/images/uploads/img_name_960x600.avif 3x, /generated_images/images/uploads/img_name_1200x750.avif 4x";
assert_eq!(
generate_srcset(&orig_img_path, &export_format, &resolutions),
result
)
}
#[test]
fn test_generate_picture_markup() {
use indoc::indoc;
let width = 300;
let height = 200;
let orig_img_path = "/images/uploads/2020-03-23_20-24-06_393.jpg";
let result = indoc! {
r#"<picture>
<source
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.avif 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.avif 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.avif 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.avif 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.avif 4x"
type="image/avif"
>
<source
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.jpg 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.jpg 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.jpg 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.jpg 4x"
type="image/jpeg"
>
<img
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
width="300"
height="200"
alt="Testing image alt"
>
</picture>"#,
};
assert_eq!(
generate_picture_markup(
orig_img_path,
width,
height,
"Testing image alt",
None,
false
)
.expect("picture markup has to be generated"),
result
);
}

View File

@ -0,0 +1,91 @@
pub fn get_max_resolution_with_crop(
(orig_width, orig_height): (u32, u32),
width: u32,
height: u32,
) -> (u32, u32) {
let width_scale = orig_width as f32 / width as f32;
let height_scale = orig_height as f32 / height as f32;
let scale = width_scale.min(height_scale);
(
(width as f32 * scale) as u32,
(height as f32 * scale) as u32,
)
}
#[test]
fn test_get_max_resolution_with_crop() {
assert_eq!(
get_max_resolution_with_crop((320, 200), 320, 200),
(320, 200),
"Original size fits"
);
// THINK: Real curious if this is what I want to do. Rather than use CSS to `object-cover` original image size
assert_eq!(
get_max_resolution_with_crop((200, 200), 300, 200),
(200, 133),
"Image has to be smaller"
);
assert_eq!(
get_max_resolution_with_crop((1000, 1000), 200, 100),
(1000, 500),
"width is maxed"
);
assert_eq!(
get_max_resolution_with_crop((1000, 1000), 100, 200),
(500, 1000),
"height is maxed"
);
assert_eq!(
get_max_resolution_with_crop((300, 200), 600, 500),
(240, 200),
"image has to be scaled down"
);
}
pub fn get_max_resolution(
(orig_width, orig_height): (u32, u32),
max_width: u32,
max_height: u32,
) -> (u32, u32) {
// If the original dimensions are within the max dimensions, return them as is
if orig_width <= max_width && orig_height <= max_height {
return (orig_width, orig_height);
}
let width_scale = max_width as f32 / orig_width as f32;
let height_scale = max_height as f32 / orig_height as f32;
// Determine the scaling factor to ensure the image fits within the bounds
let scale = width_scale.min(height_scale);
(
(orig_width as f32 * scale).round() as u32,
(orig_height as f32 * scale).round() as u32,
)
}
#[test]
fn test_get_max_resolution() {
assert_eq!(
get_max_resolution((999, 675), 1000, 800),
(999, 675),
"Original size fits"
);
assert_eq!(
get_max_resolution((1100, 400), 1000, 800),
(1000, 364),
"Image should be resized to fit within max dimensions"
);
assert_eq!(
get_max_resolution((1100, 1200), 1000, 800),
(733, 800),
"Image should be resized to fit within max dimensions"
);
assert_eq!(
get_max_resolution((1100, 800), 1000, 800),
(1000, 727),
"Image should be resized to fit within max dimensions"
);
}