sofd-error 0.5.1

One error type per OFD subsystem, all converting losslessly into a single taxonomy
Documentation
  • Coverage
  • 100%
    126 out of 126 items documented0 out of 12 items with examples
  • Size
  • Source code size: 34.7 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 710.3 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 7s Average build duration of successful builds.
  • all releases: 5s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • emfox/sOFD
    1 0 9
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • emfox

sOFD

Super OFD, Simple OFD — or whatever you wish.

A complete Rust engine for OFD documents: open, render, convert, print, sign, edit, search and audit, behind one API. Pure Rust, zero unsafe, built to GB/T 33190-2016.

English | 简体中文

Rust Edition License Standard

use sofd::{Document, RenderOptions};
use sofd_io::FileSource;

let source = FileSource::open("contract.ofd")?;
let doc = Document::open(&source)?;
let png = doc.to_png(0, &RenderOptions::default())?;

sOFD implements GB/T 33190-2016, the Chinese national standard for fixed-layout electronic documents. Parsing and rendering are written from scratch; the one C library in the tree is fontconfig, reached through font-kit by the reader on Linux.

The current release is 0.5.1. It publishes sofd-cli, so cargo install sofd-cli puts a sofd command on the PATH, and it stops the archival rewrite from mistaking a < inside a comment for a tag. What it offers is in the CHANGELOG; concrete work still to do is in the issue tracker, and the direction it serves is in docs/Roadmap.md.

This document has two halves: the first is for anyone using sOFD, the second for anyone working on it.


Part 1: Using sOFD

Features

Capability What it covers
Reading and rendering GB/T 33190 rendering, lossless zoom, sharded loading for large documents
Conversion OFD to and from PDF, SVG and PNG; batch and merge
Signatures SM2 signatures, SM3 digests, SM4 encryption and GB/T 20520 timestamps. Verification checks the proof and the certificate's window at the signing time; a caller who wants the issuer question answered passes its own trust anchors and gets a separate verdict for a signer outside them. International algorithms are named by GB/T 38540-2020 but not implemented
Delivery One engine, built as a CLI, WebAssembly, a C ABI library, an HTTP service and a desktop reader
Localization Fluent message layer, en-US and zh-CN

Working examples

One per delivery form, each a single file that opens a document and takes it somewhere, cargo run away:

Form Example What it shows
CLI command_line.rs open from a path, read what the document declares, render every page, verify signatures
C ABI c_abi.rs the same calls a C host makes, against the generated header
Browser browser.rs the JS boundary from the Rust side: raster, SVG, search
HTTP http_service.rs the router, the store and a graceful shutdown, mounted in your own process
Desktop desktop_viewer.rs the reader's interface drawn into an egui::Ui, with no window and no display

cargo build --workspace --examples builds four of them; the browser one needs --target wasm32-unknown-unknown, and CI builds it there.

Getting the artefacts

Pushing a v* tag builds every delivery form and publishes one archive per product, named for the release it came from:

Artefact For How it runs
sofd-<version>-linux.tar.gz, -macos, -windows Desktop unpack and run the sofd-reader inside; one download per system, so a user fetches only their own
sofd-<version>-web.zip Browser unpack; serve site/ over HTTP, because a file:// page cannot load the module
sofd-<version>-android.apk Android 7.0+ (arm64) sideload; signed with a throwaway debug key, not a store upload
sofd-<version>-uos-amd64.deb UOS Desktop V20 dpkg -i, or install it from the file manager
sofd-<version>-ffi.tar.gz C, C++, or anything with a C ABI link the library inside and include sofd.h beside it

iOS is the one egui platform left out: an app for it can only be installed after a macOS signing and packaging step, and no key ships with this project.

The builds are unsigned — there is no signing key — so each release carries a SHA256SUMS manifest as the only thing a download can be held to:

python3 scripts/release_checksums.py verify .
sha256sum -c SHA256SUMS          # or without this project at all

The crates themselves are not on a registry yet: which of them will publish, and what is left before they can, is issue #4.

Twenty-one crates reach a registry, but only some of them are interfaces. Add sofd and you are done; the crates it depends on come along because cargo requires them, not because you were meant to name them. Which is which is in Architecture.md § 3, and python3 scripts/publish_order.py prints the dependency closure — from sofd that is all twenty-one, while sofd-crypto alone needs three.

The command is one of them, published as sofd-cli because sofd is the library:

cargo install sofd-cli        # installs the `sofd` command

What it reads

Standard Scope
GB/T 33190-2016 Fixed-layout document format for electronic file storage and exchange
GB/T 33476.1-2016 Electronic official document format for Party and government bodies, part 1: document structure
GB/T 33476.2-2016 Same, part 2: presentation
GB/T 38540-2020 Cryptographic specification for secure electronic seals
GM/T 0099-2020 Cryptography application for open fixed-layout documents: signature, encryption, integrity
GB/T 39362-2020 Archiving specification for electronic official documents

GB/T 33476 is published in three parts; parts 1 and 2 are the ones this project follows. GM/T 0099-2020 is a cryptography industry standard issued by the State Cryptography Administration, so it lives on the industry standards platform rather than the national standards one.

Clause-level coverage lives in compliance/gb-t-33190.toml and the method behind it in compliance/CLAUSES.md: 56 clauses are registered, every one of them verified. The clause numbers are read out of the standard's transcribed text by scripts/gbt33190.py, which reads each numbered heading and checks the result against the contents pages the standard prints, so a row cannot cite a clause the standard does not have. What the engine can and cannot draw with is set out face by face in docs/Fonts.md. How it compares to other readers in the field, page by page against a second vendor's output, is in docs/Interop.md. What a signature can and cannot settle, and why the standards leave the issuer to the caller, is in docs/Trust.md.

Background

OFD and PDF are both fixed-layout formats, but OFD is an open ZIP + XML container with national cryptography and electronic seals built into the standard. Its layout is required to stay identical across software and hardware, which is why it serves as the legal carrier for official documents, licences, invoices and archives.

Rust fits here for two reasons. Document processing is CPU bound and has to run in a browser, on a server and on the desktop, so zero-cost abstraction lets the layering stay clean without a performance penalty. And since OFD files arrive from arbitrary sources, #![forbid(unsafe_code)] matters more than usual: the parser is the part handling untrusted input.


Part 2: Working on sOFD

Module map

Twenty-seven crates in five layers. An arrow points from a crate to what it depends on; boxes below the facade are pulled in only when a feature names them.

  ┌──────────────────────────────────────────────────────────────────────┐
  │  delivery forms: applications, not dependencies                      │
  │                                                                      │
  │    reader        cli        wasm        ffi        server            │
  │    egui/eframe   binary     browser     C ABI      HTTP              │
  └──────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  sofd                                                       │
  │  one handle, one feature per capability                              │
  └──────────────────────────────────────────────────────────────────────┘
                                  │
  ┌──────────────────────────────────────────────────────────────────────┐
  │  always linked: the open-to-display path                             │
  │                                                                      │
  │    parser ──► package ──► xml                                        │
  │      │             │       io                                        │
  │      ▼             │                                                 │
  │    model ◄─────────┘                                                 │
  │      │                                                               │
  │    display ──► font ──► geometry                                     │
  │                                                                      │
  │    every crate above resolves errors through error ──► common        │
  └──────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────┐
  │  behind a feature of core: one capability each                       │
  │                                                                      │
  │          render    display, geometry, font                           │
  │          print     render, display                                   │
  │          convert   render, parser, display                           │
  │          sign      crypto, package, model                            │
  │          crypto    common, error                                     │
  │          edit      parser, package, model                            │
  │          search    display, geometry                                 │
  │          archive   package, model                                    │
  │          annot     nothing: the parser it reads is never optional    │
  └──────────────────────────────────────────────────────────────────────┘

  sofd-i18n      messages for the shells; the engine emits none
  sofd-testkit   fixtures for the test suites, a dev-dependency only

The keystone is sofd-display: every output backend reads the same flattened list, so an export cannot drift from the preview. Section 3 of docs/Architecture.md carries the same inventory as a table, with the responsibility and registry status of each crate.

Where a document goes

Content.xml is a nested tree of templates. Having every output backend walk it separately would duplicate the logic and, worse, let the backends drift apart. That drift is the usual cause of an export not matching the preview.

So the tree is flattened once into a sequence of commands carrying absolute transforms, and every downstream consumer reads that:

Content.xml (nested template tree)
       |
       v  parsed once
DisplayList (flattened, absolute CTM, resources resolved)
       |
       +--> render    raster
       +--> print
       +--> convert   PDF / SVG / PNG
       +--> search    text extraction

Office and OCR conversion are on the roadmap, not in the code: there is no feature for either, so asking for one fails to build rather than compiling to nothing.

No user-facing text in the engine

Errors are structured enums with parameters, never prose:

// Any locale can render this
FormatError::PageOutOfRange { index: 42, total: 100 }

// Hardcoding prose here would make the engine unusable from C ABI, JS or Python
"page index out of range: 42 (document has 100 pages)"

The engine's own message catalogue (sofd-i18n) holds engine messages only. A presentation layer that has product-facing strings of its own — the reader does — keeps them beside itself and merges them in with I18nBuilder, so the engine never names a widget it does not own.

Untrusted input

Threat Defense
Zip Slip path traversal PackageError::IllegalPath
Zip bomb Entry count and decompressed size ceilings (512 MB)
Stack exhaustion via deep nesting DisplayListBuilder::max_depth, default 64
Hostile document hanging the renderer RenderError::BudgetExceeded, degrading to partial output
Edits silently breaking signatures EditError::WouldInvalidateSignature, requiring confirmation

Building

Rust 1.92+ with Edition 2024, which is what rust-version in the workspace manifest states; .github/workflows/rust.yml names the version CI builds with. cargo check --workspace is enough to confirm a toolchain works.

Building the crates needs no C toolchain. The reader is the exception on Linux: font-kit links fontconfig, so a machine without the headers needs libfontconfig1-dev and pkg-config installed.

git clone https://github.com/emfox/sOFD.git
cd sOFD

cargo check --workspace          # check the whole workspace
cargo build --release            # release build
cargo test --workspace           # run tests

Checks

python3 scripts/check_all.py             # everything this machine can run
python3 scripts/check_all.py --quick     # the policy gates only
python3 scripts/check_i18n.py            # message key consistency across locales
python3 scripts/check_language.py        # English-only policy and comment density
python3 scripts/check_publish.py --list  # which crates reach a registry
python3 scripts/publish_order.py         # the order a registry accepts them in
python3 scripts/check_readme.py          # the counts both READMEs state

The i18n check compares the catalogues: same file set, same key set, consistent placeholders, and the all_keys() list in Rust matching the keys in the FTL files. The publish check holds the two sets — twenty-one crates that publish and six that do not — to the whole workspace, so a new crate cannot slip in without saying where it goes, and scripts/publish_order.py holds the published set to being closed under its own dependencies. The README check reads every count out of the English README and the same sentence out of the Chinese one, so the two cannot state different figures: README.md is the source of truth and README.zh-CN.md follows it.

Contributing

Issues and pull requests are welcome. Before submitting, make sure these pass:

cargo test --workspace
cargo clippy --workspace -- -D warnings
python3 scripts/check_all.py

New code needs tests. Do not introduce unsafe; sofd-ffi is the only documented exemption, because a C ABI requires it. Code, comments, commit messages and issues are English only: docs/Conventions.md.

Performance baselines, and how to run them, are in docs/Performance.md.

License

Apache-2.0

The reader's icons are the Adwaita symbolic set, drawn by the GNOME Project and used here under CC BY-SA 3.0 US; see NOTICE.md.

Related