├── test.jpg ├── tests ├── test.icc ├── test.jpg ├── trailer.jpg ├── test.icc.txt ├── decode.rs └── tests.rs ├── .gitignore ├── .github └── dependabot.yml ├── .gitlab-ci.yml ├── src ├── density.rs ├── marker.rs ├── colorspace.rs ├── component.rs ├── errormgr.rs ├── lib.rs ├── qtable.rs ├── readsrc.rs ├── writedst.rs ├── compress.rs └── decompress.rs ├── Cargo.toml ├── README.md └── LICENSE /test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImageOptim/mozjpeg-rust/HEAD/test.jpg -------------------------------------------------------------------------------- /tests/test.icc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImageOptim/mozjpeg-rust/HEAD/tests/test.icc -------------------------------------------------------------------------------- /tests/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImageOptim/mozjpeg-rust/HEAD/tests/test.jpg -------------------------------------------------------------------------------- /tests/trailer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImageOptim/mozjpeg-rust/HEAD/tests/trailer.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | *.dSYM 3 | Cargo.lock 4 | target/ 5 | testout-r1.jpg 6 | testout-r2.jpg 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # This file is a template, and might need editing before it works on your project. 2 | # Unofficial language image. Look for the different tagged releases at: 3 | # https://hub.docker.com/r/scorpil/rust/tags/ 4 | image: "scorpil/rust:stable" 5 | 6 | before_script: 7 | - apt-get update 8 | - apt-get install -yqq --no-install-recommends build-essential nasm 9 | 10 | # Use cargo to test the project 11 | test:cargo: 12 | script: 13 | - rustc --version && cargo --version # Print version info for debugging 14 | - cargo test --verbose 15 | -------------------------------------------------------------------------------- /src/density.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone)] 2 | pub enum PixelDensityUnit { 3 | /// No units 4 | PixelAspectRatio = 0, 5 | /// Pixels per inch 6 | Inches = 1, 7 | /// Pixels per centimeter 8 | Centimeters = 2, 9 | } 10 | 11 | pub struct PixelDensity { 12 | pub unit: PixelDensityUnit, 13 | pub x: u16, 14 | pub y: u16, 15 | } 16 | 17 | impl Default for PixelDensity { 18 | fn default() -> Self { 19 | Self { 20 | unit: PixelDensityUnit::PixelAspectRatio, 21 | x: 1, 22 | y: 1, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/marker.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_int; 2 | 3 | /// Marker number identifier (APP0-APP14 and comment markers) 4 | /// 5 | /// For actual contents of markers, see `MarkerData` 6 | #[derive(Copy, Clone, Debug, PartialEq)] 7 | pub enum Marker { 8 | COM, 9 | APP(u8), 10 | } 11 | 12 | impl From for Marker { 13 | fn from(num: u8) -> Self { 14 | if num == crate::ffi::jpeg_marker::COM as u8 { 15 | Self::COM 16 | } else { 17 | Self::APP(num - crate::ffi::jpeg_marker::APP0 as u8) 18 | } 19 | } 20 | } 21 | 22 | impl From for c_int { 23 | fn from(val: Marker) -> Self { 24 | match val { 25 | Marker::APP(n) => c_int::from(n) + crate::ffi::jpeg_marker::APP0 as c_int, 26 | Marker::COM => crate::ffi::jpeg_marker::COM as c_int, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/test.icc.txt: -------------------------------------------------------------------------------- 1 | Little CMS 2 | Copyright (c) 1998-2011 Marti Maria Saguer 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Kornel "] 3 | categories = ["multimedia::images"] 4 | description = "Higher-level wrapper for Mozilla's JPEG library" 5 | documentation = "https://docs.rs/mozjpeg" 6 | homepage = "https://lib.rs/mozjpeg" 7 | include = ["/README.md", "/Cargo.toml", "/src/*.rs", "LICENSE"] 8 | keywords = ["jpeg", "libjpeg", "image", "encoder", "decoder"] 9 | license = "IJG" 10 | name = "mozjpeg" 11 | readme = "README.md" 12 | repository = "https://github.com/ImageOptim/mozjpeg-rust" 13 | version = "0.10.13" 14 | edition = "2021" 15 | rust-version = "1.71" 16 | 17 | [dependencies] 18 | libc = "0.2.155" 19 | mozjpeg-sys = { version = "2.2.3", default-features = false, features = ["unwinding"] } 20 | rgb = { version = "0.8.50", default-features = false, features = ["bytemuck"] } 21 | arrayvec = "0.7.4" 22 | bytemuck = { version = "1.20", default-features = false, features = ["min_const_generics", "align_offset"] } 23 | 24 | [features] 25 | default = ["mozjpeg-sys/default"] 26 | parallel = ["mozjpeg-sys/parallel"] 27 | nasm_simd = ["mozjpeg-sys/nasm_simd"] 28 | with_simd = ["mozjpeg-sys/with_simd"] 29 | 30 | [package.metadata.docs.rs] 31 | targets = ["x86_64-unknown-linux-gnu"] 32 | rustdoc-args = ["--generate-link-to-definition"] 33 | -------------------------------------------------------------------------------- /src/colorspace.rs: -------------------------------------------------------------------------------- 1 | pub use crate::ffi::J_COLOR_SPACE as ColorSpace; 2 | 3 | pub trait ColorSpaceExt { 4 | /// Number of channels (including unused alpha) in this color space 5 | fn num_components(&self) -> usize; 6 | } 7 | 8 | impl ColorSpaceExt for ColorSpace { 9 | fn num_components(&self) -> usize { 10 | match *self { 11 | ColorSpace::JCS_UNKNOWN => 0, 12 | ColorSpace::JCS_GRAYSCALE => 1, 13 | ColorSpace::JCS_RGB => 3, 14 | ColorSpace::JCS_YCbCr => 3, 15 | ColorSpace::JCS_CMYK => 4, 16 | ColorSpace::JCS_YCCK => 4, 17 | ColorSpace::JCS_EXT_RGB => 3, 18 | ColorSpace::JCS_EXT_RGBX => 4, 19 | ColorSpace::JCS_EXT_BGR => 3, 20 | ColorSpace::JCS_EXT_BGRX => 4, 21 | ColorSpace::JCS_EXT_XBGR => 4, 22 | ColorSpace::JCS_EXT_XRGB => 4, 23 | ColorSpace::JCS_EXT_RGBA => 4, 24 | ColorSpace::JCS_EXT_BGRA => 4, 25 | ColorSpace::JCS_EXT_ABGR => 4, 26 | ColorSpace::JCS_EXT_ARGB => 4, 27 | ColorSpace::JCS_RGB565 => 3, 28 | } 29 | } 30 | } 31 | 32 | #[test] 33 | fn test() { 34 | use crate::ffi; 35 | assert_eq!(3, ffi::J_COLOR_SPACE::JCS_YCbCr.num_components()); 36 | assert_eq!(3, ffi::J_COLOR_SPACE::JCS_RGB.num_components()); 37 | assert_eq!(1, ffi::J_COLOR_SPACE::JCS_GRAYSCALE.num_components()); 38 | } 39 | -------------------------------------------------------------------------------- /src/component.rs: -------------------------------------------------------------------------------- 1 | pub use crate::ffi::jpeg_component_info as CompInfo; 2 | use crate::ffi::DCTSIZE; 3 | use crate::qtable::QTable; 4 | 5 | pub trait CompInfoExt { 6 | /// Number of pixels per row, including padding to MCU 7 | fn row_stride(&self) -> usize; 8 | /// Total height, including padding to MCU 9 | fn col_stride(&self) -> usize; 10 | 11 | /// h,v samplinig (1..4). Number of pixels per sample (may be opposite of what you expect!) 12 | fn sampling(&self) -> (u8, u8); 13 | 14 | // Quantization table, if available 15 | fn qtable(&self) -> Option; 16 | 17 | // Number of blocks per row 18 | fn width_in_blocks(&self) -> usize; 19 | 20 | // Number of block rows 21 | fn height_in_blocks(&self) -> usize; 22 | } 23 | 24 | impl CompInfoExt for CompInfo { 25 | fn qtable(&self) -> Option { 26 | unsafe { self.quant_table.as_ref() }.map(|q_in| { 27 | let mut qtable = QTable { coeffs: [0; 64] }; 28 | for (out, q) in qtable.coeffs.iter_mut().zip(q_in.quantval.iter()) { 29 | *out = u32::from(*q); 30 | } 31 | qtable 32 | }) 33 | } 34 | 35 | fn sampling(&self) -> (u8, u8) { 36 | (self.h_samp_factor as u8, self.v_samp_factor as u8) 37 | } 38 | 39 | fn row_stride(&self) -> usize { 40 | assert!(self.width_in_blocks > 0); 41 | self.width_in_blocks as usize * DCTSIZE 42 | } 43 | 44 | fn col_stride(&self) -> usize { 45 | assert!(self.height_in_blocks > 0); 46 | self.height_in_blocks as usize * DCTSIZE 47 | } 48 | 49 | fn width_in_blocks(&self) -> usize { 50 | self.width_in_blocks as _ 51 | } 52 | 53 | fn height_in_blocks(&self) -> usize { 54 | self.height_in_blocks as _ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust wrapper for MozJPEG library 2 | 3 | This library adds a safe(r) interface on top of libjpeg-turbo and MozJPEG for reading and writing well-compressed JPEG images. 4 | 5 | The interface is still being developed, so it has rough edges and may change. 6 | 7 | In particular, error handling is weird due to libjpeg's peculiar design. Error handling can't use `Result`, but needs to depend on Rust's `resume_unwind` (a panic, basically) to signal any errors in libjpeg. It's necessary to wrap all uses of this library in `catch_unwind`. 8 | 9 | In crates compiled with `panic=abort` setting, any JPEG error will abort the process. 10 | 11 | ## Decoding example 12 | 13 | ```rust 14 | std::panic::catch_unwind(|| -> std::io::Result>> { 15 | let d = mozjpeg::Decompress::with_markers(mozjpeg::ALL_MARKERS) 16 | .from_path("tests/test.jpg")?; 17 | 18 | d.width(); // FYI 19 | d.height(); 20 | d.color_space() == mozjpeg::ColorSpace::JCS_YCbCr; 21 | for marker in d.markers() { /* read metadata or color profiles */ } 22 | 23 | // rgb() enables conversion 24 | let mut image = d.rgb()?; 25 | image.width(); 26 | image.height(); 27 | image.color_space() == mozjpeg::ColorSpace::JCS_RGB; 28 | 29 | let pixels = image.read_scanlines()?; 30 | image.finish()?; 31 | Ok(pixels) 32 | }); 33 | ``` 34 | 35 | ## Encoding example 36 | 37 | ```rust 38 | # let width = 8; let height = 8; 39 | std::panic::catch_unwind(|| -> std::io::Result> { 40 | let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_RGB); 41 | 42 | comp.set_size(width, height); 43 | let mut comp = comp.start_compress(Vec::new())?; // any io::Write will work 44 | 45 | // replace with your image data 46 | let pixels = vec![0u8; width * height * 3]; 47 | comp.write_scanlines(&pixels[..])?; 48 | 49 | let writer = comp.finish()?; 50 | Ok(writer) 51 | }); 52 | ``` 53 | -------------------------------------------------------------------------------- /src/errormgr.rs: -------------------------------------------------------------------------------- 1 | use crate::ffi; 2 | use crate::ffi::jpeg_common_struct; 3 | use std::borrow::Cow; 4 | use std::mem; 5 | use std::os::raw::c_int; 6 | 7 | pub use crate::ffi::jpeg_error_mgr as ErrorMgr; 8 | 9 | #[allow(clippy::unnecessary_box_returns)] 10 | pub(crate) fn unwinding_error_mgr() -> Box { 11 | unsafe { 12 | let mut err = Box::new(mem::zeroed()); 13 | ffi::jpeg_std_error(&mut err); 14 | err.error_exit = Some(unwind_error_exit); 15 | err.emit_message = Some(silence_message); 16 | err 17 | } 18 | } 19 | 20 | #[cold] 21 | fn formatted_message(prefix: &str, cinfo: &mut jpeg_common_struct) -> String { 22 | unsafe { 23 | let err = cinfo.err.as_ref().unwrap(); 24 | match err.format_message { 25 | Some(fmt) => { 26 | let mut buffer = mem::zeroed(); 27 | let correct_fn_type = mem::transmute::< 28 | unsafe extern "C-unwind" fn(cinfo: &mut jpeg_common_struct, buffer: &[u8; 80]), 29 | unsafe extern "C-unwind" fn(cinfo: &mut jpeg_common_struct, buffer: &mut [u8; 80])>(fmt); 30 | (correct_fn_type)(cinfo, &mut buffer); 31 | let buf = buffer.split(|&c| c == 0).next().unwrap_or_default(); 32 | let msg = String::from_utf8_lossy(buf); 33 | let mut out = String::with_capacity(prefix.len() + msg.len()); 34 | push_str_in_cap(&mut out, prefix); 35 | push_str_in_cap(&mut out, &msg); 36 | out 37 | }, 38 | None => format!("{}code {}", prefix, err.msg_code), 39 | } 40 | } 41 | } 42 | 43 | fn push_str_in_cap(out: &mut String, s: &str) { 44 | let needs_to_grow = s.len() > out.capacity().wrapping_sub(out.len()); 45 | if !needs_to_grow { 46 | out.push_str(s); 47 | } 48 | } 49 | 50 | #[cold] 51 | extern "C-unwind" fn silence_message(_cinfo: &mut jpeg_common_struct, _level: c_int) { 52 | } 53 | 54 | #[cold] 55 | extern "C-unwind" fn unwind_error_exit(cinfo: &mut jpeg_common_struct) { 56 | let msg = formatted_message("libjpeg fatal error: ", cinfo); 57 | // avoids calling panic handler 58 | std::panic::resume_unwind(Box::new(msg)); 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Independent JPEG Group's JPEG software 2 | ========================================== 3 | 4 | This distribution contains a release of the Independent JPEG Group's free JPEG 5 | software. You are welcome to redistribute this software and to use it for any 6 | purpose, subject to the conditions under LEGAL ISSUES, below. 7 | 8 | This software is the work of Tom Lane, Guido Vollbeding, Philip Gladstone, 9 | Bill Allombert, Jim Boucher, Lee Crocker, Bob Friesenhahn, Ben Jackson, 10 | Julian Minguillon, Luis Ortiz, George Phillips, Davide Rossi, Ge' Weijers, 11 | and other members of the Independent JPEG Group. 12 | 13 | IJG is not affiliated with the ISO/IEC JTC1/SC29/WG1 standards committee 14 | (also known as JPEG, together with ITU-T SG16). 15 | 16 | ---- 17 | 18 | Copyright (C)2009-2023 D. R. Commander. All Rights Reserved.
19 | Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. 20 | 21 | Redistribution and use in source and binary forms, with or without 22 | modification, are permitted provided that the following conditions are met: 23 | 24 | - Redistributions of source code must retain the above copyright notice, 25 | this list of conditions and the following disclaimer. 26 | - Redistributions in binary form must reproduce the above copyright notice, 27 | this list of conditions and the following disclaimer in the documentation 28 | and/or other materials provided with the distribution. 29 | - Neither the name of the libjpeg-turbo Project nor the names of its 30 | contributors may be used to endorse or promote products derived from this 31 | software without specific prior written permission. 32 | 33 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", 34 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 35 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 36 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 37 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 38 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 39 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 40 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 41 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 42 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 43 | POSSIBILITY OF SUCH DAMAGE. 44 | -------------------------------------------------------------------------------- /tests/decode.rs: -------------------------------------------------------------------------------- 1 | use mozjpeg::*; 2 | use std::sync::LazyLock; 3 | 4 | static RGB: LazyLock> = LazyLock::new(|| { 5 | let d = Decompress::with_markers(ALL_MARKERS) 6 | .from_path("tests/test.jpg") 7 | .unwrap(); 8 | 9 | assert_eq!(45, d.width()); 10 | assert_eq!(30, d.height()); 11 | assert_eq!(1.0, d.gamma()); 12 | assert_eq!(ColorSpace::JCS_YCbCr, d.color_space()); 13 | assert_eq!(1, d.markers().count()); 14 | 15 | let mut image = d.rgb().unwrap(); 16 | assert_eq!(45, image.width()); 17 | assert_eq!(30, image.height()); 18 | assert_eq!(ColorSpace::JCS_RGB, image.color_space()); 19 | 20 | image.read_scanlines::<[u8; 3]>().unwrap() 21 | }); 22 | 23 | #[test] 24 | fn decode_test_rgba() { 25 | let d = Decompress::with_markers(ALL_MARKERS) 26 | .from_path("tests/test.jpg") 27 | .unwrap(); 28 | 29 | assert_eq!(45, d.width()); 30 | assert_eq!(30, d.height()); 31 | assert_eq!(1.0, d.gamma()); 32 | assert_eq!(ColorSpace::JCS_YCbCr, d.color_space()); 33 | assert_eq!(1, d.markers().count()); 34 | 35 | let mut image = d.rgba().unwrap(); 36 | assert_eq!(45, image.width()); 37 | assert_eq!(30, image.height()); 38 | assert_eq!(ColorSpace::JCS_EXT_RGBA, image.color_space()); 39 | 40 | let rgba = image.read_scanlines::<[u8; 4]>().unwrap(); 41 | assert!(rgba.iter().map(|px| &px[..3]).eq(RGB.iter())); 42 | } 43 | 44 | #[test] 45 | fn decode_test_argb() { 46 | let d = Decompress::with_markers(ALL_MARKERS) 47 | .from_path("tests/test.jpg") 48 | .unwrap(); 49 | 50 | assert_eq!(45, d.width()); 51 | assert_eq!(30, d.height()); 52 | assert_eq!(1.0, d.gamma()); 53 | assert_eq!(ColorSpace::JCS_YCbCr, d.color_space()); 54 | assert_eq!(1, d.markers().count()); 55 | 56 | let mut image = d.to_colorspace(ColorSpace::JCS_EXT_ARGB).unwrap(); 57 | assert_eq!(45, image.width()); 58 | assert_eq!(30, image.height()); 59 | assert_eq!(ColorSpace::JCS_EXT_ARGB, image.color_space()); 60 | 61 | let rgba = image.read_scanlines::<[u8; 4]>().unwrap(); 62 | assert!(rgba.iter().map(|px| &px[1..]).eq(RGB.iter())); 63 | } 64 | 65 | #[test] 66 | fn decode_test_rgb_flat() { 67 | let d = Decompress::with_markers(ALL_MARKERS) 68 | .from_path("tests/test.jpg") 69 | .unwrap(); 70 | 71 | assert_eq!(45, d.width()); 72 | assert_eq!(30, d.height()); 73 | assert_eq!(1.0, d.gamma()); 74 | assert_eq!(ColorSpace::JCS_YCbCr, d.color_space()); 75 | assert_eq!(1, d.markers().count()); 76 | 77 | let mut image = d.rgb().unwrap(); 78 | assert_eq!(45, image.width()); 79 | assert_eq!(30, image.height()); 80 | assert_eq!(ColorSpace::JCS_RGB, image.color_space()); 81 | 82 | let buf_size = image.min_flat_buffer_size(); 83 | let buf = image.read_scanlines::().unwrap(); 84 | 85 | assert_eq!(buf.len(), buf_size); 86 | 87 | assert!(buf.chunks_exact(3).eq(RGB.iter())); 88 | } 89 | 90 | #[test] 91 | fn decode_test_rgba_flat() { 92 | for space in [ColorSpace::JCS_EXT_RGBA, ColorSpace::JCS_EXT_RGBX] { 93 | let d = Decompress::with_markers(ALL_MARKERS) 94 | .from_path("tests/test.jpg") 95 | .unwrap(); 96 | 97 | assert_eq!(45, d.width()); 98 | assert_eq!(30, d.height()); 99 | assert_eq!(1.0, d.gamma()); 100 | assert_eq!(ColorSpace::JCS_YCbCr, d.color_space()); 101 | assert_eq!(1, d.markers().count()); 102 | 103 | let mut image = d.to_colorspace(space).unwrap(); 104 | assert_eq!(45, image.width()); 105 | assert_eq!(30, image.height()); 106 | assert_eq!(space, image.color_space()); 107 | 108 | let buf_size = image.min_flat_buffer_size(); 109 | let buf = image.read_scanlines::().unwrap(); 110 | assert_eq!(buf.len(), buf_size); 111 | 112 | assert!(buf.chunks_exact(4).map(|px| &px[..3]).eq(RGB.iter())); 113 | } 114 | } 115 | 116 | #[test] 117 | fn decode_failure_test() { 118 | assert!(std::panic::catch_unwind(|| { 119 | Decompress::with_markers(ALL_MARKERS) 120 | .from_path("tests/test.rs") 121 | .unwrap(); 122 | }) 123 | .is_err()); 124 | } 125 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(unused_attributes)] 3 | #![allow(unused_imports)] 4 | #![allow(clippy::manual_range_contains)] 5 | 6 | use mozjpeg_sys as ffi; 7 | 8 | pub use crate::colorspace::ColorSpace; 9 | pub use crate::colorspace::ColorSpaceExt; 10 | pub use crate::component::CompInfo; 11 | pub use crate::component::CompInfoExt; 12 | pub use crate::compress::Compress; 13 | pub use crate::compress::ScanMode; 14 | pub use crate::decompress::{DctMethod, Format}; 15 | pub use crate::decompress::{Decompress, ALL_MARKERS, NO_MARKERS}; 16 | pub use crate::density::{PixelDensity, PixelDensityUnit}; 17 | use crate::ffi::boolean; 18 | use crate::ffi::jpeg_common_struct; 19 | use crate::ffi::jpeg_compress_struct; 20 | pub use crate::ffi::DCTSIZE; 21 | use crate::ffi::JDIMENSION; 22 | pub use crate::ffi::JPEG_LIB_VERSION; 23 | use crate::ffi::J_BOOLEAN_PARAM; 24 | use crate::ffi::J_INT_PARAM; 25 | pub use crate::marker::Marker; 26 | 27 | use libc::free; 28 | use std::cmp::min; 29 | use std::mem; 30 | use std::os::raw::{c_int, c_uchar, c_ulong, c_void}; 31 | use std::ptr; 32 | use std::slice; 33 | 34 | mod colorspace; 35 | mod component; 36 | pub mod compress; 37 | pub mod decompress; 38 | mod density; 39 | mod errormgr; 40 | mod marker; 41 | /// Quantization table presets from MozJPEG 42 | pub mod qtable; 43 | mod readsrc; 44 | mod writedst; 45 | 46 | #[test] 47 | fn recompress() { 48 | use crate::colorspace::{ColorSpace, ColorSpaceExt}; 49 | use std::fs::File; 50 | use std::io::{Read, Write}; 51 | 52 | let dinfo = Decompress::new_path("tests/test.jpg").unwrap(); 53 | 54 | assert_eq!(1.0, dinfo.gamma()); 55 | assert_eq!(ColorSpace::JCS_YCbCr, dinfo.color_space()); 56 | assert_eq!(dinfo.components().len(), dinfo.color_space().num_components()); 57 | 58 | let samp_factors = dinfo.components().iter().map(|c| c.v_samp_factor).collect::>(); 59 | 60 | assert_eq!((45, 30), dinfo.size()); 61 | 62 | let mut dinfo = dinfo.raw().unwrap(); 63 | 64 | let mut bitmaps = [&mut Vec::new(), &mut Vec::new(), &mut Vec::new()]; 65 | dinfo.read_raw_data(&mut bitmaps); 66 | 67 | dinfo.finish().unwrap(); 68 | 69 | fn write_jpeg(bitmaps: &[&mut Vec; 3], samp_factors: &Vec, scale: (f32, f32)) -> Vec { 70 | let mut cinfo = Compress::new(ColorSpace::JCS_YCbCr); 71 | 72 | cinfo.set_size(45, 30); 73 | 74 | #[allow(deprecated)] { 75 | cinfo.set_gamma(1.0); 76 | } 77 | 78 | cinfo.set_raw_data_in(true); 79 | 80 | cinfo.set_quality(100.); 81 | 82 | cinfo.set_luma_qtable(&qtable::AnnexK_Luma.scaled(99. * scale.0, 90. * scale.1)); 83 | cinfo.set_chroma_qtable(&qtable::AnnexK_Chroma.scaled(100. * scale.0, 60. * scale.1)); 84 | 85 | for (c, samp) in cinfo.components_mut().iter_mut().zip(samp_factors) { 86 | c.v_samp_factor = *samp; 87 | c.h_samp_factor = *samp; 88 | } 89 | 90 | let mut cinfo = cinfo.start_compress(Vec::new()).unwrap(); 91 | 92 | assert!(cinfo.write_raw_data(&bitmaps.iter().map(|c| &c[..]).collect::>())); 93 | 94 | cinfo.finish().unwrap() 95 | } 96 | 97 | let data1 = &write_jpeg(&bitmaps, &samp_factors, (1., 1.)); 98 | let data1_len = data1.len(); 99 | let data2 = &write_jpeg(&bitmaps, &samp_factors, (0.5, 0.5)); 100 | let data2_len = data2.len(); 101 | 102 | File::create("testout-r1.jpg").unwrap().write_all(data1).unwrap(); 103 | File::create("testout-r2.jpg").unwrap().write_all(data2).unwrap(); 104 | 105 | assert!(data1_len > data2_len); 106 | } 107 | 108 | #[cold] 109 | fn fail(cinfo: &mut jpeg_common_struct, code: c_int) -> ! { 110 | unsafe { 111 | let err = &mut *cinfo.err; 112 | err.msg_code = code; 113 | if let Some(e) = err.error_exit { 114 | (e)(cinfo); // should have been defined as ! 115 | } 116 | std::process::abort(); 117 | } 118 | } 119 | 120 | fn warn(cinfo: &mut jpeg_common_struct, code: c_int) { 121 | unsafe { 122 | let err = &mut *cinfo.err; 123 | err.msg_code = code; 124 | if let Some(e) = err.emit_message { 125 | (e)(cinfo, -1); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | use mozjpeg::*; 2 | 3 | pub fn decompress_jpeg(jpeg: &[u8]) -> Vec> { 4 | let decomp = mozjpeg::Decompress::new_mem(jpeg).unwrap(); 5 | 6 | let mut bitmaps:Vec<_> = decomp.components().iter().map(|c|{ 7 | Vec::with_capacity(c.row_stride() * c.col_stride()) 8 | }).collect(); 9 | 10 | let mut decomp = decomp.raw().unwrap(); 11 | { 12 | let mut bitmap_refs: Vec<_> = bitmaps.iter_mut().collect(); 13 | decomp.read_raw_data(&mut bitmap_refs); 14 | decomp.finish().unwrap(); 15 | } 16 | 17 | bitmaps 18 | } 19 | 20 | #[test] 21 | fn color_jpeg() { 22 | for size in 1..64 { 23 | let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_RGB); 24 | 25 | comp.set_scan_optimization_mode(mozjpeg::ScanMode::AllComponentsTogether); 26 | comp.set_size(size, size); 27 | let mut comp = comp.start_compress(Vec::new()).unwrap(); 28 | 29 | let lines = vec![128; size * size * 3]; 30 | comp.write_scanlines(&lines[..]).unwrap(); 31 | 32 | let jpeg = comp.finish().unwrap(); 33 | assert!(!jpeg.is_empty()); 34 | decompress_jpeg(&jpeg); 35 | } 36 | } 37 | 38 | #[test] 39 | fn raw_jpeg() { 40 | for size in 1..64 { 41 | let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_YCbCr); 42 | 43 | comp.set_scan_optimization_mode(mozjpeg::ScanMode::AllComponentsTogether); 44 | 45 | comp.set_raw_data_in(true); 46 | comp.set_size(size, size); 47 | 48 | let mut comp = comp.start_compress(Vec::new()).unwrap(); 49 | 50 | let rounded_size = (size + 7) / 8 * 8; 51 | let t = vec![128; rounded_size * rounded_size]; 52 | let components = [&t[..], &t[..], &t[..]]; 53 | comp.write_raw_data(&components[..]); 54 | 55 | let jpeg = comp.finish().unwrap(); 56 | assert!(!jpeg.is_empty()); 57 | decompress_jpeg(&jpeg); 58 | } 59 | } 60 | 61 | #[test] 62 | fn decode_test() { 63 | let d = Decompress::with_markers(ALL_MARKERS) 64 | .from_path("tests/test.jpg") 65 | .unwrap(); 66 | 67 | assert_eq!(45, d.width()); 68 | assert_eq!(30, d.height()); 69 | assert_eq!(1.0, d.gamma()); 70 | assert_eq!(ColorSpace::JCS_YCbCr, d.color_space()); 71 | assert_eq!(1, d.markers().count()); 72 | 73 | let image = d.rgb().unwrap(); 74 | assert_eq!(45, image.width()); 75 | assert_eq!(30, image.height()); 76 | assert_eq!(ColorSpace::JCS_RGB, image.color_space()); 77 | } 78 | 79 | #[test] 80 | fn decode_failure_test() { 81 | assert!(std::panic::catch_unwind(|| { 82 | Decompress::with_markers(ALL_MARKERS) 83 | .from_path("tests/test.rs") 84 | .unwrap(); 85 | }) 86 | .is_err()); 87 | } 88 | 89 | #[test] 90 | fn roundtrip() { 91 | let decoded = decode_jpeg(&std::fs::read("tests/test.jpg").unwrap()); 92 | decode_jpeg(&encode_subsampled_jpeg(decoded)); 93 | } 94 | 95 | #[test] 96 | fn icc_profile() { 97 | let decoded = decode_jpeg(&std::fs::read("tests/test.jpg").unwrap()); 98 | let img = encode_jpeg_with_icc_profile(decoded); 99 | let d = Decompress::with_markers(ALL_MARKERS) 100 | .from_mem(&img) 101 | .unwrap(); 102 | 103 | assert_eq!(45, d.width()); 104 | assert_eq!(30, d.height()); 105 | assert_eq!(1.0, d.gamma()); 106 | assert_eq!(ColorSpace::JCS_YCbCr, d.color_space()); 107 | assert_eq!(10, d.markers().count()); // 9 for icc profile 108 | 109 | // silly checks 110 | d.markers().skip(1).for_each(|marker| { 111 | assert!(marker.data.starts_with(b"ICC_PROFILE\0")); 112 | }); 113 | 114 | let image = d.rgb().unwrap(); 115 | assert_eq!(45, image.width()); 116 | assert_eq!(30, image.height()); 117 | assert_eq!(ColorSpace::JCS_RGB, image.color_space()); 118 | } 119 | 120 | fn encode_subsampled_jpeg((width, height, data): (usize, usize, Vec<[u8; 3]>)) -> Vec { 121 | let mut encoder = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_RGB); 122 | encoder.set_size(width, height); 123 | 124 | encoder.set_color_space(mozjpeg::ColorSpace::JCS_YCbCr); 125 | { 126 | let comp = encoder.components_mut(); 127 | comp[0].h_samp_factor = 1; 128 | comp[0].v_samp_factor = 1; 129 | 130 | let (h, v) = (2, 2); // CbCr420 subsampling factors 131 | // 0 - Y, 1 - Cb, 2 - Cr, 3 - K 132 | comp[1].h_samp_factor = h; 133 | comp[1].v_samp_factor = v; 134 | comp[2].h_samp_factor = h; 135 | comp[2].v_samp_factor = v; 136 | } 137 | 138 | let mut encoder = encoder.start_compress(Vec::new()).unwrap(); 139 | let _ = encoder.write_scanlines(bytemuck::cast_slice(&data)); 140 | encoder.finish().unwrap() 141 | } 142 | 143 | fn encode_jpeg_with_icc_profile((width, height, data): (usize, usize, Vec<[u8; 3]>)) -> Vec { 144 | let mut encoder = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_RGB); 145 | encoder.set_size(width, height); 146 | 147 | encoder.set_color_space(mozjpeg::ColorSpace::JCS_YCbCr); 148 | 149 | let mut encoder = encoder.start_compress(Vec::new()).unwrap(); 150 | 151 | encoder.write_icc_profile(&std::fs::read("tests/test.icc").unwrap()); 152 | 153 | let _ = encoder.write_scanlines(bytemuck::cast_slice(&data)); 154 | encoder.finish().unwrap() 155 | } 156 | 157 | fn decode_jpeg(buffer: &[u8]) -> (usize, usize, Vec<[u8; 3]>) { 158 | let mut decoder = match mozjpeg::Decompress::new_mem(buffer).unwrap().image().unwrap() { 159 | mozjpeg::decompress::Format::RGB(d) => d, 160 | _ => unimplemented!(), 161 | }; 162 | 163 | let width = decoder.width(); 164 | let height = decoder.height(); 165 | 166 | let image = decoder.read_scanlines::<[u8; 3]>().unwrap(); 167 | 168 | (width, height, image) 169 | } 170 | -------------------------------------------------------------------------------- /src/qtable.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | 3 | use std::cmp::{max, min}; 4 | use std::fmt; 5 | use std::os::raw::c_uint; 6 | type Coef = c_uint; 7 | 8 | pub struct QTable { 9 | pub(crate) coeffs: [Coef; 64], 10 | } 11 | 12 | impl PartialEq for QTable { 13 | fn eq(&self, other: &Self) -> bool { 14 | let iter2 = other.coeffs.iter().copied(); 15 | self.coeffs.iter().copied().zip(iter2).all(|(s, o)| s == o) 16 | } 17 | } 18 | 19 | impl fmt::Debug for QTable { 20 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 21 | write!(fmt, "QTable{{coeffs:{:?}}}", &self.coeffs[..]) 22 | } 23 | } 24 | 25 | const low_weights : [f32; 19] = [ 26 | 1.00, 0.85, 0.55, 0., 0., 0., 0., 0., 27 | 0.85, 0.75, 0.10, 0., 0., 0., 0., 0., 28 | 0.55, 0.10, 0.05, 29 | ]; 30 | 31 | impl QTable { 32 | #[must_use] 33 | pub fn compare(&self, other: &Self) -> (f32, f32) { 34 | let mut scales = [0.; 64]; 35 | for (s, (&a, &b)) in scales.iter_mut().zip(self.coeffs.iter().zip(other.coeffs.iter())) { 36 | *s = if b > 0 { a as f32 / b as f32 } else { 0. }; 37 | } 38 | let avg = scales.iter().sum::() / 64.; 39 | let var = scales.iter().map(|&v| (v - avg).powi(2)).sum::() / 64.; 40 | (avg, var) 41 | } 42 | 43 | #[must_use] 44 | pub fn scaled(&self, dc_quality: f32, ac_quality: f32) -> Self { 45 | let dc_scaling = Self::quality_scaling(dc_quality); 46 | let ac_scaling = Self::quality_scaling(ac_quality); 47 | 48 | let mut out = [0; 64]; 49 | { 50 | debug_assert_eq!(self.coeffs.len(), out.len()); 51 | debug_assert!(low_weights.len() < self.coeffs.len()); 52 | 53 | let (low_coefs, high_coefs) = self.coeffs.split_at(low_weights.len()); 54 | let (low_out, high_out) = out.split_at_mut(low_weights.len()); 55 | 56 | // TODO: that could be improved for 1x2 and 2x1 subsampling 57 | for ((out, coef), w) in low_out.iter_mut().zip(low_coefs).zip(&low_weights) { 58 | *out = ((*coef as f32 * (dc_scaling * w + ac_scaling * (1.-w))).round() as Coef).clamp(1, 255); 59 | } 60 | for (out, coef) in high_out.iter_mut().zip(high_coefs) { 61 | *out = ((*coef as f32 * ac_scaling).round() as Coef).clamp(1, 255); 62 | } 63 | } 64 | Self { coeffs: out } 65 | } 66 | 67 | #[must_use] 68 | pub fn as_ptr(&self) -> *const c_uint { 69 | self.coeffs.as_ptr() 70 | } 71 | 72 | // Similar to libjpeg, but result is 100x smaller 73 | fn quality_scaling(quality: f32) -> f32 { 74 | assert!(quality > 0. && quality <= 100.); 75 | 76 | if quality < 50. { 77 | 50. / quality 78 | } else { 79 | (100. - quality) / 50. 80 | } 81 | } 82 | } 83 | 84 | pub static AnnexK_Luma: QTable = QTable { 85 | coeffs: [ 86 | 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 87 | 56, 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 88 | 104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99, 89 | ], 90 | }; 91 | 92 | pub static AnnexK_Chroma: QTable = QTable { 93 | coeffs: [ 94 | 17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 95 | 99, 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 96 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 97 | ], 98 | }; 99 | 100 | pub static Flat: QTable = QTable { 101 | coeffs: [ 102 | 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 103 | 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 104 | 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 105 | ], 106 | }; 107 | 108 | pub static MSSSIM_Luma: QTable = QTable { 109 | coeffs: [ 110 | 12, 17, 20, 21, 30, 34, 56, 63, 18, 20, 20, 26, 28, 51, 61, 55, 19, 20, 21, 26, 33, 58, 69, 111 | 55, 26, 26, 26, 30, 46, 87, 86, 66, 31, 33, 36, 40, 46, 96, 100, 73, 40, 35, 46, 62, 81, 112 | 100, 111, 91, 46, 66, 76, 86, 102, 121, 120, 101, 68, 90, 90, 96, 113, 102, 105, 103, 113 | ], 114 | }; 115 | 116 | pub static MSSSIM_Chroma: QTable = QTable { 117 | coeffs: [ 118 | 8, 12, 15, 15, 86, 96, 96, 98, 13, 13, 15, 26, 90, 96, 99, 98, 12, 15, 18, 96, 99, 99, 99, 119 | 99, 17, 16, 90, 96, 99, 99, 99, 99, 96, 96, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 120 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 121 | ], 122 | }; 123 | 124 | pub static NRobidoux: QTable = QTable { 125 | coeffs: [ 126 | 16, 16, 16, 18, 25, 37, 56, 85, 16, 17, 20, 27, 34, 40, 53, 75, 16, 20, 24, 31, 43, 62, 91, 127 | 135, 18, 27, 31, 40, 53, 74, 106, 156, 25, 34, 43, 53, 69, 94, 131, 189, 37, 40, 62, 74, 128 | 94, 124, 169, 238, 56, 53, 91, 106, 131, 169, 226, 311, 85, 75, 135, 156, 189, 238, 311, 129 | 418, 130 | ], 131 | }; 132 | 133 | pub static PSNRHVS_Luma: QTable = QTable { 134 | coeffs: [ 135 | 9, 10, 12, 14, 27, 32, 51, 62, 11, 12, 14, 19, 27, 44, 59, 73, 12, 14, 18, 25, 42, 59, 79, 136 | 78, 17, 18, 25, 42, 61, 92, 87, 92, 23, 28, 42, 75, 79, 112, 112, 99, 40, 42, 59, 84, 88, 137 | 124, 132, 111, 42, 64, 78, 95, 105, 126, 125, 99, 70, 75, 100, 102, 116, 100, 107, 98, 138 | ], 139 | }; 140 | pub static PSNRHVS_Chroma: QTable = QTable { 141 | coeffs: [ 142 | 9, 10, 17, 19, 62, 89, 91, 97, 12, 13, 18, 29, 84, 91, 88, 98, 14, 19, 29, 93, 95, 95, 98, 143 | 97, 20, 26, 84, 88, 95, 95, 98, 94, 26, 86, 91, 93, 97, 99, 98, 99, 99, 100, 98, 99, 99, 144 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 97, 97, 99, 99, 99, 99, 97, 99, 145 | ], 146 | }; 147 | 148 | pub static KleinSilversteinCarney: QTable = QTable { 149 | coeffs: [ 150 | /* Relevance of human vision to JPEG-DCT compression (1992) Klein, Silverstein and Carney. 151 | */ 152 | 10, 12, 14, 19, 26, 38, 57, 86, 12, 18, 21, 28, 35, 41, 54, 76, 14, 21, 25, 32, 44, 63, 92, 153 | 136, 19, 28, 32, 41, 54, 75, 107, 157, 26, 35, 44, 54, 70, 95, 132, 190, 38, 41, 63, 75, 154 | 95, 125, 170, 239, 57, 54, 92, 107, 132, 170, 227, 312, 86, 76, 136, 157, 190, 239, 312, 155 | 419, 156 | ], 157 | }; 158 | 159 | pub static WatsonTaylorBorthwick: QTable = QTable { 160 | coeffs: [ 161 | /* DCTune perceptual optimization of compressed dental X-Rays (1997) Watson, Taylor, Borthwick 162 | */ 163 | 7, 8, 10, 14, 23, 44, 95, 241, 8, 8, 11, 15, 25, 47, 102, 255, 10, 11, 13, 19, 31, 58, 127, 164 | 255, 14, 15, 19, 27, 44, 83, 181, 255, 23, 25, 31, 44, 72, 136, 255, 255, 44, 47, 58, 83, 165 | 136, 255, 255, 255, 95, 102, 127, 181, 255, 255, 255, 255, 241, 255, 255, 255, 255, 255, 166 | 255, 255, 167 | ], 168 | }; 169 | 170 | pub static AhumadaWatsonPeterson: QTable = QTable { 171 | coeffs: [ 172 | /* A visual detection model for DCT coefficient quantization (12/9/93) Ahumada, Watson, Peterson 173 | */ 174 | 15, 11, 11, 12, 15, 19, 25, 32, 11, 13, 10, 10, 12, 15, 19, 24, 11, 10, 14, 14, 16, 18, 22, 175 | 27, 12, 10, 14, 18, 21, 24, 28, 33, 15, 12, 16, 21, 26, 31, 36, 42, 19, 15, 18, 24, 31, 38, 176 | 45, 53, 25, 19, 22, 28, 36, 45, 55, 65, 32, 24, 27, 33, 42, 53, 65, 77, 177 | ], 178 | }; 179 | 180 | pub static PetersonAhumadaWatson: QTable = QTable { 181 | coeffs: [ 182 | /* An improved detection model for DCT coefficient quantization (1993) Peterson, Ahumada and Watson 183 | */ 184 | 14, 10, 11, 14, 19, 25, 34, 45, 10, 11, 11, 12, 15, 20, 26, 33, 11, 11, 15, 18, 21, 25, 31, 185 | 38, 14, 12, 18, 24, 28, 33, 39, 47, 19, 15, 21, 28, 36, 43, 51, 59, 25, 20, 25, 33, 43, 54, 186 | 64, 74, 34, 26, 31, 39, 51, 64, 77, 91, 45, 33, 38, 47, 59, 74, 91, 108, 187 | ], 188 | }; 189 | 190 | pub static ALL_TABLES: [(&str, &QTable); 12] = [ 191 | ("Annex-K Luma", &AnnexK_Luma), 192 | ("Annex-K Chroma", &AnnexK_Chroma), 193 | ("Flat", &Flat), 194 | ("MSSSIM Luma", &MSSSIM_Luma), 195 | ("MSSSIM Chroma", &MSSSIM_Chroma), 196 | ("N. Robidoux", &NRobidoux), 197 | ("PSNRHVS Luma", &PSNRHVS_Luma), 198 | ("PSNRHVS Chroma", &PSNRHVS_Chroma), 199 | ("Klein, Silverstein, Carney", &KleinSilversteinCarney), 200 | ("Watson, Taylor, Borthwick", &WatsonTaylorBorthwick), 201 | ("Ahumada, Watson, Peterson", &AhumadaWatsonPeterson), 202 | ("Peterson, Ahumada, Watson", &PetersonAhumadaWatson), 203 | ]; 204 | 205 | #[test] 206 | fn scaling() { 207 | assert_eq!(QTable { coeffs: [100; 64] }, QTable { coeffs: [100; 64] }); 208 | assert!(QTable { coeffs: [1; 64] } != QTable { coeffs: [2; 64] }); 209 | 210 | assert_eq!(QTable{coeffs:[36; 64]}, Flat.scaled(22.,22.)); 211 | assert_eq!(QTable{coeffs:[8; 64]}, Flat.scaled(75.,75.)); 212 | assert_eq!(QTable{coeffs:[1; 64]}, Flat.scaled(100.,100.)); 213 | assert_eq!(QTable{coeffs:[2; 64]}, Flat.scaled(95.,95.)); 214 | assert_eq!(QTable{coeffs:[ 215 | 2, 6, 15, 32, 32, 32, 32, 32, 216 | 6, 9, 29, 32, 32, 32, 32, 32, 217 | 15, 29, 30, 32, 32, 32, 32, 32, 218 | 32, 32, 32, 32, 32, 32, 32, 32, 219 | 32, 32, 32, 32, 32, 32, 32, 32, 220 | 32, 32, 32, 32, 32, 32, 32, 32, 221 | 32, 32, 32, 32, 32, 32, 32, 32, 222 | 32, 32, 32, 32, 32, 32, 32, 32]}, Flat.scaled(95.,25.)); 223 | assert_eq!(PetersonAhumadaWatson, PetersonAhumadaWatson.scaled(50.,50.)); 224 | 225 | assert_eq!(QTable { coeffs: [1; 64] }, NRobidoux.scaled(99.9, 99.9)); 226 | assert_eq!(QTable { coeffs: [1; 64] }, MSSSIM_Chroma.scaled(99.8, 99.8)); 227 | } 228 | -------------------------------------------------------------------------------- /src/readsrc.rs: -------------------------------------------------------------------------------- 1 | use crate::{fail, warn}; 2 | use mozjpeg_sys::boolean; 3 | use mozjpeg_sys::jpeg_decompress_struct; 4 | use mozjpeg_sys::JERR_BAD_LENGTH; 5 | use mozjpeg_sys::{jpeg_common_struct, jpeg_resync_to_restart, jpeg_source_mgr}; 6 | use mozjpeg_sys::{JERR_FILE_READ, JERR_VIRTUAL_BUG}; 7 | use mozjpeg_sys::{JPOOL_IMAGE, JPOOL_PERMANENT, JWRN_JPEG_EOF}; 8 | use std::cell::UnsafeCell; 9 | use std::io::{self, BufRead, BufReader, Read}; 10 | use std::marker::PhantomPinned; 11 | use std::mem::MaybeUninit; 12 | use std::os::raw::{c_int, c_long, c_uint, c_void}; 13 | use std::panic::{RefUnwindSafe, UnwindSafe}; 14 | use std::ptr; 15 | use std::ptr::NonNull; 16 | 17 | pub(crate) struct SourceMgr { 18 | /// The `jpeg_source_mgr` has requirements that are tricky for Rust: 19 | /// * it must have a stable address, 20 | /// * it is mutated via `cinfo.src` raw pointer via C, while the `SourceMgr` stored elsewhere owns it. 21 | /// This requires interior mutability and a non-exclusive ownership (`Box` would be useless). 22 | inner_shared: *mut UnsafeCell>, 23 | } 24 | 25 | impl UnwindSafe for SourceMgr {} 26 | impl RefUnwindSafe for SourceMgr {} 27 | 28 | #[repr(C)] 29 | pub(crate) struct SourceMgrInner { 30 | pub(crate) iface: jpeg_source_mgr, 31 | to_consume: usize, 32 | reader: R, 33 | // jpeg_source_mgr callbacks get a pointer to the struct 34 | _pinned: PhantomPinned, 35 | } 36 | 37 | impl SourceMgr { 38 | #[inline] 39 | pub(crate) fn new(reader: R) -> io::Result { 40 | let mut src = SourceMgrInner { 41 | iface: jpeg_source_mgr { 42 | next_input_byte: ptr::null_mut(), 43 | bytes_in_buffer: 0, 44 | init_source: Some(SourceMgrInner::::init_source), 45 | fill_input_buffer: Some(SourceMgrInner::::fill_input_buffer), 46 | skip_input_data: Some(SourceMgrInner::::skip_input_data), 47 | resync_to_restart: Some(jpeg_resync_to_restart), 48 | term_source: Some(SourceMgrInner::::term_source), 49 | }, 50 | to_consume: 0, 51 | reader, 52 | _pinned: PhantomPinned, 53 | }; 54 | src.fill_input_buffer_impl()?; 55 | Ok(Self { 56 | inner_shared: Box::into_raw(Box::new(UnsafeCell::new(src))), 57 | }) 58 | } 59 | 60 | /// This will have the buffer in valid state only if libjpeg stopped decoding 61 | /// at an end of a marker, or `jpeg_consume_input` has been called. 62 | pub fn into_inner(mut self) -> R { 63 | unsafe { 64 | let mut inner = Box::from_raw(std::mem::replace(&mut self.inner_shared, ptr::null_mut())); 65 | inner.get_mut().return_unconsumed_data(); 66 | 67 | #[cfg(debug_assertions)] 68 | inner.get_mut().poison_jpeg_source_mgr(); 69 | 70 | inner.into_inner().reader 71 | } 72 | } 73 | 74 | /// Safety: `SourceMgr` can only be dropped after `cinfo.src` is set to NULL, 75 | /// or otherwise guaranteed not to be used any more via libjpeg. 76 | pub unsafe fn iface_c_ptr(&mut self) -> *mut jpeg_source_mgr { 77 | debug_assert!(!self.inner_shared.is_null()); 78 | unsafe { 79 | ptr::addr_of_mut!((*UnsafeCell::raw_get(self.inner_shared)).iface) 80 | } 81 | } 82 | } 83 | 84 | impl Drop for SourceMgr { 85 | fn drop(&mut self) { 86 | if !self.inner_shared.is_null() { 87 | unsafe { 88 | #[cfg(not(debug_assertions))] 89 | let _ = Box::from_raw(self.inner_shared); 90 | 91 | #[cfg(debug_assertions)] 92 | Box::from_raw(self.inner_shared).get_mut().poison_jpeg_source_mgr(); 93 | } 94 | } 95 | } 96 | } 97 | 98 | impl SourceMgrInner { 99 | /// Make any further use by libjpeg cause a crash 100 | #[cfg(debug_assertions)] 101 | unsafe fn poison_jpeg_source_mgr(&mut self) { 102 | extern "C-unwind" fn crash(_: &mut jpeg_decompress_struct) { 103 | panic!("cinfo.src dangling"); 104 | } 105 | extern "C-unwind" fn crash_i(cinfo: &mut jpeg_decompress_struct) -> boolean { 106 | crash(cinfo); 0 107 | } 108 | extern "C-unwind" fn crash_s(cinfo: &mut jpeg_decompress_struct, _: c_long) { 109 | crash(cinfo); 110 | } 111 | extern "C-unwind" fn crash_r(cinfo: &mut jpeg_decompress_struct, _: c_int) -> boolean { 112 | crash(cinfo); 0 113 | } 114 | ptr::write_volatile(&mut self.iface, jpeg_source_mgr { 115 | next_input_byte: ptr::NonNull::dangling().as_ptr(), 116 | bytes_in_buffer: !0, 117 | init_source: Some(crash), 118 | fill_input_buffer: Some(crash_i), 119 | skip_input_data: Some(crash_s), 120 | resync_to_restart: Some(crash_r), 121 | term_source: Some(crash), 122 | }); 123 | } 124 | } 125 | 126 | impl SourceMgrInner { 127 | #[inline] 128 | unsafe fn cast(cinfo: &mut jpeg_decompress_struct) -> &mut Self { 129 | if let Some(maybe_aliased_src) = cinfo.src.cast::>().as_ref() { 130 | // UnsafeCell is intentionally accessed via shared reference. The libjpeg library is single-threaded, 131 | // so while there are other pointers to the cell, they're not used concurrently. 132 | let this = maybe_aliased_src.get(); 133 | // Type alias to unify higher-ranked lifetimes 134 | type FnPtr<'a> = unsafe extern "C-unwind" fn(cinfo: &'a mut jpeg_decompress_struct); 135 | // This is a redundant safety check to ensure the struct is ours 136 | #[allow(unknown_lints)] 137 | #[allow(unpredictable_function_pointer_comparisons)] // it's the same pointer from the same unit 138 | if Some::(Self::init_source) == (*this).iface.init_source { 139 | return &mut *this; 140 | } 141 | } 142 | fail(&mut cinfo.common, JERR_VIRTUAL_BUG); 143 | } 144 | 145 | #[inline(never)] 146 | unsafe extern "C-unwind" fn init_source(cinfo: &mut jpeg_decompress_struct) { 147 | // Do nothing, buffer has been filled by new() 148 | let _s = Self::cast(cinfo); 149 | debug_assert!(!_s.iface.next_input_byte.is_null()); 150 | debug_assert!(_s.iface.bytes_in_buffer > 0); 151 | } 152 | 153 | #[cold] 154 | fn set_buffer_to_eoi(&mut self) { 155 | debug_assert_eq!(self.to_consume, 0); // this should happen at eof 156 | 157 | // libjpeg doesn't treat it as error, but fakes it! 158 | self.iface.next_input_byte = [0xFF, 0xD9, 0xFF, 0xD9].as_ptr(); 159 | self.iface.bytes_in_buffer = 4; 160 | } 161 | 162 | #[inline(never)] 163 | fn fill_input_buffer_impl(&mut self) -> io::Result<()> { 164 | // Do not call return_unconsumed_data() here. 165 | // here bytes_in_buffer may be != 0, because jdhuff.c doesn't update 166 | // the value after consuming the buffer. 167 | 168 | self.reader.consume(self.to_consume); 169 | self.to_consume = 0; 170 | 171 | let buf = self.reader.fill_buf()?; 172 | self.to_consume = buf.len(); 173 | 174 | self.iface.next_input_byte = buf.as_ptr(); 175 | self.iface.bytes_in_buffer = buf.len(); 176 | 177 | if buf.is_empty() { 178 | // this is EOF 179 | return Err(io::ErrorKind::UnexpectedEof.into()); 180 | } 181 | Ok(()) 182 | } 183 | 184 | /// In typical applications, it should read fresh data 185 | /// into the buffer (ignoring the current state of `next_input_byte` and 186 | /// `bytes_in_buffer`) 187 | #[inline(never)] 188 | unsafe extern "C-unwind" fn fill_input_buffer(cinfo: &mut jpeg_decompress_struct) -> boolean { 189 | let this = Self::cast(cinfo); 190 | match this.fill_input_buffer_impl() { 191 | Ok(()) => 1, 192 | Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { 193 | this.set_buffer_to_eoi(); 194 | warn(&mut cinfo.common, JWRN_JPEG_EOF); 195 | // boolean returned by this function is for async I/O, not errors. 196 | 1 197 | }, 198 | Err(_) => { 199 | fail(&mut cinfo.common, JERR_FILE_READ); 200 | }, 201 | } 202 | } 203 | 204 | /// libjpeg makes `bytes_in_buffer` up to date before calling this 205 | #[inline(never)] 206 | unsafe extern "C-unwind" fn skip_input_data(cinfo: &mut jpeg_decompress_struct, num_bytes: c_long) { 207 | if num_bytes <= 0 { 208 | return; 209 | } 210 | let this = Self::cast(cinfo); 211 | let mut num_bytes = usize::try_from(num_bytes).unwrap(); 212 | 213 | loop { 214 | if this.iface.bytes_in_buffer > 0 { 215 | let skip_from_buffer = this.iface.bytes_in_buffer.min(num_bytes); 216 | this.iface.bytes_in_buffer -= skip_from_buffer; 217 | this.iface.next_input_byte = this.iface.next_input_byte.add(skip_from_buffer); 218 | num_bytes -= skip_from_buffer; 219 | } 220 | if num_bytes == 0 { 221 | break; 222 | } 223 | if this.fill_input_buffer_impl().is_err() { 224 | fail(&mut cinfo.common, JERR_FILE_READ); 225 | } 226 | } 227 | } 228 | 229 | fn return_unconsumed_data(&mut self) { 230 | let unconsumed = self.to_consume.saturating_sub(self.iface.bytes_in_buffer); 231 | self.to_consume = 0; 232 | self.reader.consume(unconsumed); 233 | } 234 | 235 | /// `jpeg_finish_decompress` consumes data up to EOI before calling this 236 | #[inline(never)] 237 | unsafe extern "C-unwind" fn term_source(cinfo: &mut jpeg_decompress_struct) { 238 | let this = Self::cast(cinfo); 239 | this.return_unconsumed_data(); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/writedst.rs: -------------------------------------------------------------------------------- 1 | use crate::boolean; 2 | use crate::fail; 3 | use crate::ffi::jpeg_compress_struct; 4 | use crate::warn; 5 | use mozjpeg_sys::jpeg_common_struct; 6 | use mozjpeg_sys::jpeg_destination_mgr; 7 | use mozjpeg_sys::JERR_BUFFER_SIZE; 8 | use mozjpeg_sys::JERR_FILE_WRITE; 9 | use mozjpeg_sys::JERR_INPUT_EOF; 10 | use mozjpeg_sys::JERR_VIRTUAL_BUG; 11 | use mozjpeg_sys::JWRN_JPEG_EOF; 12 | use mozjpeg_sys::J_MESSAGE_CODE; 13 | use mozjpeg_sys::{JPOOL_IMAGE, JPOOL_PERMANENT}; 14 | use std::cell::UnsafeCell; 15 | use std::io; 16 | use std::io::Write; 17 | use std::marker::PhantomPinned; 18 | use std::mem::MaybeUninit; 19 | use std::os::raw::{c_int, c_long, c_uint}; 20 | use std::panic::{RefUnwindSafe, UnwindSafe}; 21 | use std::ptr; 22 | 23 | pub(crate) struct DestinationMgr { 24 | /// The `jpeg_destination_mgr` has requirements that are tricky for Rust: 25 | /// * it must have a stable address, 26 | /// * it is mutated via `cinfo.dest` raw pointer via C, while the `DestinationMgr` stored elsewhere owns it. 27 | /// This requires interior mutability and a non-exclusive ownership (`Box` would be useless). 28 | inner_shared: *mut UnsafeCell>, 29 | } 30 | 31 | impl UnwindSafe for DestinationMgr {} 32 | impl RefUnwindSafe for DestinationMgr {} 33 | 34 | #[repr(C)] 35 | struct DestinationMgrInner { 36 | /// The `iface` is mutably aliased by C code. Must be the first field. 37 | iface: jpeg_destination_mgr, 38 | buf: Vec, 39 | writer: W, 40 | // jpeg_destination_mgr callbacks get a pointer to the struct 41 | _pinned: PhantomPinned, 42 | } 43 | 44 | impl DestinationMgr { 45 | #[inline] 46 | pub fn new(writer: W, capacity: usize) -> Self { 47 | Self { 48 | inner_shared: Box::into_raw(Box::new(UnsafeCell::new( 49 | DestinationMgrInner { 50 | iface: jpeg_destination_mgr { 51 | next_output_byte: ptr::null_mut(), 52 | free_in_buffer: 0, 53 | init_destination: Some(DestinationMgrInner::::init_destination), 54 | empty_output_buffer: Some(DestinationMgrInner::::empty_output_buffer), 55 | term_destination: Some(DestinationMgrInner::::term_destination), 56 | }, 57 | // Can't use BufWriter, because it doesn't expose the unwritten buffer 58 | buf: Vec::with_capacity(if capacity > 0 { capacity.min(i32::MAX as usize) } else { 4096 }), 59 | writer, 60 | _pinned: PhantomPinned, 61 | }, 62 | ))), 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | fn iface(&mut self) -> &mut jpeg_destination_mgr { 68 | unsafe { 69 | &mut (*UnsafeCell::raw_get(self.inner_shared)).iface 70 | } 71 | } 72 | 73 | /// Must be called after `term_destination` 74 | pub fn into_inner(mut self) -> W { 75 | unsafe { 76 | #[cfg(debug_assertions)] 77 | self.poison_jpeg_destination_mgr(); 78 | 79 | let inner = std::mem::replace(&mut self.inner_shared, ptr::null_mut()); 80 | Box::from_raw(inner).into_inner().writer 81 | } 82 | } 83 | } 84 | 85 | impl DestinationMgr { 86 | /// Safety: `DestinationMgr` can only be dropped after `cinfo.dest` is set to NULL 87 | pub unsafe fn iface_c_ptr(&mut self) -> *mut jpeg_destination_mgr { 88 | debug_assert!(!self.inner_shared.is_null()); 89 | unsafe { 90 | ptr::addr_of_mut!((*UnsafeCell::raw_get(self.inner_shared)).iface) 91 | } 92 | } 93 | 94 | /// Make any further use by libjpeg cause a crash 95 | #[cfg(debug_assertions)] 96 | unsafe fn poison_jpeg_destination_mgr(&mut self) { 97 | extern "C-unwind" fn crash(_: &mut jpeg_compress_struct) { 98 | panic!("cinfo.dest dangling"); 99 | } 100 | extern "C-unwind" fn crash_i(cinfo: &mut jpeg_compress_struct) -> boolean { 101 | crash(cinfo); 0 102 | } 103 | ptr::write_volatile(self.iface_c_ptr(), jpeg_destination_mgr { 104 | next_output_byte: ptr::NonNull::dangling().as_ptr(), 105 | free_in_buffer: !0, 106 | init_destination: Some(crash), 107 | empty_output_buffer: Some(crash_i), 108 | term_destination: Some(crash) 109 | }); 110 | } 111 | } 112 | 113 | impl Drop for DestinationMgr { 114 | fn drop(&mut self) { 115 | if !self.inner_shared.is_null() { 116 | unsafe { 117 | #[cfg(debug_assertions)] 118 | self.poison_jpeg_destination_mgr(); 119 | 120 | let _ = Box::from_raw(self.inner_shared); 121 | } 122 | } 123 | } 124 | } 125 | 126 | impl DestinationMgrInner { 127 | fn reset_buffer(&mut self) { 128 | self.buf.clear(); 129 | let spare_capacity = self.buf.spare_capacity_mut(); 130 | self.iface.next_output_byte = spare_capacity.as_mut_ptr().cast(); 131 | self.iface.free_in_buffer = spare_capacity.len(); 132 | } 133 | 134 | unsafe fn write_buffer(&mut self, full: bool) -> Result<(), J_MESSAGE_CODE> { 135 | let buf = self.buf.spare_capacity_mut(); 136 | let used_capacity = if full { buf.len() } else { 137 | buf.len().checked_sub(self.iface.free_in_buffer).ok_or(JERR_BUFFER_SIZE)? 138 | }; 139 | if used_capacity > 0 { 140 | unsafe { 141 | self.buf.set_len(used_capacity); 142 | } 143 | self.writer.write_all(&self.buf).map_err(|_| JERR_FILE_WRITE)?; 144 | } 145 | self.reset_buffer(); 146 | Ok(()) 147 | } 148 | 149 | unsafe fn cast(cinfo: &mut jpeg_compress_struct) -> &mut Self { 150 | if let Some(maybe_aliased_dest) = cinfo.dest.cast::>().as_ref() { 151 | // UnsafeCell is intentionally accessed via shared reference. The libjpeg library is single-threaded, 152 | // so while there are other pointers to the cell, they're not used concurrently. 153 | let this = maybe_aliased_dest.get(); 154 | // Type alias to unify higher-ranked lifetimes 155 | type FnPtr<'a> = unsafe extern "C-unwind" fn(cinfo: &'a mut jpeg_compress_struct); 156 | // This is a redundant safety check to ensure the struct is ours 157 | #[allow(unknown_lints)] 158 | #[allow(unpredictable_function_pointer_comparisons)] // it's the same pointer from the same unit 159 | if Some::(Self::init_destination) == (*this).iface.init_destination { 160 | return &mut *this; 161 | } 162 | } 163 | fail(&mut cinfo.common, JERR_VIRTUAL_BUG); 164 | } 165 | 166 | /// This is called by `jcphuff`'s `dump_buffer()`, which does NOT keep 167 | /// the position up to date, and expects full buffer write every time. 168 | #[inline(never)] 169 | unsafe extern "C-unwind" fn empty_output_buffer(cinfo: &mut jpeg_compress_struct) -> boolean { 170 | let this = Self::cast(cinfo); 171 | if let Err(code) = this.write_buffer(true) { 172 | fail(&mut cinfo.common, code); 173 | } 174 | 1 175 | } 176 | 177 | #[inline(never)] 178 | unsafe extern "C-unwind" fn init_destination(cinfo: &mut jpeg_compress_struct) { 179 | Self::cast(cinfo).reset_buffer(); 180 | } 181 | 182 | #[inline(never)] 183 | unsafe extern "C-unwind" fn term_destination(cinfo: &mut jpeg_compress_struct) { 184 | let this = Self::cast(cinfo); 185 | if let Err(code) = this.write_buffer(false) { 186 | fail(&mut cinfo.common, code); 187 | } 188 | if this.writer.flush().is_err() { 189 | fail(&mut cinfo.common, JERR_FILE_WRITE); 190 | } 191 | this.iface.free_in_buffer = 0; 192 | } 193 | } 194 | 195 | #[test] 196 | fn w() { 197 | for any_write_first in [true, false] { 198 | for capacity in [0,1,2,3,5,10,255,256,4096] { 199 | let mut w = DestinationMgr::new(Vec::new(), capacity); 200 | let mut expected = Vec::new(); 201 | unsafe { 202 | let init_destination = w.iface().init_destination.unwrap(); 203 | let empty_output_buffer = w.iface().empty_output_buffer.unwrap(); 204 | let term_destination = w.iface().term_destination.unwrap(); 205 | 206 | let mut j: jpeg_compress_struct = std::mem::zeroed(); 207 | j.dest = w.iface_c_ptr(); 208 | (init_destination)(&mut j); 209 | assert!(w.iface().free_in_buffer > 0); 210 | if any_write_first { 211 | while w.iface().free_in_buffer > 0 { 212 | expected.push(123); 213 | *w.iface().next_output_byte = 123; 214 | w.iface().next_output_byte = w.iface().next_output_byte.add(1); 215 | w.iface().free_in_buffer -= 1; 216 | } 217 | (empty_output_buffer)(&mut j); 218 | assert!(w.iface().free_in_buffer > 0); 219 | let slice = std::slice::from_raw_parts_mut(w.iface().next_output_byte, w.iface().free_in_buffer); 220 | slice.iter_mut().enumerate().for_each(|(i, s)| *s = i as u8); 221 | expected.extend_from_slice(slice); 222 | w.iface().next_output_byte = w.iface().next_output_byte.add(1); // yes, can be invalid! 223 | w.iface().free_in_buffer = 999; // yes, can be invalid! 224 | (empty_output_buffer)(&mut j); 225 | assert!(w.iface().free_in_buffer > 0); 226 | } 227 | let slice = std::slice::from_raw_parts_mut(w.iface().next_output_byte, w.iface().free_in_buffer-1); 228 | slice.iter_mut().enumerate().for_each(|(i, s)| *s = (i*17) as u8); 229 | expected.extend_from_slice(slice); 230 | w.iface().next_output_byte = w.iface().next_output_byte.add(slice.len()); 231 | w.iface().free_in_buffer -= slice.len(); // now must be valid 232 | (term_destination)(&mut j); 233 | assert_eq!(expected, w.into_inner()); 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/compress.rs: -------------------------------------------------------------------------------- 1 | use crate::{colorspace::ColorSpace, PixelDensity}; 2 | use crate::colorspace::ColorSpaceExt; 3 | use crate::component::CompInfo; 4 | use crate::component::CompInfoExt; 5 | use crate::errormgr::unwinding_error_mgr; 6 | use crate::errormgr::ErrorMgr; 7 | use crate::fail; 8 | use crate::ffi; 9 | use crate::ffi::boolean; 10 | use crate::ffi::jpeg_compress_struct; 11 | use crate::ffi::DCTSIZE; 12 | use crate::ffi::JDIMENSION; 13 | use crate::ffi::JPEG_LIB_VERSION; 14 | use crate::ffi::J_BOOLEAN_PARAM; 15 | use crate::ffi::J_INT_PARAM; 16 | use crate::marker::Marker; 17 | use crate::qtable::QTable; 18 | use crate::writedst::DestinationMgr; 19 | use arrayvec::ArrayVec; 20 | use std::cmp::min; 21 | use std::io; 22 | use std::marker::PhantomPinned; 23 | use std::mem; 24 | use std::os::raw::{c_int, c_uchar, c_uint, c_ulong, c_void}; 25 | use std::ptr; 26 | use std::ptr::addr_of_mut; 27 | use std::slice; 28 | 29 | /// Max sampling factor is 2 30 | pub const MAX_MCU_HEIGHT: usize = 16; 31 | /// Codec doesn't allow more channels than this 32 | pub const MAX_COMPONENTS: usize = 4; 33 | 34 | /// Create a new JPEG file from pixels 35 | /// 36 | /// Wrapper for `jpeg_compress_struct` 37 | pub struct Compress { 38 | cinfo: jpeg_compress_struct, 39 | 40 | /// It's `Box`, but `cinfo` references `own_err`, 41 | /// so I need talismans to ward off nasal demons haunting self-referential structs 42 | own_err: *mut ErrorMgr, 43 | _it_is_self_referential: PhantomPinned, 44 | } 45 | 46 | #[derive(Copy, Clone)] 47 | pub enum ScanMode { 48 | AllComponentsTogether = 0, 49 | /// Can flash grayscale or green-tinted images 50 | ScanPerComponent = 1, 51 | Auto = 2, 52 | } 53 | 54 | pub struct CompressStarted { 55 | compress: Compress, 56 | /// Safety: sensitive to drop order. Needs to be dropped after `Compress` 57 | dest_mgr: DestinationMgr, 58 | } 59 | 60 | impl Compress { 61 | /// Compress image using input in this colorspace. 62 | /// 63 | /// ## Panics 64 | /// 65 | /// You need to wrap all use of this library in `std::panic::catch_unwind()` 66 | /// 67 | /// By default errors cause unwind (panic) and unwind through the C code, 68 | /// which strictly speaking is not guaranteed to work in Rust (but seems to work fine, at least on x86-64 and ARM). 69 | #[must_use] 70 | pub fn new(color_space: ColorSpace) -> Self { 71 | Self::new_err(unwinding_error_mgr(), color_space) 72 | } 73 | 74 | /// Use a specific error handler instead of the default unwinding one. 75 | /// 76 | /// Note that the error handler must either abort the process or unwind, 77 | /// it can't gracefully return due to the design of libjpeg. 78 | /// 79 | /// `color_space` refers to input color space 80 | #[must_use] 81 | pub fn new_err(err: Box, color_space: ColorSpace) -> Self { 82 | unsafe { 83 | let mut newself = Self { 84 | cinfo: mem::zeroed(), 85 | own_err: Box::into_raw(err), 86 | _it_is_self_referential: PhantomPinned, 87 | }; 88 | newself.cinfo.common.err = addr_of_mut!(*newself.own_err); 89 | 90 | let s = mem::size_of_val(&newself.cinfo); 91 | ffi::jpeg_CreateCompress(&mut newself.cinfo, JPEG_LIB_VERSION, s); 92 | 93 | newself.cinfo.in_color_space = color_space; 94 | newself.cinfo.input_components = color_space.num_components() as c_int; 95 | ffi::jpeg_set_defaults(&mut newself.cinfo); 96 | 97 | newself 98 | } 99 | } 100 | 101 | #[doc(hidden)] 102 | #[deprecated(note = "Give a Vec to start_compress instead")] 103 | pub fn set_mem_dest(&self) { 104 | } 105 | 106 | /// Settings can't be changed after this call. Returns a `CompressStarted` struct that will handle the rest of the writing. 107 | /// 108 | /// ## Panics 109 | /// 110 | /// It may panic, like all functions of this library. 111 | pub fn start_compress(self, writer: W) -> io::Result> { 112 | if !self.components().iter().any(|c| c.h_samp_factor == 1) { return Err(io::Error::new(io::ErrorKind::InvalidInput, "at least one h_samp_factor must be 1")); } 113 | if !self.components().iter().any(|c| c.v_samp_factor == 1) { return Err(io::Error::new(io::ErrorKind::InvalidInput, "at least one v_samp_factor must be 1")); } 114 | 115 | // 1bpp, rounded to 4K page 116 | let expected_file_size = (self.cinfo.image_width as usize * self.cinfo.image_height as usize / 8 + 4095) & !4095; 117 | let write_buffer_capacity = expected_file_size.clamp(1 << 12, 1 << 16); 118 | 119 | let mut started = CompressStarted { 120 | compress: self, 121 | dest_mgr: DestinationMgr::new(writer, write_buffer_capacity), 122 | }; 123 | unsafe { 124 | started.compress.cinfo.dest = started.dest_mgr.iface_c_ptr(); 125 | ffi::jpeg_start_compress(&mut started.compress.cinfo, boolean::from(true)); 126 | } 127 | Ok(started) 128 | } 129 | } 130 | 131 | impl CompressStarted { 132 | /// Add a marker to compressed file 133 | /// 134 | /// Data is max 64KB 135 | /// 136 | /// ## Panics 137 | /// 138 | /// It may panic, like all functions of this library. 139 | pub fn write_marker(&mut self, marker: Marker, data: &[u8]) { 140 | unsafe { 141 | ffi::jpeg_write_marker( 142 | &mut self.compress.cinfo, 143 | marker.into(), 144 | data.as_ptr(), 145 | data.len() as c_uint, 146 | ); 147 | } 148 | } 149 | 150 | /// Add ICC profile to compressed file 151 | /// 152 | /// ## Panics 153 | /// 154 | /// It may panic, like all functions of this library. 155 | pub fn write_icc_profile(&mut self, data: &[u8]) { 156 | const OVERHEAD_LEN: usize = 14; 157 | const MAX_BYTES_IN_MARKER: usize = 65533; 158 | const MAX_DATA_BYTES_IN_MARKER: usize = MAX_BYTES_IN_MARKER - OVERHEAD_LEN; 159 | 160 | if data.is_empty() { 161 | fail(&mut self.compress.cinfo.common, ffi::JERR_BUFFER_SIZE); 162 | } 163 | 164 | let chunks = data.chunks(MAX_DATA_BYTES_IN_MARKER); 165 | let num_chunks = chunks.len(); 166 | 167 | let mut buf = Vec::with_capacity(MAX_BYTES_IN_MARKER.min(data.len() + OVERHEAD_LEN)); 168 | 169 | chunks.enumerate().for_each(move |(current_marker, chunk)| { 170 | buf.clear(); 171 | buf.extend_from_slice(b"ICC_PROFILE\0"); 172 | buf.extend([current_marker as u8, num_chunks as u8]); 173 | buf.extend_from_slice(chunk); 174 | 175 | self.write_marker(Marker::APP(2), &buf); 176 | }); 177 | } 178 | 179 | /// Read-only view of component information 180 | #[must_use] 181 | pub fn components(&self) -> &[CompInfo] { 182 | self.compress.components() 183 | } 184 | 185 | fn can_write_more_lines(&self) -> bool { 186 | self.compress.cinfo.next_scanline < self.compress.cinfo.image_height 187 | } 188 | } 189 | 190 | impl Compress { 191 | /// Expose components for modification, e.g. to set chroma subsampling 192 | pub fn components_mut(&mut self) -> &mut [CompInfo] { 193 | if self.cinfo.comp_info.is_null() { 194 | return &mut []; 195 | } 196 | unsafe { 197 | slice::from_raw_parts_mut(self.cinfo.comp_info, self.cinfo.num_components as usize) 198 | } 199 | } 200 | 201 | /// Read-only view of component information 202 | #[must_use] 203 | pub fn components(&self) -> &[CompInfo] { 204 | if self.cinfo.comp_info.is_null() { 205 | return &[]; 206 | } 207 | unsafe { 208 | slice::from_raw_parts(self.cinfo.comp_info, self.cinfo.num_components as usize) 209 | } 210 | } 211 | } 212 | 213 | impl CompressStarted { 214 | /// Returns Ok(()) if all lines in `image_src` (not necessarily all lines of the image) were written 215 | /// 216 | /// ## Panics 217 | /// 218 | /// It may panic, like all functions of this library. 219 | pub fn write_scanlines(&mut self, image_src: &[u8]) -> io::Result<()> { 220 | if self.compress.cinfo.raw_data_in != 0 || 221 | self.compress.cinfo.input_components <= 0 || 222 | self.compress.cinfo.image_width == 0 { 223 | return Err(io::ErrorKind::InvalidInput.into()); 224 | } 225 | 226 | let byte_width = self.compress.cinfo.image_width as usize * self.compress.cinfo.input_components as usize; 227 | for rows in image_src.chunks(MAX_MCU_HEIGHT * byte_width) { 228 | let mut row_pointers = ArrayVec::<_, MAX_MCU_HEIGHT>::new(); 229 | for row in rows.chunks_exact(byte_width) { 230 | row_pointers.push(row.as_ptr()); 231 | } 232 | 233 | let mut rows_left = row_pointers.len() as u32; 234 | let mut row_pointers = row_pointers.as_ptr(); 235 | while rows_left > 0 { 236 | unsafe { 237 | let rows_written = ffi::jpeg_write_scanlines( 238 | &mut self.compress.cinfo, 239 | row_pointers, 240 | rows_left, 241 | ); 242 | debug_assert!(rows_left >= rows_written); 243 | if rows_written == 0 { 244 | return Err(io::ErrorKind::UnexpectedEof.into()); 245 | } 246 | rows_left -= rows_written; 247 | row_pointers = row_pointers.add(rows_written as usize); 248 | } 249 | } 250 | } 251 | Ok(()) 252 | } 253 | 254 | /// Advanced. Only possible after `set_raw_data_in()`. 255 | /// Write YCbCr blocks pixels instead of usual color space 256 | /// 257 | /// See `raw_data_in` in libjpeg docs 258 | /// 259 | /// ## Panic 260 | /// 261 | /// Panics if raw write wasn't enabled 262 | #[track_caller] 263 | pub fn write_raw_data(&mut self, image_src: &[&[u8]]) -> bool { 264 | if 0 == self.compress.cinfo.raw_data_in { 265 | panic!("Raw data not set"); 266 | } 267 | 268 | let mcu_height = self.compress.cinfo.max_v_samp_factor as usize * DCTSIZE; 269 | if mcu_height > MAX_MCU_HEIGHT { 270 | panic!("Subsampling factor too large"); 271 | } 272 | assert!(mcu_height > 0); 273 | 274 | let num_components = self.components().len(); 275 | if num_components > MAX_COMPONENTS || num_components > image_src.len() { 276 | panic!("Too many components: declared {}, got {}", num_components, image_src.len()); 277 | } 278 | 279 | for (ci, comp_info) in self.components().iter().enumerate() { 280 | if comp_info.row_stride() * comp_info.col_stride() > image_src[ci].len() { 281 | panic!("Bitmap too small. Expected {}x{}, got {}", comp_info.row_stride(), comp_info.col_stride(), image_src[ci].len()); 282 | } 283 | } 284 | 285 | let mut start_row = self.compress.cinfo.next_scanline as usize; 286 | while self.can_write_more_lines() { 287 | unsafe { 288 | let mut row_ptrs = [[ptr::null::(); MAX_MCU_HEIGHT]; MAX_COMPONENTS]; 289 | 290 | for ((comp_info, &image_src), comp_row_ptrs) in self.components().iter().zip(image_src).zip(row_ptrs.iter_mut()) { 291 | let row_stride = comp_info.row_stride(); 292 | 293 | let input_height = image_src.len() / row_stride; 294 | 295 | let comp_start_row = start_row * comp_info.v_samp_factor as usize 296 | / self.compress.cinfo.max_v_samp_factor as usize; 297 | let comp_height = min( 298 | input_height - comp_start_row, 299 | DCTSIZE * comp_info.v_samp_factor as usize, 300 | ); 301 | assert!(comp_height >= 8); 302 | 303 | // row_ptrs were initialized to null 304 | for (src_row, row_ptr) in image_src.chunks_exact(row_stride).skip(comp_start_row).take(comp_height).zip(comp_row_ptrs.iter_mut()) { 305 | *row_ptr = src_row.as_ptr(); 306 | } 307 | } 308 | 309 | let comp_ptrs: [*const *const u8; MAX_COMPONENTS] = std::array::from_fn(|ci| row_ptrs[ci].as_ptr()); 310 | let rows_written = ffi::jpeg_write_raw_data(&mut self.compress.cinfo, comp_ptrs.as_ptr(), mcu_height as u32) as usize; 311 | if 0 == rows_written { 312 | return false; 313 | } 314 | start_row += rows_written; 315 | } 316 | } 317 | true 318 | } 319 | } 320 | 321 | impl Compress { 322 | /// Set color space of JPEG being written, different from input color space 323 | /// 324 | /// See `jpeg_set_colorspace` in libjpeg docs 325 | pub fn set_color_space(&mut self, color_space: ColorSpace) { 326 | unsafe { 327 | ffi::jpeg_set_colorspace(&mut self.cinfo, color_space); 328 | } 329 | } 330 | 331 | /// Image size of the input 332 | pub fn set_size(&mut self, width: usize, height: usize) { 333 | self.cinfo.image_width = width as JDIMENSION; 334 | self.cinfo.image_height = height as JDIMENSION; 335 | } 336 | 337 | /// libjpeg's `input_gamma` = image gamma of input image 338 | #[deprecated(note = "it doesn't do anything")] 339 | pub fn set_gamma(&mut self, gamma: f64) { 340 | self.cinfo.input_gamma = gamma; 341 | } 342 | 343 | /// Sets pixel density of an image in the JFIF APP0 segment[^note]. 344 | /// If this method is not called, the resulting JPEG will have a default 345 | /// pixel aspect ratio of 1x1. 346 | /// 347 | /// [^note]: This method is not related to EXIF-based intrinsic image sizing, 348 | /// and does not affect rendering in browsers. 349 | pub fn set_pixel_density(&mut self, density: PixelDensity) { 350 | self.cinfo.density_unit = density.unit as u8; 351 | self.cinfo.X_density = density.x; 352 | self.cinfo.Y_density = density.y; 353 | } 354 | 355 | /// If true, it will use MozJPEG's scan optimization. Makes progressive image files smaller. 356 | pub fn set_optimize_scans(&mut self, opt: bool) { 357 | unsafe { 358 | ffi::jpeg_c_set_bool_param(&mut self.cinfo, J_BOOLEAN_PARAM::JBOOLEAN_OPTIMIZE_SCANS, boolean::from(opt)); 359 | } 360 | if !opt { 361 | self.cinfo.scan_info = ptr::null(); 362 | } 363 | } 364 | 365 | /// If 1-100 (non-zero), it will use MozJPEG's smoothing. 366 | pub fn set_smoothing_factor(&mut self, smoothing_factor: u8) { 367 | self.cinfo.smoothing_factor = c_int::from(smoothing_factor); 368 | } 369 | 370 | /// Set to `false` to make files larger for no reason 371 | pub fn set_optimize_coding(&mut self, opt: bool) { 372 | self.cinfo.optimize_coding = boolean::from(opt); 373 | } 374 | 375 | /// Specifies whether multiple scans should be considered during trellis 376 | /// quantization. 377 | pub fn set_use_scans_in_trellis(&mut self, opt: bool) { 378 | unsafe { 379 | ffi::jpeg_c_set_bool_param(&mut self.cinfo, J_BOOLEAN_PARAM::JBOOLEAN_USE_SCANS_IN_TRELLIS, boolean::from(opt)); 380 | } 381 | } 382 | 383 | /// You can only turn it on 384 | pub fn set_progressive_mode(&mut self) { 385 | unsafe { 386 | ffi::jpeg_simple_progression(&mut self.cinfo); 387 | } 388 | } 389 | 390 | /// One scan for all components looks best. Other options may flash grayscale or green images. 391 | pub fn set_scan_optimization_mode(&mut self, mode: ScanMode) { 392 | unsafe { 393 | ffi::jpeg_c_set_int_param(&mut self.cinfo, J_INT_PARAM::JINT_DC_SCAN_OPT_MODE, mode as c_int); 394 | ffi::jpeg_set_defaults(&mut self.cinfo); 395 | } 396 | } 397 | 398 | /// Reset to libjpeg v6 settings 399 | /// 400 | /// It gives files identical with libjpeg-turbo 401 | pub fn set_fastest_defaults(&mut self) { 402 | unsafe { 403 | ffi::jpeg_c_set_int_param(&mut self.cinfo, J_INT_PARAM::JINT_COMPRESS_PROFILE, ffi::JINT_COMPRESS_PROFILE_VALUE::JCP_FASTEST as c_int); 404 | ffi::jpeg_set_defaults(&mut self.cinfo); 405 | } 406 | } 407 | 408 | /// Advanced. See `raw_data_in` in libjpeg docs. 409 | pub fn set_raw_data_in(&mut self, opt: bool) { 410 | self.cinfo.raw_data_in = boolean::from(opt); 411 | } 412 | 413 | /// Set image quality. Values 60-80 are recommended. 414 | pub fn set_quality(&mut self, quality: f32) { 415 | unsafe { 416 | ffi::jpeg_set_quality(&mut self.cinfo, quality as c_int, boolean::from(false)); 417 | } 418 | } 419 | 420 | /// Instead of quality setting, use a specific quantization table. 421 | pub fn set_luma_qtable(&mut self, qtable: &QTable) { 422 | unsafe { 423 | ffi::jpeg_add_quant_table(&mut self.cinfo, 0, qtable.as_ptr(), 100, 1); 424 | } 425 | } 426 | 427 | /// Instead of quality setting, use a specific quantization table for color. 428 | pub fn set_chroma_qtable(&mut self, qtable: &QTable) { 429 | unsafe { 430 | ffi::jpeg_add_quant_table(&mut self.cinfo, 1, qtable.as_ptr(), 100, 1); 431 | } 432 | } 433 | 434 | /// Sets chroma subsampling, separately for Cb and Cr channels. 435 | /// Instead of setting samples per pixel, like in `cinfo`'s `x_samp_factor`, 436 | /// it sets size of chroma "pixels" per luma pixel. 437 | /// 438 | /// * `(1,1), (1,1)` == 4:4:4 439 | /// * `(2,1), (2,1)` == 4:2:2 440 | /// * `(2,2), (2,2)` == 4:2:0 441 | pub fn set_chroma_sampling_pixel_sizes(&mut self, cb: (u8, u8), cr: (u8, u8)) { 442 | let max_sampling_h = cb.0.max(cr.0); 443 | let max_sampling_v = cb.1.max(cr.1); 444 | 445 | let px_sizes = [(1, 1), cb, cr]; 446 | for (c, (h, v)) in self.components_mut().iter_mut().zip(px_sizes) { 447 | c.h_samp_factor = (max_sampling_h / h).into(); 448 | c.v_samp_factor = (max_sampling_v / v).into(); 449 | } 450 | } 451 | } 452 | 453 | impl CompressStarted { 454 | /// Finalize compression. 455 | /// In case of progressive files, this may actually start processing. 456 | /// 457 | /// ## Panics 458 | /// 459 | /// It may panic, like all functions of this library. 460 | #[inline] 461 | pub fn finish(mut self) -> io::Result { 462 | unsafe { 463 | ffi::jpeg_finish_compress(&mut self.compress.cinfo); 464 | } 465 | self.compress.cinfo.dest = ptr::null_mut(); 466 | drop(self.compress); 467 | Ok(self.dest_mgr.into_inner()) 468 | } 469 | 470 | #[doc(hidden)] 471 | #[deprecated(note = "use finish(); it now returns a writer given to start_compress()")] 472 | pub fn finish_compress(self) -> io::Result { 473 | self.finish() 474 | } 475 | 476 | /// Give up writing, return incomplete result 477 | #[cold] 478 | pub fn abort(mut self) -> W { 479 | self.compress.cinfo.dest = ptr::null_mut(); 480 | self.dest_mgr.into_inner() 481 | } 482 | } 483 | 484 | impl Drop for Compress { 485 | #[inline] 486 | fn drop(&mut self) { 487 | unsafe { 488 | self.cinfo.dest = ptr::null_mut(); 489 | ffi::jpeg_destroy_compress(&mut self.cinfo); 490 | // ErrorMgr is destroyed after cinfo can no longer reference it 491 | let _ = Box::from_raw(self.own_err); 492 | } 493 | } 494 | } 495 | 496 | #[test] 497 | fn write_mem() { 498 | let mut cinfo = Compress::new(ColorSpace::JCS_YCbCr); 499 | 500 | assert_eq!(3, cinfo.components().len()); 501 | 502 | cinfo.set_size(17, 33); 503 | 504 | #[allow(deprecated)] { 505 | cinfo.set_gamma(1.0); 506 | } 507 | 508 | cinfo.set_progressive_mode(); 509 | cinfo.set_scan_optimization_mode(ScanMode::AllComponentsTogether); 510 | 511 | cinfo.set_raw_data_in(true); 512 | 513 | cinfo.set_quality(88.); 514 | 515 | cinfo.set_chroma_sampling_pixel_sizes((1, 1), (1, 1)); 516 | for c in cinfo.components() { 517 | assert_eq!(c.v_samp_factor, 1); 518 | assert_eq!(c.h_samp_factor, 1); 519 | } 520 | 521 | cinfo.set_chroma_sampling_pixel_sizes((2, 2), (2, 2)); 522 | for (c, samp) in cinfo.components().iter().zip([2, 1, 1]) { 523 | assert_eq!(c.v_samp_factor, samp); 524 | assert_eq!(c.h_samp_factor, samp); 525 | } 526 | 527 | let mut cinfo = cinfo.start_compress(Vec::new()).unwrap(); 528 | 529 | cinfo.write_marker(Marker::APP(2), b"Hello World"); 530 | 531 | assert_eq!(24, cinfo.components()[0].row_stride()); 532 | assert_eq!(40, cinfo.components()[0].col_stride()); 533 | assert_eq!(16, cinfo.components()[1].row_stride()); 534 | assert_eq!(24, cinfo.components()[1].col_stride()); 535 | assert_eq!(16, cinfo.components()[2].row_stride()); 536 | assert_eq!(24, cinfo.components()[2].col_stride()); 537 | 538 | let bitmaps = cinfo 539 | .components() 540 | .iter() 541 | .map(|c| vec![128u8; c.row_stride() * c.col_stride()]) 542 | .collect::>(); 543 | 544 | assert!(cinfo.write_raw_data(&bitmaps.iter().map(|c| &c[..]).collect::>())); 545 | 546 | cinfo.finish().unwrap(); 547 | } 548 | 549 | #[test] 550 | fn convert_colorspace() { 551 | let mut cinfo = Compress::new(ColorSpace::JCS_RGB); 552 | cinfo.set_color_space(ColorSpace::JCS_GRAYSCALE); 553 | assert_eq!(1, cinfo.components().len()); 554 | 555 | cinfo.set_size(33, 15); 556 | cinfo.set_quality(44.); 557 | 558 | let mut cinfo = cinfo.start_compress(Vec::new()).unwrap(); 559 | 560 | let scanlines = vec![127u8; 33*15*3]; 561 | cinfo.write_scanlines(&scanlines).unwrap(); 562 | 563 | let res = cinfo.finish().unwrap(); 564 | assert!(!res.is_empty()); 565 | } 566 | -------------------------------------------------------------------------------- /src/decompress.rs: -------------------------------------------------------------------------------- 1 | //! See the `Decompress` struct instead. You don't need to use this module directly. 2 | use bytemuck::Pod; 3 | use crate::{colorspace::ColorSpace, PixelDensity}; 4 | use crate::colorspace::ColorSpaceExt; 5 | use crate::component::CompInfo; 6 | use crate::component::CompInfoExt; 7 | use crate::errormgr::unwinding_error_mgr; 8 | use crate::errormgr::ErrorMgr; 9 | use crate::ffi; 10 | use crate::ffi::jpeg_decompress_struct; 11 | use crate::ffi::DCTSIZE; 12 | use crate::ffi::JPEG_LIB_VERSION; 13 | use crate::ffi::J_COLOR_SPACE as COLOR_SPACE; 14 | use crate::marker::Marker; 15 | use crate::readsrc::SourceMgr; 16 | use libc::fdopen; 17 | use std::cmp::min; 18 | use std::fs::File; 19 | use std::io; 20 | use std::io::BufRead; 21 | use std::io::BufReader; 22 | use std::marker::PhantomData; 23 | use std::mem; 24 | use std::mem::MaybeUninit; 25 | use std::os::raw::{c_int, c_uchar, c_ulong, c_void}; 26 | use std::path::Path; 27 | use std::ptr; 28 | use std::ptr::addr_of_mut; 29 | use std::slice; 30 | 31 | const MAX_MCU_HEIGHT: usize = 16; 32 | const MAX_COMPONENTS: usize = 4; 33 | 34 | /// Empty list of markers 35 | /// 36 | /// By default markers are not read from JPEG files. 37 | pub const NO_MARKERS: &[Marker] = &[]; 38 | 39 | /// App 0-14 and comment markers 40 | /// 41 | /// ```rust 42 | /// # use mozjpeg::*; 43 | /// Decompress::with_markers(ALL_MARKERS); 44 | /// ``` 45 | pub const ALL_MARKERS: &[Marker] = &[ 46 | Marker::APP(0), Marker::APP(1), Marker::APP(2), Marker::APP(3), Marker::APP(4), 47 | Marker::APP(5), Marker::APP(6), Marker::APP(7), Marker::APP(8), Marker::APP(9), 48 | Marker::APP(10), Marker::APP(11), Marker::APP(12), Marker::APP(13), Marker::APP(14), 49 | Marker::COM, 50 | ]; 51 | 52 | /// Algorithm for the DCT step. 53 | #[derive(Clone, Copy, Debug)] 54 | pub enum DctMethod { 55 | /// slow but accurate integer algorithm 56 | IntegerSlow, 57 | /// faster, less accurate integer method 58 | IntegerFast, 59 | /// floating-point method 60 | Float, 61 | } 62 | 63 | /// Use `Decompress` static methods instead of creating this directly 64 | pub struct DecompressBuilder<'markers> { 65 | save_markers: &'markers [Marker], 66 | err_mgr: Option>, 67 | } 68 | 69 | #[deprecated(note = "Renamed to DecompressBuilder")] 70 | #[doc(hidden)] 71 | pub use DecompressBuilder as DecompressConfig; 72 | 73 | impl<'markers> DecompressBuilder<'markers> { 74 | #[inline] 75 | #[must_use] 76 | pub const fn new() -> Self { 77 | DecompressBuilder { 78 | err_mgr: None, 79 | save_markers: NO_MARKERS, 80 | } 81 | } 82 | 83 | #[inline] 84 | #[must_use] 85 | pub fn with_err(mut self, err: ErrorMgr) -> Self { 86 | self.err_mgr = Some(Box::new(err)); 87 | self 88 | } 89 | 90 | #[inline] 91 | #[must_use] 92 | pub const fn with_markers(mut self, save_markers: &'markers [Marker]) -> Self { 93 | self.save_markers = save_markers; 94 | self 95 | } 96 | 97 | #[inline] 98 | pub fn from_path>(self, path: P) -> io::Result>> { 99 | self.from_file(File::open(path.as_ref())?) 100 | } 101 | 102 | /// Reads from an already-open `File`. 103 | /// Use `from_reader` if you want to customize buffer size. 104 | #[inline] 105 | pub fn from_file(self, file: File) -> io::Result>> { 106 | self.from_reader(BufReader::new(file)) 107 | } 108 | 109 | /// Reads from a `Vec` or a slice. 110 | #[inline] 111 | pub fn from_mem(self, mem: &[u8]) -> io::Result> { 112 | self.from_reader(mem) 113 | } 114 | 115 | /// Takes `BufReader`. If you have `io::Read`, wrap it in `io::BufReader::new(read)`. 116 | #[inline] 117 | pub fn from_reader(self, reader: R) -> io::Result> { 118 | Decompress::from_builder_and_reader(self, reader) 119 | } 120 | } 121 | 122 | impl<'markers> Default for DecompressBuilder<'markers> { 123 | #[inline] 124 | fn default() -> Self { 125 | Self::new() 126 | } 127 | } 128 | 129 | /// Get pixels out of a JPEG file 130 | /// 131 | /// High-level wrapper for `jpeg_decompress_struct` 132 | /// 133 | /// ```rust 134 | /// # use mozjpeg::*; 135 | /// # fn t() -> std::io::Result<()> { 136 | /// let d = Decompress::new_path("image.jpg")?; 137 | /// # Ok(()) } 138 | /// ``` 139 | pub struct Decompress { 140 | cinfo: jpeg_decompress_struct, 141 | err_mgr: Box, 142 | src_mgr: Option>>, 143 | } 144 | 145 | /// Marker type and data slice returned by `MarkerIter` 146 | pub struct MarkerData<'a> { 147 | pub marker: Marker, 148 | pub data: &'a [u8], 149 | } 150 | 151 | /// See `Decompress.markers()` 152 | pub struct MarkerIter<'a> { 153 | marker_list: *mut ffi::jpeg_marker_struct, 154 | _references: ::std::marker::PhantomData>, 155 | } 156 | 157 | impl<'a> Iterator for MarkerIter<'a> { 158 | type Item = MarkerData<'a>; 159 | #[inline] 160 | fn next(&mut self) -> Option> { 161 | if self.marker_list.is_null() { 162 | return None; 163 | } 164 | unsafe { 165 | let last = &*self.marker_list; 166 | self.marker_list = last.next; 167 | Some(MarkerData { 168 | marker: last.marker.into(), 169 | data: ::std::slice::from_raw_parts(last.data, last.data_length as usize), 170 | }) 171 | } 172 | } 173 | } 174 | 175 | impl Decompress<()> { 176 | /// Short for builder().with_err() 177 | #[inline] 178 | #[doc(hidden)] 179 | #[must_use] 180 | pub fn with_err(err_mgr: ErrorMgr) -> DecompressBuilder<'static> { 181 | DecompressBuilder::new().with_err(err_mgr) 182 | } 183 | 184 | /// Short for builder().with_markers() 185 | #[inline] 186 | #[doc(hidden)] 187 | #[must_use] 188 | pub fn with_markers(markers: &[Marker]) -> DecompressBuilder<'_> { 189 | DecompressBuilder::new().with_markers(markers) 190 | } 191 | 192 | /// Use builder() 193 | #[deprecated(note = "renamed to builder()")] 194 | #[doc(hidden)] 195 | #[must_use] 196 | pub fn config() -> DecompressBuilder<'static> { 197 | DecompressBuilder::new() 198 | } 199 | 200 | /// This is `DecompressBuilder::new()` 201 | #[inline] 202 | #[must_use] 203 | pub const fn builder() -> DecompressBuilder<'static> { 204 | DecompressBuilder::new() 205 | } 206 | } 207 | 208 | impl Decompress> { 209 | /// Decode file at path 210 | #[inline] 211 | pub fn new_path>(path: P) -> io::Result { 212 | DecompressBuilder::new().from_path(path) 213 | } 214 | 215 | /// Decode an already-opened file 216 | #[inline] 217 | pub fn new_file(file: File) -> io::Result { 218 | DecompressBuilder::new().from_file(file) 219 | } 220 | } 221 | 222 | impl<'mem> Decompress<&'mem [u8]> { 223 | /// Decode from a JPEG file already in memory 224 | #[inline] 225 | pub fn new_mem(mem: &'mem [u8]) -> io::Result { 226 | DecompressBuilder::new().from_mem(mem) 227 | } 228 | } 229 | 230 | impl Decompress { 231 | /// Decode from an `io::BufRead`, which is `BufReader` wrapping any `io::Read`. 232 | #[inline] 233 | pub fn new_reader(reader: R) -> io::Result where R: BufRead { 234 | DecompressBuilder::new().from_reader(reader) 235 | } 236 | 237 | fn from_builder_and_reader(builder: DecompressBuilder<'_>, reader: R) -> io::Result where R: BufRead { 238 | let src_mgr = Box::new(SourceMgr::new(reader)?); 239 | let err_mgr = builder.err_mgr.unwrap_or_else(unwinding_error_mgr); 240 | unsafe { 241 | let mut newself = Decompress { 242 | cinfo: mem::zeroed(), 243 | src_mgr: Some(src_mgr), 244 | err_mgr, 245 | }; 246 | let src_ptr = newself.src_mgr.as_mut().unwrap().iface_c_ptr(); 247 | newself.cinfo.common.err = addr_of_mut!(*newself.err_mgr); 248 | ffi::jpeg_create_decompress(&mut newself.cinfo); 249 | newself.cinfo.src = src_ptr; 250 | for &marker in builder.save_markers { 251 | newself.save_marker(marker); 252 | } 253 | newself.read_header()?; 254 | Ok(newself) 255 | } 256 | } 257 | 258 | #[inline] 259 | #[must_use] 260 | pub fn components(&self) -> &[CompInfo] { 261 | if self.cinfo.comp_info.is_null() { 262 | return &[]; 263 | } 264 | unsafe { 265 | slice::from_raw_parts(self.cinfo.comp_info, self.cinfo.num_components as usize) 266 | } 267 | } 268 | 269 | #[inline] 270 | pub(crate) fn components_mut(&mut self) -> &mut [CompInfo] { 271 | if self.cinfo.comp_info.is_null() { 272 | return &mut []; 273 | } 274 | unsafe { 275 | slice::from_raw_parts_mut(self.cinfo.comp_info, self.cinfo.num_components as usize) 276 | } 277 | } 278 | 279 | /// Result here is mostly useless, because it will panic if the file is invalid 280 | #[inline] 281 | fn read_header(&mut self) -> io::Result<()> { 282 | // require_image = 0 allows handling this error without unwinding 283 | let res = unsafe { ffi::jpeg_read_header(&mut self.cinfo, 0) }; 284 | if res == 1 { 285 | Ok(()) 286 | } else { 287 | Err(io::Error::new(io::ErrorKind::Other, "no image in the JPEG file")) 288 | } 289 | } 290 | 291 | #[inline] 292 | #[must_use] 293 | pub fn color_space(&self) -> COLOR_SPACE { 294 | self.cinfo.jpeg_color_space 295 | } 296 | 297 | /// It's generally bogus in libjpeg 298 | #[inline] 299 | #[must_use] 300 | pub fn gamma(&self) -> f64 { 301 | self.cinfo.output_gamma 302 | } 303 | 304 | /// Get pixel density of an image from the JFIF APP0 segment. 305 | /// Returns None in case of an invalid density unit. 306 | #[inline] 307 | #[must_use] 308 | pub fn pixel_density(&mut self) -> Option { 309 | Some(PixelDensity { 310 | unit: match self.cinfo.density_unit { 311 | 0 => crate::PixelDensityUnit::PixelAspectRatio, 312 | 1 => crate::PixelDensityUnit::Inches, 313 | 2 => crate::PixelDensityUnit::Centimeters, 314 | _ => return None, 315 | }, 316 | x: self.cinfo.X_density, 317 | y: self.cinfo.Y_density, 318 | }) 319 | } 320 | 321 | /// Markers are available only if you enable them via `with_markers()` 322 | #[inline] 323 | #[must_use] 324 | pub fn markers(&self) -> MarkerIter<'_> { 325 | MarkerIter { 326 | marker_list: self.cinfo.marker_list, 327 | _references: PhantomData, 328 | } 329 | } 330 | 331 | #[inline] 332 | fn save_marker(&mut self, marker: Marker) { 333 | unsafe { 334 | ffi::jpeg_save_markers(&mut self.cinfo, marker.into(), 0xFFFF); 335 | } 336 | } 337 | 338 | /// width,height 339 | #[inline] 340 | #[must_use] 341 | pub fn size(&self) -> (usize, usize) { 342 | (self.width(), self.height()) 343 | } 344 | 345 | #[inline] 346 | #[must_use] 347 | pub fn width(&self) -> usize { 348 | self.cinfo.image_width as usize 349 | } 350 | 351 | #[inline] 352 | #[must_use] 353 | pub fn height(&self) -> usize { 354 | self.cinfo.image_height as usize 355 | } 356 | 357 | /// Start decompression with conversion to RGB 358 | #[inline(always)] 359 | pub fn rgb(self) -> io::Result> { 360 | self.to_colorspace(ffi::J_COLOR_SPACE::JCS_RGB) 361 | } 362 | 363 | /// Start decompression with conversion to `colorspace` 364 | pub fn to_colorspace(mut self, colorspace: ColorSpace) -> io::Result> { 365 | self.cinfo.out_color_space = colorspace; 366 | DecompressStarted::start_decompress(self) 367 | } 368 | 369 | /// Start decompression with conversion to RGBA 370 | #[inline(always)] 371 | pub fn rgba(self) -> io::Result> { 372 | self.to_colorspace(ffi::J_COLOR_SPACE::JCS_EXT_RGBA) 373 | } 374 | 375 | /// Start decompression with conversion to grayscale. 376 | #[inline(always)] 377 | pub fn grayscale(self) -> io::Result> { 378 | self.to_colorspace(ffi::J_COLOR_SPACE::JCS_GRAYSCALE) 379 | } 380 | 381 | /// Selects the algorithm used for the DCT step. 382 | pub fn dct_method(&mut self, method: DctMethod) { 383 | self.cinfo.dct_method = match method { 384 | DctMethod::IntegerSlow => ffi::J_DCT_METHOD::JDCT_ISLOW, 385 | DctMethod::IntegerFast => ffi::J_DCT_METHOD::JDCT_IFAST, 386 | DctMethod::Float => ffi::J_DCT_METHOD::JDCT_FLOAT, 387 | } 388 | } 389 | 390 | // If `true`, do careful upsampling of chroma components. If `false`, 391 | // a faster but sloppier method is used. Default is `true`. The visual 392 | // impact of the sloppier method is often very small. 393 | pub fn do_fancy_upsampling(&mut self, value: bool) { 394 | self.cinfo.do_fancy_upsampling = ffi::boolean::from(value); 395 | } 396 | 397 | /// If `true`, interblock smoothing is applied in early stages of decoding 398 | /// progressive JPEG files; if `false`, not. Default is `true`. Early 399 | /// progression stages look "fuzzy" with smoothing, "blocky" without. 400 | /// In any case, block smoothing ceases to be applied after the first few 401 | /// AC coefficients are known to full accuracy, so it is relevant only 402 | /// when using buffered-image mode for progressive images. 403 | pub fn do_block_smoothing(&mut self, value: bool) { 404 | self.cinfo.do_block_smoothing = ffi::boolean::from(value); 405 | } 406 | 407 | #[inline(always)] 408 | pub fn raw(mut self) -> io::Result> { 409 | self.cinfo.raw_data_out = ffi::boolean::from(true); 410 | DecompressStarted::start_decompress(self) 411 | } 412 | 413 | fn out_color_space(&self) -> ColorSpace { 414 | self.cinfo.out_color_space 415 | } 416 | 417 | /// Start decompression without colorspace conversion 418 | pub fn image(self) -> io::Result> { 419 | use crate::ffi::J_COLOR_SPACE::{JCS_CMYK, JCS_GRAYSCALE, JCS_RGB}; 420 | match self.out_color_space() { 421 | JCS_RGB => Ok(Format::RGB(DecompressStarted::start_decompress(self)?)), 422 | JCS_CMYK => Ok(Format::CMYK(DecompressStarted::start_decompress(self)?)), 423 | JCS_GRAYSCALE => Ok(Format::Gray(DecompressStarted::start_decompress(self)?)), 424 | _ => Ok(Format::RGB(self.rgb()?)), 425 | } 426 | } 427 | 428 | /// Rescales the output image by `numerator / 8` during decompression. 429 | /// `numerator` must be between 1 and 16. 430 | /// Thus setting a value of `8` will result in an unscaled image. 431 | #[track_caller] 432 | #[inline] 433 | pub fn scale(&mut self, numerator: u8) { 434 | assert!(1 <= numerator && numerator <= 16, "numerator must be between 1 and 16"); 435 | self.cinfo.scale_num = numerator.into(); 436 | self.cinfo.scale_denom = 8; 437 | } 438 | } 439 | 440 | /// See `Decompress.image()` 441 | pub enum Format { 442 | RGB(DecompressStarted), 443 | Gray(DecompressStarted), 444 | CMYK(DecompressStarted), 445 | } 446 | 447 | /// See methods on `Decompress` 448 | pub struct DecompressStarted { 449 | dec: Decompress, 450 | } 451 | 452 | impl DecompressStarted { 453 | fn start_decompress(dec: Decompress) -> io::Result { 454 | let mut dec = Self { dec }; 455 | if 0 != unsafe { ffi::jpeg_start_decompress(&mut dec.dec.cinfo) } { 456 | Ok(dec) 457 | } else { 458 | io_suspend_err() 459 | } 460 | } 461 | 462 | #[must_use] 463 | pub fn color_space(&self) -> ColorSpace { 464 | self.dec.out_color_space() 465 | } 466 | 467 | /// Gets the minimal buffer size for using `DecompressStarted::read_scanlines_flat_into` 468 | #[inline(always)] 469 | #[must_use] 470 | pub fn min_flat_buffer_size(&self) -> usize { 471 | self.color_space().num_components() * self.width() * self.height() 472 | } 473 | 474 | fn can_read_more_scanlines(&self) -> bool { 475 | self.dec.cinfo.output_scanline < self.dec.cinfo.output_height 476 | } 477 | 478 | /// Append data 479 | #[track_caller] 480 | pub fn read_raw_data(&mut self, image_dest: &mut [&mut Vec]) { 481 | while self.can_read_more_scanlines() { 482 | self.read_raw_data_chunk(image_dest); 483 | } 484 | } 485 | 486 | #[track_caller] 487 | fn read_raw_data_chunk(&mut self, image_dest: &mut [&mut Vec]) { 488 | assert!(0 != self.dec.cinfo.raw_data_out, "Raw data not set"); 489 | 490 | let mcu_height = self.dec.cinfo.max_v_samp_factor as usize * DCTSIZE; 491 | if mcu_height > MAX_MCU_HEIGHT { 492 | panic!("Subsampling factor too large"); 493 | } 494 | 495 | let num_components = self.dec.components().len(); 496 | if num_components > MAX_COMPONENTS || num_components > image_dest.len() { 497 | panic!("Too many components. Image has {}, destination vector has {} (max supported is {})", num_components, image_dest.len(), MAX_COMPONENTS); 498 | } 499 | 500 | unsafe { 501 | let mut row_ptrs = [[ptr::null_mut::(); MAX_MCU_HEIGHT]; MAX_COMPONENTS]; 502 | let mut comp_ptrs = [ptr::null_mut::<*mut ffi::JSAMPLE>(); MAX_COMPONENTS]; 503 | for ((comp_info, comp_dest), (comp_ptrs, row_ptrs)) in self.dec.components().iter().zip(&mut *image_dest).zip(comp_ptrs.iter_mut().zip(row_ptrs.iter_mut())) { 504 | let row_stride = comp_info.row_stride(); 505 | 506 | let comp_height = comp_info.v_samp_factor as usize * DCTSIZE; 507 | let required_len = comp_height * row_stride; 508 | comp_dest.try_reserve(required_len).expect("oom"); 509 | let comp_dest = &mut comp_dest.spare_capacity_mut()[..required_len]; 510 | 511 | // row_ptrs were initialized to null 512 | for (row_ptr, comp_dest) in row_ptrs.iter_mut().zip(comp_dest.chunks_exact_mut(row_stride)).take(comp_height) { 513 | *row_ptr = comp_dest.as_mut_ptr().cast(); 514 | } 515 | *comp_ptrs = row_ptrs.as_mut_ptr(); 516 | } 517 | 518 | let lines_read = ffi::jpeg_read_raw_data(&mut self.dec.cinfo, comp_ptrs.as_mut_ptr(), mcu_height as u32) as usize; 519 | 520 | assert_eq!(lines_read, mcu_height); // Partial reads would make subsampled height tricky to define 521 | 522 | for (comp_info, comp_dest) in self.dec.components().iter().zip(image_dest) { 523 | let row_stride = comp_info.row_stride(); 524 | 525 | let comp_height = comp_info.v_samp_factor as usize * DCTSIZE; 526 | let original_len = comp_dest.len(); 527 | let required_len = comp_height * row_stride; 528 | debug_assert!(original_len + required_len <= comp_dest.capacity()); 529 | comp_dest.set_len(original_len + required_len); 530 | } 531 | } 532 | } 533 | 534 | #[must_use] 535 | pub fn width(&self) -> usize { 536 | self.dec.cinfo.output_width as usize 537 | } 538 | 539 | #[must_use] 540 | pub fn height(&self) -> usize { 541 | self.dec.cinfo.output_height as usize 542 | } 543 | 544 | /// Supports any pixel type that is marked as "plain old data", see bytemuck crate. 545 | /// 546 | /// Pixels can either have number of bytes matching number of channels, e.g. RGB as 547 | /// `[u8; 3]` or `rgb::RGB8`, or be an amorphous blob of `u8`s. 548 | pub fn read_scanlines(&mut self) -> io::Result> { 549 | let num_components = self.color_space().num_components(); 550 | if num_components != mem::size_of::() && mem::size_of::() != 1 { 551 | return Err(io::Error::new( 552 | io::ErrorKind::Unsupported, 553 | format!("pixel size must have {num_components} bytes, but has {}", mem::size_of::()), 554 | )); 555 | } 556 | let width = self.width(); 557 | let height = self.height(); 558 | let mut image_dst: Vec = Vec::new(); 559 | let required_len = height * width * (num_components / mem::size_of::()); 560 | image_dst.try_reserve_exact(required_len).map_err(|_| io::ErrorKind::OutOfMemory)?; 561 | let read_len = self.read_scanlines_into_uninit(&mut image_dst.spare_capacity_mut()[..required_len])?.len(); 562 | if read_len <= required_len { 563 | unsafe { image_dst.set_len(read_len); } 564 | } 565 | Ok(image_dst) 566 | } 567 | 568 | /// Supports any pixel type that is marked as "plain old data", see bytemuck crate. 569 | /// `[u8; 3]` and `rgb::RGB8` are fine, for example. `[u8]` is allowed for any pixel type. 570 | /// 571 | /// Allocation-less version of `read_scanlines` 572 | pub fn read_scanlines_into<'dest, T: Pod>(&mut self, dest: &'dest mut [T]) -> io::Result<&'dest mut [T]> { 573 | let dest_uninit = unsafe { 574 | std::mem::transmute::<&'dest mut [T], &'dest mut [MaybeUninit]>(dest) 575 | }; 576 | self.read_scanlines_into_uninit(dest_uninit) 577 | } 578 | 579 | /// Returns written-to slice 580 | pub fn read_scanlines_into_uninit<'dest, T: Pod>(&mut self, dest: &'dest mut [MaybeUninit]) -> io::Result<&'dest mut [T]> { 581 | let num_components = self.color_space().num_components(); 582 | let item_size = if mem::size_of::() == 1 { 583 | num_components 584 | } else if num_components == mem::size_of::() { 585 | 1 586 | } else { 587 | return Err(io::Error::new( 588 | io::ErrorKind::Unsupported, 589 | format!("pixel size must have {num_components} bytes, but has {}", mem::size_of::()), 590 | )); 591 | }; 592 | let width = self.width(); 593 | let height = self.height(); 594 | let line_width = width * item_size; 595 | if dest.len() % line_width != 0 { 596 | return Err(io::Error::new( 597 | io::ErrorKind::Unsupported, 598 | format!("destination slice length must be multiple of {width}x{num_components} bytes long, got {}B", std::mem::size_of_val(dest)), 599 | )); 600 | } 601 | for row in dest.chunks_exact_mut(line_width) { 602 | if !self.can_read_more_scanlines() { 603 | return Err(io::ErrorKind::UnexpectedEof.into()); 604 | } 605 | let start_line = self.dec.cinfo.output_scanline as usize; 606 | let mut row_ptr = row.as_mut_ptr().cast::(); 607 | let rows = std::ptr::addr_of_mut!(row_ptr); 608 | unsafe { 609 | let rows_read = ffi::jpeg_read_scanlines(&mut self.dec.cinfo, rows, 1) as usize; 610 | debug_assert_eq!(start_line + rows_read, self.dec.cinfo.output_scanline as usize, "{start_line}+{rows_read} != {} of {height}", self.dec.cinfo.output_scanline); 611 | if 0 == rows_read { 612 | return Err(io::ErrorKind::UnexpectedEof.into()); 613 | } 614 | } 615 | } 616 | let dest_init = unsafe { 617 | std::mem::transmute::<&'dest mut [MaybeUninit], &'dest mut [T]>(dest) 618 | }; 619 | Ok(dest_init) 620 | } 621 | 622 | #[deprecated(note = "use read_scanlines::")] 623 | #[doc(hidden)] 624 | pub fn read_scanlines_flat(&mut self) -> io::Result> { 625 | self.read_scanlines() 626 | } 627 | 628 | #[deprecated(note = "use read_scanlines_into::")] 629 | #[doc(hidden)] 630 | pub fn read_scanlines_flat_into<'dest>(&mut self, dest: &'dest mut [u8]) -> io::Result<&'dest mut [u8]> { 631 | self.read_scanlines_into(dest) 632 | } 633 | 634 | #[must_use] 635 | pub fn components(&self) -> &[CompInfo] { 636 | self.dec.components() 637 | } 638 | 639 | #[deprecated(note = "too late to mutate, use components()")] 640 | #[doc(hidden)] 641 | pub fn components_mut(&mut self) -> &[CompInfo] { 642 | self.dec.components_mut() 643 | } 644 | 645 | #[deprecated(note = "use finish()")] 646 | #[doc(hidden)] 647 | #[must_use] 648 | pub fn finish_decompress(mut self) -> bool { 649 | self.finish_internal().is_ok() 650 | } 651 | 652 | /// Finish decompress and return the reader 653 | pub fn finish_into_inner(mut self) -> io::Result where R: BufRead { 654 | self.finish_internal()?; 655 | self.dec.cinfo.src = ptr::null_mut(); 656 | let mgr = self.dec.src_mgr.take().ok_or(io::ErrorKind::Other)?; 657 | Ok(mgr.into_inner()) 658 | } 659 | 660 | #[inline] 661 | pub fn finish(mut self) -> io::Result<()> { 662 | self.finish_internal() 663 | } 664 | 665 | #[inline] 666 | fn finish_internal(&mut self) -> io::Result<()> { 667 | if 0 != unsafe { ffi::jpeg_finish_decompress(&mut self.dec.cinfo) } { 668 | Ok(()) 669 | } else { 670 | io_suspend_err() 671 | } 672 | } 673 | } 674 | 675 | #[cold] 676 | fn io_suspend_err() -> io::Result { 677 | Err(io::ErrorKind::WouldBlock.into()) 678 | } 679 | 680 | impl Drop for Decompress { 681 | fn drop(&mut self) { 682 | unsafe { 683 | ffi::jpeg_destroy_decompress(&mut self.cinfo); 684 | } 685 | } 686 | } 687 | 688 | #[test] 689 | fn read_incomplete_file() { 690 | use crate::colorspace::{ColorSpace, ColorSpaceExt}; 691 | use std::fs::File; 692 | use std::io::Read; 693 | 694 | let data = std::fs::read("tests/test.jpg").unwrap(); 695 | assert_eq!(2169, data.len()); 696 | 697 | // the reader fakes EOI marker, so it always succeeds! 698 | for l in [data.len()/2, data.len()/3, data.len()/4, data.len()-4, data.len()-3, data.len()-2, data.len()-1] { 699 | let dinfo = Decompress::new_mem(&data[..l]).unwrap(); 700 | let mut dinfo = dinfo.rgb().unwrap(); 701 | let _bitmap: Vec<[u8; 3]> = dinfo.read_scanlines().unwrap(); 702 | let remaining = dinfo.finish_into_inner().unwrap(); 703 | assert_eq!(0, remaining.len()); 704 | } 705 | } 706 | 707 | #[test] 708 | fn mem_no_trailer() { 709 | let data = std::fs::read("tests/test.jpg").unwrap(); 710 | let dinfo = Decompress::new_mem(&data).unwrap(); 711 | let mut dinfo = dinfo.rgb().unwrap(); 712 | 713 | let _: Vec<[u8; 3]> = dinfo.read_scanlines().unwrap(); 714 | 715 | assert_eq!(dinfo.finish_into_inner().unwrap().len(), 0); 716 | } 717 | 718 | #[test] 719 | fn file_no_trailer() { 720 | let dinfo = Decompress::new_file(File::open("tests/test.jpg").unwrap()).unwrap(); 721 | let mut dinfo = dinfo.rgb().unwrap(); 722 | 723 | let _: Vec<[u8; 3]> = dinfo.read_scanlines().unwrap(); 724 | 725 | assert_eq!(dinfo.finish_into_inner().unwrap().buffer().len(), 0); 726 | } 727 | 728 | #[test] 729 | fn mem_trailer() { 730 | let data = std::fs::read("tests/trailer.jpg").unwrap(); 731 | let dinfo = Decompress::new_mem(&data).unwrap(); 732 | let mut dinfo = dinfo.rgb().unwrap(); 733 | 734 | let _: Vec<[u8; 3]> = dinfo.read_scanlines().unwrap(); 735 | 736 | assert_ne!(dinfo.finish_into_inner().unwrap().len(), 0); 737 | } 738 | 739 | #[test] 740 | fn file_trailer() { 741 | let dinfo = Decompress::new_file(File::open("tests/trailer.jpg").unwrap()).unwrap(); 742 | let mut dinfo = dinfo.rgb().unwrap(); 743 | 744 | let _: Vec<[u8; 3]> = dinfo.read_scanlines().unwrap(); 745 | 746 | assert_ne!(dinfo.finish_into_inner().unwrap().buffer().len(), 0); 747 | } 748 | 749 | #[test] 750 | fn file_trailer_bytes_left() { 751 | let dinfo = Decompress::new_file(File::open("tests/test.jpg").unwrap()).unwrap(); 752 | let mut dinfo = dinfo.rgb().unwrap(); 753 | 754 | let _: Vec<[u8; 3]> = dinfo.read_scanlines().unwrap(); 755 | 756 | assert_eq!(dinfo.finish_into_inner().unwrap().buffer().len(), 0); 757 | } 758 | 759 | #[test] 760 | fn read_file() { 761 | use crate::colorspace::{ColorSpace, ColorSpaceExt}; 762 | use std::fs::File; 763 | use std::io::Read; 764 | 765 | let data = std::fs::read("tests/test.jpg").unwrap(); 766 | assert_eq!(2169, data.len()); 767 | 768 | let dinfo = Decompress::new_mem(&data[..]).unwrap(); 769 | 770 | assert_eq!(1.0, dinfo.gamma()); 771 | assert_eq!(ColorSpace::JCS_YCbCr, dinfo.color_space()); 772 | assert_eq!(dinfo.components().len(), dinfo.color_space().num_components() as usize); 773 | 774 | assert_eq!((45, 30), dinfo.size()); 775 | { 776 | let comps = dinfo.components(); 777 | assert_eq!(2, comps[0].h_samp_factor); 778 | assert_eq!(2, comps[0].v_samp_factor); 779 | 780 | assert_eq!(48, comps[0].row_stride()); 781 | assert_eq!(32, comps[0].col_stride()); 782 | 783 | assert_eq!(1, comps[1].h_samp_factor); 784 | assert_eq!(1, comps[1].v_samp_factor); 785 | assert_eq!(1, comps[2].h_samp_factor); 786 | assert_eq!(1, comps[2].v_samp_factor); 787 | 788 | assert_eq!(24, comps[1].row_stride()); 789 | assert_eq!(16, comps[1].col_stride()); 790 | assert_eq!(24, comps[2].row_stride()); 791 | assert_eq!(16, comps[2].col_stride()); 792 | } 793 | 794 | let mut dinfo = dinfo.raw().unwrap(); 795 | 796 | let mut has_chunks = false; 797 | let mut bitmaps = [&mut Vec::new(), &mut Vec::new(), &mut Vec::new()]; 798 | while dinfo.can_read_more_scanlines() { 799 | has_chunks = true; 800 | dinfo.read_raw_data_chunk(&mut bitmaps); 801 | assert_eq!(bitmaps[0].len(), 4 * bitmaps[1].len()); 802 | } 803 | assert!(has_chunks); 804 | 805 | for (bitmap, comp) in bitmaps.iter().zip(dinfo.components()) { 806 | assert_eq!(comp.row_stride() * comp.col_stride(), bitmap.len()); 807 | } 808 | 809 | assert!(dinfo.finish().is_ok()); 810 | } 811 | 812 | #[test] 813 | fn no_markers() { 814 | use crate::colorspace::{ColorSpace, ColorSpaceExt}; 815 | use std::fs::File; 816 | use std::io::Read; 817 | 818 | // btw tests src manager with 1-byte len, which requires libjpeg to refill the buffer a lot 819 | let tricky_buf = io::BufReader::with_capacity(1, File::open("tests/test.jpg").unwrap()); 820 | let dinfo = Decompress::builder().from_reader(tricky_buf).unwrap(); 821 | assert_eq!(0, dinfo.markers().count()); 822 | 823 | let res = dinfo.rgb().unwrap().read_scanlines::<[u8; 3]>().unwrap(); 824 | assert_eq!(res.len(), 45 * 30); 825 | 826 | let dinfo = Decompress::builder().with_markers(&[]).from_path("tests/test.jpg").unwrap(); 827 | assert_eq!(0, dinfo.markers().count()); 828 | } 829 | 830 | #[test] 831 | fn buffer_into_inner() { 832 | use std::io::Read; 833 | 834 | let data = std::fs::read("tests/test.jpg").unwrap(); 835 | let orig_data_len = data.len(); 836 | 837 | let dec = Decompress::builder() 838 | .from_reader(BufReader::with_capacity(data.len()/17, std::io::Cursor::new(data))) 839 | .unwrap(); 840 | let mut dec = dec.rgb().unwrap(); 841 | let _: Vec<[u8; 3]> = dec.read_scanlines().unwrap(); 842 | let mut buf = dec.finish_into_inner().unwrap(); 843 | assert_eq!(0, buf.fill_buf().unwrap().len()); 844 | 845 | let mut data = buf.into_inner().into_inner(); 846 | assert_eq!(orig_data_len, data.len()); 847 | 848 | // put two images in one vec 849 | data.extend_from_slice(b"unexpected data after first image eoi"); 850 | data.append(&mut data.clone()); 851 | let appended_len = data.len() - orig_data_len; 852 | 853 | // read one image 854 | let dec = Decompress::builder() 855 | .from_reader(BufReader::with_capacity(data.len()/21, std::io::Cursor::new(data))) 856 | .unwrap(); 857 | let mut dec = dec.rgb().unwrap(); 858 | let _: Vec<[u8; 3]> = dec.read_scanlines().unwrap(); 859 | 860 | // expect buf to have the other one 861 | let mut buf = dec.finish_into_inner().unwrap(); 862 | let mut tmp = Vec::new(); 863 | buf.read_to_end(&mut tmp).unwrap(); 864 | let data = buf.into_inner().into_inner(); 865 | assert_eq!(appended_len, tmp.len()); 866 | assert_eq!(data[orig_data_len..], tmp); 867 | } 868 | 869 | #[test] 870 | fn read_file_rgb() { 871 | use crate::colorspace::{ColorSpace, ColorSpaceExt}; 872 | use std::fs::File; 873 | use std::io::Read; 874 | 875 | let data = std::fs::read("tests/test.jpg").unwrap(); 876 | let dinfo = Decompress::builder().with_markers(ALL_MARKERS).from_mem(&data[..]).unwrap(); 877 | 878 | assert_eq!(ColorSpace::JCS_YCbCr, dinfo.color_space()); 879 | 880 | assert_eq!(1, dinfo.markers().count()); 881 | 882 | let mut dinfo = dinfo.rgb().unwrap(); 883 | assert_eq!(ColorSpace::JCS_RGB, dinfo.color_space()); 884 | assert_eq!(dinfo.components().len(), dinfo.color_space().num_components() as usize); 885 | 886 | let bitmap: Vec<[u8; 3]> = dinfo.read_scanlines().unwrap(); 887 | assert_eq!(bitmap.len(), 45 * 30); 888 | 889 | assert!(!bitmap.contains(&[0; 3])); 890 | 891 | dinfo.finish().unwrap(); 892 | } 893 | 894 | #[test] 895 | fn drops_reader() { 896 | #[repr(align(1024))] 897 | struct CountsDrops<'a, R> {drop_count: &'a mut u8, reader: R} 898 | 899 | impl Drop for CountsDrops<'_, R> { 900 | fn drop(&mut self) { 901 | assert!(self as *mut _ as usize % 1024 == 0); // alignment 902 | *self.drop_count += 1; 903 | } 904 | } 905 | impl io::Read for CountsDrops<'_, R> { 906 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 907 | self.reader.read(buf) 908 | } 909 | } 910 | let mut drop_count = 0; 911 | let r = Decompress::builder().from_reader(BufReader::new(CountsDrops { 912 | drop_count: &mut drop_count, 913 | reader: File::open("tests/test.jpg").unwrap(), 914 | })).unwrap(); 915 | drop(r); 916 | assert_eq!(1, drop_count); 917 | } 918 | --------------------------------------------------------------------------------