diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 20:50:04 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf (patch) | |
| tree | 9078fed09c1eb4604d28b1e3ac57e26389e18ac3 /src/pardes.zig | |
| parent | a93b796a3389acaf45775d302f559e01c19ddbfc (diff) | |
| download | pardes-5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf.tar.gz pardes-5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf.zip | |
tune PDF raster resolution per native backend
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 101 |
1 files changed, 90 insertions, 11 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index bc56f966..f32eb93f 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -40,6 +40,39 @@ pub const lsp = @import("lsp/lsp.zig"); pub const Platform = enum { tty, gui, web }; pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config").platform)); +/// Native PDF quality is a shell property, but the core owns MuPDF and the +/// RGBA cache. Kitty favors wire bandwidth; SDL favors physical-pixel text +/// quality and asks the renderer to cover either fit axis without upscaling. +pub const PdfRasterPolicy = struct { + dpi: u16, + max_dimension: u16, + match_viewport: bool, +}; + +pub const kitty_pdf_raster_policy: PdfRasterPolicy = .{ + .dpi = 96, + .max_dimension = 1200, + .match_viewport = false, +}; +pub const sdl_pdf_raster_policy: PdfRasterPolicy = .{ + .dpi = 192, + .max_dimension = 4096, + .match_viewport = true, +}; +pub const pdf_raster_policy: PdfRasterPolicy = switch (platform) { + .tty => kitty_pdf_raster_policy, + .gui => sdl_pdf_raster_policy, + .web => kitty_pdf_raster_policy, +}; + +test "SDL PDF raster policy is materially denser than Kitty" { + try std.testing.expect(sdl_pdf_raster_policy.dpi >= kitty_pdf_raster_policy.dpi * 2); + try std.testing.expect(sdl_pdf_raster_policy.max_dimension > + kitty_pdf_raster_policy.max_dimension * 3); + try std.testing.expect(!kitty_pdf_raster_policy.match_viewport); + try std.testing.expect(sdl_pdf_raster_policy.match_viewport); +} + // The capacities and the two heights that are STRUCTURE, not taste: the // fixed-size pane/column arrays, and the fact that the topbar and a tag are // one row each (nothing here works at any other value). The layout numbers @@ -108,6 +141,23 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { const first_place = first.images[0].?; try std.testing.expect(first_place.rgba.len == first_place.iw * first_place.ih * 4); try std.testing.expectEqual(image.NativeFit.width, first_place.native.fit); + const request = p.pdfRenderRequest(pane) orelse return error.MissingPdfRenderRequest; + try std.testing.expectEqual(pdf_raster_policy.dpi, request.dpi); + try std.testing.expectEqual(pdf_raster_policy.max_dimension, request.max_dimension); + try std.testing.expectEqual(request, pane.pdf.?.render_request); + try std.testing.expect(@max(first_place.iw, first_place.ih) <= request.max_dimension); + if (pdf_raster_policy.match_viewport) { + const viewport = p.pdfViewport(pane) orelse return error.MissingPdfViewport; + try std.testing.expectEqual(viewport.pixel_w, request.minimum_width); + try std.testing.expectEqual(viewport.pixel_h, request.minimum_height); + try std.testing.expect(first_place.iw >= viewport.pixel_w or + @max(first_place.iw, first_place.ih) == request.max_dimension); + try std.testing.expect(first_place.ih >= viewport.pixel_h or + @max(first_place.iw, first_place.ih) == request.max_dimension); + } else { + try std.testing.expectEqual(@as(u32, 0), request.minimum_width); + try std.testing.expectEqual(@as(u32, 0), request.minimum_height); + } // The default reading view moves one visible cell row without replacing // the page pixels. Fit/pan are placement state, not texture identity. @@ -426,7 +476,9 @@ test "PDF native mouse selection, Look, and highlights share page geometry" { try std.testing.expect(searched.images[0].?.native.revision != selected_revision); // Fit and viewport changes can crop a hit which was already revealed. - // Both rearm the placement-only reveal without forcing new page pixels. + // Fit remains placement-only. SDL's physical-viewport quality request + // changes with a resize and therefore replaces pixels; Kitty's fixed, + // bandwidth-conscious request retains them. const revision_before_geometry_change = searched.images[0].?.native.revision; pv.search_reveal_pending = false; p.togglePdfFit(pane); @@ -453,10 +505,14 @@ test "PDF native mouse selection, Look, and highlights share page geometry" { try std.testing.expect(pv.search_reveal_pending); _ = frame.reset(.retain_capacity); const geometry_changed = try p.render(frame.allocator()); - try std.testing.expectEqual( - revision_before_geometry_change, - geometry_changed.images[0].?.native.revision, - ); + if (pdf_raster_policy.match_viewport) + try std.testing.expect(geometry_changed.images[0].?.native.revision != + revision_before_geometry_change) + else + try std.testing.expectEqual( + revision_before_geometry_change, + geometry_changed.images[0].?.native.revision, + ); p.update(.{ .resize = .{ .cols = p.screen_w, .rows = p.screen_h, @@ -1756,6 +1812,11 @@ const PdfView = if (pdf_enabled) struct { iw: usize = 0, ih: usize = 0, render_tried: bool = false, + /// Last quality request attempted, successful or not. A viewport change + /// gets one transactional retry; a capped/failed request does not spin on + /// every frame. Fit and pan are intentionally absent from this key. + render_request: pdf_impl.RenderRequest = .{}, + render_request_valid: bool = false, render_revision: u32 = 0, text: []u8 = &.{}, text_tried: bool = false, @@ -7644,6 +7705,7 @@ pub const Pardes = struct { if (pv.rgba.len > 0) p.gpa.free(pv.rgba); pv.rgba = &.{}; pv.render_tried = false; + pv.render_request_valid = false; } fn dropPdfSearchResults(p: *Pardes, pv: *PdfView) void { @@ -7860,6 +7922,17 @@ pub const Pardes = struct { }; } + fn pdfRenderRequest(p: *const Pardes, pane: *const Pane) ?pdf_impl.RenderRequest { + if (comptime !pdf_enabled) return null; + const viewport = p.pdfViewport(pane) orelse return null; + return .{ + .dpi = pdf_raster_policy.dpi, + .minimum_width = if (pdf_raster_policy.match_viewport) viewport.pixel_w else 0, + .minimum_height = if (pdf_raster_policy.match_viewport) viewport.pixel_h else 0, + .max_dimension = pdf_raster_policy.max_dimension, + }; + } + fn pdfGeometry(p: *const Pardes, pane: *const Pane) ?image.NativeGeometry { if (comptime !pdf_enabled) return null; const pv = pane.pdf orelse return null; @@ -9569,21 +9642,27 @@ pub const Pardes = struct { } } - /// Render the current PDF page once and attach its stable RGBA until page - /// navigation invalidates it. MuPDF rendering is bounded to a 1600-pixel - /// longest side in pdf.zig, independent of malicious page dimensions. + /// Render the current PDF page once per backend-quality request and attach + /// its stable RGBA until pixels or physical viewport quality change. + /// Fit/pan stay placement-only, while every request retains its backend's + /// hard longest-side allocation ceiling. fn drawPdf(p: *Pardes, pane: *Pane, r: Rect, tx: u16, tw: u16) bool { if (comptime !pdf_enabled) return false; const pv = &(pane.pdf orelse return false); - if (!pv.render_tried) { + const request = p.pdfRenderRequest(pane) orelse return false; + if (!pv.render_tried or !pv.render_request_valid or + !pv.render_request.eql(request)) + { pv.render_tried = true; + pv.render_request = request; + pv.render_request_valid = true; p.resolvePdfSearch(pv); const rendered = render: { const highlights = p.pdfHighlights(pv) catch break :render null; const fresh = if (highlights.len > 0) - pv.document.renderWithHighlights(p.gpa, pv.page, highlights) catch break :render null + pv.document.renderWithHighlightsAt(p.gpa, pv.page, request, highlights) catch break :render null else - pv.document.render(p.gpa, pv.page) catch break :render null; + pv.document.renderAt(p.gpa, pv.page, request) catch break :render null; break :render fresh; }; if (rendered) |fresh| { |
