Open format specification
OXS Extension: Hardanger
An extension of the OXS (Open Cross Stitch) chart format that records Hardanger embroidery — kloster blocks, cut bands, bars, fillings, surface stitches, picots and beads — without changing a single byte of standard OXS data. Anyone may implement it, in any software, without asking permission.
The key words MUST, MUST NOT, SHOULD and MAY are used in their usual normative sense. Every element and attribute name below is literal.
1. Design rules
- New top-level elements only. The extension MUST NOT add attributes to existing OXS elements, and MUST NOT change the meaning of existing data. The only guarantee XML gives you is that an unknown element is skipped — an unknown attribute on a known element is not safe.
- Standard sections stay untouched. Full crosses remain in
<fullstitches>, beads and knots in<ornaments_inc_knots_and_beads>, outlines in<backstitches>. The extension only adds. - The file is self-describing. A filling carries both its catalog id and its resolved geometry (§7.4). The same file MUST render identically after the reader's filling catalog is updated.
- Computable geometry is not stored. Region classification, cut-dot positions and phase order are derived (§9). Storing them would create a second source of truth.
- Coordinates are integer thread counts. The one exception is resolved path geometry and the two point-anchored records (picot, bead), which allow fractions — that is thread in the air, not a position on the grid.
2. Coordinate system
unit="thread"— one unit is one fabric thread.threadsperunit="2"— how many threads make one cross-stitch cell. A reader MUST read this value and MUST NOT assume 2 (on 25 ct even-weave stitched over one it is 1).origin="node"—(0,0)is the top-left grid node (the hole), the same convention OXS uses for backstitch.stx_*coordinates are always 0-based. Some OXS readers guess whether a file is 1-based and shift the standard sections; that heuristic MUST NOT be applied tostx_*elements. Their coordinate system is declared here and is not subject to guessing.- Conversion: cell
(cx, cy)↔ thread(cx · threadsperunit, cy · threadsperunit). - Thread k of a band starting at
x1occupies the interval[x1+k, x1+k+1]; its centre isx1+k+0.5. You need this when drawing cut dots (§9.2).
<stx_satin x="2" y="2" axis="h" count="5" span="4" role="kloster">.
Five stitches occupy node columns 2…6 — that is four thread intervals, not five.Why threads. Hardanger is natively counted in threads, and
threadsperunit is the single declared conversion. A thread coordinate is also identical
to the in-memory representation used by implementations (half-cell fixed point ×2), so there is zero
conversion between file and model.
3. Declaring the extension
<stx_extensions version="1" vendor="obateam"
spec="https://stitchtracker.obateam.com/oxs-ext/1">
<stx_feature id="hardanger" version="1" required="true" fallback="comment"
summary="12 kloster blocks, 6 cut bands, 12 bars, 5 fillings, 4 eyelets, 0 beads, 2 straight stitches, 4 picots" />
</stx_extensions>
| Attribute | Meaning |
|---|---|
id | Technique family. Version 1 defines exactly one: hardanger. |
version | Version of that family's definition, independent of stx_extensions/@version. It states what the file actually requires of a reader — see §12. |
required | true = without support the chart is incomplete; a reader SHOULD warn the user. |
fallback | comment | none. How the content is signalled to a reader without support (§4). |
summary | Generated count summary, in English, for display. Informative only — a reader MUST NOT parse it as data. |
The reference writer emits eight counters in a fixed order: kloster blocks,
cut bands, bars, fillings, eyelets (every
<stx_stitch>, not only eyelets), beads,
straight stitches, picots. fallback="none" means the file
carries no marker at all — that is what a block with no writable records is written with.
<stx_feature> is the extension point. Future technique families
(silkribbon, drawnthread, blackwork) get their own
<stx_feature> and their own top-level block. Version 1 does not define them.
4. Fallback and data-loss detection
With fallback="comment" a writer MUST emit exactly one
<commentbox> marker whose text starts with the fixed sentinel
HARDANGER:.
<commentboxes>
<commentbox boxleft="4" boxtop="4" boxwidth="14" boxheight="14"
boxwords="HARDANGER: 12 kloster blocks, 6 cut bands, 12 bars, 5 fillings, 4 eyelets, 0 beads, 2 straight stitches, 4 picots. Chart incomplete without Hardanger support. Requires software with Hardanger support." />
</commentboxes>
- The attributes are
boxleft/boxtop/boxwidth/boxheight/boxwords— the real OXS convention that existing implementations read and write. An element with any other spelling (x/y/width/height/text…) is skipped wholesale by existing parsers. Units are cells (halves allowed):<commentboxes>is a standard section and keeps standard OXS units. boxwordsMUST start withHARDANGER:(case-sensitive), MUST be in English (the file travels) and MUST NOT contain stitching instructions — it is a signal, not chart content. It MUST NOT be empty; existing parsers drop comments with no text.boxleft/boxtop/boxwidth/boxheightSHOULD cover the bounding box of the Hardanger work, in cells.
Detecting data loss. A foreign program that opens and saves the file drops the
stx_* elements (it does not know them) but keeps <commentboxes> (it
does). The sentinel is therefore self-sufficient: detection does not depend on
<stx_extensions>, which is lost in that round-trip along with everything else
stx_*.
HARDANGER: marker | <stx_hardanger> block | Interpretation |
|---|---|---|
| no | no | The file never had Hardanger. |
| no | yes | Complete file (writer used fallback="none"). |
| yes | yes | Complete file. |
| yes | no | The file LOST its Hardanger data. A reader MUST warn and MUST NOT silently overwrite the original. |
Keeping the marker in sync. The marker is a writer artefact, not chart content:
- a full reader MUST recognise it by the
HARDANGER:prefix and MUST hide it from the user — it is not an ordinary comment and is not editable; - a writer MUST emit the marker if and only if the
<stx_hardanger>block carries records andfallback="comment"; removing all Hardanger from a chart removes the marker on the next save; - a file MUST contain at most one marker.
<backstitches>?
A backstitch is a stitch. A shadow outline would be embroidered as an outline and would
produce an artefact that looks like part of the chart and is not. A comment is text — nobody
stitches it. An optional objecttype="stx_shadow" MAY be added in
a future version as an export toggle defaulting to OFF; version 1 does not define it.
5. The stx_hardanger block
A document MUST contain at most one
<stx_hardanger>, as a sibling of the standard sections — one place to detect, one
place to skip.
<stx_hardanger unit="thread" threadsperunit="2" origin="node">
<stx_threads>…</stx_threads>
<stx_satins>…</stx_satins>
<stx_cuts>…</stx_cuts>
<stx_bars>…</stx_bars>
<stx_fillings>…</stx_fillings>
<stx_surface>…</stx_surface>
<stx_beads>…</stx_beads>
<stx_picots>…</stx_picots>
</stx_hardanger>
All collections are optional. Their order is insignificant; a reader MUST NOT depend on it.
The coordinate declaration is mandatory. unit="thread",
origin="node" and an integer threadsperunit ≥ 1
MUST be present on the block. A reader that cannot read them (missing,
different value, out of domain) MUST reject the whole block
rather than assume defaults: a record without a declared coordinate system is a record without
meaning, not a record in the default one.
Records MUST be direct children of their collection (one level below the block). A
reader MUST ignore records nested deeper, and — if a file breaks the
one-block rule — everything outside the first <stx_hardanger>. The exception is
<stx_path>, which is by definition a child of a record.
6. Threads
<stx_threads>
<stx_thread palindex="2" weight="perle8" />
<stx_thread palindex="3" weight="perle12" />
</stx_threads>
palindex refers to the standard OXS palette. weight ∈
perle3 | perle5 | perle8 | perle12 | floss | other. Thread weight is a real property of
a Hardanger chart (kloster blocks in perle 8, bars and eyelets in perle 12), not decoration: it
drives both rendering and the shopping list.
The collection describes the threads used by Hardanger records, not the whole palette: a thread whose records are all gone drops out on the next save. Beads are excluded — a bead (§7.8) is not a thread and has no weight.
Weight follows the product, not the record's role. The thickness of pearl cotton is
part of its number (“DMC Pearl 8”), so a writer derives the weight from the brand; the craft rule
(“satin in the thicker thread, the rest in the thinner”) says which thread to pick, not how thick the
thread you picked is — the actual pair depends on the fabric count (22 ct → 5 + 8, 28 ct → 8 + 12).
A writer SHOULD therefore derive a weight only for a thread that has no
entry yet, and MUST keep a weight it read from a file: re-deriving it
authoritatively turns an imported perle8 on an unrecognised brand into
other after the first edit, which is silent interop data loss.
7. Primitives
7.1 stx_satin — satin band, including the kloster block
<stx_satin x="12" y="8" axis="h" count="5" span="4"
palindex="2" role="kloster" dir="down" />
| Attribute | Meaning |
|---|---|
x, y | Top-left node of the bounding box, in threads. |
axis | h = stitches sit side by side along x (each stitch runs in y); v = the other way round. |
count | Number of satin stitches. |
span | Length of one stitch, in threads. |
role | kloster | satin | edge |
dir | up | down | left | right — the side the band secures. |
role="kloster" means “the author cuts beside this block” — a decision
recorded by the author, not a description of shape. The book is explicit: “klosters and
longer satin stitch blocks can be put together to form cutwork areas” (p. 18),
“sometimes there are more than five satin stitches to cut beside” (p. 34).
The canonical kloster block is count="5", span="4" — a
4×4-thread bounding box, because five stitches occupy node columns x … x+4, which is
four thread intervals. That is a special case, not a requirement: a legal cutwork
boundary has count ≥ 2 and span ≥ 1, and the number of threads released at
its head is always count − 1, so bands, pairs and windows use the same arithmetic. A
single-stitch block releases nothing and borders no cutwork; role="edge" (a finishing
border) never meets a window either.
dir is significant for validation and mirroring, not only for rendering: it names the
edge where the block secures the cut thread ends. Mirroring a band
MUST flip its dir.
7.2 stx_cut — a band of removed threads
<stx_cut dir="v" x1="12" y1="12" x2="16" y2="32" derived="1" />
| Attribute | Meaning |
|---|---|
dir | Direction the removed threads run: v = vertical, h = horizontal. |
x1,y1,x2,y2 | Bounding box of the band, in threads; x1<x2, y1<y2. |
derived | 1 = the band was computed from a pair of kloster blocks; 0 = explicit author override. |
This is not a rectangle of hole. The hole is an effect, not a datum. The
band describes the physical act: “the threads running in direction dir in this area were
pulled out”.
Why. A hole rectangle cannot describe an area where threads of only
one direction were removed — and that is exactly where bars come from. The whole lattice of
a 3×3 lattice of windows (9 holes, 12 bars, 4 intact fabric crossings) follows from
6 <stx_cut> elements — three vertical and three horizontal.
Hole rectangles would need 25 elements and still could not tell a horizontal bar from a vertical
one.
7.3 stx_bar — a bar
<stx_bar x1="12" y1="18" x2="16" y2="18" kind="woven" threads="4" palindex="3" />
x1,y1 → x2,y2 is the centre line of the band of threads that survived
the cut, not its bounding box. Such bands have an even width, so the centre line always falls on an
integer coordinate. threads = how many threads remain (typically 4).
kind ∈ wrapped | woven | divided | lacedBehind | twisted | zigzag |
overwrapped | buttonholed. overwrapped is a bar wrapped too densely for its
length, so it bulges (four of them around a window read as a circle); buttonholed is a
bar covered in buttonhole stitch. The list is additive and its order is frozen (the
kind's byte is a sort key and a record tag in implementations — see §9.3); new
kinds are appended at the end.
kind rejects the RECORD, not the file. A reader
MUST NOT substitute wrapped for it: a bar whose treatment you
do not know, drawn as wrapped, is a lie about the work rather than a graceful degradation. The
practical consequence is that a kind added by a newer writer is invisible to an older reader — that
one bar drops out of the chart and the rest of the file loads normally.
The existence of a bar is computable (§9.1), but kind is the
chart author's decision and MUST be stored. A reader
MUST NOT assume wrapped when the element is absent: absence
means the author did not specify the bar, not that it is wrapped.
7.4 stx_filling — a filling inside a window
<stx_filling x1="20" y1="20" x2="24" y2="24" catalog="doves-eye"
rot="0" mirror="0" palindex="3" weight="12">
<stx_path closed="1">22.00,20.00 22.51,20.43 … 22.00,20.00</stx_path>
</stx_filling>
x1..y2— the window (the area between bars and kloster blocks), in threads.catalog— id in the writer's filling catalog, kept for re-editing. A reader MAY know nothing about it.rot∈0|90|180|270,mirror∈0|1.weight— static work-unit weight (§10).<stx_path>— the resolved geometry: one or more polylines,x,yseparated by a comma, points separated by whitespace.closed="1"closes the polyline. Fractions allowed.
<stx_path> is normative; catalog is a hint.
A renderer MUST draw from <stx_path>. A writer
MUST emit it for every filling. This is what makes rule 3 of
§1 true: the file cannot start rendering differently because someone updated a
catalog.
Parsing is strict, because <stx_path> is normative. One
malformed token invalidates the whole polyline — normative geometry must never be silently
trimmed by a point — and a <stx_filling> left with no valid polyline is rejected
whole. Otherwise “draw from <stx_path>” quietly becomes “draw from whatever
parsed”. A <stx_stitch> (§7.5) lives without paths: its
geometry comes from the catalog plus span.
7.5 stx_stitch — a surface stitch
<stx_stitch x="16" y="16" catalog="eyelet" span="4"
palindex="3" rot="0" weight="8" />
Stitches anchored to a point or a square area rather than to a window between bars: the Algerian
eyelet, the four-sided stitch. span is the size in threads. Elements with complex
geometry MAY carry <stx_path> children on the same terms
as §7.4.
catalog MUST NOT be empty — this element describes a stitch
from a catalog. A stitch with neither a catalog entry nor a square footprint is
§7.6.
7.6 stx_straight — a single straight stitch
<stx_straight x1="12" y1="8" x2="19" y2="15" palindex="2" />
One straight stitch laid on uncut fabric, described by its two ends (nodes, in threads). It lives in
the <stx_surface> collection next to <stx_stitch>, but is a
separate element because it has neither a catalog id nor a square span — and
§7.5 requires both.
It is not a stx_satin with count="1".
stx_satin@axis accepts only h/v, so a diagonal stitch has
nowhere to live there without changing the meaning of an existing attribute — which would be a
breaking change (§12).
Angle is not a property of the format. The element carries geometry: two arbitrary
nodes. An editor may restrict the gesture to four directions; a reader
MUST accept any pair of nodes. x1,y1
MUST differ from x2,y2 — a zero-length stitch is not a stitch.
7.7 stx_picot — a picot
<stx_picots>
<stx_picot x="6" y="8" kind="knotted" dir="n" palindex="2" />
</stx_picots>
| Attribute | Meaning |
|---|---|
x,y | Anchor point on the host, in threads — fractional. |
kind | knotted | looped | chain |
dir | n ne e se s sw w nw — the direction the glyph sticks out of its host. |
palindex | Working thread. |
A picot is a knob attached to existing work: the middle of a bar (sticking into the hole), the
corner of a cut window (diagonally into the hole), or the side of a filling. It gets its own
collection rather than an attribute on <stx_bar>, because the host can also be a
filling — and a streaming parser does not bind children to parents anyway.
The format carries no reference to the host. Just as cut dots
(§9.2) are computed, a picot is self-contained geometry: anchor plus direction is
enough to draw the glyph, and choosing the host is an editor rule, not a file rule. A reader
MUST accept any legal point — including one with no work underneath it.
An unknown kind/dir rejects the record, not the file.
Bead picots do not use this element — a bead is a different material and goes to §7.8.
7.8 stx_bead — a bead in the openwork
<stx_beads>
<stx_bead x="22.0" y="20.0" palindex="6" />
</stx_beads>
A bead that is part of the Hardanger work — sitting in the corner of a filling, or on a bar as a bead picot. The position is in threads and is fractional, like path points: with a window other than 4×4 threads the corner of a filling does not land on a whole thread.
It is its own element rather than a filling path, because a bead is a different
material: drawing it as a stitch would lie to any consumer counting materials —
the shopping list would show more perle cotton where a packet of beads is needed. Ordinary chart
beads unrelated to the openwork stay in the standard
<ornaments_inc_knots_and_beads> section (§1 rule 2). A bead
therefore carries no weight (§10) and never appears in
<stx_threads> (§6).
The element belongs to the version 1 surface (§12), like every other record in §7.
8. Surface stitches vs. standard sections
A Hardanger surface stitch MUST go to <stx_surface>, never
to <backstitches>, even when its geometry could be expressed as lines. The reason:
<backstitches> has defined semantics (an outline stitch) and its own rendering,
which the extension MUST NOT change.
9. Derived semantics
Everything in this section MUST be computed by the reader and MUST NOT be stored in the file.
9.1 Region classification
For a point (x,y): v = covered by a band with dir="v",
h = covered by a band with dir="h".
v | h | Region |
|---|---|---|
| ✓ | ✓ | Hole — no fabric in either direction. |
| ✓ | ✗ | Horizontal threads survived → horizontal bar. |
| ✗ | ✓ | Vertical threads survived → vertical bar. |
| ✗ | ✗ | Intact fabric. |
Region boundaries are the edges of <stx_cut> bands. Renderer and validator use the
same function.
9.2 Cut-dot positions (chart style)
A dot marks where a thread is cut — the head of a band at a kloster block — not the outline of a hole. You only cut where a kloster secures the thread, then pull the thread out along its whole length.
For a band with dir="v", x1..x2, y1..y2, with
n = x2 − x1 threads:
- dots at
y = y1andy = y2(the two heads), - at positions
x = x1 + k + 0.5fork = 0 … n−1.
Analogously for dir="h". Interior holes of a lattice carry no dots —
nothing was cut there. The rule is fully computable from <stx_cut>; the format does
not carry dots.
unsecuredEdge validation error.9.3 Canonical order
Derived from the record type, never stored. The byte is both the record's type tag and the first sort key, and the order is frozen — a reader that tracks progress per record indexes by position in this sorted array, so renumbering would misalign someone else's progress:
0. <stx_satin> (all roles)
1. <stx_cut>
2. <stx_bar>
3. <stx_filling>
4. <stx_stitch> (the <stx_surface> collection)
5. <stx_bead>
6. <stx_straight> (same COLLECTION as <stx_stitch>, separate record type)
7. <stx_picot>
Within a type, records sort by their full written content, starting with (y, x). The
standard OXS sections have their own orders and do not interleave with this one.
This is the canonical order of the format, not a stitching order. The
stitching order comes from the craft and looks different: satin and kloster blocks → pulled thread →
surface stitches → crosses and outlines → cutting and withdrawing threads → bars →
fillings → ornaments. Surface stitches — including <stx_straight>, which lies on
whole fabric — therefore come before the cutting, not after it: “everything else should be
correctly positioned before any threads are cut”. An author MAY describe
their own order in the standard properties/@instructions; the format adds no field for
it.
10. Work-unit weights
weight is the approximate number of needle passes. It is static and
MUST be in the file, so that a reader can compute progress or time
without a filling catalog.
| Unit | weight |
|---|---|
| Full cross stitch (for reference) | 1 |
Single straight stitch (stx_straight) | 1 |
Picot (stx_picot) | 1 |
| Kloster block (5 stitches) | 5 |
| Wrapped bar, 4 threads | 8 |
| Twisted bar, 4 threads | 8 |
| Zig-zag line, 4 threads | 6 |
| 16-point Algerian eyelet | 8 |
| Dove's eye | 12 |
| Square filet | 16 |
Bead (stx_bead) | — counted in pieces, not needle passes |
Catalog fillings carry their own weight attribute on the record, so a reader never needs
this table for types outside it. A reader computing a percentage SHOULD use a
weighted sum; for a chart without Hardanger every weight is 1, so the result is bit-identical to
counting units.
11. Conformance
Minimal reader (no Hardanger support)
- MUST skip unknown
stx_*elements without error; - will show the marker comment box like any other comment — that is the intended fallback: its text tells the user the chart needs Hardanger support;
- SHOULD warn when it understands
<stx_extensions>but notid="hardanger"withrequired="true".
Full reader
- MUST read
threadsperunitrather than assume a value; - MUST read
stx_*coordinates as 0-based, with no shifting heuristics (§2); - MUST compute §9 instead of expecting it in the file;
- MUST draw fillings from
<stx_path>, not fromcatalog; - MUST detect the data-loss case of §4 (marker with no block) and warn;
- MUST hide the marker comment box from the user;
- MUST put every numeric attribute through one domain chokepoint and
reject the record (not the file, and not the value silently replaced by another)
when a value is out of domain — a hostile file must neither trap nor brick the chart. The domains:
fractions finite and
|v| < 1e9; integer coordinates0…65 535;palindex0…65 534;count/span/threads/threadsperunit≥ 1;derived/mirror/closedexactly0or1;rot0|90|180|270;weight0…1 000 000. A missing required attribute (for instancederivedon<stx_cut>, orcatalogon a filling or surface stitch) rejects the record just like an out-of-domain value; - MUST reject a record carrying an unknown enum value
(
kind,dir,role,axis, threadweight) instead of substituting a default; - MUST require
x1 < x2andy1 < y2where a record is a rectangle (<stx_cut>,<stx_filling>), andx1,y1 ≠ x2,y2in<stx_straight>; - MUST NOT change how
<backstitches>renders.
Writer
- MUST emit
<stx_extensions>whenever the Hardanger block is non-empty; - MUST tie
fallbackand the marker to writable records rather than to the mere presence of the block: a block carrying only<stx_threads>or a non-defaultthreadsperunithides nothing from a minimal reader, so it goes out withfallback="none"and no marker. A marker saying “0 kloster blocks…” would come back from a foreign round-trip as a false data-loss report; - with
fallback="comment": MUST emit exactly one marker (§4) and MUST NOT emit one when the block carries no records; - MUST emit
<stx_path>for every<stx_filling>, and MUST drop a filling with no resolved geometry from the export rather than write it without a path; - MUST NOT store anything from §9;
- MUST NOT add attributes to standard OXS elements.
12. Versioning policy
stx_extensions/@version— version of the extension mechanism. A change here is a breaking change.stx_feature/@version— version of the family definition. New optional attributes and new enum values raise it and MUST be skippable by a version 1 reader.- New technique families MUST arrive as a new
<stx_feature id="…">plus a new top-level block, never by stretching<stx_hardanger>. - Removing an existing attribute, or changing its meaning, MUST raise
stx_extensions/@version.
Family hardanger version 1 is the entire surface described in
§5–§9: satins, cut bands, bars with all eight kinds,
fillings, surface stitches, beads, straight stitches and picots. A file conforming to this document
MUST declare version="1" whichever of those records it
carries.
The adaptive mechanism switches on with the first addition after publication. From
then on stx_feature/@version MUST be the highest version the
file actually requires, not the highest the writer knows: a file with no version 2 records
MUST still declare 1, because with required="true"
a hard-coded 2 would make a cautious version 1 reader reject a chart that is otherwise
entirely version 1 — a file it opened the day before. A reader keeps the highest family version it
supports and reports anything higher as unsupported instead of drawing it silently
incomplete.
13. Out of scope for version 1
- Silk ribbon (lazy daisy, spider web rose, French knots) — real, and it turns up in
the same charts as Hardanger, but it is a separate family:
<stx_feature id="silkribbon">in a future version. The architecture of §3 plans for it. - Other drawn-thread techniques (Schwalm, reticello, hemstitch) — separate
ids. - Textual instructions and left-/right-handed variants — not format content.
- The filling catalog itself — the format carries a
catalogid and the resolved path; the catalog is an implementation artefact, not part of the format. - Import from proprietary formats — “special stitches” blobs there are undocumented and cannot be mapped faithfully.
14. Reference file
A real chart exported from StitchTracker Studio, not a hand-written sample: 27×27 cells, two medallions, 32 kloster blocks, 12 cut bands, 12 bars and 6 fillings (maltese flower and twisted Y, each with its resolved multi-polyline geometry), plus 264 backstitches, 3 full crosses and 5 beads in the standard OXS sections. Opened by a reader without Hardanger support it shows the cross-stitch skeleton, the outlines and the marker comment box; opened by a full reader it shows the openwork.
Implementations today: StitchTracker (reader, iPadOS/iOS) and StitchTracker Studio (writer). If you are implementing this extension in your own software and something here is ambiguous, or you need a test vector for a case the reference file does not cover, write to krzysiekobateam@gmail.com — corrections to this document are welcome.