Penpot v3 package model and official builder
Document status:
verified. Canonical source.
Summary
Penpot v3 is a ZIP package with JSON metadata and optional binary objects. The manifest, file, page and per-shape members expose stable UUIDs, feature flags and a data-migration list. Penpot’s official JavaScript library can build the same representation outside the editor and is therefore a credential-free foreign producer for an adapter fixture.
The executable NUIF profile covers one legacy per-shape package, one page, one board and direct rectangle, ellipse and literal-text children. It is not a general Penpot importer and does not treat package retention as semantic support for unknown Penpot features.
Evidence
- The technical file-format reference identifies v3 as a ZIP archive with JSON
metadata and binary assets, manifest version 1, per-file JSON, page members,
per-shape members and an
objects/directory. It distinguishes the package format version from the evolving file data version and points tobackend/src/app/binfile/v3.cljas the manifest implementation. https://help.penpot.app/technical-guide/developer/data-model/penpot-file-format/ (retrieved 2026-08-30). - The data-model guide describes pages and components as containers over shape trees. Files can refer to shared libraries, and media objects can be stored separately. https://help.penpot.app/technical-guide/developer/data-model/ (retrieved 2026-08-30).
- The data guide states that many attributes are optional, missing properties
express defaults, and import/export removes
nullproperties. Package projection must therefore compare interpreted values rather than assuming member presence is a distinct state. https://help.penpot.app/technical-guide/developer/data-guide/ (retrieved 2026-08-30). - Penpot’s
library/package exposes a builder and byte export independent of a logged-in editor. The repository source inlibrary/src/lib/builder.cljsandlibrary/src/lib/export.cljsis the primary implementation locator. The committed fixture is generated by exact npm dependency@penpot/library1.1.0 and its SHA-256 is checked after repeat generation. https://github.com/penpot/penpot/tree/develop/library (retrieved 2026-08-30). - The compact v3 representation embeds shapes in page entries. The upstream
change describes it as opt-in through
binfile-v3-compact/format: "compact"while retaining the per-shape representation as the compatibility default. The compact profile remains excluded until upstream stabilization and an independent fixture exist. https://github.com/penpot/penpot/issues/10727 (retrieved 2026-08-30). zip8.6.0 supports bounded archive reading and writing with optional compression features. Its defaults enable more algorithms and crypto than this profile requires, so the workspace disables defaults and enables only Deflate throughdeflate-flate2-zlib-rs. https://docs.rs/zip/8.6.0/zip/ (retrieved 2026-08-30).- CVE-2025-29787 affected the crate’s high-level filesystem extraction routine before 2.3.0 when archive symlinks could redirect later writes. The selected version is beyond the patched boundary; the NUIF adapter additionally never extracts to a filesystem and rejects symlink, directory, encrypted, duplicate and unsafe-path entries before JSON parsing. https://github.com/zip-rs/zip2/security/advisories/GHSA-94vh-gphv-8pm8 (retrieved 2026-08-30).
Mechanism
Import first enforces whole-package, member-count, expanded-byte, per-member, compression-ratio, name, entry-kind and compression-method limits. Every member is then read into bounded memory. The parser validates one manifest/file/page, the root-frame/board relationship and the declared shape subset. Mapped JSON scalars retain member-qualified byte spans; all original member payloads and compression choices remain in a retentive package object.
No-op synchronization returns the original archive bytes. A mapped edit patches only the recorded scalar spans within affected uncompressed payloads and then rebuilds the package deterministically. Unedited member payloads are identical, although ZIP container metadata can change after a rebuild. The rebuilt package is imported again and must equal the requested canonical document before it is returned. Structural and out-of-profile edits fail without partial output.
Native output stores JSON members below 4 KiB and deflates larger members; foreign packages retain each member’s original method. The threshold follows a same-machine profile comparison that reduced small-package writer allocation without changing semantic or retentive laws. It is not part of the Penpot format and can change only with deterministic-output fixtures and a recorded benchmark.
The foreign producer fixture exercises a path independent of NUIF’s writer. The profile runner adds an opaque binary member and unknown JSON object, applies eight mapped changes, checks untouched payload identity and exact canonical re-import, and covers typed traversal and one-over resource failures. A second bridge runs export, import, synchronization and re-import through the public CLI.
NUIF relevance
Borrow the inspectable package boundary, stable UUIDs, explicit feature flags, separate format/data versions and separation of JSON metadata from binary objects.
Adapt one page and a bounded board/rectangle/ellipse/text subset. Package identity maps to NUIF identity, while the page remains structural package data. Unknown fields and members are retentive evidence, not lossless NUIF semantics.
Reject direct mapping of every Penpot shape or library object to a universal NUIF kind. Constraints, grids, variants, interactions, text runs, paths, effects, component libraries, shared-file relations, tokens and media exceed the current model/profile and require their own correspondence and fidelity laws.
Open questions
- When will Penpot declare the compact page representation stable, and will its manifest or feature markers provide an unambiguous dispatch key?
- Does the official library publish a compatibility guarantee for generated packages across Penpot editor releases?
- Which Penpot text fields can carry content-addressed font identity without profile-owned plug-in metadata once variable and embedded fonts are included?