quoter/src/thumbnail.rs
Untone b0d7162460
All checks were successful
deploy / deploy (push) Successful in 58s
orig-ext-case
2024-10-23 17:33:39 +03:00

184 lines
7.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use actix_web::error::ErrorInternalServerError;
use image::{imageops::FilterType, DynamicImage, ImageFormat};
use log::warn;
use std::{collections::HashMap, io::Cursor};
use crate::{app_state::AppState, s3_utils::upload_to_s3};
pub const THUMB_WIDTHS: [u32; 7] = [10, 40, 110, 300, 600, 800, 1400];
/// Парсит путь к файлу, извлекая оригинальное имя, требуемую ширину и формат.
/// Примеры:
/// - "filename_150.ext" -> ("filename", 150, "ext")
/// - "unsafe/1440x/production/image/439efaa0-816f-11ef-b201-439da98539bc.jpg" -> ("439efaa0-816f-11ef-b201-439da98539bc", 1440, "jpg")
/// - "unsafe/production/image/5627e002-0c53-11ee-9565-0242ac110006.png" -> ("5627e002-0c53-11ee-9565-0242ac110006", 0, "png")
/// - "unsafe/development/image/439efaa0-816f-11ef-b201-439da98539bc.jpg/webp" -> ("439efaa0-816f-11ef-b201-439da98539bc", 0, "webp")
pub fn parse_file_path(requested_path: &str) -> (String, u32, String) {
let mut path = requested_path.to_string();
if requested_path.ends_with("/webp") {
path = path.replace("/webp", "");
}
let mut path_parts: Vec<&str> = path.split('/').collect();
let mut extension = String::new();
let mut width = 0;
let mut base_filename = String::new();
if path_parts.is_empty() {
return (path.to_string(), width, extension);
}
// пытаемся извлечь формат из имени файла
if let Some(filename_part) = path_parts.pop() {
if let Some((base, ext_part)) = filename_part.rsplit_once('.') {
extension = ext_part.to_string();
base_filename = base.to_string(); // Устанавливаем base_filename без расширения
} else {
base_filename = filename_part.to_string();
}
}
// Если base_filename ещё не установлено, извлекаем его
if base_filename.is_empty() {
if let Some(filename_part) = path_parts.pop() {
if let Some((base, ext_part)) = filename_part.rsplit_once('.') {
extension = ext_part.to_string();
base_filename = base.to_string();
} else {
base_filename = filename_part.to_string();
}
}
}
// Извлечение ширины из base_filename, если она есть
if let Some((name_part, width_str)) = base_filename.rsplit_once('_') {
if let Ok(w) = width_str.parse::<u32>() {
width = w;
base_filename = name_part.to_string();
}
}
// Проверка на старую ширину в путях, начинающихся с "unsafe"
if path.starts_with("unsafe") && width == 0 {
if path_parts.len() >= 2 {
if let Some(old_width_str) = path_parts.get(1) { // Получаем второй элемент
let old_width_str = old_width_str.trim_end_matches('x');
if let Ok(w) = old_width_str.parse::<u32>() {
width = w;
}
}
}
}
(base_filename, width, extension)
}
/// Генерирует миниатюры изображения.
///
/// Теперь функция принимает дополнительный параметр `format`, который определяет формат сохранения миниатюр.
/// Это позволяет поддерживать различные форматы изображений без необходимости заранее предугадывать их.
pub async fn generate_thumbnails(
image: &DynamicImage,
format: ImageFormat
) -> Result<HashMap<u32, Vec<u8>>, actix_web::Error> {
let mut thumbnails = HashMap::new();
for &width in THUMB_WIDTHS.iter().filter(|&&w| w < image.width()) {
let thumbnail = image.resize(width, u32::MAX, FilterType::Lanczos3); // Ресайз изображения по ширине
let mut buffer = Vec::new();
thumbnail
.write_to(&mut Cursor::new(&mut buffer), format)
.map_err(|e| {
log::error!("Ошибка при сохранении миниатюры: {}", e);
ErrorInternalServerError("Не удалось сгенерировать миниатюру")
})?; // Сохранение изображения в указанном формате
thumbnails.insert(width, buffer);
}
Ok(thumbnails)
}
/// Определяет формат изображения на основе расширения файла.
fn determine_image_format(extension: &str) -> Result<ImageFormat, actix_web::Error> {
match extension.to_lowercase().as_str() {
"jpg" | "jpeg" => Ok(ImageFormat::Jpeg),
"png" => Ok(ImageFormat::Png),
"gif" => Ok(ImageFormat::Gif),
"bmp" => Ok(ImageFormat::Bmp),
"ico" => Ok(ImageFormat::Ico),
"tiff" => Ok(ImageFormat::Tiff),
"webp" => Ok(ImageFormat::WebP),
_ => {
log::error!("Неподдерживаемый формат изображения: {}", extension);
Err(ErrorInternalServerError("Неподдерживаемый формат изображения"))
},
}
}
/// Сохраняет данные миниатюры.
///
/// Обновлена для передачи корректного формата изображения.
pub async fn thumbdata_save(
original_data: Vec<u8>,
state: &AppState,
original_filename: &str,
content_type: String,
) -> Result<(), actix_web::Error> {
if content_type.starts_with("image") {
warn!("original file name: {}", original_filename);
let (base_filename, _, extension) = parse_file_path(&original_filename);
warn!("detected file extension: {}", extension);
let ext = extension.to_lowercase();
let filename = format!("{}.{}", base_filename, ext);
let img = match image::load_from_memory(&original_data) {
Ok(img) => img,
Err(e) => {
warn!("cannot load image from memory: {}", e);
return Err(ErrorInternalServerError("cant load image"));
}
};
warn!("generate thumbnails for {}", filename);
// Определяем формат изображения
let format = determine_image_format(&ext)?;
// Генерация миниатюр с использованием определённого формата
match generate_thumbnails(&img, format).await {
Ok(thumbnails_bytes) => {
for (thumb_width, thumbnail) in thumbnails_bytes {
let thumb_filename = format!("{}_{}.{}", base_filename, thumb_width, ext);
// Загружаем миниатюру в S3
if let Err(e) = upload_to_s3(
&state.storj_client,
&state.bucket,
&thumb_filename,
thumbnail,
&content_type,
)
.await
{
warn!("cannot load thumb {}: {}", thumb_filename, e);
} else {
warn!("thumb {} uploaded to storj", thumb_filename);
}
}
}
Err(e) => {
warn!("cannot generate thumbnails for {}: {}", filename, e);
return Err(e);
}
}
}
Ok(())
}
/// Выбирает ближайший подходящий размер из предопределённых.
pub fn find_closest_width(requested_width: u32) -> u32 {
*THUMB_WIDTHS
.iter()
.min_by_key(|&&width| (width as i32 - requested_width as i32).abs())
.unwrap_or(&THUMB_WIDTHS[0]) // Возвращаем самый маленький размер, если ничего не подошло
}