summaryrefslogtreecommitdiff
path: root/src/pdf.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-09 10:41:33 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a (patch)
tree91fce278abc85e4f7491de8b5f02184c8a2d5a56 /src/pdf.zig
parent2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff)
downloadpardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz
pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip
replace ArrayLists with bounded storage
Diffstat (limited to 'src/pdf.zig')
-rw-r--r--src/pdf.zig388
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" {