diff options
Diffstat (limited to 'src/pdf.zig')
| -rw-r--r-- | src/pdf.zig | 388 |
1 files changed, 270 insertions, 118 deletions
diff --git a/src/pdf.zig b/src/pdf.zig index 84d6a4e5..2a674172 100644 --- a/src/pdf.zig +++ b/src/pdf.zig @@ -7,6 +7,125 @@ const c = @cImport({ @cInclude("pdf_bridge.h"); }); +const allocation_header_size = 16; +var bridge_allocator: std.mem.Allocator = undefined; +var bridge_started = false; + +comptime { + if (allocation_header_size < @sizeOf(usize)) + @compileError("PDF allocation header is too small"); +} + +fn bridgeAlloc(user: ?*anyopaque, size_arg: usize) callconv(.c) ?*anyopaque { + const user_ptr = user orelse return null; + const allocator: *std.mem.Allocator = @ptrCast(@alignCast(user_ptr)); + const size = @max(size_arg, 1); + const total = std.math.add(usize, allocation_header_size, size) catch + return null; + const bytes = allocator.alignedAlloc(u8, .@"16", total) catch return null; + const header: *align(16) usize = @ptrCast(bytes.ptr); + header.* = total; + return @ptrCast(bytes.ptr + allocation_header_size); +} + +fn bridgeFree(user: ?*anyopaque, pointer: ?*anyopaque) callconv(.c) void { + const data = pointer orelse return; + const user_ptr = user orelse @panic("PDF allocator callback has no allocator"); + const allocator: *std.mem.Allocator = @ptrCast(@alignCast(user_ptr)); + const data_bytes: [*]u8 = @ptrCast(data); + const base: [*]align(16) u8 = + @ptrCast(@alignCast(data_bytes - allocation_header_size)); + const header: *align(16) usize = @ptrCast(base); + allocator.free(base[0..header.*]); +} + +fn bridgeRealloc( + user: ?*anyopaque, + pointer: ?*anyopaque, + size_arg: usize, +) callconv(.c) ?*anyopaque { + if (pointer == null) return bridgeAlloc(user, size_arg); + if (size_arg == 0) { + bridgeFree(user, pointer); + return null; + } + + const user_ptr = user orelse return null; + const allocator: *std.mem.Allocator = @ptrCast(@alignCast(user_ptr)); + const total = std.math.add(usize, allocation_header_size, size_arg) catch + return null; + const data_bytes: [*]u8 = @ptrCast(pointer.?); + const base: [*]align(16) u8 = + @ptrCast(@alignCast(data_bytes - allocation_header_size)); + const header: *align(16) usize = @ptrCast(base); + const resized = allocator.realloc(base[0..header.*], total) catch return null; + const resized_header: *align(16) usize = @ptrCast(resized.ptr); + resized_header.* = total; + return @ptrCast(resized.ptr + allocation_header_size); +} + +/// Installs the allocator used by all subsequently opened PDF documents and +/// by MuPDF itself. Call once before opening any documents. +pub fn start(allocator: std.mem.Allocator) void { + if (bridge_started) @panic("PDF allocator is already started"); + bridge_allocator = allocator; + const descriptor = c.pardes_pdf_allocator{ + .user = &bridge_allocator, + .alloc = bridgeAlloc, + .realloc = bridgeRealloc, + .free = bridgeFree, + }; + if (c.pardes_pdf_set_allocator(&descriptor) != c.PARDES_PDF_OK) + @panic("cannot start PDF allocator while documents are live"); + bridge_started = true; +} + +/// Restores libc and MuPDF's default allocator. Every Document must have been +/// destroyed first so no allocation can outlive its allocator domain. +pub fn stop() void { + if (!bridge_started) @panic("PDF allocator is not started"); + if (c.pardes_pdf_set_allocator(null) != c.PARDES_PDF_OK) + @panic("cannot stop PDF allocator while documents are live"); + bridge_started = false; + bridge_allocator = undefined; +} + +test "PDF C allocator callbacks preserve and free exact allocations" { + var allocator = std.testing.allocator; + + var live: ?*anyopaque = bridgeAlloc(&allocator, 4) orelse + return error.OutOfMemory; + defer if (live) |pointer| bridgeFree(&allocator, pointer); + const original: [*]u8 = @ptrCast(live.?); + @memcpy(original[0..4], "data"); + + live = bridgeRealloc(&allocator, live, 32) orelse + return error.OutOfMemory; + const grown: [*]u8 = @ptrCast(live.?); + try std.testing.expectEqualSlices(u8, "data", grown[0..4]); + + try std.testing.expect(bridgeAlloc( + &allocator, + std.math.maxInt(usize), + ) == null); + + const zero = bridgeAlloc(&allocator, 0) orelse return error.OutOfMemory; + bridgeFree(&allocator, zero); + try std.testing.expect(bridgeRealloc(&allocator, live, 0) == null); + live = null; +} + +test "custom allocator owns a complete PDF document lifecycle" { + start(std.testing.allocator); + defer stop(); + + var document = try Document.open("docs/design.pdf"); + defer document.deinit(); + const raster = try document.render(std.testing.allocator, 0); + defer std.testing.allocator.free(raster.rgba); + try std.testing.expect(raster.rgba.len > 0); +} + /// 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. @@ -741,15 +860,22 @@ pub const Document = struct { document: *Document, gpa: std.mem.Allocator, page: usize, - start: Point, - end: Point, + selection_start: Point, + selection_end: Point, ) !Selection { const page_number = try document.checkedPage(page); - if (!validPoint(start) or !validPoint(end)) return error.InvalidPoint; + if (!validPoint(selection_start) or !validPoint(selection_end)) + return error.InvalidPoint; var selected: c.pardes_pdf_selection = std.mem.zeroes(c.pardes_pdf_selection); - const status = c.pardes_pdf_select(document.handle, page_number, start, end, &selected); + const status = c.pardes_pdf_select( + document.handle, + page_number, + selection_start, + selection_end, + &selected, + ); if (status == c.PARDES_PDF_LIMIT_EXCEEDED) return error.TooManyResults; if (status != c.PARDES_PDF_OK) @@ -783,19 +909,20 @@ pub const Document = struct { document: *Document, gpa: std.mem.Allocator, page: usize, - start: Point, - end: Point, + selection_start: Point, + selection_end: Point, ) ![]u8 { const page_number = try document.checkedPage(page); - if (!validPoint(start) or !validPoint(end)) return error.InvalidPoint; + if (!validPoint(selection_start) or !validPoint(selection_end)) + return error.InvalidPoint; var text: c.pardes_pdf_owned_text = std.mem.zeroes(c.pardes_pdf_owned_text); if (c.pardes_pdf_copy_selection( document.handle, page_number, - start, - end, + selection_start, + selection_end, &text, ) != c.PARDES_PDF_OK) return error.TextFailed; defer c.pardes_pdf_drop_owned_text(document.handle, text.handle); @@ -857,93 +984,120 @@ fn expectOpaque(rgba: []const u8) !void { try std.testing.expectEqual(@as(u8, 0xff), rgba[alpha]); } -fn makeOffsetRotatedPdf(gpa: std.mem.Allocator) ![]u8 { - const stream = "q 1 0 0 rg 120 220 160 150 re f Q\n"; - var bytes: std.ArrayList(u8) = .empty; - errdefer bytes.deinit(gpa); - var offsets: [5]usize = @splat(0); +const generated_pdf_fragment_max = 256; - try bytes.appendSlice(gpa, "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); - offsets[1] = bytes.items.len; - try bytes.appendSlice(gpa, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"); - offsets[2] = bytes.items.len; - try bytes.appendSlice(gpa, "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n"); - offsets[3] = bytes.items.len; - try bytes.appendSlice(gpa, "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [100 200 300 400] " ++ - "/CropBox [120 220 280 370] /Rotate 90 /Resources << >> " ++ - "/Contents 4 0 R >>\nendobj\n"); - offsets[4] = bytes.items.len; - try bytes.print(gpa, "4 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ stream.len, stream }); +const PdfBytes = struct { + storage: ?[]u8 = null, + len: usize = 0, - const xref = bytes.items.len; - try bytes.appendSlice(gpa, "xref\n0 5\n0000000000 65535 f \n"); - for (offsets[1..]) |offset| - try bytes.print(gpa, "{d:0>10} 00000 n \n", .{offset}); - try bytes.print(gpa, "trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{d}\n%%EOF\n", .{xref}); - return bytes.toOwnedSlice(gpa); -} + fn appendSlice(bytes: *PdfBytes, source: []const u8) !void { + const end = std.math.add(usize, bytes.len, source.len) catch + return error.GeneratedPdfTooLarge; + if (bytes.storage) |storage| { + if (end > storage.len) return error.GeneratedPdfTooLarge; + @memcpy(storage[bytes.len..end], source); + } + bytes.len = end; + } -fn beginPdfObject( - bytes: *std.ArrayList(u8), + fn print(bytes: *PdfBytes, comptime format: []const u8, args: anytype) !void { + var fragment: [generated_pdf_fragment_max]u8 = undefined; + const rendered = std.fmt.bufPrint(&fragment, format, args) catch + return error.GeneratedPdfFragmentTooLong; + try bytes.appendSlice(rendered); + } +}; + +fn allocateGeneratedPdf( gpa: std.mem.Allocator, offsets: []usize, - number: usize, -) !void { - offsets[number] = bytes.items.len; - try bytes.print(gpa, "{d} 0 obj\n", .{number}); + context: anytype, + comptime emit: anytype, +) ![]u8 { + @memset(offsets, 0); + var counter: PdfBytes = .{}; + try emit(&counter, offsets, context); + + const result = try gpa.alloc(u8, counter.len); + errdefer gpa.free(result); + @memset(offsets, 0); + var bytes: PdfBytes = .{ .storage = result }; + try emit(&bytes, offsets, context); + std.debug.assert(bytes.len == result.len); + return result; } -fn finishGeneratedPdf( - bytes: *std.ArrayList(u8), - gpa: std.mem.Allocator, - offsets: []const usize, -) ![]u8 { - const xref = bytes.items.len; - try bytes.print(gpa, "xref\n0 {d}\n0000000000 65535 f \n", .{offsets.len}); +fn beginPdfObject(bytes: *PdfBytes, offsets: []usize, number: usize) !void { + offsets[number] = bytes.len; + try bytes.print("{d} 0 obj\n", .{number}); +} + +fn finishGeneratedPdf(bytes: *PdfBytes, offsets: []const usize) !void { + const xref = bytes.len; + try bytes.print("xref\n0 {d}\n0000000000 65535 f \n", .{offsets.len}); for (offsets[1..]) |offset| - try bytes.print(gpa, "{d:0>10} 00000 n \n", .{offset}); + try bytes.print("{d:0>10} 00000 n \n", .{offset}); try bytes.print( - gpa, "trailer\n<< /Size {d} /Root 1 0 R >>\nstartxref\n{d}\n%%EOF\n", .{ offsets.len, xref }, ); - return bytes.toOwnedSlice(gpa); } -fn makeOutlinePdf(gpa: std.mem.Allocator) ![]u8 { - var bytes: std.ArrayList(u8) = .empty; - errdefer bytes.deinit(gpa); - var offsets: [12]usize = @splat(0); +fn emitOffsetRotatedPdf(bytes: *PdfBytes, offsets: []usize, stream: []const u8) !void { + try bytes.appendSlice("%PDF-1.4\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 [100 200 300 400] " ++ + "/CropBox [120 220 280 370] /Rotate 90 /Resources << >> " ++ + "/Contents 4 0 R >>\nendobj\n"); + try beginPdfObject(bytes, offsets, 4); + try bytes.print("<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ stream.len, stream }); + try finishGeneratedPdf(bytes, offsets); +} - try bytes.appendSlice(gpa, "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"); - try beginPdfObject(&bytes, gpa, &offsets, 1); - try bytes.appendSlice(gpa, "<< /Type /Catalog /Pages 2 0 R /Outlines 7 0 R /PageMode /UseOutlines >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, &offsets, 2); - try bytes.appendSlice(gpa, "<< /Type /Pages /Count 3 /Kids [3 0 R 4 0 R 5 0 R] >>\nendobj\n"); +fn makeOffsetRotatedPdf(gpa: std.mem.Allocator) ![]u8 { + const stream = "q 1 0 0 rg 120 220 160 150 re f Q\n"; + var offsets: [5]usize = undefined; + return allocateGeneratedPdf(gpa, &offsets, stream, emitOffsetRotatedPdf); +} + +fn emitOutlinePdf(bytes: *PdfBytes, offsets: []usize, _: void) !void { + 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 /Outlines 7 0 R /PageMode /UseOutlines >>\nendobj\n"); + try beginPdfObject(bytes, offsets, 2); + try bytes.appendSlice("<< /Type /Pages /Count 3 /Kids [3 0 R 4 0 R 5 0 R] >>\nendobj\n"); for (3..6) |page| { - try beginPdfObject(&bytes, gpa, &offsets, page); - try bytes.appendSlice(gpa, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 300] /Resources << >> >>\nendobj\n"); + try beginPdfObject(bytes, offsets, page); + try bytes.appendSlice("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 300] /Resources << >> >>\nendobj\n"); } - try beginPdfObject(&bytes, gpa, &offsets, 6); - try bytes.appendSlice(gpa, "<< >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, &offsets, 7); - try bytes.appendSlice(gpa, "<< /Type /Outlines /First 8 0 R /Last 11 0 R /Count 4 >>\nendobj\n"); + try beginPdfObject(bytes, offsets, 6); + try bytes.appendSlice("<< >>\nendobj\n"); + try beginPdfObject(bytes, offsets, 7); + try bytes.appendSlice("<< /Type /Outlines /First 8 0 R /Last 11 0 R /Count 4 >>\nendobj\n"); // Destinationless branch with a deliberately missing /Title. - try beginPdfObject(&bytes, gpa, &offsets, 8); - try bytes.appendSlice(gpa, "<< /Parent 7 0 R /First 9 0 R /Last 9 0 R /Next 10 0 R /Count 1 >>\nendobj\n"); + try beginPdfObject(bytes, offsets, 8); + try bytes.appendSlice("<< /Parent 7 0 R /First 9 0 R /Last 9 0 R /Next 10 0 R /Count 1 >>\nendobj\n"); // UTF-16BE "Café 子", with bold/italic + color metadata. - try beginPdfObject(&bytes, gpa, &offsets, 9); - try bytes.appendSlice(gpa, "<< /Title <FEFF00430061006600E900205B50> /Parent 8 0 R " ++ + try beginPdfObject(bytes, offsets, 9); + try bytes.appendSlice("<< /Title <FEFF00430061006600E900205B50> /Parent 8 0 R " ++ "/Dest [4 0 R /XYZ 12 34 null] /F 3 /C [0.2 0.4 0.6] >>\nendobj\n"); // A present empty title and a destination with only one usable axis. - try beginPdfObject(&bytes, gpa, &offsets, 10); - try bytes.appendSlice(gpa, "<< /Title () /Parent 7 0 R /Prev 8 0 R /Next 11 0 R " ++ + try beginPdfObject(bytes, offsets, 10); + try bytes.appendSlice("<< /Title () /Parent 7 0 R /Prev 8 0 R /Next 11 0 R " ++ "/Dest [5 0 R /FitH 70] >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, &offsets, 11); - try bytes.appendSlice(gpa, "<< /Title (External) /Parent 7 0 R /Prev 10 0 R " ++ + try beginPdfObject(bytes, offsets, 11); + try bytes.appendSlice("<< /Title (External) /Parent 7 0 R /Prev 10 0 R " ++ "/A << /S /URI /URI (https://example.com/manual) >> >>\nendobj\n"); + try finishGeneratedPdf(bytes, offsets); +} - return finishGeneratedPdf(&bytes, gpa, &offsets); +fn makeOutlinePdf(gpa: std.mem.Allocator) ![]u8 { + var offsets: [12]usize = undefined; + return allocateGeneratedPdf(gpa, &offsets, {}, emitOutlinePdf); } /// TEST-ONLY generated fixture shared with the core integration tests. It is @@ -958,81 +1112,79 @@ pub fn makeNoOutlineTestPdf(gpa: std.mem.Allocator) ![]u8 { return makeOffsetRotatedPdf(gpa); } -fn makeTooDeepOutlinePdf(gpa: std.mem.Allocator) ![]u8 { - const levels = c.PARDES_PDF_MAX_OUTLINE_DEPTH + 1; +fn emitTooDeepOutlinePdf(bytes: *PdfBytes, offsets: []usize, levels: usize) !void { const first_outline_item = 5; - const object_count = first_outline_item + levels; - const offsets = try gpa.alloc(usize, object_count); - defer gpa.free(offsets); - @memset(offsets, 0); - var bytes: std.ArrayList(u8) = .empty; - errdefer bytes.deinit(gpa); - - try bytes.appendSlice(gpa, "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"); - try beginPdfObject(&bytes, gpa, offsets, 1); - try bytes.appendSlice(gpa, "<< /Type /Catalog /Pages 2 0 R /Outlines 4 0 R >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, offsets, 2); - try bytes.appendSlice(gpa, "<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, offsets, 3); - try bytes.appendSlice(gpa, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << >> >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, offsets, 4); - try bytes.print(gpa, "<< /Type /Outlines /First 5 0 R /Last 5 0 R /Count {d} >>\nendobj\n", .{levels}); + 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 /Outlines 4 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 100 100] /Resources << >> >>\nendobj\n"); + try beginPdfObject(bytes, offsets, 4); + try bytes.print("<< /Type /Outlines /First 5 0 R /Last 5 0 R /Count {d} >>\nendobj\n", .{levels}); for (0..levels) |level| { const number = first_outline_item + level; - try beginPdfObject(&bytes, gpa, offsets, number); - try bytes.print(gpa, "<< /Title (Level {d}) /Parent {d} 0 R", .{ + try beginPdfObject(bytes, offsets, number); + try bytes.print("<< /Title (Level {d}) /Parent {d} 0 R", .{ level, if (level == 0) 4 else number - 1, }); if (level + 1 < levels) { - try bytes.print(gpa, " /First {d} 0 R /Last {d} 0 R /Count {d}", .{ + try bytes.print(" /First {d} 0 R /Last {d} 0 R /Count {d}", .{ number + 1, number + 1, levels - level - 1, }); } - try bytes.appendSlice(gpa, " >>\nendobj\n"); + try bytes.appendSlice(" >>\nendobj\n"); } - - return finishGeneratedPdf(&bytes, gpa, offsets); + try finishGeneratedPdf(bytes, offsets); } -fn makeWideOutlinePdf(gpa: std.mem.Allocator, items: usize) ![]u8 { - if (items == 0) return error.EmptyOutlineFixture; - const first_outline_item = 5; - const object_count = try std.math.add(usize, first_outline_item, items); +fn makeTooDeepOutlinePdf(gpa: std.mem.Allocator) ![]u8 { + const levels: usize = c.PARDES_PDF_MAX_OUTLINE_DEPTH + 1; + const object_count = 5 + levels; const offsets = try gpa.alloc(usize, object_count); defer gpa.free(offsets); - @memset(offsets, 0); - var bytes: std.ArrayList(u8) = .empty; - errdefer bytes.deinit(gpa); + return allocateGeneratedPdf(gpa, offsets, levels, emitTooDeepOutlinePdf); +} - try bytes.appendSlice(gpa, "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"); - try beginPdfObject(&bytes, gpa, offsets, 1); - try bytes.appendSlice(gpa, "<< /Type /Catalog /Pages 2 0 R /Outlines 4 0 R >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, offsets, 2); - try bytes.appendSlice(gpa, "<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, offsets, 3); - try bytes.appendSlice(gpa, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << >> >>\nendobj\n"); - try beginPdfObject(&bytes, gpa, offsets, 4); - try bytes.print(gpa, "<< /Type /Outlines /First 5 0 R /Last {d} 0 R /Count {d} >>\nendobj\n", .{ +fn emitWideOutlinePdf(bytes: *PdfBytes, offsets: []usize, items: usize) !void { + const first_outline_item = 5; + 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 /Outlines 4 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 100 100] /Resources << >> >>\nendobj\n"); + try beginPdfObject(bytes, offsets, 4); + try bytes.print("<< /Type /Outlines /First 5 0 R /Last {d} 0 R /Count {d} >>\nendobj\n", .{ first_outline_item + items - 1, items, }); for (0..items) |ordinal| { const number = first_outline_item + ordinal; - try beginPdfObject(&bytes, gpa, offsets, number); - try bytes.print(gpa, "<< /Title (Section {d}) /Parent 4 0 R /Dest [3 0 R /Fit]", .{ordinal}); + try beginPdfObject(bytes, offsets, number); + try bytes.print("<< /Title (Section {d}) /Parent 4 0 R /Dest [3 0 R /Fit]", .{ordinal}); if (ordinal != 0) - try bytes.print(gpa, " /Prev {d} 0 R", .{number - 1}); + try bytes.print(" /Prev {d} 0 R", .{number - 1}); if (ordinal + 1 < items) - try bytes.print(gpa, " /Next {d} 0 R", .{number + 1}); - try bytes.appendSlice(gpa, " >>\nendobj\n"); + try bytes.print(" /Next {d} 0 R", .{number + 1}); + try bytes.appendSlice(" >>\nendobj\n"); } + try finishGeneratedPdf(bytes, offsets); +} - return finishGeneratedPdf(&bytes, gpa, offsets); +fn makeWideOutlinePdf(gpa: std.mem.Allocator, items: usize) ![]u8 { + if (items == 0) return error.EmptyOutlineFixture; + const object_count = try std.math.add(usize, 5, items); + const offsets = try gpa.alloc(usize, object_count); + defer gpa.free(offsets); + return allocateGeneratedPdf(gpa, offsets, items, emitWideOutlinePdf); } test "PDF outline is a stable owned DFS view with native destinations" { |
