Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 11 additions & 4 deletions gen_test.zig
Original file line number Diff line number Diff line change
@@ -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});
}
30 changes: 10 additions & 20 deletions src/bench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)});

Expand All @@ -65,7 +59,7 @@ pub fn main() !void {
var page_count: usize = 0;

for (&times) |*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});
Expand All @@ -80,7 +74,7 @@ pub fn main() !void {

doc.close();

const end = std.time.nanoTimestamp();
const end = compat.nanoTimestamp();
t.* = end - start;
}

Expand Down Expand Up @@ -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);
}
4 changes: 3 additions & 1 deletion src/capi.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const compat = @import("compat.zig");
const builtin = @import("builtin");
const zpdf = @import("root.zig");

Expand Down Expand Up @@ -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;

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buffer is not deinitialized if extractText fails (catch return null). If any bytes were appended before the error, this leaks memory from c_allocator. Add an errdefer buffer.deinit(c_allocator); after initialization (and keep the current toOwnedSlice ownership transfer on success).

Suggested change
var buffer: std.ArrayList(u8) = .empty;
var buffer: std.ArrayList(u8) = .empty;
errdefer buffer.deinit(c_allocator);

Copilot uses AI. Check for mistakes.
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;
Expand Down
Loading
Loading