From ad652f3fcdae9e2e76244f4e7ab8ef67dbef8817 Mon Sep 17 00:00:00 2001 From: itsjunetime Date: Thu, 16 May 2024 18:35:37 -0600 Subject: [PATCH] Store images as Vec png data before rendering to keep memory usage lower --- src/main.rs | 3 +++ src/renderer.rs | 42 ++++++++++++++---------------------------- src/tui.rs | 46 +++++++++++++++++++--------------------------- 3 files changed, 36 insertions(+), 55 deletions(-) diff --git a/src/main.rs b/src/main.rs index b907f3e..9ea5964 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,6 +97,9 @@ async fn main() -> Result<(), Box> { Some(renderer_msg) = tui_rx.recv() => { match renderer_msg { Ok(RenderInfo::NumPages(num)) => tui.set_n_pages(num), + // TODO maybe somehow add more incremental creation of ImageData where it + // renders them all to `Box` only when you approach the page but + // still renders them to Vec no matter what? Ok(RenderInfo::Page(img, page_num)) => tui.page_ready(img, page_num), Err(e) => tui.show_error(e) } diff --git a/src/renderer.rs b/src/renderer.rs index 0db71e8..68b162b 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,7 +1,5 @@ use cairo::{Antialias, Format}; -use image::{DynamicImage, ImageFormat}; use itertools::Itertools; -use oxipng::Options; use poppler::{Document, Page}; use ratatui::layout::Rect; use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender}; @@ -22,7 +20,13 @@ pub enum RenderError { pub enum RenderInfo { NumPages(usize), - Page(DynamicImage, usize) + Page(ImageData, usize) +} + +pub struct ImageData { + pub data: Vec, + pub width: u16, + pub _height: u16 } // this function has to be sync (non-async) because the poppler::Document needs to be held during @@ -129,24 +133,7 @@ pub fn start_rendering( // render the page let to_send = render_single_page(page, area) - .and_then(|img_data| match image::load_from_memory_with_format(&img_data, ImageFormat::Png) { - Ok(img) => { - // TODO find some way to do oxipng stuff maybe. Perchance throw them - // all onto a new thread or whatever. idk. - /*let sender_clone = sender.clone(); - std::thread::spawn(move || { - let optimized = oxipng::optimize_from_memory( - &img_data, - &Options::default() - ).unwrap(); - let img = image::load_from_memory_with_format(&optimized, ImageFormat::Png).unwrap(); - sender_clone.blocking_send(Ok(RenderInfo::Page(img, num))).unwrap(); - });*/ - println!("data is {} while img is {}", img_data.len(), img.as_rgb8().unwrap().as_raw().len()); - Ok(img) - }, - Err(e) => Err(format!("Couldn't create DynamicImage: {e}")) - }).map(|img| RenderInfo::Page(img, num)) + .map(|data| RenderInfo::Page(data, num)) .map_err(RenderError::Render); // then send it over @@ -171,8 +158,7 @@ pub fn start_rendering( fn render_single_page( page: Page, area: Rect, -//) -> Result { -) -> Result, String> { +) -> Result { // First, get the font size; the number of pixels (width x height) per font character (I // think; it's at least something like that) on this terminal screen. let size = crossterm::terminal::window_size() @@ -238,9 +224,9 @@ fn render_single_page( ctx.target().write_to_png(&mut img_data) .map_err(|e| format!("Couldn't write surface to png: {e}"))?; - /*let img = image::load_from_memory_with_format(&img_data, ImageFormat::Png) - .map_err(|e| format!("Couldn't load image from provided data: {e}"))?; - - Ok(img)*/ - Ok(img_data) + Ok(ImageData { + data: img_data, + width: surface_width as u16 / col_w, + _height: surface_height as u16 / col_h + }) } diff --git a/src/tui.rs b/src/tui.rs index 35500f3..bc86a62 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,11 +1,11 @@ use std::rc::Rc; use crossterm::event::{Event, KeyCode, MouseEventKind}; -use image::DynamicImage; +use image::ImageFormat; use ratatui::{layout::{Constraint, Flex, Layout, Rect}, style::{Color, Style}, text::Span, widgets::{Block, Borders, Padding}, Frame}; use ratatui_image::{picker::Picker, protocol::Protocol, Image, Resize}; -use crate::{renderer::RenderError, skip::Skip}; +use crate::{renderer::{ImageData, RenderError}, skip::Skip}; pub struct Tui { name: String, @@ -32,8 +32,8 @@ struct LastRender { } enum RenderedImage { - Image(DynamicImage), - Text(Box) + Data(ImageData), + Image(Box) } enum InputCommand { @@ -173,8 +173,8 @@ impl Tui { page.as_ref().map(|img| ( idx, match img { - RenderedImage::Image(img) => (img.width() / self.picker.font_size.0 as u32) as u16, - RenderedImage::Text(img) => img.rect().width, + RenderedImage::Data(data) => data.width, + RenderedImage::Image(img) => img.rect().width, } )) ) @@ -227,21 +227,16 @@ impl Tui { fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) { match self.rendered[page_idx] { Some(ref page_img) => { - let dyn_img = match page_img { - RenderedImage::Image(_) => { - // Couldn't think of a better way to do this. We need to `take` the - // image out so we can transform it into a RenderedImage::Text, but we - // don't want to `take` it out when it's already a `Text` or `None`... - // idk, maybe i'll think of something better later. - let Some(RenderedImage::Image(img)) = self.rendered[page_idx].take() else { - unreachable!(); - }; + let txt_img = match page_img { + RenderedImage::Data(img_data) => { + let dyn_img = image::load_from_memory_with_format(&img_data.data, ImageFormat::Png) + .expect("Cairo produced invalid png data; that really shouldn't happen"); // We don't actually want to Crop this image, but we've already // verified (with the ImageSurface stuff) that the image is the correct // size for the area given, so to save ratatui the work of having to // resize it, we tell them to crop it to fit. - let dyn_img = match self.picker.new_protocol(img, img_area, Resize::Crop) { + let txt_img = match self.picker.new_protocol(dyn_img, img_area, Resize::Crop) { Ok(img) => img, Err(e) => { self.error = Some(format!("Couldn't convert DynamicImage to ratatui image: {e}")); @@ -249,17 +244,17 @@ impl Tui { } }; - self.rendered[page_idx] = Some(RenderedImage::Text(dyn_img)); - let Some(RenderedImage::Text(ref txt)) = self.rendered[page_idx] else { + self.rendered[page_idx] = Some(RenderedImage::Image(txt_img)); + let Some(RenderedImage::Image(ref txt)) = self.rendered[page_idx] else { unreachable!(); }; txt } - RenderedImage::Text(ref img) => img, + RenderedImage::Image(ref img) => img, }; - frame.render_widget(Image::new(&**dyn_img), img_area); + frame.render_widget(Image::new(&**txt_img), img_area); }, None => Self::render_loading_in(frame, img_area) }; @@ -300,14 +295,13 @@ impl Tui { // mark that we need to re-render the images } - pub fn page_ready(&mut self, img: DynamicImage, page_num: usize) { + pub fn page_ready(&mut self, img: ImageData, page_num: usize) { // If this new image woulda fit within the available space on the last render AND it's // within the range where it might've been rendered with the last shown pages, then reset // the last rect marker so that all images are forced to redraw on next render and this one // is drawn with them - let img_w = (img.width() / self.picker.font_size.0 as u32) as u16; - if img_w <= self.last_render.unused_width { - let num_fit = self.last_render.unused_width / img_w; + if img.width <= self.last_render.unused_width { + let num_fit = self.last_render.unused_width / img.width; if page_num >= self.page && (self.page + num_fit as usize) >= page_num { self.last_render.rect = Rect::default(); } @@ -316,9 +310,7 @@ impl Tui { // We always just set this here because we handle reloading in the `set_n_pages` function. // If the document was reloaded, then It'll have the `set_n_pages` called to set the new // number of pages, so the vec will already be cleared - self.rendered[page_num] = Some(RenderedImage::Image(img)); - - if page_num > 10 { panic!() } + self.rendered[page_num] = Some(RenderedImage::Data(img)); } pub fn handle_event(&mut self, ev: Event) -> Option {