.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.
Container
Section titled “Container”A .hooppack file is a standard ZIP archive (PKZIP):
- Uniform type identifier
app.stitchpencil.hooppack, conforming topublic.dataandpublic.content— deliberately not tocom.pkware.zip-archive, because a type that conforms to zip is one the Files app unpacks instead of opening. manifest.jsonat 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
deflateorstore. 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.
manifest.json
Section titled “manifest.json”{ "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 } ]}| Key | Required | Meaning |
|---|---|---|
format | yes | Always stitchpencil-pack. |
version | yes | Integer, 1. A reader accepts lower or equal versions and refuses higher ones with the number. |
name | yes | The pack’s name; becomes a section in the library. |
author | no | Free text, shown with the pack. |
license | no | SPDX identifier where one exists (CC-BY-4.0, OFL-1.1, CC0-1.0), otherwise free text. Shown, not interpreted. |
website, generator | no | Informational. |
items | yes | One entry per drawing; order is the order in the library. |
Per item:
| Key | Required | Meaning |
|---|---|---|
file | yes | Path of the SVG inside the archive. |
name | yes | English display name. |
names | no | Localized names by BCP 47 tag; the app picks its UI language, else name. |
kind | yes | shape or carve. |
lineArt | shape only | true = open outline for run or satin, never fill. Default: derived from the SVG (below). |
spacingMM | carve only | Gap between copies, default 0 (copies touch). |
stagger | carve only | Rows 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.
Shapes: one SVG per shape
Section titled “Shapes: one SVG per shape”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"plusstroke): an outline that is inserted as a run. One that closes (… Z) stays closed.lineArtin 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
useare not read, and an importer should say what was left out;rect,circle,ellipse,line,polyline,polygonandgwithtransformare. - 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>Carve tiles: one SVG per texture
Section titled “Carve tiles: one SVG per texture”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>Writing a pack
Section titled “Writing a pack”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
Section titled “Example”example.hooppack — the manifest above’s smaller cousin: one heart, one wave, under CC0. Unzip it to see the layout.
Reference implementation
Section titled “Reference implementation”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.