mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 12:38:58 -06:00
View: keep earlier tiles on screen while a zoom's new tiles render (#415)
* View: keep earlier tiles on screen while a zoom's new tiles render Zooming a tiled page (larger than 4096 device pixels) dropped every tile of the previous scale at once, so each zoom step fell back to the 2048 px backdrop until the new tiles arrived: the page went blurry on every frame of a pinch. Tiles are now keyed by scale; those of earlier zooms stay (nearest scale on top, at most 64) and are drawn stretched under the current ones until those cover the page. Entering the tiled range also keeps a whole-page raster sharper than the backdrop instead of replacing it with the backdrop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * hayro-syntax: fix float mantissa overflow on high-precision coordinates (#199) jsPDF outputs double-precision floating-point coordinates with 16+ decimal places in /MediaBox. In hayro-syntax, read_inner accumulated float mantissa digits into u64 using wrapping arithmetic. Integer mantissas exceeding u64::MAX wrapped modulo 2^64, severely corrupting page dimensions (e.g., 3874.96 pt became 185.61 pt, rendering as a warped ribbon). Accumulation now uses checked arithmetic: trailing fractional digits exceeding u64 capacity are safely ignored without incrementing decimal_shift. This preserves full IEEE-754 f64 precision while preventing overflow for numbers with arbitrary numbers of decimal places. Includes: - Unit test in hayro-syntax with reported values and a 50-digit fraction. - Synthetic PDF regression test in pdfcraft-render verifying inspect(). - Vendored patch (6) documentation in vendor/README.md. Fixes #199 * M2: bound page-label work and output before applying labels Share bounded PDF Roman and repeated-letter formatters between inspection and editing. Bound tree traversal, raw and decoded prefixes, individual labels and aggregate label output, with checked numbering arithmetic. Stage inspection labels before applying them and retain physical page numbers with a warning when optional labels cannot be processed. Reject unsupported label edits before changing the document tree. Add synthetic limit, cycle, Unicode, numbering and atomic-edit regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: mesh shadings can't exhaust memory or run for minutes `cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in place) found a page whose Coons patch shading (type 6), a 147-byte stream with 1-bit coordinates decoded onto +-40000, took the renderer past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by pixel over each triangle's device-space bounding box into a map, with nothing but the u16 cast bounding it: up to 65536 x 65536 entries for one triangle. A synthetic single patch passed 6 GB in six seconds; ten thousand triangles that each cover half a 1000 x 1000 page ran for minutes. Two rounds of review of earlier versions found more: every patch was cut into 722 triangles (about 150 KB) before anything limited them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever, and one with 4294967295 read the whole stream into one row; a mesh painted a thousand times was triangulated a thousand times; and the first caps cut content without a word. Vendored hayro-interpret patch (18): - `ShadingPattern::encode_within(area)`: the vendored hayro renderer passes the area it will actually draw (the path's bounding box on the canvas, which it already computes) plus a pixel for texels at a fractional edge, and triangles are sampled only inside it, so the map holds at most that area's pixels. - `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per triangle in it. A triangle's bounding box is about twice its area and a sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and small dense ones (a patch in a few pixels is 722 triangles) still shade completely; overlapping ones stop. - Coons and tensor-product patches are triangulated one at a time. - Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19 vertices per row. - A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200 mesh is 29 million) mesh triangles in all, reset per page like the existing paint budget. - Each of these cuts logs a warning. `encode()` keeps its signature; with no area it samples within a fixed 2^24 visits. Synthetic regression tests in render: - `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle mesh decoded onto +-40000 render and the rest of the page draws; a patch on the page, the part of the huge one that covers it, and a patch filling a 4 x 4 pt rectangle as a pattern are all shaded. - `many_overlapping_mesh_triangles_finish`: 10,000 triangles each covering half of a 1000 x 1000 page finish, and are drawn. - `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches beside the page, and a lattice with `/VerticesPerRow 0`. Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a mutation: no visit budget stalls past 60 s; no clip leaves the covering patch unshaded; no per-triangle allowance leaves the small patch unshaded; triangulating all patches first runs out of memory (killed at 5.8 GB); no rows check runs out of memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * M14: use adapter texture limits for HiDPI surfaces (#577) Preserve the existing egui-wgpu device descriptor while enabling the selected adapter's supported 2D texture extent. Cover the observed 8600-pixel Outlook startup surface and downlevel limit preservation. Co-authored-by: Codex <noreply@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: philkwek <philkwek03@gmail.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: James Locke <336218001+jcl-bit@users.noreply.github.com> Co-authored-by: msaulc <103976208+msaulc@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Codex
Claude Opus 5.5
philkwek
Streetblock
OpenAI Codex
James Locke
msaulc
Brandon Thomas
parent
bd4736d39c
commit
a70c2b4938
@@ -133,6 +133,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run
|
||||
- **2026-10-09 (M6, field rotation):** Regenerated form appearances honour `/MK /R` (0, 90, 180, 270). Filling or restyling a rotated field keeps the text, check mark and button caption turned, and Field Properties ▸ Position and `form_set_props` `rotation` set it, swapping width and height around the field's center when the axis changes. `form.appearance-regeneration` stays partial (Helvetica metrics are still approximate). Overall estimate unchanged (about 30–35%).
|
||||
- **2026-10-09 (M2, JPX sample normalization):** JPEG 2000 rescaling uses bounded integer arithmetic for supported 1–32-bit samples, including the 32-bit boundary that previously shifted past the integer width. Zero-sized rows are refused before scaling. Small contributor-original codestreams exercise decoding and rendering; endpoint and standard-depth regressions cover the same helper. Overall estimate unchanged (about 30–35%).
|
||||
|
||||
- **2026-10-09 (M14, #577):** The desktop GPU setup enables the selected adapter's supported 2D texture extent instead of egui-wgpu's fixed 8192-pixel extent. This removes the artificial surface ceiling behind a mixed-DPI Windows startup crash when opening Outlook PDF attachments. Synthetic regressions cover the observed 8600 × 3423 surface, downlevel adapters and preservation of other device limits. Hardware resolution limits still apply; milestone and overall effort estimates are unchanged.
|
||||
|
||||
- **2026-10-09 (M5.7, #434 partial):** Natural-size image stamps placed at a click or through `stamp_custom` remain upright on rotated pages, with a displayed-space-sized rectangle and a counterrotated standard appearance matrix. Move, resize, restyle and undo/redo preserve that orientation. Native and external Poppler save/reopen rendering cover all four quarter-turns; UI tests verify the displayed bounds. Explicit rectangle placement, image signatures, PDF-page stamps and image-file orientation metadata remain outside this repair. #434 stays open; milestone table and effort estimates remain unchanged.
|
||||
- **2026-10-09 (M1, raw-stream decoding limits):** COS checks unfiltered decoded data against the caller and per-stream limits before copying it, including empty filter arrays and streams whose Crypt filter was applied on load. Strict decoding uses the same raw-data guard. Tiny synthetic regressions cover exceeded, exact and zero-byte limits while retaining the filtered decoder behavior. Milestone and overall effort estimates unchanged (about 30–35%).
|
||||
|
||||
|
||||
@@ -390,6 +390,16 @@ fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()>
|
||||
/// its window appeared. D3D12 is the native, best-supported backend there.
|
||||
fn configure_gpu(native: &mut eframe::NativeOptions) {
|
||||
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut native.wgpu_options.wgpu_setup else { return };
|
||||
// egui-wgpu requests only 8192 pixels even when the adapter can render larger surfaces.
|
||||
// A restored 3440-point window at 250% DPI requests 8600 pixels and otherwise panics in
|
||||
// Surface::configure before the app starts. Keep the renderer's other device requirements,
|
||||
// but enable the adapter's actual 2D texture extent (also for smaller/downlevel adapters).
|
||||
let device_descriptor = std::sync::Arc::clone(&setup.device_descriptor);
|
||||
setup.device_descriptor = std::sync::Arc::new(move |adapter| {
|
||||
let mut descriptor = device_descriptor(adapter);
|
||||
descriptor.required_limits = surface_texture_limits(descriptor.required_limits, &adapter.limits());
|
||||
descriptor
|
||||
});
|
||||
if std::env::var_os("WGPU_POWER_PREF").is_none() {
|
||||
setup.power_preference = eframe::wgpu::PowerPreference::LowPower;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -419,6 +429,13 @@ fn configure_gpu(native: &mut eframe::NativeOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Surface textures must fit the device's enabled limits, not just the physical GPU's limits.
|
||||
/// Changing only the 2D extent preserves egui-wgpu's backend-specific downlevel requirements.
|
||||
fn surface_texture_limits(mut required: eframe::wgpu::Limits, supported: &eframe::wgpu::Limits) -> eframe::wgpu::Limits {
|
||||
required.max_texture_dimension_2d = supported.max_texture_dimension_2d;
|
||||
required
|
||||
}
|
||||
|
||||
/// A GPU with a connected monitor.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
@@ -555,6 +572,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restored_hidpi_window_fits_the_adapters_surface_limit() {
|
||||
// The Outlook launch crash: 3440 x 1369 saved points at 250% DPI requested this surface.
|
||||
let (width, height) = (8600, 3423);
|
||||
let required = eframe::wgpu::Limits::default();
|
||||
assert!(width > required.max_texture_dimension_2d);
|
||||
let supported = eframe::wgpu::Limits { max_texture_dimension_2d: 16384, ..required.clone() };
|
||||
let enabled = super::surface_texture_limits(required, &supported);
|
||||
assert!(width <= enabled.max_texture_dimension_2d);
|
||||
assert!(height <= enabled.max_texture_dimension_2d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_limits_preserve_other_requirements_and_never_overrequest() {
|
||||
for required in [eframe::wgpu::Limits::default(), eframe::wgpu::Limits::downlevel_webgl2_defaults()] {
|
||||
for extent in [4096, 8192, 16384] {
|
||||
let supported = eframe::wgpu::Limits { max_texture_dimension_2d: extent, ..required.clone() };
|
||||
let enabled = super::surface_texture_limits(required.clone(), &supported);
|
||||
let mut expected = required.clone();
|
||||
expected.max_texture_dimension_2d = extent;
|
||||
assert_eq!(enabled, expected);
|
||||
assert_eq!(enabled.max_texture_dimension_2d, supported.max_texture_dimension_2d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn winit_carries_the_windows_11_monitor_scale_fix() {
|
||||
// Issue #324: winit 0.30.13 as released nudges a window dragged onto a monitor with another
|
||||
|
||||
@@ -222,8 +222,9 @@ pub struct DocView {
|
||||
thumbs: HashMap<usize, TextureHandle>,
|
||||
/// Thumbnails that are out of date (still shown until their replacement arrives).
|
||||
stale_thumbs: HashSet<usize>,
|
||||
/// Sharp tiles of large pages: (page, tile x, tile y) → (scale tag, texture).
|
||||
tiles: HashMap<(usize, u32, u32), (u64, TextureHandle)>,
|
||||
/// Sharp tiles of large pages: (page, scale tag, tile x, tile y) → texture. Tiles of an
|
||||
/// earlier zoom stay, drawn stretched, until those of the current one cover the page.
|
||||
tiles: HashMap<(usize, u64, u32, u32), TextureHandle>,
|
||||
/// Text layers, extracted in the background on demand (selection, find, copy).
|
||||
pub(crate) texts: HashMap<usize, Arc<PageText>>,
|
||||
pub(crate) text_failed: HashSet<usize>,
|
||||
@@ -520,7 +521,7 @@ impl DocView {
|
||||
if self.thumbs.contains_key(&page) {
|
||||
self.stale_thumbs.insert(page);
|
||||
}
|
||||
self.tiles.retain(|(p, _, _), _| *p != page);
|
||||
self.tiles.retain(|(p, _, _, _), _| *p != page);
|
||||
self.texts.remove(&page);
|
||||
self.text_failed.remove(&page);
|
||||
self.errors.remove(&page);
|
||||
@@ -954,7 +955,7 @@ impl DocView {
|
||||
let page = r.request.page;
|
||||
if let Some(t) = r.request.tile {
|
||||
let tex = ctx.load_texture(format!("tile-{:?}-{page}-{}-{}", self.id, t.x, t.y), img, TextureOptions::LINEAR);
|
||||
self.tiles.insert((page, t.x / TILE, t.y / TILE), (r.request.tag, tex));
|
||||
self.tiles.insert((page, r.request.tag, t.x / TILE, t.y / TILE), tex);
|
||||
continue;
|
||||
}
|
||||
if r.request.tag & THUMB_TAG != 0 {
|
||||
@@ -1046,6 +1047,14 @@ fn scale_tag(scale: f32) -> u64 {
|
||||
(f64::from(scale) * 65536.0).round() as u64
|
||||
}
|
||||
|
||||
/// The scale of a raster tagged by [`scale_tag`].
|
||||
fn tag_scale(tag: u64) -> f32 {
|
||||
(tag as f64 / 65536.0) as f32
|
||||
}
|
||||
|
||||
/// Tiles of earlier zooms kept for a page while its current tiles are rendered.
|
||||
const OLD_TILES: usize = 64;
|
||||
|
||||
/// `r` moved so its corner lies on a whole physical pixel, so that a raster drawn from there
|
||||
/// maps texel for texel onto the screen.
|
||||
fn snap_to_pixels(r: Rect, ppp: f32) -> Rect {
|
||||
@@ -1590,7 +1599,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
// A backdrop, or a raster at an older scale until the new one arrives.
|
||||
xf.paint_image(painter, p.tex.id(), 0.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
if p.tag != want_tag {
|
||||
// Under tiles, a whole-page raster sharper than the backdrop (from before
|
||||
// the page grew past the tiling size) is a better backdrop: keep it.
|
||||
let sharper = tiled && p.tag != STALE_TAG && tag_scale(p.tag) >= want_scale;
|
||||
if p.tag != want_tag && !sharper {
|
||||
wanted.push((i, want_scale, want_tag, None));
|
||||
}
|
||||
}
|
||||
@@ -1613,6 +1625,21 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
let (v0, v1) = corners.iter().fold((1.0f32, 0.0f32), |(a, b), c| (a.min(c.1), b.max(c.1)));
|
||||
let (vx0, vy0) = ((u0.max(0.0) * dw as f32) as u32, (v0.max(0.0) * dh as f32) as u32);
|
||||
let (vx1, vy1) = (((u1.min(1.0) * dw as f32).ceil() as u32).min(dw), ((v1.min(1.0) * dh as f32).ceil() as u32).min(dh));
|
||||
// Tiles of earlier zooms first, stretched, the nearest scale last (on top):
|
||||
// until the current tiles arrive, a zoom shows them rather than the backdrop.
|
||||
let mut older: Vec<(f32, u32, u32, &TextureHandle)> = view
|
||||
.tiles
|
||||
.iter()
|
||||
.filter(|((p, t, _, _), _)| *p == i && *t != tag)
|
||||
.map(|((_, t, tx, ty), tex)| (tag_scale(*t), *tx, *ty, tex))
|
||||
.collect();
|
||||
older.sort_by(|a, b| (b.0 / scale).ln().abs().total_cmp(&(a.0 / scale).ln().abs()));
|
||||
for (s, tx, ty, tex) in older {
|
||||
let (ow, oh) = (device_pixels(pw_pt, s) as f32, device_pixels(ph_pt, s) as f32);
|
||||
let [w, h] = tex.size();
|
||||
let (x, y) = ((tx * TILE) as f32, (ty * TILE) as f32);
|
||||
xf.paint_image(painter, tex.id(), x / ow, y / oh, (x + w as f32) / ow, (y + h as f32) / oh);
|
||||
}
|
||||
for ty in vy0 / TILE..=(vy1.saturating_sub(1)) / TILE {
|
||||
for tx in vx0 / TILE..=(vx1.saturating_sub(1)) / TILE {
|
||||
let (x, y) = (tx * TILE, ty * TILE);
|
||||
@@ -1620,8 +1647,8 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
if w == 0 || h == 0 {
|
||||
continue;
|
||||
}
|
||||
match view.tiles.get(&(i, tx, ty)) {
|
||||
Some((ttag, tex)) if *ttag == tag => {
|
||||
match view.tiles.get(&(i, tag, tx, ty)) {
|
||||
Some(tex) => {
|
||||
let (fw, fh) = (dw as f32, dh as f32);
|
||||
xf.texel_aligned([dw as usize, dh as usize], ppp).paint_image(
|
||||
painter,
|
||||
@@ -2056,8 +2083,18 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
let cur = view.current;
|
||||
// Nearest pages first; for each page, the backdrop before its tiles.
|
||||
wanted.sort_by_key(|w| ((w.0 as isize - cur as isize).unsigned_abs(), w.3.is_some()));
|
||||
// Tiles for other zoom levels or far-away pages are useless: free them.
|
||||
view.tiles.retain(|(p, _, _), (t, _)| *t == tag && p.abs_diff(cur) <= 2);
|
||||
// Tiles of far-away pages are useless: free them. Tiles of other zooms stay only while a
|
||||
// current tile of their page is still missing, at most `OLD_TILES` of them, nearest scale first.
|
||||
let missing: HashSet<usize> = wanted.iter().filter(|w| w.3.is_some()).map(|w| w.0).collect();
|
||||
view.tiles.retain(|(p, t, _, _), _| p.abs_diff(cur) <= 2 && (*t == tag || missing.contains(p)));
|
||||
let mut older: Vec<(f32, (usize, u64, u32, u32))> =
|
||||
view.tiles.keys().filter(|k| k.1 != tag).map(|k| ((tag_scale(k.1) / scale).ln().abs(), *k)).collect();
|
||||
if older.len() > OLD_TILES {
|
||||
older.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
for (_, k) in older.drain(OLD_TILES..) {
|
||||
view.tiles.remove(&k);
|
||||
}
|
||||
}
|
||||
let mut queue: Vec<RenderRequest> =
|
||||
wanted.iter().map(|&(page, scale, tag, tile)| RenderRequest { page, kind: RequestKind::Pixels, tile, scale, tag }).collect();
|
||||
// Text layers: visible pages for selection, every page while a search is active.
|
||||
|
||||
@@ -818,3 +818,61 @@ fn compare_page_with_raster(h: &mut Harness<'static, PdfCraftApp>, pdf: &[u8], z
|
||||
}
|
||||
(sum as f64 / n as f64, worst)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zooming_a_tiled_page_shows_its_earlier_tiles_until_new_ones_arrive() {
|
||||
let _gpu = gpu();
|
||||
// A 1200 pt square page of fine lines (0.5 pt every 1.7 pt): tiled at 300 %.
|
||||
let mut content = String::from("0 g\n");
|
||||
for i in 0..670 {
|
||||
let v = 30.0 + 1.7 * i as f32;
|
||||
content.push_str(&format!("{v} 30 0.5 1140 re 30 {v} 1140 0.5 re\n"));
|
||||
}
|
||||
content.push_str("f\n");
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 1200 1200] /Contents 4 0 R >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len()
|
||||
)
|
||||
.into_bytes();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1000.0, 800.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.open_bytes("grid.pdf", None, pdf.clone()).expect("opens");
|
||||
app.set_option("left", "closed").unwrap();
|
||||
app.set_option("panel", "none").unwrap();
|
||||
app.set_option("zoom", "300").unwrap();
|
||||
app
|
||||
});
|
||||
for _ in 0..400 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
// Detail in the middle of the window: the summed difference between neighbouring pixels.
|
||||
let detail = |h: &mut Harness<'static, PdfCraftApp>| {
|
||||
let img = h.render().expect("renders");
|
||||
let (cx, cy) = (img.width() / 2, img.height() / 2);
|
||||
let mut sum = 0u64;
|
||||
for y in cy - 100..cy + 100 {
|
||||
for x in cx - 150..cx + 150 {
|
||||
sum += u64::from(img.get_pixel(x, y)[0].abs_diff(img.get_pixel(x + 1, y)[0]));
|
||||
}
|
||||
}
|
||||
sum
|
||||
};
|
||||
let sharp = detail(&mut h);
|
||||
// The first frame at a new zoom has no tiles for it yet (they are requested at its end).
|
||||
h.state_mut().set_option("zoom", "310").unwrap();
|
||||
h.step();
|
||||
let first = detail(&mut h);
|
||||
assert!(first * 10 > sharp * 7, "the first frame after zooming lost its detail: {first} against {sharp}");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ By default PdfCraft's own crates (`pdfcraft*`) log at `info` and everything else
|
||||
covers every target starting with it (`pdfcraft*=debug`). The logger is
|
||||
`apps/pdfcraft/src/logging.rs`. It never records the control-channel token or document passwords.
|
||||
|
||||
## GPU surface limits
|
||||
|
||||
The desktop renderer enables the selected adapter's supported 2D texture extent, while keeping
|
||||
egui-wgpu's other device requirements. Restored windows on mixed-DPI monitors can exceed its
|
||||
default 8192-pixel extent even when the GPU supports a larger surface (#577). The enabled extent
|
||||
never exceeds the adapter's capability; this does not make surfaces larger than the hardware
|
||||
limit renderable.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Effect |
|
||||
|
||||
Reference in New Issue
Block a user