use std::io::{Cursor, Read};
use super::{Canvas, GenerationResult};
fn download_image(url: &str) -> Result<image::DynamicImage, String> {
let img_response = ureq::get(url)
.call()
.map_err(|e| format!("Failed to download image: {}", e))?;
let mut bytes: Vec<u8> = Vec::new();
img_response
.into_reader()
.read_to_end(&mut bytes)
.map_err(|e| format!("Failed to read image data: {}", e))?;
image::load_from_memory(&bytes)
.map_err(|e| format!("Failed to decode image: {}", e))
}
fn decode_base64_image(b64: &str) -> Result<image::DynamicImage, String> {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| format!("Failed to decode base64: {}", e))?;
image::load_from_memory(&bytes)
.map_err(|e| format!("Failed to decode image: {}", e))
}
fn image_to_canvas(img: &image::DynamicImage) -> Canvas {
let rgb = img.to_rgb8();
let w = rgb.width();
let h = rgb.height();
let mut canvas: Canvas = Vec::with_capacity(h as usize);
for y in 0..h {
let mut row = Vec::with_capacity(w as usize);
for x in 0..w {
let pixel = rgb.get_pixel(x, y);
row.push([pixel[0], pixel[1], pixel[2]]);
}
canvas.push(row);
}
canvas
}
fn is_gpt_image(model: &str) -> bool {
model.starts_with("gpt-image")
}
fn call_api(prompt: &str, api_key: &str, model: &str, size: &str) -> Result<image::DynamicImage, String> {
let json = if is_gpt_image(model) {
serde_json::json!({
"model": model,
"prompt": prompt,
"n": 1,
"size": size,
})
} else {
serde_json::json!({
"model": model,
"prompt": prompt,
"n": 1,
"size": size,
"response_format": "url",
})
};
let response = ureq::post("https://api.openai.com/v1/images/generations")
.set("Authorization", &format!("Bearer {}", api_key))
.set("Content-Type", "application/json")
.send_json(json)
.map_err(|e| format!("API request failed: {}", e))?;
let body: serde_json::Value = response
.into_json()
.map_err(|e| format!("Failed to parse API response: {}", e))?;
if let Some(err) = body["error"]["message"].as_str() {
return Err(format!("API error: {}", err));
}
if is_gpt_image(model) {
let b64 = body["data"][0]["b64_json"]
.as_str()
.ok_or_else(|| "No base64 image data in response".to_string())?;
decode_base64_image(b64)
} else {
let url = body["data"][0]["url"]
.as_str()
.ok_or_else(|| "No image URL in response".to_string())?;
download_image(url)
}
}
fn image_to_png_bytes(img: &image::DynamicImage) -> Result<Vec<u8>, String> {
let mut buf = Cursor::new(Vec::new());
img.write_to(&mut buf, image::ImageFormat::Png)
.map_err(|e| format!("Failed to encode PNG: {}", e))?;
Ok(buf.into_inner())
}
fn call_edits_api(
prompt: &str,
api_key: &str,
model: &str,
reference_images: &[Vec<u8>],
) -> Result<image::DynamicImage, String> {
let boundary = "----BitArtBoundary9876543210";
let mut body: Vec<u8> = Vec::new();
for (i, png_bytes) in reference_images.iter().enumerate() {
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(
format!(
"Content-Disposition: form-data; name=\"image[]\"; filename=\"frame{}.png\"\r\n",
i
)
.as_bytes(),
);
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
body.extend_from_slice(png_bytes);
body.extend_from_slice(b"\r\n");
}
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(b"Content-Disposition: form-data; name=\"prompt\"\r\n\r\n");
body.extend_from_slice(prompt.as_bytes());
body.extend_from_slice(b"\r\n");
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(b"Content-Disposition: form-data; name=\"model\"\r\n\r\n");
body.extend_from_slice(model.as_bytes());
body.extend_from_slice(b"\r\n");
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(b"Content-Disposition: form-data; name=\"size\"\r\n\r\n");
body.extend_from_slice(b"1024x1024\r\n");
body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
let response = ureq::post("https://api.openai.com/v1/images/edits")
.set("Authorization", &format!("Bearer {}", api_key))
.set(
"Content-Type",
&format!("multipart/form-data; boundary={}", boundary),
)
.send_bytes(&body)
.map_err(|e| format!("Edits API request failed: {}", e))?;
let resp_body: serde_json::Value = response
.into_json()
.map_err(|e| format!("Failed to parse edits API response: {}", e))?;
if let Some(err) = resp_body["error"]["message"].as_str() {
return Err(format!("API error: {}", err));
}
if let Some(b64) = resp_body["data"][0]["b64_json"].as_str() {
return decode_base64_image(b64);
}
if let Some(url) = resp_body["data"][0]["url"].as_str() {
return download_image(url);
}
Err("No image data in edits response".to_string())
}
fn single_size(_model: &str) -> &'static str {
"1024x1024"
}
pub fn generate(prompt: &str, api_key: &str, model: &str) -> Result<GenerationResult, String> {
let full_prompt = format!(
"Pixel art, 8-bit retro style, clear shapes with black outlines, vibrant colors, game sprite style: {}",
prompt
);
let img = call_api(&full_prompt, api_key, model, single_size(model))?;
let canvas = image_to_canvas(&img);
Ok(GenerationResult {
canvas,
model: model.to_string(),
})
}
pub fn generate_spritesheet(prompt: &str, api_key: &str, model: &str) -> Result<Vec<Canvas>, String> {
let base_prompt = format!(
"Pixel art, 8-bit retro style, clear shapes with black outlines, vibrant colors, \
game sprite style, plain white background, centered character: {}",
prompt
);
let img1 = call_api(&base_prompt, api_key, model, single_size(model))?;
let png1 = image_to_png_bytes(&img1)?;
let canvas1 = image_to_canvas(&img1);
if !is_gpt_image(model) {
return Ok(vec![canvas1.clone(), canvas1.clone(), canvas1]);
}
let edit_prompt_2 = format!(
"This is frame 1 of a pixel art animation of: {}. \
Create frame 2 with ONE very subtle change — the character must stay in the EXACT same position, \
same size, same outline. Only change a tiny detail that makes sense for the subject \
(like a slight arm shift or eye blink). Keep the white background. Keep everything else identical.",
prompt
);
let img2 = call_edits_api(&edit_prompt_2, api_key, model, &[png1.clone()])?;
let png2 = image_to_png_bytes(&img2)?;
let canvas2 = image_to_canvas(&img2);
let edit_prompt_3 = format!(
"These are frames 1 and 2 of a pixel art animation of: {}. \
Create frame 3 that completes the animation loop. The character must stay in the EXACT same position. \
Make a subtle change that flows from frame 2 back toward frame 1 (like returning to rest position). \
Keep the white background. Keep everything else identical.",
prompt
);
let img3 = call_edits_api(&edit_prompt_3, api_key, model, &[png1, png2])?;
let canvas3 = image_to_canvas(&img3);
Ok(vec![canvas1, canvas2, canvas3])
}