-
Notifications
You must be signed in to change notification settings - Fork 2
Process animated sources frame by frame #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ use bytes::Bytes; | |
| use libvips::VipsImage; | ||
| use std::time::Instant; | ||
| use thiserror::Error; | ||
| use tracing::debug; | ||
| use tracing::{debug, warn}; | ||
|
|
||
| pub use scale_on_load::{load_scale_factor, load_shrink_factor, thumbnail_covers}; | ||
|
|
||
|
|
@@ -94,6 +94,18 @@ pub fn process_image( | |
| let frames = animation::split(&img)?; | ||
| enforce_frame_limit(&parsed_options, &frames, source_bytes)?; | ||
|
|
||
| // Trim measures one image's borders, and an animation is not one image. Run | ||
| // per frame it finds a different region in each — a subject that grows across | ||
| // the animation trims to a different width every frame — and no animated | ||
| // container can hold that, because the frames share a single canvas. The | ||
| // join would refuse them and the whole request would fail on an option that | ||
| // is otherwise perfectly reasonable. imgproxy draws the same conclusion and | ||
| // drops the option with a warning rather than failing, so this matches it. | ||
| if parsed_options.trim.is_some() && frames.images.len() > 1 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an animated source is collapsed to one loaded frame—such as with Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional, and matching imgproxy: upstream computes |
||
| warn!("Trim is not supported for animated images; ignoring it for this request"); | ||
| parsed_options.trim = None; | ||
| } | ||
|
|
||
| let processed = frames | ||
| .images | ||
| .into_iter() | ||
|
|
@@ -105,6 +117,26 @@ pub fn process_image( | |
| }) | ||
| .collect::<Result<Vec<_>, ProcessingError>>()?; | ||
|
|
||
| // Both ceilings below describe a frame the caller sees. Applying them to | ||
| // the joined stack measured frame height times frame count, so a ten-frame | ||
| // 100x100 animation failed a 500px limit and a tall stack was needlessly | ||
| // downscaled — with the page height passed to the encoder left describing | ||
| // the frames from before that scaling, which misdivides them. | ||
| // | ||
| // The configured ceiling is checked first: it is policy, not fitting. A | ||
| // result over `max_result_dimension` is refused, and letting the encoder | ||
| // limit quietly scale it down first turned that refusal into acceptance — | ||
| // a 20,000px result under an 18,000px ceiling came back as 16,383px | ||
| // instead of the documented 400. | ||
| if let Some(frame) = processed.first() { | ||
| enforce_result_dimension(&parsed_options, frame)?; | ||
| } | ||
|
|
||
| let processed = processed | ||
| .into_iter() | ||
| .map(|frame| fit_within_format_limits(frame, &output_format, parsed_options.resizing_algorithm.as_deref())) | ||
| .collect::<Result<Vec<_>, ProcessingError>>()?; | ||
|
|
||
| let (mut img, page_height) = animation::join(processed)?; | ||
|
|
||
| img = colorspace::to_result(img, save::format_supports_color_profile(&output_format))?; | ||
|
|
@@ -119,8 +151,6 @@ pub fn process_image( | |
| } | ||
| } | ||
|
|
||
| enforce_result_dimension(&parsed_options, &img)?; | ||
|
|
||
| let quality = parsed_options | ||
| .quality | ||
| .or_else(|| parsed_options.save.format_quality.get(&output_format).copied()) | ||
|
|
@@ -177,6 +207,44 @@ fn apply_dpr(parsed_options: &mut ParsedOptions) { | |
| } | ||
| } | ||
|
|
||
| /// Scales a result down to what the output container can address. | ||
| /// | ||
| /// WebP cannot represent a side over 16383 and the HEIF family stops at 16384. | ||
| /// Handing the encoder something larger fails at the very end of the pipeline, | ||
| /// after all the work is done, with a message about the codec rather than about | ||
| /// the size — so a request that is merely too big for its chosen format looks | ||
| /// like a server fault. imgproxy rescales here for the same reason. | ||
| fn fit_within_format_limits( | ||
| img: VipsImage, | ||
| output_format: &str, | ||
| resizing_algorithm: Option<&str>, | ||
| ) -> Result<VipsImage, ProcessingError> { | ||
| let Some(limit) = save::format_max_dimension(output_format) else { | ||
| return Ok(img); | ||
| }; | ||
|
|
||
| let largest = img.get_width().max(img.get_height()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For animated WebP, AVIF, HEIF, or GIF output, Useful? React with 👍 / 👎. |
||
| let Ok(largest) = u32::try_from(largest) else { | ||
| return Ok(img); | ||
| }; | ||
| if largest <= limit { | ||
| return Ok(img); | ||
|
Comment on lines
+230
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a JPEG result has a side between 65,501 and 65,535 pixels, Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| let scale = f64::from(limit) / f64::from(largest); | ||
| debug!( | ||
| "Rescaling by {:.4} so a {}px result fits the {} limit of {}px", | ||
| scale, largest, output_format, limit | ||
| ); | ||
| Ok(transform::resize_with_algorithm( | ||
| &img, | ||
| scale, | ||
| None, | ||
| resizing_algorithm, | ||
| "Error fitting the result to the output format", | ||
| )?) | ||
| } | ||
|
|
||
| /// Rejects an animation whose individual frames are too large. | ||
| /// | ||
| /// The source-resolution limit measures the whole stack, which for an animation | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a per-frame transform such as
trimremoves different left/right borders from individual frames while leaving their heights equal, this new width check rejects the otherwise valid animation. Sinceprocess_imagenow runs the pipeline independently for every frame, animated requests with changing horizontal content can routinely produce this geometry; normalize the processed frames to a shared canvas (or derive one common trim region) instead of returning an animation error.Useful? React with 👍 / 👎.