mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
273 lines
9.2 KiB
Rust
273 lines
9.2 KiB
Rust
// 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<u32>,
|
|
}
|
|
|
|
pub(super) fn try_decode_heif_primary_still(
|
|
input: &[u8],
|
|
media_limits: &MediaLimits,
|
|
metrics: &TransformMetrics,
|
|
) -> Result<Option<DecodedHEIFPrimaryStill>, 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<Option<DecodedAnimation>, 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<Option<DecodedAnimation>, 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<GifResizeDims> {
|
|
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<Vec<u8>, 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()
|
|
}
|