diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-08 12:44:10 +0100 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-08 12:44:10 +0100 |
| commit | 0c20fb86633104744dbccf30ad732296694fff1b (patch) | |
| tree | 02ffb8494086960b4a84decf3bdc2c8c61bfc4f6 /src/protocol/jpeg.rs | |
Initial pipewiremain
Diffstat (limited to 'src/protocol/jpeg.rs')
| -rw-r--r-- | src/protocol/jpeg.rs | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/src/protocol/jpeg.rs b/src/protocol/jpeg.rs new file mode 100644 index 0000000..8398800 --- /dev/null +++ b/src/protocol/jpeg.rs @@ -0,0 +1,351 @@ +//! JPEG parsing utilities for the UPP protocol + +use crate::error::{JpegError, Result}; +use tracing::{debug, trace, warn}; + +/// JPEG marker constants +const JPEG_SOI: u8 = 0xD8; // Start of Image +const JPEG_EOI: u8 = 0xD9; // End of Image +const JPEG_SOS: u8 = 0xDA; // Start of Scan +const JPEG_SOF0: u8 = 0xC0; // Start of Frame (Baseline DCT) +const JPEG_SOF1: u8 = 0xC1; // Start of Frame (Extended sequential DCT) +const JPEG_SOF2: u8 = 0xC2; // Start of Frame (Progressive DCT) + +/// JPEG image dimensions +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JpegDimensions { + pub width: u16, + pub height: u16, +} + +impl JpegDimensions { + /// Create new dimensions + pub fn new(width: u16, height: u16) -> Self { + Self { width, height } + } + + /// Check if dimensions are valid + pub fn is_valid(&self) -> bool { + self.width > 0 && self.height > 0 + } + + /// Get aspect ratio + pub fn aspect_ratio(&self) -> f64 { + if self.height > 0 { + self.width as f64 / self.height as f64 + } else { + 0.0 + } + } +} + +/// JPEG parser for extracting metadata +pub struct JpegParser { + enable_debug: bool, +} + +impl JpegParser { + /// Create a new JPEG parser + pub fn new() -> Self { + Self { + enable_debug: false, + } + } + + /// Create a new JPEG parser with debug enabled + pub fn new_with_debug(enable_debug: bool) -> Self { + Self { enable_debug } + } + + /// Parse JPEG dimensions from raw data + pub fn parse_dimensions(&self, data: &[u8]) -> Result<JpegDimensions> { + if data.len() < 4 { + return Err(JpegError::InvalidHeader.into()); + } + + // Check JPEG SOI marker + if data[0] != 0xFF || data[1] != JPEG_SOI { + return Err(JpegError::InvalidHeader.into()); + } + + trace!("Parsing JPEG dimensions from {} bytes", data.len()); + + let mut i = 2; + while i + 3 < data.len() { + // Look for marker + if data[i] != 0xFF { + i += 1; + continue; + } + + let marker = data[i + 1]; + i += 2; + + // Check for end markers + if marker == JPEG_EOI || marker == JPEG_SOS { + break; + } + + // Check if we have enough data for segment length + if i + 1 >= data.len() { + break; + } + + // Read segment length (big-endian) + let segment_length = ((data[i] as u16) << 8) | (data[i + 1] as u16); + if segment_length < 2 || i + segment_length as usize > data.len() { + warn!("Invalid segment length: {}", segment_length); + break; + } + + // Check for SOF markers (Start of Frame) + if self.is_sof_marker(marker) { + if segment_length < 7 { + return Err(JpegError::InvalidHeader.into()); + } + + // Height and width are in big-endian format + let height = ((data[i + 3] as u16) << 8) | (data[i + 4] as u16); + let width = ((data[i + 5] as u16) << 8) | (data[i + 6] as u16); + + if self.enable_debug { + debug!( + "Found SOF marker 0x{:02X}, dimensions: {}x{}", + marker, width, height + ); + } + + if width > 0 && height > 0 { + return Ok(JpegDimensions::new(width, height)); + } else { + return Err(JpegError::DimensionsNotFound.into()); + } + } + + // Move to next segment + i += segment_length as usize; + } + + Err(JpegError::DimensionsNotFound.into()) + } + + /// Check if a marker is a Start of Frame marker + fn is_sof_marker(&self, marker: u8) -> bool { + matches!(marker, JPEG_SOF0 | JPEG_SOF1 | JPEG_SOF2) + } + + /// Extract JPEG metadata + pub fn parse_metadata(&self, data: &[u8]) -> Result<JpegMetadata> { + let dimensions = self.parse_dimensions(data)?; + + Ok(JpegMetadata { + dimensions, + file_size: data.len(), + is_valid: true, + }) + } + + /// Validate JPEG data + pub fn validate_jpeg(&self, data: &[u8]) -> Result<bool> { + if data.len() < 4 { + return Ok(false); + } + + // Check SOI marker + if data[0] != 0xFF || data[1] != JPEG_SOI { + return Ok(false); + } + + // Check EOI marker (should be near the end) + if data.len() >= 2 { + let end = data.len() - 2; + if data[end] == 0xFF && data[end + 1] == JPEG_EOI { + return Ok(true); + } + } + + // If we can't find EOI, check if we can parse dimensions + Ok(self.parse_dimensions(data).is_ok()) + } + + /// Check if data represents a complete JPEG frame + pub fn is_complete_jpeg(&self, data: &[u8]) -> bool { + if data.len() < 4 { + return false; + } + + // Must start with SOI marker + if data[0] != 0xFF || data[1] != JPEG_SOI { + return false; + } + + // Must end with EOI marker + if data.len() >= 2 { + let end = data.len() - 2; + if data[end] == 0xFF && data[end + 1] == JPEG_EOI { + return true; + } + } + + false + } +} + +impl Default for JpegParser { + fn default() -> Self { + Self::new() + } +} + +/// JPEG metadata +#[derive(Debug, Clone)] +pub struct JpegMetadata { + pub dimensions: JpegDimensions, + pub file_size: usize, + pub is_valid: bool, +} + +impl JpegMetadata { + /// Create new metadata + pub fn new(dimensions: JpegDimensions, file_size: usize) -> Self { + Self { + dimensions, + file_size, + is_valid: true, + } + } + + /// Get estimated bit depth (assume 8-bit for most JPEGs) + pub fn estimated_bit_depth(&self) -> u8 { + 8 + } + + /// Get estimated color space (assume YUV for most JPEGs) + pub fn estimated_color_space(&self) -> &'static str { + "YUV" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::Error; + + // Sample JPEG data for testing (minimal valid JPEG) + const MINIMAL_JPEG: &[u8] = &[ + 0xFF, 0xD8, // SOI + 0xFF, 0xE0, // APP0 + 0x00, 0x10, // Length + 0x4A, 0x46, 0x49, 0x46, 0x00, // "JFIF\0" + 0x01, 0x01, // Version + 0x00, // Units + 0x00, 0x01, // Density + 0x00, 0x01, 0x00, 0x00, // No thumbnail + 0xFF, 0xC0, // SOF0 + 0x00, 0x0B, // Length + 0x08, // Precision + 0x00, 0x40, // Height (64) + 0x00, 0x40, // Width (64) + 0x03, // Components + 0x01, 0x11, 0x00, // Y component + 0x02, 0x11, 0x01, // U component + 0x03, 0x11, 0x01, // V component + 0xFF, 0xD9, // EOI + ]; + + #[test] + fn test_jpeg_dimensions_creation() { + let dims = JpegDimensions::new(640, 480); + assert_eq!(dims.width, 640); + assert_eq!(dims.height, 480); + assert!(dims.is_valid()); + assert_eq!(dims.aspect_ratio(), 640.0 / 480.0); + } + + #[test] + fn test_jpeg_dimensions_validation() { + let dims = JpegDimensions::new(0, 480); + assert!(!dims.is_valid()); + + let dims = JpegDimensions::new(640, 0); + assert!(!dims.is_valid()); + + let dims = JpegDimensions::new(0, 0); + assert!(!dims.is_valid()); + } + + #[test] + fn test_jpeg_parser_creation() { + let parser = JpegParser::new(); + assert!(!parser.enable_debug); + + let parser = JpegParser::new_with_debug(true); + assert!(parser.enable_debug); + } + + #[test] + fn test_jpeg_parser_parse_dimensions() { + let parser = JpegParser::new(); + let dimensions = parser.parse_dimensions(MINIMAL_JPEG).unwrap(); + assert_eq!(dimensions.width, 64); + assert_eq!(dimensions.height, 64); + } + + #[test] + fn test_jpeg_parser_invalid_header() { + let parser = JpegParser::new(); + let invalid_data = &[0x00, 0x01, 0x02, 0x03]; + + let result = parser.parse_dimensions(invalid_data); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::Jpeg(JpegError::InvalidHeader) + )); + } + + #[test] + fn test_jpeg_parser_short_data() { + let parser = JpegParser::new(); + let short_data = &[0xFF, 0xD8]; + + let result = parser.parse_dimensions(short_data); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::Jpeg(JpegError::InvalidHeader) + )); + } + + #[test] + fn test_jpeg_parser_validate_jpeg() { + let parser = JpegParser::new(); + assert!(parser.validate_jpeg(MINIMAL_JPEG).unwrap()); + + let invalid_data = &[0x00, 0x01, 0x02, 0x03]; + assert!(!parser.validate_jpeg(invalid_data).unwrap()); + } + + #[test] + fn test_jpeg_metadata_creation() { + let dimensions = JpegDimensions::new(640, 480); + let metadata = JpegMetadata::new(dimensions, 1024); + + assert_eq!(metadata.dimensions.width, 640); + assert_eq!(metadata.dimensions.height, 480); + assert_eq!(metadata.file_size, 1024); + assert!(metadata.is_valid); + assert_eq!(metadata.estimated_bit_depth(), 8); + assert_eq!(metadata.estimated_color_space(), "YUV"); + } + + #[test] + fn test_sof_marker_detection() { + let parser = JpegParser::new(); + assert!(parser.is_sof_marker(JPEG_SOF0)); + assert!(parser.is_sof_marker(JPEG_SOF1)); + assert!(parser.is_sof_marker(JPEG_SOF2)); + assert!(!parser.is_sof_marker(0x00)); + assert!(!parser.is_sof_marker(JPEG_SOI)); + } +} |
