quoter/src/thumbnail.rs
Untone 0982dff45b
Some checks failed
deploy / deploy (push) Failing after 5s
heic-bypass
2024-11-13 12:03:32 +03:00

206 lines
8.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),
"webp" => Ok(ImageFormat::WebP),
"heic" | "heif" | "tiff" | "tif" => {
// Конвертируем HEIC и TIFF в JPEG при сохранении
Ok(ImageFormat::Jpeg)
},
_ => {
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);
// Для HEIC файлов просто сохраняем оригинал как миниатюру
if content_type == "image/heic" {
warn!("HEIC file detected, using original as thumbnail");
let thumb_filename = format!("{}_{}.heic", base_filename, THUMB_WIDTHS[0]);
if let Err(e) = upload_to_s3(
&state.storj_client,
&state.bucket,
&thumb_filename,
original_data,
&content_type,
)
.await
{
warn!("cannot save HEIC thumb {}: {}", thumb_filename, e);
return Err(ErrorInternalServerError("cant save HEIC thumbnail"));
}
return Ok(());
}
// Для остальных изображений продолжаем как обычно
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 {}", original_filename);
let format = determine_image_format(&extension.to_lowercase())?;
match generate_thumbnails(&img, format).await {
Ok(thumbnails_bytes) => {
for (thumb_width, thumbnail) in thumbnails_bytes {
let thumb_filename = format!("{}_{}.{}", base_filename, thumb_width, extension);
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 {}: {}", original_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]) // Возвращаем самый маленький размер, если ничего не подошло
}