Fix tiff compression to size

This commit is contained in:
Matteo Paonessa 2025-02-21 14:11:55 +01:00
parent ea8b3f95a9
commit 0d99042cbf
15 changed files with 152 additions and 225 deletions

21
.github/workflows/clippy.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Clippy check
on:
push:
paths:
- 'src/**'
- '.github/**'
pull_request:
paths:
- 'src/**'
- '.github/**'
env:
RUSTFLAGS: "-Dwarnings"
jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Clippy
run: cargo clippy --all-targets --all-features

27
.github/workflows/fmt.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Code formatting
on:
push:
paths:
- 'src/**'
- '.github/**'
pull_request:
paths:
- 'src/**'
- '.github/**'
jobs:
fmt_check:
name: Fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Check code formatting
run: cargo fmt -- --check

View File

@ -37,7 +37,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "z
libc = "0.2"
gifsicle = { version = "1.95", optional = true }
webp = { version = "0.3.0", optional = true }
infer = "0.16.0"
infer = "0.19.0"
image = { version = "0.25.1", default-features = false }
img-parts = "0.3.3"
bytes = "1.7"

View File

@ -13,12 +13,7 @@ fn main() -> ExitCode {
parameters.keep_metadata = true;
parameters.webp.quality = 60;
match convert(
input,
output,
&parameters,
caesium::SupportedFileTypes::WebP,
) {
match convert(input, output, &parameters, caesium::SupportedFileTypes::WebP) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{}", e);

View File

@ -79,15 +79,14 @@ pub fn convert_in_memory(
code: 10404,
})?;
let compressed_converted_image =
compress_in_memory(output_image, parameters).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10405,
})?;
let compressed_converted_image = compress_in_memory(output_image, parameters).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10405,
})?;
if parameters.keep_metadata {
let dyn_image = DynImage::from_bytes(Bytes::from(compressed_converted_image.clone()))
.map_err(|e| CaesiumError {
let dyn_image =
DynImage::from_bytes(Bytes::from(compressed_converted_image.clone())).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10408,
})?;

View File

@ -4,11 +4,7 @@ use std::os::raw::{c_int, c_void};
use crate::error::CaesiumError;
use crate::CSParameters;
pub fn compress(
input_path: String,
output_path: String,
parameters: &CSParameters,
) -> Result<(), CaesiumError> {
pub fn compress(input_path: String, output_path: String, parameters: &CSParameters) -> Result<(), CaesiumError> {
if parameters.width > 0 || parameters.height > 0 {
return Err(CaesiumError {
message: "GIF resizing is not supported".to_string(),
@ -58,11 +54,7 @@ fn lossless(input_path: String, output_path: String) -> Result<(), CaesiumError>
}
}
pub fn lossy(
input_path: String,
output_path: String,
parameters: &CSParameters,
) -> Result<(), CaesiumError> {
pub fn lossy(input_path: String, output_path: String, parameters: &CSParameters) -> Result<(), CaesiumError> {
unsafe {
let input_file = libc::fopen(
CString::new(input_path)

View File

@ -3,9 +3,7 @@ use std::os::raw::c_char;
use crate::parameters::ChromaSubsampling;
use crate::parameters::TiffCompression::{Deflate, Lzw, Packbits, Uncompressed};
use crate::{
compress, compress_to_size, convert, error, CSParameters, SupportedFileTypes, TiffDeflateLevel,
};
use crate::{compress, compress_to_size, convert, error, CSParameters, SupportedFileTypes, TiffDeflateLevel};
#[repr(C)]
pub struct CCSParameters {

View File

@ -18,11 +18,7 @@ use crate::CSParameters;
static JPEG_ERROR: AtomicI32 = AtomicI32::new(0);
pub fn compress(
input_path: String,
output_path: String,
parameters: &CSParameters,
) -> Result<(), CaesiumError> {
pub fn compress(input_path: String, output_path: String, parameters: &CSParameters) -> Result<(), CaesiumError> {
let in_file = fs::read(input_path).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20100,
@ -40,10 +36,7 @@ pub fn compress(
Ok(())
}
pub fn compress_in_memory(
mut in_file: Vec<u8>,
parameters: &CSParameters,
) -> Result<Vec<u8>, CaesiumError> {
pub fn compress_in_memory(mut in_file: Vec<u8>, parameters: &CSParameters) -> Result<Vec<u8>, CaesiumError> {
if parameters.width > 0 || parameters.height > 0 {
if parameters.keep_metadata {
let metadata = extract_metadata(in_file.clone());
@ -200,11 +193,7 @@ unsafe fn lossy(in_file: Vec<u8>, parameters: &CSParameters) -> Result<Vec<u8>,
let row_stride = dst_info.image_width as usize * dst_info.input_components as usize;
dst_info.dct_method = J_DCT_METHOD::JDCT_ISLOW;
dst_info.optimize_coding = i32::from(true);
jpeg_set_quality(
&mut dst_info,
parameters.jpeg.quality as i32,
false as boolean,
);
jpeg_set_quality(&mut dst_info, parameters.jpeg.quality as i32, false as boolean);
if !parameters.jpeg.progressive {
dst_info.scan_info = null();
@ -244,17 +233,12 @@ fn extract_metadata(image: Vec<u8>) -> (Option<img_parts::Bytes>, Option<img_par
}
//TODO if image is resized, change "PixelXDimension" and "PixelYDimension"
fn save_metadata(
image_buffer: Vec<u8>,
iccp: Option<img_parts::Bytes>,
exif: Option<img_parts::Bytes>,
) -> Vec<u8> {
fn save_metadata(image_buffer: Vec<u8>, iccp: Option<img_parts::Bytes>, exif: Option<img_parts::Bytes>) -> Vec<u8> {
if iccp.is_some() || exif.is_some() {
let mut dyn_image =
match PartsJpeg::from_bytes(img_parts::Bytes::from(image_buffer.clone())) {
Ok(d) => d,
Err(_) => return image_buffer,
};
let mut dyn_image = match PartsJpeg::from_bytes(img_parts::Bytes::from(image_buffer.clone())) {
Ok(d) => d,
Err(_) => return image_buffer,
};
dyn_image.set_icc_profile(iccp);
dyn_image.set_exif(exif);
@ -269,27 +253,16 @@ fn save_metadata(
}
}
unsafe fn write_metadata(
src_info: &mut jpeg_decompress_struct,
dst_info: &mut jpeg_compress_struct,
) {
unsafe fn write_metadata(src_info: &mut jpeg_decompress_struct, dst_info: &mut jpeg_compress_struct) {
let mut marker = src_info.marker_list;
while !marker.is_null() {
jpeg_write_marker(
dst_info,
(*marker).marker as i32,
(*marker).data,
(*marker).data_length,
);
jpeg_write_marker(dst_info, (*marker).marker as i32, (*marker).data, (*marker).data_length);
marker = (*marker).next;
}
}
unsafe fn set_chroma_subsampling(
subsampling: ChromaSubsampling,
dst_info: &mut jpeg_compress_struct,
) {
unsafe fn set_chroma_subsampling(subsampling: ChromaSubsampling, dst_info: &mut jpeg_compress_struct) {
(*dst_info.comp_info.add(1)).h_samp_factor = 1;
(*dst_info.comp_info.add(1)).v_samp_factor = 1;
(*dst_info.comp_info.add(2)).h_samp_factor = 1;

View File

@ -37,11 +37,7 @@ mod webp;
/// # Returns
///
/// * `Result<(), CaesiumError>` - Returns `Ok(())` if compression is successful, otherwise returns a `CaesiumError`.
pub fn compress(
input_path: String,
output_path: String,
parameters: &CSParameters,
) -> error::Result<()> {
pub fn compress(input_path: String, output_path: String, parameters: &CSParameters) -> error::Result<()> {
validate_parameters(parameters)?;
let file_type = get_filetype_from_path(&input_path);
@ -151,7 +147,7 @@ pub fn compress_to_size_in_memory(
smallest_result = result;
}
}
return if return_smallest {
return if return_smallest || smallest_result.len() <= max_output_size {
Ok(smallest_result)
} else {
Err(CaesiumError {
@ -194,9 +190,7 @@ pub fn compress_to_size_in_memory(
let compressed_file_size = compressed_file.len();
if compressed_file_size <= max_output_size
&& max_output_size - compressed_file_size < tolerance
{
if compressed_file_size <= max_output_size && max_output_size - compressed_file_size < tolerance {
break compressed_file;
}
@ -264,18 +258,15 @@ pub fn compress_to_size(
return Ok(());
}
let compressed_file =
compress_to_size_in_memory(in_file, parameters, max_output_size, return_smallest)?;
let compressed_file = compress_to_size_in_memory(in_file, parameters, max_output_size, return_smallest)?;
let mut out_file = File::create(output_path).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10203,
})?;
out_file
.write_all(&compressed_file)
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 10204,
})?;
out_file.write_all(&compressed_file).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10204,
})?;
Ok(())
}
@ -311,23 +302,20 @@ pub fn convert(
message: e.to_string(),
code: 10410,
})?;
let output_buffer =
convert_in_memory(in_file, parameters, format).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10411,
})?;
let output_buffer = convert_in_memory(in_file, parameters, format).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10411,
})?;
let mut out_file = File::create(output_path).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10412,
})?;
out_file
.write_all(&output_buffer)
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 10413,
})?;
out_file.write_all(&output_buffer).map_err(|e| CaesiumError {
message: e.to_string(),
code: 10413,
})?;
Ok(())
}

View File

@ -10,23 +10,14 @@ use crate::error::CaesiumError;
use crate::resize::resize;
use crate::CSParameters;
pub fn compress(
input_path: String,
output_path: String,
parameters: &CSParameters,
) -> Result<(), CaesiumError> {
pub fn compress(input_path: String, output_path: String, parameters: &CSParameters) -> Result<(), CaesiumError> {
let mut in_file = fs::read(input_path).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20200,
})?;
if parameters.width > 0 || parameters.height > 0 {
in_file = resize(
in_file,
parameters.width,
parameters.height,
ImageFormat::Png,
)?;
in_file = resize(in_file, parameters.width, parameters.height, ImageFormat::Png)?;
}
let optimized_png = compress_in_memory(in_file, parameters)?;
@ -44,17 +35,9 @@ pub fn compress(
Ok(())
}
pub fn compress_in_memory(
in_file: Vec<u8>,
parameters: &CSParameters,
) -> Result<Vec<u8>, CaesiumError> {
pub fn compress_in_memory(in_file: Vec<u8>, parameters: &CSParameters) -> Result<Vec<u8>, CaesiumError> {
let input = if parameters.width > 0 || parameters.height > 0 {
resize(
in_file,
parameters.width,
parameters.height,
ImageFormat::Png,
)?
resize(in_file, parameters.width, parameters.height, ImageFormat::Png)?
} else {
in_file
};
@ -98,20 +81,16 @@ fn lossy(in_file: Vec<u8>, parameters: &CSParameters) -> Result<Vec<u8>, Caesium
code: 20207,
})?;
let (palette, pixels) = quantization
.remapped(&mut liq_image)
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 20208,
})?;
let (palette, pixels) = quantization.remapped(&mut liq_image).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20208,
})?;
let mut encoder = lodepng::Encoder::new();
encoder
.set_palette(palette.as_slice())
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 20212,
})?;
encoder.set_palette(palette.as_slice()).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20212,
})?;
let png_vec = encoder
.encode(pixels.as_slice(), rgba_bitmap.width, rgba_bitmap.height)
.map_err(|e| CaesiumError {
@ -142,11 +121,9 @@ fn lossless(in_file: Vec<u8>, parameters: &CSParameters) -> Result<Vec<u8>, Caes
}
let optimized_png =
oxipng::optimize_from_memory(in_file.as_slice(), &oxipng_options).map_err(|e| {
CaesiumError {
message: e.to_string(),
code: 20210,
}
oxipng::optimize_from_memory(in_file.as_slice(), &oxipng_options).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20210,
})?;
Ok(optimized_png)

View File

@ -18,7 +18,7 @@ pub fn resize(
let orientation = get_jpeg_orientation(buffer_slice);
(desired_width, desired_height) = match orientation {
5..=8 => (height, width),
_ => (width, height)
_ => (width, height),
};
}
@ -34,8 +34,7 @@ pub fn resize(
code: 10301,
})?;
let dimensions =
compute_dimensions(image.width(), image.height(), desired_width, desired_height);
let dimensions = compute_dimensions(image.width(), image.height(), desired_width, desired_height);
image = image.resize_exact(dimensions.0, dimensions.1, FilterType::Lanczos3);
let mut resized_file: Vec<u8> = vec![];
@ -106,10 +105,7 @@ fn downscale_on_width() {
let original_width = 800;
let original_height = 600;
assert_eq!(
compute_dimensions(original_width, original_height, 750, 0),
(750, 563)
)
assert_eq!(compute_dimensions(original_width, original_height, 750, 0), (750, 563))
}
#[test]
@ -117,8 +113,5 @@ fn downscale_on_height() {
let original_width = 800;
let original_height = 600;
assert_eq!(
compute_dimensions(original_width, original_height, 0, 478),
(637, 478)
)
assert_eq!(compute_dimensions(original_width, original_height, 0, 478), (637, 478))
}

View File

@ -12,23 +12,17 @@ use crate::parameters::TiffCompression;
use crate::resize::resize_image;
use crate::{CSParameters, TiffDeflateLevel};
pub fn compress(
input_path: String,
output_path: String,
parameters: &CSParameters,
) -> Result<(), CaesiumError> {
pub fn compress(input_path: String, output_path: String, parameters: &CSParameters) -> Result<(), CaesiumError> {
let mut input_file = File::open(input_path).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20500,
})?;
let mut input_data = Vec::new();
input_file
.read_to_end(&mut input_data)
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 20501,
})?;
input_file.read_to_end(&mut input_data).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20501,
})?;
let compressed_image = compress_in_memory(input_data, parameters)?;
@ -37,30 +31,23 @@ pub fn compress(
code: 20502,
})?;
output_file
.write_all(&compressed_image)
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 20503,
})?;
output_file.write_all(&compressed_image).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20503,
})?;
Ok(())
}
pub fn compress_in_memory(
in_file: Vec<u8>,
parameters: &CSParameters,
) -> Result<Vec<u8>, CaesiumError> {
let decoding_result =
match panic::catch_unwind(|| image::load_from_memory_with_format(in_file.as_slice(), Tiff))
{
Ok(i) => i,
Err(_) => {
return Err(CaesiumError {
message: "Failed to decode TIFF image".to_string(),
code: 20504,
});
}
};
pub fn compress_in_memory(in_file: Vec<u8>, parameters: &CSParameters) -> Result<Vec<u8>, CaesiumError> {
let decoding_result = match panic::catch_unwind(|| image::load_from_memory_with_format(in_file.as_slice(), Tiff)) {
Ok(i) => i,
Err(_) => {
return Err(CaesiumError {
message: "Failed to decode TIFF image".to_string(),
code: 20504,
});
}
};
let mut image = match decoding_result {
Ok(i) => i,
Err(e) => {
@ -106,18 +93,12 @@ pub fn compress_in_memory(
},
TiffCompression::Lzw => match color_type {
image::ColorType::Rgb8 => encoder.write_image_with_compression::<RGB8, Lzw>(
image.width(),
image.height(),
Lzw,
image.as_bytes(),
),
image::ColorType::Rgba8 => encoder.write_image_with_compression::<RGBA8, Lzw>(
image.width(),
image.height(),
Lzw,
image.as_bytes(),
),
image::ColorType::Rgb8 => {
encoder.write_image_with_compression::<RGB8, Lzw>(image.width(), image.height(), Lzw, image.as_bytes())
}
image::ColorType::Rgba8 => {
encoder.write_image_with_compression::<RGBA8, Lzw>(image.width(), image.height(), Lzw, image.as_bytes())
}
_ => {
return Err(CaesiumError {
message: format!("Unsupported TIFF color type ({:?})", color_type).to_string(),

View File

@ -12,23 +12,17 @@ use crate::error::CaesiumError;
use crate::resize::resize_image;
use crate::CSParameters;
pub fn compress(
input_path: String,
output_path: String,
parameters: &CSParameters,
) -> Result<(), CaesiumError> {
pub fn compress(input_path: String, output_path: String, parameters: &CSParameters) -> Result<(), CaesiumError> {
let mut input_file = File::open(input_path).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20300,
})?;
let mut input_data = Vec::new();
input_file
.read_to_end(&mut input_data)
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 20301,
})?;
input_file.read_to_end(&mut input_data).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20301,
})?;
let compressed_image = compress_in_memory(input_data, parameters)?;
@ -37,19 +31,14 @@ pub fn compress(
code: 20302,
})?;
output_file
.write_all(&compressed_image)
.map_err(|e| CaesiumError {
message: e.to_string(),
code: 20303,
})?;
output_file.write_all(&compressed_image).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20303,
})?;
Ok(())
}
pub fn compress_in_memory(
in_file: Vec<u8>,
parameters: &CSParameters,
) -> Result<Vec<u8>, CaesiumError> {
pub fn compress_in_memory(in_file: Vec<u8>, parameters: &CSParameters) -> Result<Vec<u8>, CaesiumError> {
let mut iccp: Option<Bytes> = None;
let mut exif: Option<Bytes> = None;
@ -59,9 +48,7 @@ pub fn compress_in_memory(
message: e.to_string(),
code: 20306,
})?
.map_or((None, None), |dyn_img| {
(dyn_img.icc_profile(), dyn_img.exif())
});
.map_or((None, None), |dyn_img| (dyn_img.icc_profile(), dyn_img.exif()));
}
let must_resize = parameters.width > 0 || parameters.height > 0;
@ -120,12 +107,10 @@ pub fn compress_in_memory(
if must_resize {
if images_data.get(i).is_some() {
encoder.add_frame(
AnimFrame::from_image(images_data.get(i).unwrap(), last_ms).map_err(
|e| CaesiumError {
message: e.to_string(),
code: 20310,
},
)?,
AnimFrame::from_image(images_data.get(i).unwrap(), last_ms).map_err(|e| CaesiumError {
message: e.to_string(),
code: 20310,
})?,
);
}
} else {
@ -206,14 +191,12 @@ fn to_rgba(value: u32) -> [u8; 4] {
fn to_dynamic_image(frame: AnimFrame) -> DynamicImage {
if frame.get_layout().is_alpha() {
let image =
ImageBuffer::from_raw(frame.width(), frame.height(), frame.get_image().to_owned())
.expect("ImageBuffer couldn't be created");
let image = ImageBuffer::from_raw(frame.width(), frame.height(), frame.get_image().to_owned())
.expect("ImageBuffer couldn't be created");
DynamicImage::ImageRgba8(image)
} else {
let image =
ImageBuffer::from_raw(frame.width(), frame.height(), frame.get_image().to_owned())
.expect("ImageBuffer couldn't be created");
let image = ImageBuffer::from_raw(frame.width(), frame.height(), frame.get_image().to_owned())
.expect("ImageBuffer couldn't be created");
DynamicImage::ImageRgb8(image)
}
}

View File

@ -126,7 +126,7 @@ fn rgb8_downscale() {
#[test]
fn rgb8_downscale_to_size() {
let max_output_size = 10_000;
let max_output_size = 100_000;
let output = "tests/samples/output/downscale_rgb8_to_size.tif";
initialize(output);
let mut params = CSParameters::new();
@ -138,7 +138,7 @@ fn rgb8_downscale_to_size() {
String::from(output),
&mut params,
max_output_size,
false,
true,
)
.unwrap();
assert!(std::path::Path::new(output).exists());