From 5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 2 Aug 2026 20:50:04 -0300 Subject: tune PDF raster resolution per native backend --- src/pdf.zig | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 7 deletions(-) (limited to 'src/pdf.zig') 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 }, -- cgit v1.3