diff --git a/CREDITS.md b/CREDITS.md index 0554e31..d8aad7c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,7 +1,7 @@ # Project Dependencies Package: flaca - Version: 2.4.3 - Generated: 2024-01-06 06:34:27 UTC + Version: 2.4.4 + Generated: 2024-01-08 04:24:33 UTC | Package | Version | Author(s) | License | | ---- | ---- | ---- | ---- | @@ -24,7 +24,7 @@ | [fyi_msg](https://github.com/Blobfolio/fyi) | 0.11.8 | [Blobfolio, LLC.](mailto:hello@blobfolio.com) | WTFPL | | [hashbrown](https://github.com/rust-lang/hashbrown) | 0.14.3 | [Amanieu d'Antras](mailto:amanieu@gmail.com) | Apache-2.0 or MIT | | [indexmap](https://github.com/bluss/indexmap) | 2.1.0 | | Apache-2.0 or MIT | -| [libc](https://github.com/rust-lang/libc) | 0.2.151 | The Rust Project Developers | Apache-2.0 or MIT | +| [libc](https://github.com/rust-lang/libc) | 0.2.152 | The Rust Project Developers | Apache-2.0 or MIT | | [libdeflate-sys](https://github.com/adamkewley/libdeflater) | 1.19.0 | [Adam Kewley](mailto:contact@adamkewley.com) | Apache-2.0 | | [libdeflater](https://github.com/adamkewley/libdeflater) | 1.19.0 | [Adam Kewley](mailto:contact@adamkewley.com) | Apache-2.0 | | [log](https://github.com/rust-lang/log) | 0.4.20 | The Rust Project Developers | Apache-2.0 or MIT | diff --git a/Cargo.toml b/Cargo.toml index 7cab669..d85eeff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flaca" -version = "2.4.3" +version = "2.4.4" license = "WTFPL" authors = ["Josh Stoik "] edition = "2021" @@ -18,7 +18,7 @@ exclude = [ [package.metadata.deb] maintainer = "Josh Stoik " -copyright = "2022, Blobfolio, LLC " +copyright = "2024, Blobfolio, LLC " license-file = ["./LICENSE", "0"] extended-description = """\n\ Flaca is a CLI tool for x86-64 Linux machines that simplifies the task of losslessly compressing JPEG and PNG images for use on the web.\n\ @@ -125,11 +125,7 @@ default-features = false cc = "1.0.*" dowser = "0.8.*" -[profile.release.package."*"] -opt-level = 3 - [profile.release] lto = true codegen-units = 1 -opt-level = 1 strip = true diff --git a/release/man/flaca.1 b/release/man/flaca.1 index 7558904..7629e5d 100644 --- a/release/man/flaca.1 +++ b/release/man/flaca.1 @@ -1,6 +1,6 @@ -.TH "FLACA" "1" "January 2024" "Flaca v2.4.3" "User Commands" +.TH "FLACA" "1" "January 2024" "Flaca v2.4.4" "User Commands" .SH NAME -Flaca \- Manual page for flaca v2.4.3. +Flaca \- Manual page for flaca v2.4.4. .SH DESCRIPTION Brute\-force, lossless JPEG and PNG compression. .SS USAGE: diff --git a/src/image/jpegtran.rs b/src/image/jpegtran.rs index ae0fa82..b51f9a2 100644 --- a/src/image/jpegtran.rs +++ b/src/image/jpegtran.rs @@ -19,6 +19,7 @@ use mozjpeg_sys::{ j_compress_ptr, j_decompress_ptr, JCROP_CODE_JCROP_UNSET, + jpeg_common_struct, jpeg_compress_struct, jpeg_copy_critical_parameters, jpeg_create_compress, @@ -43,16 +44,20 @@ use mozjpeg_sys::{ jvirt_barray_ptr, JXFORM_CODE_JXFORM_NONE, }; -use std::ffi::{ - c_uint, - c_ulong, +use std::{ + ffi::{ + c_int, + c_uint, + c_ulong, + }, + marker::PhantomPinned, }; use super::ffi::EncodedImage; // We need a couple more things from jpegtran. Mozjpeg-sys includes the right -// sources but doesn't export the definitions. +// sources but doesn't export these definitions for whatever reason. extern "C-unwind" { fn jcopy_markers_setup(srcinfo: j_decompress_ptr, option: c_uint); fn jcopy_markers_execute( @@ -101,139 +106,228 @@ pub(super) fn optimize(src: &[u8]) -> Option> { iMCU_sample_height: 0, }; - let mut meta = InOut::default(); + // Our original image length. let src_size = src.len() as c_ulong; // We know this fits. + // Set up the decompression/compression structs. + let mut srcinfo = JpegSrcInfo::from(src); + let mut dstinfo = JpegDstInfo::from(&mut srcinfo); + unsafe { // Load the source file. - jpeg_mem_src(&mut meta.src, src.as_ptr(), src_size); + jpeg_mem_src(&mut srcinfo.cinfo, srcinfo.raw.as_ptr(), src_size); // Ignore markers. - jcopy_markers_setup(&mut meta.src, 0); + jcopy_markers_setup(&mut srcinfo.cinfo, 0); // Read the file header to get to the goods. - jpeg_read_header(&mut meta.src, 1); + jpeg_read_header(&mut srcinfo.cinfo, 1); // Read a few more properties into the source struct. - if jtransform_request_workspace(&mut meta.src, &mut transformoption) == 0 { + if jtransform_request_workspace(&mut srcinfo.cinfo, &mut transformoption) == 0 { return None; } } // Read source file as DCT coefficients. let src_coef_arrays: *mut jvirt_barray_ptr = unsafe { - jpeg_read_coefficients(&mut meta.src) + jpeg_read_coefficients(&mut srcinfo.cinfo) }; // Initialize destination compression parameters from source values. - unsafe { jpeg_copy_critical_parameters(&meta.src, &mut meta.dst); } + unsafe { jpeg_copy_critical_parameters(&srcinfo.cinfo, &mut dstinfo.cinfo); } // Adjust destination parameters if required by transform options, and sync // the coefficient arrays. let dst_coef_arrays: *mut jvirt_barray_ptr = unsafe { jtransform_adjust_parameters( - &mut meta.src, - &mut meta.dst, + &mut srcinfo.cinfo, + &mut dstinfo.cinfo, src_coef_arrays, &mut transformoption, ) }; // Turn on "code optimizing". - meta.dst.optimize_coding = 1; + dstinfo.cinfo.optimize_coding = 1; + + // Compress! let mut out = EncodedImage::default(); unsafe { // Enable "progressive". - jpeg_simple_progression(&mut meta.dst); + jpeg_simple_progression(&mut dstinfo.cinfo); // And load the destination file. - jpeg_mem_dest(&mut meta.dst, &mut out.buf, &mut out.size); + jpeg_mem_dest(&mut dstinfo.cinfo, &mut out.buf, &mut out.size); // Start the compressor. Note: no data is written here. - jpeg_write_coefficients(&mut meta.dst, dst_coef_arrays); + jpeg_write_coefficients(&mut dstinfo.cinfo, dst_coef_arrays); // Make sure we aren't copying any markers. - jcopy_markers_execute(&mut meta.src, &mut meta.dst, 0); + jcopy_markers_execute(&mut srcinfo.cinfo, &mut dstinfo.cinfo, 0); // Execute and write the transformation, if any. jtransform_execute_transform( - &mut meta.src, - &mut meta.dst, + &mut srcinfo.cinfo, + &mut dstinfo.cinfo, src_coef_arrays, &mut transformoption, ); } + // Finish it up, and note whether or not it (probably) worked. + let happy = dstinfo.finish(); + + // The decompression will have finished much earlier, but we had to wait + // to call this deconstructor until now because of all the shared + // references. + unsafe { jpeg_finish_decompress(&mut srcinfo.cinfo); } + // Return it if we got it! - if meta.build() && ! out.is_empty() && out.size < src_size { - Some(out) - } + if happy && ! out.is_empty() && out.size < src_size { Some(out) } else { None } } -/// # Source and Destination Data. +/// # JPEG Source Info. /// -/// This wrapper struct exists to help ensure C memory is freed correctly on -/// exit. -struct InOut { - src_err: jpeg_error_mgr, - src: jpeg_decompress_struct, - dst_err: jpeg_error_mgr, - dst: jpeg_compress_struct, - built: bool, +/// This struct is used to parse the source image details and related errors. +/// The abstraction is primarily used to ensure the C-related resources are +/// correctly broken down on drop. +struct JpegSrcInfo<'a> { + raw: &'a [u8], + cinfo: jpeg_decompress_struct, + err: Box, } -impl Default for InOut { +impl<'a> From<&'a [u8]> for JpegSrcInfo<'a> { #[allow(unsafe_code)] - fn default() -> Self { + fn from(raw: &'a [u8]) -> Self { let mut out = Self { - src_err: unsafe { std::mem::zeroed() }, - src: unsafe { std::mem::zeroed() }, - dst_err: unsafe { std::mem::zeroed() }, - dst: unsafe { std::mem::zeroed() }, - built: false, + raw, + cinfo: unsafe { std::mem::zeroed() }, + err: new_err(), }; - // Initialize the memory. unsafe { - out.src.common.err = jpeg_std_error(&mut out.src_err); - out.dst.common.err = jpeg_std_error(&mut out.dst_err); - jpeg_create_decompress(&mut out.src); - jpeg_create_compress(&mut out.dst); + // Set up the error, then the struct. + out.cinfo.common.err = std::ptr::addr_of_mut!(*out.err); + jpeg_create_decompress(&mut out.cinfo); } - // The trace levels should already match, but just in caseā€¦ - out.src_err.trace_level = out.dst_err.trace_level; - - // Done! out } } -impl InOut { +impl<'a> Drop for JpegSrcInfo<'a> { #[allow(unsafe_code)] - /// # Finish Compression. - fn build(&mut self) -> bool { - // Only build once. - if self.built { false } - else { - unsafe { jpeg_finish_compress(&mut self.dst) }; - self.built = true; - 0 == unsafe { (*self.dst.common.err).msg_code } + fn drop(&mut self) { + unsafe { jpeg_destroy_decompress(&mut self.cinfo); } + } +} + + + +/// # JPEG Destination Info. +/// +/// This struct is used to hold the output-related image details, but not the +/// image itself. +/// +/// On the surface, this looks almost exactly like the `JpegSrcInfo` wrapper, +/// but its error is a raw pointer because `mozjpeg` is really weird. Haha. +struct JpegDstInfo { + cinfo: jpeg_compress_struct, + err: *mut jpeg_error_mgr, + _pin: PhantomPinned, +} + +impl From<&mut JpegSrcInfo<'_>> for JpegDstInfo { + #[allow(unsafe_code)] + fn from(src: &mut JpegSrcInfo<'_>) -> Self { + let mut out = Self { + cinfo: unsafe { std::mem::zeroed() }, + err: Box::into_raw(new_err()), + _pin: PhantomPinned, + }; + + unsafe { + // Set up the error, then the struct. + out.cinfo.common.err = std::ptr::addr_of_mut!(*out.err); + jpeg_create_compress(&mut out.cinfo); + + // Sync the source trace level with the destination. + src.err.trace_level = (*out.err).trace_level; } + + out } } -impl Drop for InOut { +impl Drop for JpegDstInfo { #[allow(unsafe_code)] fn drop(&mut self) { - self.build(); unsafe { - jpeg_destroy_compress(&mut self.dst); - jpeg_finish_decompress(&mut self.src); - jpeg_destroy_decompress(&mut self.src); + jpeg_destroy_compress(&mut self.cinfo); + + // The error pointer is no longer accessible. + let _ = Box::from_raw(self.err); + } + } +} + +impl JpegDstInfo { + #[allow(unsafe_code)] + /// # Finish Compression! + /// + /// This finishes writing the new image, consuming the details struct in + /// the process. + /// + /// A simple `true`/`false` boolean is returned to indicate (likely) + /// success. + fn finish(mut self) -> bool { + unsafe { + jpeg_finish_compress(&mut self.cinfo); + 0 == (*self.cinfo.common.err).msg_code } } } + + + +#[allow(clippy::unnecessary_box_returns, unsafe_code)] +/// # New Unwinding Error. +/// +/// Mozjpeg is largely designed to panic anytime there's an error instead of +/// returning helpful status messages or anything like that. +/// +/// This initializes a new error struct for de/compression use with handlers +/// set to suppress the messaging and unwind. +/// +/// Shout out to the [mozjpeg](https://github.com/ImageOptim/mozjpeg-rust/blob/main/src/errormgr.rs) +/// crate for the inspiration! +fn new_err() -> Box { + unsafe { + let mut err = Box::new(std::mem::zeroed()); + jpeg_std_error(&mut err); + err.error_exit = Some(unwind_error_exit); + err.emit_message = Some(silence_message); + err + } +} + +#[cold] +/// # Error Message. +/// +/// This is a noop method; no error message is printed. +extern "C-unwind" fn silence_message(_cinfo: &mut jpeg_common_struct, _msg_level: c_int) {} + +#[cold] +#[allow(unsafe_code)] +/// # Error Exit. +/// +/// Emit an unwinding panic so we can recover somewhat gracefully from mozjpeg +/// errors. +extern "C-unwind" fn unwind_error_exit(_cinfo: &mut jpeg_common_struct) { + std::panic::resume_unwind(Box::new(())); +} diff --git a/src/image/mod.rs b/src/image/mod.rs index ef617e5..d27dab6 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -67,7 +67,17 @@ pub(super) fn encode(file: &Path, kinds: ImageKind, oxi: &OxipngOptions) -> Opti // Do JPEG stuff? else if ImageKind::is_jpeg(&raw) { if ImageKind::None == kinds & ImageKind::Jpeg { return None; } - encode_mozjpeg(&mut raw); + + // Mozjpeg usually panics on error, so we have to do a weird little + // dance to keep it from killing the whole thread. + raw = std::panic::catch_unwind(move || { + encode_mozjpeg(&mut raw); + raw + }).ok()?; + + // Double-check the image type again, just in case the copy itself + // panicked. + if ! ImageKind::is_jpeg(&raw) { return None; } } // Bad image. else { return None; }