lib/pdf/src/document.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_arena = @import("alloc_arena");
3
4 const cmap = @import("cmap.zig");
5 const content = @import("content.zig");
6 const filter = @import("filter/root.zig");
7 const font = @import("font.zig");
8 const object = @import("object.zig");
9 const xref = @import("xref.zig");
10
11 const DocumentLocalError = error{
12 UnknownObject,
13 NotADictionary,
14 NotAStream,
15 MissingPages,
16 BadStreamLength,
17 BadObjectStream,
18 ResolveDepth,
19 };
20
21 pub const DocumentError = DocumentLocalError || xref.XrefError;
22
23 pub const Stream = struct {
24 dict: object.Dict,
25 data: []const u8,
26 };
27
28 pub const Page = struct {
29 contents: []const object.Reference,
30 resources: ?object.Dict,
31 };
32
33 const ContainerMember = struct {
34 number: u32,
35 offset: usize,
36 };
37
38 const Container = struct {
39 data: []const u8,
40 first: usize,
41 members: []const ContainerMember,
42 };
43
44 const ParsedObject = struct {
45 value: object.Value,
46 end: usize,
47 bytes: []const u8,
48 };
49
50 const container_depth_max: u8 = 16;
51
52 pub const Document = struct {
53 document_allocator: std.mem.Allocator,
54 arena_state: alloc_arena.Arena,
55 filter_storage: *filter.Storage,
56 bytes: []const u8,
57 table: xref.Table,
58 containers: std.AutoHashMapUnmanaged(u32, Container),
59 fonts: std.AutoHashMapUnmanaged(u32, *const font.Font),
60
61 pub fn init(
62 document_allocator: std.mem.Allocator,
63 filter_storage: *filter.Storage,
64 bytes: []const u8,
65 ) DocumentError!Document {
66 var arena_state = alloc_arena.Arena.init(document_allocator);
67 errdefer arena_state.deinit();
68 var table = try xref.parse(
69 document_allocator,
70 arena_state.allocator(),
71 filter_storage,
72 bytes,
73 );
74 errdefer table.deinit(document_allocator);
75 return .{
76 .document_allocator = document_allocator,
77 .arena_state = arena_state,
78 .filter_storage = filter_storage,
79 .bytes = bytes,
80 .table = table,
81 .containers = .empty,
82 .fonts = .empty,
83 };
84 }
85
86 pub fn deinit(self: *Document) void {
87 self.fonts.deinit(self.document_allocator);
88 self.containers.deinit(self.document_allocator);
89 self.table.deinit(self.document_allocator);
90 self.arena_state.deinit();
91 }
92
93 pub fn objectValue(self: *Document, reference: object.Reference) DocumentError!object.Value {
94 return ObjectAccess.init(self).objectValue(reference);
95 }
96
97 pub fn objectStream(self: *Document, reference: object.Reference) DocumentError!Stream {
98 return ObjectAccess.init(self).objectStream(reference);
99 }
100
101 pub fn resolve(self: *Document, value: object.Value) DocumentError!object.Value {
102 return ObjectAccess.init(self).resolve(value);
103 }
104
105 pub fn decodeStream(self: *Document, stream: Stream) DocumentError![]u8 {
106 return ObjectAccess.init(self).decodeStream(stream);
107 }
108
109 pub fn pagesAppend(self: *Document, list_allocator: std.mem.Allocator, pages: *std.ArrayList(Page)) DocumentError!void {
110 const catalog = switch (try self.objectValue(self.table.root)) {
111 .dict => |dict| dict,
112 else => return error.NotADictionary,
113 };
114 const pages_value = catalog.get("Pages") orelse return error.MissingPages;
115 const pages_ref = switch (pages_value) {
116 .reference => |reference| reference,
117 else => return error.MissingPages,
118 };
119 try self.walkPages(list_allocator, pages, pages_ref, null, 0);
120 }
121
122 fn walkPages(self: *Document, list_allocator: std.mem.Allocator, pages: *std.ArrayList(Page), reference: object.Reference, inherited: ?object.Dict, depth: usize) DocumentError!void {
123 if (depth > 64) return error.ResolveDepth;
124 const node = switch (try self.objectValue(reference)) {
125 .dict => |dict| dict,
126 else => return error.NotADictionary,
127 };
128 const kind = node.get("Type") orelse return error.NotADictionary;
129 const kind_name = switch (kind) {
130 .name => |name| name,
131 else => return error.NotADictionary,
132 };
133 const resources: ?object.Dict = blk: {
134 const value = node.get("Resources") orelse break :blk inherited;
135 break :blk switch (try self.resolve(value)) {
136 .dict => |dict| dict,
137 else => inherited,
138 };
139 };
140 if (std.mem.eql(u8, kind_name, "Pages")) {
141 const kids = node.get("Kids") orelse return;
142 const items = switch (try self.resolve(kids)) {
143 .array => |array| array,
144 else => return,
145 };
146 for (items) |item| {
147 switch (item) {
148 .reference => |kid| try self.walkPages(list_allocator, pages, kid, resources, depth + 1),
149 else => {},
150 }
151 }
152 return;
153 }
154 if (!std.mem.eql(u8, kind_name, "Page")) return;
155 const arena = self.arena_state.allocator();
156 var refs: std.ArrayList(object.Reference) = .empty;
157 if (node.get("Contents")) |contents| {
158 switch (contents) {
159 .reference => |stream_ref| refs.append(arena, stream_ref) catch return error.OutOfMemory,
160 .array => |items| {
161 for (items) |item| {
162 switch (item) {
163 .reference => |stream_ref| refs.append(arena, stream_ref) catch return error.OutOfMemory,
164 else => {},
165 }
166 }
167 },
168 else => {},
169 }
170 }
171 const owned = refs.toOwnedSlice(arena) catch return error.OutOfMemory;
172 pages.append(list_allocator, .{ .contents = owned, .resources = resources }) catch return error.OutOfMemory;
173 }
174
175 pub fn fontSetAlloc(self: *Document, resources: ?object.Dict) DocumentError!font.Set {
176 const resources_dict = resources orelse return .{};
177 const fonts_value = resources_dict.get("Font") orelse return .{};
178 const fonts_dict = switch (try self.resolve(fonts_value)) {
179 .dict => |dict| dict,
180 else => return .{},
181 };
182 const arena = self.arena_state.allocator();
183 const entries = arena.alloc(font.Entry, fonts_dict.entries.len) catch return error.OutOfMemory;
184 var count: usize = 0;
185 for (fonts_dict.entries) |entry| {
186 switch (entry.value) {
187 .reference => |reference| {
188 entries[count] = .{ .name = entry.key, .font = try self.loadFont(reference) };
189 count += 1;
190 },
191 else => {},
192 }
193 }
194 return .{ .entries = entries[0..count] };
195 }
196
197 fn loadFont(self: *Document, reference: object.Reference) DocumentError!*const font.Font {
198 if (self.fonts.get(reference.number)) |cached| return cached;
199 const arena = self.arena_state.allocator();
200 const created = arena.create(font.Font) catch return error.OutOfMemory;
201 created.* = .{ .map = self.fontMapAlloc(reference) catch |err| switch (err) {
202 error.OutOfMemory => return error.OutOfMemory,
203 else => null,
204 } };
205 self.fonts.put(self.document_allocator, reference.number, created) catch return error.OutOfMemory;
206 return created;
207 }
208
209 fn fontMapAlloc(self: *Document, reference: object.Reference) DocumentError!?cmap.Map {
210 const dict = switch (try self.objectValue(reference)) {
211 .dict => |dict| dict,
212 else => return null,
213 };
214 const to_unicode = dict.get("ToUnicode") orelse return null;
215 const stream_ref = switch (to_unicode) {
216 .reference => |stream_ref| stream_ref,
217 else => return null,
218 };
219 const stream = try self.objectStream(stream_ref);
220 const arena = self.arena_state.allocator();
221 const decoded = try self.decodeStream(stream);
222 defer self.filter_storage.reset();
223 return cmap.parseAlloc(arena, decoded) catch |err| switch (err) {
224 error.OutOfMemory => return error.OutOfMemory,
225 error.BadCMap => null,
226 };
227 }
228 };
229
230 const ObjectAccess = struct {
231 document: *Document,
232 remaining_containers: u8,
233
234 fn init(document: *Document) ObjectAccess {
235 return .{
236 .document = document,
237 .remaining_containers = @intCast(@min(
238 document.bytes.len,
239 @as(usize, container_depth_max),
240 )),
241 };
242 }
243
244 fn nested(self: ObjectAccess) DocumentError!ObjectAccess {
245 std.debug.assert(self.remaining_containers <= container_depth_max);
246 if (self.remaining_containers == 0) return error.ResolveDepth;
247 return .{
248 .document = self.document,
249 .remaining_containers = self.remaining_containers - 1,
250 };
251 }
252
253 fn objectValue(self: ObjectAccess, reference: object.Reference) DocumentError!object.Value {
254 const parsed = try self.parseObject(reference);
255 return parsed.value;
256 }
257
258 fn objectStream(self: ObjectAccess, reference: object.Reference) DocumentError!Stream {
259 const parsed = try self.parseObject(reference);
260 const dict = switch (parsed.value) {
261 .dict => |dict| dict,
262 else => return error.NotAStream,
263 };
264 var parser = object.Parser.init(parsed.bytes, parsed.end);
265 const data_start = parser.streamStart() catch return error.NotAStream;
266 const length_value = dict.get("Length") orelse return error.BadStreamLength;
267 const length: usize = switch (try self.resolve(length_value)) {
268 .integer => |value| if (value >= 0)
269 @intCast(value)
270 else
271 return error.BadStreamLength,
272 else => return error.BadStreamLength,
273 };
274 const data_end = std.math.add(usize, data_start, length) catch
275 return error.BadStreamLength;
276 if (data_end > parsed.bytes.len) return error.BadStreamLength;
277 return .{ .dict = dict, .data = parsed.bytes[data_start..data_end] };
278 }
279
280 fn resolve(self: ObjectAccess, value: object.Value) DocumentError!object.Value {
281 var current = value;
282 var depth: u8 = 0;
283 while (true) {
284 switch (current) {
285 .reference => |reference| {
286 if (depth == 32) return error.ResolveDepth;
287 depth += 1;
288 current = try self.objectValue(reference);
289 },
290 else => return current,
291 }
292 }
293 }
294
295 fn decodeStream(self: ObjectAccess, stream: Stream) DocumentError![]u8 {
296 const filter_name = blk: {
297 const raw = stream.dict.get("Filter") orelse break :blk null;
298 const unwrapped = try filter.single(try self.resolve(raw));
299 break :blk try filter.nameOf(try self.resolve(unwrapped));
300 };
301 const params = blk: {
302 const raw = stream.dict.get("DecodeParms") orelse
303 break :blk filter.Predictor{};
304 const unwrapped = try filter.single(try self.resolve(raw));
305 break :blk try filter.paramsOf(try self.resolve(unwrapped));
306 };
307 return filter.bytesFromStream(
308 self.document.filter_storage,
309 filter_name,
310 params,
311 stream.data,
312 );
313 }
314
315 fn parseObject(
316 self: ObjectAccess,
317 reference: object.Reference,
318 ) DocumentError!ParsedObject {
319 const document = self.document;
320 const location = document.table.locations.get(reference.number) orelse
321 return error.UnknownObject;
322 switch (location) {
323 .offset => |offset| {
324 var parser = object.Parser.init(document.bytes, offset);
325 _ = try parser.parseUnsigned(u32);
326 _ = try parser.parseUnsigned(u16);
327 try parser.expectKeyword("obj");
328 const value = try parser.parseValue(document.arena_state.allocator());
329 return .{ .value = value, .end = parser.pos, .bytes = document.bytes };
330 },
331 .compressed => |compressed| {
332 const container = try self.loadContainer(compressed.container);
333 if (compressed.index >= container.members.len) return error.UnknownObject;
334 const member = container.members[compressed.index];
335 if (member.number != reference.number) return error.BadObjectStream;
336 const start = std.math.add(
337 usize,
338 container.first,
339 member.offset,
340 ) catch return error.BadObjectStream;
341 if (start >= container.data.len) return error.BadObjectStream;
342 var parser = object.Parser.init(container.data, start);
343 const value = try parser.parseValue(document.arena_state.allocator());
344 return .{ .value = value, .end = parser.pos, .bytes = container.data };
345 },
346 }
347 }
348
349 fn loadContainer(self: ObjectAccess, number: u32) DocumentError!*const Container {
350 const document = self.document;
351 if (document.containers.getPtr(number)) |loaded| return loaded;
352 const nested_access = try self.nested();
353 const location = document.table.locations.get(number) orelse
354 return error.UnknownObject;
355 switch (location) {
356 .offset => {},
357 .compressed => return error.BadObjectStream,
358 }
359 const stream = try nested_access.objectStream(.{
360 .number = number,
361 .generation = 0,
362 });
363 const type_value = stream.dict.get("Type") orelse return error.BadObjectStream;
364 switch (type_value) {
365 .name => |name| if (!std.mem.eql(u8, name, "ObjStm"))
366 return error.BadObjectStream,
367 else => return error.BadObjectStream,
368 }
369 const count = try nested_access.unsignedEntry(stream.dict, "N");
370 const first = try nested_access.unsignedEntry(stream.dict, "First");
371 const arena = document.arena_state.allocator();
372 const decoded = try nested_access.decodeStream(stream);
373 defer document.filter_storage.reset();
374 const data = arena.dupe(u8, decoded) catch return error.OutOfMemory;
375 const members = arena.alloc(ContainerMember, count) catch return error.OutOfMemory;
376 var parser = object.Parser.init(data[0..@min(first, data.len)], 0);
377 for (members) |*member| {
378 const member_number = parser.parseUnsigned(u32) catch
379 return error.BadObjectStream;
380 const member_offset = parser.parseUnsigned(usize) catch
381 return error.BadObjectStream;
382 member.* = .{ .number = member_number, .offset = member_offset };
383 }
384 const slot = document.containers.getOrPut(
385 document.document_allocator,
386 number,
387 ) catch return error.OutOfMemory;
388 if (!slot.found_existing) {
389 slot.value_ptr.* = .{ .data = data, .first = first, .members = members };
390 }
391 return slot.value_ptr;
392 }
393
394 fn unsignedEntry(
395 self: ObjectAccess,
396 dict: object.Dict,
397 key: []const u8,
398 ) DocumentError!usize {
399 const value = dict.get(key) orelse return error.BadObjectStream;
400 return switch (try self.resolve(value)) {
401 .integer => |raw| if (raw >= 0)
402 @intCast(raw)
403 else
404 error.BadObjectStream,
405 else => error.BadObjectStream,
406 };
407 }
408 };
409
410 pub fn extractTextAlloc(
411 text_allocator: std.mem.Allocator,
412 filter_storage: *filter.Storage,
413 bytes: []const u8,
414 ) ![]u8 {
415 var document = try Document.init(text_allocator, filter_storage, bytes);
416 defer document.deinit();
417 var pages: std.ArrayList(Page) = .empty;
418 defer pages.deinit(text_allocator);
419 try document.pagesAppend(text_allocator, &pages);
420 var out = std.ArrayList(u8).empty;
421 errdefer out.deinit(text_allocator);
422 for (pages.items) |page| {
423 const fonts = try document.fontSetAlloc(page.resources);
424 for (page.contents) |reference| {
425 const stream = try document.objectStream(reference);
426 {
427 const decoded = try document.decodeStream(stream);
428 defer filter_storage.reset();
429 try content.extractTextAppend(text_allocator, &out, decoded, fonts);
430 }
431 }
432 }
433 return try out.toOwnedSlice(text_allocator);
434 }
435
436 fn checkObjectStreamDepth(container_count: u8, expect_rejection: bool) !void {
437 std.debug.assert(container_count > 0);
438 std.debug.assert(container_count <= container_depth_max + 1);
439 var bytes_buffer: [4096]u8 = undefined;
440 var writer = std.Io.Writer.fixed(&bytes_buffer);
441 var filter_storage = try filter.Storage.init(std.testing.allocator, .{
442 .bounds = .{ .input_bytes = 7, .decoded_bytes = 7 },
443 });
444 defer filter_storage.deinit(std.testing.allocator);
445 filter_storage.activate();
446 var document = Document{
447 .document_allocator = std.testing.allocator,
448 .arena_state = alloc_arena.Arena.init(std.testing.allocator),
449 .filter_storage = &filter_storage,
450 .bytes = &.{},
451 .table = .{ .root = .{ .number = 0, .generation = 0 } },
452 .containers = .empty,
453 .fonts = .empty,
454 };
455 defer document.deinit();
456 const container_total: usize = container_count;
457 for (0..container_depth_max + 1) |index| {
458 if (index == container_total) break;
459 const container_number: u32 = @intCast(index + 1);
460 const member_number: u32 = if (index == 0) 200 else @intCast(99 + index);
461 try document.table.locations.put(
462 std.testing.allocator,
463 container_number,
464 .{ .offset = writer.buffered().len },
465 );
466 try document.table.locations.put(
467 std.testing.allocator,
468 member_number,
469 .{ .compressed = .{ .container = container_number, .index = 0 } },
470 );
471 try writer.print(
472 "{d} 0 obj\n<< /Type /ObjStm /N 1 /First 6 /Length ",
473 .{container_number},
474 );
475 if (index + 1 < container_total) {
476 try writer.print("{d} 0 R", .{@as(u32, 100) + @as(u32, @intCast(index))});
477 } else {
478 try writer.writeByte('7');
479 }
480 try writer.print(
481 " >>\nstream\n{d} 0 7\nendstream\nendobj\n",
482 .{member_number},
483 );
484 }
485 document.bytes = writer.buffered();
486 const reference = object.Reference{ .number = 200, .generation = 0 };
487 if (expect_rejection) {
488 if (document.objectValue(reference)) |_| {
489 return error.TestExpectedError;
490 } else |err| {
491 try std.testing.expectEqual(error.ResolveDepth, err);
492 return;
493 }
494 }
495 const value = try document.objectValue(reference);
496 const integer = switch (value) {
497 .integer => |found| found,
498 else => return error.TestExpectedInteger,
499 };
500 try std.testing.expectEqual(@as(i64, 7), integer);
501 }
502
503 test "PDF object stream depth accepts its bound and rejects max plus one" {
504 try checkObjectStreamDepth(container_depth_max, false);
505 try checkObjectStreamDepth(container_depth_max + 1, true);
506 }