OFD (GB/T 33190-2016) backend for GNOME Papers,
built out-of-tree on the rofd-ffi C ABI
(rofd's OFD parser and Cairo renderer).
Ships one shared module and five small data files (the discovery descriptor, the MIME definition, the thumbnailer entry, the association entry and the Sushi plugin); only the module and the descriptor live in Papers' backend directory. No Papers fork, no Papers rebuild, no patching of Papers sources.
Phases 0, 1, 2 and 3 are implemented and tested against Papers 51.1 (tag 51.1;
the build needs only the installed papers-document-4.0 headers) and rofd ABI v1:
the module loads through Papers' own backend discovery, renders pages correctly at
every rotation and zoom step, prints (vector text), reports metadata, serves text,
selection, search, page links and the outline, opens documents from a file
descriptor (the Nautilus previewer's path) and ships as an Arch package on the AUR.
meson test -C _buildruns the unit conversions, the factory round-trip with a deliberate discovery failure, the concurrency case (workers using a document while another thread reloads it), the rotation/zoom/origin render matrix (including the 4–16× budget fit and the oblique-rotation snap), the same-options equivalence against a direct FFI render, a real GTK print-to-file whose PDF text is verified with poppler, the metadata fields and mask bits, the text/layout/find/selection checks and the link/outline model — each compared against a direct rofd query of the same fixture — the load error paths, the fd path against the URI path (page geometry, text, pixels, info, temp-copy lifetime), and (with-Drofd_src=<rofd checkout>) the invoice oracle against rofd's reference render.- Install:
papers-ofdon the AUR (51.1.2-1), built from thev51.1.2tag archive by the AUR recipe, which is maintained in the AUR repository — this repository carries no PKGBUILD. Remaining: the upstream MR (phase 4).
Implementation plan: docs/plan.md — phases, interface contract,
acceptance criteria, verification playbook, risks (§5).
Research notebook (raw evidence with file:line): .planning/ in the working
copy.
| Capability | Notes |
|---|---|
Open *.ofd (double-click, papers file.ofd, gio open) |
local files, by URI or by file descriptor |
| Scroll, zoom, rotate, fit-width/fit-page | full-page Cairo raster from rofd-render |
| Print and print-to-PDF | direct Cairo rendering into the print context, vector text |
Thumbnails (Papers sidebar, Nautilus, papers-thumbnailer) |
reuse of the page renderer |
| Metadata | title, author, subject, keywords, creator, dates, page count |
| Select/copy text, Ctrl+F, caret navigation | rofd's text layout + search; the I-beam cursor comes from the glyph region |
| In-page links and the outline sidebar | GOTO/URI links; the outline model feeds the links sidebar |
| Nautilus previewer (space bar) | Sushi renders documents through Papers' own libraries; data/papers-ofd-sushi.js makes it claim application/ofd, which Papers' own desktop file does not list out of tree |
| Loading from a file descriptor | load_fd: the descriptor is re-entered through /proc/self/fd, or copied into the user cache when /proc cannot be used |
Not supported (rofd has no support for them, and Papers hides the corresponding UI automatically): annotations, signatures/seals, form filling, attachments, layers, transitions, editing/saving.
Papers discovers backends at runtime by scanning
<libdir>/papers/<api>/backends (pkg-config --variable=backenddir papers-document-4.0, currently /usr/lib/papers/6/backends) for
*.papers-backend key files:
[Papers Backend]
Module=ofddocument
TypeDescription=Fixed-layout Documents
MimeType=application/ofd;It loads libofddocument.so, calls the exported pps_backend_query_type() and
gets an OfdDocument — a PpsDocument subclass that also implements
PpsDocumentText, PpsDocumentFind, PpsDocumentLinks, PpsSelection and
PpsDocumentPrint. Each PpsPage carries its own rofd_page_t, and every call is
a thin translation into rofd's C ABI (units, coordinates, options, errors). One
reader/writer lock guards the state the document owns — the rofd handle, the
renderer and the page-geometry cache — with load/load_fd as the writer and
every vfunc as a reader, the model of Papers' PDF backend
(libview/pps-poppler.c). Renders therefore run concurrently, as the ABI's
concurrent read-only contract allows; only a reload excludes them, which is what
keeps a handle from being freed under a call that is using it.
| Dependency | Notes |
|---|---|
Papers ≥ 51, < 52 (papers-document-4.0) |
system pkg-config; backends' ABI version is the papers/6 directory component |
| rofd-ffi (ABI v1, ≥ 0.4.2) | librofd_ffi.so.0 + rofd.h. The floor is the module's own: rofd ≤ 0.4.0 aborts a whole page render on a document object carrying a singular transform, and 0.4.2 turns that into a diagnostic (A19); the AUR package requires ≥ 0.4.3, the fork release that also carries the pkg-config file, the page-size query and the font-resolved text geometry, and the build distinguishes a rofd-ffi.pc that is too old from a missing one. The pkg-config file and the carried changes both come from the fork's own release: YangtseSu/rofd integrates them on its dev branch (one feat/* / fix/* branch and upstream PR per change) and tags releases there (v0.4.3-fork.2 — upstream 0.4.3 plus the page-size query #11, the font-resolved text geometry #13, the image decode budget #14, the region-AA test tolerance #15 and rofd-ffi.pc #12; the first four merged upstream on 2026-10-19, so dev no longer carries them, and the geometry is carried as two branches — #13 for the advance and the box width, the fork's feat/font-text-band for the band and the selection margin. The fork's own rules are its FORK.md), and the AUR librofd-ffi (0.4.3.fork.2-1) builds that tarball, so it installs rofd-ffi.pc and the fork ABI with no patch files (patches/README.md is the inventory). Stock upstream rofd works too, falling back to the slow page-size path and to rofd's size-based text geometry. Do not go below 0.4.2: earlier rofd fails whole pages whose objects carry a singular transform (A19). Delivery policy: plan D11 |
| Papers/rofd sources (optional) | Not needed to build: the module compiles against papers-document-4.0 (headers + libppsdocument-4.0) and rofd.h + librofd_ffi.so.0. $ROFD_SRC is required to carry or re-verify the rofd changes we ship (patches/README.md) and to run rofd's own test suite; a Papers checkout ($PAPERS_SRC) only speeds up grepping — its citations are written against the upstream repository at tag 51.1. scripts/dev-env.sh reports what this machine has (environment → untracked local.conf → siblings ../papers, ../rofd) and flags a same-named directory that is not the expected repository |
| Meson ≥ 1.0, pkg-config, C compiler, GLib/GIO/GTK4/Cairo | pulled in through the Papers dependency |
| Cairo + FreeType/fontconfig, a CJK font for CJK documents | rofd resolves glyphs through system fonts; its defaults name Noto Sans CJK SC |
rofd-ffi sets the SONAME librofd_ffi.so.0. A plain cargo build -p rofd-ffi
tree needs a librofd_ffi.so.0 symlink next to librofd_ffi.so, otherwise
Papers' g_module_open fails with "Failed to load backend for …".
# 0. resolve dependencies for this machine (see Requirements)
. scripts/dev-env.sh
# 1. rofd ABI library — only when you have the checkout; the installed
# librofd-ffi package works too, and step 2 then uses its header/library
if [ -d "$ROFD_SRC" ]; then
(cd "$ROFD_SRC" && cargo build --release --offline -p rofd-ffi)
fi
if [ "$ROFD_ABI_SOURCE" = checkout ]; then
ln -sf librofd_ffi.so "$ROFD_LIBDIR/librofd_ffi.so.0"
fi
# 2. this module (-Drofd_src=<checkout> also builds the invoice oracle test;
# -Drofd_page_size=disabled builds the per-page fallback for latency runs)
meson setup _build \
-Drofd_libdir="$ROFD_LIBDIR" \
-Drofd_includedir="$ROFD_INCLUDEDIR" \
-Drofd_src="$ROFD_SRC"
meson compile -C _build
meson test -C _build
# 3. dev backend directory: the whole system backend directory first — modules and
# descriptors live together, so descriptors alone break PDF/DjVu/TIFF — then ours
mkdir -p _build/devbackends
cp -a "$(pkg-config --variable=backenddir papers-document-4.0)/." _build/devbackends/
cp _build/src/libofddocument.so _build/data/*.papers-backend _build/devbackends/
# 4. application/ofd comes from our own MIME file; stage it without installing
mkdir -p _build/xdg/mime/packages
cp data/papers-ofd.xml _build/xdg/mime/packages/
update-mime-database _build/xdg/mime
export PPS_BACKENDS_DIR=$PWD/_build/devbackends
export XDG_DATA_DIRS=$PWD/_build/xdg:/usr/share
papers "$ROFD_SRC/learning/test.ofd"
papers-thumbnailer -s 256 "$ROFD_SRC/learning/test.ofd" /tmp/ofd.png # thumbnails
# the previewer's invocation (Nautilus passes --fd plus the sniffed type)
papers-previewer --fd 3 3< "$ROFD_SRC/learning/test.ofd"scripts/dev-env.sh prints the resolution table on its own
(PAPERS_SRC, ROFD_SRC, ROFD_INCLUDEDIR, ROFD_LIBDIR, the ABI tier in use)
and exits non-zero when neither the installed package nor a checkout provides the
ABI. Sourcing it (. scripts/dev-env.sh) exports those variables; bash and zsh are
supported and the script hands control back without ending the caller's shell.
Fixtures named in docs/plan.md §6 live under $ROFD_SRC.
PPS_BACKENDS_DIR replaces the whole backend directory: keep the system
.papers-backend files present (as above) or PDF/DjVu/TIFF stop working for that
session. The full validation matrix — fixtures, checks per phase, cross-repo ABI
tests — is in docs/plan.md §6.
Arch Linux: papers-ofd on the AUR
(51.1.2-1) — yay -S papers-ofd, paru -S papers-ofd, or
git clone https://aur.archlinux.org/papers-ofd.git && cd papers-ofd
makepkg -siThe recipe lives in that AUR repository (this repository carries no PKGBUILD, so
there is no copy to keep in sync); its source is the v51.1.2 tag archive of this
repository, so makepkg verifies a checksum, and its
check() runs the test suite (13 tests on a host with Sushi, 12 without it — the
invoice oracle needs -Drofd_src, which a package build does not have;
ofd-print-file skips itself when GTK has no display). The fonts the suite
measures with are the recipe's checkdepends (noto-fonts, noto-fonts-cjk,
declared in 51.1.1-2): the fixtures carry text but no font
file, so a host without them fails ofd-text — with the missing package named
instead of an opaque assertion (R4, plan §8). Dependencies are
librofd-ffi>=0.4.3 (the ABI plus its rofd-ffi.pc), papers>=51 and papers<52,
cairo, gcc-libs, glib2, glibc. shared-mime-info and desktop-file-utils
are not listed because their pacman hooks (which index the MIME xml and the desktop
file) arrive through papers already.
Versions track the Papers line rather than our own feature count (plan D13):
51.1.2 is the Papers 51 line that the dependency bound asserts, verified against
Papers 51.1 — the second release for that baseline (51.1.1 was the first, the
initial tag; 51.1.2 carries the review campaign's fixes). Only the last component
moves with our releases, and packaging-only changes stay in the AUR pkgrel.
The package also installs the Sushi plugin below (into Sushi's system plugin
directory; -Dsushi_plugin_dir moves or skips it) and lists sushi as an optional
dependency — without Sushi the file is inert and only the space-bar preview is
missing.
Uninstalling removes only the files below — none is owned by another package, so Papers and its other backends are untouched.
<backenddir>/libofddocument.so the module
<backenddir>/org.gnome.Papers.OfdDocument.papers-backend discovery descriptor
/usr/share/mime/packages/papers-ofd.xml application/ofd (glob + zip magic)
/usr/share/thumbnailers/papers-ofd.thumbnailer Nautilus thumbnails
/usr/share/applications/org.gnome.Papers.Ofd.desktop association entry (NoDisplay)
/usr/lib/sushi/plugins-1/papers-ofd-sushi.js space-bar previews (see Limitations)
Papers builds its own desktop and thumbnailer MIME lists from in-tree backends
only, hence the separate association and thumbnailer entries. The Sushi plugin
exists for the same reason: Sushi's document renderer claims exactly the MIME
types of Papers' own desktop file, so application/ofd needs a plugin until
Papers carries it itself (§7,
A26 in Appendix A). The MIME magic is a
nested rule at priority 70: below application/zip's priority it would lose every
content-only guess, which is the only detection the previewer has. Details and the
patch-free vs patch-the-desktop tradeoff:
docs/plan.md §7.
- Open latency on a 300-page generated fixture (content on every page; 5 runs,
best of,
tests/ofd-latency.c):pps_document_load→ metadata walk →setup_cache→ first rendered page is ~130–140 ms on the shipping build (the page-size query compiled in) against ~115–120 ms on the fallback (-Drofd_page_size=disabled). Both numbers move with the machine — the shipping build measured 129 ms one day and 138–140 ms the next, the fallback 117–119 ms in both — so the shape matters more than the last digit: the shipping build spends ~26–28 ms inload(the renderer and rofd's font stack are created there, which is also why the first render is ~20 ms rather than ~48 ms), ~18–20 ms on the size walk (rofd_document_get_page_size_mm), ~66–71 ms parsing every page insidesetup_cacheand ~20 ms on the first render; the fallback has no metadata-only query, so it parses every page during the size walk (~70 ms) and rofd's page cache then makessetup_cachealmost free (~0.1 ms), with the same ~20 ms first render. Both builds parse every page either way, so the fast path pays off for callers that only need page sizes, not for the shell's own open path. - Render budget. rofd charges the worst case of a page raster
(≈9 B/px: the finished ARGB32 target, its A8 clip mask and a clipped
temporary) against
max_raster_bytes, where Papers' own allowance is 4 B/px — thepage-cache-sizesetting, 200 MiB by default, which is what clamps the maximum zoom (≈10.2× on A4). The module therefore derives the budget from each request and caps it at 450 MiB (that allowance converted to rofd's rate), so every zoom the view admits renders; a request beyond the cap — only reachable from a caller that sets a target size itself, or a largerpage-cache-size— renders at the largest scale that fits instead of failing. The page texture and the selection overlay share the fitted scale, so they cannot disagree. One shape the fit cannot save is a canvas dimension above cairo's 32767-pixel limit (a page far wider than it is tall, at a high zoom): it has no surface to draw into, and that refusal is reported atg_warningrather than silently. Rotation is snapped to the nearest quarter turn: rofd's renderer accepts nothing else, while Papers' model allows any 0–360. - Selection highlights are drawn as filled rectangles; the glyphs underneath are not recoloured, because rofd's ABI exposes no text mask. Invisible on systems whose poppler has transparent-selection support (the common case).
- A wrapped multi-line search match is reported as a single result rectangle.
- Text geometry follows the ABI. For a text run that declares no explicit
DeltaX, rofd takes each scalar's advance from the resolved font and sizes its box by that face's typographic ascent and descent — and grows a selection region by an eighth of its height on both sides — only in the fork's release (shipped inv0.4.3-fork.2and built by the AURlibrofd-ffipackage, and now merged upstream as#13for the advance and the box width; the band and the selection margin are the fork'sfeat/font-text-band, proposed as#16). That is the model of a PDF reader's highlight: all glyphs of a line share one band that clears the text above and below. With a stocklibrofd-ffi, rofd's semantic layer uses the font size instead, so search highlights, caret positions and selection regions on such text can sit right of the glyphs, and the box stops at the baseline, leaving the bottom of the text (CJK overshoot, Latin descenders) unhighlighted. Everything else is unaffected, and the test reports which model the linked library uses: the linefirst line: ABI layout … mm, ink … mm -> font-resolved|font-size footprint geometryis printed to stdout byofd-text, so it lands in_build/meson-logs/testlog.txt(or on screen withmeson test -C _build ofd-text -v;meson test --print-errorlogsprints only a failing test's output). - The space-bar preview needs the shipped Sushi plugin, which depends on Sushi
internals (its plugin directory, the document renderer's content-type list).
It is removed together with the plugin once Papers lists
application/ofdin its own desktop file, i.e. after the phase-4 move in-tree; if Sushi changes, the plugin loads with an error and previews of other types are unaffected. - "Open With" in Nautilus lists every installed application, so both desktop
entries that run
papers(Papers' own and this package'sNoDisplayassociation entry) appear as rows there. Where another package declaresapplication/ofdwith a visible entry it wins the default association — observed withdeepin-readerinstalled, wherexdg-mime query default application/ofdanswersdeepin-reader.desktop— because aNoDisplayentry is not offered as a default;xdg-mime default org.gnome.Papers.Ofd.desktop application/ofdsets ours for the user. Both the duplicate and this route disappear with the phase-4 move, which also retires the association entry. - Named destinations (
find_link_dest/find_link_page) are stubs. - Selection is declined on rotated pages, which is where Papers refuses to start
one itself ("Selection in rotated documents has never worked",
pps-view.c). - At most 4096 search results per page (
OFD_FIND_MAX_RESULTS_PER_PAGE). - rofd's render options carry a single uniform scale: a render target whose aspect ratio differs from the page is not stretched (poppler's backend scales the axes independently). Papers always derives target sizes from the page size, so this is unobservable there (§4).
| Phase | Content | Status |
|---|---|---|
| 0 | Loadable module, discovery, page geometry | done |
| 1 | Rendering, rotation/zoom matrix, metadata, printing | done |
| 2 | Text, selection, find, links, outline | done |
| 3 | load_fd (previewer), packaging, install checks, Sushi plugin |
done |
| 4 | Upstream MR into Papers' libdocument/backend/ofd/ |
planned |
Per-phase deliverables, steps and acceptance criteria:
docs/plan.md §5.
The module uses public PpsDocument API only (the private-but-exported helper
symbols are avoided), so phase 4 is a file move plus a MIME-list addition in
Papers' meson.build. If the glue code has to change, an assumption in the plan's
interface contract was wrong and should be re-verified instead of patched around.
GPL-2.0-or-later (see COPYING), matching Papers. Links rofd, which is
LGPL-2.1-or-later; the combination is compatible.