rfb_encodings/jpeg/
turbojpeg.rs

1// Copyright 2025 Dustin McAfee
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! FFI bindings to libjpeg-turbo's `TurboJPEG` API.
16//!
17//! This module provides a safe Rust wrapper around the `TurboJPEG` C API
18//! for high-performance JPEG compression.
19
20use std::ffi::c_void;
21use std::os::raw::{c_char, c_int, c_uchar, c_ulong};
22
23// TurboJPEG pixel format constants
24/// RGB pixel format (red, green, blue)
25pub const TJPF_RGB: c_int = 0;
26/// BGR pixel format (blue, green, red)
27#[allow(dead_code)]
28pub const TJPF_BGR: c_int = 1;
29/// RGBX pixel format (red, green, blue, unused)
30#[allow(dead_code)]
31pub const TJPF_RGBX: c_int = 2;
32/// BGRX pixel format (blue, green, red, unused)
33#[allow(dead_code)]
34pub const TJPF_BGRX: c_int = 3;
35/// XBGR pixel format (unused, blue, green, red)
36#[allow(dead_code)]
37pub const TJPF_XBGR: c_int = 4;
38/// XRGB pixel format (unused, red, green, blue)
39#[allow(dead_code)]
40pub const TJPF_XRGB: c_int = 5;
41/// Grayscale pixel format
42#[allow(dead_code)]
43pub const TJPF_GRAY: c_int = 6;
44
45// TurboJPEG chrominance subsampling constants
46/// 4:4:4 chrominance subsampling (no subsampling)
47#[allow(dead_code)]
48pub const TJSAMP_444: c_int = 0;
49/// 4:2:2 chrominance subsampling (2x1 subsampling)
50pub const TJSAMP_422: c_int = 1;
51/// 4:2:0 chrominance subsampling (2x2 subsampling)
52#[allow(dead_code)]
53pub const TJSAMP_420: c_int = 2;
54/// Grayscale (no chrominance)
55#[allow(dead_code)]
56pub const TJSAMP_GRAY: c_int = 3;
57
58// Opaque TurboJPEG handle
59type TjHandle = *mut c_void;
60
61// External C functions from libjpeg-turbo
62#[link(name = "turbojpeg")]
63extern "C" {
64    fn tjInitCompress() -> TjHandle;
65    fn tjDestroy(handle: TjHandle) -> c_int;
66    fn tjCompress2(
67        handle: TjHandle,
68        src_buf: *const c_uchar,
69        width: c_int,
70        pitch: c_int,
71        height: c_int,
72        pixel_format: c_int,
73        jpeg_buf: *mut *mut c_uchar,
74        jpeg_size: *mut c_ulong,
75        jpeg_subsamp: c_int,
76        jpeg_qual: c_int,
77        flags: c_int,
78    ) -> c_int;
79    fn tjFree(buffer: *mut c_uchar);
80    fn tjGetErrorStr2(handle: TjHandle) -> *const c_char;
81}
82
83/// Safe Rust wrapper for `TurboJPEG` compression.
84pub struct TurboJpegEncoder {
85    handle: TjHandle,
86}
87
88impl TurboJpegEncoder {
89    /// Creates a new `TurboJPEG` encoder.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if `TurboJPEG` initialization fails
94    pub fn new() -> Result<Self, String> {
95        let handle = unsafe { tjInitCompress() };
96        if handle.is_null() {
97            return Err("Failed to initialize TurboJPEG compressor".to_string());
98        }
99        Ok(Self { handle })
100    }
101
102    /// Compresses RGB image data to JPEG format.
103    ///
104    /// # Arguments
105    /// * `rgb_data` - RGB pixel data (3 bytes per pixel)
106    /// * `width` - Image width in pixels
107    /// * `height` - Image height in pixels
108    /// * `quality` - JPEG quality (1-100, where 100 is best quality)
109    ///
110    /// # Returns
111    ///
112    /// JPEG-compressed data as a `Vec<u8>`
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the data size is invalid or JPEG compression fails
117    #[allow(clippy::cast_possible_truncation)] // JPEG dimensions limited to u16 range
118    pub fn compress_rgb(
119        &mut self,
120        rgb_data: &[u8],
121        width: u16,
122        height: u16,
123        quality: u8,
124    ) -> Result<Vec<u8>, String> {
125        let expected_size = (width as usize) * (height as usize) * 3;
126        if rgb_data.len() != expected_size {
127            return Err(format!(
128                "Invalid RGB data size: expected {}, got {}",
129                expected_size,
130                rgb_data.len()
131            ));
132        }
133
134        let mut jpeg_buf: *mut c_uchar = std::ptr::null_mut();
135        let mut jpeg_size: c_ulong = 0;
136
137        let result = unsafe {
138            tjCompress2(
139                self.handle,
140                rgb_data.as_ptr(),
141                c_int::from(width),
142                0, // pitch = 0 means width * pixel_size
143                c_int::from(height),
144                TJPF_RGB,
145                &raw mut jpeg_buf,
146                &raw mut jpeg_size,
147                TJSAMP_422, // 4:2:2 subsampling for good quality/size balance
148                c_int::from(quality),
149                0, // flags
150            )
151        };
152
153        if result != 0 {
154            let error_msg = self.get_error_string();
155            return Err(format!("TurboJPEG compression failed: {error_msg}"));
156        }
157
158        if jpeg_buf.is_null() {
159            return Err("TurboJPEG returned null buffer".to_string());
160        }
161
162        // Copy JPEG data to Rust Vec
163        let jpeg_data =
164            unsafe { std::slice::from_raw_parts(jpeg_buf, jpeg_size as usize).to_vec() };
165
166        // Free TurboJPEG buffer
167        unsafe {
168            tjFree(jpeg_buf);
169        }
170
171        Ok(jpeg_data)
172    }
173
174    /// Gets the last error message from `TurboJPEG`.
175    fn get_error_string(&self) -> String {
176        unsafe {
177            let c_str = tjGetErrorStr2(self.handle);
178            if c_str.is_null() {
179                return "Unknown error".to_string();
180            }
181            std::ffi::CStr::from_ptr(c_str)
182                .to_string_lossy()
183                .into_owned()
184        }
185    }
186}
187
188impl Drop for TurboJpegEncoder {
189    fn drop(&mut self) {
190        unsafe {
191            tjDestroy(self.handle);
192        }
193    }
194}
195
196unsafe impl Send for TurboJpegEncoder {}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_encoder_creation() {
204        let encoder = TurboJpegEncoder::new();
205        assert!(encoder.is_ok());
206    }
207
208    #[test]
209    fn test_compress_rgb() {
210        let mut encoder = TurboJpegEncoder::new().unwrap();
211
212        // Create a simple 2x2 red image
213        let rgb_data = vec![255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0];
214
215        let result = encoder.compress_rgb(&rgb_data, 2, 2, 90);
216        assert!(result.is_ok());
217
218        let jpeg_data = result.unwrap();
219        assert!(!jpeg_data.is_empty());
220        // JPEG files start with 0xFF 0xD8
221        assert_eq!(jpeg_data[0], 0xFF);
222        assert_eq!(jpeg_data[1], 0xD8);
223    }
224}