Skip to main content

opi_coding_agent/
image.rs

1//! Image input handling for CLI/TUI attachment (task 3.4).
2
3use std::path::PathBuf;
4
5use opi_ai::message::{ImageSource, InputContent, MediaType};
6
7/// Default maximum image attachment size: 20 MiB.
8pub const DEFAULT_MAX_IMAGE_BYTES: u64 = 20 * 1024 * 1024;
9
10/// Detect media type from file extension.
11pub fn detect_media_type(path: PathBuf) -> Option<MediaType> {
12    let ext = path.extension()?.to_str()?.to_ascii_lowercase();
13    match ext.as_str() {
14        "png" => Some(MediaType::Png),
15        "jpg" | "jpeg" => Some(MediaType::Jpeg),
16        "gif" => Some(MediaType::Gif),
17        "webp" => Some(MediaType::WebP),
18        _ => None,
19    }
20}
21
22/// Load an image file and return an `InputContent::Image` with bytes source.
23pub fn load_image(path: &PathBuf) -> Result<InputContent, ImageLoadError> {
24    load_image_with_limit(path, DEFAULT_MAX_IMAGE_BYTES)
25}
26
27/// Load an image file with an explicit maximum byte limit.
28pub fn load_image_with_limit(
29    path: &PathBuf,
30    max_image_bytes: u64,
31) -> Result<InputContent, ImageLoadError> {
32    let media_type = detect_media_type(path.clone()).ok_or_else(|| ImageLoadError {
33        path: path.clone(),
34        reason: "unsupported image format (accepted: png, jpg/jpeg, gif, webp)".into(),
35    })?;
36    let size = std::fs::metadata(path).map_err(|e| ImageLoadError {
37        path: path.clone(),
38        reason: format!("failed to read file metadata: {e}"),
39    })?;
40    if size.len() > max_image_bytes {
41        return Err(ImageLoadError {
42            path: path.clone(),
43            reason: format!(
44                "image is {} bytes, exceeding max_image_bytes limit of {} bytes",
45                size.len(),
46                max_image_bytes
47            ),
48        });
49    }
50    let data = std::fs::read(path).map_err(|e| ImageLoadError {
51        path: path.clone(),
52        reason: format!("failed to read file: {e}"),
53    })?;
54    Ok(InputContent::Image {
55        source: ImageSource::Bytes { data },
56        media_type,
57    })
58}
59
60/// Error from loading an image file.
61#[derive(Debug)]
62pub struct ImageLoadError {
63    pub path: PathBuf,
64    pub reason: String,
65}
66
67impl std::fmt::Display for ImageLoadError {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(
70            f,
71            "image load error for {}: {}",
72            self.path.display(),
73            self.reason
74        )
75    }
76}
77
78impl std::error::Error for ImageLoadError {}