0.1.0-overlay

This commit is contained in:
Untone 2024-10-23 20:06:34 +03:00
parent 0d4f21c79b
commit 6e90529420
9 changed files with 556 additions and 237 deletions

594
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "discoursio-quoter"
version = "0.0.9"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -24,6 +24,8 @@ actix-multipart = "0.7.2"
log = "0.4.22"
env_logger = "0.11.5"
actix = "0.13.5"
imageproc = "0.25.0"
ab_glyph = "0.2.29"
[[bin]]
name = "quoter"

View File

@ -32,10 +32,15 @@
7. **Сохранение информации о загруженных файлах в Redis**:
- Имя каждого загруженного файла сохраняется в Redis для отслеживания загруженных пользователем файлов. Это позволяет учитывать квоты и управлять пространством, занимаемым файлами.
8. **Оверлей для shout**:
- При загрузке файла, если он является изображением, и в запросе присутствует параметр `s=<shout_id>`, то к файлу будет добавлен оверлей с данными shout.
## Использование
Нужно задать следующие переменные среды:
- `REDIS_URL`: URL для подключения к Redis. Используется для управления квотами и хранения информации о загружаемых файлах.
- `CDN_DOMAIN`: Домен CDN для генерации публичных URL-адресов загруженных файлов.
- `AUTH_URL`: URL для подключения к сервису аутентификации.
- `CORE_URL`: URL для подключения к сервису core.
- `STORJ_ACCESS_KEY`, `STORJ_SECRET_KEY`, `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`

BIN
src/Muller-Regular.woff2 Normal file

Binary file not shown.

72
src/core.rs Normal file
View File

@ -0,0 +1,72 @@
use reqwest::Client as HTTPClient;
use serde::Deserialize;
use serde_json::json;
use std::{collections::HashMap, env, error::Error};
// Структура для десериализации ответа от сервиса аутентификации
#[derive(Deserialize)]
struct CoreResponse {
data: Option<Shout>,
}
#[derive(Deserialize)]
pub struct ShoutTopic {
pub slug: String,
pub title: String,
}
#[derive(Deserialize)]
pub struct ShoutAuthor {
pub name: String,
}
#[derive(Deserialize)]
pub struct Shout {
pub title: String,
pub created_at: String,
pub created_by: i32,
pub main_topic: String,
pub topics: Vec<ShoutTopic>,
pub authors: Vec<ShoutAuthor>,
pub layout: String,
}
pub async fn get_shout_by_id(shout_id: i32) -> Result<Shout, Box<dyn Error>> {
let mut variables = HashMap::<String, i32>::new();
let api_base = env::var("CORE_URL")?;
let query_name = "get_shout";
let operation = "GetShout";
if shout_id != 0 {
variables.insert("shout_id".to_string(), shout_id);
}
let gql = json!({
"query": format!("query {}($slug: String, $shout_id: Int) {{ {}(slug: $slug, shout_id: $shout_id) {{ title created_at created_by topics {{ title slug }} authors {{ id name }} }} }}", operation, query_name),
"operationName": operation,
"variables": variables
});
let client = HTTPClient::new();
let response = client
.post(&api_base)
.json(&gql)
.send()
.await?;
if response.status().is_success() {
let core_response: CoreResponse = response.json().await?;
if let Some(shout) = core_response.data {
return Ok(shout);
}
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"Shout not found",
)))
} else {
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
response.status().to_string(),
)))
}
}

View File

@ -9,7 +9,7 @@ use crate::thumbnail::{find_closest_width, parse_file_path, thumbdata_save};
/// Обработчик для скачивания файла и генерации миниатюры, если она недоступна.
pub async fn proxy_handler(
_req: HttpRequest,
req: HttpRequest,
requested_res: web::Path<String>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
@ -48,6 +48,11 @@ pub async fn proxy_handler(
warn!("content_type: {}", content_type);
let shout_id = match req.query_string().contains("s=") {
true => req.query_string().split("s=").collect::<Vec<&str>>().pop().unwrap_or(""),
false => ""
};
return match state.get_path(&filekey).await {
Ok(Some(stored_path)) => {
warn!("stored_path: {}", stored_path);
@ -55,7 +60,7 @@ pub async fn proxy_handler(
if check_file_exists(&state.storj_client, &state.bucket, &stored_path).await? {
if content_type.starts_with("image") {
return match requested_width == 0 {
true => serve_file(&stored_path, &state).await,
true => serve_file(&stored_path, &state, shout_id).await,
false => {
// find closest thumb width
let closest: u32 = find_closest_width(requested_width as u32);
@ -71,7 +76,7 @@ pub async fn proxy_handler(
{
Ok(true) => {
warn!("serve existed thumb file: {}", thumb_filename);
serve_file(thumb_filename, &state).await
serve_file(thumb_filename, &state, shout_id).await
},
Ok(false) => {
if let Ok(filedata) = load_file_from_s3(
@ -91,7 +96,7 @@ pub async fn proxy_handler(
)
.await;
warn!("serve new thumb file: {}", thumb_filename);
serve_file(thumb_filename, &state).await
serve_file(thumb_filename, &state, shout_id).await
} else {
error!("cannot generate thumbnail");
Err(ErrorInternalServerError(

View File

@ -2,10 +2,11 @@ use actix_web::{error::ErrorInternalServerError, HttpResponse, Result};
use mime_guess::MimeGuess;
use crate::app_state::AppState;
use crate::overlay::generate_overlay;
use crate::s3_utils::check_file_exists;
/// Функция для обслуживания файла по заданному пути.
pub async fn serve_file(filepath: &str, state: &AppState) -> Result<HttpResponse, actix_web::Error> {
pub async fn serve_file(filepath: &str, state: &AppState, shout_id: &str) -> Result<HttpResponse, actix_web::Error> {
if filepath.is_empty() {
return Err(ErrorInternalServerError("Filename is empty".to_string()));
}
@ -32,7 +33,12 @@ pub async fn serve_file(filepath: &str, state: &AppState) -> Result<HttpResponse
.await
.map_err(|_| ErrorInternalServerError("Failed to read object body"))?;
let data_bytes = data.into_bytes();
let data_bytes = match shout_id.is_empty() {
true => data.into_bytes(),
false => generate_overlay(shout_id, data.into_bytes()).await?
};
let mime_type = MimeGuess::from_path(&filepath).first_or_octet_stream();
Ok(HttpResponse::Ok()

View File

@ -3,6 +3,8 @@ mod auth;
mod handlers;
mod s3_utils;
mod thumbnail;
mod core;
mod overlay;
use actix_web::{middleware::Logger, web, App, HttpServer};
use app_state::AppState;

93
src/overlay.rs Normal file
View File

@ -0,0 +1,93 @@
use std::{error::Error, io::Cursor};
use actix_web::web::Bytes;
use log::warn;
use image::Rgba;
use imageproc::drawing::{draw_text_mut, draw_filled_rect_mut};
use imageproc::rect::Rect;
use ab_glyph::{Font, FontArc, PxScale};
use crate::core::get_shout_by_id;
pub async fn generate_overlay<'a>(shout_id: &'a str, filedata: Bytes) -> Result<Bytes, Box<dyn Error>> {
// Получаем shout из GraphQL
let shout_id_int = shout_id.parse::<i32>().unwrap_or(0);
match get_shout_by_id(shout_id_int).await {
Ok(shout) => {
// Преобразуем Bytes в ImageBuffer
let img = image::load_from_memory(&filedata)?;
let mut img = img.to_rgba8();
// Загружаем шрифт
let font_vec = Vec::from(include_bytes!("Muller-Regular.woff2") as &[u8]);
let font = FontArc::try_from_vec(font_vec).unwrap();
// Получаем размеры изображения
let (img_width, img_height) = img.dimensions();
let max_text_width = (img_width as f32) * 0.8;
let max_text_height = (img_height as f32) * 0.8;
// Начальный масштаб
let mut scale: f32 = 24.0;
let text_length = shout.title.chars().count() as f32;
let mut text_width = scale * text_length;
let text_height = scale;
// Регулируем масштаб, пока текст не впишется в 80% от размеров изображения
while text_width > max_text_width || text_height > max_text_height {
scale -= 1.0;
if scale <= 0.0 {
break;
}
text_width = scale * text_length;
// text_height остается равным scale
}
// Рассчёт позиции текста для центрирования
let x = ((img_width as f32 - text_width) / 2.0).ceil() as i32;
let y = ((img_height as f32 - text_height) / 2.0).ceil() as i32;
// Задаём отступы для подложки
let padding_x = 10;
let padding_y = 5;
// Определяем размеры подложки
let rect_width = text_width.ceil() as u32 + (2 * padding_x);
let rect_height = text_height.ceil() as u32 + (2 * padding_y);
// Определяем координаты подложки
let rect_x = x - padding_x as i32;
let rect_y = y - padding_y as i32;
// Создаём прямоугольник
let rect = Rect::at(rect_x, rect_y).of_size(rect_width, rect_height);
// Задаём цвет подложки (полупрозрачный серый)
let background_color = Rgba([128u8, 128u8, 128u8, 128u8]); // RGBA: серый с прозрачностью 50%
// Рисуем подложку
draw_filled_rect_mut(&mut img, rect, background_color);
// Рисуем текст поверх подложки
let scaled_font = font.as_scaled(scale).font;
draw_text_mut(
&mut img,
Rgba([255u8, 255u8, 255u8, 255u8]), // Белый цвет текста
x,
y,
PxScale::from(scale),
&scaled_font,
&shout.title,
);
// Преобразуем ImageBuffer обратно в Bytes
let mut buffer = Vec::new();
img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)?;
Ok(Bytes::from(buffer))
},
Err(e) => {
warn!("Error getting shout: {}", e);
Ok(filedata)
}
}
}