1use crate::textflow::Rect;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub struct ImageId(pub usize);
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ImageFormat {
10 Jpeg,
12 Png,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ImageFit {
19 Fit,
21 Fill,
23 Stretch,
25 None,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ColorSpace {
32 DeviceRGB,
34 DeviceGray,
36}
37
38impl ColorSpace {
39 pub fn pdf_name(&self) -> &'static str {
41 match self {
42 ColorSpace::DeviceRGB => "DeviceRGB",
43 ColorSpace::DeviceGray => "DeviceGray",
44 }
45 }
46}
47
48pub struct ImageData {
50 pub width: u32,
52 pub height: u32,
54 pub format: ImageFormat,
56 pub color_space: ColorSpace,
58 pub bits_per_component: u8,
60 pub data: Vec<u8>,
62 pub smask_data: Option<Vec<u8>>,
64}
65
66#[derive(Debug)]
68pub struct ImagePlacement {
69 pub x: f64,
71 pub y: f64,
73 pub width: f64,
75 pub height: f64,
77 pub clip: Option<ClipRect>,
79}
80
81#[derive(Debug)]
83pub struct ClipRect {
84 pub x: f64,
86 pub y: f64,
88 pub width: f64,
90 pub height: f64,
92}
93
94pub fn detect_format(data: &[u8]) -> Result<ImageFormat, String> {
96 if data.len() < 4 {
97 return Err("Image data too short to detect format".to_string());
98 }
99 if data[0] == 0xFF && data[1] == 0xD8 {
100 Ok(ImageFormat::Jpeg)
101 } else if data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 {
102 Ok(ImageFormat::Png)
103 } else {
104 Err("Unsupported image format (expected JPEG or PNG)".to_string())
105 }
106}
107
108pub fn load_image(data: Vec<u8>) -> Result<ImageData, String> {
110 let format = detect_format(&data)?;
111 match format {
112 ImageFormat::Jpeg => parse_jpeg(data),
113 ImageFormat::Png => parse_png(data),
114 }
115}
116
117fn parse_jpeg(data: Vec<u8>) -> Result<ImageData, String> {
120 let (width, height, components) = jpeg_dimensions(&data)?;
121 let color_space = match components {
122 1 => ColorSpace::DeviceGray,
123 3 => ColorSpace::DeviceRGB,
124 _ => {
125 return Err(format!(
126 "Unsupported JPEG component count: {} (expected 1 or 3)",
127 components
128 ))
129 }
130 };
131
132 Ok(ImageData {
133 width,
134 height,
135 format: ImageFormat::Jpeg,
136 color_space,
137 bits_per_component: 8,
138 data,
139 smask_data: None,
140 })
141}
142
143fn jpeg_dimensions(data: &[u8]) -> Result<(u32, u32, u8), String> {
145 let len = data.len();
146 let mut i = 0;
147 while i + 1 < len {
148 if data[i] != 0xFF {
149 i += 1;
150 continue;
151 }
152 let marker = data[i + 1];
153 if (0xC0..=0xC3).contains(&marker) {
155 if i + 9 >= len {
156 return Err("JPEG SOF marker truncated".to_string());
157 }
158 let height = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
159 let width = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
160 let components = data[i + 9];
161 return Ok((width, height, components));
162 }
163 if marker == 0xFF || marker == 0x00 {
165 i += 1;
166 continue;
167 }
168 if marker == 0xD8 || marker == 0xD9 || (0xD0..=0xD7).contains(&marker) {
170 i += 2;
171 continue;
172 }
173 if i + 3 >= len {
175 break;
176 }
177 let seg_len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
178 i += 2 + seg_len;
179 }
180 Err("No SOF marker found in JPEG data".to_string())
181}
182
183fn parse_png(data: Vec<u8>) -> Result<ImageData, String> {
185 let decoder = png::Decoder::new(data.as_slice());
186 let mut reader = decoder
187 .read_info()
188 .map_err(|e| format!("PNG decode error: {}", e))?;
189
190 let mut buf = vec![0u8; reader.output_buffer_size()];
191 let info = reader
192 .next_frame(&mut buf)
193 .map_err(|e| format!("PNG frame error: {}", e))?;
194 buf.truncate(info.buffer_size());
195
196 let width = info.width;
197 let height = info.height;
198
199 match info.color_type {
200 png::ColorType::Rgb => Ok(ImageData {
201 width,
202 height,
203 format: ImageFormat::Png,
204 color_space: ColorSpace::DeviceRGB,
205 bits_per_component: 8,
206 data: buf,
207 smask_data: None,
208 }),
209 png::ColorType::Rgba => {
210 let pixel_count = (width * height) as usize;
211 let mut rgb = Vec::with_capacity(pixel_count * 3);
212 let mut alpha = Vec::with_capacity(pixel_count);
213 for chunk in buf.chunks_exact(4) {
214 rgb.push(chunk[0]);
215 rgb.push(chunk[1]);
216 rgb.push(chunk[2]);
217 alpha.push(chunk[3]);
218 }
219 Ok(ImageData {
220 width,
221 height,
222 format: ImageFormat::Png,
223 color_space: ColorSpace::DeviceRGB,
224 bits_per_component: 8,
225 data: rgb,
226 smask_data: Some(alpha),
227 })
228 }
229 png::ColorType::Grayscale => Ok(ImageData {
230 width,
231 height,
232 format: ImageFormat::Png,
233 color_space: ColorSpace::DeviceGray,
234 bits_per_component: 8,
235 data: buf,
236 smask_data: None,
237 }),
238 png::ColorType::GrayscaleAlpha => {
239 let pixel_count = (width * height) as usize;
240 let mut gray = Vec::with_capacity(pixel_count);
241 let mut alpha = Vec::with_capacity(pixel_count);
242 for chunk in buf.chunks_exact(2) {
243 gray.push(chunk[0]);
244 alpha.push(chunk[1]);
245 }
246 Ok(ImageData {
247 width,
248 height,
249 format: ImageFormat::Png,
250 color_space: ColorSpace::DeviceGray,
251 bits_per_component: 8,
252 data: gray,
253 smask_data: Some(alpha),
254 })
255 }
256 other => Err(format!("Unsupported PNG color type: {:?}", other)),
257 }
258}
259
260pub fn calculate_placement(img_w: u32, img_h: u32, rect: &Rect, fit: ImageFit) -> ImagePlacement {
265 let iw = img_w as f64;
266 let ih = img_h as f64;
267 let pdf_bottom = rect.y;
268
269 match fit {
270 ImageFit::Fit => {
271 let scale_x = rect.width / iw;
272 let scale_y = rect.height / ih;
273 let scale = scale_x.min(scale_y);
274 let w = iw * scale;
275 let h = ih * scale;
276 let x = rect.x + (rect.width - w) / 2.0;
278 let y = pdf_bottom + (rect.height - h) / 2.0;
279 ImagePlacement {
280 x,
281 y,
282 width: w,
283 height: h,
284 clip: None,
285 }
286 }
287 ImageFit::Fill => {
288 let scale_x = rect.width / iw;
289 let scale_y = rect.height / ih;
290 let scale = scale_x.max(scale_y);
291 let w = iw * scale;
292 let h = ih * scale;
293 let x = rect.x + (rect.width - w) / 2.0;
295 let y = pdf_bottom + (rect.height - h) / 2.0;
296 ImagePlacement {
297 x,
298 y,
299 width: w,
300 height: h,
301 clip: Some(ClipRect {
302 x: rect.x,
303 y: pdf_bottom,
304 width: rect.width,
305 height: rect.height,
306 }),
307 }
308 }
309 ImageFit::Stretch => ImagePlacement {
310 x: rect.x,
311 y: pdf_bottom,
312 width: rect.width,
313 height: rect.height,
314 clip: None,
315 },
316 ImageFit::None => {
317 let y = pdf_bottom + (rect.height - ih);
319 ImagePlacement {
320 x: rect.x,
321 y,
322 width: iw,
323 height: ih,
324 clip: None,
325 }
326 }
327 }
328}