Files
tdf/src/converter.rs
T
itsjunetime f893ecba4d - Add profiling with --profile-time
- Update ratatui_image to get improvements from parallelization and removing unnecessary hashing
- Add benchmarks for only converting pages (not rendering)
- Add option to define number of pages to prerender on converter
2024-06-05 00:20:18 -06:00

130 lines
3.5 KiB
Rust

use flume::{Receiver, SendError, Sender, TryRecvError};
use futures_util::stream::StreamExt;
use image::ImageFormat;
use itertools::Itertools;
use ratatui_image::{picker::Picker, protocol::Protocol, Resize};
use crate::renderer::{fill_default, PageInfo, RenderError};
pub struct ConvertedPage {
pub page: Box<dyn Protocol>,
pub num: usize,
pub num_results: usize
}
pub enum ConverterMsg {
NumPages(usize),
GoToPage(usize),
AddImg(PageInfo)
}
pub async fn run_conversion_loop(
sender: Sender<Result<ConvertedPage, RenderError>>,
receiver: Receiver<ConverterMsg>,
mut picker: Picker,
prerender: usize
) -> Result<(), SendError<Result<ConvertedPage, RenderError>>> {
let mut images = vec![];
let mut page: usize = 0;
fn next_page(
images: &mut [Option<PageInfo>],
picker: &mut Picker,
page: usize,
iteration: &mut usize,
prerender: usize
) -> Result<Option<ConvertedPage>, RenderError> {
if images.is_empty() || *iteration >= prerender {
return Ok(None);
}
// This kinda mimics the way the renderer alternates between going above and below the
// current page (within the bounds of how many pages there are) until we've done 20
let idx_start = page.saturating_sub(prerender / 2);
let idx_end = idx_start.saturating_add(prerender).min(images.len());
// then we go through all the indices available to us and find the first one that has an
// image available to steal
let Some((page_info, new_iter)) = (idx_start..page)
.interleave(page..idx_end)
.enumerate()
.skip(*iteration)
.find_map(|(i_idx, p_idx)| images[p_idx].take().map(|p| (p, i_idx)))
else {
return Ok(None);
};
let img_area = page_info.img_data.area;
let dyn_img =
image::load_from_memory_with_format(&page_info.img_data.data, ImageFormat::Png)
.map_err(|e| {
RenderError::Render(format!("Couldn't convert Vec<u8> to DynamicImage: {e}"))
})?;
// 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 txt_img = picker
.new_protocol(dyn_img, img_area, Resize::None)
.map_err(|e| {
RenderError::Render(format!(
"Couldn't convert DynamicImage to ratatui image: {e}"
))
})?;
// update the iteration to the iteration that we stole this image from
*iteration = new_iter;
Ok(Some(ConvertedPage {
page: txt_img,
num: page_info.page,
num_results: page_info.search_results
}))
}
fn handle_notif(msg: ConverterMsg, images: &mut Vec<Option<PageInfo>>, page: &mut usize) {
match msg {
ConverterMsg::AddImg(img) => {
let page_num = img.page;
images[page_num] = Some(img);
}
ConverterMsg::NumPages(n_pages) => {
fill_default(images, n_pages);
*page = (*page).min(n_pages - 1);
}
ConverterMsg::GoToPage(new_page) => *page = new_page
}
}
'outer: loop {
let mut iteration = 0;
loop {
match receiver.try_recv() {
Ok(msg) => {
handle_notif(msg, &mut images, &mut page);
continue 'outer;
}
Err(TryRecvError::Empty) => (),
// if it's disconnected, we're done. just return.
Err(TryRecvError::Disconnected) => return Ok(())
}
match next_page(&mut images, &mut picker, page, &mut iteration, prerender) {
Ok(None) => break,
Ok(Some(img)) => sender.send(Ok(img))?,
Err(e) => sender.send(Err(e))?
}
}
let Some(msg) = receiver.stream().next().await else {
break;
};
handle_notif(msg, &mut images, &mut page);
}
Ok(())
}