diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3b06d0d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + name: Zig ${{ matrix.zig-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + zig-version: + - 0.15.2 + - 0.16.0 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Zig + uses: mlugg/setup-zig@v2 + with: + version: ${{ matrix.zig-version }} + + - name: Run tests + run: zig build test + + - name: Build native targets + run: zig build + + - name: Build WebAssembly target + run: zig build wasm diff --git a/build.zig b/build.zig index d31ee66d..a47dc9a1 100644 --- a/build.zig +++ b/build.zig @@ -136,6 +136,16 @@ pub fn build(b: *std.Build) void { const run_encoding_unit_tests = b.addRunArtifact(encoding_unit_tests); + const compat_unit_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/compat.zig"), + .target = target, + .optimize = optimize, + }), + }); + + const run_compat_unit_tests = b.addRunArtifact(compat_unit_tests); + const interpreter_unit_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/interpreter.zig"), @@ -173,6 +183,7 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&run_parser_unit_tests.step); test_step.dependOn(&run_xref_unit_tests.step); test_step.dependOn(&run_encoding_unit_tests.step); + test_step.dependOn(&run_compat_unit_tests.step); test_step.dependOn(&run_interpreter_unit_tests.step); test_step.dependOn(&run_testpdf_unit_tests.step); test_step.dependOn(&run_integration_tests.step); diff --git a/gen_test.zig b/gen_test.zig index 7625e3c4..f0325407 100644 --- a/gen_test.zig +++ b/gen_test.zig @@ -1,17 +1,24 @@ const std = @import("std"); +const compat = @import("src/compat.zig"); const testpdf = @import("src/testpdf.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = compat.generalPurposeAllocator(); defer _ = gpa.deinit(); const allocator = gpa.allocator(); + // Zig 0.16 file I/O requires an Io implementation. Keep this local to the + // standalone generator instead of adopting the richer process.Init entrypoint. + var threaded: if (@hasDecl(std.process, "Init")) std.Io.Threaded else void = if (@hasDecl(std.process, "Init")) .init(allocator, .{}) else {}; + defer if (@hasDecl(std.process, "Init")) threaded.deinit(); + if (@hasDecl(std.process, "Init")) compat.setIo(threaded.io()); + const pdf_data = try testpdf.generateMinimalPdf(allocator, "Hello from zpdf!"); defer allocator.free(pdf_data); - const file = try std.fs.cwd().createFile("test.pdf", .{}); - defer file.close(); - try file.writeAll(pdf_data); + const file = try compat.createFileCwd("test.pdf"); + defer compat.closeFile(file); + try compat.writeAllFile(file, pdf_data); std.debug.print("Generated test.pdf ({} bytes)\n", .{pdf_data.len}); } diff --git a/src/bench.zig b/src/bench.zig index e6617adb..30214837 100644 --- a/src/bench.zig +++ b/src/bench.zig @@ -4,19 +4,15 @@ //! Run with: zig build bench -- path/to/test.pdf const std = @import("std"); +const compat = @import("compat.zig"); const zpdf = @import("root.zig"); const WARMUP_RUNS = 2; const BENCH_RUNS = 5; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); +pub const main = compat.MainWithArgs(mainInner).main; +fn mainInner(allocator: std.mem.Allocator, args: []const []const u8) !void { if (args.len < 2) { std.debug.print( \\ZPDF Benchmark Suite @@ -48,13 +44,11 @@ pub fn main() !void { \\ , .{pdf_path}); - // Get file size - const file = std.fs.cwd().openFile(pdf_path, .{}) catch |err| { + // Get file size. + const file_size = compat.fileSizeCwd(pdf_path) catch |err| { std.debug.print("Error opening file: {}\n", .{err}); return; }; - const file_size = (try file.stat()).size; - file.close(); std.debug.print("Size: {d:.2} MB\n\n", .{@as(f64, @floatFromInt(file_size)) / (1024 * 1024)}); @@ -65,7 +59,7 @@ pub fn main() !void { var page_count: usize = 0; for (×) |*t| { - const start = std.time.nanoTimestamp(); + const start = compat.nanoTimestamp(); const doc = zpdf.Document.open(allocator, pdf_path) catch |err| { std.debug.print("ZPDF error: {}\n", .{err}); @@ -80,7 +74,7 @@ pub fn main() !void { doc.close(); - const end = std.time.nanoTimestamp(); + const end = compat.nanoTimestamp(); t.* = end - start; } @@ -131,13 +125,9 @@ const CharCounter = struct { }; fn benchMutool(allocator: std.mem.Allocator, pdf_path: []const u8) !f64 { - const start = std.time.nanoTimestamp(); - - var child = std.process.Child.init(&.{ "mutool", "draw", "-F", "txt", "-o", "/dev/null", pdf_path }, allocator); - child.stderr_behavior = .Ignore; - child.stdout_behavior = .Ignore; + const start = compat.nanoTimestamp(); - _ = try child.spawnAndWait(); + _ = try compat.runIgnored(&.{ "mutool", "draw", "-F", "txt", "-o", "/dev/null", pdf_path }, allocator); - return @floatFromInt(std.time.nanoTimestamp() - start); + return @floatFromInt(compat.nanoTimestamp() - start); } diff --git a/src/capi.zig b/src/capi.zig index 954874af..1f56967b 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const compat = @import("compat.zig"); const builtin = @import("builtin"); const zpdf = @import("root.zig"); @@ -56,7 +57,8 @@ export fn zpdf_extract_page(handle: ?*ZpdfDocument, page_num: c_int, out_len: *u if (page_num < 0) return null; var buffer: std.ArrayList(u8) = .empty; - doc.extractText(@intCast(page_num), buffer.writer(c_allocator)) catch return null; + defer buffer.deinit(c_allocator); + doc.extractText(@intCast(page_num), compat.arrayListWriter(&buffer, c_allocator)) catch return null; const slice = buffer.toOwnedSlice(c_allocator) catch return null; out_len.* = slice.len; diff --git a/src/compat.zig b/src/compat.zig new file mode 100644 index 00000000..80e1f3f7 --- /dev/null +++ b/src/compat.zig @@ -0,0 +1,289 @@ +const std = @import("std"); + +/// Debug/general-purpose allocator type renamed in Zig 0.16. +pub const GeneralPurposeAllocator = if (@hasDecl(std.heap, "GeneralPurposeAllocator")) + std.heap.GeneralPurposeAllocator +else + std.heap.DebugAllocator; + +pub fn generalPurposeAllocator() GeneralPurposeAllocator(.{}) { + return if (comptime @hasDecl(std.heap, "GeneralPurposeAllocator")) + .{} + else + .init; +} + +/// Build a Zig entrypoint that supplies an allocator and argv to `main_fn` on +/// both the pre-0.16 process API and the Zig 0.16 `std.process.Init` API. +pub fn MainWithArgs(comptime main_fn: anytype) type { + return if (@hasDecl(std.process, "Init")) struct { + pub fn main(init: std.process.Init) !void { + setIo(init.io); + const args = try init.minimal.args.toSlice(init.arena.allocator()); + try main_fn(init.gpa, args); + } + } else struct { + pub fn main() !void { + var gpa = generalPurposeAllocator(); + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + try main_fn(allocator, args); + } + }; +} + +/// Compatibility helpers for Zig 0.15.x and 0.16.x. +/// +/// Zig 0.16 removed std.ArrayList(u8).writer(allocator). This small adapter +/// provides the subset of writer behavior used by zpdf while relying only on +/// ArrayList methods that are available in both 0.15 and 0.16. +pub fn arrayListWriter(list: *std.ArrayList(u8), allocator: std.mem.Allocator) ArrayListWriter { + return .{ + .list = list, + .allocator = allocator, + }; +} + +pub const ArrayListWriter = struct { + list: *std.ArrayList(u8), + allocator: std.mem.Allocator, + + pub fn writeAll(self: @This(), bytes: []const u8) !void { + try self.list.appendSlice(self.allocator, bytes); + } + + pub fn writeByte(self: @This(), byte: u8) !void { + try self.list.append(self.allocator, byte); + } + + pub fn print(self: @This(), comptime fmt: []const u8, args: anytype) !void { + try self.list.print(self.allocator, fmt, args); + } +}; + +pub const has_legacy_fs_file = @hasDecl(std.fs, "File"); +pub const File = if (has_legacy_fs_file) std.fs.File else std.Io.File; + +var current_io: if (has_legacy_fs_file) void else ?std.Io = if (has_legacy_fs_file) {} else null; + +pub fn setIo(io_value: anytype) void { + if (comptime !has_legacy_fs_file) current_io = io_value; +} + +fn currentIo() std.Io { + return current_io orelse @panic("std.Io not initialized"); +} + +pub fn stdoutWriter(buffer: []u8) if (has_legacy_fs_file) @TypeOf(std.fs.File.stdout().writer(buffer)) else @TypeOf(std.Io.File.stdout().writer(currentIo(), buffer)) { + return if (comptime has_legacy_fs_file) + std.fs.File.stdout().writer(buffer) + else + std.Io.File.stdout().writer(currentIo(), buffer); +} + +pub fn stderrWriter(buffer: []u8) if (has_legacy_fs_file) @TypeOf(std.fs.File.stderr().writer(buffer)) else @TypeOf(std.Io.File.stderr().writer(currentIo(), buffer)) { + return if (comptime has_legacy_fs_file) + std.fs.File.stderr().writer(buffer) + else + std.Io.File.stderr().writer(currentIo(), buffer); +} + +pub fn writeAllStdout(bytes: []const u8) !void { + if (comptime has_legacy_fs_file) { + try std.fs.File.stdout().writeAll(bytes); + } else { + var buffer: [4096]u8 = undefined; + var w = std.Io.File.stdout().writer(currentIo(), &buffer); + try w.interface.writeAll(bytes); + try w.interface.flush(); + } +} + +pub fn createFileCwd(path: []const u8) !File { + return if (comptime has_legacy_fs_file) + try std.fs.cwd().createFile(path, .{}) + else + try std.Io.Dir.cwd().createFile(currentIo(), path, .{}); +} + +pub fn closeFile(file: File) void { + if (comptime has_legacy_fs_file) + file.close() + else + file.close(currentIo()); +} + +pub fn fileSizeCwd(path: []const u8) !u64 { + if (comptime has_legacy_fs_file) { + const file = try std.fs.cwd().openFile(path, .{}); + defer file.close(); + return (try file.stat()).size; + } else { + const file = try std.Io.Dir.cwd().openFile(currentIo(), path, .{}); + defer file.close(currentIo()); + return (try file.stat(currentIo())).size; + } +} + +pub fn deleteFileCwd(path: []const u8) void { + if (comptime has_legacy_fs_file) { + std.fs.cwd().deleteFile(path) catch {}; + } else { + std.Io.Dir.cwd().deleteFile(currentIo(), path) catch {}; + } +} + +pub fn readFileAllocAlignedCwd(allocator: std.mem.Allocator, path: []const u8, comptime alignment: std.mem.Alignment) ![]align(alignment.toByteUnits()) u8 { + if (comptime has_legacy_fs_file) { + const file = try std.fs.cwd().openFile(path, .{}); + defer file.close(); + + const stat = try file.stat(); + const data = try allocator.alignedAlloc(u8, alignment, stat.size); + errdefer allocator.free(data); + const bytes_read = try file.readAll(data); + if (bytes_read != stat.size) return error.UnexpectedEof; + return data; + } else { + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + + const file = try std.Io.Dir.cwd().openFile(io, path, .{}); + defer file.close(io); + + const stat = try file.stat(io); + const data = try allocator.alignedAlloc(u8, alignment, stat.size); + errdefer allocator.free(data); + const bytes_read = try file.readPositionalAll(io, data, 0); + if (bytes_read != stat.size) return error.UnexpectedEof; + return data; + } +} + +pub fn mmapFileReadOnlyCwd(allocator: std.mem.Allocator, path: []const u8) ![]align(std.heap.page_size_min) u8 { + if (comptime has_legacy_fs_file) { + const file = try std.fs.cwd().openFile(path, .{}); + defer file.close(); + + const stat = try file.stat(); + return std.posix.mmap( + null, + stat.size, + std.posix.PROT.READ, + .{ .TYPE = .PRIVATE }, + file.handle, + 0, + ); + } else { + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + + const file = try std.Io.Dir.cwd().openFile(io, path, .{}); + defer file.close(io); + + const stat = try file.stat(io); + return std.posix.mmap( + null, + stat.size, + .{ .READ = true }, + .{ .TYPE = .PRIVATE }, + file.handle, + 0, + ); + } +} + +pub fn writeAllFile(file: File, bytes: []const u8) !void { + if (comptime has_legacy_fs_file) { + try file.writeAll(bytes); + } else { + var buffer: [4096]u8 = undefined; + var w = file.writer(currentIo(), &buffer); + try w.interface.writeAll(bytes); + try w.interface.flush(); + } +} + +pub fn fileWriter(file: File, buffer: []u8) if (has_legacy_fs_file) @TypeOf(file.writer(buffer)) else @TypeOf(file.writer(currentIo(), buffer)) { + return if (comptime has_legacy_fs_file) + file.writer(buffer) + else + file.writer(currentIo(), buffer); +} + +pub fn nanoTimestamp() i128 { + return if (comptime @hasDecl(std.time, "nanoTimestamp")) + std.time.nanoTimestamp() + else + @intCast(std.Io.Timestamp.now(currentIo(), .awake).nanoseconds); +} + +pub fn runIgnored(argv: []const []const u8, allocator: std.mem.Allocator) !u8 { + if (comptime @hasDecl(std.process.Child, "init")) { + var child = std.process.Child.init(argv, allocator); + child.stderr_behavior = .Ignore; + child.stdout_behavior = .Ignore; + const term = try child.spawnAndWait(); + return switch (term) { + .Exited => |code| code, + else => 255, + }; + } else { + var child = try std.process.spawn(currentIo(), .{ + .argv = argv, + .stdin = .ignore, + .stdout = .ignore, + .stderr = .ignore, + }); + const term = try child.wait(currentIo()); + return switch (term) { + .exited => |code| code, + else => 255, + }; + } +} + +test "compat ArrayList writer" { + var list: std.ArrayList(u8) = .empty; + defer list.deinit(std.testing.allocator); + + var writer = arrayListWriter(&list, std.testing.allocator); + try writer.writeAll("hello"); + try writer.writeByte(' '); + try writer.print("{}", .{123}); + + try std.testing.expectEqualStrings("hello 123", list.items); +} + +test "compat cwd file helpers" { + var threaded: if (has_legacy_fs_file) void else std.Io.Threaded = if (has_legacy_fs_file) {} else .init(std.testing.allocator, .{}); + defer if (!has_legacy_fs_file) threaded.deinit(); + if (!has_legacy_fs_file) setIo(threaded.io()); + + const path = "zpdf-compat-test.tmp"; + deleteFileCwd(path); + defer deleteFileCwd(path); + + const file = try createFileCwd(path); + try writeAllFile(file, "abc123"); + closeFile(file); + + try std.testing.expectEqual(@as(u64, 6), try fileSizeCwd(path)); + const data = try readFileAllocAlignedCwd(std.testing.allocator, path, .fromByteUnits(1)); + defer std.testing.allocator.free(data); + try std.testing.expectEqualStrings("abc123", data); +} + +test "compat nano timestamp returns monotonic-ish value" { + var threaded: if (has_legacy_fs_file) void else std.Io.Threaded = if (has_legacy_fs_file) {} else .init(std.testing.allocator, .{}); + defer if (!has_legacy_fs_file) threaded.deinit(); + if (!has_legacy_fs_file) setIo(threaded.io()); + + _ = nanoTimestamp(); +} diff --git a/src/encoding.zig b/src/encoding.zig index c00b762b..b208bf63 100644 --- a/src/encoding.zig +++ b/src/encoding.zig @@ -11,6 +11,7 @@ //! CID fonts use CIDToGIDMap instead const std = @import("std"); +const compat = @import("compat.zig"); const parser = @import("parser.zig"); const decompress = @import("decompress.zig"); const cff = @import("cff.zig"); @@ -581,7 +582,7 @@ fn parseFontDescriptor(font_dict: Object.Dict, resolve_fn: *const fn (ctx: *cons stream.dict.get("Filter"), stream.dict.get("DecodeParms"), ) catch null; - + if (data) |d| { encoding.cff_data = d; if (cff.CffParser.init(encoding.allocator, d)) |parser_inst| { @@ -1358,7 +1359,7 @@ test "WinAnsi decode ASCII" { var output: std.ArrayList(u8) = .empty; defer output.deinit(std.testing.allocator); - try enc.decode("Hello", output.writer(std.testing.allocator)); + try enc.decode("Hello", compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("Hello", output.items); } @@ -1370,7 +1371,7 @@ test "WinAnsi decode extended" { defer output.deinit(std.testing.allocator); // 0x93 = left double quote, 0x94 = right double quote - try enc.decode(&[_]u8{ 0x93, 'H', 'i', 0x94 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x93, 'H', 'i', 0x94 }, compat.arrayListWriter(&output, std.testing.allocator)); // Should be "Hi" with smart quotes try std.testing.expectEqualStrings("\xe2\x80\x9cHi\xe2\x80\x9d", output.items); @@ -1394,7 +1395,7 @@ test "CID font decode UTF-16BE" { defer output.deinit(std.testing.allocator); // UTF-16BE for "A" (0x0041) - try enc.decode(&[_]u8{ 0x00, 0x41 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x00, 0x41 }, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("A", output.items); } @@ -1409,7 +1410,7 @@ test "CID font decode CJK character" { defer output.deinit(std.testing.allocator); // UTF-16BE for Chinese character "中" (U+4E2D) - try enc.decode(&[_]u8{ 0x4E, 0x2D }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x4E, 0x2D }, compat.arrayListWriter(&output, std.testing.allocator)); // Should output UTF-8 encoding of U+4E2D = 0xE4 0xB8 0xAD try std.testing.expectEqualStrings("中", output.items); } @@ -1435,7 +1436,7 @@ test "CID font with CMap ranges" { defer output.deinit(std.testing.allocator); // Character code 0x0002 should map to 'B' - try enc.decode(&[_]u8{ 0x00, 0x02 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x00, 0x02 }, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("B", output.items); } @@ -1451,7 +1452,7 @@ test "CID font decode surrogate pairs" { // UTF-16BE surrogate pair for U+1F600 (😀) // High surrogate: 0xD83D, Low surrogate: 0xDE00 - try enc.decode(&[_]u8{ 0xD8, 0x3D, 0xDE, 0x00 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0xD8, 0x3D, 0xDE, 0x00 }, compat.arrayListWriter(&output, std.testing.allocator)); // Should output UTF-8 encoding of U+1F600 = 0xF0 0x9F 0x98 0x80 try std.testing.expectEqualStrings("😀", output.items); } @@ -1467,7 +1468,7 @@ test "MacRoman encoding" { defer output.deinit(std.testing.allocator); // 0xCA in MacRoman is a non-breaking space - try enc.decode(&[_]u8{0xCA}, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{0xCA}, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqual(@as(usize, 2), output.items.len); // UTF-8 NBSP is 2 bytes } @@ -1481,7 +1482,7 @@ test "encoding differences array" { var output: std.ArrayList(u8) = .empty; defer output.deinit(std.testing.allocator); - try enc.decode(&[_]u8{65}, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{65}, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("B", output.items); } @@ -1497,7 +1498,7 @@ test "CID identity mapping" { defer output.deinit(std.testing.allocator); // 0x0041 = 'A' in Unicode - try enc.decode(&[_]u8{ 0x00, 0x41 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x00, 0x41 }, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("A", output.items); } @@ -1516,15 +1517,15 @@ test "CMap range mapping" { var output: std.ArrayList(u8) = .empty; defer output.deinit(std.testing.allocator); - try enc.decode(&[_]u8{ 0x01, 0x00 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x01, 0x00 }, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("X", output.items); output.clearRetainingCapacity(); - try enc.decode(&[_]u8{ 0x01, 0x01 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x01, 0x01 }, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("Y", output.items); output.clearRetainingCapacity(); - try enc.decode(&[_]u8{ 0x01, 0x02 }, output.writer(std.testing.allocator)); + try enc.decode(&[_]u8{ 0x01, 0x02 }, compat.arrayListWriter(&output, std.testing.allocator)); try std.testing.expectEqualStrings("Z", output.items); } diff --git a/src/integration_test.zig b/src/integration_test.zig index 5f2d5285..9ef9af68 100644 --- a/src/integration_test.zig +++ b/src/integration_test.zig @@ -3,6 +3,7 @@ //! Tests the full parsing and extraction pipeline using generated PDFs. const std = @import("std"); +const compat = @import("compat.zig"); const zpdf = @import("root.zig"); const testpdf = @import("testpdf.zig"); @@ -31,7 +32,7 @@ test "extract text from minimal PDF" { var output: std.ArrayList(u8) = .empty; defer output.deinit(allocator); - try doc.extractText(0, output.writer(allocator)); + try doc.extractText(0, compat.arrayListWriter(&output, allocator)); // Should contain our test text try std.testing.expect(std.mem.indexOf(u8, output.items, "Test123") != null); @@ -63,7 +64,7 @@ test "extract all text from multi-page PDF" { var output: std.ArrayList(u8) = .empty; defer output.deinit(allocator); - try doc.extractAllText(output.writer(allocator)); + try doc.extractAllText(compat.arrayListWriter(&output, allocator)); try std.testing.expect(std.mem.indexOf(u8, output.items, "PageA") != null); try std.testing.expect(std.mem.indexOf(u8, output.items, "PageB") != null); @@ -81,7 +82,7 @@ test "parse TJ operator PDF" { var output: std.ArrayList(u8) = .empty; defer output.deinit(allocator); - try doc.extractText(0, output.writer(allocator)); + try doc.extractText(0, compat.arrayListWriter(&output, allocator)); // TJ with spacing should produce "Hello World" (with space from -200 adjustment) try std.testing.expect(std.mem.indexOf(u8, output.items, "Hello") != null); @@ -258,7 +259,7 @@ test "extract text from incremental PDF - gets updated content" { var output: std.ArrayList(u8) = .empty; defer output.deinit(allocator); - try doc.extractText(0, output.writer(allocator)); + try doc.extractText(0, compat.arrayListWriter(&output, allocator)); // Should extract "Updated Text" NOT "Original Text" // because incremental update replaced object 4 @@ -280,7 +281,7 @@ test "page tree tolerates leaf node without /Type" { var output: std.ArrayList(u8) = .empty; defer output.deinit(allocator); - try doc.extractText(0, output.writer(allocator)); + try doc.extractText(0, compat.arrayListWriter(&output, allocator)); try std.testing.expect(std.mem.indexOf(u8, output.items, "NoTypeTest") != null); } @@ -295,7 +296,7 @@ test "inline image does not corrupt text extraction" { var output: std.ArrayList(u8) = .empty; defer output.deinit(allocator); - try doc.extractText(0, output.writer(allocator)); + try doc.extractText(0, compat.arrayListWriter(&output, allocator)); // Both text spans surrounding the inline image must be present (Fix 1: BI/EI skip) try std.testing.expect(std.mem.indexOf(u8, output.items, "Before") != null); @@ -987,7 +988,7 @@ test "superscript positioning does not insert spurious newline" { var output: std.ArrayList(u8) = .empty; defer output.deinit(allocator); - try doc.extractText(0, output.writer(allocator)); + try doc.extractText(0, compat.arrayListWriter(&output, allocator)); // All three text chunks must be present try std.testing.expect(std.mem.indexOf(u8, output.items, "Hello") != null); diff --git a/src/main.zig b/src/main.zig index c298c26b..b82e3016 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,16 +7,12 @@ //! Designed to be a drop-in comparison with `mutool draw -F txt` const std = @import("std"); +const compat = @import("compat.zig"); const zpdf = @import("root.zig"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); +pub const main = compat.MainWithArgs(mainInner).main; +fn mainInner(allocator: std.mem.Allocator, args: []const []const u8) !void { if (args.len < 2) { try printUsage(); return; @@ -42,7 +38,7 @@ pub fn main() !void { fn printUsage() !void { var buf: [4096]u8 = undefined; - var bw = std.fs.File.stdout().writer(&buf); + var bw = compat.stdoutWriter(&buf); const stdout = &bw.interface; defer stdout.flush() catch {}; try stdout.writeAll( @@ -162,13 +158,13 @@ fn runExtract(allocator: std.mem.Allocator, args: []const []const u8) !void { // Setup output const output_handle = if (output_file) |out_path| - std.fs.cwd().createFile(out_path, .{}) catch |err| { + compat.createFileCwd(out_path) catch |err| { std.debug.print("Error creating {s}: {}\n", .{ out_path, err }); return; } else null; - defer if (output_handle) |h| h.close(); + defer if (output_handle) |h| compat.closeFile(h); // Parse page range const pages = parsePageRange(allocator, page_range, doc.pages.items.len) catch |err| { @@ -192,12 +188,12 @@ fn runExtract(allocator: std.mem.Allocator, args: []const []const u8) !void { defer allocator.free(result); if (output_handle) |h| { - h.writeAll(result) catch |err| { + compat.writeAllFile(h, result) catch |err| { std.debug.print("Error writing output: {}\n", .{err}); return; }; } else { - std.fs.File.stdout().writeAll(result) catch |err| { + compat.writeAllStdout(result) catch |err| { std.debug.print("Error writing output: {}\n", .{err}); return; }; @@ -211,23 +207,23 @@ fn runExtract(allocator: std.mem.Allocator, args: []const []const u8) !void { defer allocator.free(result); if (output_handle) |h| { - h.writeAll(result) catch |err| { + compat.writeAllFile(h, result) catch |err| { std.debug.print("Error writing output: {}\n", .{err}); return; }; } else { - std.fs.File.stdout().writeAll(result) catch |err| { + compat.writeAllStdout(result) catch |err| { std.debug.print("Error writing output: {}\n", .{err}); return; }; } } else if (output_handle) |h| { - var file_writer = h.writer(&write_buf); + var file_writer = compat.fileWriter(h, &write_buf); const writer = &file_writer.interface; defer writer.flush() catch {}; try doExtract(doc, pages, output_format, extraction_mode, allocator, writer); } else { - var stdout_writer = std.fs.File.stdout().writer(&write_buf); + var stdout_writer = compat.stdoutWriter(&write_buf); const writer = &stdout_writer.interface; defer writer.flush() catch {}; try doExtract(doc, pages, output_format, extraction_mode, allocator, writer); @@ -236,7 +232,7 @@ fn runExtract(allocator: std.mem.Allocator, args: []const []const u8) !void { // Report errors if any if (doc.errors.items.len > 0) { var stderr_buf: [4096]u8 = undefined; - var stderr_bw = std.fs.File.stderr().writer(&stderr_buf); + var stderr_bw = compat.stderrWriter(&stderr_buf); const stderr = &stderr_bw.interface; defer stderr.flush() catch {}; try stderr.print("\nWarning: {} errors encountered during extraction\n", .{doc.errors.items.len}); @@ -384,7 +380,7 @@ fn doExtract(doc: *zpdf.Document, pages: []const usize, output_format: OutputFor if (ii > 0) try writer.writeAll(","); try writer.print("\n {{\"rect\": [{d:.1}, {d:.1}, {d:.1}, {d:.1}], \"width\": {}, \"height\": {}}}", .{ img.rect[0], img.rect[1], img.rect[2], img.rect[3], - img.width, img.height, + img.width, img.height, }); } if (images.len > 0) try writer.writeAll("\n "); @@ -556,7 +552,7 @@ fn runInfo(allocator: std.mem.Allocator, args: []const []const u8) !void { defer doc.close(); var stdout_buf: [4096]u8 = undefined; - var stdout_bw = std.fs.File.stdout().writer(&stdout_buf); + var stdout_bw = compat.stdoutWriter(&stdout_buf); const stdout = &stdout_bw.interface; defer stdout.flush() catch {}; @@ -670,7 +666,7 @@ fn runSearch(allocator: std.mem.Allocator, args: []const []const u8) !void { defer zpdf.Document.freeSearchResults(allocator, results); var stdout_buf: [4096]u8 = undefined; - var stdout_bw = std.fs.File.stdout().writer(&stdout_buf); + var stdout_bw = compat.stdoutWriter(&stdout_buf); const stdout = &stdout_bw.interface; defer stdout.flush() catch {}; @@ -705,7 +701,7 @@ fn runBench(allocator: std.mem.Allocator, args: []const []const u8) !void { const parallel = args.len > 1 and std.mem.eql(u8, args[1], "--parallel"); var stdout_buf: [4096]u8 = undefined; - var stdout_bw = std.fs.File.stdout().writer(&stdout_buf); + var stdout_bw = compat.stdoutWriter(&stdout_buf); const stdout = &stdout_bw.interface; defer stdout.flush() catch {}; @@ -716,7 +712,7 @@ fn runBench(allocator: std.mem.Allocator, args: []const []const u8) !void { var page_count: usize = 0; for (×) |*t| { - const start = std.time.nanoTimestamp(); + const start = compat.nanoTimestamp(); const doc = zpdf.Document.open(allocator, path) catch |err| { std.debug.print("Error opening {s}: {}\n", .{ path, err }); @@ -740,7 +736,7 @@ fn runBench(allocator: std.mem.Allocator, args: []const []const u8) !void { doc.close(); - const end = std.time.nanoTimestamp(); + const end = compat.nanoTimestamp(); t.* = @intCast(end - start); } @@ -766,18 +762,14 @@ fn runBench(allocator: std.mem.Allocator, args: []const []const u8) !void { // Try to run mutool for comparison try stdout.writeAll("\nAttempting mutool comparison...\n"); - var child = std.process.Child.init(&.{ "mutool", "convert", "-F", "text", "-o", "/dev/null", path }, allocator); - child.stderr_behavior = .Ignore; - child.stdout_behavior = .Ignore; - - const mutool_start = std.time.nanoTimestamp(); - const term = child.spawnAndWait() catch { + const mutool_start = compat.nanoTimestamp(); + const mutool_exit_code = compat.runIgnored(&.{ "mutool", "convert", "-F", "text", "-o", "/dev/null", path }, allocator) catch { try stdout.writeAll(" mutool not found or failed\n"); return; }; - const mutool_end = std.time.nanoTimestamp(); + const mutool_end = compat.nanoTimestamp(); - if (term.Exited == 0) { + if (mutool_exit_code == 0) { const mutool_ms = @as(f64, @floatFromInt(mutool_end - mutool_start)) / 1_000_000.0; try stdout.print(" MuPDF: {d:.2} ms\n", .{mutool_ms}); try stdout.print(" Speedup: {d:.2}x\n", .{mutool_ms / mean_ms}); diff --git a/src/root.zig b/src/root.zig index cdbadc43..e8db2a93 100644 --- a/src/root.zig +++ b/src/root.zig @@ -10,6 +10,7 @@ //! 5. Explicit error budgets - caller controls tolerance const std = @import("std"); +const compat = @import("compat.zig"); const builtin = @import("builtin"); const native_os = builtin.os.tag; @@ -148,31 +149,15 @@ pub const Document = struct { @compileError("File I/O is not available on WASM. Use openFromMemory instead."); } - const file = try std.fs.cwd().openFile(path, .{}); - defer file.close(); - - const stat = try file.stat(); - const size = stat.size; - if (comptime is_windows) { - // Windows: read file into allocated memory (no mmap support) - const data = try allocator.alignedAlloc(u8, .fromByteUnits(std.heap.page_size_min), size); - errdefer allocator.free(data); - const bytes_read = try file.readAll(data); - if (bytes_read != size) { - return error.UnexpectedEof; - } + const data = try compat.readFileAllocAlignedCwd( + allocator, + path, + .fromByteUnits(std.heap.page_size_min), + ); return openFromMemoryOwnedAlloc(allocator, data, config); } else { - // POSIX: memory map the file - const data = try std.posix.mmap( - null, - size, - std.posix.PROT.READ, - .{ .TYPE = .PRIVATE }, - file.handle, - 0, - ); + const data = try compat.mmapFileReadOnlyCwd(allocator, path); return openFromMemoryOwned(allocator, data, config); } } @@ -778,7 +763,7 @@ pub const Document = struct { const content = pagetree.getPageContents(parse_allocator, scratch_allocator, self.data, &self.xref_table, page, &self.object_cache) catch return output.toOwnedSlice(allocator); self.ensurePageFonts(page_num); - try extractTextFromContent(scratch_allocator, content, page_num, &self.font_cache, output.writer(allocator)); + try extractTextFromContent(scratch_allocator, content, page_num, &self.font_cache, compat.arrayListWriter(&output, allocator)); return output.toOwnedSlice(allocator); } @@ -863,7 +848,7 @@ pub const Document = struct { if (content.len == 0) continue; self.ensurePageFonts(page_num); - try extractTextFromContent(scratch_allocator, content, page_num, &self.font_cache, result.writer(allocator)); + try extractTextFromContent(scratch_allocator, content, page_num, &self.font_cache, compat.arrayListWriter(&result, allocator)); } } @@ -1117,7 +1102,7 @@ pub const Document = struct { if (s.len > 0) switch (s[0]) { 'D' => { // Decimal - buf.writer(allocator).print("{}", .{page_number}) catch return null; + compat.arrayListWriter(&buf, allocator).print("{}", .{page_number}) catch return null; }, 'r' => { // Lowercase roman @@ -1136,7 +1121,7 @@ pub const Document = struct { formatAlpha(&buf, allocator, page_number, true) catch return null; }, else => { - buf.writer(allocator).print("{}", .{page_number}) catch return null; + compat.arrayListWriter(&buf, allocator).print("{}", .{page_number}) catch return null; }, }; } @@ -1144,7 +1129,7 @@ pub const Document = struct { if (buf.items.len == 0) { // No style, just return prefix or page number if (prefix == null) { - buf.writer(allocator).print("{}", .{page_idx + 1}) catch return null; + compat.arrayListWriter(&buf, allocator).print("{}", .{page_idx + 1}) catch return null; } } @@ -1153,7 +1138,7 @@ pub const Document = struct { fn formatRoman(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, number: usize, upper: bool) !void { if (number == 0 or number > 3999) { - try buf.writer(allocator).print("{}", .{number}); + try compat.arrayListWriter(buf, allocator).print("{}", .{number}); return; } const values = [_]struct { v: u16, s_upper: []const u8, s_lower: []const u8 }{ @@ -1182,7 +1167,7 @@ pub const Document = struct { fn formatAlpha(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, number: usize, upper: bool) !void { if (number == 0) { - try buf.writer(allocator).print("{}", .{number}); + try compat.arrayListWriter(buf, allocator).print("{}", .{number}); return; } // a=1, b=2, ..., z=26, aa=27, ab=28, ... @@ -2477,7 +2462,7 @@ pub fn extractTextFromFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 var output: std.ArrayList(u8) = .empty; errdefer output.deinit(allocator); - try doc.extractAllText(output.writer(allocator)); + try doc.extractAllText(compat.arrayListWriter(&output, allocator)); return output.toOwnedSlice(allocator); } @@ -2490,7 +2475,7 @@ pub fn extractTextFromMemory(allocator: std.mem.Allocator, data: []const u8) ![] var output: std.ArrayList(u8) = .empty; errdefer output.deinit(allocator); - try doc.extractAllText(output.writer(allocator)); + try doc.extractAllText(compat.arrayListWriter(&output, allocator)); return output.toOwnedSlice(allocator); } diff --git a/src/testpdf.zig b/src/testpdf.zig index 78c3213c..ef434973 100644 --- a/src/testpdf.zig +++ b/src/testpdf.zig @@ -4,13 +4,14 @@ //! These are hand-crafted PDFs that exercise specific features. const std = @import("std"); +const compat = @import("compat.zig"); /// Generate a minimal PDF with plain text pub fn generateMinimalPdf(allocator: std.mem.Allocator, text: []const u8) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); // Header try writer.writeAll("%PDF-1.4\n"); @@ -41,7 +42,7 @@ pub fn generateMinimalPdf(allocator: std.mem.Allocator, text: []const u8) ![]u8 // Build content stream var content: std.ArrayList(u8) = .empty; defer content.deinit(allocator); - var cw = content.writer(allocator); + var cw = compat.arrayListWriter(&content, allocator); try cw.writeAll("BT\n"); try cw.writeAll("/F1 12 Tf\n"); @@ -85,7 +86,7 @@ pub fn generateMultiPagePdf(allocator: std.mem.Allocator, pages_text: []const [] var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); var offsets: std.ArrayList(u64) = .empty; defer offsets.deinit(allocator); @@ -130,7 +131,7 @@ pub fn generateMultiPagePdf(allocator: std.mem.Allocator, pages_text: []const [] // Content stream var content: std.ArrayList(u8) = .empty; defer content.deinit(allocator); - var cw = content.writer(allocator); + var cw = compat.arrayListWriter(&content, allocator); try cw.writeAll("BT\n/F1 12 Tf\n100 700 Td\n"); try cw.print("({s}) Tj\n", .{text}); try cw.writeAll("ET\n"); @@ -166,7 +167,7 @@ pub fn generateTJPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -205,7 +206,7 @@ pub fn generateCIDFontPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -298,7 +299,7 @@ pub fn generateCIDFontPdf(allocator: std.mem.Allocator) ![]u8 { pub fn generatePdfWithoutPageType(allocator: std.mem.Allocator, text: []const u8) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -317,7 +318,7 @@ pub fn generatePdfWithoutPageType(allocator: std.mem.Allocator, text: []const u8 var content: std.ArrayList(u8) = .empty; defer content.deinit(allocator); - var cw = content.writer(allocator); + var cw = compat.arrayListWriter(&content, allocator); try cw.writeAll("BT\n/F1 12 Tf\n100 700 Td\n"); try cw.print("({s}) Tj\n", .{text}); try cw.writeAll("ET\n"); @@ -346,7 +347,7 @@ pub fn generatePdfWithoutPageType(allocator: std.mem.Allocator, text: []const u8 pub fn generateInlineImagePdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -393,7 +394,7 @@ pub fn generateInlineImagePdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateSuperscriptPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -487,7 +488,7 @@ pub fn generateIncrementalPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); // ===== ORIGINAL PDF ===== try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -554,7 +555,7 @@ pub fn generateEncryptedPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -638,7 +639,7 @@ test "generate incremental PDF" { pub fn generateMetadataPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -686,7 +687,7 @@ pub fn generateMetadataPdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateOutlinePdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -756,7 +757,7 @@ pub fn generateOutlinePdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateLinkPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -805,7 +806,7 @@ pub fn generateLinkPdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateFormFieldPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -860,7 +861,7 @@ pub fn generateFormFieldPdf(allocator: std.mem.Allocator) ![]u8 { pub fn generatePageLabelPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -961,7 +962,7 @@ test "generate page label PDF" { pub fn generateNestedOutlinePdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -1038,7 +1039,7 @@ pub fn generateNestedOutlinePdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateMultiLinkPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -1098,7 +1099,7 @@ pub fn generateMultiLinkPdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateAllFormFieldsPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -1162,7 +1163,7 @@ pub fn generateAllFormFieldsPdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateExtendedPageLabelPdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -1198,7 +1199,7 @@ pub fn generateExtendedPageLabelPdf(allocator: std.mem.Allocator) ![]u8 { var content: std.ArrayList(u8) = .empty; defer content.deinit(allocator); - var cw = content.writer(allocator); + var cw = compat.arrayListWriter(&content, allocator); try cw.writeAll("BT\n/F1 12 Tf\n100 700 Td\n"); try cw.print("({s}) Tj\n", .{page_texts[pg]}); try cw.writeAll("ET\n"); @@ -1236,7 +1237,7 @@ pub fn generateExtendedPageLabelPdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateImagePdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); @@ -1290,7 +1291,7 @@ pub fn generateImagePdf(allocator: std.mem.Allocator) ![]u8 { pub fn generateUtf16BePdf(allocator: std.mem.Allocator) ![]u8 { var pdf: std.ArrayList(u8) = .empty; errdefer pdf.deinit(allocator); - var writer = pdf.writer(allocator); + var writer = compat.arrayListWriter(&pdf, allocator); try writer.writeAll("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"); diff --git a/src/wapi.zig b/src/wapi.zig index 6dec209b..acc6bf27 100644 --- a/src/wapi.zig +++ b/src/wapi.zig @@ -4,6 +4,7 @@ //! Uses memory-based loading since file I/O is not available in WASM. const std = @import("std"); +const compat = @import("compat.zig"); const zpdf = @import("root.zig"); // Use WASM page allocator @@ -86,7 +87,8 @@ export fn zpdf_extract_page(handle: i32, page_num: i32, out_len: *usize) ?[*]u8 if (documents[idx]) |doc| { var buffer: std.ArrayList(u8) = .empty; - doc.extractText(@intCast(page_num), buffer.writer(wasm_allocator)) catch return null; + defer buffer.deinit(wasm_allocator); + doc.extractText(@intCast(page_num), compat.arrayListWriter(&buffer, wasm_allocator)) catch return null; // Handle empty buffer - toOwnedSlice returns undefined ptr for empty slice if (buffer.items.len == 0) {