diff options
| -rw-r--r-- | build.zig | 8 | ||||
| -rw-r--r-- | mupdf.zig | 56 |
2 files changed, 59 insertions, 5 deletions
@@ -61,6 +61,13 @@ pub fn build(b: *std.Build) void { const dump_path = b.option([]const u8, "dump", "dump .zon embedded into the web shell (-Dplatform=web)"); const is_web = platform == .web; const enable_mupdf = b.option(bool, "mupdf", "native PDF rendering with MuPDF (AGPL/commercial; native default on, web off; -Dmupdf=false disables)") orelse !is_web; + // JPEG 2000, and with it scanned PDFs: a scan is one /JPXDecode image per + // page, so without this MuPDF decodes nothing and every page comes back + // blank. On by default — a viewer that cannot open scans is the more + // surprising default — and a switch at all because it is 31 files of + // third-party C parsing untrusted input. See the OPENJPEG block in + // mupdf.zig. + const enable_jpx = b.option(bool, "jpx", "JPEG 2000 in PDFs, for scanned documents (default on; -Djpx=false drops openjpeg)") orelse true; const is_web_target = target.result.cpu.arch == .wasm32 and target.result.os.tag == .freestanding; // wasm: size is the budget const optimize = if (is_web) .ReleaseSmall else requested_optimize; @@ -362,6 +369,7 @@ pub fn build(b: *std.Build) void { const mupdf = mupdf_build.add(b, io, mupdf_dep, .{ .target = target, .optimize = c_optimize, + .jpx = enable_jpx, }); const mupdf_mod = b.createModule(.{ .target = target, @@ -8,11 +8,17 @@ const std = @import("std"); pub const Result = struct { dependency: *std.Build.Dependency, library: *std.Build.Step.Compile, + /// Whether this archive carries the JPEG 2000 decoder. Remembered rather + /// than re-derived because `linkTo` has to hand consumers the SAME + /// preprocessor view the archive was built with — `FZ_ENABLE_JPX` reaches + /// the public headers, and a consumer that disagrees is an ODR bug that + /// only shows up as a wrong struct layout at runtime. + jpx: bool, /// Give a Zig module the same preprocessor view as the library, expose the /// public C headers to @cImport, and propagate the static link. pub fn linkTo(self: Result, module: *std.Build.Module) void { - configureModule(module, self.dependency); + configureModule(module, self.dependency, self.jpx); // MuPDF's public context headers select the lock-debugging contract // from NDEBUG. Every consumer must therefore agree with the archive, // even when its own Zig optimization/safety mode differs. @@ -24,13 +30,16 @@ pub const Result = struct { pub fn add(b: *std.Build, io: std.Io, dependency: *std.Build.Dependency, options: struct { target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, + /// Compile openjpeg and let MuPDF decode JPEG 2000. See the OPENJPEG + /// block below for why this is a switch rather than always-on. + jpx: bool = true, }) Result { const module = b.createModule(.{ .target = options.target, .optimize = options.optimize, .link_libc = true, }); - configureModule(module, dependency); + configureModule(module, dependency, options.jpx); // MuPDF's upstream release build defines NDEBUG independently of the // optimizer. Without it, an optimized Zig build still enables Fitz's // debug-locking/assertion paths. linkTo applies the same public-header @@ -101,6 +110,40 @@ pub fn add(b: *std.Build, io: std.Io, dependency: *std.Build.Dependency, options }, }); + // --- OPENJPEG --- + // + // The JPEG 2000 decoder, and the reason a scanned PDF is not a blank page: + // a scan is typically one /JPXDecode image per page, so without this MuPDF + // raises "JPX support disabled" for every one of them and hands back a + // page with nothing drawn on it. Everything else still works — the pane + // opens, the page count is right — which makes it look like a renderer bug + // rather than a missing codec. + // + // A switch rather than always-on because it is 31 files of third-party C + // that exists to parse untrusted input, and openjpeg has the CVE history + // to match. Default on: a PDF viewer that cannot open scans is the more + // surprising default, and the ones that matter are exactly the documents + // nobody can retypeset. + // + // Flags are MuPDF's own OPENJPEG_CFLAGS plus OPENJPEG_BUILD_CFLAGS from + // Makelists; the include path is already on the module (configureModule + // adds it unconditionally, because encode-jpx.c wants the header even when + // the codec is off). -fno-sanitize=undefined for the reason source/fitz + // needs it: this is upstream C full of deliberate wrapping arithmetic, and + // ReleaseSafe's trap-mode UBSan would turn a legal shift into a crash. + if (options.jpx) module.addCSourceFiles(.{ + .root = dependency.path(""), + .files = makeSources(b, makelists, "OPENJPEG_SRC"), + .flags = &.{ + "-std=gnu11", + "-fno-sanitize=undefined", + "-DOPJ_STATIC", + "-DOPJ_HAVE_INTTYPES_H", + "-DOPJ_HAVE_STDINT_H", + "-DMUTEX_pthread=0", + }, + }); + const library = b.addLibrary(.{ .name = "mupdf", .linkage = .static, @@ -109,7 +152,7 @@ pub fn add(b: *std.Build, io: std.Io, dependency: *std.Build.Dependency, options if (options.target.result.os.tag != .windows) module.linkSystemLibrary("m", .{}); - return .{ .dependency = dependency, .library = library }; + return .{ .dependency = dependency, .library = library, .jpx = options.jpx }; } /// Add a link-complete smoke test. Unlike merely producing an archive, this @@ -203,7 +246,7 @@ pub fn addProbe(b: *std.Build, result: Result, options: struct { return &run.step; } -fn configureModule(module: *std.Build.Module, dependency: *std.Build.Dependency) void { +fn configureModule(module: *std.Build.Module, dependency: *std.Build.Dependency, jpx: bool) void { module.addIncludePath(dependency.path("include")); module.addIncludePath(dependency.path("source/fitz")); module.addIncludePath(dependency.path("source/pdf")); @@ -236,8 +279,11 @@ fn configureModule(module: *std.Build.Module, dependency: *std.Build.Dependency) .{ "FZ_ENABLE_BARCODE", "0" }, .{ "FZ_ENABLE_BROTLI", "0" }, .{ "FZ_ENABLE_ICC", "0" }, - .{ "FZ_ENABLE_JPX", "0" }, }) |macro| module.addCMacro(macro[0], macro[1]); + // The one codec that is a build option rather than a fixed answer, and it + // reaches the public headers — so consumers get it through linkTo from the + // archive's own `jpx`, never re-derived. See the OPENJPEG block in add(). + module.addCMacro("FZ_ENABLE_JPX", if (jpx) "1" else "0"); module.addCMacro("OCR_DISABLED", "1"); module.addCMacro("TOFU", "1"); module.addCMacro("TOFU_CJK", "1"); |
