1#![allow(dead_code)]
6
7use alloc::{string::String, vec, vec::Vec};
8
9use super::renderer::draw_string_into_buffer;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ImageFormat {
18 Ppm,
19 Bmp,
20 Tga,
21 Qoi,
22 Unknown,
23}
24
25#[derive(Debug, Clone)]
27pub struct Image {
28 pub width: usize,
29 pub height: usize,
30 pub pixels: Vec<u32>,
32 pub format: ImageFormat,
33}
34
35#[derive(Debug, Clone)]
37pub enum ImageViewerState {
38 Empty,
39 Loading,
40 Loaded,
41 Error(String),
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum ImageViewerAction {
51 None,
52 Close,
53 ZoomIn,
54 ZoomOut,
55 ZoomFit,
56}
57
58pub struct ImageViewer {
64 pub state: ImageViewerState,
65 pub image: Option<Image>,
66 pub filename: String,
67
68 pub zoom_level: usize,
70
71 pub offset_x: isize,
73 pub offset_y: isize,
74
75 pub surface_id: Option<u32>,
77
78 pub width: usize,
80 pub height: usize,
81
82 pub toolbar_height: usize,
84}
85
86const ZOOM_MIN: usize = 25;
87const ZOOM_MAX: usize = 400;
88const ZOOM_STEP: usize = 25;
89const PAN_STEP: isize = 16;
90
91impl ImageViewer {
92 pub fn new() -> Self {
94 Self {
95 state: ImageViewerState::Empty,
96 image: None,
97 filename: String::new(),
98 zoom_level: 100,
99 offset_x: 0,
100 offset_y: 0,
101 surface_id: None,
102 width: 640,
103 height: 480,
104 toolbar_height: 32,
105 }
106 }
107
108 pub fn load_ppm(data: &[u8]) -> Result<Image, &'static str> {
114 if data.len() < 3 {
115 return Err("ppm: data too short");
116 }
117
118 let is_p6 = data.starts_with(b"P6");
119 let is_p3 = data.starts_with(b"P3");
120 if !is_p3 && !is_p6 {
121 return Err("ppm: unsupported magic (expected P3 or P6)");
122 }
123
124 let mut pos: usize = 2; let mut tokens: Vec<usize> = Vec::new();
128
129 while tokens.len() < 3 && pos < data.len() {
130 while pos < data.len()
132 && (data[pos] == b' '
133 || data[pos] == b'\n'
134 || data[pos] == b'\r'
135 || data[pos] == b'\t')
136 {
137 pos += 1;
138 }
139 if pos < data.len() && data[pos] == b'#' {
141 while pos < data.len() && data[pos] != b'\n' {
142 pos += 1;
143 }
144 continue;
145 }
146 let start = pos;
148 while pos < data.len() && data[pos] >= b'0' && data[pos] <= b'9' {
149 pos += 1;
150 }
151 if pos > start {
152 let num = parse_ascii_usize(&data[start..pos])?;
153 tokens.push(num);
154 }
155 }
156
157 if tokens.len() < 3 {
158 return Err("ppm: incomplete header");
159 }
160
161 let width = tokens[0];
162 let height = tokens[1];
163 let max_val = tokens[2];
164 if width == 0 || height == 0 || max_val == 0 {
165 return Err("ppm: zero dimension or maxval");
166 }
167
168 let pixel_count = width * height;
169 let mut pixels = Vec::with_capacity(pixel_count);
170
171 if is_p6 {
172 if pos < data.len() {
174 pos += 1;
175 }
176 let bpp = if max_val > 255 { 6 } else { 3 };
177 let needed = pixel_count * bpp;
178 if pos + needed > data.len() {
179 return Err("ppm: P6 data truncated");
180 }
181 if bpp == 3 {
182 for i in 0..pixel_count {
183 let base = pos + i * 3;
184 let r = data[base] as u32;
185 let g = data[base + 1] as u32;
186 let b = data[base + 2] as u32;
187 pixels.push(0xFF00_0000 | (r << 16) | (g << 8) | b);
188 }
189 } else {
190 for i in 0..pixel_count {
192 let base = pos + i * 6;
193 let r = data[base] as u32;
194 let g = data[base + 2] as u32;
195 let b = data[base + 4] as u32;
196 pixels.push(0xFF00_0000 | (r << 16) | (g << 8) | b);
197 }
198 }
199 } else {
200 let mut rgb_vals: Vec<u32> = Vec::new();
202 while rgb_vals.len() < pixel_count * 3 && pos < data.len() {
203 while pos < data.len()
205 && (data[pos] == b' '
206 || data[pos] == b'\n'
207 || data[pos] == b'\r'
208 || data[pos] == b'\t')
209 {
210 pos += 1;
211 }
212 if pos < data.len() && data[pos] == b'#' {
213 while pos < data.len() && data[pos] != b'\n' {
214 pos += 1;
215 }
216 continue;
217 }
218 let start = pos;
219 while pos < data.len() && data[pos] >= b'0' && data[pos] <= b'9' {
220 pos += 1;
221 }
222 if pos > start {
223 let v = parse_ascii_usize(&data[start..pos]).unwrap_or(0) as u32;
224 let normalised = if max_val != 255 {
226 v * 255 / (max_val as u32)
227 } else {
228 v
229 };
230 rgb_vals.push(normalised);
231 }
232 }
233
234 if rgb_vals.len() < pixel_count * 3 {
235 return Err("ppm: P3 data truncated");
236 }
237
238 for i in 0..pixel_count {
239 let r = rgb_vals[i * 3];
240 let g = rgb_vals[i * 3 + 1];
241 let b = rgb_vals[i * 3 + 2];
242 pixels.push(0xFF00_0000 | (r << 16) | (g << 8) | b);
243 }
244 }
245
246 Ok(Image {
247 width,
248 height,
249 pixels,
250 format: ImageFormat::Ppm,
251 })
252 }
253
254 pub fn load_bmp(data: &[u8]) -> Result<Image, &'static str> {
256 if data.len() < 54 {
257 return Err("bmp: data too short for header");
258 }
259 if data[0] != 0x42 || data[1] != 0x4D {
260 return Err("bmp: invalid magic");
261 }
262
263 let data_offset = read_le_u32(data, 10) as usize;
265 let dib_size = read_le_u32(data, 14);
266 if dib_size < 40 {
267 return Err("bmp: unsupported DIB header size");
268 }
269
270 let width = read_le_i32(data, 18);
271 let height_raw = read_le_i32(data, 22);
272 let bpp = read_le_u16(data, 28) as usize;
273 let compression = read_le_u32(data, 30);
274
275 if compression != 0 && compression != 3 {
276 return Err("bmp: compressed BMPs not supported");
277 }
278 if bpp != 24 && bpp != 32 {
279 return Err("bmp: only 24-bit and 32-bit supported");
280 }
281 if width <= 0 {
282 return Err("bmp: invalid width");
283 }
284
285 let w = width as usize;
286 let (h, bottom_up) = if height_raw < 0 {
288 ((-height_raw) as usize, false)
289 } else {
290 (height_raw as usize, true)
291 };
292
293 if w == 0 || h == 0 {
294 return Err("bmp: zero dimension");
295 }
296
297 let bytes_per_pixel = bpp / 8;
298 let row_size_raw = w * bytes_per_pixel;
299 let row_stride = (row_size_raw + 3) & !3;
301
302 let needed = data_offset + row_stride * h;
303 if data.len() < needed {
304 return Err("bmp: pixel data truncated");
305 }
306
307 let pixel_count = w * h;
308 let mut pixels = vec![0u32; pixel_count];
309
310 for row in 0..h {
311 let src_row = if bottom_up { h - 1 - row } else { row };
312 let src_off = data_offset + src_row * row_stride;
313 let dst_off = row * w;
314
315 for col in 0..w {
316 let px_off = src_off + col * bytes_per_pixel;
317 let b = data[px_off] as u32;
318 let g = data[px_off + 1] as u32;
319 let r = data[px_off + 2] as u32;
320 let a = if bpp == 32 {
321 data[px_off + 3] as u32
322 } else {
323 0xFF
324 };
325 pixels[dst_off + col] = (a << 24) | (r << 16) | (g << 8) | b;
326 }
327 }
328
329 Ok(Image {
330 width: w,
331 height: h,
332 pixels,
333 format: ImageFormat::Bmp,
334 })
335 }
336
337 pub fn load_tga(data: &[u8]) -> Result<Image, &'static str> {
339 let frame = crate::video::decode::decode_tga(data).map_err(|_| "tga: decode failed")?;
340 Ok(video_frame_to_image(&frame, ImageFormat::Tga))
341 }
342
343 pub fn load_qoi(data: &[u8]) -> Result<Image, &'static str> {
345 let frame = crate::video::decode::decode_qoi(data).map_err(|_| "qoi: decode failed")?;
346 Ok(video_frame_to_image(&frame, ImageFormat::Qoi))
347 }
348
349 pub fn load_file(&mut self, filename: &str, data: &[u8]) {
351 self.filename = String::from(filename);
352 self.state = ImageViewerState::Loading;
353
354 let fmt = detect_image_format(data);
355 let result = match fmt {
356 ImageFormat::Ppm => Self::load_ppm(data),
357 ImageFormat::Bmp => Self::load_bmp(data),
358 ImageFormat::Tga => Self::load_tga(data),
359 ImageFormat::Qoi => Self::load_qoi(data),
360 ImageFormat::Unknown => {
361 if filename.ends_with(".ppm") || filename.ends_with(".pnm") {
363 Self::load_ppm(data)
364 } else if filename.ends_with(".bmp") {
365 Self::load_bmp(data)
366 } else if filename.ends_with(".tga") {
367 Self::load_tga(data)
368 } else if filename.ends_with(".qoi") {
369 Self::load_qoi(data)
370 } else {
371 Err("unknown image format")
372 }
373 }
374 };
375
376 match result {
377 Ok(img) => {
378 self.image = Some(img);
379 self.state = ImageViewerState::Loaded;
380 self.zoom_level = 100;
381 self.offset_x = 0;
382 self.offset_y = 0;
383 }
384 Err(e) => {
385 self.image = None;
386 self.state = ImageViewerState::Error(String::from(e));
387 }
388 }
389 }
390
391 pub fn zoom_in(&mut self) {
397 if self.zoom_level < ZOOM_MAX {
398 self.zoom_level += ZOOM_STEP;
399 }
400 }
401
402 pub fn zoom_out(&mut self) {
404 if self.zoom_level > ZOOM_MIN {
405 self.zoom_level -= ZOOM_STEP;
406 }
407 }
408
409 pub fn zoom_fit(&mut self) {
411 if let Some(ref img) = self.image {
412 if img.width == 0 || img.height == 0 {
413 return;
414 }
415 let view_w = self.width;
416 let view_h = self.height.saturating_sub(self.toolbar_height);
417 let zx = view_w * 100 / img.width;
419 let zy = view_h * 100 / img.height;
420 let z = zx.min(zy).clamp(ZOOM_MIN, ZOOM_MAX);
421 self.zoom_level = z;
422 self.offset_x = 0;
423 self.offset_y = 0;
424 }
425 }
426
427 pub fn handle_key(&mut self, key: u8) -> ImageViewerAction {
433 match key {
434 b'+' | b'=' => {
435 self.zoom_in();
436 ImageViewerAction::ZoomIn
437 }
438 b'-' => {
439 self.zoom_out();
440 ImageViewerAction::ZoomOut
441 }
442 b'0' => {
443 self.zoom_fit();
444 ImageViewerAction::ZoomFit
445 }
446 b'h' | b'H' => {
448 self.offset_x -= PAN_STEP;
449 ImageViewerAction::None
450 }
451 b'l' | b'L' => {
452 self.offset_x += PAN_STEP;
453 ImageViewerAction::None
454 }
455 b'k' | b'K' => {
456 self.offset_y -= PAN_STEP;
457 ImageViewerAction::None
458 }
459 b'j' | b'J' => {
460 self.offset_y += PAN_STEP;
461 ImageViewerAction::None
462 }
463 0x1B => ImageViewerAction::Close,
464 _ => ImageViewerAction::None,
465 }
466 }
467
468 pub fn render_to_buffer(&self, buffer: &mut [u32], buf_width: usize, buf_height: usize) {
476 let byte_len = buf_width * buf_height * 4;
477 let mut byte_buf = vec![0u8; byte_len];
478
479 let tb_h = self.toolbar_height;
481 for y in tb_h..buf_height {
482 for x in 0..buf_width {
483 let off = (y * buf_width + x) * 4;
484 if off + 3 >= byte_buf.len() {
485 break;
486 }
487 let gray: u8 = if ((x >> 4) ^ ((y - tb_h) >> 4)) & 1 == 0 {
489 0x3C
490 } else {
491 0x48
492 };
493 byte_buf[off] = gray;
494 byte_buf[off + 1] = gray;
495 byte_buf[off + 2] = gray;
496 byte_buf[off + 3] = 0xFF;
497 }
498 }
499
500 for y in 0..tb_h.min(buf_height) {
502 for x in 0..buf_width {
503 let off = (y * buf_width + x) * 4;
504 if off + 3 < byte_buf.len() {
505 byte_buf[off] = 0x2A;
506 byte_buf[off + 1] = 0x2A;
507 byte_buf[off + 2] = 0x2A;
508 byte_buf[off + 3] = 0xFF;
509 }
510 }
511 }
512
513 {
515 if !self.filename.is_empty() {
517 draw_string_into_buffer(
518 &mut byte_buf,
519 buf_width,
520 self.filename.as_bytes(),
521 8,
522 8,
523 0xDDDDDD,
524 );
525 } else {
526 draw_string_into_buffer(&mut byte_buf, buf_width, b"(no image)", 8, 8, 0x888888);
527 }
528
529 let zoom_str = format_zoom(self.zoom_level);
531 let zoom_x = buf_width.saturating_sub(zoom_str.len() * 8 + 8);
532 draw_string_into_buffer(&mut byte_buf, buf_width, &zoom_str, zoom_x, 8, 0xAAFFAA);
533
534 let btn_x = zoom_x.saturating_sub(56);
536 draw_string_into_buffer(&mut byte_buf, buf_width, b"[-] [+]", btn_x, 8, 0x888888);
537 }
538
539 {
541 let y = tb_h.saturating_sub(1);
542 for x in 0..buf_width {
543 let off = (y * buf_width + x) * 4;
544 if off + 3 < byte_buf.len() {
545 byte_buf[off] = 0x55;
546 byte_buf[off + 1] = 0x55;
547 byte_buf[off + 2] = 0x55;
548 byte_buf[off + 3] = 0xFF;
549 }
550 }
551 }
552
553 if let Some(ref img) = self.image {
555 let view_w = buf_width;
556 let view_h = buf_height.saturating_sub(tb_h);
557
558 let scaled_w = img.width * self.zoom_level / 100;
560 let scaled_h = img.height * self.zoom_level / 100;
561
562 let base_x: isize = if scaled_w < view_w {
564 ((view_w - scaled_w) / 2) as isize
565 } else {
566 -self.offset_x
567 };
568 let base_y: isize = if scaled_h < view_h {
569 ((view_h - scaled_h) / 2) as isize
570 } else {
571 -self.offset_y
572 };
573
574 for dy in 0..view_h {
576 let dst_y = tb_h + dy;
577 if dst_y >= buf_height {
578 break;
579 }
580
581 let img_rel_y = dy as isize - base_y;
582 if img_rel_y < 0 || img_rel_y >= scaled_h as isize {
583 continue;
584 }
585 let src_y = (img_rel_y as usize) * 100 / self.zoom_level;
587 if src_y >= img.height {
588 continue;
589 }
590
591 for dx in 0..view_w {
592 if dx >= buf_width {
593 break;
594 }
595
596 let img_rel_x = dx as isize - base_x;
597 if img_rel_x < 0 || img_rel_x >= scaled_w as isize {
598 continue;
599 }
600 let src_x = (img_rel_x as usize) * 100 / self.zoom_level;
601 if src_x >= img.width {
602 continue;
603 }
604
605 let src_px = img.pixels[src_y * img.width + src_x];
606 let a = ((src_px >> 24) & 0xFF) as u8;
607 let r = ((src_px >> 16) & 0xFF) as u8;
608 let g = ((src_px >> 8) & 0xFF) as u8;
609 let b = (src_px & 0xFF) as u8;
610
611 let off = (dst_y * buf_width + dx) * 4;
612 if off + 3 < byte_buf.len() {
613 if a == 0xFF {
614 byte_buf[off] = b;
615 byte_buf[off + 1] = g;
616 byte_buf[off + 2] = r;
617 byte_buf[off + 3] = 0xFF;
618 } else if a > 0 {
619 let inv = 255 - a as u16;
621 let a16 = a as u16;
622 byte_buf[off] =
623 ((b as u16 * a16 + byte_buf[off] as u16 * inv) / 255) as u8;
624 byte_buf[off + 1] =
625 ((g as u16 * a16 + byte_buf[off + 1] as u16 * inv) / 255) as u8;
626 byte_buf[off + 2] =
627 ((r as u16 * a16 + byte_buf[off + 2] as u16 * inv) / 255) as u8;
628 byte_buf[off + 3] = 0xFF;
629 }
630 }
632 }
633 }
634 } else {
635 let msg: &[u8] = match &self.state {
637 ImageViewerState::Empty => b"No image loaded. Open PPM, BMP, TGA, or QOI.",
638 ImageViewerState::Loading => b"Loading...",
639 ImageViewerState::Error(_) => b"Error loading image.",
640 ImageViewerState::Loaded => b"(empty)",
641 };
642 let msg_x = buf_width / 2 - (msg.len() * 8) / 2;
643 let msg_y = tb_h + (buf_height.saturating_sub(tb_h)) / 2;
644 draw_string_into_buffer(&mut byte_buf, buf_width, msg, msg_x, msg_y, 0x888888);
645 }
646
647 for (i, chunk) in byte_buf.chunks_exact(4).enumerate() {
649 if i < buffer.len() {
650 buffer[i] = (chunk[3] as u32) << 24
651 | (chunk[2] as u32) << 16
652 | (chunk[1] as u32) << 8
653 | (chunk[0] as u32);
654 }
655 }
656 }
657}
658
659pub fn detect_image_format(data: &[u8]) -> ImageFormat {
665 if data.len() >= 4 {
666 if data[0] == b'q' && data[1] == b'o' && data[2] == b'i' && data[3] == b'f' {
668 return ImageFormat::Qoi;
669 }
670 }
671 if data.len() >= 2 {
672 if data[0] == b'P' && (data[1] == b'3' || data[1] == b'6') {
674 return ImageFormat::Ppm;
675 }
676 if data[0] == 0x42 && data[1] == 0x4D {
678 return ImageFormat::Bmp;
679 }
680 }
681 if data.len() >= 18 {
683 let color_map_type = data[1];
684 let image_type = data[2];
685 let pixel_depth = data[16];
686 let valid_cmt = color_map_type <= 1;
687 let valid_type = matches!(image_type, 1 | 2 | 3 | 9 | 10 | 11);
688 let valid_depth = matches!(pixel_depth, 8 | 15 | 16 | 24 | 32);
689 if valid_cmt && valid_type && valid_depth {
690 return ImageFormat::Tga;
691 }
692 }
693 ImageFormat::Unknown
694}
695
696fn video_frame_to_image(frame: &crate::video::VideoFrame, fmt: ImageFormat) -> Image {
699 let w = frame.width as usize;
700 let h = frame.height as usize;
701 let pixel_count = w * h;
702 let mut pixels = Vec::with_capacity(pixel_count);
703
704 for y in 0..frame.height {
705 for x in 0..frame.width {
706 let (r, g, b, a) = frame.get_pixel(x, y);
707 let px = ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
709 pixels.push(px);
710 }
711 }
712
713 Image {
714 width: w,
715 height: h,
716 pixels,
717 format: fmt,
718 }
719}
720
721pub fn nearest_neighbor_scale(src: &Image, dst_width: usize, dst_height: usize) -> Vec<u32> {
725 if dst_width == 0 || dst_height == 0 || src.width == 0 || src.height == 0 {
726 return Vec::new();
727 }
728
729 let mut out = vec![0u32; dst_width * dst_height];
730 for dy in 0..dst_height {
731 let sy = dy * src.height / dst_height;
732 let sy = sy.min(src.height - 1);
733 for dx in 0..dst_width {
734 let sx = dx * src.width / dst_width;
735 let sx = sx.min(src.width - 1);
736 out[dy * dst_width + dx] = src.pixels[sy * src.width + sx];
737 }
738 }
739 out
740}
741
742fn parse_ascii_usize(bytes: &[u8]) -> Result<usize, &'static str> {
748 let mut val: usize = 0;
749 for &b in bytes {
750 if !b.is_ascii_digit() {
751 return Err("non-digit in number");
752 }
753 val = val
754 .checked_mul(10)
755 .and_then(|v| v.checked_add((b - b'0') as usize))
756 .ok_or("number overflow")?;
757 }
758 Ok(val)
759}
760
761fn read_le_u32(data: &[u8], off: usize) -> u32 {
763 (data[off] as u32)
764 | ((data[off + 1] as u32) << 8)
765 | ((data[off + 2] as u32) << 16)
766 | ((data[off + 3] as u32) << 24)
767}
768
769fn read_le_i32(data: &[u8], off: usize) -> i32 {
771 read_le_u32(data, off) as i32
772}
773
774fn read_le_u16(data: &[u8], off: usize) -> u16 {
776 (data[off] as u16) | ((data[off + 1] as u16) << 8)
777}
778
779impl ImageViewer {
780 pub fn render_to_u8_buffer(&self, buf: &mut [u8], buf_width: usize, buf_height: usize) {
784 let pixel_count = buf_width * buf_height;
785 let mut u32_buf = vec![0u32; pixel_count];
786 self.render_to_buffer(&mut u32_buf, buf_width, buf_height);
787 for (i, &px) in u32_buf.iter().enumerate() {
789 let off = i * 4;
790 if off + 3 < buf.len() {
791 buf[off] = (px & 0xFF) as u8; buf[off + 1] = ((px >> 8) & 0xFF) as u8; buf[off + 2] = ((px >> 16) & 0xFF) as u8; buf[off + 3] = ((px >> 24) & 0xFF) as u8; }
796 }
797 }
798}
799
800impl Default for ImageViewer {
801 fn default() -> Self {
802 Self::new()
803 }
804}
805
806fn format_zoom(pct: usize) -> Vec<u8> {
810 use alloc::format;
811 let s = format!("{}%", pct);
812 s.into_bytes()
813}