TDF: A New Document Format

Presented by Trevor, Wolf, and Mars

What is TDF?

TDF is a new static document format built to be a better PDF.

TDF is designed from scratch to handle:

  • Automatic and good deduplication – the same font or image is stored once, shared everywhere
  • Lazy loading pages – only load what you actually render. Most PDFs can’t do this!
  • Built-in easy signature support – append-only and verifiable
  • Any storage backend for the file itself – binary blob, IPFS, JSON, SQL, whatever

How is it different from PDF?

PDF was designed in 1993 for print. Its storage model is fixed: one file, one byte layout, one way to read it.

TDF separates what a document is from where and how it is stored:

PDF TDF
Storage Single BYTESTREAM Swappable backends (BYTESTREAM, IPFS, JSON, SQL, …)
Deduplication None Identical primitives are automatically interned
Signatures Optional, bolted on First-class append-only store with ordering guarantees
Data loading Eager Lazy – fonts and images load only when rendered
Groups / nesting Flat Recursive pointer chains with inherited styling via tags

General file structure

The Manifest

Segment Contents
Header Magic bytes, version number, checksum, segment offsets
Meta Title, document metadata, compression settings
Pages Fixed-size page entries — each one points into the page store

What is a “pointer”?

A thing that references something in store, and a little bit of extra unique data about that item’s existence.

The “Store”s

Store What it contains
Pages store Page entries that point to a group of items, representing all of the stuff that shows up on a given page
Item store Document structure primitives (layout/text/image refs) and their pointer chains when interned
Data store Raw bytes for large assets like fonts and images, addressed in the Item Store via pointers
Signature store Append-only signature records for verifiable history and ordering of the entire TDF, including the signature store itself

Why are we using stores

  • It’s a DAG so so that we can have easy deduplication while maintaining performance
  • It allows for lazy loading of data by storing stuff across different stores
  • We can easily define the data storage contracts of a given store. E.g. append only store

Backends

Goal

We want to store a TDF as a:

  • BYTESTREAM
  • The nix store
  • Connected IPFS blobs
  • JSON
  • BSON
  • Microsoft SQL Server
  • Libreoffice Calc

How do we do this?

The “backend”

If you think about, every storage mechanism type has some shared functionality:

  • You can allocate and dereference pointers to underlying data
  • You can “cluster” stuff together so that you can load it faster

In a simple binary file, a pointer is an offset, and a cluster of stuff is a contiguous range (so we can easily load an entire page worth of items at once). But it doesn’t have to be so! In IPFS, a pointer is a content hash, and a cluster of stuff is a set of content hashes as an IPFS merkle DAG.

So why not make this generic?

The “vec” backend

pub struct VecBackend {
    page_store: PageStoreImpl,
    item_store: ItemStoreImpl,
    data_store: DataStoreImpl,
    signature_store: SignatureStoreImpl,
}

pub trait Backend: Sized {
    type Types: BackendTypes;
}

pub struct VecTypes;

impl BackendTypes for VecTypes {
    type Single<S: StoreTypes> = VecSinglePointer<S>;
    type Group<S: StoreTypes> = VecGroupPointer<S>;
}

pub struct VecSinglePointer<S: StoreTypes> {
    pub index: usize,
    pub unique: S::Unique,
}

pub struct VecGroupPointer<S: StoreTypes> {
    range: VecRange,
    uniques: Vec<S::Unique>,
}

The “frontend”

How the backend itself is accessed using backend-specific methods. This allows for, for example, easy interning.

Interning

“Grouping” via the backend access:

  1. Go through, see what points to what
  2. Group together commonly pointed at stuff
  3. Remove the individual pointers and change the clumps of identical pointers to be single ones that point to the group instead

The renderer

We iterate and draw! So simple!

let reader = DummyTDFBuilder::default()
    .add_page(vec![
        (
            ItemPrimitive::Shape(Shape {
                kind: ShapeKind::Circle,
            }),
            ItemUnique {
                position: Position { x: 1, y: 2 },
                ..Default::default()
            },
        ),
        (
            ItemPrimitive::TextBox(TextBox {
                content: "hi".into(),
                font: None,
            }),
            ItemUnique {
                position: Position { x: 3, y: 4 },
                ..Default::default()
            },
        ),
    ])
    .build();
items (primitive, unique):
  - Shape: Circle
      pos: (1, 2)
      tags: {}

  - TextBox: "hi"
      font: (comic_sans)
      pos: (3, 4)
      tags: {}

Bonus Content

Signatures

The signature store is append-only: you can only add to the end, never reorder or delete. Each signature hashes over everything before it — so when you verify signature N, you have a guarantee that all prior content existed at the time it was signed.

graph LR
  subgraph chain["append only →"]
    I0["Visual Signature 1"] --> I1["Visual Signature 2"] --> SA(["Sig A"]) --> I2["Visual Signature 3"] --> I3["Visual Signature 4"] --> SB(["Sig B"])
  end
  I0 & I1 -.->|"hash 0"| SA
  SA & I2 & I3 -.->|"hash 1"| SB

Extra extras

Tags

  • Flowing text and selecting text across pages
  • Accessibility features like alt text
  • Mention being able to easily manipulate a TDF whereas PDFs are hard to manipulate and you have to resave the entire thing
  • Chunked compression via the backend

All Together

Wolf, Trevor & Mars and walk through reading a TDF from scratch

sequenceDiagram
    participant C as Caller
    participant R as TDFReader
    participant Seg as Pages Segment
    participant PgS as Page Store
    participant IS as Item Store
    participant B as Backend
    participant DS as Data Store

    C->>+R: iter_page_items(40)
    R->>Seg: seek pages_offset + 40 * entry_size
    Seg-->>R: page entry
    R->>+PgS: get ItemPointer for page 40
    PgS-->>-R: ItemPointer (PointerRange)
    R->>+IS: iter_rec(pointer)
    IS->>+B: iter_item_children_rec(pointer)
    Note over B: recursively follows refs,<br/>reduces uniques at each hop
    B-->>-IS: (ItemPrimitive, reduced ItemUnique) pairs
    IS-->>-R: iterator
    R-->>-C: Iterator<(ItemPrimitive, ItemUnique)>

    loop for each primitive with a Handle
        C->>+R: deref_handle(handle)
        R->>+DS: get(handle)
        DS-->>-R: DataPrimitive
        R-->>-C: DataPrimitive (font / image bytes)
    end

Welcome to Olin 806