mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
469 lines
17 KiB
Rust
469 lines
17 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use super::native_runtime::clear_vips_error;
|
|
use super::{ImageDimensions, MediaError, native_status_error};
|
|
use crate::{
|
|
constants,
|
|
image_transform::{ImageOptions, ResizeMode},
|
|
media_limits::MediaLimits,
|
|
native,
|
|
};
|
|
use libc::c_int;
|
|
use std::ptr;
|
|
|
|
const VIPS_IMAGE_BANDS_MAX: c_int = 4;
|
|
const RESIZE_SCALE_IDENTITY_THRESHOLD: f64 = 0.999;
|
|
|
|
pub(crate) fn page_height(
|
|
image: &native::VipsImageHandle<'_>,
|
|
) -> Result<Option<c_int>, MediaError> {
|
|
let field = c"page-height";
|
|
let has_page_height =
|
|
unsafe { native::fluxer_vips_image_has_field(image.as_ptr(), field.as_ptr()) } != 0;
|
|
if !has_page_height {
|
|
return Ok(None);
|
|
}
|
|
let mut page_height = 0;
|
|
let rc = unsafe {
|
|
native::fluxer_vips_image_get_int(image.as_ptr(), field.as_ptr(), &mut page_height)
|
|
};
|
|
if rc != 0 {
|
|
clear_vips_error();
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
if page_height <= 0 {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
let total_height = unsafe { native::fluxer_vips_image_get_height(image.as_ptr()) };
|
|
if total_height <= 0 || total_height % page_height != 0 {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
Ok(Some(page_height))
|
|
}
|
|
|
|
pub(crate) fn validate_dimensions_u32(
|
|
media_limits: &MediaLimits,
|
|
width: u32,
|
|
height: u32,
|
|
) -> Result<(), MediaError> {
|
|
let max_dimension = media_limits.image_dimension();
|
|
if width == 0 || height == 0 || width > max_dimension || height > max_dimension {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
if width as usize * height as usize > media_limits.image_pixels() {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_jxl_decode_pixels(
|
|
sniffed_mime: &str,
|
|
dims: ImageDimensions,
|
|
) -> Result<(), MediaError> {
|
|
if sniffed_mime != "image/jxl" {
|
|
return Ok(());
|
|
}
|
|
let pixels = (dims.width as usize)
|
|
.saturating_mul(dims.height as usize)
|
|
.saturating_mul(dims.pages.max(1) as usize);
|
|
if pixels > constants::MAX_JXL_DECODE_PIXELS {
|
|
return Err(MediaError::TooLargeToProcess);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn static_output_pixels(options: &ImageOptions, dims: ImageDimensions) -> usize {
|
|
let source_width = dims.width as f64;
|
|
let source_height = dims.height as f64;
|
|
let source_pixels = dims.width as usize * dims.height as usize;
|
|
if options.wants_cover_crop()
|
|
&& let (Some(width), Some(height)) = (options.width, options.height)
|
|
{
|
|
return (width as usize * height as usize).min(source_pixels);
|
|
}
|
|
let width_scale = options
|
|
.width
|
|
.map_or(1.0, |width| width as f64 / source_width);
|
|
let height_scale = options
|
|
.height
|
|
.map_or(1.0, |height| height as f64 / source_height);
|
|
let scale = width_scale.min(height_scale).min(1.0);
|
|
((source_width * scale).round() * (source_height * scale).round()) as usize
|
|
}
|
|
|
|
pub(crate) fn validate_static_output_pixels(
|
|
options: &ImageOptions,
|
|
dims: ImageDimensions,
|
|
) -> Result<(), MediaError> {
|
|
if static_output_pixels(options, dims) > constants::MAX_STATIC_OUTPUT_PIXELS {
|
|
return Err(MediaError::TooLargeToProcess);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_dimensions(
|
|
media_limits: &MediaLimits,
|
|
width: c_int,
|
|
height: c_int,
|
|
) -> Result<(), MediaError> {
|
|
if width <= 0 || height <= 0 {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
validate_dimensions_u32(media_limits, width as u32, height as u32)
|
|
}
|
|
|
|
pub(super) fn validate_animation_geometry(
|
|
media_limits: &MediaLimits,
|
|
width: u32,
|
|
height: u32,
|
|
frames: u32,
|
|
) -> Result<(), MediaError> {
|
|
validate_dimensions_u32(media_limits, width, height)?;
|
|
if frames == 0 || frames > media_limits.animated_frames() {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
let frame_pixels = width as usize * height as usize;
|
|
if frames > 1 && frame_pixels > media_limits.animated_total_pixels() / frames as usize {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_vips_image(
|
|
media_limits: &MediaLimits,
|
|
image: &native::VipsImageHandle<'_>,
|
|
) -> Result<(), MediaError> {
|
|
let width = unsafe { native::fluxer_vips_image_get_width(image.as_ptr()) };
|
|
let height = unsafe { native::fluxer_vips_image_get_height(image.as_ptr()) };
|
|
match page_height(image)? {
|
|
Some(page_height) => {
|
|
validate_dimensions(media_limits, width, page_height)?;
|
|
let frames = height / page_height;
|
|
let width = usize::try_from(width).map_err(|_| MediaError::InvalidImageDimensions)?;
|
|
let page_height =
|
|
usize::try_from(page_height).map_err(|_| MediaError::InvalidImageDimensions)?;
|
|
let frames = u32::try_from(frames).map_err(|_| MediaError::InvalidImageDimensions)?;
|
|
if frames > media_limits.animated_frames() {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
let total_pixels = width
|
|
.checked_mul(page_height)
|
|
.and_then(|pixels| pixels.checked_mul(frames as usize))
|
|
.ok_or(MediaError::InvalidImageDimensions)?;
|
|
if total_pixels > media_limits.animated_total_pixels() {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
}
|
|
None => validate_dimensions(media_limits, width, height)?,
|
|
}
|
|
let bands = unsafe { native::fluxer_vips_image_get_bands(image.as_ptr()) };
|
|
if bands <= 0 || bands > VIPS_IMAGE_BANDS_MAX {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
let format = unsafe { native::fluxer_vips_image_get_format(image.as_ptr()) };
|
|
let supported_format = unsafe {
|
|
format == native::fluxer_vips_format_uchar
|
|
|| format == native::fluxer_vips_format_ushort
|
|
|| format == native::fluxer_vips_format_float
|
|
};
|
|
if !supported_format {
|
|
return Err(MediaError::MediaDecodeFailed);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn normalize_vips_image_to_uchar<'source>(
|
|
media_limits: &MediaLimits,
|
|
image: native::VipsImageHandle<'source>,
|
|
) -> Result<native::VipsImageHandle<'source>, MediaError> {
|
|
validate_vips_image(media_limits, &image)?;
|
|
let format = unsafe { native::fluxer_vips_image_get_format(image.as_ptr()) };
|
|
if format == unsafe { native::fluxer_vips_format_uchar } {
|
|
return Ok(image);
|
|
}
|
|
let mut normalized_raw = ptr::null_mut();
|
|
let rc = unsafe { native::fluxer_vips_image_to_rgba(image.as_ptr(), &mut normalized_raw) };
|
|
let normalized = unsafe { image.adopt_derived_raw(normalized_raw) };
|
|
if let Some(error) = native_status_error(
|
|
native::NativeStatus::from_code(rc),
|
|
MediaError::MediaTransformFailed,
|
|
) {
|
|
clear_vips_error();
|
|
return Err(error);
|
|
}
|
|
drop(image);
|
|
let normalized = normalized.ok_or_else(|| {
|
|
clear_vips_error();
|
|
MediaError::MediaTransformFailed
|
|
})?;
|
|
validate_vips_image(media_limits, &normalized)?;
|
|
let normalized_format = unsafe { native::fluxer_vips_image_get_format(normalized.as_ptr()) };
|
|
if normalized_format != unsafe { native::fluxer_vips_format_uchar } {
|
|
return Err(MediaError::MediaTransformFailed);
|
|
}
|
|
Ok(normalized)
|
|
}
|
|
|
|
pub(crate) fn resize_loaded_image<'source>(
|
|
media_limits: &MediaLimits,
|
|
image: native::VipsImageHandle<'source>,
|
|
options: &ImageOptions,
|
|
) -> Result<native::VipsImageHandle<'source>, MediaError> {
|
|
let (Some(target_width), Some(target_height)) = (options.width, options.height) else {
|
|
return resize_loaded_image_fit_inside(media_limits, image, options);
|
|
};
|
|
if options.animation.is_animated() || options.resize_mode == ResizeMode::Fit {
|
|
return resize_loaded_image_fit_inside(media_limits, image, options);
|
|
}
|
|
|
|
let source_width = unsafe { native::fluxer_vips_image_get_width(image.as_ptr()) };
|
|
let source_height = unsafe { native::fluxer_vips_image_get_height(image.as_ptr()) };
|
|
validate_dimensions(media_limits, source_width, source_height)?;
|
|
|
|
let scale_width = target_width as f64 / source_width as f64;
|
|
let scale_height = target_height as f64 / source_height as f64;
|
|
let scale = scale_width.max(scale_height).min(1.0);
|
|
let current = resize_loaded_image_by_scale(media_limits, image, scale)?;
|
|
let scaled_width = unsafe { native::fluxer_vips_image_get_width(current.as_ptr()) };
|
|
let scaled_height = unsafe { native::fluxer_vips_image_get_height(current.as_ptr()) };
|
|
validate_dimensions(media_limits, scaled_width, scaled_height)?;
|
|
|
|
let final_width = scaled_width.min(target_width as c_int);
|
|
let final_height = scaled_height.min(target_height as c_int);
|
|
if final_width == scaled_width && final_height == scaled_height {
|
|
return Ok(current);
|
|
}
|
|
|
|
let had_page_height = page_height(¤t)?.is_some();
|
|
let left = (scaled_width - final_width) / 2;
|
|
let top = (scaled_height - final_height) / 2;
|
|
let cropped = extract_vips_image_area(¤t, left, top, final_width, final_height)?;
|
|
drop(current);
|
|
if had_page_height {
|
|
unsafe { native::fluxer_vips_set_page_height(cropped.as_ptr(), final_height) };
|
|
}
|
|
validate_vips_image(media_limits, &cropped)?;
|
|
Ok(cropped)
|
|
}
|
|
|
|
fn resize_loaded_image_fit_inside<'source>(
|
|
media_limits: &MediaLimits,
|
|
image: native::VipsImageHandle<'source>,
|
|
options: &ImageOptions,
|
|
) -> Result<native::VipsImageHandle<'source>, MediaError> {
|
|
if options.width.is_none() && options.height.is_none() {
|
|
return Ok(image);
|
|
}
|
|
let source_width = unsafe { native::fluxer_vips_image_get_width(image.as_ptr()) };
|
|
let total_height = unsafe { native::fluxer_vips_image_get_height(image.as_ptr()) };
|
|
let source_height = if options.animation.is_animated() {
|
|
page_height(&image)?.unwrap_or(total_height)
|
|
} else {
|
|
total_height
|
|
};
|
|
validate_dimensions(media_limits, source_width, source_height)?;
|
|
|
|
let scale = match (options.width, options.height) {
|
|
(Some(width), Some(height)) => {
|
|
(width as f64 / source_width as f64).min(height as f64 / source_height as f64)
|
|
}
|
|
(Some(width), None) => width as f64 / source_width as f64,
|
|
(None, Some(height)) => height as f64 / source_height as f64,
|
|
(None, None) => unreachable!("dimension presence is checked before the scale calculation"),
|
|
}
|
|
.min(1.0);
|
|
resize_loaded_image_by_scale(media_limits, image, scale)
|
|
}
|
|
|
|
pub(crate) fn resize_loaded_image_by_scale<'source>(
|
|
media_limits: &MediaLimits,
|
|
image: native::VipsImageHandle<'source>,
|
|
scale: f64,
|
|
) -> Result<native::VipsImageHandle<'source>, MediaError> {
|
|
assert!(scale.is_finite());
|
|
assert!(scale > 0.0);
|
|
assert!(scale <= 1.0);
|
|
if scale >= RESIZE_SCALE_IDENTITY_THRESHOLD {
|
|
return Ok(image);
|
|
}
|
|
let old_page_height = page_height(&image)?;
|
|
if let Some(old_page_height) = old_page_height {
|
|
let old_total_height = unsafe { native::fluxer_vips_image_get_height(image.as_ptr()) };
|
|
let page_count = old_total_height / old_page_height;
|
|
if page_count > 1 {
|
|
validate_vips_image(media_limits, &image)?;
|
|
return resize_loaded_animation_pages_by_scale(
|
|
media_limits,
|
|
image,
|
|
scale,
|
|
old_page_height,
|
|
page_count,
|
|
);
|
|
}
|
|
}
|
|
let resized = resize_vips_image(&image, scale)?;
|
|
drop(image);
|
|
if old_page_height.is_some() {
|
|
let new_total_height = unsafe { native::fluxer_vips_image_get_height(resized.as_ptr()) };
|
|
if new_total_height <= 0 {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
unsafe { native::fluxer_vips_set_page_height(resized.as_ptr(), new_total_height) };
|
|
}
|
|
validate_vips_image(media_limits, &resized)?;
|
|
Ok(resized)
|
|
}
|
|
|
|
fn resize_loaded_animation_pages_by_scale<'source>(
|
|
media_limits: &MediaLimits,
|
|
image: native::VipsImageHandle<'source>,
|
|
scale: f64,
|
|
old_page_height: c_int,
|
|
page_count: c_int,
|
|
) -> Result<native::VipsImageHandle<'source>, MediaError> {
|
|
assert!(old_page_height > 0);
|
|
assert!(page_count > 1);
|
|
let source_width = unsafe { native::fluxer_vips_image_get_width(image.as_ptr()) };
|
|
validate_dimensions(media_limits, source_width, old_page_height)?;
|
|
let page_count_usize =
|
|
usize::try_from(page_count).map_err(|_| MediaError::InvalidImageDimensions)?;
|
|
let mut resized_pages = Vec::new();
|
|
resized_pages
|
|
.try_reserve_exact(page_count_usize)
|
|
.map_err(|_| MediaError::AllocationFailed)?;
|
|
let mut resized_page_width = 0;
|
|
let mut resized_page_height = 0;
|
|
for page_index in 0..page_count {
|
|
let top = page_index
|
|
.checked_mul(old_page_height)
|
|
.ok_or(MediaError::InvalidImageDimensions)?;
|
|
let page = extract_vips_image_area(&image, 0, top, source_width, old_page_height)?;
|
|
let resized_page = resize_vips_image(&page, scale)?;
|
|
drop(page);
|
|
let page_width = unsafe { native::fluxer_vips_image_get_width(resized_page.as_ptr()) };
|
|
let page_height = unsafe { native::fluxer_vips_image_get_height(resized_page.as_ptr()) };
|
|
validate_dimensions(media_limits, page_width, page_height)?;
|
|
if page_index == 0 {
|
|
resized_page_width = page_width;
|
|
resized_page_height = page_height;
|
|
} else if page_width != resized_page_width || page_height != resized_page_height {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
resized_pages.push(resized_page);
|
|
}
|
|
assert_eq!(resized_pages.len(), page_count_usize);
|
|
let output_width =
|
|
u32::try_from(resized_page_width).map_err(|_| MediaError::InvalidImageDimensions)?;
|
|
let output_page_height =
|
|
u32::try_from(resized_page_height).map_err(|_| MediaError::InvalidImageDimensions)?;
|
|
let output_page_count =
|
|
u32::try_from(page_count).map_err(|_| MediaError::InvalidImageDimensions)?;
|
|
validate_animation_geometry(
|
|
media_limits,
|
|
output_width,
|
|
output_page_height,
|
|
output_page_count,
|
|
)?;
|
|
let expected_total_height = resized_page_height
|
|
.checked_mul(page_count)
|
|
.ok_or(MediaError::InvalidImageDimensions)?;
|
|
let mut page_pointers = Vec::new();
|
|
page_pointers
|
|
.try_reserve_exact(page_count_usize)
|
|
.map_err(|_| MediaError::AllocationFailed)?;
|
|
page_pointers.extend(resized_pages.iter().map(native::VipsImageHandle::as_ptr));
|
|
assert_eq!(page_pointers.len(), page_count_usize);
|
|
let max_pages = c_int::try_from(media_limits.animated_frames())
|
|
.expect("validated animated frame limit must fit c_int");
|
|
let mut joined_raw = ptr::null_mut();
|
|
let rc = unsafe {
|
|
native::fluxer_vips_join_animation_pages(
|
|
image.as_ptr(),
|
|
page_pointers.as_mut_ptr(),
|
|
page_count,
|
|
max_pages,
|
|
media_limits.animated_total_pixels(),
|
|
&mut joined_raw,
|
|
)
|
|
};
|
|
let joined = unsafe { image.adopt_derived_raw(joined_raw) };
|
|
if let Some(error) = native_status_error(
|
|
native::NativeStatus::from_code(rc),
|
|
MediaError::MediaTransformFailed,
|
|
) {
|
|
clear_vips_error();
|
|
return Err(error);
|
|
}
|
|
drop(resized_pages);
|
|
drop(image);
|
|
let joined = joined.ok_or_else(|| {
|
|
clear_vips_error();
|
|
MediaError::MediaTransformFailed
|
|
})?;
|
|
let joined_width = unsafe { native::fluxer_vips_image_get_width(joined.as_ptr()) };
|
|
let joined_total_height = unsafe { native::fluxer_vips_image_get_height(joined.as_ptr()) };
|
|
if joined_width != resized_page_width || joined_total_height != expected_total_height {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
if page_height(&joined)? != Some(resized_page_height) {
|
|
return Err(MediaError::InvalidImageDimensions);
|
|
}
|
|
validate_vips_image(media_limits, &joined)?;
|
|
Ok(joined)
|
|
}
|
|
|
|
fn extract_vips_image_area<'source>(
|
|
image: &native::VipsImageHandle<'source>,
|
|
left: c_int,
|
|
top: c_int,
|
|
width: c_int,
|
|
height: c_int,
|
|
) -> Result<native::VipsImageHandle<'source>, MediaError> {
|
|
let mut extracted_raw = ptr::null_mut();
|
|
let rc = unsafe {
|
|
native::fluxer_vips_extract_area(
|
|
image.as_ptr(),
|
|
&mut extracted_raw,
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
)
|
|
};
|
|
let extracted = unsafe { image.adopt_derived_raw(extracted_raw) };
|
|
if let Some(error) = native_status_error(
|
|
native::NativeStatus::from_code(rc),
|
|
MediaError::MediaTransformFailed,
|
|
) {
|
|
clear_vips_error();
|
|
return Err(error);
|
|
}
|
|
extracted.ok_or_else(|| {
|
|
clear_vips_error();
|
|
MediaError::MediaTransformFailed
|
|
})
|
|
}
|
|
|
|
fn resize_vips_image<'source>(
|
|
image: &native::VipsImageHandle<'source>,
|
|
scale: f64,
|
|
) -> Result<native::VipsImageHandle<'source>, MediaError> {
|
|
assert!(scale.is_finite());
|
|
assert!(scale > 0.0);
|
|
assert!(scale < RESIZE_SCALE_IDENTITY_THRESHOLD);
|
|
let mut resized_raw = ptr::null_mut();
|
|
let rc = unsafe { native::fluxer_vips_resize(image.as_ptr(), &mut resized_raw, scale) };
|
|
let resized = unsafe { image.adopt_derived_raw(resized_raw) };
|
|
if let Some(error) = native_status_error(
|
|
native::NativeStatus::from_code(rc),
|
|
MediaError::MediaTransformFailed,
|
|
) {
|
|
clear_vips_error();
|
|
return Err(error);
|
|
}
|
|
resized.ok_or_else(|| {
|
|
clear_vips_error();
|
|
MediaError::MediaTransformFailed
|
|
})
|
|
}
|