mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-01 23:51:46 -04:00
Store images as Vec<u8> png data before rendering to keep memory usage lower
This commit is contained in:
@@ -97,6 +97,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Some(renderer_msg) = tui_rx.recv() => {
|
Some(renderer_msg) = tui_rx.recv() => {
|
||||||
match renderer_msg {
|
match renderer_msg {
|
||||||
Ok(RenderInfo::NumPages(num)) => tui.set_n_pages(num),
|
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<dyn Protocol>` only when you approach the page but
|
||||||
|
// still renders them to Vec<u8> no matter what?
|
||||||
Ok(RenderInfo::Page(img, page_num)) => tui.page_ready(img, page_num),
|
Ok(RenderInfo::Page(img, page_num)) => tui.page_ready(img, page_num),
|
||||||
Err(e) => tui.show_error(e)
|
Err(e) => tui.show_error(e)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-28
@@ -1,7 +1,5 @@
|
|||||||
use cairo::{Antialias, Format};
|
use cairo::{Antialias, Format};
|
||||||
use image::{DynamicImage, ImageFormat};
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use oxipng::Options;
|
|
||||||
use poppler::{Document, Page};
|
use poppler::{Document, Page};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender};
|
use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender};
|
||||||
@@ -22,7 +20,13 @@ pub enum RenderError {
|
|||||||
|
|
||||||
pub enum RenderInfo {
|
pub enum RenderInfo {
|
||||||
NumPages(usize),
|
NumPages(usize),
|
||||||
Page(DynamicImage, usize)
|
Page(ImageData, usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImageData {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub width: u16,
|
||||||
|
pub _height: u16
|
||||||
}
|
}
|
||||||
|
|
||||||
// this function has to be sync (non-async) because the poppler::Document needs to be held during
|
// 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
|
// render the page
|
||||||
let to_send = render_single_page(page, area)
|
let to_send = render_single_page(page, area)
|
||||||
.and_then(|img_data| match image::load_from_memory_with_format(&img_data, ImageFormat::Png) {
|
.map(|data| RenderInfo::Page(data, num))
|
||||||
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_err(RenderError::Render);
|
.map_err(RenderError::Render);
|
||||||
|
|
||||||
// then send it over
|
// then send it over
|
||||||
@@ -171,8 +158,7 @@ pub fn start_rendering(
|
|||||||
fn render_single_page(
|
fn render_single_page(
|
||||||
page: Page,
|
page: Page,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
//) -> Result<DynamicImage, String> {
|
) -> Result<ImageData, String> {
|
||||||
) -> Result<Vec<u8>, String> {
|
|
||||||
// First, get the font size; the number of pixels (width x height) per font character (I
|
// 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.
|
// think; it's at least something like that) on this terminal screen.
|
||||||
let size = crossterm::terminal::window_size()
|
let size = crossterm::terminal::window_size()
|
||||||
@@ -238,9 +224,9 @@ fn render_single_page(
|
|||||||
ctx.target().write_to_png(&mut img_data)
|
ctx.target().write_to_png(&mut img_data)
|
||||||
.map_err(|e| format!("Couldn't write surface to png: {e}"))?;
|
.map_err(|e| format!("Couldn't write surface to png: {e}"))?;
|
||||||
|
|
||||||
/*let img = image::load_from_memory_with_format(&img_data, ImageFormat::Png)
|
Ok(ImageData {
|
||||||
.map_err(|e| format!("Couldn't load image from provided data: {e}"))?;
|
data: img_data,
|
||||||
|
width: surface_width as u16 / col_w,
|
||||||
Ok(img)*/
|
_height: surface_height as u16 / col_h
|
||||||
Ok(img_data)
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-27
@@ -1,11 +1,11 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crossterm::event::{Event, KeyCode, MouseEventKind};
|
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::{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 ratatui_image::{picker::Picker, protocol::Protocol, Image, Resize};
|
||||||
|
|
||||||
use crate::{renderer::RenderError, skip::Skip};
|
use crate::{renderer::{ImageData, RenderError}, skip::Skip};
|
||||||
|
|
||||||
pub struct Tui {
|
pub struct Tui {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -32,8 +32,8 @@ struct LastRender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RenderedImage {
|
enum RenderedImage {
|
||||||
Image(DynamicImage),
|
Data(ImageData),
|
||||||
Text(Box<dyn Protocol>)
|
Image(Box<dyn Protocol>)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InputCommand {
|
enum InputCommand {
|
||||||
@@ -173,8 +173,8 @@ impl Tui {
|
|||||||
page.as_ref().map(|img| (
|
page.as_ref().map(|img| (
|
||||||
idx,
|
idx,
|
||||||
match img {
|
match img {
|
||||||
RenderedImage::Image(img) => (img.width() / self.picker.font_size.0 as u32) as u16,
|
RenderedImage::Data(data) => data.width,
|
||||||
RenderedImage::Text(img) => img.rect().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) {
|
fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) {
|
||||||
match self.rendered[page_idx] {
|
match self.rendered[page_idx] {
|
||||||
Some(ref page_img) => {
|
Some(ref page_img) => {
|
||||||
let dyn_img = match page_img {
|
let txt_img = match page_img {
|
||||||
RenderedImage::Image(_) => {
|
RenderedImage::Data(img_data) => {
|
||||||
// Couldn't think of a better way to do this. We need to `take` the
|
let dyn_img = image::load_from_memory_with_format(&img_data.data, ImageFormat::Png)
|
||||||
// image out so we can transform it into a RenderedImage::Text, but we
|
.expect("Cairo produced invalid png data; that really shouldn't happen");
|
||||||
// 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!();
|
|
||||||
};
|
|
||||||
|
|
||||||
// We don't actually want to Crop this image, but we've already
|
// We don't actually want to Crop this image, but we've already
|
||||||
// verified (with the ImageSurface stuff) that the image is the correct
|
// 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
|
// size for the area given, so to save ratatui the work of having to
|
||||||
// resize it, we tell them to crop it to fit.
|
// 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,
|
Ok(img) => img,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.error = Some(format!("Couldn't convert DynamicImage to ratatui image: {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));
|
self.rendered[page_idx] = Some(RenderedImage::Image(txt_img));
|
||||||
let Some(RenderedImage::Text(ref txt)) = self.rendered[page_idx] else {
|
let Some(RenderedImage::Image(ref txt)) = self.rendered[page_idx] else {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
};
|
};
|
||||||
|
|
||||||
txt
|
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)
|
None => Self::render_loading_in(frame, img_area)
|
||||||
};
|
};
|
||||||
@@ -300,14 +295,13 @@ impl Tui {
|
|||||||
// mark that we need to re-render the images
|
// 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
|
// 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
|
// 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
|
// the last rect marker so that all images are forced to redraw on next render and this one
|
||||||
// is drawn with them
|
// is drawn with them
|
||||||
let img_w = (img.width() / self.picker.font_size.0 as u32) as u16;
|
if img.width <= self.last_render.unused_width {
|
||||||
if img_w <= self.last_render.unused_width {
|
let num_fit = self.last_render.unused_width / img.width;
|
||||||
let num_fit = self.last_render.unused_width / img_w;
|
|
||||||
if page_num >= self.page && (self.page + num_fit as usize) >= page_num {
|
if page_num >= self.page && (self.page + num_fit as usize) >= page_num {
|
||||||
self.last_render.rect = Rect::default();
|
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.
|
// 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
|
// 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
|
// number of pages, so the vec will already be cleared
|
||||||
self.rendered[page_num] = Some(RenderedImage::Image(img));
|
self.rendered[page_num] = Some(RenderedImage::Data(img));
|
||||||
|
|
||||||
if page_num > 10 { panic!() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_event(&mut self, ev: Event) -> Option<InputAction> {
|
pub fn handle_event(&mut self, ev: Event) -> Option<InputAction> {
|
||||||
|
|||||||
Reference in New Issue
Block a user