PES file structure
This page documents the PES bytes Satin Studio currently reads or writes, plus version/header facts we use for compatibility research. The byte-level notes are based on Satin Studio's stitchcodecs implementation and public PES/PEC format research.
PES is a hybrid file: the PES section stores higher-level design/application data, while the PEC section stores the machine stitch payload. Satin Studio currently imports the PEC stitch payload and exports a compact/truncated #PES0060 wrapper around a PEC payload.
All integer and floating-point fields below are little-endian unless the field name says _be. Structs are Rust-like documentation, not Rust API.
Basic serialized types
type U8 = u8; // 1 byte
type U16 = u16; // 2 bytes, little-endian
type U24 = [u8; 3]; // 3 bytes, little-endian where documented
type U32 = u32; // 4 bytes, little-endian
type F32 = f32; // 4 bytes, little-endian IEEE-754
struct String8<const LEN: usize> {
/// One byte, max 255.
len: U8,
/// Exactly `len` bytes. Strings are not NUL-terminated.
bytes: [u8; LEN],
}
Common PES prefix
Every PES file Satin Studio recognizes starts with this 12-byte prefix:
struct PesPrefix {
/// Bytes 0..4: always `*b"#PES"`.
magic: [u8; 4],
/// Bytes 4..8: four ASCII digits, for example `*b"0060"`.
version_digits: [u8; 4],
/// Bytes 8..12. Absolute offset from file start to the PEC payload area.
/// Observed compatible files usually point directly at `b"LA:"`.
/// Some files may point at `b"#PEC0001"`; in that case the PEC payload
/// begins eight bytes later.
pec_payload_offset: U32,
}
Documented PES signatures:
| Signature | Meaning in docs/tools | Satin Studio behavior |
|---|---|---|
#PES0001 | PES v1 | Imports when pec_payload_offset locates compatible PEC stitch data. |
#PES0020 | PES v2 | Imports when compatible PEC stitch data is present. |
#PES0025 | PES v2.5 | Recognized as a PES version; needs public compatibility samples. |
#PES0030 | PES v3 | Imports when compatible PEC stitch data is present. |
#PES0040 | PES v4 | Imports when compatible PEC stitch data is present. |
#PES0050 | PES v5 | Imports when compatible PEC stitch data is present. |
#PES0055 | PES v5.5 | Imports when compatible PEC stitch data is present. |
#PES0056 | PES v5.6 | Recognized as a PES version; needs public compatibility samples. |
#PES0060 | PES v6 | Imports compatible PEC data; Satin Studio exports a compact subset. |
#PES0070 | PES v7 | Imports when compatible PEC stitch data is present. |
#PES0080 | PES v8 | Imports when compatible PEC stitch data is present. |
#PES0090 | PES v9 | Imports when compatible PEC stitch data is present. |
#PES0100 | PES v10 | Imports when compatible PEC stitch data is present. |
Files with .pes extensions that do not begin with a supported PES or PEC signature are rejected as unsupported rather than guessed.
Current Satin Studio export: compact #PES0060
Satin Studio exports a compact/truncated PES v6 file. It writes no PES object blocks (CEmbOne, CSewSeg, vector objects, or order blocks); the machine stitch data lives in the embedded PEC payload.
struct SatinPesV6File<const THREADS: usize> {
/// `magic = *b"#PES"`, `version_digits = *b"0060"`.
/// `pec_payload_offset` points to `pec_payload` beginning with `b"LA:"`.
prefix: PesPrefix,
header: SatinPesV6Header<THREADS>,
/// Literal zero padding after the truncated PES header.
post_header_zero_padding: [u8; 5], // [0x00, 0x00, 0x00, 0x00, 0x00]
/// Literal zero fields before the PEC payload.
post_header_zero_a: U16, // 0x0000
post_header_zero_b: U16, // 0x0000
/// Embedded PEC payload without a leading `#PEC0001` signature.
pec_payload: PecPayloadNoSignature,
/// PES-side addendum after the PEC graphics region.
addendum: SatinPesV6Addendum<THREADS>,
/// Present in Satin Studio v6 exports.
final_zero: U16, // 0x0000
}
Compact v6 header
struct SatinPesV6Header<const THREADS: usize> {
hoop_size_indicator: U16, // 0x0001
subversion_text: [u8; 2], // *b"02"
design_name: String8<12>, // "StitchCodecs"
category: String8<0>, // empty: one length byte, 0x00
author: String8<0>, // empty: one length byte, 0x00
keywords: String8<0>, // empty: one length byte, 0x00
comments: String8<0>, // empty: one length byte, 0x00
optimize_hoop_change: U16, // 0x0000
design_page_is_custom: U16, // 0x0000
hoop_width_mm: U16, // 0x0064 (100)
hoop_height_mm: U16, // 0x0064 (100)
hoop_rotation_90_degrees: U16, // 0x0000
design_width_mm: U16, // 0x00c8 (200)
design_height_mm: U16, // 0x00c8 (200)
design_page_section_width: U16, // 0x0064 (100)
design_page_section_height: U16, // 0x0064 (100)
unknown_design_property: U16, // 0x0064 (100)
design_page_background: U16, // 0x0007
design_page_foreground: U16, // 0x0013
show_grid: U16, // 0x0001
with_axes: U16, // 0x0001
snap_to_grid: U16, // 0x0000
grid_interval: U16, // 0x0064 (100)
unknown_p9: U16, // 0x0001
optimize_entry_exit_points: U16, // 0x0000
from_image_path: String8<0>, // empty: one length byte, 0x00
/// Affine transform for an optional background image.
image_transform_a: F32, // 1.0
image_transform_b: F32, // 0.0
image_transform_c: F32, // 0.0
image_transform_d: F32, // 1.0
image_transform_e: F32, // 0.0
image_transform_f: F32, // 0.0
programmable_fill_pattern_count: U16, // 0x0000
motif_pattern_count: U16, // 0x0000
feather_pattern_count: U16, // 0x0000
thread_count: U16, // THREADS, truncated to u16::MAX by the writer
threads: [SatinPesV6Thread; THREADS],
/// Number of PES object blocks. Satin Studio's compact export writes zero
/// and relies on the following PEC payload for machine stitches.
pes_object_count: U16, // 0x0000
}
Thread records are variable-length because the three text fields are String8 values:
struct SatinPesV6Thread<const CATALOG: usize, const NAME: usize, const BRAND: usize> {
catalog_number: String8<CATALOG>,
/// Exact byte order in the PES header.
red: U8,
green: U8,
blue: U8,
reserved_zero: U8, // 0x00
thread_type: U32, // 0x0000000a
name: String8<NAME>,
brand: String8<BRAND>,
chart: String8<12>, // "StitchCodecs"
}
V6 addendum
PES v4+ files can include a PEC addendum after the traditional PEC graphics block, and v6 compatibility may depend on this area. Satin Studio writes this addendum exactly as follows:
struct SatinPesV6Addendum<const THREADS: usize> {
/// Exactly 128 bytes. The first bytes are the PEC color table:
/// - if non-empty: one `color_count_minus_one` byte, then one PEC palette
/// index byte per thread/color block;
/// - remaining bytes are literal spaces, `0x20`.
color_table_then_space_padding: [u8; 128],
/// One all-zero 144-byte block per thread.
per_thread_zero_padding: [[u8; 0x90]; THREADS],
/// One 24-bit color per thread. Disk byte order is blue, green, red.
thread_colors_bgr: [[u8; 3]; THREADS],
}
The final two zero bytes after this addendum are represented by SatinPesV6File::final_zero.
Older PES header shapes
Satin Studio does not currently parse these PES metadata fields into editable project objects, but the structures are useful when classifying files. All begin with PesPrefix.
struct PesV1AfterPrefix {
hoop_size_or_scale_to_fit: U16, // commonly 0x0001
use_existing_design_area: U16, // commonly 0x0001
segment_block_count: U16,
// PES v1 files may then contain CEmbOne / CSewSeg blocks before PEC.
}
struct PesV2AfterPrefix {
hoop_size: HoopSize,
hoop_rotation_90_degrees: U16,
design_page_background: U16,
design_page_foreground: U16,
show_grid: U16,
with_axes: U16,
snap_to_grid: U16,
grid_interval: U16,
p9_curves: U16,
optimize_entry_exit_points: U16,
}
struct PesV25AfterPrefix {
unknown_0: U16, // commonly 0x0001
unknown_1: U16, // commonly 0x0000
v2_fields: PesV2AfterPrefix,
pes_object_count: U16,
}
struct PesV3AfterPrefix {
unknown_0: U16, // commonly 0x0001
subversion_text: [u8; 2], // observed as *b"02" or *b"10"
v2_fields: PesV2AfterPrefix,
}
struct PesV4AfterPrefix<const NAME: usize, const CATEGORY: usize, const AUTHOR: usize, const KEYWORDS: usize, const COMMENTS: usize> {
unknown_0: U16, // commonly 0x0001
subversion_text: [u8; 2], // observed as *b"02" or *b"10"
description: PesDescriptionStrings<NAME, CATEGORY, AUTHOR, KEYWORDS, COMMENTS>,
optimize_hoop_change: U16,
v2_fields: PesV2AfterPrefix,
trailing_unknown: [u8; 9], // one u8-like flag plus seven zero bytes plus one u16-like flag in source tables
}
struct PesV5AfterPrefix<const NAME: usize, const CATEGORY: usize, const AUTHOR: usize, const KEYWORDS: usize, const COMMENTS: usize, const IMAGE_PATH: usize, const THREADS: usize> {
hoop_size_indicator: U16,
subversion_text: [u8; 2],
description: PesDescriptionStrings<NAME, CATEGORY, AUTHOR, KEYWORDS, COMMENTS>,
optimize_hoop_change: U16,
hoop_size: HoopSize,
hoop_rotation_90_degrees: U16,
design_page_background: U16,
design_page_foreground: U16,
show_grid: U16,
with_axes: U16,
snap_to_grid: U16,
grid_interval: U16,
unknown_p9: U16,
optimize_entry_exit_points: U16,
from_image_path: String8<IMAGE_PATH>,
image_transform: AffineTransform,
programmable_fill_patterns: CountedUnknownList,
motif_patterns: CountedUnknownList,
feather_patterns: CountedUnknownList,
threads: PesColorList<THREADS>,
pes_object_count: U16,
}
Shared substructures:
struct HoopSize {
width_mm: U16,
height_mm: U16,
}
struct AffineTransform {
a: F32,
b: F32,
c: F32,
d: F32,
e: F32,
f: F32,
}
struct PesDescriptionStrings<const NAME: usize, const CATEGORY: usize, const AUTHOR: usize, const KEYWORDS: usize, const COMMENTS: usize> {
name: String8<NAME>,
category: String8<CATEGORY>,
author: String8<AUTHOR>,
keywords: String8<KEYWORDS>,
comments: String8<COMMENTS>,
}
struct PesColorList<const THREADS: usize> {
thread_count: U16,
threads: [SatinPesV6Thread; THREADS],
}
struct CountedUnknownList {
count: U16,
// Followed by `count` version-specific records. Satin Studio currently
// writes count 0 for programmable fill, motif, and feather patterns.
}
PES v6 uses the v5 family of fields plus design_page_is_custom, design_width_mm, design_height_mm, design_page_section_width, design_page_section_height, and unknown_design_property, as shown in SatinPesV6Header above.
Embedded PEC payload
The embedded machine payload is documented on the PEC file structure page.
Notes
PES is partially documented in public format research, but machine and software behavior still varies by version. Satin Studio documents the exact subset it reads and writes here rather than treating any third-party description as authoritative.