add: lossy encode webp images
This commit is contained in:
parent
03480d32db
commit
24162573ee
8 changed files with 145 additions and 437 deletions
|
@ -15,7 +15,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
|||
tera = "1.20.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tower-http = { version = "0.6.2", features = ["trace", "fs", "catch-panic"] }
|
||||
tower-http = { version = "0.6.4", features = ["trace", "fs", "catch-panic"] }
|
||||
axum = { version = "0.8.4", features = ["macros", "ws"] }
|
||||
tokio = { version = "1.45.0", features = ["macros", "rt-multi-thread"] }
|
||||
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
||||
|
@ -47,3 +47,4 @@ async-stripe = { version = "0.41.0", features = [
|
|||
"runtime-tokio-hyper",
|
||||
] }
|
||||
emojis = "0.6.4"
|
||||
webp = "0.3.0"
|
||||
|
|
|
@ -57,31 +57,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Create an image buffer given an input of `bytes`
|
||||
pub fn save_buffer(path: &str, bytes: Vec<u8>, format: image::ImageFormat) -> std::io::Result<()> {
|
||||
let pre_img_buffer = match image::load_from_memory(&bytes) {
|
||||
Ok(i) => i,
|
||||
Err(_) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Image failed",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let file = File::create(path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
if pre_img_buffer.write_to(&mut writer, format).is_err() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Image conversion failed",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A file extractor accepting:
|
||||
/// * `multipart/form-data`
|
||||
///
|
||||
|
@ -163,3 +138,64 @@ where
|
|||
Ok(Self(body, json))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an image buffer given an input of `bytes`.
|
||||
pub fn save_buffer(path: &str, bytes: Vec<u8>, format: image::ImageFormat) -> std::io::Result<()> {
|
||||
let pre_img_buffer = match image::load_from_memory(&bytes) {
|
||||
Ok(i) => i,
|
||||
Err(_) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Image failed",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let file = File::create(path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
if pre_img_buffer.write_to(&mut writer, format).is_err() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Image conversion failed",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const WEBP_ENCODE_QUALITY: f32 = 85.0;
|
||||
|
||||
/// Create a WEBP image buffer given an input of `bytes`.
|
||||
pub fn save_webp_buffer(path: &str, bytes: Vec<u8>) -> std::io::Result<()> {
|
||||
let img = match image::load_from_memory(&bytes) {
|
||||
Ok(i) => i,
|
||||
Err(_) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Image failed",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let encoder = match webp::Encoder::from_image(&img) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mem = encoder.encode(WEBP_ENCODE_QUALITY);
|
||||
|
||||
if std::fs::write(path, &*mem).is_err() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Image conversion failed",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use axum::{extract::Path, response::IntoResponse, Extension, Json};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use image::ImageFormat;
|
||||
use tetratto_core::model::{
|
||||
communities::Post,
|
||||
permissions::FinePermission,
|
||||
|
@ -9,7 +8,7 @@ use tetratto_core::model::{
|
|||
};
|
||||
use crate::{
|
||||
get_user_from_token,
|
||||
image::{save_buffer, JsonMultipart},
|
||||
image::{save_webp_buffer, JsonMultipart},
|
||||
routes::api::v1::{CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext},
|
||||
State,
|
||||
};
|
||||
|
@ -109,11 +108,8 @@ pub async fn create_request(
|
|||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
if let Err(e) = save_buffer(
|
||||
&upload.path(&data.0).to_string(),
|
||||
image.to_vec(),
|
||||
ImageFormat::WebP,
|
||||
) {
|
||||
if let Err(e) = save_webp_buffer(&upload.path(&data.0).to_string(), image.to_vec())
|
||||
{
|
||||
return Json(Error::MiscError(e.to_string()).into());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ tetratto-l10n = { path = "../l10n" }
|
|||
serde_json = "1.0.140"
|
||||
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
bitflags = "2.9.0"
|
||||
bitflags = "2.9.1"
|
||||
async-recursion = "1.1.1"
|
||||
md-5 = "0.10.6"
|
||||
base16ct = { version = "0.2.0", features = ["alloc"] }
|
||||
|
|
|
@ -13,11 +13,11 @@ use crate::model::{
|
|||
permissions::FinePermission,
|
||||
};
|
||||
use crate::{auto_method, execute, get, query_row, query_rows, params};
|
||||
use tetratto_shared::unix_epoch_timestamp;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
||||
use tetratto_shared::unix_epoch_timestamp;
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::Row;
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ license.workspace = true
|
|||
[dependencies]
|
||||
ammonia = "4.1.0"
|
||||
chrono = "0.4.41"
|
||||
comrak = "0.39.0"
|
||||
markdown = "1.0.0"
|
||||
hex_fmt = "0.3.0"
|
||||
rand = "0.9.1"
|
||||
serde = "1.0.219"
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
use ammonia::Builder;
|
||||
use comrak::{Options, markdown_to_html};
|
||||
use markdown::{to_html_with_options, Options, CompileOptions, ParseOptions};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Render markdown input into HTML
|
||||
pub fn render_markdown(input: &str) -> String {
|
||||
let mut options = Options::default();
|
||||
let options = Options {
|
||||
compile: CompileOptions {
|
||||
allow_any_img_src: false,
|
||||
allow_dangerous_html: true,
|
||||
gfm_task_list_item_checkable: false,
|
||||
gfm_tagfilter: false,
|
||||
..Default::default()
|
||||
},
|
||||
parse: ParseOptions {
|
||||
gfm_strikethrough_single_tilde: false,
|
||||
math_text_single_dollar: false,
|
||||
mdx_expression_parse: None,
|
||||
mdx_esm_parse: None,
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
options.extension.table = true;
|
||||
options.extension.superscript = true;
|
||||
options.extension.strikethrough = true;
|
||||
options.extension.autolink = true;
|
||||
options.extension.header_ids = Option::Some(String::new());
|
||||
// options.extension.tagfilter = true;
|
||||
options.render.unsafe_ = true;
|
||||
// options.render.escape = true;
|
||||
options.parse.smart = false;
|
||||
|
||||
let html = markdown_to_html(input, &options);
|
||||
let html = match to_html_with_options(input, &options) {
|
||||
Ok(h) => h,
|
||||
Err(e) => e.to_string(),
|
||||
};
|
||||
|
||||
let mut allowed_attributes = HashSet::new();
|
||||
allowed_attributes.insert("id");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue