quoter/src/thumbnail.rs
Untone cbb8025a0c
Some checks failed
deploy / deploy (push) Failing after 5s
fix-thumb1
2024-11-02 04:59:14 +03:00

201 lines
8.6 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 path_parts: Vec<&str> = path.split('/').collect();
let mut extension = String::new();
let mut width = 0;
let mut base_filename = String::new();
// Получаем последнюю часть пути (имя файла)
if let Some(last_part) = path_parts.last() {
// Разделяем имя файла и расширение
if let Some((name, ext)) = last_part.rsplit_once('.') {
extension = ext.to_string();
base_filename = name.to_string();
} else {
base_filename = last_part.to_string();
}
}
// Проверяем наличие ширины в пути
for part in path_parts.iter() {
if part.ends_with('x') {
if let Ok(w) = part.trim_end_matches('x').parse::<u32>() {
width = w;
break;
}
}
}
// Извлечение ширины из 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;
}
}
}
}
// Если ширина не найдена в пути, проверяем имя файла
if width == 0 {
if let Some((name, width_str)) = base_filename.rsplit_once('_') {
if let Ok(w) = width_str.parse::<u32>() {
width = w;
base_filename = name.to_string();
}
}
}
// Очищаем base_filename от возможных префиксов пути
if let Some(uuid) = base_filename.split('/').last() {
base_filename = uuid.to_string();
}
(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);
}
}
}
Err(e) => {
warn!("cannot generate thumbnails for {}: {}", filename, e);
return Err(e);
}
}
}
Ok(())
}
/// Выбирает ближайший подходящий размер из предопределённых.
/// Если `requested_width` больше максимальной ширины в `THUMB_WIDTHS`,
/// возвращает максимальную ширину.
pub fn find_closest_width(requested_width: u32) -> u32 {
// Проверяем, превышает ли запрошенная ширина максимальную доступную ширину
if requested_width > *THUMB_WIDTHS.last().unwrap() {
return *THUMB_WIDTHS.last().unwrap();
}
// Находим ширину с минимальной абсолютной разницей с запрошенной
*THUMB_WIDTHS
.iter()
.min_by_key(|&&width| (width as i32 - requested_width as i32).abs())
.unwrap_or(&THUMB_WIDTHS[0]) // Возвращаем самый маленький размер, если ничего не подошло
}