summaryrefslogtreecommitdiff
path: root/src/pdf.zig
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/pdf.zig
parenta93b796a3389acaf45775d302f559e01c19ddbfc (diff)
downloadpardes-5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf.tar.gz
pardes-5d5f2a25011c751452fb4ff42e28ee84d4bb4fdf.zip
tune PDF raster resolution per native backend
Diffstat (limited to 'src/pdf.zig')
-rw-r--r--src/pdf.zig107
1 files changed, 100 insertions, 7 deletions
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 },