vtcode_core/utils/
image_processing.rs1use anyhow::{Context, Result};
6use std::path::Path;
7pub use vtcode_commons::image::*;
8use vtcode_commons::paths::is_safe_relative_path;
9
10pub 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
66pub async fn read_image_file<P: AsRef<Path>>(file_path: P) -> Result<ImageData> {
68 let path = file_path.as_ref();
69
70 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 let file_contents = tokio::fs::read(path)
87 .await
88 .with_context(|| format!("Failed to read image file: {}", path.display()))?;
89
90 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 let mime_type = detect_mime_type_from_extension(path)?;
100
101 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
112pub 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}