summaryrefslogtreecommitdiff
path: root/src/pdf.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pdf.zig')
-rw-r--r--src/pdf.zig128
1 files changed, 123 insertions, 5 deletions
diff --git a/src/pdf.zig b/src/pdf.zig
index 8ddca497..d98d5ff2 100644
--- a/src/pdf.zig
+++ b/src/pdf.zig
@@ -66,13 +66,17 @@ test "custom allocator owns a complete PDF document lifecycle" {
}
/// 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.
+/// to avoid backend upscaling; `max_dimension` caps a raster row always, and
+/// the page's height unless that would draw it below `display_*`, the size
+/// the page is shown at (0: no floor). A page taller than that is never one
+/// allocation: its owner renders bands of it (`Raster.band`).
pub const RenderRequest = struct {
dpi: u16 = 144,
minimum_width: u32 = 0,
minimum_height: u32 = 0,
max_dimension: u16 = 1600,
+ display_width: u32 = 0,
+ display_height: u32 = 0,
pub fn eql(a: RenderRequest, b: RenderRequest) bool {
return std.meta.eql(a, b);
@@ -81,8 +85,12 @@ pub const RenderRequest = struct {
pub const default_render_request: RenderRequest = .{};
pub const absolute_max_render_dimension: u16 = 4096;
+/// The most one render buffer holds: a whole page, or one band of a page
+/// taller than this.
pub const max_owned_raster_bytes: usize = @as(usize, absolute_max_render_dimension) *
absolute_max_render_dimension * 4;
+/// The tallest page raster any request produces (rows), whatever it is shown at.
+pub const max_raster_rows: usize = c.PARDES_PDF_MAX_RASTER_ROWS;
/// 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;
@@ -895,11 +903,13 @@ pub const Document = struct {
bounded.minimum_width,
bounded.minimum_height,
bounded.max_dimension,
+ bounded.display_width,
+ bounded.display_height,
&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;
- return layout;
+ // The page's shape, not an allocation: a page taller than
+ // `max_owned_raster_bytes` allows is rendered a band at a time.
+ return checkedRasterLayout(raw_layout);
}
/// Rasterize `band` of a page into `rgba`, which MUST be exactly
@@ -932,6 +942,7 @@ pub const Document = struct {
if (rgba.len != band.len) return error.BadPixmap;
if (band.height == 0 or band.y + band.height > raster.height)
return error.BadPixmap;
+ if (band.len > max_owned_raster_bytes) return error.PixmapTooLarge;
const bounded = try boundedRequest(document, page, request);
if (highlights.len > c.PARDES_PDF_MAX_RESULT_QUADS) return error.RenderFailed;
const highlight_ptr: ?[*]const c.pardes_pdf_highlight =
@@ -943,6 +954,8 @@ pub const Document = struct {
bounded.minimum_width,
bounded.minimum_height,
bounded.max_dimension,
+ bounded.display_width,
+ bounded.display_height,
highlight_ptr,
highlights.len,
rgba.ptr,
@@ -982,6 +995,8 @@ pub const Document = struct {
bounded.minimum_width,
bounded.minimum_height,
bounded.max_dimension,
+ bounded.display_width,
+ bounded.display_height,
if (highlights.len == 0) null else @ptrCast(highlights.ptr),
highlights.len,
rgba.ptr,
@@ -1015,6 +1030,8 @@ pub const Document = struct {
bounded.minimum_width,
bounded.minimum_height,
bounded.max_dimension,
+ bounded.display_width,
+ bounded.display_height,
if (highlights.len == 0) null else @ptrCast(highlights.ptr),
highlights.len,
&row_start,
@@ -1028,6 +1045,8 @@ pub const Document = struct {
minimum_width: c_int,
minimum_height: c_int,
max_dimension: c_int,
+ display_width: c_int,
+ display_height: c_int,
};
fn boundedRequest(
@@ -1051,9 +1070,15 @@ pub const Document = struct {
@as(u32, @intCast(std.math.maxInt(c_int))),
)),
.max_dimension = request.max_dimension,
+ .display_width = clampInt(request.display_width),
+ .display_height = clampInt(request.display_height),
};
}
+ fn clampInt(value: u32) c_int {
+ return @intCast(@min(value, @as(u32, @intCast(std.math.maxInt(c_int)))));
+ }
+
fn renderInternal(
document: *Document,
gpa: std.mem.Allocator,
@@ -1062,6 +1087,7 @@ pub const Document = struct {
highlights: ?[]const Highlight,
) !Render {
const raster = try document.measureRenderAt(page, request);
+ if (raster.len > max_owned_raster_bytes) return error.PixmapTooLarge;
const rgba = try gpa.alloc(u8, raster.len);
errdefer gpa.free(rgba);
try document.renderIntoAt(
@@ -1453,6 +1479,94 @@ pub fn makeLinksTestPdf(gpa: std.mem.Allocator) ![]u8 {
return allocateGeneratedPdf(gpa, &offsets, {}, emitLinksPdf);
}
+/// The tall test page: 600 x 9000 pt, fifteen A4 pages' height on one.
+pub const tall_test_page: PageSize = .{ .width = 600, .height = 9000 };
+
+fn emitTallPdf(bytes: *PdfBytes, offsets: []usize, _: void) !void {
+ // A labelled row every 100 pt, each with a slanting rule, so a band
+ // edge or a seam between chunks cuts through text and through lines.
+ var storage: [16 * 1024]u8 = undefined;
+ var stream: std.Io.Writer = .fixed(&storage);
+ var row: usize = 0;
+ while (row * 100 + 160 < 9000) : (row += 1) {
+ const y = 9000 - 60 - row * 100;
+ stream.print(
+ "BT /F1 28 Tf 40 {d} Td (Row {d}) Tj ET\n0.2 0.3 0.9 RG 3 w 20 {d} m 580 {d} l S\n",
+ .{ y, row, y - 20, y - 80 },
+ ) catch return error.GeneratedPdfTooLarge;
+ }
+ stream.writeAll("BT /F1 28 Tf 40 40 Td (bottommost) Tj ET\n") catch return error.GeneratedPdfTooLarge;
+ const content = stream.buffered();
+ try bytes.appendSlice("%PDF-1.7\n%\xE2\xE3\xCF\xD3\n");
+ try beginPdfObject(bytes, offsets, 1);
+ try bytes.appendSlice("<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
+ try beginPdfObject(bytes, offsets, 2);
+ try bytes.appendSlice("<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n");
+ try beginPdfObject(bytes, offsets, 3);
+ try bytes.appendSlice("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 9000] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>\nendobj\n");
+ try beginPdfObject(bytes, offsets, 4);
+ try bytes.print("<< /Length {d} >>\nstream\n", .{content.len});
+ try bytes.appendSlice(content);
+ try bytes.appendSlice("endstream\nendobj\n");
+ try beginPdfObject(bytes, offsets, 5);
+ try bytes.appendSlice("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n");
+ try finishGeneratedPdf(bytes, offsets);
+}
+
+/// TEST-ONLY: one page fifteen times as tall as it is wide (`tall_test_page`),
+/// "Row N" every 100 pt from the top and "bottommost" at its foot.
+pub fn makeTallTestPdf(gpa: std.mem.Allocator) ![]u8 {
+ var offsets: [6]usize = undefined;
+ return allocateGeneratedPdf(gpa, &offsets, {}, emitTallPdf);
+}
+
+test "a tall page rasterizes at the size it is shown, however many max_dimensions tall" {
+ const bytes = try makeTallTestPdf(std.testing.allocator);
+ defer std.testing.allocator.free(bytes);
+ var document = try Document.openBytes(bytes);
+ defer document.deinit();
+ const size = try document.pageSize(0);
+ try std.testing.expectEqual(tall_test_page.width, size.width);
+ try std.testing.expectEqual(tall_test_page.height, size.height);
+
+ // Shown 960 px wide (fit width): 960 px a row, 14400 rows, though that is
+ // far past max_dimension. The old clamp of the longest side made this
+ // 273 x 4096, drawn 3.5 times too large.
+ const shown: RenderRequest = .{ .dpi = 192, .minimum_width = 960, .minimum_height = 600, .max_dimension = 4096, .display_width = 960 };
+ const tall = try document.measureRenderAt(0, shown);
+ try std.testing.expectEqual(@as(usize, 960), tall.width);
+ try std.testing.expectEqual(@as(usize, 14400), tall.height);
+ // Kitty's 96 dpi, as any page has it, not 1200 rows' worth.
+ const kitty = try document.measureRenderAt(0, .{ .dpi = 96, .max_dimension = 1200, .display_width = 944 });
+ try std.testing.expectEqual(@as(usize, 800), kitty.width);
+ try std.testing.expectEqual(@as(usize, 12000), kitty.height);
+ // The dpi never takes it past what is shown: that would be bands of
+ // pixels nobody sees.
+ const small = try document.measureRenderAt(0, .{ .dpi = 192, .max_dimension = 4096, .display_width = 300 });
+ try std.testing.expectEqual(@as(usize, 300), small.width);
+ // Shown whole (fit height), the height cap holds: the page is 600 px
+ // tall on screen and 4096 rows is already more.
+ const whole = try document.measureRenderAt(0, .{ .dpi = 192, .minimum_width = 960, .minimum_height = 600, .max_dimension = 4096, .display_height = 600 });
+ try std.testing.expectEqual(@as(usize, 4096), whole.height);
+ // Without a display size, the old budget.
+ const blind = try document.measureRenderAt(0, .{ .dpi = 192, .minimum_width = 960, .max_dimension = 4096 });
+ try std.testing.expectEqual(@as(usize, 4096), blind.height);
+ // A row is never longer than max_dimension, whatever is shown.
+ const wide = try document.measureRenderAt(0, .{ .dpi = 192, .minimum_width = 10_000, .max_dimension = 4096, .display_width = 10_000 });
+ try std.testing.expectEqual(@as(usize, 4096), wide.width);
+ // One buffer is bounded even when the page is not: a whole render of a
+ // page past it is refused, a band of it is not.
+ const poster: RenderRequest = .{ .dpi = 72, .minimum_width = 4096, .max_dimension = 4096, .display_width = 4096 };
+ const huge = try document.measureRenderAt(0, poster);
+ try std.testing.expect(huge.len > max_owned_raster_bytes);
+ try std.testing.expectError(error.PixmapTooLarge, document.renderAt(std.testing.allocator, 0, poster));
+ const band = huge.band(huge.height - 100, 100);
+ const rows = try std.testing.allocator.alloc(u8, band.len);
+ defer std.testing.allocator.free(rows);
+ try document.renderIntoAt(0, poster, huge, band, &.{}, rows);
+ try expectOpaque(rows);
+}
+
test "PDF links own URIs across page eviction and preserve internal coordinates" {
const gpa = std.testing.allocator;
start(gpa);
@@ -1830,6 +1944,8 @@ test "RGBA allocation and buffer-validation failures leave the document renderab
default_render_request.minimum_width,
default_render_request.minimum_height,
default_render_request.max_dimension,
+ 0,
+ 0,
&raw_layout,
));
const layout = try checkedRasterLayout(raw_layout);
@@ -1844,6 +1960,8 @@ test "RGBA allocation and buffer-validation failures leave the document renderab
default_render_request.minimum_width,
default_render_request.minimum_height,
default_render_request.max_dimension,
+ 0,
+ 0,
null,
0,
scratch.ptr,