0.1.0-overlay
This commit is contained in:
parent
0d4f21c79b
commit
6e90529420
594
Cargo.lock
generated
594
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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
BIN
src/Muller-Regular.woff2
Normal file
Binary file not shown.
72
src/core.rs
Normal file
72
src/core.rs
Normal 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(),
|
||||
)))
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
93
src/overlay.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user