Implement compression quota refunds and admin manual subscription

This commit is contained in:
2025-12-19 23:28:32 +08:00
commit 11f48fd3dd
106 changed files with 27848 additions and 0 deletions

533
src/services/compress.rs Normal file
View File

@@ -0,0 +1,533 @@
use crate::error::{AppError, ErrorCode};
use crate::state::AppState;
use img_parts::{Bytes as ImgBytes, DynImage, ImageEXIF, ImageICC};
use image::codecs::bmp::BmpEncoder;
use image::codecs::gif::{GifDecoder, GifEncoder};
use image::codecs::ico::IcoEncoder;
use image::codecs::jpeg::JpegEncoder;
use image::codecs::png::PngEncoder;
use image::codecs::tiff::TiffEncoder;
use image::{DynamicImage, ExtendedColorType, ImageEncoder};
use image::{AnimationDecoder, GenericImageView};
use oxipng::StripChunks;
use rgb::FromSlice;
use std::io::Cursor;
#[derive(Debug, Clone, Copy)]
pub enum CompressionLevel {
High,
Medium,
Low,
}
impl CompressionLevel {
pub fn as_str(self) -> &'static str {
match self {
Self::High => "high",
Self::Medium => "medium",
Self::Low => "low",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFmt {
Png,
Jpeg,
Webp,
Avif,
Gif,
Bmp,
Tiff,
Ico,
}
impl ImageFmt {
pub fn as_str(self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg => "jpeg",
Self::Webp => "webp",
Self::Avif => "avif",
Self::Gif => "gif",
Self::Bmp => "bmp",
Self::Tiff => "tiff",
Self::Ico => "ico",
}
}
pub fn extension(self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg => "jpg",
Self::Webp => "webp",
Self::Avif => "avif",
Self::Gif => "gif",
Self::Bmp => "bmp",
Self::Tiff => "tiff",
Self::Ico => "ico",
}
}
pub fn content_type(self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Jpeg => "image/jpeg",
Self::Webp => "image/webp",
Self::Avif => "image/avif",
Self::Gif => "image/gif",
Self::Bmp => "image/bmp",
Self::Tiff => "image/tiff",
Self::Ico => "image/x-icon",
}
}
}
pub fn parse_level(value: &str) -> Result<CompressionLevel, AppError> {
match value.trim().to_ascii_lowercase().as_str() {
"" | "medium" => Ok(CompressionLevel::Medium),
"high" => Ok(CompressionLevel::High),
"low" => Ok(CompressionLevel::Low),
_ => Err(AppError::new(
ErrorCode::InvalidRequest,
"level 仅支持 high/medium/low",
)),
}
}
pub fn parse_compression_rate(value: &str) -> Result<u8, AppError> {
let rate: u8 = value
.trim()
.parse()
.map_err(|_| AppError::new(ErrorCode::InvalidRequest, "compression_rate 需为 1-100 的整数"))?;
if !(1..=100).contains(&rate) {
return Err(AppError::new(
ErrorCode::InvalidRequest,
"compression_rate 需在 1-100 之间",
));
}
Ok(rate)
}
pub fn rate_to_level(rate: u8) -> CompressionLevel {
match rate {
1..=33 => CompressionLevel::Low,
34..=66 => CompressionLevel::Medium,
_ => CompressionLevel::High,
}
}
pub fn parse_output_format(value: &str) -> Result<ImageFmt, AppError> {
match value.trim().to_ascii_lowercase().as_str() {
"png" => Ok(ImageFmt::Png),
"jpeg" | "jpg" => Ok(ImageFmt::Jpeg),
"webp" => Ok(ImageFmt::Webp),
"avif" => Ok(ImageFmt::Avif),
"gif" => Ok(ImageFmt::Gif),
"bmp" => Ok(ImageFmt::Bmp),
"tif" | "tiff" => Ok(ImageFmt::Tiff),
"ico" => Ok(ImageFmt::Ico),
_ => Err(AppError::new(
ErrorCode::InvalidRequest,
"output_format 仅支持 png/jpeg/webp/avif/gif/bmp/tiff/ico",
)),
}
}
pub fn detect_format(bytes: &[u8]) -> Result<ImageFmt, AppError> {
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
return Ok(ImageFmt::Png);
}
if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xD8 {
return Ok(ImageFmt::Jpeg);
}
if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
return Ok(ImageFmt::Webp);
}
if bytes.len() >= 12 && &bytes[4..8] == b"ftyp" {
if bytes[8..12].eq_ignore_ascii_case(b"avif")
|| bytes[8..12].eq_ignore_ascii_case(b"avis")
{
return Ok(ImageFmt::Avif);
}
}
if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
return Ok(ImageFmt::Gif);
}
if bytes.len() >= 2 && bytes[0] == 0x42 && bytes[1] == 0x4D {
return Ok(ImageFmt::Bmp);
}
if bytes.len() >= 4 {
if &bytes[0..4] == b"II*\x00" || &bytes[0..4] == b"MM\x00*" {
return Ok(ImageFmt::Tiff);
}
if bytes[0] == 0x00
&& bytes[1] == 0x00
&& (bytes[2] == 0x01 || bytes[2] == 0x02)
&& bytes[3] == 0x00
{
return Ok(ImageFmt::Ico);
}
}
Err(AppError::new(
ErrorCode::UnsupportedFormat,
"不支持的图片格式",
))
}
pub async fn compress_image_bytes(
state: &AppState,
input: &[u8],
format_in: ImageFmt,
format_out: ImageFmt,
level: CompressionLevel,
compression_rate: Option<u8>,
max_width: Option<u32>,
max_height: Option<u32>,
preserve_metadata: bool,
) -> Result<Vec<u8>, AppError> {
if format_in == ImageFmt::Gif {
if is_animated_gif(input)? {
return Err(AppError::new(
ErrorCode::UnsupportedFormat,
"暂不支持动图 GIF",
));
}
}
let rate = effective_rate(compression_rate, level);
let (icc_profile, exif) = if preserve_metadata {
extract_metadata(input)
} else {
(None, None)
};
let mut resized = false;
let mut output = if format_in == ImageFmt::Png
&& format_out == ImageFmt::Png
&& max_width.is_none()
&& max_height.is_none()
{
let preset = png_preset_from_rate(rate);
let mut opts = oxipng::Options::from_preset(preset);
if !preserve_metadata {
opts.strip = StripChunks::Safe;
}
oxipng::optimize_from_memory(input, &opts)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "PNG 压缩失败").with_source(err))?
} else {
let image = image::load_from_memory(input)
.map_err(|err| AppError::new(ErrorCode::InvalidImage, "图片解码失败").with_source(err))?;
enforce_pixel_limit(state, &image)?;
let (image, did_resize) = resize_if_needed(image, max_width, max_height);
resized = did_resize;
match format_out {
ImageFmt::Png => encode_png(image, rate, preserve_metadata)?,
ImageFmt::Jpeg => encode_jpeg(image, rate)?,
ImageFmt::Webp => encode_webp(image, rate)?,
ImageFmt::Avif => encode_avif(image, rate)?,
ImageFmt::Gif => encode_gif(image, rate)?,
ImageFmt::Bmp => encode_bmp(image)?,
ImageFmt::Tiff => encode_tiff(image)?,
ImageFmt::Ico => encode_ico(image)?,
}
};
if preserve_metadata {
output = apply_metadata(output, icc_profile, exif)?;
}
if !resized && output.len() >= input.len() {
if preserve_metadata {
return Ok(input.to_vec());
}
let stripped = strip_metadata(input).unwrap_or_else(|_| input.to_vec());
return Ok(if stripped.len() <= input.len() {
stripped
} else {
input.to_vec()
});
}
Ok(output)
}
fn enforce_pixel_limit(state: &AppState, image: &DynamicImage) -> Result<(), AppError> {
let (w, h) = image.dimensions();
let pixels = (w as u64).saturating_mul(h as u64);
if pixels > state.config.max_image_pixels {
return Err(AppError::new(
ErrorCode::TooManyPixels,
format!("图片像素过大({}x{}", w, h),
));
}
Ok(())
}
fn resize_if_needed(
image: DynamicImage,
max_width: Option<u32>,
max_height: Option<u32>,
) -> (DynamicImage, bool) {
if max_width.is_none() && max_height.is_none() {
return (image, false);
}
let (w, h) = image.dimensions();
let (target_w, target_h) = fit_within(w, h, max_width, max_height);
if target_w == w && target_h == h {
return (image, false);
}
(
image.resize(target_w, target_h, image::imageops::FilterType::Lanczos3),
true,
)
}
fn fit_within(w: u32, h: u32, max_width: Option<u32>, max_height: Option<u32>) -> (u32, u32) {
let mut scale = 1.0_f64;
if let Some(mw) = max_width.filter(|v| *v > 0) {
scale = scale.min(mw as f64 / w as f64);
}
if let Some(mh) = max_height.filter(|v| *v > 0) {
scale = scale.min(mh as f64 / h as f64);
}
if scale >= 1.0 {
return (w, h);
}
let nw = (w as f64 * scale).round().max(1.0) as u32;
let nh = (h as f64 * scale).round().max(1.0) as u32;
(nw, nh)
}
fn encode_png(
image: DynamicImage,
rate: u8,
preserve_metadata: bool,
) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let mut out = Vec::new();
let encoder = PngEncoder::new(&mut out);
encoder
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "PNG 编码失败").with_source(err))?;
let preset = png_preset_from_rate(rate);
let mut opts = oxipng::Options::from_preset(preset);
if !preserve_metadata {
opts.strip = StripChunks::Safe;
}
oxipng::optimize_from_memory(&out, &opts)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "PNG 优化失败").with_source(err))
}
fn encode_jpeg(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
let rgb = image.to_rgb8();
let (w, h) = rgb.dimensions();
let mut out = Vec::new();
let quality = jpeg_quality_from_rate(rate);
let mut encoder = JpegEncoder::new_with_quality(&mut out, quality);
encoder
.encode(rgb.as_raw(), w, h, ExtendedColorType::Rgb8)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "JPEG 编码失败").with_source(err))?;
Ok(out)
}
fn encode_webp(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let encoder = webp::Encoder::from_rgba(rgba.as_raw(), w, h);
let bytes = if rate <= 10 {
encoder.encode_lossless()
} else {
encoder.encode(webp_quality_from_rate(rate))
};
Ok(bytes.to_vec())
}
fn encode_avif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let quality = avif_quality_from_rate(rate);
let raw = rgba.into_raw();
let pixels = raw.as_rgba();
let img = ravif::Img::new(pixels, w as usize, h as usize);
let encoder = ravif::Encoder::new().with_quality(quality);
let encoded = encoder
.encode_rgba(img)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "AVIF 编码失败").with_source(err))?;
Ok(encoded.avif_file)
}
fn encode_gif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let mut out = Vec::new();
let speed = gif_speed_from_rate(rate);
{
let mut encoder = GifEncoder::new_with_speed(&mut out, speed);
encoder
.encode(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "GIF 编码失败").with_source(err))?;
}
Ok(out)
}
fn encode_bmp(image: DynamicImage) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let mut out = Vec::new();
let encoder = BmpEncoder::new(&mut out);
encoder
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "BMP 编码失败").with_source(err))?;
Ok(out)
}
fn encode_tiff(image: DynamicImage) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let mut out = Cursor::new(Vec::new());
let encoder = TiffEncoder::new(&mut out);
encoder
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "TIFF 编码失败").with_source(err))?;
Ok(out.into_inner())
}
fn encode_ico(image: DynamicImage) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let mut out = Vec::new();
let encoder = IcoEncoder::new(&mut out);
encoder
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "ICO 编码失败").with_source(err))?;
Ok(out)
}
fn extract_metadata(input: &[u8]) -> (Option<ImgBytes>, Option<ImgBytes>) {
let bytes = ImgBytes::copy_from_slice(input);
match DynImage::from_bytes(bytes) {
Ok(Some(img)) => (img.icc_profile(), img.exif()),
_ => (None, None),
}
}
fn apply_metadata(
output: Vec<u8>,
icc_profile: Option<ImgBytes>,
exif: Option<ImgBytes>,
) -> Result<Vec<u8>, AppError> {
if icc_profile.is_none() && exif.is_none() {
return Ok(output);
}
let out_bytes = ImgBytes::from(output);
let dyn_img = DynImage::from_bytes(out_bytes.clone())
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "解析输出图片元数据失败").with_source(err))?;
let Some(mut img) = dyn_img else {
return Ok(out_bytes.to_vec());
};
img.set_icc_profile(icc_profile);
img.set_exif(exif);
let mut buf = Vec::new();
img.encoder()
.write_to(&mut buf)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "写入图片元数据失败").with_source(err))?;
Ok(buf)
}
fn strip_metadata(input: &[u8]) -> Result<Vec<u8>, AppError> {
let bytes = ImgBytes::copy_from_slice(input);
let dyn_img = DynImage::from_bytes(bytes.clone())
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "解析图片元数据失败").with_source(err))?;
let Some(mut img) = dyn_img else {
return Ok(bytes.to_vec());
};
img.set_icc_profile(None);
img.set_exif(None);
let mut buf = Vec::new();
img.encoder()
.write_to(&mut buf)
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "写入图片元数据失败").with_source(err))?;
Ok(buf)
}
fn effective_rate(rate: Option<u8>, level: CompressionLevel) -> u8 {
match rate {
Some(value) => value.clamp(1, 100),
None => match level {
CompressionLevel::Low => 25,
CompressionLevel::Medium => 55,
CompressionLevel::High => 80,
},
}
}
fn png_preset_from_rate(rate: u8) -> u8 {
(((rate.saturating_sub(1)) as f32 / 99.0) * 6.0).round() as u8
}
fn jpeg_quality_from_rate(rate: u8) -> u8 {
quality_from_rate(rate, 35, 95)
}
fn webp_quality_from_rate(rate: u8) -> f32 {
quality_from_rate(rate, 40, 92) as f32
}
fn avif_quality_from_rate(rate: u8) -> f32 {
quality_from_rate(rate, 35, 90) as f32
}
fn quality_from_rate(rate: u8, min_quality: u8, max_quality: u8) -> u8 {
let min_q = min_quality as i32;
let max_q = max_quality as i32;
let rate = rate.clamp(1, 100) as i32;
let span = max_q - min_q;
let q = max_q - ((rate - 1) * span / 99);
q.clamp(min_q, max_q) as u8
}
fn gif_speed_from_rate(rate: u8) -> i32 {
let rate = rate.clamp(1, 100) as i32;
1 + ((rate - 1) * 29 / 99)
}
fn is_animated_gif(input: &[u8]) -> Result<bool, AppError> {
let decoder = GifDecoder::new(Cursor::new(input))
.map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?;
let mut frames = decoder.into_frames().into_iter();
if let Some(frame) = frames.next() {
frame.map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?;
}
if let Some(frame) = frames.next() {
frame.map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?;
return Ok(true);
}
Ok(false)
}