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.

Extension version
1
Feature family
hardanger, version 1
Vendor
obateam
Status
Stable, shipping
This document
2026-09-11
Base format
OXS

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

0 2 4 6 8 10 0 2 4 6 8 x="2" y="2" count = 5 satin stitches span = 4 threads · bbox 4×4
One kloster block: <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>
AttributeMeaning
idTechnique family. Version 1 defines exactly one: hardanger.
versionVersion of that family's definition, independent of stx_extensions/@version. It states what the file actually requires of a reader — see §12.
requiredtrue = without support the chart is incomplete; a reader SHOULD warn the user.
fallbackcomment | none. How the content is signalled to a reader without support (§4).
summaryGenerated 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>

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> blockInterpretation
nonoThe file never had Hardanger.
noyesComplete file (writer used fallback="none").
yesyesComplete file.
yesnoThe 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:

Why not a shadow outline in <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" />
AttributeMeaning
x, yTop-left node of the bounding box, in threads.
axish = stitches sit side by side along x (each stitch runs in y); v = the other way round.
countNumber of satin stitches.
spanLength of one stitch, in threads.
rolekloster | satin | edge
dirup | 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.

Off-by-one. The canonical block is five stitches across four threads. Not four across four, not five across five. It is the classic implementation bug and the most common beginner's mistake at the fabric, too. The four-thread rhythm check applies to the canonical block only — a seven-stitch block spans six threads and is not an error.

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" />
AttributeMeaning
dirDirection the removed threads run: v = vertical, h = horizontal.
x1,y1,x2,y2Bounding box of the band, in threads; x1<x2, y1<y2.
derived1 = 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.

An unknown 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>
<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.
Implementation note. The points live in the element's text content, not in an attribute. Event-driven parsers (SAX/XMLParser) that so far only read attributes have to add character-content handling for this one element.

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>
AttributeMeaning
x,yAnchor point on the host, in threads — fractional.
kindknotted | looped | chain
dirn ne e se s sw w nw — the direction the glyph sticks out of its host.
palindexWorking 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".

vhRegion
✓✓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:

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.

x1=2 x2=6 y1=4 y2=8 kloster secures the ends 4 threads pulled out (dir="v") dots at x1+k+0.5 on y1 and y2 only
Dots sit at the two heads of the band, on thread centres — never around the hole. A dot with no kloster behind it is the 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.

Unitweight
Full cross stitch (for reference)1
Single straight stitch (stx_straight)1
Picot (stx_picot)1
Kloster block (5 stitches)5
Wrapped bar, 4 threads8
Twisted bar, 4 threads8
Zig-zag line, 4 threads6
16-point Algerian eyelet8
Dove's eye12
Square filet16
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)

Full reader

Writer

12. Versioning policy

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

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.

⬇ hardanger-demo.oxs (115 KB)

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.