1#[cfg(feature = "image")]
7use image::DynamicImage;
8
9use crate::style::Color;
10
11pub struct HalfBlockImage {
20 pub width: u32,
22 pub height: u32,
24 pub pixels: Vec<(Color, Color)>,
26}
27
28#[cfg(feature = "image")]
29impl HalfBlockImage {
30 pub fn from_dynamic(img: &DynamicImage, width: u32, height: u32) -> Self {
42 let Some(pixel_height) = height.checked_mul(2) else {
43 return Self::empty(width, height);
44 };
45 let pixels_total = u64::from(width).saturating_mul(u64::from(pixel_height));
46 if pixels_total == 0 || pixels_total > crate::buffer::MAX_IMAGE_PIXELS {
47 return Self::empty(width, height);
48 }
49 let resized = img.resize_exact(width, pixel_height, image::imageops::FilterType::Lanczos3);
50 let rgba = resized.to_rgba8();
51
52 let mut pixels = Vec::with_capacity((width as usize) * (height as usize));
53 for row in 0..height {
54 for col in 0..width {
55 let upper_y = row * 2;
56 let lower_y = row * 2 + 1;
57
58 let up = rgba.get_pixel(col, upper_y);
59 let lo = rgba.get_pixel(col, lower_y);
60
61 let upper = Color::Rgb(up[0], up[1], up[2]);
62 let lower = Color::Rgb(lo[0], lo[1], lo[2]);
63 pixels.push((upper, lower));
64 }
65 }
66
67 Self {
68 width,
69 height,
70 pixels,
71 }
72 }
73}
74
75impl HalfBlockImage {
76 pub fn from_rgb(rgb_data: &[u8], width: u32, height: u32) -> Self {
83 let Some(pixel_height) = height.checked_mul(2) else {
84 return Self::empty(width, height);
85 };
86 let pixels_total = u64::from(width).saturating_mul(u64::from(pixel_height));
87 if pixels_total == 0 || pixels_total > crate::buffer::MAX_IMAGE_PIXELS {
88 return Self::empty(width, height);
89 }
90 let Some(stride) = (width as usize).checked_mul(3) else {
91 return Self::empty(width, height);
92 };
93 let mut pixels = Vec::with_capacity((width as usize) * (height as usize));
94
95 for row in 0..height {
96 for col in 0..width {
97 let upper_y = (row * 2) as usize;
98 let lower_y = (row * 2 + 1) as usize;
99 let x = (col * 3) as usize;
100
101 let (ur, ug, ub) = if upper_y < pixel_height as usize {
102 let offset = upper_y * stride + x;
103 if offset + 2 < rgb_data.len() {
104 (rgb_data[offset], rgb_data[offset + 1], rgb_data[offset + 2])
105 } else {
106 (0, 0, 0)
107 }
108 } else {
109 (0, 0, 0)
110 };
111
112 let (lr, lg, lb) = if lower_y < pixel_height as usize {
113 let offset = lower_y * stride + x;
114 if offset + 2 < rgb_data.len() {
115 (rgb_data[offset], rgb_data[offset + 1], rgb_data[offset + 2])
116 } else {
117 (0, 0, 0)
118 }
119 } else {
120 (0, 0, 0)
121 };
122
123 pixels.push((Color::Rgb(ur, ug, ub), Color::Rgb(lr, lg, lb)));
124 }
125 }
126
127 Self {
128 width,
129 height,
130 pixels,
131 }
132 }
133
134 fn empty(width: u32, height: u32) -> Self {
135 Self {
136 width,
137 height,
138 pixels: Vec::new(),
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn from_rgb_rejects_oversized_dimensions() {
149 let img = HalfBlockImage::from_rgb(&[], 10_000, 10_000);
152 assert!(img.pixels.is_empty());
153 }
154
155 #[test]
156 fn from_rgb_rejects_overflowing_height() {
157 let img = HalfBlockImage::from_rgb(&[], 1, u32::MAX);
158 assert!(img.pixels.is_empty());
159 }
160}