Skip to content

.label file format

A .label file is a JSON document conforming to the LabelDocument shape. It's portable, human-readable, and round-trip safe through toJSON / fromJSON — serialising a loaded document produces byte-identical output to the original, modulo the updatedAt timestamp, which is bumped on load.

The schema

ts
interface LabelDocument {
  id: string; // stable ID — survives renames
  version: number; // matches CURRENT_DOCUMENT_VERSION on load
  name: string;
  description?: string;
  createdAt: string; // ISO-8601
  updatedAt: string; // ISO-8601 — bumped on every mutation
  canvas: CanvasConfig; // see below
  objects: LabelObject[]; // one of five object types
  metadata: Record<string, unknown>; // user-defined; never rendered
}

interface CanvasConfig {
  widthDots: number;
  heightDots: number; // 0 = continuous — auto-crop to content
  dpi: number;
  margins: { top: number; right: number; bottom: number; left: number };
  background: string; // CSS colour — used for continuous crop
  grid: { enabled: boolean; spacingDots: number };
}

See Document model for the five object-type shapes and their fields.

Example — the Christmas-card address label

The walkthrough in Template engine uses christmas-card.label, committed to the docs as the single source of truth. Opening it produces:

json
{
  "id": "christmas-card-address-label",
  "version": 1,
  "name": "Christmas card address label",
  "description": "62 mm continuous tape. Used for the docs walkthrough.",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z",
  "canvas": {
    "widthDots": 696,
    "heightDots": 0,
    "dpi": 300,
    "margins": { "top": 12, "right": 12, "bottom": 12, "left": 12 },
    "background": "#ffffff",
    "grid": { "enabled": false, "spacingDots": 10 }
  },
  "objects": [
    {
      "id": "name",
      "type": "text",
      "name": "Recipient name",
      "x": 24,
      "y": 24,
      "width": 648,
      "height": 60,
      "rotation": 0,
      "opacity": 1,
      "locked": false,
      "visible": true,
      "color": "#ff0000",
      "content": "{{name}}",
      "fontFamily": "Burnmark Sans",
      "fontSize": 44,
      "fontWeight": "bold",
      "fontStyle": "normal",
      "textAlign": "left",
      "verticalAlign": "top",
      "letterSpacing": 0,
      "lineHeight": 1.2,
      "invert": false,
      "wrap": true,
      "autoHeight": false
    }
    /* … two more TextObjects for address line and postcode/city — see the file … */
  ],
  "metadata": {
    "category": "holiday",
    "locale": "nl_NL",
    "docsExample": true
  }
}

Field-by-field annotations:

FieldPurpose
idStable UUID / slug. Survives renames — use it for foreign keys, never name.
versionSchema version. Equals CURRENT_DOCUMENT_VERSION (1 today). Older files are migrated forward on load.
nameHuman-readable title. Shown in UIs; safe to change.
descriptionOptional long-form summary. Never rendered.
createdAtISO-8601 creation timestamp. Immutable.
updatedAtISO-8601 last-mutation timestamp. Bumped on every add/update/remove/setCanvas and also on loadDocument.
canvas.widthDotsMedia width in printer dots (696 = 62 mm at 300 dpi).
canvas.heightDots0 for continuous tape (auto-crop); positive for die-cut labels.
canvas.dpiMatters for PDF export (points per inch); the raster pipeline works in dots.
canvas.marginsVisual guide for the UI. Does not clip rendering.
canvas.backgroundCSS colour. Cropped out on continuous labels to find content bounds.
canvas.gridDesigner-UI snap hint. Not rendered.
objects[].idAssigned by designer.add(). Stable across history snapshots.
objects[].typeDiscriminator — text, image, barcode, shape, group. Immutable via update().
objects[].x,y,width,heightTop-left bounding box in canvas dots.
objects[].rotationDegrees, clockwise; pivot at bbox centre.
objects[].opacity0..1. Composited in full-colour space before 1bpp dithering — see Colour model.
objects[].lockedUI hint; core does not enforce.
objects[].visibleHidden objects do not render.
objects[].colorCSS string. Drives plane routing during renderPlanes().
metadataUser-defined dictionary. Never rendered. Use it for categorisation, tags, ownership — anything app-specific.

Asset references

ImageObject.assetKey is a content-addressed key into the designer's AssetLoader. The image bytes are never inlined into the .label JSON — that would make files huge, history snapshots expensive, and diffs unreadable.

json
{
  "type": "image",
  "assetKey": "e1b2c9d3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9",
  "fit": "contain",
  "threshold": 128,
  "dither": true,
  "invert": false
  /* …plus BaseObject fields… */
}

Shipping a document to another environment means you either:

  • Ship the asset store alongside (same IndexedDB, same S3 bucket, same filesystem). The assetKey resolves on the other side.
  • Bundle via exportBundled(doc, assetLoader) — produces a .zip with label.json + assets/<assetKey> entries. See Export → exportBundled.

Version field and migrations

ts
import { CURRENT_DOCUMENT_VERSION } from '@burnmark-io/designer-core';
  • Today CURRENT_DOCUMENT_VERSION === 1. Every document you write today has "version": 1.
  • When version 2 ships, the runtime will include a registered migration from 1 → 2. Loading a v1 document through fromJSON() or migrateDocument() will apply that migration and return a v2 document.
  • Loading a document that is newer than the running code's CURRENT_DOCUMENT_VERSION throws DocumentMigrationError. Upgrade @burnmark-io/designer-core to read the file.

The migration chain runs sequentially: a v1 document loaded on code that supports v3 runs v1→v2 and v2→v3 before returning. See Document model → Versioning for the registerMigration API.

Forward compatibility

If you're building infrastructure that stores .label files long-term:

  • Don't parse the JSON yourself. Use fromJSON() — it runs the migration chain. Stored files are always kept at their original version on disk; migration happens on load.
  • Pin your writers, float your readers. Readers (apps that display labels) should be on the latest @burnmark-io/designer-core. Writers (CI jobs, scripts) can stay on older versions without blocking reader updates, because old writers produce files the new readers can migrate.
  • Keep metadata for app-specific data. Don't add new top-level fields to the document; add them to metadata. That's what the dictionary exists for.

The metadata field

Free-form user-defined storage. The renderer ignores it; the migration chain leaves it alone; serialisation passes it through.

Useful patterns:

  • Template categorisation. { "category": "shipping", "subcategory": "air-freight" }.
  • Ownership. { "createdBy": "piet@example.nl", "tenant": "acme" }.
  • Change tracking. { "revision": 12, "previousId": "…" }.
  • Locale. { "locale": "nl_NL", "currency": "EUR" }.

Nothing is validated — Record<string, unknown> — so be disciplined about what you put there. Treat it as your app's custom header.

Not affiliated with Dymo, Brother, Avery, or any hardware vendor.