diff options
Diffstat (limited to 'src/pdf.zig')
| -rw-r--r-- | src/pdf.zig | 128 |
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, |
