gardesk/garshot / c9208e3

Browse files

add JPEG, WebP, and PPM/PAM encoding support

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c9208e36543473a9efe85c36ade71ee1a848f681
Parents
5b7c39e
Tree
969a824

4 changed files

StatusFile+-
A garshot/src/encode/jpeg.rs 62 0
M garshot/src/encode/mod.rs 32 2
A garshot/src/encode/ppm.rs 104 0
A garshot/src/encode/webp.rs 52 0
garshot/src/encode/jpeg.rsadded
@@ -0,0 +1,62 @@
1
+//! JPEG encoding for garshot.
2
+
3
+use std::io::Cursor;
4
+use std::path::Path;
5
+
6
+use image::{ImageBuffer, RgbaImage};
7
+
8
+use crate::error::Result;
9
+
10
+/// Encode RGBA image data to JPEG file.
11
+pub fn encode_jpeg(data: &[u8], width: u32, height: u32, path: &Path, quality: u8) -> Result<()> {
12
+    let jpeg_data = encode_jpeg_to_vec(data, width, height, quality)?;
13
+    std::fs::write(path, jpeg_data)?;
14
+    Ok(())
15
+}
16
+
17
+/// Encode RGBA image data to JPEG bytes.
18
+pub fn encode_jpeg_to_vec(data: &[u8], width: u32, height: u32, quality: u8) -> Result<Vec<u8>> {
19
+    // Create image buffer from RGBA data
20
+    let img: RgbaImage = ImageBuffer::from_raw(width, height, data.to_vec())
21
+        .ok_or_else(|| crate::error::GarshotError::EncodeError("Invalid image dimensions".into()))?;
22
+
23
+    // Convert to RGB (JPEG doesn't support alpha)
24
+    let rgb_img = image::DynamicImage::ImageRgba8(img).to_rgb8();
25
+
26
+    // Encode to JPEG
27
+    let mut buffer = Cursor::new(Vec::new());
28
+    let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buffer, quality);
29
+    encoder
30
+        .encode(
31
+            rgb_img.as_raw(),
32
+            width,
33
+            height,
34
+            image::ExtendedColorType::Rgb8,
35
+        )
36
+        .map_err(|e| crate::error::GarshotError::EncodeError(e.to_string()))?;
37
+
38
+    Ok(buffer.into_inner())
39
+}
40
+
41
+#[cfg(test)]
42
+mod tests {
43
+    use super::*;
44
+
45
+    #[test]
46
+    fn test_encode_jpeg_to_vec() {
47
+        // Create a 2x2 red image
48
+        let data = vec![
49
+            255, 0, 0, 255, // Red pixel
50
+            255, 0, 0, 255, // Red pixel
51
+            255, 0, 0, 255, // Red pixel
52
+            255, 0, 0, 255, // Red pixel
53
+        ];
54
+
55
+        let jpeg_data = encode_jpeg_to_vec(&data, 2, 2, 90).unwrap();
56
+
57
+        // JPEG files start with FF D8 FF
58
+        assert!(jpeg_data.len() > 10);
59
+        assert_eq!(jpeg_data[0], 0xFF);
60
+        assert_eq!(jpeg_data[1], 0xD8);
61
+    }
62
+}
garshot/src/encode/mod.rsmodified
@@ -1,8 +1,14 @@
11
 //! Image encoding modules for garshot.
22
 
3
+pub mod jpeg;
34
 pub mod png;
5
+pub mod ppm;
6
+pub mod webp;
47
 
8
+pub use self::jpeg::{encode_jpeg, encode_jpeg_to_vec};
59
 pub use self::png::{encode_png, encode_png_to_vec};
10
+pub use self::ppm::{encode_pam, encode_pam_to_vec, encode_ppm, encode_ppm_to_vec};
11
+pub use self::webp::{encode_webp, encode_webp_to_vec};
612
 
713
 use std::path::Path;
814
 
@@ -15,11 +21,35 @@ pub fn encode(
1521
     height: u32,
1622
     path: &Path,
1723
     format: &str,
18
-    _quality: u8,
24
+    quality: u8,
1925
 ) -> Result<()> {
2026
     match format.to_lowercase().as_str() {
2127
         "png" => encode_png(data, width, height, path),
22
-        // TODO: Sprint 6 will add jpeg, webp, ppm, pam
28
+        "jpg" | "jpeg" => encode_jpeg(data, width, height, path, quality),
29
+        "webp" => encode_webp(data, width, height, path, quality),
30
+        "ppm" => encode_ppm(data, width, height, path),
31
+        "pam" => encode_pam(data, width, height, path),
32
+        _ => Err(GarshotError::EncodeError(format!(
33
+            "Unsupported format: {}",
34
+            format
35
+        ))),
36
+    }
37
+}
38
+
39
+/// Encode image data to bytes based on format.
40
+pub fn encode_to_vec(
41
+    data: &[u8],
42
+    width: u32,
43
+    height: u32,
44
+    format: &str,
45
+    quality: u8,
46
+) -> Result<Vec<u8>> {
47
+    match format.to_lowercase().as_str() {
48
+        "png" => encode_png_to_vec(data, width, height),
49
+        "jpg" | "jpeg" => encode_jpeg_to_vec(data, width, height, quality),
50
+        "webp" => encode_webp_to_vec(data, width, height, quality),
51
+        "ppm" => encode_ppm_to_vec(data, width, height),
52
+        "pam" => encode_pam_to_vec(data, width, height),
2353
         _ => Err(GarshotError::EncodeError(format!(
2454
             "Unsupported format: {}",
2555
             format
garshot/src/encode/ppm.rsadded
@@ -0,0 +1,104 @@
1
+//! PPM/PAM encoding for garshot.
2
+//!
3
+//! PPM (Portable Pixel Map) and PAM (Portable Arbitrary Map) are simple
4
+//! uncompressed formats, ideal for piping to other tools.
5
+
6
+use std::io::Write;
7
+use std::path::Path;
8
+
9
+use crate::error::Result;
10
+
11
+/// Encode RGBA image data to PPM file (RGB, no alpha).
12
+pub fn encode_ppm(data: &[u8], width: u32, height: u32, path: &Path) -> Result<()> {
13
+    let ppm_data = encode_ppm_to_vec(data, width, height)?;
14
+    std::fs::write(path, ppm_data)?;
15
+    Ok(())
16
+}
17
+
18
+/// Encode RGBA image data to PPM bytes (RGB, no alpha).
19
+pub fn encode_ppm_to_vec(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
20
+    let mut buffer = Vec::with_capacity(width as usize * height as usize * 3 + 50);
21
+
22
+    // PPM header: P6 (binary RGB)
23
+    writeln!(buffer, "P6")?;
24
+    writeln!(buffer, "{} {}", width, height)?;
25
+    writeln!(buffer, "255")?;
26
+
27
+    // Convert RGBA to RGB
28
+    for pixel in data.chunks_exact(4) {
29
+        buffer.push(pixel[0]); // R
30
+        buffer.push(pixel[1]); // G
31
+        buffer.push(pixel[2]); // B
32
+    }
33
+
34
+    Ok(buffer)
35
+}
36
+
37
+/// Encode RGBA image data to PAM file (with alpha).
38
+pub fn encode_pam(data: &[u8], width: u32, height: u32, path: &Path) -> Result<()> {
39
+    let pam_data = encode_pam_to_vec(data, width, height)?;
40
+    std::fs::write(path, pam_data)?;
41
+    Ok(())
42
+}
43
+
44
+/// Encode RGBA image data to PAM bytes (with alpha).
45
+pub fn encode_pam_to_vec(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
46
+    let mut buffer = Vec::with_capacity(data.len() + 100);
47
+
48
+    // PAM header
49
+    writeln!(buffer, "P7")?;
50
+    writeln!(buffer, "WIDTH {}", width)?;
51
+    writeln!(buffer, "HEIGHT {}", height)?;
52
+    writeln!(buffer, "DEPTH 4")?;
53
+    writeln!(buffer, "MAXVAL 255")?;
54
+    writeln!(buffer, "TUPLTYPE RGB_ALPHA")?;
55
+    writeln!(buffer, "ENDHDR")?;
56
+
57
+    // Raw RGBA data
58
+    buffer.extend_from_slice(data);
59
+
60
+    Ok(buffer)
61
+}
62
+
63
+#[cfg(test)]
64
+mod tests {
65
+    use super::*;
66
+
67
+    #[test]
68
+    fn test_encode_ppm_to_vec() {
69
+        // Create a 2x2 red image
70
+        let data = vec![
71
+            255, 0, 0, 255, // Red pixel
72
+            0, 255, 0, 255, // Green pixel
73
+            0, 0, 255, 255, // Blue pixel
74
+            255, 255, 255, 255, // White pixel
75
+        ];
76
+
77
+        let ppm_data = encode_ppm_to_vec(&data, 2, 2).unwrap();
78
+
79
+        // Check header starts correctly
80
+        assert!(ppm_data.starts_with(b"P6\n2 2\n255\n"));
81
+
82
+        // Header is "P6\n2 2\n255\n" = 11 bytes, then 12 bytes of pixel data
83
+        assert_eq!(ppm_data.len(), 11 + 12);
84
+    }
85
+
86
+    #[test]
87
+    fn test_encode_pam_to_vec() {
88
+        let data = vec![
89
+            255, 0, 0, 128, // Semi-transparent red
90
+            0, 255, 0, 255, // Opaque green
91
+        ];
92
+
93
+        let pam_data = encode_pam_to_vec(&data, 2, 1).unwrap();
94
+
95
+        // Check header contains expected elements
96
+        let header_str = String::from_utf8_lossy(&pam_data);
97
+        assert!(header_str.contains("P7"));
98
+        assert!(header_str.contains("WIDTH 2"));
99
+        assert!(header_str.contains("HEIGHT 1"));
100
+        assert!(header_str.contains("DEPTH 4"));
101
+        assert!(header_str.contains("TUPLTYPE RGB_ALPHA"));
102
+        assert!(header_str.contains("ENDHDR"));
103
+    }
104
+}
garshot/src/encode/webp.rsadded
@@ -0,0 +1,52 @@
1
+//! WebP encoding for garshot.
2
+
3
+use std::io::Cursor;
4
+use std::path::Path;
5
+
6
+use image::{ImageBuffer, RgbaImage};
7
+
8
+use crate::error::Result;
9
+
10
+/// Encode RGBA image data to WebP file.
11
+pub fn encode_webp(data: &[u8], width: u32, height: u32, path: &Path, quality: u8) -> Result<()> {
12
+    let webp_data = encode_webp_to_vec(data, width, height, quality)?;
13
+    std::fs::write(path, webp_data)?;
14
+    Ok(())
15
+}
16
+
17
+/// Encode RGBA image data to WebP bytes.
18
+pub fn encode_webp_to_vec(data: &[u8], width: u32, height: u32, _quality: u8) -> Result<Vec<u8>> {
19
+    // Create image buffer from RGBA data
20
+    let img: RgbaImage = ImageBuffer::from_raw(width, height, data.to_vec())
21
+        .ok_or_else(|| crate::error::GarshotError::EncodeError("Invalid image dimensions".into()))?;
22
+
23
+    // Encode to WebP (lossless for screenshots)
24
+    let mut buffer = Cursor::new(Vec::new());
25
+    img.write_to(&mut buffer, image::ImageFormat::WebP)
26
+        .map_err(|e| crate::error::GarshotError::EncodeError(e.to_string()))?;
27
+
28
+    Ok(buffer.into_inner())
29
+}
30
+
31
+#[cfg(test)]
32
+mod tests {
33
+    use super::*;
34
+
35
+    #[test]
36
+    fn test_encode_webp_to_vec() {
37
+        // Create a 2x2 red image
38
+        let data = vec![
39
+            255, 0, 0, 255, // Red pixel
40
+            255, 0, 0, 255, // Red pixel
41
+            255, 0, 0, 255, // Red pixel
42
+            255, 0, 0, 255, // Red pixel
43
+        ];
44
+
45
+        let webp_data = encode_webp_to_vec(&data, 2, 2, 90).unwrap();
46
+
47
+        // WebP files start with RIFF header
48
+        assert!(webp_data.len() > 10);
49
+        assert_eq!(&webp_data[0..4], b"RIFF");
50
+        assert_eq!(&webp_data[8..12], b"WEBP");
51
+    }
52
+}