@richardmcquiston01/unofficial-xcs-writer 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +86 -0
- package/CHANGELOG.md +15 -0
- package/README.md +12 -95
- package/package.json +3 -2
package/API.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## `assertXcsFormat(buffer: ArrayBuffer): void`
|
|
4
|
+
|
|
5
|
+
Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
|
|
6
|
+
|
|
7
|
+
## `readXcsFile(buffer: ArrayBuffer): string`
|
|
8
|
+
|
|
9
|
+
Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
|
|
10
|
+
|
|
11
|
+
## `extractXcsTokens(buffer: ArrayBuffer): string[]`
|
|
12
|
+
|
|
13
|
+
Scans all `TEXT` display objects across all canvases and returns the unique token names found (without braces). For example, `{{LastName}}` in the file → `"LastName"` in the returned array.
|
|
14
|
+
|
|
15
|
+
## `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
|
|
16
|
+
|
|
17
|
+
Applies substitution to every `TEXT` display object and returns the modified file encoded as a UTF-8 `Uint8Array`. Tokens with no matching entry in `values` fall back to `variable.defaultValue`, or an empty string if no default is set.
|
|
18
|
+
|
|
19
|
+
Whenever a display's text actually changes, its glyph outline data (`charJSONs` / `fontData.glyphData`) is regenerated from a real font so xTool Studio renders the new text correctly — xTool Studio only trusts a TEXT display's stored glyph shapes; it does not re-shape text on its own unless you manually reselect the font inside the app. Displays whose text doesn't change are left completely untouched.
|
|
20
|
+
|
|
21
|
+
Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
|
|
25
|
+
renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Any `fontFamily` not present in `fonts` falls back to a bundled default (Arimo, Apache-2.0, metric-compatible with Arial) — so the fix works out of the box with no font files required, just with shapes drawn from Arimo rather than the exact requested typeface.
|
|
29
|
+
|
|
30
|
+
**Known limitation:** `renderXcsFile` only reproduces straight horizontal-baseline layout when substituting into an *existing* TEXT display. xTool Studio's `style.curveX`/`curveY` curved-text formula is undocumented and can't be reverse-decoded from a file alone, so a curved display's substituted text gets correctly-shaped glyphs laid out straight instead of on the original curve. This doesn't apply to the *building* API below — there, you supply the curve angle yourself (see `layoutCurvedGlyphText`), so there's nothing to decode.
|
|
31
|
+
|
|
32
|
+
## `XcsVariable`
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
interface XcsVariable {
|
|
36
|
+
token: string; // token name without braces, e.g. "LastName"
|
|
37
|
+
defaultValue?: string;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## `.xs` (v2 workspace) format
|
|
42
|
+
|
|
43
|
+
## `assertXsFormat(buffer: ArrayBuffer): void`
|
|
44
|
+
|
|
45
|
+
Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
|
|
46
|
+
|
|
47
|
+
## `extractXsTokens(buffer: ArrayBuffer): string[]`
|
|
48
|
+
|
|
49
|
+
Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
|
|
50
|
+
|
|
51
|
+
## `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
|
|
52
|
+
|
|
53
|
+
Same substitution and glyph-regeneration behavior as `renderXcsFile` (see above — including the same `fonts` parameter and curved-text limitation), applied to every displays chunk. Returns a re-zipped `.xs` archive; every entry other than the modified displays chunks is passed through byte-for-byte unchanged.
|
|
54
|
+
|
|
55
|
+
## `buildXsArchive(file: XCSFile): Uint8Array`
|
|
56
|
+
|
|
57
|
+
Packages an already-`generate()`d project (see Building, below) into a `.xs` v2 ZIP archive. You won't normally call this directly — use `XCSGenerator.toXsBytes()` instead, which calls it for you.
|
|
58
|
+
|
|
59
|
+
## `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
|
|
60
|
+
|
|
61
|
+
Lower-level glyph-extraction primitives everything else in this library is built on, exported for direct use. See `src/glyphs.ts` for the exact xTool JSON conventions these were reverse-engineered against.
|
|
62
|
+
|
|
63
|
+
## Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
|
|
64
|
+
|
|
65
|
+
`createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). `deviceId` (and `options.deviceId`) accepts a `XTOOL_MACHINES` key, a `MachineProfile` object, or a raw device id string — see "Building a project from scratch" in the [README](./README.md). Chainable methods:
|
|
66
|
+
|
|
67
|
+
| Method | Adds |
|
|
68
|
+
|--------|------|
|
|
69
|
+
| `.addText(text, x, y, options?)` | A `TEXT` display. Pass `options.layout` (see below) for real glyph outlines — without it, text renders as placeholder blocks in xTool Studio. |
|
|
70
|
+
| `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
|
|
71
|
+
| `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
|
|
72
|
+
| `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
|
|
73
|
+
|
|
74
|
+
`.addText()`/`.addPath()`/`.addBitmap()` all also accept a `processing: { processingType, values }` option — see "Building a project from scratch" in the [README](./README.md); it only affects `.toXsBytes()` output.
|
|
75
|
+
|
|
76
|
+
Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string), `.toBytes()` (`Uint8Array`, matching `renderXcsFile`'s output type, as `.xcs`), or `.toXsBytes()` (`Uint8Array`, matching `renderXsFile`'s output type, as `.xs`).
|
|
77
|
+
|
|
78
|
+
Text layout — build the `layout` option for `.addText()` with one of:
|
|
79
|
+
|
|
80
|
+
| Function | Layout |
|
|
81
|
+
|----------|--------|
|
|
82
|
+
| `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
|
|
83
|
+
| `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
|
|
84
|
+
| `layoutCurvedGlyphText(font, text, emSizeMm, curveDeg, boxWidthMm, boxHeightMm, align?, letterSpacingMm?)` | Single-line, curved along an arc spanning `curveDeg` degrees across a `boxWidthMm`-wide box (positive arches the apex upward, negative bows it downward — same convention as SVG `<textPath>`/canvas curved-text editors). Multi-line curved text isn't supported (matches typical curved-text editors, which also flatten multi-line curved text to one line). **Not yet visually verified against real xTool Studio rendering** (there's no way to check outside the application itself) — the geometry is self-consistent and unit-tested, but treat curved output as best-effort until you've spot-checked it. |
|
|
85
|
+
|
|
86
|
+
All three return `GlyphTextLayout | null` (`null` for empty/whitespace-only text). `translateGlyphLayout(layout, dxMm, dyMm)` cheaply shifts an already-built layout without relaying out the font.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.1] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **README restructure**: removed the "Releasing" subsection (internal maintainer process,
|
|
15
|
+
not user-facing); moved the full "API" section into a new `API.md` (added to `package.json`'s
|
|
16
|
+
`files` so it ships with the npm package), with the README's "API" section now just a link to
|
|
17
|
+
it; added a disclaimer blockquote under the title stating this is an unofficial,
|
|
18
|
+
community-built library not affiliated with xTool; removed the hyperlink on "xTool Creative
|
|
19
|
+
Space" in the intro paragraph (kept as plain text); moved "Buy Me a Coffee" up to sit directly
|
|
20
|
+
above the API section instead of at the very end.
|
|
21
|
+
- **Copyright section**: added a note that xTool, xTool Studio, and xTool Creative Space are
|
|
22
|
+
registered trademarks of Makeblock Co., Ltd., and that this project isn't affiliated with,
|
|
23
|
+
endorsed by, or sponsored by Makeblock or xTool.
|
|
24
|
+
|
|
10
25
|
## [0.8.0] - 2026-09-21
|
|
11
26
|
|
|
12
27
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# unofficial-xcs-writer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Disclaimer:** This is an unofficial, community-built library. It is not affiliated with, endorsed by, or sponsored by xTool in any way.
|
|
4
|
+
|
|
5
|
+
A framework-agnostic TypeScript library for reading, writing, building, and applying variable substitution to `.xcs` and `.xs` files produced by xTool Creative Space.
|
|
4
6
|
|
|
5
7
|
## What is an XCS / XS file?
|
|
6
8
|
|
|
@@ -134,92 +136,17 @@ A device id not in the catalog still works exactly as before (no `.xs` `deviceCo
|
|
|
134
136
|
`extName`/`device.id` all set to that string, default power `55`); you can also pass a
|
|
135
137
|
`MachineProfile` object directly for a machine not yet in the catalog.
|
|
136
138
|
|
|
137
|
-
##
|
|
138
|
-
|
|
139
|
-
### `assertXcsFormat(buffer: ArrayBuffer): void`
|
|
140
|
-
|
|
141
|
-
Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
|
|
142
|
-
|
|
143
|
-
### `readXcsFile(buffer: ArrayBuffer): string`
|
|
144
|
-
|
|
145
|
-
Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
|
|
146
|
-
|
|
147
|
-
### `extractXcsTokens(buffer: ArrayBuffer): string[]`
|
|
148
|
-
|
|
149
|
-
Scans all `TEXT` display objects across all canvases and returns the unique token names found (without braces). For example, `{{LastName}}` in the file → `"LastName"` in the returned array.
|
|
150
|
-
|
|
151
|
-
### `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
|
|
152
|
-
|
|
153
|
-
Applies substitution to every `TEXT` display object and returns the modified file encoded as a UTF-8 `Uint8Array`. Tokens with no matching entry in `values` fall back to `variable.defaultValue`, or an empty string if no default is set.
|
|
154
|
-
|
|
155
|
-
Whenever a display's text actually changes, its glyph outline data (`charJSONs` / `fontData.glyphData`) is regenerated from a real font so xTool Studio renders the new text correctly — xTool Studio only trusts a TEXT display's stored glyph shapes; it does not re-shape text on its own unless you manually reselect the font inside the app. Displays whose text doesn't change are left completely untouched.
|
|
156
|
-
|
|
157
|
-
Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
|
|
161
|
-
renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Any `fontFamily` not present in `fonts` falls back to a bundled default (Arimo, Apache-2.0, metric-compatible with Arial) — so the fix works out of the box with no font files required, just with shapes drawn from Arimo rather than the exact requested typeface.
|
|
165
|
-
|
|
166
|
-
**Known limitation:** `renderXcsFile` only reproduces straight horizontal-baseline layout when substituting into an *existing* TEXT display. xTool Studio's `style.curveX`/`curveY` curved-text formula is undocumented and can't be reverse-decoded from a file alone, so a curved display's substituted text gets correctly-shaped glyphs laid out straight instead of on the original curve. This doesn't apply to the *building* API below — there, you supply the curve angle yourself (see `layoutCurvedGlyphText`), so there's nothing to decode.
|
|
167
|
-
|
|
168
|
-
### `XcsVariable`
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
interface XcsVariable {
|
|
172
|
-
token: string; // token name without braces, e.g. "LastName"
|
|
173
|
-
defaultValue?: string;
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### `.xs` (v2 workspace) format
|
|
178
|
-
|
|
179
|
-
### `assertXsFormat(buffer: ArrayBuffer): void`
|
|
180
|
-
|
|
181
|
-
Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
|
|
182
|
-
|
|
183
|
-
### `extractXsTokens(buffer: ArrayBuffer): string[]`
|
|
184
|
-
|
|
185
|
-
Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
|
|
186
|
-
|
|
187
|
-
### `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
|
|
188
|
-
|
|
189
|
-
Same substitution and glyph-regeneration behavior as `renderXcsFile` (see above — including the same `fonts` parameter and curved-text limitation), applied to every displays chunk. Returns a re-zipped `.xs` archive; every entry other than the modified displays chunks is passed through byte-for-byte unchanged.
|
|
190
|
-
|
|
191
|
-
### `buildXsArchive(file: XCSFile): Uint8Array`
|
|
192
|
-
|
|
193
|
-
Packages an already-`generate()`d project (see Building, below) into a `.xs` v2 ZIP archive. You won't normally call this directly — use `XCSGenerator.toXsBytes()` instead, which calls it for you.
|
|
194
|
-
|
|
195
|
-
### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
|
|
196
|
-
|
|
197
|
-
Lower-level glyph-extraction primitives everything else in this library is built on, exported for direct use. See `src/glyphs.ts` for the exact xTool JSON conventions these were reverse-engineered against.
|
|
198
|
-
|
|
199
|
-
### Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
|
|
200
|
-
|
|
201
|
-
`createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). `deviceId` (and `options.deviceId`) accepts a `XTOOL_MACHINES` key, a `MachineProfile` object, or a raw device id string — see "Building a project from scratch" above. Chainable methods:
|
|
202
|
-
|
|
203
|
-
| Method | Adds |
|
|
204
|
-
|--------|------|
|
|
205
|
-
| `.addText(text, x, y, options?)` | A `TEXT` display. Pass `options.layout` (see below) for real glyph outlines — without it, text renders as placeholder blocks in xTool Studio. |
|
|
206
|
-
| `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
|
|
207
|
-
| `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
|
|
208
|
-
| `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
|
|
139
|
+
## Buy Me a Coffee
|
|
209
140
|
|
|
210
|
-
|
|
141
|
+
If this app, code, or repository has helped you or someone you know, please consider donating. I appreciate any help to offset the costs of development and/or AI Credits.
|
|
211
142
|
|
|
212
|
-
|
|
143
|
+
[**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
|
|
213
144
|
|
|
214
|
-
|
|
145
|
+
[](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
|
|
215
146
|
|
|
216
|
-
|
|
217
|
-
|----------|--------|
|
|
218
|
-
| `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
|
|
219
|
-
| `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
|
|
220
|
-
| `layoutCurvedGlyphText(font, text, emSizeMm, curveDeg, boxWidthMm, boxHeightMm, align?, letterSpacingMm?)` | Single-line, curved along an arc spanning `curveDeg` degrees across a `boxWidthMm`-wide box (positive arches the apex upward, negative bows it downward — same convention as SVG `<textPath>`/canvas curved-text editors). Multi-line curved text isn't supported (matches typical curved-text editors, which also flatten multi-line curved text to one line). **Not yet visually verified against real xTool Studio rendering** (there's no way to check outside the application itself) — the geometry is self-consistent and unit-tested, but treat curved output as best-effort until you've spot-checked it. |
|
|
147
|
+
## API
|
|
221
148
|
|
|
222
|
-
|
|
149
|
+
See [API.md](./API.md) for the full API reference — function signatures, options, types, and important behavior notes (glyph regeneration, curved-text limitations, machine catalog usage, etc.).
|
|
223
150
|
|
|
224
151
|
## Development
|
|
225
152
|
|
|
@@ -239,14 +166,6 @@ Output files after `pnpm build`:
|
|
|
239
166
|
| `dist/index.cjs` | CommonJS |
|
|
240
167
|
| `dist/index.d.ts` | TypeScript declarations |
|
|
241
168
|
|
|
242
|
-
### Releasing
|
|
243
|
-
|
|
244
|
-
Publishing to npm is automated. Bump the version in `package.json`, add a
|
|
245
|
-
`CHANGELOG.md` entry, then create a GitHub release tagged `vX.Y.Z` — the
|
|
246
|
-
[publish workflow](.github/workflows/publish.yml) checks the tag against
|
|
247
|
-
`package.json`, rebuilds, runs the tests, and publishes with provenance.
|
|
248
|
-
It needs an `NPM_TOKEN` repository secret with publish rights.
|
|
249
|
-
|
|
250
169
|
## Notes
|
|
251
170
|
|
|
252
171
|
- All inputs and outputs use plain browser-compatible types (`ArrayBuffer`, `Uint8Array`, `string`) — no Node.js APIs, no DOM, no framework required. (`atob`/`crypto.randomUUID` are used for the bundled font and glyph IDs; both are available in browsers and Node ≥ 19.)
|
|
@@ -259,10 +178,8 @@ It needs an `NPM_TOKEN` repository secret with publish rights.
|
|
|
259
178
|
|
|
260
179
|
[MIT](LICENSE). The bundled Arimo fallback font is separately licensed under Apache-2.0 (`third_party/arimo/LICENSE`).
|
|
261
180
|
|
|
262
|
-
##
|
|
263
|
-
|
|
264
|
-
If this app, code, or repository has helped you or someone you know, please consider donating. I appreciate any help to offset the costs of development and/or AI Credits.
|
|
181
|
+
## Copyright
|
|
265
182
|
|
|
266
|
-
|
|
183
|
+
(c)2026 Richard McQuiston
|
|
267
184
|
|
|
268
|
-
|
|
185
|
+
xTool, xTool Studio, and xTool Creative Space are registered trademarks of Makeblock Co., Ltd. This project is not affiliated with, endorsed by, or sponsored by Makeblock Co., Ltd. or xTool.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@richardmcquiston01/unofficial-xcs-writer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Read, write, build, and apply variable substitution to xTool Creative Space .xcs and .xs files, with real glyph outline extraction for text",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"dist",
|
|
34
34
|
"third_party",
|
|
35
35
|
"donate.svg",
|
|
36
|
-
"CHANGELOG.md"
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"API.md"
|
|
37
38
|
],
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/node": "^26.6.2",
|