1#![forbid(unsafe_code)]
16
17#[cfg(feature = "image")]
18use image_rs::DynamicImage;
19use std::cmp;
20
21#[cfg(feature = "glyph")]
22pub mod glyph;
23
24#[repr(C)]
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct Color {
31 pub r: f64,
32 pub g: f64,
33 pub b: f64,
34 pub a: f64,
35}
36
37impl Color {
38 pub const TRANSPARENT: Self = Self {
39 r: 0.0,
40 g: 0.0,
41 b: 0.0,
42 a: 0.0,
43 };
44 pub const BLACK: Self = Self {
45 r: 0.0,
46 g: 0.0,
47 b: 0.0,
48 a: 1.0,
49 };
50 pub const WHITE: Self = Self {
51 r: 1.0,
52 g: 1.0,
53 b: 1.0,
54 a: 1.0,
55 };
56}
57
58#[cfg(feature = "wgpu")]
59impl From<Color> for wgpu::Color {
60 fn from(c: Color) -> Self {
61 wgpu::Color {
62 r: c.r,
63 g: c.g,
64 b: c.b,
65 a: c.a,
66 }
67 }
68}
69
70#[cfg(feature = "wgpu")]
71impl From<wgpu::Color> for Color {
72 fn from(c: wgpu::Color) -> Self {
73 Color {
74 r: c.r,
75 g: c.g,
76 b: c.b,
77 a: c.a,
78 }
79 }
80}
81
82pub const MAX_GRAPHIC_DIMENSIONS: [usize; 2] = [4096, 4096];
83
84#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
85pub struct Graphic {
86 pub id: GraphicId,
87 pub offset_x: u16,
88 pub offset_y: u16,
89}
90
91#[inline]
96pub fn kitty_image_key(image_id: u32) -> u64 {
97 image_id as u64
98}
99
100#[inline]
105pub fn atlas_image_key(graphic_id: u64) -> u64 {
106 (1u64 << 32) + graphic_id
107}
108
109#[derive(Debug, Clone)]
112pub struct GraphicOverlay {
113 pub image_id: u64,
115 pub x: f32,
117 pub y: f32,
118 pub width: f32,
120 pub height: f32,
121 pub z_index: i32,
123 pub source_rect: [f32; 4],
128}
129
130impl GraphicOverlay {
131 pub const FULL_SOURCE_RECT: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
133}
134
135#[derive(Eq, PartialEq, Clone, Debug, Copy, Hash, PartialOrd, Ord)]
139pub struct GraphicId(pub u64);
140
141impl GraphicId {
142 #[inline]
144 pub const fn new(value: u64) -> Self {
145 Self(value)
146 }
147
148 #[inline]
150 pub const fn get(self) -> u64 {
151 self.0
152 }
153}
154
155#[derive(Eq, PartialEq, Clone, Debug, Copy)]
157pub enum ColorType {
158 Rgb,
160
161 Rgba,
163}
164
165#[derive(Eq, PartialEq, Clone, Debug)]
167pub struct GraphicData {
168 pub id: GraphicId,
170
171 pub width: usize,
173
174 pub height: usize,
176
177 pub color_type: ColorType,
179
180 pub pixels: Vec<u8>,
182
183 pub is_opaque: bool,
185
186 pub resize: Option<ResizeCommand>,
188
189 pub display_width: Option<usize>,
192
193 pub display_height: Option<usize>,
196
197 pub transmit_time: std::time::Instant,
200}
201
202impl GraphicData {
203 #[inline]
206 pub fn maybe_transparent(&self) -> bool {
207 !self.is_opaque && self.color_type == ColorType::Rgba
208 }
209
210 pub fn is_filled(&self, x: usize, y: usize, width: usize, height: usize) -> bool {
215 if x + width >= self.width || y + height >= self.height {
218 return false;
219 }
220
221 if !self.maybe_transparent() {
224 return true;
225 }
226
227 debug_assert!(self.color_type == ColorType::Rgba);
228
229 for offset_y in y..y + height {
230 let offset = offset_y * self.width * 4;
231 let row = &self.pixels[offset..offset + width * 4];
232
233 if row.chunks_exact(4).any(|pixel| pixel.last() != Some(&255)) {
234 return false;
235 }
236 }
237
238 true
239 }
240
241 #[cfg(feature = "image")]
242 pub fn from_dynamic_image(id: GraphicId, image: DynamicImage) -> Self {
243 let color_type;
244 let width;
245 let height;
246 let pixels;
247
248 match image {
249 DynamicImage::ImageRgba8(image) => {
257 color_type = ColorType::Rgba;
258 width = image.width() as usize;
259 height = image.height() as usize;
260 pixels = image.into_raw();
261 }
262
263 _ => {
264 let image = image.into_rgba8();
266 color_type = ColorType::Rgba;
267 width = image.width() as usize;
268 height = image.height() as usize;
269 pixels = image.into_raw();
270 }
271 }
272
273 GraphicData {
274 id,
275 width,
276 height,
277 color_type,
278 pixels,
279 is_opaque: false,
280 resize: None,
281 display_width: None,
282 display_height: None,
283 transmit_time: std::time::Instant::now(),
284 }
285 }
286
287 pub fn compute_display_dimensions(
291 &self,
292 cell_width: usize,
293 cell_height: usize,
294 view_width: usize,
295 view_height: usize,
296 ) -> (usize, usize) {
297 let resize = match self.resize {
298 Some(resize) => resize,
299 None => return (self.width, self.height),
300 };
301
302 if (resize.width == ResizeParameter::Auto
303 && resize.height == ResizeParameter::Auto)
304 || self.height == 0
305 || self.width == 0
306 {
307 return (self.width, self.height);
308 }
309
310 let mut width = match resize.width {
311 ResizeParameter::Auto => 1,
312 ResizeParameter::Pixels(n) => n as usize,
313 ResizeParameter::Cells(n) => n as usize * cell_width,
314 ResizeParameter::WindowPercent(n) => n as usize * view_width / 100,
315 };
316
317 let mut height = match resize.height {
318 ResizeParameter::Auto => 1,
319 ResizeParameter::Pixels(n) => n as usize,
320 ResizeParameter::Cells(n) => n as usize * cell_height,
321 ResizeParameter::WindowPercent(n) => n as usize * view_height / 100,
322 };
323
324 if width == 0 || height == 0 {
325 return (self.width, self.height);
326 }
327
328 if resize.width == ResizeParameter::Auto {
329 width =
330 (self.width as f64 * height as f64 / self.height as f64).round() as usize;
331 }
332
333 if resize.height == ResizeParameter::Auto {
334 height =
335 (self.height as f64 * width as f64 / self.width as f64).round() as usize;
336 }
337
338 width = cmp::min(width, MAX_GRAPHIC_DIMENSIONS[0]);
339 height = cmp::min(height, MAX_GRAPHIC_DIMENSIONS[1]);
340
341 if resize.preserve_aspect_ratio {
342 let scale_w = width as f64 / self.width as f64;
344 let scale_h = height as f64 / self.height as f64;
345 let scale = scale_w.min(scale_h);
346 width = (self.width as f64 * scale).round() as usize;
347 height = (self.height as f64 * scale).round() as usize;
348 }
349
350 (width, height)
351 }
352
353 #[cfg(feature = "image")]
355 pub fn resized(
356 self,
357 cell_width: usize,
358 cell_height: usize,
359 view_width: usize,
360 view_height: usize,
361 ) -> Option<Self> {
362 let resize = match self.resize {
363 Some(resize) => resize,
364 None => return Some(self),
365 };
366
367 if (resize.width == ResizeParameter::Auto
368 && resize.height == ResizeParameter::Auto)
369 || self.height == 0
370 || self.width == 0
371 {
372 return Some(self);
373 }
374
375 let mut width = match resize.width {
376 ResizeParameter::Auto => 1,
377 ResizeParameter::Pixels(n) => n as usize,
378 ResizeParameter::Cells(n) => n as usize * cell_width,
379 ResizeParameter::WindowPercent(n) => n as usize * view_width / 100,
380 };
381
382 let mut height = match resize.height {
383 ResizeParameter::Auto => 1,
384 ResizeParameter::Pixels(n) => n as usize,
385 ResizeParameter::Cells(n) => n as usize * cell_height,
386 ResizeParameter::WindowPercent(n) => n as usize * view_height / 100,
387 };
388
389 if width == 0 || height == 0 {
390 return None;
391 }
392
393 if resize.width == ResizeParameter::Auto {
395 width = self.width * height / self.height;
396 }
397
398 if resize.height == ResizeParameter::Auto {
399 height = self.height * width / self.width;
400 }
401
402 width = cmp::min(width, MAX_GRAPHIC_DIMENSIONS[0]);
404 height = cmp::min(height, MAX_GRAPHIC_DIMENSIONS[1]);
405
406 tracing::trace!("Resize new graphic to width={}, height={}", width, height,);
407
408 let dynimage = match self.color_type {
410 ColorType::Rgb => {
411 let buffer = image_rs::RgbImage::from_raw(
412 self.width as u32,
413 self.height as u32,
414 self.pixels,
415 )?;
416 DynamicImage::ImageRgb8(buffer)
417 }
418
419 ColorType::Rgba => {
420 let buffer = image_rs::RgbaImage::from_raw(
421 self.width as u32,
422 self.height as u32,
423 self.pixels,
424 )?;
425 DynamicImage::ImageRgba8(buffer)
426 }
427 };
428
429 let width = width as u32;
431 let height = height as u32;
432 let filter = image_rs::imageops::FilterType::Triangle;
434
435 let new_image = if resize.preserve_aspect_ratio {
436 dynimage.resize(width, height, filter)
437 } else {
438 dynimage.resize_exact(width, height, filter)
439 };
440
441 Some(Self::from_dynamic_image(self.id, new_image))
442 }
443}
444
445#[derive(Eq, PartialEq, Clone, Copy, Debug)]
447pub enum ResizeParameter {
448 Auto,
450
451 Cells(u32),
453
454 Pixels(u32),
456
457 WindowPercent(u32),
459}
460
461#[derive(Eq, PartialEq, Clone, Copy, Debug)]
463pub struct ResizeCommand {
464 pub width: ResizeParameter,
465
466 pub height: ResizeParameter,
467
468 pub preserve_aspect_ratio: bool,
469}
470
471#[test]
472fn check_opaque_region() {
473 let graphic = GraphicData {
474 id: GraphicId::new(1),
475 width: 10,
476 height: 10,
477 color_type: ColorType::Rgb,
478 pixels: vec![255; 10 * 10 * 3],
479 is_opaque: true,
480 resize: None,
481 display_width: None,
482 display_height: None,
483 transmit_time: std::time::Instant::now(),
484 };
485
486 assert!(graphic.is_filled(1, 1, 3, 3));
487 assert!(!graphic.is_filled(8, 8, 10, 10));
488
489 let pixels = {
490 let mut data = vec![255; 10 * 10 * 4];
492 for y in 3..6 {
493 let offset = y * 10 * 4;
494 data[offset..offset + 3 * 4].fill(0);
495 }
496 data
497 };
498
499 let graphic = GraphicData {
500 id: GraphicId::new(1),
501 pixels,
502 width: 10,
503 height: 10,
504 color_type: ColorType::Rgba,
505 is_opaque: false,
506 resize: None,
507 display_width: None,
508 display_height: None,
509 transmit_time: std::time::Instant::now(),
510 };
511
512 assert!(graphic.is_filled(0, 0, 3, 3));
513 assert!(!graphic.is_filled(1, 1, 4, 4));
514}