vtcode_core/utils/
image_processing.rs1use anyhow::{Context, Result};
6pub use vtcode_commons::image::*;
7
8pub async fn read_image_from_url(url: &str) -> Result<ImageData> {
10 if !url.starts_with("http://") && !url.starts_with("https://") {
11 return Err(anyhow::anyhow!("Invalid URL: {url}"));
12 }
13
14 let client = reqwest::Client::builder()
15 .timeout(std::time::Duration::from_secs(30))
16 .build()
17 .context("Failed to create HTTP client")?;
18
19 let response = client
20 .get(url)
21 .send()
22 .await
23 .with_context(|| format!("Failed to fetch image from URL: {url}"))?;
24
25 if !response.status().is_success() {
26 return Err(anyhow::anyhow!("HTTP error when fetching image: {} (status: {})", url, response.status()));
27 }
28
29 let content_length = response.content_length().unwrap_or(0);
30 if content_length > 20 * 1024 * 1024 {
31 return Err(anyhow::anyhow!("Image from URL too large: {content_length} bytes (max 20MB)"));
32 }
33
34 let declared_mime_type = response
35 .headers()
36 .get(reqwest::header::CONTENT_TYPE)
37 .and_then(|v| v.to_str().ok())
38 .and_then(detect_mime_type_from_content_type);
39
40 if let Some(content_type) = response
44 .headers()
45 .get(reqwest::header::CONTENT_TYPE)
46 .and_then(|v| v.to_str().ok())
47 && content_type.to_lowercase().starts_with("image/")
48 && declared_mime_type
49 .as_deref()
50 .is_none_or(|mime| !is_supported_image_mime_type(mime))
51 {
52 return Err(anyhow::anyhow!("Unsupported image content type for LLM vision: {content_type}"));
53 }
54
55 let mime_type = declared_mime_type;
56
57 let file_contents = response
58 .bytes()
59 .await
60 .with_context(|| format!("Failed to read response body from: {url}"))?
61 .to_vec();
62
63 let leading = file_contents.iter().take_while(|byte| byte.is_ascii_whitespace()).count();
66 if file_contents.len() > leading && file_contents[leading] == b'<' {
67 return Err(anyhow::anyhow!("URL did not return raster image data: {url}"));
68 }
69
70 let mime_type = mime_type.unwrap_or_else(|| detect_mime_type_from_data(&file_contents));
71 if !is_supported_image_mime_type(&mime_type) {
72 return Err(anyhow::anyhow!("Unsupported image content type for LLM vision: {mime_type}"));
73 }
74 let base64_data = encode_to_base64(&file_contents);
75
76 Ok(ImageData {
77 base64_data,
78 mime_type,
79 file_path: url.to_string(),
80 size: file_contents.len() as u64,
81 })
82}