wip: pause checkpoint — P6 feature complete, gate interrupted (not for merge)

This commit is contained in:
swaits committed 2026-10-27 14:41:34 -06:00
1 parent cc4f104f06
commit ce36640874
68 files changed
+4592 -374

No files matched your search

+31 -65
View File
@@ -1,7 +1,7 @@
//! Rendering receipts and documents to textures, off the UI thread.
//!
//! PDFs are rasterized with `hayro` (pure Rust), images decoded with
//! `image`; Markdown and plain text are read as UTF-8 and drawn as text
//! PDFs are rasterized by the one extractor in `ledger-import` (pure Rust,
//! plan P6), images decoded with `image`; Markdown and plain text are read as UTF-8 and drawn as text
//! (Markdown by `egui_commonmark`), so they follow the theme and select
//! like any text. Results are cached by blob hash: the same bytes never render
//! twice, and a hash is the content, so the cache never goes stale. Bytes
@@ -61,6 +61,10 @@ pub const MAX_SIDE: f32 = 8192.0;
/// lines.
const THUMB_TEXT_BYTES: usize = 2048;
/// A page shown in full is rendered at most this many pixels on its
/// longest side: a letter page at twice its points (612 x 792).
const PAGE_PX: u32 = 1584;
/// Where the live viewer keeps thumbnails: the user's cache directory
/// (`$XDG_CACHE_HOME`, else `~/.cache`), `wanderbooks/thumbs/v1-560`;
/// the version and width name how they were drawn, so a change in either
@@ -322,23 +326,27 @@ fn render_thumb(job: &Job, dir: Option<&std::path::Path>) -> Result<Rendered, St
return Ok(Rendered::Pages(vec![to_color_image(img.to_rgba8())]));
}
let img = if job.mime == "application/pdf" || bytes.starts_with(b"%PDF") {
let png = pdf_first_page_png(bytes)?;
// A page rendered twice the tile's width, then cut down here: the
// same pixels a picture is, and one extractor for every reader
// (plan P6).
let png = ledger_import::extract::page_png(&bytes, 0, (THUMB_PX * 2.0) as u32)
.map_err(|e| e.to_string())?;
image::load_from_memory(&png).map_err(|e| e.to_string())?
} else if job.mime.starts_with("image/") {
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
if img.width() as f32 > THUMB_PX {
let h = (img.height() as f32 * THUMB_PX / img.width() as f32).max(1.0);
img.resize_exact(
THUMB_PX as u32,
h as u32,
image::imageops::FilterType::Triangle,
)
} else {
img
}
image::load_from_memory(&bytes).map_err(|e| e.to_string())?
} else {
return Err(format!("{} can't be previewed", job.mime));
};
let img = if img.width() as f32 > THUMB_PX {
let h = (img.height() as f32 * THUMB_PX / img.width() as f32).max(1.0);
img.resize_exact(
THUMB_PX as u32,
h as u32,
image::imageops::FilterType::Triangle,
)
} else {
img
};
let img = img.crop_imm(0, 0, img.width(), img.height().min(THUMB_MAX_H));
if let Some(path) = &kept {
let mut png = Vec::new();
@@ -385,43 +393,13 @@ fn keep(path: &std::path::Path, png: &[u8]) {
}
}
/// A PDF's first page as a PNG, [`THUMB_PX`] wide.
fn pdf_first_page_png(bytes: Vec<u8>) -> Result<Vec<u8>, String> {
use hayro::hayro_interpret::InterpreterSettings;
use hayro::hayro_syntax::Pdf;
use hayro::vello_cpu::color::palette::css::WHITE;
use hayro::{PixmapSettings, RenderCache, RenderSettings, render};
let pdf = Pdf::new(bytes).map_err(|e| format!("not a readable PDF: {e:?}"))?;
let pages = pdf.pages();
let page = pages.first().ok_or("the PDF has no pages")?;
let (w, h) = page.render_dimensions();
// THUMB_PX wide, but never past a texture's side on a long page (the
// picture is cut to THUMB_MAX_H after), and never nothing.
let scale = (THUMB_PX / w.max(1.0))
.min(MAX_SIDE / h.max(1.0))
.max(1.0 / w.max(h).max(1.0));
let px = PixmapSettings {
x_scale: scale,
y_scale: scale,
bg_color: WHITE,
};
let pixmap = render(
page,
&RenderCache::new(),
&InterpreterSettings::default(),
&RenderSettings::default(),
&px,
);
pixmap.into_png().map_err(|e| format!("{e:?}"))
}
fn render(job: &Job) -> Result<Rendered, String> {
let bytes = BlobStore::open_read_only(&job.root)
.get(&job.sha)
.map_err(|e| e.to_string())?;
let is_pdf = job.mime == "application/pdf" || bytes.starts_with(b"%PDF");
if is_pdf {
render_pdf(bytes).map(Rendered::Pages)
render_pdf(&bytes).map(Rendered::Pages)
} else if job.mime.starts_with("image/") {
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
// Past a texture's side, scaled down to fit (aspect kept).
@@ -478,27 +456,15 @@ pub fn text_of(bytes: Vec<u8>) -> Result<String, String> {
Ok(text)
}
fn render_pdf(bytes: Vec<u8>) -> Result<Vec<ColorImage>, String> {
use hayro::hayro_interpret::InterpreterSettings;
use hayro::hayro_syntax::Pdf;
use hayro::vello_cpu::color::palette::css::WHITE;
use hayro::{PixmapSettings, RenderCache, RenderSettings, render};
let pdf = Pdf::new(bytes).map_err(|e| format!("not a readable PDF: {e:?}"))?;
let cache = RenderCache::new();
let interp = InterpreterSettings::default();
let settings = RenderSettings::default();
/// A PDF's pages, in order, through the one extractor (`ledger-import`,
/// plan P6): each at most [`PAGE_PX`] pixels on its longest side, at most
/// [`ledger_import::extract::MAX_PAGES`] pages.
fn render_pdf(bytes: &[u8]) -> Result<Vec<ColorImage>, String> {
let pages = ledger_import::extract::page_count(bytes).map_err(|e| e.to_string())?;
let mut out = Vec::new();
for page in pdf.pages().iter().take(30) {
// Twice the page's size, but never past a texture's side.
let (w, h) = page.render_dimensions();
let scale = 2.0f32.min(MAX_SIDE / w.max(h).max(1.0));
let px = PixmapSettings {
x_scale: scale,
y_scale: scale,
bg_color: WHITE,
};
let pixmap = render(page, &cache, &interp, &settings, &px);
let png = pixmap.into_png().map_err(|e| format!("{e:?}"))?;
for page in 0..pages.min(ledger_import::extract::MAX_PAGES) {
let png =
ledger_import::extract::page_png(bytes, page, PAGE_PX).map_err(|e| e.to_string())?;
let img = image::load_from_memory(&png).map_err(|e| e.to_string())?;
out.push(to_color_image(img.to_rgba8()));
}