Skip to content

.hooppack — shape and carve packs

.hooppack is the open container for library content: starter shapes and carve tiles that StitchPencil’s library can be filled with from outside the app — a designer’s set of silhouettes, a partner’s texture collection, a digitiser’s own drawings handed to a friend. It is a sibling of .hoop, not part of it: a pack carries geometry to start from, never stitches and never a design.

Anyone may implement readers and writers; this page is the normative reference for pack version 1. No licence, registration or royalty applies to the format itself.

Two decisions shape it. SVG is the only drawing format — every vector tool writes it, and the app already reads it for its own bundled shapes. A manifest from day one, so author and licence travel with the files.

A .hooppack file is a standard ZIP archive (PKZIP):

  • Uniform type identifier app.stitchpencil.hooppack, conforming to public.data and public.content — deliberately not to com.pkware.zip-archive, because a type that conforms to zip is one the Files app unpacks instead of opening.
  • manifest.json at the archive root, UTF-8, no BOM — or inside the one top-level folder that holds one. A pack zipped as a folder, the way Finder and Files do it, is a pack.
  • Every drawing the manifest names is a file in the archive, path relative to the manifest, forward slashes, no .., no absolute paths. Subfolders are allowed (shapes/heart.svg).
  • Compression deflate or store. No encryption.

A reader MUST enforce: manifest ≤ 64 KB, ≤ 500 items, each SVG ≤ 2 MB, archive ≤ 50 MB. A drawing over its limit costs that drawing, not the pack.

A reader MUST ignore any file the manifest does not name — a preview image, a note, Finder’s __MACOSX/ twins, an archive inside the archive. None of it is read, and neither its size nor its name decides anything.

A pack is identified by the SHA-256 of the archive bytes: importing the same pack twice replaces it, never duplicates it.

A single .svg file (≤ 2 MB) is also a valid input to the same importer — a pack of one item with the defaults below and the file name as the name. Its identity carries the kind it was read as, so the same drawing can be in a library as a shape and as a tile.

{
"format": "stitchpencil-pack",
"version": 1,
"name": "Creator Textures",
"author": "Creator Name",
"license": "CC-BY-4.0",
"website": "https://example.com",
"generator": "Affinity Designer 2",
"items": [
{ "file": "shapes/heart.svg", "name": "Heart", "kind": "shape",
"names": { "de": "Herz" } },
{ "file": "shapes/anchor-line.svg", "name": "Anchor outline",
"kind": "shape", "lineArt": true },
{ "file": "carve/wave.svg", "name": "Wave", "kind": "carve",
"spacingMM": 0, "stagger": false },
{ "file": "carve/arches.svg", "name": "Arches", "kind": "carve",
"spacingMM": 0, "stagger": true }
]
}
KeyRequiredMeaning
formatyesAlways stitchpencil-pack.
versionyesInteger, 1. A reader accepts lower or equal versions and refuses higher ones with the number.
nameyesThe pack’s name; becomes a section in the library.
authornoFree text, shown with the pack.
licensenoSPDX identifier where one exists (CC-BY-4.0, OFL-1.1, CC0-1.0), otherwise free text. Shown, not interpreted.
website, generatornoInformational.
itemsyesOne entry per drawing; order is the order in the library.

Per item:

KeyRequiredMeaning
fileyesPath of the SVG inside the archive.
nameyesEnglish display name.
namesnoLocalized names by BCP 47 tag; the app picks its UI language, else name.
kindyesshape or carve.
lineArtshape onlytrue = open outline for run or satin, never fill. Default: derived from the SVG (below).
spacingMMcarve onlyGap between copies, default 0 (copies touch).
staggercarve onlyRows offset by half a tile, default false.

Unknown keys are ignored. An item of a kind a reader does not know is passed over with a warning, and the rest of the pack is read. version is 1 or higher. Names — pack, author, item — are shown as one line of at most 80 characters; control characters and line breaks are removed on reading.

A file the manifest names that is missing, unreadable, too large or empty is a warning, not a failure: the rest of the pack is still worth having. A pack with nothing usable at all is refused.

The SVG is parsed with a hardened parser and fitted to a few editable bezier anchors per ring — the same conversion the app’s bundled shapes go through. Rules for a file that arrives clean:

  • Physical units. width="40mm" height="36mm" viewBox="0 0 40 36" — one user unit is one millimetre. Without physical units a reader assumes 96 dpi (1 px = 0.2646 mm).
  • One filled path is a silhouette. Subpaths inside it are holes or islands (even-odd). Several filled paths in one file are several rings of the same shape, not several shapes.
  • Stroke without fill is line art (fill="none" plus stroke): an outline that is inserted as a run. One that closes (… Z) stays closed. lineArt in the manifest overrides the derivation.
  • Area and line together. A shape may hold closed rings and open ones — a flower and its stem. The closed rings follow the active stitch; the open ones always arrive as runs beside them.
  • The size is the geometry’s. A stroke’s width does not count: a 16 mm square drawn with a 4 mm stroke is a 16 mm shape.
  • Paths only. Text, images, gradients, filters, clipping and use are not read, and an importer should say what was left out; rect, circle, ellipse, line, polyline, polygon and g with transform are.
  • A starter shape, not an illustration. At most 64 rings and 3000 anchors per shape, 12 000 points per tile; more is passed over.
  • Embroidery scale. Details under about 3 mm do not sew; a fitter keeps anchors few, but a clean file starts with few.
  • Colours are ignored: a shape takes the active stitch when inserted.

A shape is stored in a ±1 unit box (largest ring first) together with its drawn size in millimetres, so it lands on the canvas at the size it was drawn.

<svg xmlns="http://www.w3.org/2000/svg" width="40mm" height="36mm" viewBox="0 0 40 36">
<path fill="#000" d="M20 34 C4 22 0 14 4 7 C8 1 16 2 20 8 C24 2 32 1 36 7 C40 14 36 22 20 34 Z"/>
</svg>

A carve item is a tile that repeats into a running texture. Strokes only, no fill; several strokes per tile are welcome (a weave is three lines, one line is a scribble). The tile is the drawn content — its bounds are the strokes’ bounds, not the viewBox — and the gap to the next copy is spacingMM in the manifest. Element size 4–8 mm reads well in stitches.

Strokes are flattened to polylines and stored in millimetres, normalised to the origin. Stroke width and colour are ignored: the needle draws the line.

<svg xmlns="http://www.w3.org/2000/svg" width="8mm" height="4mm" viewBox="0 0 8 4">
<path fill="none" stroke="#000" stroke-width="0.3" d="M0 2 C2 0 2 0 4 2 C6 4 6 4 8 2"/>
</svg>

A writer that exports a library produces the same kind of file: shapes as filled paths (or stroked, for line art) at their millimetres, tiles as stroked polylines, one SVG per item, and the manifest first in the archive. A conforming writer is deterministic: it stamps every entry with a fixed date (StitchPencil uses 2026-01-01 00:00 UTC), so the same library written twice is the same bytes and keeps its id. A writer refuses more than 500 items — what it writes, a reader must read.

example.hooppack — the manifest above’s smaller cousin: one heart, one wave, under CC0. Unzip it to see the layout.

HoopPack.swift under Packages/Core/Sources/SvgImportKit/ in the StitchPencil source: reader, writer and the tests that hold every limit on this page in place.