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 }