// SPDX-License-Identifier: AGPL-3.0-or-later use super::image_probe::animated_probe_from_image; use super::loaded_image::validate_dimensions; use super::native_runtime::{clear_vips_error, vips_runtime}; use super::{ MediaError, native_animated_frame_limit, native_buffer_to_media_bytes, native_status_error, }; use crate::{ constants, image_transform::ImageOptions, media_limits::MediaLimits, metrics::transform::TransformMetrics, mime, native::{self, NativeStatus, VipsImageHandle, buffer::NativeBuffer}, }; use libc::{c_int, c_void, size_t}; use std::ptr; pub(super) struct DecodedHEIFPrimaryStill { pub(super) image: VipsImageHandle<'static>, pub(super) hdr_tone_mapped: bool, } pub(super) struct DecodedAnimation { pub(super) image: VipsImageHandle<'static>, pub(super) loop_count: Option, } pub(super) fn try_decode_heif_primary_still( input: &[u8], media_limits: &MediaLimits, metrics: &TransformMetrics, ) -> Result, MediaError> { let mut raw = ptr::null_mut(); let mut facts = native::FluxerHEIFPrimaryStillDecodeFacts::empty(); let rc = unsafe { native::fluxer_heif_decode_primary_still( input.as_ptr().cast(), input.len(), 0, &mut raw, media_limits.image_pixels(), c_int::try_from(media_limits.image_dimension()) .map_err(|_| MediaError::InvalidImageDimensions)?, &mut facts, ) }; let image = unsafe { VipsImageHandle::from_raw_owned(raw) }; match NativeStatus::from_code(rc) { NativeStatus::Ok if image.is_some() => {} NativeStatus::Ok | NativeStatus::Unsupported | NativeStatus::CodecFailure => { clear_vips_error(); metrics.record_heif_primary_still_decode_failure(); return Ok(None); } status => { clear_vips_error(); metrics.record_heif_primary_still_decode_failure(); return Err(native_status_error(status, MediaError::MediaDecodeFailed) .expect("non-success native status must map to an error")); } } metrics.record_heif_primary_still_decode(); if facts.hdr_gain_map_detected != 0 { metrics.record_heif_hdr_gain_map_detected(); } let image = image.ok_or(MediaError::MediaDecodeFailed)?; Ok(Some(DecodedHEIFPrimaryStill { image, hdr_tone_mapped: facts.hdr_tone_mapped != 0, })) } pub(super) fn try_decode_apng( input: &[u8], animated: bool, media_limits: &MediaLimits, ) -> Result, MediaError> { let runtime = vips_runtime()?; let mut raw = ptr::null_mut(); let mut num_plays = 0u32; let rc = unsafe { native::fluxer_ffmpeg_decode_apng( input.as_ptr().cast(), input.len(), runtime.config().ffmpeg_decoder_threads(), 0, &mut raw, if animated { native_animated_frame_limit(media_limits) } else { 1 }, media_limits.animated_total_pixels(), if animated { 1 } else { 0 }, &mut num_plays, ) }; let image = unsafe { VipsImageHandle::from_raw_owned(raw) }; let status = NativeStatus::from_code(rc); match status { NativeStatus::Ok if image.is_some() => { let image = image.ok_or(MediaError::MediaDecodeFailed)?; Ok(Some(DecodedAnimation { image, loop_count: Some(num_plays), })) } NativeStatus::Ok | NativeStatus::Unsupported | NativeStatus::CodecFailure => { clear_vips_error(); if animated { return Err(MediaError::MediaDecodeFailed); } Ok(None) } status => { clear_vips_error(); Err(native_status_error(status, MediaError::MediaDecodeFailed) .expect("non-success native status must map to an error")) } } } pub(super) fn decode_heif_sequence( input: &[u8], media_limits: &MediaLimits, ) -> Result, MediaError> { let runtime = vips_runtime()?; let mut raw = ptr::null_mut(); let mut frame_count = 0; let rc = unsafe { native::fluxer_ffmpeg_decode_heif_sequence( input.as_ptr().cast(), input.len(), runtime.config().ffmpeg_decoder_threads(), 0, &mut raw, native_animated_frame_limit(media_limits), media_limits.animated_total_pixels(), &mut frame_count, ) }; let image = unsafe { VipsImageHandle::from_raw_owned(raw) }; match NativeStatus::from_code(rc) { NativeStatus::Ok if image.is_some() => {} NativeStatus::Ok | NativeStatus::Unsupported | NativeStatus::CodecFailure => { clear_vips_error(); return Ok(None); } status => { clear_vips_error(); return Err(native_status_error(status, MediaError::MediaDecodeFailed) .expect("non-success native status must map to an error")); } } let image = image.ok_or(MediaError::MediaDecodeFailed)?; let frame_count = u32::try_from(frame_count).map_err(|_| MediaError::MediaDecodeFailed)?; if frame_count < 2 { return Ok(None); } let probe = animated_probe_from_image(&image)?.ok_or(MediaError::MediaDecodeFailed)?; let pages = u32::try_from(probe.pages).map_err(|_| MediaError::MediaDecodeFailed)?; if pages != frame_count { return Err(MediaError::MediaDecodeFailed); } validate_dimensions(media_limits, probe.width, probe.height)?; Ok(Some(DecodedAnimation { image, loop_count: None, })) } #[derive(Clone, Copy, Debug)] pub(super) struct GifResizeDims { pub(super) width: c_int, pub(super) height: c_int, } impl GifResizeDims { pub(super) fn fits_work_budget(self, frames: c_int) -> bool { (self.width as usize) .saturating_mul(self.height as usize) .saturating_mul(frames.max(1) as usize) <= constants::MAX_GIF_RESIZE_OUTPUT_PIXELS } } pub(super) fn gif_resize_dims( sniffed: mime::SniffInfo, options: &ImageOptions, ) -> Option { if sniffed.width == 0 || sniffed.height == 0 { return None; } let src_w = sniffed.width; let src_h = sniffed.height; let scale = match (options.width, options.height) { (Some(0), _) | (_, Some(0)) => return None, (Some(w), Some(h)) => (w as f64 / src_w as f64).min(h as f64 / src_h as f64), (Some(w), None) => w as f64 / src_w as f64, (None, Some(h)) => h as f64 / src_h as f64, (None, None) => return None, } .min(1.0); let target_w = ((src_w as f64) * scale).round().max(1.0) as u32; let target_h = ((src_h as f64) * scale).round().max(1.0) as u32; if target_w == src_w && target_h == src_h { return None; } Some(GifResizeDims { width: target_w as c_int, height: target_h as c_int, }) } pub(super) fn resize_animated_gif_with_ffmpeg( input: &[u8], dims: GifResizeDims, options: &ImageOptions, media_limits: &MediaLimits, ) -> Result, MediaError> { let runtime = vips_runtime()?; let mut out_ptr: *mut c_void = ptr::null_mut(); let mut out_size: size_t = 0; let mut out_capacity: size_t = 0; let rc = unsafe { native::fluxer_ffmpeg_resize_gif( input.as_ptr().cast(), input.len(), runtime.config().ffmpeg_decoder_threads(), dims.width, dims.height, options.deadline_ms.unwrap_or(0), native_animated_frame_limit(media_limits), // The GIF fast path resizes without re-timing, so the encode budget must never bind // tighter than the decode budget. Capping it here silently dropped the tail of any // animation longer than FLUXER_MEDIA_PROXY_MAX_ENCODE_DURATION_MS. native_animated_frame_limit(media_limits), c_int::MAX, media_limits.animated_total_pixels(), media_limits.max_media_proxy_bytes(), &mut out_ptr, &mut out_size, &mut out_capacity, ) }; let output = unsafe { NativeBuffer::from_av_owned_with_resident_bytes(out_ptr, out_size, out_capacity) }; match NativeStatus::from_code(rc) { NativeStatus::Ok => {} NativeStatus::DeadlineExceeded => return Err(MediaError::RequestTimeout), NativeStatus::WorkLimitExceeded | NativeStatus::OutputLimitExceeded => { return Err(MediaError::StreamTooLong); } NativeStatus::AllocationFailed => return Err(MediaError::AllocationFailed), NativeStatus::Unsupported | NativeStatus::InvalidDimensions | NativeStatus::CodecFailure => { clear_vips_error(); return Err(MediaError::MediaTransformFailed); } } let out = match output { Some(out) => out, None => { clear_vips_error(); return Err(MediaError::MediaTransformFailed); } }; native_buffer_to_media_bytes(out)?.try_into_vec() }