summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 20:50:04 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf (patch)
tree9078fed09c1eb4604d28b1e3ac57e26389e18ac3 /src
parenta93b796a3389acaf45775d302f559e01c19ddbfc (diff)
downloadpardes-5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf.tar.gz
pardes-5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf.zip
tune PDF raster resolution per native backend
Diffstat (limited to 'src')
-rw-r--r--src/pardes.zig101
-rw-r--r--src/pdf.zig107
-rw-r--r--src/pdf_bridge.c25
-rw-r--r--src/pdf_bridge.h10
4 files changed, 221 insertions, 22 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| {
diff --git a/src/pdf.zig b/src/pdf.zig
index 48b15cf1..b06c85e8 100644
--- a/src/pdf.zig
+++ b/src/pdf.zig
@@ -7,13 +7,31 @@ const c = @cImport({
@cInclude("pdf_bridge.h");
});
-pub const max_render_dimension = 1600;
+/// One raster-quality request. `minimum_*` asks MuPDF for enough source pixels
+/// to avoid backend upscaling; `max_dimension` remains the hard allocation
+/// ceiling for hostile page sizes and very large displays.
+pub const RenderRequest = struct {
+ dpi: u16 = 144,
+ minimum_width: u32 = 0,
+ minimum_height: u32 = 0,
+ max_dimension: u16 = 1600,
+
+ pub fn eql(a: RenderRequest, b: RenderRequest) bool {
+ return std.meta.eql(a, b);
+ }
+};
+
+pub const default_render_request: RenderRequest = .{};
+pub const absolute_max_render_dimension: u16 = 4096;
+pub const max_owned_raster_bytes: usize = @as(usize, absolute_max_render_dimension) *
+ absolute_max_render_dimension * 4;
/// MuPDF's per-document eviction cache ceiling. The owned RGBA render buffer
/// is allocated separately by Pardes and does not count against this store.
pub const store_limit_bytes: usize = c.PARDES_PDF_STORE_LIMIT_BYTES;
test "MuPDF document cache has an explicit conservative ceiling" {
try std.testing.expectEqual(@as(usize, 64 * 1024 * 1024), store_limit_bytes);
+ try std.testing.expectEqual(@as(usize, 64 * 1024 * 1024), max_owned_raster_bytes);
}
/// Page-space geometry is normalized to the page bounds, not a render. It
@@ -135,7 +153,16 @@ pub const Document = struct {
/// Render one zero-based page, bounded before allocation. MuPDF supplies
/// row-strided RGB; Pardes' attachment boundary is packed straight RGBA.
pub fn render(document: *Document, gpa: std.mem.Allocator, page: usize) !Render {
- return document.renderInternal(gpa, page, null);
+ return document.renderAt(gpa, page, default_render_request);
+ }
+
+ pub fn renderAt(
+ document: *Document,
+ gpa: std.mem.Allocator,
+ page: usize,
+ request: RenderRequest,
+ ) !Render {
+ return document.renderInternal(gpa, page, request, null);
}
/// Render and bake normalized oriented highlights through MuPDF's draw
@@ -146,31 +173,64 @@ pub const Document = struct {
page: usize,
highlights: []const Highlight,
) !Render {
- return document.renderInternal(gpa, page, highlights);
+ return document.renderWithHighlightsAt(
+ gpa,
+ page,
+ default_render_request,
+ highlights,
+ );
+ }
+
+ pub fn renderWithHighlightsAt(
+ document: *Document,
+ gpa: std.mem.Allocator,
+ page: usize,
+ request: RenderRequest,
+ highlights: []const Highlight,
+ ) !Render {
+ return document.renderInternal(gpa, page, request, highlights);
}
fn renderInternal(
document: *Document,
gpa: std.mem.Allocator,
page: usize,
+ request: RenderRequest,
highlights: ?[]const Highlight,
) !Render {
if (page >= document.pages or page > std.math.maxInt(c_int))
return error.PageOutOfRange;
+ if (request.dpi == 0 or request.max_dimension == 0 or
+ request.max_dimension > absolute_max_render_dimension)
+ return error.InvalidRenderRequest;
+ const minimum_width: c_int = @intCast(@min(
+ request.minimum_width,
+ @as(u32, @intCast(std.math.maxInt(c_int))),
+ ));
+ const minimum_height: c_int = @intCast(@min(
+ request.minimum_height,
+ @as(u32, @intCast(std.math.maxInt(c_int))),
+ ));
var pixmap: c.pardes_pdf_pixmap = std.mem.zeroes(c.pardes_pdf_pixmap);
const status = if (highlights) |items|
if (items.len == 0)
c.pardes_pdf_render(
document.handle,
@intCast(page),
- max_render_dimension,
+ request.dpi,
+ minimum_width,
+ minimum_height,
+ request.max_dimension,
&pixmap,
)
else
c.pardes_pdf_render_with_highlights(
document.handle,
@intCast(page),
- max_render_dimension,
+ request.dpi,
+ minimum_width,
+ minimum_height,
+ request.max_dimension,
@ptrCast(items.ptr),
items.len,
&pixmap,
@@ -179,7 +239,10 @@ pub const Document = struct {
c.pardes_pdf_render(
document.handle,
@intCast(page),
- max_render_dimension,
+ request.dpi,
+ minimum_width,
+ minimum_height,
+ request.max_dimension,
&pixmap,
);
if (status != c.PARDES_PDF_OK) return error.RenderFailed;
@@ -197,7 +260,9 @@ pub const Document = struct {
stride < width * 3)
return error.BadPixmap;
- const rgba = try gpa.alloc(u8, width * height * 4);
+ const rgba_len = width * height * 4;
+ if (rgba_len > max_owned_raster_bytes) return error.PixmapTooLarge;
+ const rgba = try gpa.alloc(u8, rgba_len);
errdefer gpa.free(rgba);
const samples: [*]const u8 = @ptrCast(pixmap.samples);
try expandRgb(rgba, samples[0 .. stride * height], width, height, stride);
@@ -493,6 +558,34 @@ test "offset crop and rotation keep normalized highlights pixel-aligned" {
try std.testing.expectEqual(@as(usize, 300), plain.width);
try std.testing.expectEqual(@as(usize, 320), plain.height);
+ const kitty = try document.renderAt(std.testing.allocator, 0, .{
+ .dpi = 96,
+ .max_dimension = 1200,
+ });
+ defer std.testing.allocator.free(kitty.rgba);
+ try std.testing.expect(kitty.width < plain.width);
+ try std.testing.expect(kitty.height < plain.height);
+
+ const sdl = try document.renderAt(std.testing.allocator, 0, .{
+ .dpi = 192,
+ .minimum_width = 640,
+ .minimum_height = 480,
+ .max_dimension = 4096,
+ });
+ defer std.testing.allocator.free(sdl.rgba);
+ try std.testing.expect(sdl.width >= 640);
+ try std.testing.expect(sdl.height >= 480);
+ try std.testing.expect(sdl.width * sdl.height >= kitty.width * kitty.height * 4);
+
+ const capped = try document.renderAt(std.testing.allocator, 0, .{
+ .dpi = 192,
+ .minimum_width = 4096,
+ .minimum_height = 4096,
+ .max_dimension = 512,
+ });
+ defer std.testing.allocator.free(capped.rgba);
+ try std.testing.expectEqual(@as(usize, 512), @max(capped.width, capped.height));
+
const left_half = Quad{
.ul = .{ .x = 0, .y = 0 },
.ur = .{ .x = 0.5, .y = 0 },
diff --git a/src/pdf_bridge.c b/src/pdf_bridge.c
index a26fea55..320919b1 100644
--- a/src/pdf_bridge.c
+++ b/src/pdf_bridge.c
@@ -234,17 +234,24 @@ int
pardes_pdf_render(
pardes_pdf_document *document,
int page_number,
+ int dpi,
+ int minimum_width,
+ int minimum_height,
int max_dimension,
pardes_pdf_pixmap *out)
{
return pardes_pdf_render_with_highlights(
- document, page_number, max_dimension, NULL, 0, out);
+ document, page_number, dpi, minimum_width, minimum_height,
+ max_dimension, NULL, 0, out);
}
int
pardes_pdf_render_with_highlights(
pardes_pdf_document *document,
int page_number,
+ int dpi,
+ int minimum_width,
+ int minimum_height,
int max_dimension,
const pardes_pdf_highlight *highlights,
size_t highlight_count,
@@ -256,12 +263,15 @@ pardes_pdf_render_with_highlights(
fz_path *path = NULL;
fz_matrix ctm;
fz_rect bounds;
+ float page_width;
+ float page_height;
float longest;
- float scale = 2.0f; /* 144 dpi; PDF user space is 72 dpi. */
+ float scale;
size_t i;
if (document == NULL || out == NULL || page_number < 0 ||
- page_number >= document->page_count || max_dimension < 1 ||
+ page_number >= document->page_count || dpi < 1 ||
+ minimum_width < 0 || minimum_height < 0 || max_dimension < 1 ||
(highlight_count != 0 && highlights == NULL) ||
highlight_count > PARDES_PDF_MAX_RESULT_QUADS)
return PARDES_PDF_ERROR;
@@ -275,9 +285,16 @@ pardes_pdf_render_with_highlights(
{
pardes_pdf_cache_page(document, page_number, 0);
bounds = document->cached_bounds;
- longest = fmaxf(bounds.x1 - bounds.x0, bounds.y1 - bounds.y0);
+ page_width = bounds.x1 - bounds.x0;
+ page_height = bounds.y1 - bounds.y0;
+ longest = fmaxf(page_width, page_height);
if (!(longest > 0.0f))
fz_throw(ctx, FZ_ERROR_FORMAT, "PDF page has empty bounds");
+ scale = (float)dpi / 72.0f;
+ if (minimum_width > 0)
+ scale = fmaxf(scale, (float)minimum_width / page_width);
+ if (minimum_height > 0)
+ scale = fmaxf(scale, (float)minimum_height / page_height);
if (longest * scale > (float)max_dimension)
scale = (float)max_dimension / longest;
/*
diff --git a/src/pdf_bridge.h b/src/pdf_bridge.h
index 85f477d0..77b3dc58 100644
--- a/src/pdf_bridge.h
+++ b/src/pdf_bridge.h
@@ -105,9 +105,16 @@ enum {
pardes_pdf_document *pardes_pdf_open(const char *path, int *page_count);
void pardes_pdf_close(pardes_pdf_document *document);
+/*
+ * Start at dpi, raise the uniform scale until both optional minimum pixel
+ * dimensions are met, then clamp the page's longest side to max_dimension.
+ */
int pardes_pdf_render(
pardes_pdf_document *document,
int page,
+ int dpi,
+ int minimum_width,
+ int minimum_height,
int max_dimension,
pardes_pdf_pixmap *out
);
@@ -115,6 +122,9 @@ int pardes_pdf_render(
int pardes_pdf_render_with_highlights(
pardes_pdf_document *document,
int page,
+ int dpi,
+ int minimum_width,
+ int minimum_height,
int max_dimension,
const pardes_pdf_highlight *highlights,
size_t highlight_count,