fix #395 Re-encode JPEG/WEBP/PNG if it contains EXIF metadata or size is large

This commit is contained in:
GitHub
2025-12-29 14:19:18 +08:00
parent 03d76adf2a
commit 943bc03a56
3 changed files with 63 additions and 157 deletions

76
Cargo.lock generated
View File

@@ -74,12 +74,6 @@ dependencies = [
"rustversion",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "askama"
version = "0.15.1"
@@ -809,12 +803,6 @@ dependencies = [
"dtoa",
]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.15.0"
@@ -981,14 +969,13 @@ dependencies = [
"http",
"identicon",
"image",
"img-parts",
"include_dir",
"indexmap",
"infer",
"jieba-rs",
"jiff",
"kamadak-exif",
"latex2mathml",
"mozjpeg",
"nanoid",
"pulldown-cmark",
"rand 0.9.2",
@@ -1524,17 +1511,6 @@ dependencies = [
"quick-error",
]
[[package]]
name = "img-parts"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19734e3c43b2a850f5889c077056e47c874095f2d87e853c7c41214ae67375f0"
dependencies = [
"bytes",
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "include-flate"
version = "0.3.1"
@@ -1726,6 +1702,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kamadak-exif"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837"
dependencies = [
"mutate_once",
]
[[package]]
name = "latex2mathml"
version = "0.2.3"
@@ -1999,31 +1984,6 @@ dependencies = [
"pxfm",
]
[[package]]
name = "mozjpeg"
version = "0.10.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7891b80aaa86097d38d276eb98b3805d6280708c4e0a1e6f6aed9380c51fec9"
dependencies = [
"arrayvec",
"bytemuck",
"libc",
"mozjpeg-sys",
"rgb",
]
[[package]]
name = "mozjpeg-sys"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f0dc668bf9bf888c88e2fb1ab16a406d2c380f1d082b20d51dd540ab2aa70c1"
dependencies = [
"cc",
"dunce",
"libc",
"nasm-rs",
]
[[package]]
name = "multer"
version = "3.1.0"
@@ -2047,6 +2007,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b"
[[package]]
name = "mutate_once"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af"
[[package]]
name = "nanoid"
version = "0.4.0"
@@ -2056,16 +2022,6 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "nasm-rs"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34f676553b60ccbb76f41f9ae8f2428dac3f259ff8f1c2468a174778d06a1af9"
dependencies = [
"jobserver",
"log",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"

View File

@@ -36,14 +36,13 @@ image = { version = "0.25.9", default-features = false, features = [
"gif",
"webp"
] }
img-parts = "0.4.0"
include_dir = "0.7.4"
indexmap = "2"
infer = "0.19.0"
jieba-rs = "0.8.1"
jiff = { version = "0.2.15", default-features = false, features = ["std"] }
kamadak-exif = "0.6.1"
latex2mathml = "0.2.3"
mozjpeg = "0.10.13"
nanoid = "0.4.0"
pulldown-cmark = { version = "0.13.0", features = [
"simd",

View File

@@ -19,12 +19,10 @@ use axum_extra::{
headers::{Cookie, Referer},
};
use data_encoding::HEXLOWER;
use image::{ImageFormat, imageops::FilterType};
use img_parts::{DynImage, ImageEXIF};
use mozjpeg::{ColorSpace, Compress, ScanMode};
use image::{ImageEncoder, ImageFormat, ImageReader, codecs::jpeg::JpegEncoder};
use ring::digest::{Context, SHA1_FOR_LEGACY_USE_ONLY};
use serde::Deserialize;
use std::io::Cursor;
use tokio::fs::{self, remove_file};
use tracing::error;
@@ -239,7 +237,11 @@ pub(crate) async fn upload_post(
let user_uploads = DB
.inner()
.open_partition("user_uploads", Default::default())?;
while let Some(field) = multipart.next_field().await.unwrap() {
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| AppError::Custom(e.to_string()))?
{
if imgs.len() > 10 {
break;
}
@@ -253,65 +255,38 @@ pub(crate) async fn upload_post(
};
let image_format_detected = image::guess_format(&data)?;
let ext;
let img_data = match image_format_detected {
ImageFormat::Png | ImageFormat::Jpeg | ImageFormat::WebP => {
if let Ok(Some(mut img)) = DynImage::from_bytes(data) {
img.set_exif(None);
let img_noexif = img.encoder().bytes();
ImageFormat::Jpeg | ImageFormat::WebP | ImageFormat::Png => {
// Re-encode JPEG/WEBP/PNG if it contains EXIF metadata or size is large
let mut re_encode = false;
// author: "Kim tae hyeon <kimth0734@gmail.com>"
// https://github.com/altair823/image_compressor/blob/main/src/compressor.rs
// license = "MIT"
let dyn_img =
image::load_from_memory_with_format(&img_noexif, image_format_detected)?;
let factor = Factor::get(img_noexif.len());
// resize
let width = (dyn_img.width() as f32 * factor.size_ratio) as u32;
let height = (dyn_img.width() as f32 * factor.size_ratio) as u32;
let resized_img = dyn_img.resize(width, height, FilterType::Lanczos3);
// compress
let mut comp = Compress::new(ColorSpace::JCS_RGB);
comp.set_scan_optimization_mode(ScanMode::Auto);
comp.set_quality(factor.quality);
let target_width = resized_img.width() as usize;
let target_height = resized_img.height() as usize;
comp.set_size(target_width, target_height);
comp.set_optimize_scans(true);
let mut comp = comp.start_compress(Vec::new()).unwrap();
let mut line: usize = 0;
let resized_img_data = resized_img.into_rgb8().into_vec();
loop {
if line > target_height - 1 {
break;
}
let idx = line * target_width * 3..(line + 1) * target_width * 3;
comp.write_scanlines(&resized_img_data[idx]).unwrap();
line += 1;
let exifreader = exif::Reader::new();
if let Ok(exif) = exifreader.read_from_container(&mut Cursor::new(&data)) {
if !exif.buf().is_empty() {
re_encode = true;
}
}
if let Ok(comp) = comp.finish() {
ext = *image_format_detected
.extensions_str()
.get(0)
.unwrap_or_else(|| &"jpeg");
comp
} else {
continue;
}
let quality = Quality::get(data.len());
if quality < 100 {
re_encode = true;
}
if re_encode {
let dyn_img = ImageReader::new(std::io::Cursor::new(&data))
.with_guessed_format()?
.decode()?;
let mut writer = Vec::new();
let mut encoder = JpegEncoder::new_with_quality(&mut writer, quality);
encoder.set_exif_metadata(vec![]).unwrap();
dyn_img.write_with_encoder(encoder)?;
writer
} else {
continue;
data.to_vec()
}
}
ImageFormat::Gif => {
ext = "gif";
data.to_vec()
}
ImageFormat::Gif => data.to_vec(),
_ => {
continue;
}
@@ -321,6 +296,10 @@ pub(crate) async fn upload_post(
context.update(&img_data);
let digest = context.finish();
let sha1 = HEXLOWER.encode(digest.as_ref());
let ext = *image_format_detected
.extensions_str()
.get(0)
.unwrap_or_else(|| &"jpg");
let fname = format!("{}.{}", &sha1[0..20], ext);
let location = format!("{}/{}", &CONFIG.upload_path, fname);
@@ -347,46 +326,18 @@ pub(crate) async fn upload_post(
}
#[derive(Copy, Clone)]
struct Factor {
/// Quality of the new compressed image.
/// Values range from 0 to 100 in float.
quality: f32,
struct Quality;
/// Ratio for resize the new compressed image.
/// Values range from 0 to 1 in float.
size_ratio: f32,
}
impl Factor {
/// Create a new `Factor` instance.
/// The `quality` range from 0 to 100 in float,
/// and `size_ratio` range from 0 to 1 in float.
///
/// # Panics
///
/// - If the quality value is 0 or less.
/// - If the quality value exceeds 100.
/// - If the size ratio value is 0 or less.
/// - If the size ratio value exceeds 1.
fn new(quality: f32, size_ratio: f32) -> Self {
if (quality > 0. && quality <= 100.) && (size_ratio > 0. && size_ratio <= 1.) {
Self {
quality,
size_ratio,
}
} else {
panic!("Wrong Factor argument!");
}
}
fn get(file_size: usize) -> Factor {
impl Quality {
fn get(file_size: usize) -> u8 {
match file_size {
file_size if file_size > 5000000 => Factor::new(70., 0.75),
file_size if file_size > 1000000 => Factor::new(75., 0.8),
file_size if file_size > 600000 => Factor::new(80., 0.85),
file_size if file_size > 400000 => Factor::new(85., 0.9),
file_size if file_size > 200000 => Factor::new(90., 0.95),
_ => Factor::new(100., 1.0),
file_size if file_size > 5000000 => 70,
file_size if file_size > 1500000 => 75,
file_size if file_size > 1000000 => 80,
file_size if file_size > 800000 => 85,
file_size if file_size > 600000 => 90,
file_size if file_size > 400000 => 95,
_ => 100,
}
}
}