lib/pdf/src/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pdf = @import("root.zig");
3
4 const FilterStorage = pdf.FilterStorage;
5 const extractTextAlloc = pdf.extractTextAlloc;
6 const testing = @import("testing.zig");
7
8 test "browser-adjacent: pdf package namespace" {
9 try std.testing.expect(!@hasDecl(pdf, "testing"));
10 std.testing.refAllDecls(pdf);
11 _ = @import("document.zig");
12 _ = @import("filter/test.zig");
13 }
14
15 fn extractTestAlloc(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
16 var storage = try FilterStorage.init(allocator, .{ .bounds = .{
17 .input_bytes = bytes.len,
18 .decoded_bytes = bytes.len,
19 } });
20 defer storage.deinit(allocator);
21 storage.activate();
22 return extractTextAlloc(allocator, &storage, bytes);
23 }
24
25 test "PDF root composes caller-selected stream filter storage" {
26 comptime {
27 @stardustClaim(
28 @import("alloc_phase").capacity.witness(@import("./filter/root.zig").Storage, "pdf_filter_root"),
29 null,
30 null,
31 null,
32 null,
33 null,
34 null,
35 );
36 }
37
38 const fixture = try testing.buildFixtureAlloc(std.testing.allocator, "BT /F1 12 Tf 72 700 Td (Hello, PDF world!) Tj ET", .{});
39 defer std.testing.allocator.free(fixture);
40 var storage = try FilterStorage.init(std.testing.allocator, .{ .bounds = .{
41 .input_bytes = fixture.len,
42 .decoded_bytes = fixture.len,
43 } });
44 defer storage.deinit(std.testing.allocator);
45 storage.activate();
46 const text = try extractTextAlloc(std.testing.allocator, &storage, fixture);
47 defer std.testing.allocator.free(text);
48 try std.testing.expectEqualStrings("Hello, PDF world!\n", text);
49 try std.testing.expect(!storage.status().in_use);
50 }
51
52 test "pdf extracts text from a flate compressed stream" {
53 const fixture = try testing.buildFixtureAlloc(std.testing.allocator, "BT (Compressed ) Tj (payload) Tj ET", .{ .compress = true });
54 defer std.testing.allocator.free(fixture);
55 const text = try extractTestAlloc(std.testing.allocator, fixture);
56 defer std.testing.allocator.free(text);
57 try std.testing.expectEqualStrings("Compressed payload\n", text);
58 }
59
60 test "pdf walks multi page trees in order" {
61 const fixture = try testing.buildFixtureAlloc(
62 std.testing.allocator,
63 "BT (First page.) Tj ET",
64 .{ .second_page = "BT (Second page.) Tj ET" },
65 );
66 defer std.testing.allocator.free(fixture);
67 const text = try extractTestAlloc(std.testing.allocator, fixture);
68 defer std.testing.allocator.free(text);
69 try std.testing.expectEqualStrings("First page.\nSecond page.\n", text);
70 }
71
72 test "pdf maps ligature codes through tounicode cmaps and inherited resources" {
73 const cmap_body =
74 "/CIDInit /ProcSet findresource begin\nbegincmap\n" ++
75 "1 begincodespacerange\n<00> <FF>\nendcodespacerange\n" ++
76 "1 beginbfrange\n<20> <7E> <0020>\nendbfrange\n" ++
77 "1 beginbfchar\n<02> <00660069>\nendbfchar\nendcmap\nend\n";
78 const fixture = try testing.buildFixtureAlloc(
79 std.testing.allocator,
80 "BT /F1 12 Tf (Arti\\002cial intelligence) Tj ET",
81 .{ .to_unicode = cmap_body },
82 );
83 defer std.testing.allocator.free(fixture);
84 const text = try extractTestAlloc(std.testing.allocator, fixture);
85 defer std.testing.allocator.free(text);
86 try std.testing.expectEqualStrings("Artificial intelligence\n", text);
87 }
88
89 test "pdf reports malformed xref streams as typed errors" {
90 const body = "%PDF-1.5\n7 0 obj\n<< /Type /XRef >>\nstream\nendstream\nendobj\nstartxref\n9\n%%EOF";
91 try std.testing.expectError(error.BadXrefStream, extractTestAlloc(std.testing.allocator, body));
92 }
93
94 fn upFilterAlloc(allocator: std.mem.Allocator, raw: []const u8, row_len: usize) ![]u8 {
95 var out = std.ArrayList(u8).empty;
96 errdefer out.deinit(allocator);
97 var row_start: usize = 0;
98 while (row_start < raw.len) : (row_start += row_len) {
99 try out.append(allocator, 2);
100 var index: usize = 0;
101 while (index < row_len) : (index += 1) {
102 const above: u8 = if (row_start == 0) 0 else raw[row_start - row_len + index];
103 try out.append(allocator, raw[row_start + index] -% above);
104 }
105 }
106 return out.toOwnedSlice(allocator);
107 }
108
109 fn buildXrefStreamFixtureAlloc(allocator: std.mem.Allocator, page_content: []const u8) ![]u8 {
110 var out = std.ArrayList(u8).empty;
111 errdefer out.deinit(allocator);
112 try out.appendSlice(allocator, "%PDF-1.5\n");
113
114 const content_offset = out.items.len;
115 const content_head = try std.fmt.allocPrint(allocator, "4 0 obj\n<< /Length {d} >>\nstream\n", .{page_content.len});
116 defer allocator.free(content_head);
117 try out.appendSlice(allocator, content_head);
118 try out.appendSlice(allocator, page_content);
119 try out.appendSlice(allocator, "\nendstream\nendobj\n");
120
121 var inner = std.ArrayList(u8).empty;
122 defer inner.deinit(allocator);
123 var header = std.ArrayList(u8).empty;
124 defer header.deinit(allocator);
125 const bodies = [_][]const u8{
126 "<< /Type /Catalog /Pages 2 0 R >>",
127 "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
128 "<< /Type /Page /Parent 2 0 R /Contents 4 0 R >>",
129 };
130 for (bodies, 1..) |body, number| {
131 const line = try std.fmt.allocPrint(allocator, "{d} {d} ", .{ number, inner.items.len });
132 defer allocator.free(line);
133 try header.appendSlice(allocator, line);
134 try inner.appendSlice(allocator, body);
135 try inner.append(allocator, '\n');
136 }
137 const first = header.items.len;
138 try header.appendSlice(allocator, inner.items);
139 const container_data = try testing.deflateAlloc(allocator, header.items);
140 defer allocator.free(container_data);
141 const container_offset = out.items.len;
142 const container_head = try std.fmt.allocPrint(
143 allocator,
144 "5 0 obj\n<< /Type /ObjStm /N 3 /First {d} /Length {d} /Filter /FlateDecode >>\nstream\n",
145 .{ first, container_data.len },
146 );
147 defer allocator.free(container_head);
148 try out.appendSlice(allocator, container_head);
149 try out.appendSlice(allocator, container_data);
150 try out.appendSlice(allocator, "\nendstream\nendobj\n");
151
152 const xref_offset = out.items.len;
153 const Entry = struct { kind: u8, second: u32, third: u8 };
154 const entries = [_]Entry{
155 .{ .kind = 0, .second = 0, .third = 0 },
156 .{ .kind = 2, .second = 5, .third = 0 },
157 .{ .kind = 2, .second = 5, .third = 1 },
158 .{ .kind = 2, .second = 5, .third = 2 },
159 .{ .kind = 1, .second = @intCast(content_offset), .third = 0 },
160 .{ .kind = 1, .second = @intCast(container_offset), .third = 0 },
161 .{ .kind = 1, .second = @intCast(xref_offset), .third = 0 },
162 };
163 var raw_rows = std.ArrayList(u8).empty;
164 defer raw_rows.deinit(allocator);
165 for (entries) |entry| {
166 try raw_rows.append(allocator, entry.kind);
167 try raw_rows.append(allocator, @intCast((entry.second >> 16) & 0xFF));
168 try raw_rows.append(allocator, @intCast((entry.second >> 8) & 0xFF));
169 try raw_rows.append(allocator, @intCast(entry.second & 0xFF));
170 try raw_rows.append(allocator, entry.third);
171 }
172 const filtered = try upFilterAlloc(allocator, raw_rows.items, 5);
173 defer allocator.free(filtered);
174 const deflated = try testing.deflateAlloc(allocator, filtered);
175 defer allocator.free(deflated);
176 const xref_head = try std.fmt.allocPrint(
177 allocator,
178 "6 0 obj\n<< /Type /XRef /Size 7 /W [1 3 1] /Root 1 0 R /Length {d} /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 5 >> >>\nstream\n",
179 .{deflated.len},
180 );
181 defer allocator.free(xref_head);
182 try out.appendSlice(allocator, xref_head);
183 try out.appendSlice(allocator, deflated);
184 const tail = try std.fmt.allocPrint(allocator, "\nendstream\nendobj\nstartxref\n{d}\n%%EOF", .{xref_offset});
185 defer allocator.free(tail);
186 try out.appendSlice(allocator, tail);
187 return out.toOwnedSlice(allocator);
188 }
189
190 test "pdf extracts text through xref and object streams with png predictors" {
191 const fixture = try buildXrefStreamFixtureAlloc(std.testing.allocator, "BT (From xref stream fixture!) Tj ET");
192 defer std.testing.allocator.free(fixture);
193 const text = try extractTestAlloc(std.testing.allocator, fixture);
194 defer std.testing.allocator.free(text);
195 try std.testing.expectEqualStrings("From xref stream fixture!\n", text);
196 }