mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
- Remove unused oxipng dep
- throw converter onto its own task - switch to using multi-thread runtime - use unbounded channels in a few more places to prevent deadlocks
This commit is contained in:
+105
-120
@@ -1,138 +1,123 @@
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll}
|
||||
};
|
||||
|
||||
use futures_util::Stream;
|
||||
use image::ImageFormat;
|
||||
use itertools::Itertools;
|
||||
use ratatui_image::{picker::Picker, protocol::Protocol, Resize};
|
||||
use tokio::sync::mpsc::{error::{SendError, TryRecvError}, UnboundedReceiver, UnboundedSender};
|
||||
|
||||
use crate::renderer::{PageInfo, RenderError};
|
||||
use crate::renderer::{fill_default, PageInfo, RenderError};
|
||||
|
||||
const MAX_ITER: usize = 20;
|
||||
|
||||
pub struct Converter {
|
||||
images: Vec<Option<PageInfo>>,
|
||||
picker: Picker,
|
||||
page: usize,
|
||||
// once it reaches 20, we're done rendering images
|
||||
iteration: usize
|
||||
}
|
||||
|
||||
impl Converter {
|
||||
pub fn new(picker: Picker) -> Self {
|
||||
Self {
|
||||
images: vec![],
|
||||
picker,
|
||||
page: 0,
|
||||
iteration: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_img(&mut self, page: PageInfo) {
|
||||
let page_num = page.page;
|
||||
self.images[page_num] = Some(page);
|
||||
// just reset it to 0 so we grab this image again next time we try to get an image (if this
|
||||
// image is in the current list of iterations, so to speak)
|
||||
self.iteration = 0;
|
||||
}
|
||||
|
||||
pub fn set_n_pages(&mut self, pages: usize) {
|
||||
self.images = Vec::with_capacity(pages);
|
||||
for _ in 0..pages {
|
||||
self.images.push(None);
|
||||
}
|
||||
|
||||
self.page = self.page.min(pages - 1);
|
||||
}
|
||||
|
||||
pub fn go_to_page(&mut self, page: usize) {
|
||||
self.page = page;
|
||||
self.iteration = 0;
|
||||
}
|
||||
|
||||
pub fn change_page_by(&mut self, change: isize) {
|
||||
self.page = (self.page as isize + change) as usize;
|
||||
// We just reset iteration here. I think there's some heuristic we could do to place
|
||||
// iteration exactly where it needs to be to render the next page, but trying to determine
|
||||
// that caused me a lot of bugs, and only causes the slightest inefficiency (down below,
|
||||
// when we skip `self.iteration` elements in an iterator), so it's like whatever
|
||||
self.iteration = 0;
|
||||
}
|
||||
|
||||
pub fn get_next_img(&mut self) -> Option<ConversionResult> {
|
||||
// In this fn, we return Poll::Pending and don't store a Waker 'cause this will be called
|
||||
// in a loop with tokio::select, and in no other context. The pending that we return on one
|
||||
// iteration will just be dropped/cancelled as soon as some other action happens, and then
|
||||
// next time select is called, this'll be checked again, and then we might be in the right
|
||||
// circumstance to return a Ready
|
||||
if self.iteration >= MAX_ITER || self.images.is_empty() {
|
||||
return 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 = self.page.saturating_sub(MAX_ITER / 2);
|
||||
let idx_end = idx_start.saturating_add(MAX_ITER).min(self.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 (page_info, iteration) = (idx_start..self.page)
|
||||
.interleave(self.page..idx_end)
|
||||
.enumerate()
|
||||
.skip(self.iteration)
|
||||
.find_map(|(i_idx, p_idx)| self.images[p_idx].take().map(|p| (p, i_idx)))?;
|
||||
|
||||
let img_area = page_info.img_data.area;
|
||||
|
||||
let dyn_img =
|
||||
match image::load_from_memory_with_format(&page_info.img_data.data, ImageFormat::Png) {
|
||||
Ok(dt) => dt,
|
||||
Err(e) =>
|
||||
return Some(Err(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 = match self.picker.new_protocol(dyn_img, img_area, Resize::Crop) {
|
||||
Ok(img) => img,
|
||||
Err(e) =>
|
||||
return Some(Err(RenderError::Render(format!(
|
||||
"Couldn't convert DynamicImage to ratatui image: {e}"
|
||||
)))),
|
||||
};
|
||||
|
||||
// update the iteration to the iteration that we stole this image from
|
||||
self.iteration = iteration;
|
||||
|
||||
Some(Ok(ConvertedPage {
|
||||
page: txt_img,
|
||||
num: page_info.page,
|
||||
num_results: page_info.search_results
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConvertedPage {
|
||||
pub page: Box<dyn Protocol>,
|
||||
pub num: usize,
|
||||
pub num_results: usize
|
||||
}
|
||||
|
||||
type ConversionResult = Result<ConvertedPage, RenderError>;
|
||||
pub enum ConverterMsg {
|
||||
NumPages(usize),
|
||||
GoToPage(usize),
|
||||
AddImg(PageInfo)
|
||||
}
|
||||
|
||||
impl Stream for Converter {
|
||||
type Item = ConversionResult;
|
||||
pub async fn run_conversion_loop(
|
||||
sender: UnboundedSender<Result<ConvertedPage, RenderError>>,
|
||||
mut receiver: UnboundedReceiver<ConverterMsg>,
|
||||
mut picker: Picker
|
||||
) -> Result<(), SendError<Result<ConvertedPage, RenderError>>> {
|
||||
let mut images = vec![];
|
||||
let mut page: usize = 0;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.get_next_img() {
|
||||
Some(res) => Poll::Ready(Some(res)),
|
||||
None => Poll::Pending
|
||||
fn next_page(
|
||||
images: &mut [Option<PageInfo>],
|
||||
picker: &mut Picker,
|
||||
page: usize,
|
||||
iteration: &mut usize
|
||||
) -> Result<Option<ConvertedPage>, RenderError> {
|
||||
if images.is_empty() || *iteration >= MAX_ITER {
|
||||
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(MAX_ITER / 2);
|
||||
let idx_end = idx_start.saturating_add(MAX_ITER).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::Crop)
|
||||
.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) => (),
|
||||
Err(TryRecvError::Disconnected) => panic!("Disconnected :(")
|
||||
}
|
||||
|
||||
match next_page(&mut images, &mut picker, page, &mut iteration) {
|
||||
Ok(None) => break,
|
||||
Ok(Some(img)) => sender.send(Ok(img))?,
|
||||
Err(e) => sender.send(Err(e))?
|
||||
}
|
||||
}
|
||||
|
||||
let Some(msg) = receiver.recv().await else {
|
||||
break;
|
||||
};
|
||||
|
||||
handle_notif(msg, &mut images, &mut page);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user