broot/kitty/
image_renderer.rs

1use {
2    super::{
3        detect_support::{
4            detect_kitty_graphics_protocol_display,
5            get_tmux_nest_count,
6            is_ssh,
7        },
8        terminal_esc::{
9            get_esc_seq,
10            get_tmux_header,
11            get_tmux_tail,
12        },
13    },
14    crate::{
15        display::{
16            W,
17            cell_size_in_pixels,
18        },
19        errors::ProgramError,
20    },
21    base64::{
22        self,
23        Engine,
24        engine::general_purpose::STANDARD as BASE64,
25    },
26    cli_log::*,
27    crokey::crossterm::{
28        QueueableCommand,
29        cursor,
30        style::Color,
31    },
32    flate2::{
33        Compression,
34        write::ZlibEncoder,
35    },
36    crate::image::zune_compat::{
37        DynamicImage,
38        RgbImage,
39        RgbaImage,
40    },
41    lru::LruCache,
42    rustc_hash::FxBuildHasher,
43    serde::Deserialize,
44    std::{
45        fs::File,
46        io::{
47            self,
48            Read,
49            Write,
50        },
51        num::NonZeroUsize,
52        path::{
53            Path,
54            PathBuf,
55        },
56    },
57    tempfile,
58    termimad::{
59        Area,
60        fill_bg,
61    },
62};
63
64/// How to send the image to kitty
65///
66/// Note that I didn't test yet the named shared memory
67/// solution offered by kitty.
68///
69/// Documentation:
70///  https://sw.kovidgoyal.net/kitty/graphics-protocol/#the-transmission-medium
71#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum TransmissionMedium {
74    /// write a temp file, then give its path to kitty
75    /// in the payload of the escape sequence. It's quite
76    /// fast on SSD but a big downside is that it doesn't
77    /// work if you're distant
78    #[default]
79    TempFile,
80    /// send the whole rgb or rgba data, encoded in base64,
81    /// in the payloads of several escape sequence (each one
82    /// containing at most 4096 bytes). Works if broot runs
83    /// on remote.
84    Chunks,
85}
86
87/// How to display the image
88#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum KittyGraphicsDisplay {
91    /// Not supported
92    None,
93    /// detect support automatically
94    #[default]
95    Detect,
96    /// display directly
97    Direct,
98    /// use Unicode placeholders
99    Unicode,
100}
101
102#[derive(Debug, Clone)]
103pub struct KittyImageRendererOptions {
104    pub display: KittyGraphicsDisplay,
105    pub transmission_medium: TransmissionMedium,
106    pub kept_temp_files: NonZeroUsize,
107    pub is_tmux: bool,
108}
109
110enum ImageData {
111    Rgb(RgbImage),
112    Rgba(RgbaImage),
113}
114impl From<&DynamicImage> for ImageData {
115    fn from(img: &DynamicImage) -> Self {
116        if let Some(rgba) = img.as_rgba8() {
117            debug!("using rgba");
118            Self::Rgba(rgba)
119        } else if let Some(rgb) = img.as_rgb8() {
120            debug!("using rgb");
121            Self::Rgb(rgb)
122        } else {
123            debug!("converting to rgb8");
124            Self::Rgb(img.to_rgb8())
125        }
126    }
127}
128impl ImageData {
129    fn kitty_format(&self) -> &'static str {
130        match self {
131            Self::Rgba(_) => "32",
132            Self::Rgb(_) => "24",
133        }
134    }
135    fn bytes(&self) -> Vec<u8> {
136        match self {
137            Self::Rgb(img) => img.as_raw(),
138            Self::Rgba(img) => img.as_raw(),
139        }
140    }
141}
142
143/// The max size of a data payload in a kitty escape sequence
144/// according to kitty's documentation
145const CHUNK_SIZE: usize = 4096;
146
147/// Unicode placeholder character
148const PLACHOLDER: &str = "\u{10EEEE}";
149/// Unicode placeholder diacritic characters
150#[rustfmt::skip]
151const DIACRITICS: &[&str] = &[
152    "\u{0305}", "\u{030D}", "\u{030E}", "\u{0310}", "\u{0312}", "\u{033D}", "\u{033E}", "\u{033F}",
153    "\u{0346}", "\u{034A}", "\u{034B}", "\u{034C}", "\u{0350}", "\u{0351}", "\u{0352}", "\u{0357}",
154    "\u{035B}", "\u{0363}", "\u{0364}", "\u{0365}", "\u{0366}", "\u{0367}", "\u{0368}", "\u{0369}",
155    "\u{036A}", "\u{036B}", "\u{036C}", "\u{036D}", "\u{036E}", "\u{036F}", "\u{0483}", "\u{0484}",
156    "\u{0485}", "\u{0486}", "\u{0487}", "\u{0592}", "\u{0593}", "\u{0594}", "\u{0595}", "\u{0597}",
157    "\u{0598}", "\u{0599}", "\u{059C}", "\u{059D}", "\u{059E}", "\u{059F}", "\u{05A0}", "\u{05A1}",
158    "\u{05A8}", "\u{05A9}", "\u{05AB}", "\u{05AC}", "\u{05AF}", "\u{05C4}", "\u{0610}", "\u{0611}",
159    "\u{0612}", "\u{0613}", "\u{0614}", "\u{0615}", "\u{0616}", "\u{0617}", "\u{0657}", "\u{0658}",
160    "\u{0659}", "\u{065A}", "\u{065B}", "\u{065D}", "\u{065E}", "\u{06D6}", "\u{06D7}", "\u{06D8}",
161    "\u{06D9}", "\u{06DA}", "\u{06DB}", "\u{06DC}", "\u{06DF}", "\u{06E0}", "\u{06E1}", "\u{06E2}",
162    "\u{06E4}", "\u{06E7}", "\u{06E8}", "\u{06EB}", "\u{06EC}", "\u{0730}", "\u{0732}", "\u{0733}",
163    "\u{0735}", "\u{0736}", "\u{073A}", "\u{073D}", "\u{073F}", "\u{0740}", "\u{0741}", "\u{0743}",
164    "\u{0745}", "\u{0747}", "\u{0749}", "\u{074A}", "\u{07EB}", "\u{07EC}", "\u{07ED}", "\u{07EE}",
165    "\u{07EF}", "\u{07F0}", "\u{07F1}", "\u{07F3}", "\u{0816}", "\u{0817}", "\u{0818}", "\u{0819}",
166    "\u{081B}", "\u{081C}", "\u{081D}", "\u{081E}", "\u{081F}", "\u{0820}", "\u{0821}", "\u{0822}",
167    "\u{0823}", "\u{0825}", "\u{0826}", "\u{0827}", "\u{0829}", "\u{082A}", "\u{082B}", "\u{082C}",
168    "\u{082D}", "\u{0951}", "\u{0953}", "\u{0954}", "\u{0F82}", "\u{0F83}", "\u{0F86}", "\u{0F87}",
169    "\u{135D}", "\u{135E}", "\u{135F}", "\u{17DD}", "\u{193A}", "\u{1A17}", "\u{1A75}", "\u{1A76}",
170    "\u{1A77}", "\u{1A78}", "\u{1A79}", "\u{1A7A}", "\u{1A7B}", "\u{1A7C}", "\u{1B6B}", "\u{1B6D}",
171    "\u{1B6E}", "\u{1B6F}", "\u{1B70}", "\u{1B71}", "\u{1B72}", "\u{1B73}", "\u{1CD0}", "\u{1CD1}",
172    "\u{1CD2}", "\u{1CDA}", "\u{1CDB}", "\u{1CE0}", "\u{1DC0}", "\u{1DC1}", "\u{1DC3}", "\u{1DC4}",
173    "\u{1DC5}", "\u{1DC6}", "\u{1DC7}", "\u{1DC8}", "\u{1DC9}", "\u{1DCB}", "\u{1DCC}", "\u{1DD1}",
174    "\u{1DD2}", "\u{1DD3}", "\u{1DD4}", "\u{1DD5}", "\u{1DD6}", "\u{1DD7}", "\u{1DD8}", "\u{1DD9}",
175    "\u{1DDA}", "\u{1DDB}", "\u{1DDC}", "\u{1DDD}", "\u{1DDE}", "\u{1DDF}", "\u{1DE0}", "\u{1DE1}",
176    "\u{1DE2}", "\u{1DE3}", "\u{1DE4}", "\u{1DE5}", "\u{1DE6}", "\u{1DFE}", "\u{20D0}", "\u{20D1}",
177    "\u{20D4}", "\u{20D5}", "\u{20D6}", "\u{20D7}", "\u{20DB}", "\u{20DC}", "\u{20E1}", "\u{20E7}",
178    "\u{20E9}", "\u{20F0}", "\u{2CEF}", "\u{2CF0}", "\u{2CF1}", "\u{2DE0}", "\u{2DE1}", "\u{2DE2}",
179    "\u{2DE3}", "\u{2DE4}", "\u{2DE5}", "\u{2DE6}", "\u{2DE7}", "\u{2DE8}", "\u{2DE9}", "\u{2DEA}",
180    "\u{2DEB}", "\u{2DEC}", "\u{2DED}", "\u{2DEE}", "\u{2DEF}", "\u{2DF0}", "\u{2DF1}", "\u{2DF2}",
181    "\u{2DF3}", "\u{2DF4}", "\u{2DF5}", "\u{2DF6}", "\u{2DF7}", "\u{2DF8}", "\u{2DF9}", "\u{2DFA}",
182    "\u{2DFB}", "\u{2DFC}", "\u{2DFD}", "\u{2DFE}", "\u{2DFF}", "\u{A66F}", "\u{A67C}", "\u{A67D}",
183    "\u{A6F0}", "\u{A6F1}", "\u{A8E0}", "\u{A8E1}", "\u{A8E2}", "\u{A8E3}", "\u{A8E4}", "\u{A8E5}",
184    "\u{A8E6}", "\u{A8E7}", "\u{A8E8}", "\u{A8E9}", "\u{A8EA}", "\u{A8EB}", "\u{A8EC}", "\u{A8ED}",
185    "\u{A8EE}", "\u{A8EF}", "\u{A8F0}", "\u{A8F1}", "\u{AAB0}", "\u{AAB2}", "\u{AAB3}", "\u{AAB7}",
186    "\u{AAB8}", "\u{AABE}", "\u{AABF}", "\u{AAC1}", "\u{FE20}", "\u{FE21}", "\u{FE22}", "\u{FE23}",
187    "\u{FE24}", "\u{FE25}", "\u{FE26}", "\u{10A0F}", "\u{10A38}", "\u{1D185}", "\u{1D186}",
188    "\u{1D187}", "\u{1D188}", "\u{1D189}", "\u{1D1AA}", "\u{1D1AB}", "\u{1D1AC}", "\u{1D1AD}",
189    "\u{1D242}", "\u{1D243}", "\u{1D244}"
190];
191
192fn div_ceil(
193    a: u32,
194    b: u32,
195) -> u32 {
196    a / b + (0 != a % b) as u32
197}
198
199/// The image renderer, with knowledge of the console cells
200/// dimensions, and built only on a compatible terminal
201#[derive(Debug)]
202pub struct KittyImageRenderer {
203    cell_width: u32,
204    cell_height: u32,
205    next_id: usize,
206    options: KittyImageRendererOptions,
207    /// paths of temp files which have been written, with key
208    /// being the input image path
209    temp_files: LruCache<String, PathBuf, FxBuildHasher>,
210}
211
212enum KittyImageData {
213    Png { path: PathBuf },
214    Image { data: ImageData },
215}
216
217/// An image prepared for a precise area on screen
218struct KittyImage {
219    id: usize,
220    data: KittyImageData,
221    img_width: u32,
222    img_height: u32,
223    area: Area,
224    display: KittyGraphicsDisplay,
225    is_tmux: bool,
226    tmux_nest_count: u32,
227}
228impl KittyImage {
229    fn new(
230        src: &DynamicImage,
231        png_path: Option<PathBuf>,
232        available_area: &Area,
233        renderer: &mut KittyImageRenderer,
234    ) -> Self {
235        let (img_width, img_height) = src.dimensions();
236        let area = renderer.rendering_area(img_width, img_height, available_area);
237        let data = if let Some(path) = png_path {
238            KittyImageData::Png { path }
239        } else {
240            KittyImageData::Image { data: src.into() }
241        };
242        let id = renderer.new_id();
243        let display = renderer.options.display;
244        let is_tmux = renderer.options.is_tmux;
245        let tmux_nest_count = if is_tmux { get_tmux_nest_count() } else { 0 };
246        Self {
247            id,
248            data,
249            img_width,
250            img_height,
251            area,
252            display,
253            is_tmux,
254            tmux_nest_count,
255        }
256    }
257    fn print_placeholder_grid(
258        &self,
259        w: &mut W,
260    ) -> Result<(), ProgramError> {
261        let id_str = if self.id < 256 {
262            format!("\u{1b}[38;5;{}m", self.id)
263        } else {
264            format!(
265                "\u{1b}[38;2;{};{};{}m",
266                (self.id >> 16) & 0xff,
267                (self.id >> 8) & 0xff,
268                self.id & 0xff
269            )
270        };
271        let id_msb_str = if self.id >= (1 << 24) {
272            DIACRITICS[self.id >> 24]
273        } else {
274            ""
275        };
276        for y in 0..(self.area.height).min(DIACRITICS.len() as u16) {
277            w.queue(cursor::MoveTo(self.area.left, self.area.top + y))?;
278            write!(w, "{}", &id_str)?;
279            if id_msb_str.is_empty() {
280                write!(w, "{}{}", PLACHOLDER, DIACRITICS[y as usize])?;
281            } else {
282                write!(
283                    w,
284                    "{}{}{}{}",
285                    PLACHOLDER, DIACRITICS[y as usize], DIACRITICS[0], id_msb_str
286                )?;
287            }
288            write!(w, "{}", PLACHOLDER.repeat(self.area.width as usize - 1),)?;
289            write!(w, "\u{1b}[39m")?;
290        }
291        Ok(())
292    }
293    fn compress(data: &[u8]) -> Result<Vec<u8>, ProgramError> {
294        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
295        encoder.write_all(data).expect("Zlib encoder error");
296        Ok(encoder.finish().expect("Zlib encoder error"))
297    }
298    /// Render the image by sending multiple kitty escape sequences, each
299    /// one with part of the image raw data (encoded as base64)
300    fn print_with_chunks(
301        &self,
302        w: &mut W,
303    ) -> Result<(), ProgramError> {
304        let esc = get_esc_seq(self.tmux_nest_count);
305        let tmux_header = self
306            .is_tmux
307            .then_some(get_tmux_header(self.tmux_nest_count));
308        let tmux_tail = self.is_tmux.then_some(get_tmux_tail(self.tmux_nest_count));
309        let display_tag = match self.display {
310            KittyGraphicsDisplay::Unicode => "q=2,U=1,",
311            _ => "",
312        };
313        let mut png_buf = Vec::new();
314        let (bytes, compression_tag, format) = match &self.data {
315            KittyImageData::Png { path } => {
316                // Compressing PNG files increases the size
317                File::open(path)?.read_to_end(&mut png_buf)?;
318                (png_buf, "", "100")
319            }
320            KittyImageData::Image { data } => (
321                KittyImage::compress(&data.bytes())?,
322                "o=z,",
323                data.kitty_format(),
324            ),
325        };
326        let encoded = BASE64.encode(bytes);
327        let mut pos = 0;
328        if self.display == KittyGraphicsDisplay::Direct {
329            w.queue(cursor::MoveTo(self.area.left, self.area.top))?;
330        }
331        if let Some(s) = &tmux_header {
332            write!(w, "{s}")?;
333        }
334        write!(
335            w,
336            "{}_Gq=2,a=t,f={},t=d,i={},s={},v={},{}",
337            &esc, format, self.id, self.img_width, self.img_height, compression_tag,
338        )?;
339        loop {
340            if pos != 0 {
341                if let Some(s) = &tmux_header {
342                    write!(w, "{s}")?;
343                }
344                write!(w, "{}_Gq=2,", &esc)?;
345            }
346            if pos + CHUNK_SIZE < encoded.len() {
347                write!(w, "m=1;{}{}\\", &encoded[pos..pos + CHUNK_SIZE], &esc)?;
348                pos += CHUNK_SIZE;
349                if let Some(s) = &tmux_tail {
350                    write!(w, "{s}")?;
351                }
352            } else {
353                // last chunk
354                write!(w, "m=0;{}{}\\", &encoded[pos..encoded.len()], &esc)?;
355                if let Some(s) = &tmux_tail {
356                    write!(w, "{s}")?;
357                }
358                // display image
359                if let Some(s) = &tmux_header {
360                    write!(w, "{s}")?;
361                }
362                write!(
363                    w,
364                    "{}_G{}a=p,i={},c={},r={}{}\\",
365                    &esc, display_tag, self.id, self.area.width, self.area.height, &esc,
366                )?;
367                if let Some(s) = &tmux_tail {
368                    write!(w, "{s}")?;
369                }
370                if self.display == KittyGraphicsDisplay::Unicode {
371                    self.print_placeholder_grid(w)?;
372                }
373                break;
374            }
375        }
376        Ok(())
377    }
378    /// Render the image by giving to kitty the path to a file in the
379    /// payload of a unique kitty escape sequence
380    fn print_with_path(
381        &self,
382        w: &mut W,
383        path: &Path,
384        format: &str,
385        transmission: &str,
386    ) -> Result<(), ProgramError> {
387        let esc = get_esc_seq(self.tmux_nest_count);
388        let tmux_header = self
389            .is_tmux
390            .then_some(get_tmux_header(self.tmux_nest_count));
391        let tmux_tail = self.is_tmux.then_some(get_tmux_tail(self.tmux_nest_count));
392        if self.display == KittyGraphicsDisplay::Direct {
393            w.queue(cursor::MoveTo(self.area.left, self.area.top))?;
394        }
395        let display_tag = match self.display {
396            KittyGraphicsDisplay::Unicode => "q=2,U=1,",
397            _ => "",
398        };
399        let path = path
400            .to_str()
401            .ok_or_else(|| io::Error::other("Path can't be converted to UTF8"))?;
402        let encoded_path = BASE64.encode(path);
403        if let KittyImageData::Image { data: _ } = self.data {
404            debug!("temp file written: {:?}", path);
405        }
406        if let Some(s) = &tmux_header {
407            write!(w, "{s}")?;
408        }
409        write!(
410            w,
411            "{}_G{}a=T,f={},t={},i={},s={},v={},c={},r={};{}{}\\",
412            &esc,
413            display_tag,
414            format,
415            transmission,
416            self.id,
417            self.img_width,
418            self.img_height,
419            self.area.width,
420            self.area.height,
421            encoded_path,
422            &esc,
423        )?;
424        if let Some(s) = &tmux_tail {
425            write!(w, "{s}")?;
426        }
427        if self.display == KittyGraphicsDisplay::Unicode {
428            self.print_placeholder_grid(w)?;
429        }
430        Ok(())
431    }
432    /// Render the image by giving to kitty the path to a PNG file in
433    /// the payload of a unique kitty escape sequence
434    pub fn print_with_png(
435        &self,
436        w: &mut W,
437    ) -> Result<(), ProgramError> {
438        // Compression slows things down
439        if let KittyImageData::Png { path } = &self.data {
440            self.print_with_path(w, path.as_path(), "100", "f")?;
441        };
442        Ok(())
443    }
444    /// Render the image by writing the raw data in a temporary file
445    /// then giving to kitty the path to this file in the payload of
446    /// a unique kitty escape sequence
447    pub fn print_with_temp_file(
448        &self,
449        w: &mut W,
450        temp_file: Option<File>, // if None, no need to write it
451        temp_file_path: &Path,
452    ) -> Result<(), ProgramError> {
453        // Compression slows things down
454        if let KittyImageData::Image { data } = &self.data {
455            if let Some(mut temp_file) = temp_file {
456                temp_file.write_all(&data.bytes())?;
457                temp_file.flush()?;
458                debug!("file len: {}", temp_file.metadata().unwrap().len());
459            }
460            self.print_with_path(w, temp_file_path, data.kitty_format(), "t")?;
461        };
462        Ok(())
463    }
464}
465
466impl KittyImageRenderer {
467    /// Called only once (at most) by the KittyManager
468    pub fn new(mut options: KittyImageRendererOptions) -> Option<Self> {
469        if options.display == KittyGraphicsDisplay::Detect {
470            options.display = detect_kitty_graphics_protocol_display();
471        }
472        if options.display == KittyGraphicsDisplay::None {
473            return None;
474        }
475        let hasher = FxBuildHasher;
476        let temp_files = LruCache::with_hasher(options.kept_temp_files, hasher);
477        let options = if is_ssh() {
478            KittyImageRendererOptions {
479                transmission_medium: TransmissionMedium::Chunks,
480                ..options
481            }
482        } else {
483            options
484        };
485        cell_size_in_pixels()
486            .ok()
487            .map(|(cell_width, cell_height)| Self {
488                cell_width,
489                cell_height,
490                next_id: 1,
491                options,
492                temp_files,
493            })
494    }
495    pub fn delete_temp_files(&mut self) {
496        for (_, temp_file_path) in self.temp_files.into_iter() {
497            debug!("removing temp file: {:?}", temp_file_path);
498            if let Err(e) = std::fs::remove_file(temp_file_path) {
499                error!("failed to remove temp file: {:?}", e);
500            }
501        }
502    }
503    /// return a new image id
504    fn new_id(&mut self) -> usize {
505        let new_id = self.next_id;
506        self.next_id += 1;
507        new_id
508    }
509    fn is_path_png(path: &Path) -> bool {
510        match path.extension() {
511            Some(ext) => ext == "png" || ext == "PNG",
512            None => false,
513        }
514    }
515    /// Clean the area, then print the dynamicImage and
516    /// return the KittyImageId for later removal from screen
517    pub fn print(
518        &mut self,
519        w: &mut W,
520        src: &DynamicImage,
521        src_path: &Path,
522        area: &Area,
523        bg: Color,
524    ) -> Result<usize, ProgramError> {
525        // clean the background below (and around) the image
526        for y in area.top..area.top + area.height {
527            w.queue(cursor::MoveTo(area.left, y))?;
528            fill_bg(w, area.width as usize, bg)?;
529        }
530
531        let png_path = KittyImageRenderer::is_path_png(src_path).then_some(src_path.to_path_buf());
532        let is_png = png_path.is_some();
533        let img = KittyImage::new(src, png_path, area, self);
534        debug!(
535            "transmission medium: {:?}",
536            self.options.transmission_medium
537        );
538        w.flush()?;
539        match self.options.transmission_medium {
540            TransmissionMedium::TempFile if is_png => {
541                img.print_with_png(w)?;
542            }
543            TransmissionMedium::TempFile => {
544                let temp_file_key = format!("{:?}-{}x{}", src_path, img.img_width, img.img_height,);
545                let mut old_path = None;
546                if let Some(cached_path) = self.temp_files.pop(&temp_file_key) {
547                    if cached_path.exists() {
548                        old_path = Some(cached_path);
549                    }
550                }
551                let temp_file_path = if let Some(temp_file_path) = old_path {
552                    // the temp file is still there
553                    img.print_with_temp_file(w, None, &temp_file_path)?;
554                    temp_file_path
555                } else {
556                    // either the temp file itself has been removed (unlikely), the temp
557                    // cache entry has been removed, or we just never viewed this image
558                    // with this size before
559                    let (temp_file, path) = tempfile::Builder::new()
560                        .prefix("broot-tty-graphics-protocol-")
561                        .tempfile()?
562                        .keep()
563                        .map_err(|_| io::Error::other("temp file can't be kept"))?;
564                    img.print_with_temp_file(w, Some(temp_file), &path)?;
565                    path
566                };
567                if let Some((_, old_path)) = self.temp_files.push(temp_file_key, temp_file_path) {
568                    debug!("removing temp file: {:?}", &old_path);
569                    if let Err(e) = std::fs::remove_file(&old_path) {
570                        error!("failed to remove temp file: {:?}", e);
571                    }
572                }
573            }
574            TransmissionMedium::Chunks => img.print_with_chunks(w)?,
575        }
576        Ok(img.id)
577    }
578    fn rendering_area(
579        &self,
580        img_width: u32,
581        img_height: u32,
582        area: &Area,
583    ) -> Area {
584        let area_cols: u32 = area.width.into();
585        let area_rows: u32 = area.height.into();
586        let rdim = self.rendering_dim(img_width, img_height, area_cols, area_rows);
587        Area::new(
588            area.left + ((area_cols - rdim.0) / 2) as u16,
589            area.top + ((area_rows - rdim.1) / 2) as u16,
590            rdim.0 as u16,
591            rdim.1 as u16,
592        )
593    }
594    fn rendering_dim(
595        &self,
596        img_width: u32,
597        img_height: u32,
598        area_cols: u32,
599        area_rows: u32,
600    ) -> (u32, u32) {
601        let optimal_cols = div_ceil(img_width, self.cell_width);
602        let optimal_rows = div_ceil(img_height, self.cell_height);
603        debug!("area: {:?}", (area_cols, area_rows));
604        debug!("optimal: {:?}", (optimal_cols, optimal_rows));
605        if optimal_cols <= area_cols && optimal_rows <= area_rows {
606            // no constraint (TODO center?)
607            (optimal_cols, optimal_rows)
608        } else if optimal_cols * area_rows > optimal_rows * area_cols {
609            // we're constrained in width
610            debug!("constrained in width");
611            (area_cols, optimal_rows * area_cols / optimal_cols)
612        } else {
613            // we're constrained in height
614            debug!("constrained in height");
615            (optimal_cols * area_rows / optimal_rows, area_rows)
616        }
617    }
618}