Skip to main content

vtcode_core/utils/
image_processing.rs

1//! Image processing utilities
2//!
3//! Re-exports and high-level wrappers for image processing.
4
5use anyhow::{Context, Result};
6use std::path::Path;
7pub use vtcode_commons::image::*;
8use vtcode_commons::paths::is_safe_relative_path;
9
10/// Reads an image from a URL and converts it to base64 format
11pub async fn read_image_from_url(url: &str) -> Result<ImageData> {
12    if !url.starts_with("http://") && !url.starts_with("https://") {
13        return Err(anyhow::anyhow!("Invalid URL: {}", url));
14    }
15
16    let client = reqwest::Client::builder()
17        .timeout(std::time::Duration::from_secs(30))
18        .build()
19        .context("Failed to create HTTP client")?;
20
21    let response = client
22        .get(url)
23        .send()
24        .await
25        .with_context(|| format!("Failed to fetch image from URL: {}", url))?;
26
27    if !response.status().is_success() {
28        return Err(anyhow::anyhow!(
29            "HTTP error when fetching image: {} (status: {})",
30            url,
31            response.status()
32        ));
33    }
34
35    let content_length = response.content_length().unwrap_or(0);
36    if content_length > 20 * 1024 * 1024 {
37        return Err(anyhow::anyhow!(
38            "Image from URL too large: {} bytes (max 20MB)",
39            content_length
40        ));
41    }
42
43    let mime_type = response
44        .headers()
45        .get(reqwest::header::CONTENT_TYPE)
46        .and_then(|v| v.to_str().ok())
47        .and_then(detect_mime_type_from_content_type);
48
49    let file_contents = response
50        .bytes()
51        .await
52        .with_context(|| format!("Failed to read response body from: {}", url))?
53        .to_vec();
54
55    let mime_type = mime_type.unwrap_or_else(|| detect_mime_type_from_data(&file_contents));
56    let base64_data = encode_to_base64(&file_contents);
57
58    Ok(ImageData {
59        base64_data,
60        mime_type,
61        file_path: url.to_string(),
62        size: file_contents.len() as u64,
63    })
64}
65
66/// Reads an image file from the local filesystem and converts it to base64 format
67pub async fn read_image_file<P: AsRef<Path>>(file_path: P) -> Result<ImageData> {
68    let path = file_path.as_ref();
69
70    // Validate the file path to ensure it's safe
71    if !is_safe_relative_path(&path.to_string_lossy()) {
72        return Err(anyhow::anyhow!(
73            "Unsafe or traversal detected in image path: {}",
74            path.display()
75        ));
76    }
77
78    if !has_supported_image_extension(path) {
79        return Err(anyhow::anyhow!(
80            "Unsupported image extension for path: {}",
81            path.display()
82        ));
83    }
84
85    // Read the file contents
86    let file_contents = tokio::fs::read(path)
87        .await
88        .with_context(|| format!("Failed to read image file: {}", path.display()))?;
89
90    // Validate file size (max 20MB for most LLM providers)
91    if file_contents.len() > 20 * 1024 * 1024 {
92        return Err(anyhow::anyhow!(
93            "Image file too large: {} bytes (max 20MB)",
94            file_contents.len()
95        ));
96    }
97
98    // Detect MIME type based on file extension
99    let mime_type = detect_mime_type_from_extension(path)?;
100
101    // Encode to base64
102    let base64_data = encode_to_base64(&file_contents);
103
104    Ok(ImageData {
105        base64_data,
106        mime_type,
107        file_path: path.display().to_string(),
108        size: file_contents.len() as u64,
109    })
110}
111
112/// Reads an image file from an absolute path (or already validated path) and converts it to base64.
113///
114/// This skips relative-path safety checks and should only be used when the caller has validated
115/// the path scope and intent.
116pub async fn read_image_file_any_path<P: AsRef<Path>>(file_path: P) -> Result<ImageData> {
117    let path = file_path.as_ref();
118
119    if !has_supported_image_extension(path) {
120        return Err(anyhow::anyhow!(
121            "Unsupported image extension for path: {}",
122            path.display()
123        ));
124    }
125
126    let file_contents = tokio::fs::read(path)
127        .await
128        .with_context(|| format!("Failed to read image file: {}", path.display()))?;
129
130    if file_contents.len() > 20 * 1024 * 1024 {
131        return Err(anyhow::anyhow!(
132            "Image file too large: {} bytes (max 20MB)",
133            file_contents.len()
134        ));
135    }
136
137    let mime_type = detect_mime_type_from_extension(path)?;
138    let base64_data = encode_to_base64(&file_contents);
139
140    Ok(ImageData {
141        base64_data,
142        mime_type,
143        file_path: path.display().to_string(),
144        size: file_contents.len() as u64,
145    })
146}