@richardmcquiston01/unofficial-xcs-writer 0.2.0 → 0.3.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-07-14
9
+
10
+ ### Added
11
+
12
+ - Root `LICENSE` file (MIT) — previously declared in `package.json` but not shipped.
13
+ - `prepublishOnly` script that rebuilds and runs the test suite before every
14
+ `npm publish`, preventing stale-`dist/` releases.
15
+ - This changelog.
16
+
17
+ ### Fixed
18
+
19
+ - Republished the glyph outline extraction feature with a correct build. The
20
+ 0.2.0 tarball on npm was packed from a stale `dist/` and does not actually
21
+ contain the glyph regeneration code its README describes — use 0.3.0 instead.
22
+
23
+ ## [0.2.0] - 2026-07-14
24
+
25
+ > **Warning:** the npm tarball for this version was built from a stale `dist/`
26
+ > and lacks the glyph extraction feature listed below. Use 0.3.0 or later.
27
+
28
+ ### Added
29
+
30
+ - Real per-character glyph outline extraction (`src/glyphs.ts`, powered by
31
+ [opentype.js](https://github.com/opentypejs/opentype.js)). When
32
+ `renderXcsFile` changes a TEXT display's text, its `charJSONs` /
33
+ `fontData.glyphData` are regenerated with real glyph outlines so xTool
34
+ Studio renders the substituted text correctly.
35
+ - Optional `fonts` parameter on `renderXcsFile` to supply real font bytes
36
+ (TTF/OTF/WOFF) keyed by `style.fontFamily`.
37
+ - Bundled fallback font: Arimo v5.2.8 (Apache-2.0, metric-compatible with
38
+ Arial) for families not supplied via `fonts`.
39
+ - Lower-level glyph primitives exported for direct use: `loadFont`,
40
+ `loadDefaultFont`, `layoutText`.
41
+
42
+ ### Known limitations
43
+
44
+ - Curved-text layout (`style.curveX`/`curveY`) is not reproduced —
45
+ substituted text on a curved TEXT display gets correctly-shaped glyphs laid
46
+ out on a straight baseline.
47
+
48
+ ## [0.1.0] - 2026-05-19
49
+
50
+ ### Added
51
+
52
+ - Initial release: read, validate, and apply `{{token}}` variable
53
+ substitution to xTool Creative Space `.xcs` files.
54
+ - Core API: `assertXcsFormat`, `readXcsFile`, `extractXcsTokens`,
55
+ `renderXcsFile`, and the `XcsVariable` type.
56
+ - Dual ESM + CJS build with TypeScript declarations via tsup.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Richard McQuiston
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -117,3 +117,7 @@ Output files after `pnpm build`:
117
117
  - Only `TEXT` display objects are modified during substitution. Geometry, bitmaps, device configuration, and all other fields pass through unchanged.
118
118
  - This library targets the current xTool Creative Space JSON format. Older versions of the software may export a different structure.
119
119
  - Glyph outline extraction is powered by [opentype.js](https://github.com/opentypejs/opentype.js). The bundled fallback font is [Arimo](https://fonts.google.com/specimen/Arimo) v5.2.8 via [`@fontsource/arimo`](https://www.npmjs.com/package/@fontsource/arimo) (Apache License 2.0, see `third_party/arimo/LICENSE`).
120
+
121
+ ## License
122
+
123
+ [MIT](LICENSE). The bundled Arimo fallback font is separately licensed under Apache-2.0 (`third_party/arimo/LICENSE`).
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -22,11 +32,194 @@ var index_exports = {};
22
32
  __export(index_exports, {
23
33
  assertXcsFormat: () => assertXcsFormat,
24
34
  extractXcsTokens: () => extractXcsTokens,
35
+ layoutText: () => layoutText,
36
+ loadDefaultFont: () => loadDefaultFont,
37
+ loadFont: () => loadFont,
25
38
  readXcsFile: () => readXcsFile,
26
39
  renderXcsFile: () => renderXcsFile
27
40
  });
28
41
  module.exports = __toCommonJS(index_exports);
29
42
 
43
+ // src/glyphs.ts
44
+ var import_opentype = __toESM(require("opentype.js"), 1);
45
+
46
+ // src/assets/arimo-regular.ts
47
+ var ARIMO_REGULAR_WOFF_BASE64 = "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";
48
+
49
+ // src/glyphs.ts
50
+ var MM_PER_EM = 25.4;
51
+ var cachedDefaultFont = null;
52
+ function base64ToArrayBuffer(base64) {
53
+ const binary = atob(base64);
54
+ const bytes = new Uint8Array(binary.length);
55
+ for (let i = 0; i < binary.length; i++) {
56
+ bytes[i] = binary.charCodeAt(i);
57
+ }
58
+ return bytes.buffer;
59
+ }
60
+ function loadFont(buffer) {
61
+ return import_opentype.default.parse(buffer);
62
+ }
63
+ function loadDefaultFont() {
64
+ if (!cachedDefaultFont) {
65
+ cachedDefaultFont = loadFont(base64ToArrayBuffer(ARIMO_REGULAR_WOFF_BASE64));
66
+ }
67
+ return cachedDefaultFont;
68
+ }
69
+ function scaleFor(font) {
70
+ return MM_PER_EM / font.unitsPerEm;
71
+ }
72
+ function fontInfoFor(font) {
73
+ const scale = scaleFor(font);
74
+ const os2 = font.tables["os2"];
75
+ const capHeight = os2?.sCapHeight ?? Math.round(font.unitsPerEm * 0.7);
76
+ const xHeight = os2?.sxHeight ?? Math.round(font.unitsPerEm * 0.5);
77
+ return {
78
+ unitsPerEm: font.unitsPerEm,
79
+ lineHeight: (font.ascender - font.descender) * scale,
80
+ ascent: font.ascender * scale,
81
+ descent: font.descender * scale,
82
+ capHeight: capHeight * scale,
83
+ xHeight: xHeight * scale,
84
+ lineGap: 0
85
+ };
86
+ }
87
+ function isEmptyGlyph(glyph) {
88
+ const path = glyph.path;
89
+ return !path || path.commands.length === 0;
90
+ }
91
+ function pathToDString(path, scale) {
92
+ const fmt = (n) => {
93
+ const rounded = Math.round(n * 100) / 100;
94
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);
95
+ };
96
+ const parts = [];
97
+ for (const cmd of path.commands) {
98
+ switch (cmd.type) {
99
+ case "M":
100
+ parts.push(`M${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);
101
+ break;
102
+ case "L":
103
+ parts.push(`L${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);
104
+ break;
105
+ case "Q":
106
+ parts.push(
107
+ `Q${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`
108
+ );
109
+ break;
110
+ case "C":
111
+ parts.push(
112
+ `C${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x2 * scale)} ${fmt(-cmd.y2 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`
113
+ );
114
+ break;
115
+ case "Z":
116
+ parts.push("Z");
117
+ break;
118
+ }
119
+ }
120
+ return parts.join("");
121
+ }
122
+ function bboxFor(path, scale) {
123
+ const box = path.getBoundingBox();
124
+ return {
125
+ minX: box.x1 * scale,
126
+ minY: box.y1 * scale,
127
+ maxX: box.x2 * scale,
128
+ maxY: box.y2 * scale
129
+ };
130
+ }
131
+ function generateUuid() {
132
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
133
+ return crypto.randomUUID();
134
+ }
135
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
136
+ const r = Math.random() * 16 | 0;
137
+ const v = c === "x" ? r : r & 3 | 8;
138
+ return v.toString(16);
139
+ });
140
+ }
141
+ function layoutText(font, text, originX, originY) {
142
+ const scale = scaleFor(font);
143
+ const fontInfo = fontInfoFor(font);
144
+ const glyphData = {};
145
+ const charJSONs = [];
146
+ let graphicX = null;
147
+ let graphicY = originY;
148
+ let firstBox = null;
149
+ let minX = Infinity;
150
+ let minY = Infinity;
151
+ let maxX = -Infinity;
152
+ let maxY = -Infinity;
153
+ for (const char of text) {
154
+ const glyph = font.charToGlyph(char);
155
+ const advanceWidth = (glyph.advanceWidth ?? 0) * scale;
156
+ if (!glyphData[char]) {
157
+ const path = glyph.path;
158
+ if (isEmptyGlyph(glyph)) {
159
+ glyphData[char] = {
160
+ dPath: "",
161
+ advanceWidth,
162
+ advanceHeight: fontInfo.lineHeight,
163
+ leftBearing: 0,
164
+ topBearing: 0,
165
+ bbox: { minX: 0, minY: 0, maxX: 0, maxY: 0 }
166
+ };
167
+ } else {
168
+ const bbox = bboxFor(path, scale);
169
+ glyphData[char] = {
170
+ dPath: pathToDString(path, scale),
171
+ advanceWidth,
172
+ advanceHeight: fontInfo.lineHeight,
173
+ leftBearing: (glyph.leftSideBearing ?? 0) * scale,
174
+ topBearing: fontInfo.ascent - bbox.maxY,
175
+ bbox
176
+ };
177
+ }
178
+ }
179
+ const data = glyphData[char];
180
+ const hasInk = data.dPath !== "";
181
+ if (graphicX === null) {
182
+ graphicX = hasInk ? originX - data.bbox.minX : originX;
183
+ if (hasInk) graphicY = originY + data.bbox.maxY;
184
+ }
185
+ if (hasInk) {
186
+ const x = graphicX + data.bbox.minX;
187
+ const y = graphicY - data.bbox.maxY;
188
+ const width = data.bbox.maxX - data.bbox.minX;
189
+ const height = data.bbox.maxY - data.bbox.minY;
190
+ charJSONs.push({
191
+ id: generateUuid(),
192
+ type: "PATH",
193
+ x,
194
+ y,
195
+ width,
196
+ height,
197
+ offsetX: x,
198
+ offsetY: y,
199
+ graphicX,
200
+ graphicY,
201
+ dPath: data.dPath,
202
+ fillRule: "nonzero",
203
+ isFill: true
204
+ });
205
+ if (!firstBox) firstBox = { minX: x, minY: y, maxX: x + width, maxY: y + height };
206
+ minX = Math.min(minX, x);
207
+ minY = Math.min(minY, y);
208
+ maxX = Math.max(maxX, x + width);
209
+ maxY = Math.max(maxY, y + height);
210
+ }
211
+ graphicX += advanceWidth;
212
+ }
213
+ return {
214
+ fontData: { fontInfo, glyphData },
215
+ charJSONs,
216
+ x: firstBox ? firstBox.minX : null,
217
+ y: firstBox ? firstBox.minY : null,
218
+ width: Number.isFinite(maxX - minX) ? maxX - minX : 0,
219
+ height: Number.isFinite(maxY - minY) ? maxY - minY : 0
220
+ };
221
+ }
222
+
30
223
  // src/xcs.ts
31
224
  var TOKEN_RE = /\{\{([^}]+)\}\}/g;
32
225
  function assertXcsFormat(buffer) {
@@ -57,17 +250,44 @@ function extractXcsTokens(buffer) {
57
250
  }
58
251
  return Array.from(seen);
59
252
  }
60
- function renderXcsFile(buffer, variables, values) {
253
+ function resolveFont(fontFamily, fonts, cache) {
254
+ const key = fontFamily ?? "";
255
+ const cached = cache.get(key);
256
+ if (cached) return cached;
257
+ const suppliedBuffer = fontFamily ? fonts?.[fontFamily] : void 0;
258
+ const font = suppliedBuffer ? loadFont(suppliedBuffer) : loadDefaultFont();
259
+ cache.set(key, font);
260
+ return font;
261
+ }
262
+ function renderXcsFile(buffer, variables, values, fonts) {
61
263
  const project = JSON.parse(readXcsFile(buffer));
264
+ const fontCache = /* @__PURE__ */ new Map();
62
265
  for (const canvas of project.canvas) {
63
266
  for (const display of canvas.displays) {
64
267
  if (display.type === "TEXT" && typeof display.text === "string") {
65
- let text = display.text;
268
+ const originalText = display.text;
269
+ let text = originalText;
66
270
  for (const variable of variables) {
67
271
  const replacement = values[variable.token] ?? variable.defaultValue ?? "";
68
272
  text = text.replaceAll(`{{${variable.token}}}`, replacement);
69
273
  }
70
274
  display.text = text;
275
+ if (text === originalText) continue;
276
+ const style = display.style;
277
+ const font = resolveFont(style?.fontFamily, fonts, fontCache);
278
+ const originX = typeof display.x === "number" ? display.x : 0;
279
+ const originY = typeof display.y === "number" ? display.y : 0;
280
+ const layout = layoutText(font, text, originX, originY);
281
+ display.fontData = layout.fontData;
282
+ display.charJSONs = layout.charJSONs;
283
+ if (layout.x !== null && layout.y !== null) {
284
+ display.x = layout.x;
285
+ display.y = layout.y;
286
+ display.offsetX = layout.x;
287
+ display.offsetY = layout.y;
288
+ display.width = layout.width;
289
+ display.height = layout.height;
290
+ }
71
291
  }
72
292
  }
73
293
  }
@@ -77,6 +297,9 @@ function renderXcsFile(buffer, variables, values) {
77
297
  0 && (module.exports = {
78
298
  assertXcsFormat,
79
299
  extractXcsTokens,
300
+ layoutText,
301
+ loadDefaultFont,
302
+ loadFont,
80
303
  readXcsFile,
81
304
  renderXcsFile
82
305
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/xcs.ts"],"sourcesContent":["export type { XcsVariable, XcsProject, XcsCanvas, XcsDisplay } from './types.js';\nexport { assertXcsFormat, readXcsFile, extractXcsTokens, renderXcsFile } from './xcs.js';\n","import type { XcsProject, XcsVariable } from './types.js';\n\nconst TOKEN_RE = /\\{\\{([^}]+)\\}\\}/g;\n\nexport function assertXcsFormat(buffer: ArrayBuffer): void {\n let project: unknown;\n try {\n project = JSON.parse(new TextDecoder().decode(buffer));\n } catch (err) {\n throw new Error(`Not a valid .xcs file: ${(err as Error).message}`);\n }\n if (\n typeof project !== 'object' ||\n project === null ||\n !Array.isArray((project as Record<string, unknown>).canvas)\n ) {\n throw new Error('Not a valid .xcs file: missing canvas array.');\n }\n}\n\nexport function readXcsFile(buffer: ArrayBuffer): string {\n return new TextDecoder().decode(buffer);\n}\n\nexport function extractXcsTokens(buffer: ArrayBuffer): string[] {\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\n const seen = new Set<string>();\n\n for (const canvas of project.canvas) {\n for (const display of canvas.displays) {\n if (display.type === 'TEXT' && typeof display.text === 'string') {\n for (const match of display.text.matchAll(TOKEN_RE)) {\n seen.add(match[1]);\n }\n }\n }\n }\n\n return Array.from(seen);\n}\n\nexport function renderXcsFile(\n buffer: ArrayBuffer,\n variables: XcsVariable[],\n values: Record<string, string>,\n): Uint8Array {\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\n\n for (const canvas of project.canvas) {\n for (const display of canvas.displays) {\n if (display.type === 'TEXT' && typeof display.text === 'string') {\n let text = display.text;\n for (const variable of variables) {\n const replacement = values[variable.token] ?? variable.defaultValue ?? '';\n text = text.replaceAll(`{{${variable.token}}}`, replacement);\n }\n display.text = text;\n }\n }\n }\n\n return new TextEncoder().encode(JSON.stringify(project));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,IAAM,WAAW;AAEV,SAAS,gBAAgB,QAA2B;AACzD,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,MAAM,CAAC;AAAA,EACvD,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,0BAA2B,IAAc,OAAO,EAAE;AAAA,EACpE;AACA,MACE,OAAO,YAAY,YACnB,YAAY,QACZ,CAAC,MAAM,QAAS,QAAoC,MAAM,GAC1D;AACA,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACF;AAEO,SAAS,YAAY,QAA6B;AACvD,SAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AACxC;AAEO,SAAS,iBAAiB,QAA+B;AAC9D,QAAM,UAAU,KAAK,MAAM,YAAY,MAAM,CAAC;AAC9C,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,UAAU,QAAQ,QAAQ;AACnC,eAAW,WAAW,OAAO,UAAU;AACrC,UAAI,QAAQ,SAAS,UAAU,OAAO,QAAQ,SAAS,UAAU;AAC/D,mBAAW,SAAS,QAAQ,KAAK,SAAS,QAAQ,GAAG;AACnD,eAAK,IAAI,MAAM,CAAC,CAAC;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,cACd,QACA,WACA,QACY;AACZ,QAAM,UAAU,KAAK,MAAM,YAAY,MAAM,CAAC;AAE9C,aAAW,UAAU,QAAQ,QAAQ;AACnC,eAAW,WAAW,OAAO,UAAU;AACrC,UAAI,QAAQ,SAAS,UAAU,OAAO,QAAQ,SAAS,UAAU;AAC/D,YAAI,OAAO,QAAQ;AACnB,mBAAW,YAAY,WAAW;AAChC,gBAAM,cAAc,OAAO,SAAS,KAAK,KAAK,SAAS,gBAAgB;AACvE,iBAAO,KAAK,WAAW,KAAK,SAAS,KAAK,MAAM,WAAW;AAAA,QAC7D;AACA,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC;AACzD;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/glyphs.ts","../src/assets/arimo-regular.ts","../src/xcs.ts"],"sourcesContent":["export type { XcsVariable, XcsProject, XcsCanvas, XcsDisplay } from './types.js';\r\nexport { assertXcsFormat, readXcsFile, extractXcsTokens, renderXcsFile } from './xcs.js';\r\nexport type {\r\n GlyphBBox,\r\n GlyphData,\r\n FontInfo,\r\n FontData,\r\n CharJson,\r\n TextLayoutResult,\r\n} from './glyphs.js';\r\nexport { loadFont, loadDefaultFont, layoutText } from './glyphs.js';\r\n","// opentype.js is a CJS-only UMD build with no \"exports\" map, so under\r\n// native ESM only the default import is reliably populated -- a\r\n// namespace import (`import * as opentype`) leaves named properties\r\n// like `.parse` undefined at runtime in built ESM output.\r\nimport opentype from 'opentype.js';\r\nimport { ARIMO_REGULAR_WOFF_BASE64 } from './assets/arimo-regular.js';\r\n\r\n/**\r\n * Real glyph outline extraction for XCS TEXT displays, replacing the\r\n * placeholder-square glyphs that a naive generator (or an .xcs file\r\n * whose TEXT display has an empty `charJSONs`) would otherwise produce.\r\n *\r\n * xTool Studio's on-disk format was reverse-engineered by diffing a\r\n * generated .xcs file against the same file re-saved by xTool Studio,\r\n * both unchanged and after manually re-selecting the font (see this\r\n * repo's CLAUDE.md \"Open Issues\" for the full writeup). Key findings\r\n * baked into the math below:\r\n *\r\n * - `charJSONs` (not `fontData.glyphData`) is what actually renders on\r\n * canvas -- one PATH-shaped entry per non-whitespace character.\r\n * - Font design coordinates are always scaled so that 1 em == 25.4mm\r\n * (1 inch), independent of the `style.fontSize` value written\r\n * alongside it (empirically confirmed against real Arial metrics).\r\n * - `fontData.glyphData[char].bbox` / `advanceWidth` / `leftBearing`\r\n * use the font's native Y-up convention. `charJSONs[i].dPath` uses a\r\n * Y-down convention (flip Y) and is expressed in glyph-local\r\n * coordinates relative to that character's own pen/baseline origin\r\n * (`graphicX`/`graphicY`), i.e. dPath is NOT pre-translated to the\r\n * character's absolute canvas position.\r\n * - `charJSONs[i].x/y` is the character's ink bounding-box top-left in\r\n * absolute canvas coordinates: x = graphicX + bbox.minX,\r\n * y = graphicY - bbox.maxY. width/height are the ink bbox extents.\r\n * - A TEXT display's own `x`/`y` equals the FIRST character's box\r\n * top-left (x[0]/y[0]) -- i.e. it is not a separate anchor to\r\n * recompute, it doubles as charJSONs[0]'s position.\r\n *\r\n * Known limitation: this only replicates STRAIGHT horizontal-baseline\r\n * layout (constant graphicY, cumulative graphicX via advance widths).\r\n * xTool Studio's `style.curveX`/`curveY` curved-text layout formula is\r\n * undocumented and was not reverse-engineerable with confidence from\r\n * available samples, so curved text is not reproduced -- substituted\r\n * text on a curved TEXT display will render with real glyph shapes,\r\n * correctly shaped, but laid out on a straight baseline instead of\r\n * the original curve.\r\n */\r\n\r\nconst MM_PER_EM = 25.4;\r\n\r\nexport interface GlyphBBox {\r\n minX: number;\r\n minY: number;\r\n maxX: number;\r\n maxY: number;\r\n}\r\n\r\nexport interface GlyphData {\r\n dPath: string;\r\n advanceWidth: number;\r\n advanceHeight: number;\r\n leftBearing: number;\r\n topBearing: number;\r\n bbox: GlyphBBox;\r\n}\r\n\r\nexport interface FontInfo {\r\n unitsPerEm: number;\r\n lineHeight: number;\r\n ascent: number;\r\n descent: number;\r\n capHeight: number;\r\n xHeight: number;\r\n lineGap: number;\r\n}\r\n\r\nexport interface FontData {\r\n fontInfo: FontInfo;\r\n glyphData: Record<string, GlyphData>;\r\n}\r\n\r\nexport interface CharJson {\r\n id: string;\r\n type: 'PATH';\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n offsetX: number;\r\n offsetY: number;\r\n graphicX: number;\r\n graphicY: number;\r\n dPath: string;\r\n fillRule: 'nonzero';\r\n isFill: boolean;\r\n}\r\n\r\nexport interface TextLayoutResult {\r\n fontData: FontData;\r\n charJSONs: CharJson[];\r\n /** New display x/y (== charJSONs[0]'s box top-left), only present if text is non-empty. */\r\n x: number | null;\r\n y: number | null;\r\n width: number;\r\n height: number;\r\n}\r\n\r\nlet cachedDefaultFont: opentype.Font | null = null;\r\n\r\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\r\n const binary = atob(base64);\r\n const bytes = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i);\r\n }\r\n return bytes.buffer;\r\n}\r\n\r\n/** Loads and parses a font file (TTF/OTF/WOFF) for glyph extraction. */\r\nexport function loadFont(buffer: ArrayBuffer): opentype.Font {\r\n return opentype.parse(buffer);\r\n}\r\n\r\n/** The bundled fallback font (Arimo, metric-compatible with Arial, Apache-2.0). */\r\nexport function loadDefaultFont(): opentype.Font {\r\n if (!cachedDefaultFont) {\r\n cachedDefaultFont = loadFont(base64ToArrayBuffer(ARIMO_REGULAR_WOFF_BASE64));\r\n }\r\n return cachedDefaultFont;\r\n}\r\n\r\nfunction scaleFor(font: opentype.Font): number {\r\n return MM_PER_EM / font.unitsPerEm;\r\n}\r\n\r\nfunction fontInfoFor(font: opentype.Font): FontInfo {\r\n const scale = scaleFor(font);\r\n const os2 = font.tables['os2'] as { sCapHeight?: number; sxHeight?: number } | undefined;\r\n const capHeight = os2?.sCapHeight ?? Math.round(font.unitsPerEm * 0.7);\r\n const xHeight = os2?.sxHeight ?? Math.round(font.unitsPerEm * 0.5);\r\n return {\r\n unitsPerEm: font.unitsPerEm,\r\n lineHeight: (font.ascender - font.descender) * scale,\r\n ascent: font.ascender * scale,\r\n descent: font.descender * scale,\r\n capHeight: capHeight * scale,\r\n xHeight: xHeight * scale,\r\n lineGap: 0,\r\n };\r\n}\r\n\r\n/** A glyph with no visible ink (whitespace, or a font with no outline for this character). */\r\nfunction isEmptyGlyph(glyph: opentype.Glyph): boolean {\r\n const path = glyph.path;\r\n return !path || path.commands.length === 0;\r\n}\r\n\r\nfunction pathToDString(path: opentype.Path, scale: number): string {\r\n const fmt = (n: number): string => {\r\n const rounded = Math.round(n * 100) / 100;\r\n return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);\r\n };\r\n // xTool's dPath uses a Y-down convention; opentype.js paths are Y-up\r\n // (baseline at 0, ascenders positive), so Y is negated here.\r\n const parts: string[] = [];\r\n for (const cmd of path.commands) {\r\n switch (cmd.type) {\r\n case 'M':\r\n parts.push(`M${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);\r\n break;\r\n case 'L':\r\n parts.push(`L${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);\r\n break;\r\n case 'Q':\r\n parts.push(\r\n `Q${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`\r\n );\r\n break;\r\n case 'C':\r\n parts.push(\r\n `C${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x2 * scale)} ${fmt(-cmd.y2 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`\r\n );\r\n break;\r\n case 'Z':\r\n parts.push('Z');\r\n break;\r\n }\r\n }\r\n return parts.join('');\r\n}\r\n\r\nfunction bboxFor(path: opentype.Path, scale: number): GlyphBBox {\r\n const box = path.getBoundingBox();\r\n return {\r\n minX: box.x1 * scale,\r\n minY: box.y1 * scale,\r\n maxX: box.x2 * scale,\r\n maxY: box.y2 * scale,\r\n };\r\n}\r\n\r\nfunction generateUuid(): string {\r\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\r\n return v.toString(16);\r\n });\r\n}\r\n\r\n/**\r\n * Builds glyphData + charJSONs for `text` laid out on a straight\r\n * horizontal baseline, anchored so the first character's ink\r\n * bounding-box top-left lands at (originX, originY) -- matching how\r\n * an xTool Studio TEXT display's own x/y equals charJSONs[0]'s x/y.\r\n */\r\nexport function layoutText(\r\n font: opentype.Font,\r\n text: string,\r\n originX: number,\r\n originY: number\r\n): TextLayoutResult {\r\n const scale = scaleFor(font);\r\n const fontInfo = fontInfoFor(font);\r\n const glyphData: Record<string, GlyphData> = {};\r\n const charJSONs: CharJson[] = [];\r\n\r\n let graphicX: number | null = null;\r\n let graphicY = originY;\r\n let firstBox: GlyphBBox | null = null;\r\n let minX = Infinity;\r\n let minY = Infinity;\r\n let maxX = -Infinity;\r\n let maxY = -Infinity;\r\n\r\n for (const char of text) {\r\n const glyph = font.charToGlyph(char);\r\n const advanceWidth = (glyph.advanceWidth ?? 0) * scale;\r\n\r\n if (!glyphData[char]) {\r\n // Raw, unscaled path in the font's native Y-up convention\r\n // (baseline at 0, ascenders positive) -- NOT glyph.getPath(x, y,\r\n // fontSize), which opentype.js Y-flips for canvas rendering when\r\n // given explicit coordinates, and would double-flip against our\r\n // own conversion below.\r\n const path = glyph.path;\r\n if (isEmptyGlyph(glyph)) {\r\n glyphData[char] = {\r\n dPath: '',\r\n advanceWidth,\r\n advanceHeight: fontInfo.lineHeight,\r\n leftBearing: 0,\r\n topBearing: 0,\r\n bbox: { minX: 0, minY: 0, maxX: 0, maxY: 0 },\r\n };\r\n } else {\r\n const bbox = bboxFor(path, scale);\r\n glyphData[char] = {\r\n dPath: pathToDString(path, scale),\r\n advanceWidth,\r\n advanceHeight: fontInfo.lineHeight,\r\n leftBearing: (glyph.leftSideBearing ?? 0) * scale,\r\n topBearing: fontInfo.ascent - bbox.maxY,\r\n bbox,\r\n };\r\n }\r\n }\r\n\r\n const data = glyphData[char];\r\n const hasInk = data.dPath !== '';\r\n\r\n if (graphicX === null) {\r\n // Anchor the first character (visible or not) at (originX, originY).\r\n graphicX = hasInk ? originX - data.bbox.minX : originX;\r\n if (hasInk) graphicY = originY + data.bbox.maxY;\r\n }\r\n\r\n if (hasInk) {\r\n const x = graphicX + data.bbox.minX;\r\n const y = graphicY - data.bbox.maxY;\r\n const width = data.bbox.maxX - data.bbox.minX;\r\n const height = data.bbox.maxY - data.bbox.minY;\r\n\r\n // dPath is glyph-local (relative to the pen origin graphicX/Y),\r\n // matching the observed xTool convention -- same shape as\r\n // fontData.glyphData[char].dPath, computed once and cached above.\r\n charJSONs.push({\r\n id: generateUuid(),\r\n type: 'PATH',\r\n x,\r\n y,\r\n width,\r\n height,\r\n offsetX: x,\r\n offsetY: y,\r\n graphicX,\r\n graphicY,\r\n dPath: data.dPath,\r\n fillRule: 'nonzero',\r\n isFill: true,\r\n });\r\n\r\n if (!firstBox) firstBox = { minX: x, minY: y, maxX: x + width, maxY: y + height };\r\n minX = Math.min(minX, x);\r\n minY = Math.min(minY, y);\r\n maxX = Math.max(maxX, x + width);\r\n maxY = Math.max(maxY, y + height);\r\n }\r\n\r\n graphicX += advanceWidth;\r\n }\r\n\r\n return {\r\n fontData: { fontInfo, glyphData },\r\n charJSONs,\r\n x: firstBox ? firstBox.minX : null,\r\n y: firstBox ? firstBox.minY : null,\r\n width: Number.isFinite(maxX - minX) ? maxX - minX : 0,\r\n height: Number.isFinite(maxY - minY) ? maxY - minY : 0,\r\n };\r\n}\r\n","// Arimo Regular (WOFF), subset: Latin.\r\n// Source: @fontsource/arimo v5.2.8 (https://www.npmjs.com/package/@fontsource/arimo)\r\n// License: Apache License 2.0 -- see third_party/arimo/LICENSE\r\n// Used as the default fallback font for real glyph outline extraction\r\n// when the caller doesn't supply a font file for a TEXT display's\r\n// fontFamily. Arimo is metric-compatible with Arial.\r\nexport const ARIMO_REGULAR_WOFF_BASE64 = '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';\r\n","import type { XcsProject, XcsVariable } from './types.js';\r\nimport { loadDefaultFont, loadFont, layoutText } from './glyphs.js';\r\nimport type * as opentype from 'opentype.js';\r\n\r\nconst TOKEN_RE = /\\{\\{([^}]+)\\}\\}/g;\r\n\r\nexport function assertXcsFormat(buffer: ArrayBuffer): void {\r\n let project: unknown;\r\n try {\r\n project = JSON.parse(new TextDecoder().decode(buffer));\r\n } catch (err) {\r\n throw new Error(`Not a valid .xcs file: ${(err as Error).message}`);\r\n }\r\n if (\r\n typeof project !== 'object' ||\r\n project === null ||\r\n !Array.isArray((project as Record<string, unknown>).canvas)\r\n ) {\r\n throw new Error('Not a valid .xcs file: missing canvas array.');\r\n }\r\n}\r\n\r\nexport function readXcsFile(buffer: ArrayBuffer): string {\r\n return new TextDecoder().decode(buffer);\r\n}\r\n\r\nexport function extractXcsTokens(buffer: ArrayBuffer): string[] {\r\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\r\n const seen = new Set<string>();\r\n\r\n for (const canvas of project.canvas) {\r\n for (const display of canvas.displays) {\r\n if (display.type === 'TEXT' && typeof display.text === 'string') {\r\n for (const match of display.text.matchAll(TOKEN_RE)) {\r\n seen.add(match[1]);\r\n }\r\n }\r\n }\r\n }\r\n\r\n return Array.from(seen);\r\n}\r\n\r\nfunction resolveFont(\r\n fontFamily: string | undefined,\r\n fonts: Record<string, ArrayBuffer> | undefined,\r\n cache: Map<string, opentype.Font>,\r\n): opentype.Font {\r\n const key = fontFamily ?? '';\r\n const cached = cache.get(key);\r\n if (cached) return cached;\r\n\r\n const suppliedBuffer = fontFamily ? fonts?.[fontFamily] : undefined;\r\n const font = suppliedBuffer ? loadFont(suppliedBuffer) : loadDefaultFont();\r\n cache.set(key, font);\r\n return font;\r\n}\r\n\r\n/**\r\n * Substitutes `{{token}}` placeholders in TEXT display `text` fields.\r\n *\r\n * Whenever a display's text actually changes, its glyph outline data\r\n * (`charJSONs` / `fontData.glyphData`) is regenerated from a real font\r\n * so xTool Studio renders the new text correctly instead of showing\r\n * stale or placeholder glyph shapes (see src/glyphs.ts for the format\r\n * this was reverse-engineered from, and its documented limitations --\r\n * notably, curved-text layout is not reproduced).\r\n *\r\n * Pass `fonts` to supply real font file bytes keyed by the TEXT\r\n * display's `style.fontFamily`; displays whose font isn't supplied\r\n * fall back to a bundled metric-compatible default (Arimo, standing in\r\n * for Arial). Displays whose text doesn't change are left untouched.\r\n */\r\nexport function renderXcsFile(\r\n buffer: ArrayBuffer,\r\n variables: XcsVariable[],\r\n values: Record<string, string>,\r\n fonts?: Record<string, ArrayBuffer>,\r\n): Uint8Array {\r\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\r\n const fontCache = new Map<string, opentype.Font>();\r\n\r\n for (const canvas of project.canvas) {\r\n for (const display of canvas.displays) {\r\n if (display.type === 'TEXT' && typeof display.text === 'string') {\r\n const originalText = display.text;\r\n let text = originalText;\r\n for (const variable of variables) {\r\n const replacement = values[variable.token] ?? variable.defaultValue ?? '';\r\n text = text.replaceAll(`{{${variable.token}}}`, replacement);\r\n }\r\n display.text = text;\r\n\r\n if (text === originalText) continue;\r\n\r\n const style = display.style as { fontFamily?: string } | undefined;\r\n const font = resolveFont(style?.fontFamily, fonts, fontCache);\r\n const originX = typeof display.x === 'number' ? display.x : 0;\r\n const originY = typeof display.y === 'number' ? display.y : 0;\r\n\r\n const layout = layoutText(font, text, originX, originY);\r\n display.fontData = layout.fontData;\r\n display.charJSONs = layout.charJSONs;\r\n if (layout.x !== null && layout.y !== null) {\r\n display.x = layout.x;\r\n display.y = layout.y;\r\n display.offsetX = layout.x;\r\n display.offsetY = layout.y;\r\n display.width = layout.width;\r\n display.height = layout.height;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return new TextEncoder().encode(JSON.stringify(project));\r\n}\r\n"],"mappings":"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","names":["opentype"]}
package/dist/index.d.cts CHANGED
@@ -1,3 +1,5 @@
1
+ import opentype from 'opentype.js';
2
+
1
3
  interface XcsVariable {
2
4
  token: string;
3
5
  defaultValue?: string;
@@ -20,6 +22,84 @@ interface XcsProject {
20
22
  declare function assertXcsFormat(buffer: ArrayBuffer): void;
21
23
  declare function readXcsFile(buffer: ArrayBuffer): string;
22
24
  declare function extractXcsTokens(buffer: ArrayBuffer): string[];
23
- declare function renderXcsFile(buffer: ArrayBuffer, variables: XcsVariable[], values: Record<string, string>): Uint8Array;
25
+ /**
26
+ * Substitutes `{{token}}` placeholders in TEXT display `text` fields.
27
+ *
28
+ * Whenever a display's text actually changes, its glyph outline data
29
+ * (`charJSONs` / `fontData.glyphData`) is regenerated from a real font
30
+ * so xTool Studio renders the new text correctly instead of showing
31
+ * stale or placeholder glyph shapes (see src/glyphs.ts for the format
32
+ * this was reverse-engineered from, and its documented limitations --
33
+ * notably, curved-text layout is not reproduced).
34
+ *
35
+ * Pass `fonts` to supply real font file bytes keyed by the TEXT
36
+ * display's `style.fontFamily`; displays whose font isn't supplied
37
+ * fall back to a bundled metric-compatible default (Arimo, standing in
38
+ * for Arial). Displays whose text doesn't change are left untouched.
39
+ */
40
+ declare function renderXcsFile(buffer: ArrayBuffer, variables: XcsVariable[], values: Record<string, string>, fonts?: Record<string, ArrayBuffer>): Uint8Array;
41
+
42
+ interface GlyphBBox {
43
+ minX: number;
44
+ minY: number;
45
+ maxX: number;
46
+ maxY: number;
47
+ }
48
+ interface GlyphData {
49
+ dPath: string;
50
+ advanceWidth: number;
51
+ advanceHeight: number;
52
+ leftBearing: number;
53
+ topBearing: number;
54
+ bbox: GlyphBBox;
55
+ }
56
+ interface FontInfo {
57
+ unitsPerEm: number;
58
+ lineHeight: number;
59
+ ascent: number;
60
+ descent: number;
61
+ capHeight: number;
62
+ xHeight: number;
63
+ lineGap: number;
64
+ }
65
+ interface FontData {
66
+ fontInfo: FontInfo;
67
+ glyphData: Record<string, GlyphData>;
68
+ }
69
+ interface CharJson {
70
+ id: string;
71
+ type: 'PATH';
72
+ x: number;
73
+ y: number;
74
+ width: number;
75
+ height: number;
76
+ offsetX: number;
77
+ offsetY: number;
78
+ graphicX: number;
79
+ graphicY: number;
80
+ dPath: string;
81
+ fillRule: 'nonzero';
82
+ isFill: boolean;
83
+ }
84
+ interface TextLayoutResult {
85
+ fontData: FontData;
86
+ charJSONs: CharJson[];
87
+ /** New display x/y (== charJSONs[0]'s box top-left), only present if text is non-empty. */
88
+ x: number | null;
89
+ y: number | null;
90
+ width: number;
91
+ height: number;
92
+ }
93
+ /** Loads and parses a font file (TTF/OTF/WOFF) for glyph extraction. */
94
+ declare function loadFont(buffer: ArrayBuffer): opentype.Font;
95
+ /** The bundled fallback font (Arimo, metric-compatible with Arial, Apache-2.0). */
96
+ declare function loadDefaultFont(): opentype.Font;
97
+ /**
98
+ * Builds glyphData + charJSONs for `text` laid out on a straight
99
+ * horizontal baseline, anchored so the first character's ink
100
+ * bounding-box top-left lands at (originX, originY) -- matching how
101
+ * an xTool Studio TEXT display's own x/y equals charJSONs[0]'s x/y.
102
+ */
103
+ declare function layoutText(font: opentype.Font, text: string, originX: number, originY: number): TextLayoutResult;
24
104
 
25
- export { type XcsCanvas, type XcsDisplay, type XcsProject, type XcsVariable, assertXcsFormat, extractXcsTokens, readXcsFile, renderXcsFile };
105
+ export { type CharJson, type FontData, type FontInfo, type GlyphBBox, type GlyphData, type TextLayoutResult, type XcsCanvas, type XcsDisplay, type XcsProject, type XcsVariable, assertXcsFormat, extractXcsTokens, layoutText, loadDefaultFont, loadFont, readXcsFile, renderXcsFile };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import opentype from 'opentype.js';
2
+
1
3
  interface XcsVariable {
2
4
  token: string;
3
5
  defaultValue?: string;
@@ -20,6 +22,84 @@ interface XcsProject {
20
22
  declare function assertXcsFormat(buffer: ArrayBuffer): void;
21
23
  declare function readXcsFile(buffer: ArrayBuffer): string;
22
24
  declare function extractXcsTokens(buffer: ArrayBuffer): string[];
23
- declare function renderXcsFile(buffer: ArrayBuffer, variables: XcsVariable[], values: Record<string, string>): Uint8Array;
25
+ /**
26
+ * Substitutes `{{token}}` placeholders in TEXT display `text` fields.
27
+ *
28
+ * Whenever a display's text actually changes, its glyph outline data
29
+ * (`charJSONs` / `fontData.glyphData`) is regenerated from a real font
30
+ * so xTool Studio renders the new text correctly instead of showing
31
+ * stale or placeholder glyph shapes (see src/glyphs.ts for the format
32
+ * this was reverse-engineered from, and its documented limitations --
33
+ * notably, curved-text layout is not reproduced).
34
+ *
35
+ * Pass `fonts` to supply real font file bytes keyed by the TEXT
36
+ * display's `style.fontFamily`; displays whose font isn't supplied
37
+ * fall back to a bundled metric-compatible default (Arimo, standing in
38
+ * for Arial). Displays whose text doesn't change are left untouched.
39
+ */
40
+ declare function renderXcsFile(buffer: ArrayBuffer, variables: XcsVariable[], values: Record<string, string>, fonts?: Record<string, ArrayBuffer>): Uint8Array;
41
+
42
+ interface GlyphBBox {
43
+ minX: number;
44
+ minY: number;
45
+ maxX: number;
46
+ maxY: number;
47
+ }
48
+ interface GlyphData {
49
+ dPath: string;
50
+ advanceWidth: number;
51
+ advanceHeight: number;
52
+ leftBearing: number;
53
+ topBearing: number;
54
+ bbox: GlyphBBox;
55
+ }
56
+ interface FontInfo {
57
+ unitsPerEm: number;
58
+ lineHeight: number;
59
+ ascent: number;
60
+ descent: number;
61
+ capHeight: number;
62
+ xHeight: number;
63
+ lineGap: number;
64
+ }
65
+ interface FontData {
66
+ fontInfo: FontInfo;
67
+ glyphData: Record<string, GlyphData>;
68
+ }
69
+ interface CharJson {
70
+ id: string;
71
+ type: 'PATH';
72
+ x: number;
73
+ y: number;
74
+ width: number;
75
+ height: number;
76
+ offsetX: number;
77
+ offsetY: number;
78
+ graphicX: number;
79
+ graphicY: number;
80
+ dPath: string;
81
+ fillRule: 'nonzero';
82
+ isFill: boolean;
83
+ }
84
+ interface TextLayoutResult {
85
+ fontData: FontData;
86
+ charJSONs: CharJson[];
87
+ /** New display x/y (== charJSONs[0]'s box top-left), only present if text is non-empty. */
88
+ x: number | null;
89
+ y: number | null;
90
+ width: number;
91
+ height: number;
92
+ }
93
+ /** Loads and parses a font file (TTF/OTF/WOFF) for glyph extraction. */
94
+ declare function loadFont(buffer: ArrayBuffer): opentype.Font;
95
+ /** The bundled fallback font (Arimo, metric-compatible with Arial, Apache-2.0). */
96
+ declare function loadDefaultFont(): opentype.Font;
97
+ /**
98
+ * Builds glyphData + charJSONs for `text` laid out on a straight
99
+ * horizontal baseline, anchored so the first character's ink
100
+ * bounding-box top-left lands at (originX, originY) -- matching how
101
+ * an xTool Studio TEXT display's own x/y equals charJSONs[0]'s x/y.
102
+ */
103
+ declare function layoutText(font: opentype.Font, text: string, originX: number, originY: number): TextLayoutResult;
24
104
 
25
- export { type XcsCanvas, type XcsDisplay, type XcsProject, type XcsVariable, assertXcsFormat, extractXcsTokens, readXcsFile, renderXcsFile };
105
+ export { type CharJson, type FontData, type FontInfo, type GlyphBBox, type GlyphData, type TextLayoutResult, type XcsCanvas, type XcsDisplay, type XcsProject, type XcsVariable, assertXcsFormat, extractXcsTokens, layoutText, loadDefaultFont, loadFont, readXcsFile, renderXcsFile };
package/dist/index.js CHANGED
@@ -1,3 +1,183 @@
1
+ // src/glyphs.ts
2
+ import opentype from "opentype.js";
3
+
4
+ // src/assets/arimo-regular.ts
5
+ var ARIMO_REGULAR_WOFF_BASE64 = "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";
6
+
7
+ // src/glyphs.ts
8
+ var MM_PER_EM = 25.4;
9
+ var cachedDefaultFont = null;
10
+ function base64ToArrayBuffer(base64) {
11
+ const binary = atob(base64);
12
+ const bytes = new Uint8Array(binary.length);
13
+ for (let i = 0; i < binary.length; i++) {
14
+ bytes[i] = binary.charCodeAt(i);
15
+ }
16
+ return bytes.buffer;
17
+ }
18
+ function loadFont(buffer) {
19
+ return opentype.parse(buffer);
20
+ }
21
+ function loadDefaultFont() {
22
+ if (!cachedDefaultFont) {
23
+ cachedDefaultFont = loadFont(base64ToArrayBuffer(ARIMO_REGULAR_WOFF_BASE64));
24
+ }
25
+ return cachedDefaultFont;
26
+ }
27
+ function scaleFor(font) {
28
+ return MM_PER_EM / font.unitsPerEm;
29
+ }
30
+ function fontInfoFor(font) {
31
+ const scale = scaleFor(font);
32
+ const os2 = font.tables["os2"];
33
+ const capHeight = os2?.sCapHeight ?? Math.round(font.unitsPerEm * 0.7);
34
+ const xHeight = os2?.sxHeight ?? Math.round(font.unitsPerEm * 0.5);
35
+ return {
36
+ unitsPerEm: font.unitsPerEm,
37
+ lineHeight: (font.ascender - font.descender) * scale,
38
+ ascent: font.ascender * scale,
39
+ descent: font.descender * scale,
40
+ capHeight: capHeight * scale,
41
+ xHeight: xHeight * scale,
42
+ lineGap: 0
43
+ };
44
+ }
45
+ function isEmptyGlyph(glyph) {
46
+ const path = glyph.path;
47
+ return !path || path.commands.length === 0;
48
+ }
49
+ function pathToDString(path, scale) {
50
+ const fmt = (n) => {
51
+ const rounded = Math.round(n * 100) / 100;
52
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);
53
+ };
54
+ const parts = [];
55
+ for (const cmd of path.commands) {
56
+ switch (cmd.type) {
57
+ case "M":
58
+ parts.push(`M${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);
59
+ break;
60
+ case "L":
61
+ parts.push(`L${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);
62
+ break;
63
+ case "Q":
64
+ parts.push(
65
+ `Q${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`
66
+ );
67
+ break;
68
+ case "C":
69
+ parts.push(
70
+ `C${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x2 * scale)} ${fmt(-cmd.y2 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`
71
+ );
72
+ break;
73
+ case "Z":
74
+ parts.push("Z");
75
+ break;
76
+ }
77
+ }
78
+ return parts.join("");
79
+ }
80
+ function bboxFor(path, scale) {
81
+ const box = path.getBoundingBox();
82
+ return {
83
+ minX: box.x1 * scale,
84
+ minY: box.y1 * scale,
85
+ maxX: box.x2 * scale,
86
+ maxY: box.y2 * scale
87
+ };
88
+ }
89
+ function generateUuid() {
90
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
91
+ return crypto.randomUUID();
92
+ }
93
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
94
+ const r = Math.random() * 16 | 0;
95
+ const v = c === "x" ? r : r & 3 | 8;
96
+ return v.toString(16);
97
+ });
98
+ }
99
+ function layoutText(font, text, originX, originY) {
100
+ const scale = scaleFor(font);
101
+ const fontInfo = fontInfoFor(font);
102
+ const glyphData = {};
103
+ const charJSONs = [];
104
+ let graphicX = null;
105
+ let graphicY = originY;
106
+ let firstBox = null;
107
+ let minX = Infinity;
108
+ let minY = Infinity;
109
+ let maxX = -Infinity;
110
+ let maxY = -Infinity;
111
+ for (const char of text) {
112
+ const glyph = font.charToGlyph(char);
113
+ const advanceWidth = (glyph.advanceWidth ?? 0) * scale;
114
+ if (!glyphData[char]) {
115
+ const path = glyph.path;
116
+ if (isEmptyGlyph(glyph)) {
117
+ glyphData[char] = {
118
+ dPath: "",
119
+ advanceWidth,
120
+ advanceHeight: fontInfo.lineHeight,
121
+ leftBearing: 0,
122
+ topBearing: 0,
123
+ bbox: { minX: 0, minY: 0, maxX: 0, maxY: 0 }
124
+ };
125
+ } else {
126
+ const bbox = bboxFor(path, scale);
127
+ glyphData[char] = {
128
+ dPath: pathToDString(path, scale),
129
+ advanceWidth,
130
+ advanceHeight: fontInfo.lineHeight,
131
+ leftBearing: (glyph.leftSideBearing ?? 0) * scale,
132
+ topBearing: fontInfo.ascent - bbox.maxY,
133
+ bbox
134
+ };
135
+ }
136
+ }
137
+ const data = glyphData[char];
138
+ const hasInk = data.dPath !== "";
139
+ if (graphicX === null) {
140
+ graphicX = hasInk ? originX - data.bbox.minX : originX;
141
+ if (hasInk) graphicY = originY + data.bbox.maxY;
142
+ }
143
+ if (hasInk) {
144
+ const x = graphicX + data.bbox.minX;
145
+ const y = graphicY - data.bbox.maxY;
146
+ const width = data.bbox.maxX - data.bbox.minX;
147
+ const height = data.bbox.maxY - data.bbox.minY;
148
+ charJSONs.push({
149
+ id: generateUuid(),
150
+ type: "PATH",
151
+ x,
152
+ y,
153
+ width,
154
+ height,
155
+ offsetX: x,
156
+ offsetY: y,
157
+ graphicX,
158
+ graphicY,
159
+ dPath: data.dPath,
160
+ fillRule: "nonzero",
161
+ isFill: true
162
+ });
163
+ if (!firstBox) firstBox = { minX: x, minY: y, maxX: x + width, maxY: y + height };
164
+ minX = Math.min(minX, x);
165
+ minY = Math.min(minY, y);
166
+ maxX = Math.max(maxX, x + width);
167
+ maxY = Math.max(maxY, y + height);
168
+ }
169
+ graphicX += advanceWidth;
170
+ }
171
+ return {
172
+ fontData: { fontInfo, glyphData },
173
+ charJSONs,
174
+ x: firstBox ? firstBox.minX : null,
175
+ y: firstBox ? firstBox.minY : null,
176
+ width: Number.isFinite(maxX - minX) ? maxX - minX : 0,
177
+ height: Number.isFinite(maxY - minY) ? maxY - minY : 0
178
+ };
179
+ }
180
+
1
181
  // src/xcs.ts
2
182
  var TOKEN_RE = /\{\{([^}]+)\}\}/g;
3
183
  function assertXcsFormat(buffer) {
@@ -28,17 +208,44 @@ function extractXcsTokens(buffer) {
28
208
  }
29
209
  return Array.from(seen);
30
210
  }
31
- function renderXcsFile(buffer, variables, values) {
211
+ function resolveFont(fontFamily, fonts, cache) {
212
+ const key = fontFamily ?? "";
213
+ const cached = cache.get(key);
214
+ if (cached) return cached;
215
+ const suppliedBuffer = fontFamily ? fonts?.[fontFamily] : void 0;
216
+ const font = suppliedBuffer ? loadFont(suppliedBuffer) : loadDefaultFont();
217
+ cache.set(key, font);
218
+ return font;
219
+ }
220
+ function renderXcsFile(buffer, variables, values, fonts) {
32
221
  const project = JSON.parse(readXcsFile(buffer));
222
+ const fontCache = /* @__PURE__ */ new Map();
33
223
  for (const canvas of project.canvas) {
34
224
  for (const display of canvas.displays) {
35
225
  if (display.type === "TEXT" && typeof display.text === "string") {
36
- let text = display.text;
226
+ const originalText = display.text;
227
+ let text = originalText;
37
228
  for (const variable of variables) {
38
229
  const replacement = values[variable.token] ?? variable.defaultValue ?? "";
39
230
  text = text.replaceAll(`{{${variable.token}}}`, replacement);
40
231
  }
41
232
  display.text = text;
233
+ if (text === originalText) continue;
234
+ const style = display.style;
235
+ const font = resolveFont(style?.fontFamily, fonts, fontCache);
236
+ const originX = typeof display.x === "number" ? display.x : 0;
237
+ const originY = typeof display.y === "number" ? display.y : 0;
238
+ const layout = layoutText(font, text, originX, originY);
239
+ display.fontData = layout.fontData;
240
+ display.charJSONs = layout.charJSONs;
241
+ if (layout.x !== null && layout.y !== null) {
242
+ display.x = layout.x;
243
+ display.y = layout.y;
244
+ display.offsetX = layout.x;
245
+ display.offsetY = layout.y;
246
+ display.width = layout.width;
247
+ display.height = layout.height;
248
+ }
42
249
  }
43
250
  }
44
251
  }
@@ -47,6 +254,9 @@ function renderXcsFile(buffer, variables, values) {
47
254
  export {
48
255
  assertXcsFormat,
49
256
  extractXcsTokens,
257
+ layoutText,
258
+ loadDefaultFont,
259
+ loadFont,
50
260
  readXcsFile,
51
261
  renderXcsFile
52
262
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/xcs.ts"],"sourcesContent":["import type { XcsProject, XcsVariable } from './types.js';\n\nconst TOKEN_RE = /\\{\\{([^}]+)\\}\\}/g;\n\nexport function assertXcsFormat(buffer: ArrayBuffer): void {\n let project: unknown;\n try {\n project = JSON.parse(new TextDecoder().decode(buffer));\n } catch (err) {\n throw new Error(`Not a valid .xcs file: ${(err as Error).message}`);\n }\n if (\n typeof project !== 'object' ||\n project === null ||\n !Array.isArray((project as Record<string, unknown>).canvas)\n ) {\n throw new Error('Not a valid .xcs file: missing canvas array.');\n }\n}\n\nexport function readXcsFile(buffer: ArrayBuffer): string {\n return new TextDecoder().decode(buffer);\n}\n\nexport function extractXcsTokens(buffer: ArrayBuffer): string[] {\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\n const seen = new Set<string>();\n\n for (const canvas of project.canvas) {\n for (const display of canvas.displays) {\n if (display.type === 'TEXT' && typeof display.text === 'string') {\n for (const match of display.text.matchAll(TOKEN_RE)) {\n seen.add(match[1]);\n }\n }\n }\n }\n\n return Array.from(seen);\n}\n\nexport function renderXcsFile(\n buffer: ArrayBuffer,\n variables: XcsVariable[],\n values: Record<string, string>,\n): Uint8Array {\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\n\n for (const canvas of project.canvas) {\n for (const display of canvas.displays) {\n if (display.type === 'TEXT' && typeof display.text === 'string') {\n let text = display.text;\n for (const variable of variables) {\n const replacement = values[variable.token] ?? variable.defaultValue ?? '';\n text = text.replaceAll(`{{${variable.token}}}`, replacement);\n }\n display.text = text;\n }\n }\n }\n\n return new TextEncoder().encode(JSON.stringify(project));\n}\n"],"mappings":";AAEA,IAAM,WAAW;AAEV,SAAS,gBAAgB,QAA2B;AACzD,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,MAAM,CAAC;AAAA,EACvD,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,0BAA2B,IAAc,OAAO,EAAE;AAAA,EACpE;AACA,MACE,OAAO,YAAY,YACnB,YAAY,QACZ,CAAC,MAAM,QAAS,QAAoC,MAAM,GAC1D;AACA,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACF;AAEO,SAAS,YAAY,QAA6B;AACvD,SAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AACxC;AAEO,SAAS,iBAAiB,QAA+B;AAC9D,QAAM,UAAU,KAAK,MAAM,YAAY,MAAM,CAAC;AAC9C,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,UAAU,QAAQ,QAAQ;AACnC,eAAW,WAAW,OAAO,UAAU;AACrC,UAAI,QAAQ,SAAS,UAAU,OAAO,QAAQ,SAAS,UAAU;AAC/D,mBAAW,SAAS,QAAQ,KAAK,SAAS,QAAQ,GAAG;AACnD,eAAK,IAAI,MAAM,CAAC,CAAC;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,cACd,QACA,WACA,QACY;AACZ,QAAM,UAAU,KAAK,MAAM,YAAY,MAAM,CAAC;AAE9C,aAAW,UAAU,QAAQ,QAAQ;AACnC,eAAW,WAAW,OAAO,UAAU;AACrC,UAAI,QAAQ,SAAS,UAAU,OAAO,QAAQ,SAAS,UAAU;AAC/D,YAAI,OAAO,QAAQ;AACnB,mBAAW,YAAY,WAAW;AAChC,gBAAM,cAAc,OAAO,SAAS,KAAK,KAAK,SAAS,gBAAgB;AACvE,iBAAO,KAAK,WAAW,KAAK,SAAS,KAAK,MAAM,WAAW;AAAA,QAC7D;AACA,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC;AACzD;","names":[]}
1
+ {"version":3,"sources":["../src/glyphs.ts","../src/assets/arimo-regular.ts","../src/xcs.ts"],"sourcesContent":["// opentype.js is a CJS-only UMD build with no \"exports\" map, so under\r\n// native ESM only the default import is reliably populated -- a\r\n// namespace import (`import * as opentype`) leaves named properties\r\n// like `.parse` undefined at runtime in built ESM output.\r\nimport opentype from 'opentype.js';\r\nimport { ARIMO_REGULAR_WOFF_BASE64 } from './assets/arimo-regular.js';\r\n\r\n/**\r\n * Real glyph outline extraction for XCS TEXT displays, replacing the\r\n * placeholder-square glyphs that a naive generator (or an .xcs file\r\n * whose TEXT display has an empty `charJSONs`) would otherwise produce.\r\n *\r\n * xTool Studio's on-disk format was reverse-engineered by diffing a\r\n * generated .xcs file against the same file re-saved by xTool Studio,\r\n * both unchanged and after manually re-selecting the font (see this\r\n * repo's CLAUDE.md \"Open Issues\" for the full writeup). Key findings\r\n * baked into the math below:\r\n *\r\n * - `charJSONs` (not `fontData.glyphData`) is what actually renders on\r\n * canvas -- one PATH-shaped entry per non-whitespace character.\r\n * - Font design coordinates are always scaled so that 1 em == 25.4mm\r\n * (1 inch), independent of the `style.fontSize` value written\r\n * alongside it (empirically confirmed against real Arial metrics).\r\n * - `fontData.glyphData[char].bbox` / `advanceWidth` / `leftBearing`\r\n * use the font's native Y-up convention. `charJSONs[i].dPath` uses a\r\n * Y-down convention (flip Y) and is expressed in glyph-local\r\n * coordinates relative to that character's own pen/baseline origin\r\n * (`graphicX`/`graphicY`), i.e. dPath is NOT pre-translated to the\r\n * character's absolute canvas position.\r\n * - `charJSONs[i].x/y` is the character's ink bounding-box top-left in\r\n * absolute canvas coordinates: x = graphicX + bbox.minX,\r\n * y = graphicY - bbox.maxY. width/height are the ink bbox extents.\r\n * - A TEXT display's own `x`/`y` equals the FIRST character's box\r\n * top-left (x[0]/y[0]) -- i.e. it is not a separate anchor to\r\n * recompute, it doubles as charJSONs[0]'s position.\r\n *\r\n * Known limitation: this only replicates STRAIGHT horizontal-baseline\r\n * layout (constant graphicY, cumulative graphicX via advance widths).\r\n * xTool Studio's `style.curveX`/`curveY` curved-text layout formula is\r\n * undocumented and was not reverse-engineerable with confidence from\r\n * available samples, so curved text is not reproduced -- substituted\r\n * text on a curved TEXT display will render with real glyph shapes,\r\n * correctly shaped, but laid out on a straight baseline instead of\r\n * the original curve.\r\n */\r\n\r\nconst MM_PER_EM = 25.4;\r\n\r\nexport interface GlyphBBox {\r\n minX: number;\r\n minY: number;\r\n maxX: number;\r\n maxY: number;\r\n}\r\n\r\nexport interface GlyphData {\r\n dPath: string;\r\n advanceWidth: number;\r\n advanceHeight: number;\r\n leftBearing: number;\r\n topBearing: number;\r\n bbox: GlyphBBox;\r\n}\r\n\r\nexport interface FontInfo {\r\n unitsPerEm: number;\r\n lineHeight: number;\r\n ascent: number;\r\n descent: number;\r\n capHeight: number;\r\n xHeight: number;\r\n lineGap: number;\r\n}\r\n\r\nexport interface FontData {\r\n fontInfo: FontInfo;\r\n glyphData: Record<string, GlyphData>;\r\n}\r\n\r\nexport interface CharJson {\r\n id: string;\r\n type: 'PATH';\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n offsetX: number;\r\n offsetY: number;\r\n graphicX: number;\r\n graphicY: number;\r\n dPath: string;\r\n fillRule: 'nonzero';\r\n isFill: boolean;\r\n}\r\n\r\nexport interface TextLayoutResult {\r\n fontData: FontData;\r\n charJSONs: CharJson[];\r\n /** New display x/y (== charJSONs[0]'s box top-left), only present if text is non-empty. */\r\n x: number | null;\r\n y: number | null;\r\n width: number;\r\n height: number;\r\n}\r\n\r\nlet cachedDefaultFont: opentype.Font | null = null;\r\n\r\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\r\n const binary = atob(base64);\r\n const bytes = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i);\r\n }\r\n return bytes.buffer;\r\n}\r\n\r\n/** Loads and parses a font file (TTF/OTF/WOFF) for glyph extraction. */\r\nexport function loadFont(buffer: ArrayBuffer): opentype.Font {\r\n return opentype.parse(buffer);\r\n}\r\n\r\n/** The bundled fallback font (Arimo, metric-compatible with Arial, Apache-2.0). */\r\nexport function loadDefaultFont(): opentype.Font {\r\n if (!cachedDefaultFont) {\r\n cachedDefaultFont = loadFont(base64ToArrayBuffer(ARIMO_REGULAR_WOFF_BASE64));\r\n }\r\n return cachedDefaultFont;\r\n}\r\n\r\nfunction scaleFor(font: opentype.Font): number {\r\n return MM_PER_EM / font.unitsPerEm;\r\n}\r\n\r\nfunction fontInfoFor(font: opentype.Font): FontInfo {\r\n const scale = scaleFor(font);\r\n const os2 = font.tables['os2'] as { sCapHeight?: number; sxHeight?: number } | undefined;\r\n const capHeight = os2?.sCapHeight ?? Math.round(font.unitsPerEm * 0.7);\r\n const xHeight = os2?.sxHeight ?? Math.round(font.unitsPerEm * 0.5);\r\n return {\r\n unitsPerEm: font.unitsPerEm,\r\n lineHeight: (font.ascender - font.descender) * scale,\r\n ascent: font.ascender * scale,\r\n descent: font.descender * scale,\r\n capHeight: capHeight * scale,\r\n xHeight: xHeight * scale,\r\n lineGap: 0,\r\n };\r\n}\r\n\r\n/** A glyph with no visible ink (whitespace, or a font with no outline for this character). */\r\nfunction isEmptyGlyph(glyph: opentype.Glyph): boolean {\r\n const path = glyph.path;\r\n return !path || path.commands.length === 0;\r\n}\r\n\r\nfunction pathToDString(path: opentype.Path, scale: number): string {\r\n const fmt = (n: number): string => {\r\n const rounded = Math.round(n * 100) / 100;\r\n return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);\r\n };\r\n // xTool's dPath uses a Y-down convention; opentype.js paths are Y-up\r\n // (baseline at 0, ascenders positive), so Y is negated here.\r\n const parts: string[] = [];\r\n for (const cmd of path.commands) {\r\n switch (cmd.type) {\r\n case 'M':\r\n parts.push(`M${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);\r\n break;\r\n case 'L':\r\n parts.push(`L${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`);\r\n break;\r\n case 'Q':\r\n parts.push(\r\n `Q${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`\r\n );\r\n break;\r\n case 'C':\r\n parts.push(\r\n `C${fmt(cmd.x1 * scale)} ${fmt(-cmd.y1 * scale)} ${fmt(cmd.x2 * scale)} ${fmt(-cmd.y2 * scale)} ${fmt(cmd.x * scale)} ${fmt(-cmd.y * scale)}`\r\n );\r\n break;\r\n case 'Z':\r\n parts.push('Z');\r\n break;\r\n }\r\n }\r\n return parts.join('');\r\n}\r\n\r\nfunction bboxFor(path: opentype.Path, scale: number): GlyphBBox {\r\n const box = path.getBoundingBox();\r\n return {\r\n minX: box.x1 * scale,\r\n minY: box.y1 * scale,\r\n maxX: box.x2 * scale,\r\n maxY: box.y2 * scale,\r\n };\r\n}\r\n\r\nfunction generateUuid(): string {\r\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\r\n return v.toString(16);\r\n });\r\n}\r\n\r\n/**\r\n * Builds glyphData + charJSONs for `text` laid out on a straight\r\n * horizontal baseline, anchored so the first character's ink\r\n * bounding-box top-left lands at (originX, originY) -- matching how\r\n * an xTool Studio TEXT display's own x/y equals charJSONs[0]'s x/y.\r\n */\r\nexport function layoutText(\r\n font: opentype.Font,\r\n text: string,\r\n originX: number,\r\n originY: number\r\n): TextLayoutResult {\r\n const scale = scaleFor(font);\r\n const fontInfo = fontInfoFor(font);\r\n const glyphData: Record<string, GlyphData> = {};\r\n const charJSONs: CharJson[] = [];\r\n\r\n let graphicX: number | null = null;\r\n let graphicY = originY;\r\n let firstBox: GlyphBBox | null = null;\r\n let minX = Infinity;\r\n let minY = Infinity;\r\n let maxX = -Infinity;\r\n let maxY = -Infinity;\r\n\r\n for (const char of text) {\r\n const glyph = font.charToGlyph(char);\r\n const advanceWidth = (glyph.advanceWidth ?? 0) * scale;\r\n\r\n if (!glyphData[char]) {\r\n // Raw, unscaled path in the font's native Y-up convention\r\n // (baseline at 0, ascenders positive) -- NOT glyph.getPath(x, y,\r\n // fontSize), which opentype.js Y-flips for canvas rendering when\r\n // given explicit coordinates, and would double-flip against our\r\n // own conversion below.\r\n const path = glyph.path;\r\n if (isEmptyGlyph(glyph)) {\r\n glyphData[char] = {\r\n dPath: '',\r\n advanceWidth,\r\n advanceHeight: fontInfo.lineHeight,\r\n leftBearing: 0,\r\n topBearing: 0,\r\n bbox: { minX: 0, minY: 0, maxX: 0, maxY: 0 },\r\n };\r\n } else {\r\n const bbox = bboxFor(path, scale);\r\n glyphData[char] = {\r\n dPath: pathToDString(path, scale),\r\n advanceWidth,\r\n advanceHeight: fontInfo.lineHeight,\r\n leftBearing: (glyph.leftSideBearing ?? 0) * scale,\r\n topBearing: fontInfo.ascent - bbox.maxY,\r\n bbox,\r\n };\r\n }\r\n }\r\n\r\n const data = glyphData[char];\r\n const hasInk = data.dPath !== '';\r\n\r\n if (graphicX === null) {\r\n // Anchor the first character (visible or not) at (originX, originY).\r\n graphicX = hasInk ? originX - data.bbox.minX : originX;\r\n if (hasInk) graphicY = originY + data.bbox.maxY;\r\n }\r\n\r\n if (hasInk) {\r\n const x = graphicX + data.bbox.minX;\r\n const y = graphicY - data.bbox.maxY;\r\n const width = data.bbox.maxX - data.bbox.minX;\r\n const height = data.bbox.maxY - data.bbox.minY;\r\n\r\n // dPath is glyph-local (relative to the pen origin graphicX/Y),\r\n // matching the observed xTool convention -- same shape as\r\n // fontData.glyphData[char].dPath, computed once and cached above.\r\n charJSONs.push({\r\n id: generateUuid(),\r\n type: 'PATH',\r\n x,\r\n y,\r\n width,\r\n height,\r\n offsetX: x,\r\n offsetY: y,\r\n graphicX,\r\n graphicY,\r\n dPath: data.dPath,\r\n fillRule: 'nonzero',\r\n isFill: true,\r\n });\r\n\r\n if (!firstBox) firstBox = { minX: x, minY: y, maxX: x + width, maxY: y + height };\r\n minX = Math.min(minX, x);\r\n minY = Math.min(minY, y);\r\n maxX = Math.max(maxX, x + width);\r\n maxY = Math.max(maxY, y + height);\r\n }\r\n\r\n graphicX += advanceWidth;\r\n }\r\n\r\n return {\r\n fontData: { fontInfo, glyphData },\r\n charJSONs,\r\n x: firstBox ? firstBox.minX : null,\r\n y: firstBox ? firstBox.minY : null,\r\n width: Number.isFinite(maxX - minX) ? maxX - minX : 0,\r\n height: Number.isFinite(maxY - minY) ? maxY - minY : 0,\r\n };\r\n}\r\n","// Arimo Regular (WOFF), subset: Latin.\r\n// Source: @fontsource/arimo v5.2.8 (https://www.npmjs.com/package/@fontsource/arimo)\r\n// License: Apache License 2.0 -- see third_party/arimo/LICENSE\r\n// Used as the default fallback font for real glyph outline extraction\r\n// when the caller doesn't supply a font file for a TEXT display's\r\n// fontFamily. Arimo is metric-compatible with Arial.\r\nexport const ARIMO_REGULAR_WOFF_BASE64 = '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';\r\n","import type { XcsProject, XcsVariable } from './types.js';\r\nimport { loadDefaultFont, loadFont, layoutText } from './glyphs.js';\r\nimport type * as opentype from 'opentype.js';\r\n\r\nconst TOKEN_RE = /\\{\\{([^}]+)\\}\\}/g;\r\n\r\nexport function assertXcsFormat(buffer: ArrayBuffer): void {\r\n let project: unknown;\r\n try {\r\n project = JSON.parse(new TextDecoder().decode(buffer));\r\n } catch (err) {\r\n throw new Error(`Not a valid .xcs file: ${(err as Error).message}`);\r\n }\r\n if (\r\n typeof project !== 'object' ||\r\n project === null ||\r\n !Array.isArray((project as Record<string, unknown>).canvas)\r\n ) {\r\n throw new Error('Not a valid .xcs file: missing canvas array.');\r\n }\r\n}\r\n\r\nexport function readXcsFile(buffer: ArrayBuffer): string {\r\n return new TextDecoder().decode(buffer);\r\n}\r\n\r\nexport function extractXcsTokens(buffer: ArrayBuffer): string[] {\r\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\r\n const seen = new Set<string>();\r\n\r\n for (const canvas of project.canvas) {\r\n for (const display of canvas.displays) {\r\n if (display.type === 'TEXT' && typeof display.text === 'string') {\r\n for (const match of display.text.matchAll(TOKEN_RE)) {\r\n seen.add(match[1]);\r\n }\r\n }\r\n }\r\n }\r\n\r\n return Array.from(seen);\r\n}\r\n\r\nfunction resolveFont(\r\n fontFamily: string | undefined,\r\n fonts: Record<string, ArrayBuffer> | undefined,\r\n cache: Map<string, opentype.Font>,\r\n): opentype.Font {\r\n const key = fontFamily ?? '';\r\n const cached = cache.get(key);\r\n if (cached) return cached;\r\n\r\n const suppliedBuffer = fontFamily ? fonts?.[fontFamily] : undefined;\r\n const font = suppliedBuffer ? loadFont(suppliedBuffer) : loadDefaultFont();\r\n cache.set(key, font);\r\n return font;\r\n}\r\n\r\n/**\r\n * Substitutes `{{token}}` placeholders in TEXT display `text` fields.\r\n *\r\n * Whenever a display's text actually changes, its glyph outline data\r\n * (`charJSONs` / `fontData.glyphData`) is regenerated from a real font\r\n * so xTool Studio renders the new text correctly instead of showing\r\n * stale or placeholder glyph shapes (see src/glyphs.ts for the format\r\n * this was reverse-engineered from, and its documented limitations --\r\n * notably, curved-text layout is not reproduced).\r\n *\r\n * Pass `fonts` to supply real font file bytes keyed by the TEXT\r\n * display's `style.fontFamily`; displays whose font isn't supplied\r\n * fall back to a bundled metric-compatible default (Arimo, standing in\r\n * for Arial). Displays whose text doesn't change are left untouched.\r\n */\r\nexport function renderXcsFile(\r\n buffer: ArrayBuffer,\r\n variables: XcsVariable[],\r\n values: Record<string, string>,\r\n fonts?: Record<string, ArrayBuffer>,\r\n): Uint8Array {\r\n const project = JSON.parse(readXcsFile(buffer)) as XcsProject;\r\n const fontCache = new Map<string, opentype.Font>();\r\n\r\n for (const canvas of project.canvas) {\r\n for (const display of canvas.displays) {\r\n if (display.type === 'TEXT' && typeof display.text === 'string') {\r\n const originalText = display.text;\r\n let text = originalText;\r\n for (const variable of variables) {\r\n const replacement = values[variable.token] ?? variable.defaultValue ?? '';\r\n text = text.replaceAll(`{{${variable.token}}}`, replacement);\r\n }\r\n display.text = text;\r\n\r\n if (text === originalText) continue;\r\n\r\n const style = display.style as { fontFamily?: string } | undefined;\r\n const font = resolveFont(style?.fontFamily, fonts, fontCache);\r\n const originX = typeof display.x === 'number' ? display.x : 0;\r\n const originY = typeof display.y === 'number' ? display.y : 0;\r\n\r\n const layout = layoutText(font, text, originX, originY);\r\n display.fontData = layout.fontData;\r\n display.charJSONs = layout.charJSONs;\r\n if (layout.x !== null && layout.y !== null) {\r\n display.x = layout.x;\r\n display.y = layout.y;\r\n display.offsetX = layout.x;\r\n display.offsetY = layout.y;\r\n display.width = layout.width;\r\n display.height = layout.height;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return new TextEncoder().encode(JSON.stringify(project));\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richardmcquiston01/unofficial-xcs-writer",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Read, write, and apply variable substitution to xTool Creative Space .xcs files, with real glyph outline extraction for substituted text",
5
5
  "license": "MIT",
6
6
  "author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
@@ -30,10 +30,12 @@
30
30
  "types": "./dist/index.d.ts",
31
31
  "files": [
32
32
  "dist",
33
- "third_party"
33
+ "third_party",
34
+ "CHANGELOG.md"
34
35
  ],
35
36
  "scripts": {
36
37
  "build": "tsup",
38
+ "prepublishOnly": "pnpm build && pnpm test",
37
39
  "dev": "tsup --watch",
38
40
  "typecheck": "tsc --noEmit",
39
41
  "test": "vitest run",