summaryrefslogtreecommitdiff
path: root/src/pdf.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 23:29:50 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit90eed6a89c66d6243ea43e799113409244308563 (patch)
tree70b2842ae1a98ba829aae23c6bee47eb640e6aa1 /src/pdf.zig
parentf7f18f483483140f1ad0ff0edbe0fb550086b80f (diff)
downloadpardes-90eed6a89c66d6243ea43e799113409244308563.tar.gz
pardes-90eed6a89c66d6243ea43e799113409244308563.zip
render MuPDF pages directly into owned RGBA
Diffstat (limited to 'src/pdf.zig')
-rw-r--r--src/pdf.zig216
1 files changed, 133 insertions, 83 deletions
diff --git a/src/pdf.zig b/src/pdf.zig
index a1c05681..28f976e4 100644
--- a/src/pdf.zig
+++ b/src/pdf.zig
@@ -166,8 +166,7 @@ pub const Document = struct {
return .{ .width = size.width, .height = size.height };
}
- /// Render one zero-based page, bounded before allocation. MuPDF supplies
- /// row-strided RGB; Pardes' attachment boundary is packed straight RGBA.
+ /// Render one zero-based page directly into allocator-owned packed RGBA.
pub fn render(document: *Document, gpa: std.mem.Allocator, page: usize) !Render {
return document.renderAt(gpa, page, default_render_request);
}
@@ -227,62 +226,42 @@ pub const Document = struct {
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),
- request.dpi,
- minimum_width,
- minimum_height,
- request.max_dimension,
- &pixmap,
- )
- else
- c.pardes_pdf_render_with_highlights(
- document.handle,
- @intCast(page),
- request.dpi,
- minimum_width,
- minimum_height,
- request.max_dimension,
- @ptrCast(items.ptr),
- items.len,
- &pixmap,
- )
- else
- c.pardes_pdf_render(
- document.handle,
- @intCast(page),
- request.dpi,
- minimum_width,
- minimum_height,
- request.max_dimension,
- &pixmap,
- );
- if (status != c.PARDES_PDF_OK) return error.RenderFailed;
- defer c.pardes_pdf_drop_pixmap(document.handle, pixmap.handle);
-
- if (pixmap.samples == null or pixmap.width < 1 or pixmap.height < 1 or
- pixmap.stride < 1 or pixmap.components != 3)
- return error.BadPixmap;
- const width: usize = @intCast(pixmap.width);
- const height: usize = @intCast(pixmap.height);
- const stride: usize = @intCast(pixmap.stride);
- if (width > std.math.maxInt(usize) / 4 or
- height > std.math.maxInt(usize) / (width * 4) or
- height > std.math.maxInt(usize) / stride or
- stride < width * 3)
- return error.BadPixmap;
+ const items: []const Highlight = highlights orelse &.{};
+ if (items.len > c.PARDES_PDF_MAX_RESULT_QUADS)
+ return error.RenderFailed;
+ var raw_layout: c.pardes_pdf_raster_layout = std.mem.zeroes(c.pardes_pdf_raster_layout);
+ if (c.pardes_pdf_measure_render(
+ document.handle,
+ @intCast(page),
+ request.dpi,
+ minimum_width,
+ minimum_height,
+ request.max_dimension,
+ &raw_layout,
+ ) != c.PARDES_PDF_OK) return error.RenderFailed;
+ const layout = try checkedRasterLayout(raw_layout);
+ if (layout.len > max_owned_raster_bytes) return error.PixmapTooLarge;
- const rgba_len = width * height * 4;
- if (rgba_len > max_owned_raster_bytes) return error.PixmapTooLarge;
- const rgba = try gpa.alloc(u8, rgba_len);
+ const rgba = try gpa.alloc(u8, layout.len);
errdefer gpa.free(rgba);
- const samples: [*]const u8 = @ptrCast(pixmap.samples);
- try expandRgb(rgba, samples[0 .. stride * height], width, height, stride);
- return .{ .rgba = rgba, .width = width, .height = height };
+ const highlight_ptr: ?[*]const c.pardes_pdf_highlight =
+ if (items.len == 0) null else @ptrCast(items.ptr);
+ if (c.pardes_pdf_render_into(
+ document.handle,
+ @intCast(page),
+ request.dpi,
+ minimum_width,
+ minimum_height,
+ request.max_dimension,
+ highlight_ptr,
+ items.len,
+ rgba.ptr,
+ rgba.len,
+ @intCast(layout.width),
+ @intCast(layout.height),
+ @intCast(layout.stride),
+ ) != c.PARDES_PDF_OK) return error.RenderFailed;
+ return .{ .rgba = rgba, .width = layout.width, .height = layout.height };
}
/// Plain UTF-8-ish text projection for one zero-based page. MuPDF owns the
@@ -428,26 +407,33 @@ fn validQuad(quad: Quad) bool {
validPoint(quad.ll) and validPoint(quad.lr);
}
-fn expandRgb(
- rgba: []u8,
- rgb: []const u8,
+const RasterLayout = struct {
width: usize,
height: usize,
stride: usize,
-) !void {
- if (rgba.len != width * height * 4 or stride < width * 3 or
- rgb.len < stride * height)
+ len: usize,
+};
+
+fn checkedRasterLayout(raw: c.pardes_pdf_raster_layout) !RasterLayout {
+ if (raw.width < 1 or raw.height < 1 or raw.stride < 1)
return error.BadPixmap;
- for (0..height) |y| {
- const src = rgb[y * stride ..][0 .. width * 3];
- const dst = rgba[y * width * 4 ..][0 .. width * 4];
- for (0..width) |x| {
- dst[x * 4 + 0] = src[x * 3 + 0];
- dst[x * 4 + 1] = src[x * 3 + 1];
- dst[x * 4 + 2] = src[x * 3 + 2];
- dst[x * 4 + 3] = 0xff;
- }
- }
+ const width: usize = @intCast(raw.width);
+ const height: usize = @intCast(raw.height);
+ const stride: usize = @intCast(raw.stride);
+ const expected_stride = std.math.mul(usize, width, 4) catch
+ return error.BadPixmap;
+ if (stride != expected_stride) return error.BadPixmap;
+ const len = std.math.mul(usize, stride, height) catch
+ return error.BadPixmap;
+ if (raw.samples_len != len) return error.BadPixmap;
+ return .{ .width = width, .height = height, .stride = stride, .len = len };
+}
+
+fn expectOpaque(rgba: []const u8) !void {
+ if (rgba.len % 4 != 0) return error.BadPixmap;
+ var alpha: usize = 3;
+ while (alpha < rgba.len) : (alpha += 4)
+ try std.testing.expectEqual(@as(u8, 0xff), rgba[alpha]);
}
fn makeOffsetRotatedPdf(gpa: std.mem.Allocator) ![]u8 {
@@ -476,17 +462,78 @@ fn makeOffsetRotatedPdf(gpa: std.mem.Allocator) ![]u8 {
return bytes.toOwnedSlice(gpa);
}
-test "RGB pixmap expansion respects row stride and writes opaque RGBA" {
- const rgb = [_]u8{
- 1, 2, 3, 4, 5, 6, 99, 99,
- 7, 8, 9, 10, 11, 12, 88, 88,
- };
- var rgba: [16]u8 = undefined;
- try expandRgb(&rgba, &rgb, 2, 2, 8);
- try std.testing.expectEqualSlices(u8, &.{
- 1, 2, 3, 255, 4, 5, 6, 255,
- 7, 8, 9, 255, 10, 11, 12, 255,
- }, &rgba);
+test "caller-owned RGBA layout is packed and overflow checked" {
+ const layout = try checkedRasterLayout(.{
+ .width = 2,
+ .height = 3,
+ .stride = 8,
+ .samples_len = 24,
+ });
+ try std.testing.expectEqual(@as(usize, 2), layout.width);
+ try std.testing.expectEqual(@as(usize, 3), layout.height);
+ try std.testing.expectEqual(@as(usize, 8), layout.stride);
+ try std.testing.expectEqual(@as(usize, 24), layout.len);
+ try std.testing.expectError(error.BadPixmap, checkedRasterLayout(.{
+ .width = 2,
+ .height = 3,
+ .stride = 7,
+ .samples_len = 21,
+ }));
+}
+
+test "RGBA allocation and buffer-validation failures leave the document renderable" {
+ var document = try Document.open("docs/design.pdf");
+ defer document.deinit();
+
+ var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
+ .fail_index = 0,
+ });
+ try std.testing.expectError(
+ error.OutOfMemory,
+ document.render(failing.allocator(), 0),
+ );
+
+ var raw_layout: c.pardes_pdf_raster_layout = std.mem.zeroes(c.pardes_pdf_raster_layout);
+ try std.testing.expectEqual(c.PARDES_PDF_OK, c.pardes_pdf_measure_render(
+ document.handle,
+ 0,
+ default_render_request.dpi,
+ default_render_request.minimum_width,
+ default_render_request.minimum_height,
+ default_render_request.max_dimension,
+ &raw_layout,
+ ));
+ const layout = try checkedRasterLayout(raw_layout);
+ {
+ const scratch = try std.testing.allocator.alloc(u8, layout.len);
+ defer std.testing.allocator.free(scratch);
+ @memset(scratch, 0xa5);
+ try std.testing.expectEqual(c.PARDES_PDF_ERROR, c.pardes_pdf_render_into(
+ document.handle,
+ 0,
+ default_render_request.dpi,
+ default_render_request.minimum_width,
+ default_render_request.minimum_height,
+ default_render_request.max_dimension,
+ null,
+ 0,
+ scratch.ptr,
+ scratch.len - 1,
+ raw_layout.width,
+ raw_layout.height,
+ raw_layout.stride,
+ ));
+ for (scratch) |byte|
+ try std.testing.expectEqual(@as(u8, 0xa5), byte);
+ }
+
+ const after = try document.render(std.testing.allocator, 0);
+ defer std.testing.allocator.free(after.rgba);
+ const repeated = try document.render(std.testing.allocator, 0);
+ defer std.testing.allocator.free(repeated.rgba);
+ try std.testing.expectEqualSlices(u8, after.rgba, repeated.rgba);
+ try expectOpaque(after.rgba);
+ try expectOpaque(repeated.rgba);
}
test "MuPDF search returns normalized oriented quads and word selection text" {
@@ -544,6 +591,9 @@ test "highlighted render changes pixels while plain render stays stable" {
const plain_after = try document.render(std.testing.allocator, 0);
defer std.testing.allocator.free(plain_after.rgba);
+ try expectOpaque(plain_before.rgba);
+ try expectOpaque(marked.rgba);
+ try expectOpaque(plain_after.rgba);
try std.testing.expectEqual(plain_before.width, marked.width);
try std.testing.expectEqual(plain_before.height, marked.height);
try std.testing.expect(!std.mem.eql(u8, plain_before.rgba, marked.rgba));