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 }