libfreeform
Parser for Apple Freeform pasteboard data — Rust core, WebAssembly bindings for Node, Bun, Deno, and browsers.
Freeform writes one selection in several parallel pasteboard flavors. This library snapshots every flavor, decodes supported tiers independently, and retains unknown records and bytes for newer decoders:
| Pasteboard flavor | Decoded data |
|---|---|
com.apple.drawing |
PencilKit spline controls, full affine transforms, proven point channels, exact ink identifiers, and raw stroke records |
com.apple.freeform.CRLNativeData |
Typed, record-bounded board items: geometry/style, shapes, text, connectors, tables, media, groups, and native ink |
com.apple.freeform.TSUDescription |
Ordered item classes and recursive routing hints |
CRLNativeMetadata, CRLAsset.*, state/style/text/render flavors |
Correlation metadata, embedded assets, state flags, style/text selections, and PNG/TIFF/PDF fallbacks |
decodePasteboard returns a decoded, failed, or absent outcome for each independent tier. A damaged TSU manifest cannot discard valid native data; mismatched tiers are reported instead of partially joined. Private CRL formats remain OS-version fragile, so unsupported fields retain their bounded raw records rather than receiving guessed values. The wire formats are documented in docs/FORMAT.md.
Rust
use ;
let flavors =
.into_iter
.filter_map
.collect;
let decoded = decode_pasteboard;
if let Decoded = decoded.drawing
if let Decoded = decoded.native
The optional serde feature derives Serialize/Deserialize (camelCase field names) on all decoded types. Full API docs: docs.rs/libfreeform.
JavaScript / TypeScript
The package is a single ESM module backed by WebAssembly. The wasm initializes at import — no init call, no async ceremony:
import from 'libfreeform';
import from 'node:fs';
const ;
const decoded = ;
if
if
- Node ≥ 20 / Bun — works as-is (the
nodeexport condition loads the wasm synchronously from disk). - Deno —
deno add npm:libfreeform, then import as above; run with-Rso the module may read its own.wasmfile. - Browsers via a bundler (Vite, webpack 5, Rollup) — works as-is; the wasm is referenced with the standard
new URL(..., import.meta.url)asset pattern. - Browsers via CDN —
import { decodePasteboard } from 'https://esm.sh/libfreeform'.
Every function is fully typed (index.d.ts ships with the package); decoded objects use camelCase keys mirroring the Rust types.
JS API
| Function | Purpose |
|---|---|
decodePasteboard(snapshot) |
Decode exact ordered flavors into independent tier outcomes while retaining assets, renders, state, text, style, unknown flavors, and diagnostics |
decodePkDrawing(bytes) |
Decode local PencilKit spline controls, transforms, proven point channels, ink metadata, and raw records |
decodeCrlNative(bytes, tsu?) |
Decode record-bounded CRL items and optionally perform a strict TSU join |
parseTsuDescription(bytes) |
Decode ordered classes and recursive typed hint values |
classifyBlob(name, bytes) |
Classify exact UTIs/dump aliases and validated drawing/render signatures |
isPkDrawing(bytes) |
Check the "wrd" signature without crossing the Wasm boundary |
hasFreeformContent(snapshot) |
Test whether a snapshot contains an importable structured, asset, text/style, or rendered flavor |
Capturing pasteboard data
pbpaste only sees text. Copy something in Freeform, then capture one atomic snapshot:
The destination contains manifest.json plus collision-safe flavor-NNNNNN.bin files. The manifest preserves exact UTIs, ordering, byte counts, and the stable pasteboard changeCount; capture retries if the pasteboard changes and atomically replaces an older snapshot without stale conditional assets.
Stability
The com.apple.freeform.* formats are private and can change with any Freeform or OS update. CRL records are decoded only inside established object boundaries, and unknown fields remain available as raw bytes. FreeformCompatibility and per-tier failures let callers choose a render fallback without confusing absence, truncation, unsupported versions, or correlation failures.
Repository layout
src/— the parser (pure Rust rlib; published to crates.io)libfreeform-wasm/lib.rs— target-gated wasm-bindgen module; the same crate emits it as a cdylib for npmnpm/— the npm package: entry points, types, build scripttests/— cross-runtime smoke tests (Node, Bun, Deno, headless Chromium)docs/FORMAT.md— the reverse-engineered format documentationtools/dump_pasteboard.swift— pasteboard capture utility
Development
&&
Building the npm package needs the wasm32-unknown-unknown target, a wasm-bindgen CLI matching the pin in root Cargo.toml, and optionally wasm-opt (binaryen). See RELEASING.md for the release process.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.