mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-14 22:48:38 +09:00
fix(media-proxy): bound animated transforms by output pixels (#3402)
This commit is contained in:
1 parent
50c28aa8fa
commit
5a2d81ebbb
8 files changed
+72
-10
No files matched your search
@@ -16,7 +16,7 @@ pub const MAX_ANIMATED_FRAMES_DEFAULT: u32 = 20_000;
|
||||
pub const MAX_ANIMATED_TOTAL_PIXELS_DEFAULT: usize = 4 * MAX_MEDIA_IMAGE_PIXELS_DEFAULT;
|
||||
pub const MAX_JXL_DECODE_PIXELS: usize = 4096 * 4096;
|
||||
pub const MAX_STATIC_OUTPUT_PIXELS: usize = 6144 * 4096;
|
||||
pub const MAX_GIF_RESIZE_OUTPUT_PIXELS: usize = 16 << 20;
|
||||
pub const MAX_ANIMATED_OUTPUT_PIXELS: usize = 16 << 20;
|
||||
pub const SVG_RASTER_MAX_DIMENSION: u32 = 4096;
|
||||
pub const LARGE_SOURCE_LOG_PIXELS: usize = 4096 * 4096;
|
||||
const _: () = assert!(MAX_ANIMATED_FRAMES_DEFAULT >= 20_000);
|
||||
|
||||
@@ -182,7 +182,7 @@ impl GifResizeDims {
|
||||
(self.width as usize)
|
||||
.saturating_mul(self.height as usize)
|
||||
.saturating_mul(frames.max(1) as usize)
|
||||
<= constants::MAX_GIF_RESIZE_OUTPUT_PIXELS
|
||||
<= constants::MAX_ANIMATED_OUTPUT_PIXELS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ use super::image_probe::{
|
||||
};
|
||||
use super::loaded_image::{
|
||||
normalize_vips_image_to_uchar, page_height, resize_loaded_image, resize_loaded_image_by_scale,
|
||||
validate_dimensions_u32, validate_jxl_decode_pixels, validate_static_output_pixels,
|
||||
validate_vips_image,
|
||||
static_output_pixels, validate_dimensions_u32, validate_jxl_decode_pixels,
|
||||
validate_static_output_pixels, validate_vips_image,
|
||||
};
|
||||
use super::native_runtime::{clear_vips_error, ensure_vips_init, last_vips_error};
|
||||
use super::transform_plan::{
|
||||
@@ -128,6 +128,31 @@ fn ensure_decode_cost_admissible(
|
||||
admitted
|
||||
}
|
||||
|
||||
fn ensure_animated_output_admissible(
|
||||
input: &[u8],
|
||||
options: &ImageOptions,
|
||||
media_limits: &MediaLimits,
|
||||
) -> Result<(), MediaError> {
|
||||
let dims = probe_image_dims(media_limits, input)?;
|
||||
if dims.pages <= 1 {
|
||||
return Ok(());
|
||||
}
|
||||
let output_pixels = static_output_pixels(options, dims).saturating_mul(dims.pages as usize);
|
||||
if output_pixels <= constants::MAX_ANIMATED_OUTPUT_PIXELS {
|
||||
return Ok(());
|
||||
}
|
||||
tracing::warn!(
|
||||
input_bytes = input.len(),
|
||||
width = dims.width,
|
||||
height = dims.height,
|
||||
pages = dims.pages,
|
||||
out_width = options.width.unwrap_or(0),
|
||||
out_height = options.height.unwrap_or(0),
|
||||
"animated transform refused by the output work budget"
|
||||
);
|
||||
Err(MediaError::TooLargeToProcess)
|
||||
}
|
||||
|
||||
pub fn transform_image(
|
||||
input: &[u8],
|
||||
options: &ImageOptions,
|
||||
@@ -184,6 +209,9 @@ pub fn transform_image(
|
||||
content_type: "image/gif",
|
||||
});
|
||||
}
|
||||
if animated {
|
||||
ensure_animated_output_admissible(input, options, media_limits)?;
|
||||
}
|
||||
if should_transform_animated_webp_direct(sniffed, options, format) {
|
||||
validate_dimensions_u32(media_limits, sniffed.width, sniffed.height)?;
|
||||
let resolved = resolve_auto_quality(
|
||||
|
||||
@@ -574,8 +574,8 @@ async fn serve_image_transform(
|
||||
requested_download: transform.requested_download,
|
||||
});
|
||||
}
|
||||
let can_serve_original = transform.width.is_none()
|
||||
&& transform.height.is_none()
|
||||
let can_serve_original = ((transform.width.is_none() && transform.height.is_none())
|
||||
|| options.is_animated())
|
||||
&& is_browser_displayable_image(sniffed.mime);
|
||||
let mut too_large_original = None;
|
||||
let leader_original = &mut too_large_original;
|
||||
|
||||
@@ -325,3 +325,35 @@ fn a_cache_larger_than_the_pod_takes_at_most_half_of_it() {
|
||||
fn the_budget_never_drops_below_its_floor() {
|
||||
assert_eq!(128 << 20, native_memory_budget_bytes(64 << 20, 0));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_animated_transform_over_the_work_budget_serves_the_original() {
|
||||
let webp = crate::test_fixtures::ANIMATED_WEBP_1024_17_FRAMES;
|
||||
let response = attachment_transform(
|
||||
webp,
|
||||
"anim.webp",
|
||||
&[("format", "webp"), ("animated", "true"), ("width", "1000")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
assert_eq!(webp, body.as_ref());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_animated_transform_within_the_work_budget_is_transformed() {
|
||||
let webp = crate::test_fixtures::ANIMATED_WEBP_1024_17_FRAMES;
|
||||
let response = attachment_transform(
|
||||
webp,
|
||||
"anim.webp",
|
||||
&[("format", "webp"), ("animated", "true"), ("width", "128")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
assert_ne!(webp, body.as_ref());
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -32,6 +32,7 @@ pub fn apng_header(frames: u32) -> Vec<u8> {
|
||||
|
||||
pub const JXL_4100_LOSSY_ALPHA: &[u8] = include_bytes!("jxl_4100_lossy_alpha.jxl");
|
||||
pub const JXL_64_LOSSY_ALPHA: &[u8] = include_bytes!("jxl_64_lossy_alpha.jxl");
|
||||
pub const ANIMATED_WEBP_1024_17_FRAMES: &[u8] = include_bytes!("animated_webp_1024_17_frames.webp");
|
||||
pub const JPEG_ROTATED_MISMATCHED_SCAN: &[u8] = include_bytes!("jpeg_rotated_mismatched_scan.jpg");
|
||||
|
||||
pub fn synthetic_png(width: u32, height: u32) -> Vec<u8> {
|
||||
|
||||
@@ -10,10 +10,11 @@ pub use adversarial::{
|
||||
};
|
||||
pub use ffmpeg_cli::{ffmpeg_gen_media, ffmpeg_gen_mp4, ffmpeg_gen_rotated_mp4, ffmpeg_mirror_mp4};
|
||||
pub use images::{
|
||||
JPEG_ROTATED_MISMATCHED_SCAN, JXL_64_LOSSY_ALPHA, JXL_4100_LOSSY_ALPHA, animated_gif_fixture,
|
||||
animated_gif_frames, apng_header, first_webp_anim_frame_size, gif_frame_delays_cs,
|
||||
gif_loop_count, minimal_gif, png_dimensions, synthetic_bmp, synthetic_png,
|
||||
webp_animation_loop_count, webp_canvas_size, webp_chunk_payloads, webp_with_metadata_chunk,
|
||||
ANIMATED_WEBP_1024_17_FRAMES, JPEG_ROTATED_MISMATCHED_SCAN, JXL_64_LOSSY_ALPHA,
|
||||
JXL_4100_LOSSY_ALPHA, animated_gif_fixture, animated_gif_frames, apng_header,
|
||||
first_webp_anim_frame_size, gif_frame_delays_cs, gif_loop_count, minimal_gif, png_dimensions,
|
||||
synthetic_bmp, synthetic_png, webp_animation_loop_count, webp_canvas_size, webp_chunk_payloads,
|
||||
webp_with_metadata_chunk,
|
||||
};
|
||||
pub use media::{
|
||||
fixture_audio_mp3_with_png_cover_art, fixture_audio_mp4_with_attached_picture,
|
||||
|
||||
Reference in new issue
Block a user