@pixerate/editor 0.268.1 → 0.269.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.
Files changed (59) hide show
  1. package/dist/agent/index.cjs +27 -27
  2. package/dist/agent/index.js +1 -1
  3. package/dist/animation-editor/index.cjs +61 -61
  4. package/dist/animation-editor/index.js +5 -5
  5. package/dist/brushes/import/abr.d.cts +48 -0
  6. package/dist/brushes/import/abr.d.ts +48 -0
  7. package/dist/brushes/import/bplist.d.cts +24 -0
  8. package/dist/brushes/import/bplist.d.ts +24 -0
  9. package/dist/brushes/import/pack.d.cts +82 -0
  10. package/dist/brushes/import/pack.d.ts +82 -0
  11. package/dist/brushes/import/zip.d.cts +31 -0
  12. package/dist/brushes/import/zip.d.ts +31 -0
  13. package/dist/brushes/index.cjs +26 -6
  14. package/dist/brushes/index.d.cts +2 -0
  15. package/dist/brushes/index.d.ts +2 -0
  16. package/dist/brushes/index.js +22 -2
  17. package/dist/{chunk-SIFAIGB3.js → chunk-3AJCEJNH.js} +2 -162
  18. package/dist/{chunk-YXP6X3SY.cjs → chunk-3KNQ2W6X.cjs} +697 -10
  19. package/dist/{chunk-QNY7BDFP.js → chunk-4CPOKP27.js} +1 -1
  20. package/dist/{chunk-J4GXP53R.cjs → chunk-4NMPUJUK.cjs} +86 -86
  21. package/dist/{chunk-LDOD7VCT.js → chunk-4YSPQGZW.js} +1 -1
  22. package/dist/{chunk-JBB3GDBB.cjs → chunk-56RHCIJV.cjs} +2 -2
  23. package/dist/{chunk-4IMD2TLY.cjs → chunk-AI4F33IV.cjs} +3166 -2966
  24. package/dist/{chunk-YOVXQHA3.cjs → chunk-BENSE3VX.cjs} +23 -23
  25. package/dist/{chunk-VDGBHYC7.js → chunk-F7OO3DHP.js} +157 -1
  26. package/dist/{chunk-UVTO3D4E.js → chunk-FUDP5QBB.js} +1 -1
  27. package/dist/{chunk-3XDSNIGF.js → chunk-ININNHLM.js} +687 -0
  28. package/dist/{chunk-WIC7TG73.cjs → chunk-M4UQA2T4.cjs} +18 -18
  29. package/dist/{chunk-ITURMC2M.js → chunk-N3UMQVKX.js} +176 -176
  30. package/dist/{chunk-UWEQ55MY.cjs → chunk-N4KP3UQR.cjs} +157 -1
  31. package/dist/{chunk-D5BXSOSU.js → chunk-QGVTOCNB.js} +3118 -2918
  32. package/dist/{chunk-M6DNZNVY.js → chunk-RWDSSK2I.js} +4 -2
  33. package/dist/{chunk-CVFAAZXE.cjs → chunk-SPIO2GJU.cjs} +8 -6
  34. package/dist/{chunk-RHEMMGCN.cjs → chunk-T4JPGBW3.cjs} +173 -173
  35. package/dist/{chunk-HFY5LG5J.js → chunk-THBJHPUL.js} +1 -1
  36. package/dist/{chunk-NPWWSCWT.cjs → chunk-VBRHE4AM.cjs} +35 -35
  37. package/dist/{chunk-AZC2NDAR.js → chunk-WB22XMW3.js} +6 -6
  38. package/dist/{chunk-NIBFDFYW.cjs → chunk-YBMUHIOM.cjs} +12 -172
  39. package/dist/extensions/index.cjs +18 -4
  40. package/dist/extensions/index.d.cts +1 -0
  41. package/dist/extensions/index.d.ts +1 -0
  42. package/dist/extensions/index.js +17 -3
  43. package/dist/extensions/prompt-editor.d.cts +109 -0
  44. package/dist/extensions/prompt-editor.d.ts +109 -0
  45. package/dist/generation/index.cjs +15 -15
  46. package/dist/generation/index.js +3 -3
  47. package/dist/grammar/index.cjs +4 -4
  48. package/dist/grammar/index.js +7 -7
  49. package/dist/image-editor/index.cjs +785 -785
  50. package/dist/image-editor/index.js +6 -6
  51. package/dist/index.cjs +27 -13
  52. package/dist/index.js +23 -9
  53. package/dist/paint-editor/index.cjs +91 -91
  54. package/dist/paint-editor/index.js +6 -6
  55. package/dist/vector/index.cjs +1001 -1001
  56. package/dist/vector/index.js +6 -6
  57. package/package.json +1 -1
  58. package/skills/pixerate-brushes/SKILL.md +11 -2
  59. package/skills/pixerate-brushes-design/SKILL.md +2 -0
@@ -3,6 +3,7 @@ import {
3
3
  } from "./chunk-VE6UKYQ2.js";
4
4
  import {
5
5
  BRUSH_HOST_PROFILES,
6
+ BRUSH_ID,
6
7
  BRUSH_JSON_SCHEMA,
7
8
  BRUSH_KINDS,
8
9
  BRUSH_MEDIA,
@@ -12,6 +13,7 @@ import {
12
13
  MAX_BRUSH_SOURCE_INPUT_PIXELS,
13
14
  MAX_BRUSH_SOURCE_SIZE,
14
15
  PAINT_PROFILE,
16
+ brushSourceFromMask,
15
17
  brushSourceIdOf,
16
18
  brushSourceIds,
17
19
  brushSourceRef,
@@ -19,12 +21,19 @@ import {
19
21
  closest,
20
22
  degrade,
21
23
  isBrushSourceRef,
24
+ makeBrushSource,
22
25
  normalizeBrushSpec,
23
26
  resolveCategory,
24
27
  resolveEngine,
25
28
  summarizeBrushSource,
26
29
  withKeptIssues
27
30
  } from "./chunk-KZI3YWKW.js";
31
+ import {
32
+ base64ToBytes
33
+ } from "./chunk-OTTT5N3K.js";
34
+ import {
35
+ inflateRaw
36
+ } from "./chunk-QGHEDC7B.js";
28
37
 
29
38
  // src/brushes/patch.ts
30
39
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -58,6 +67,638 @@ function changedBrushPaths(a, b, path = "") {
58
67
  return JSON.stringify(a) === JSON.stringify(b) ? [] : [path];
59
68
  }
60
69
 
70
+ // src/brushes/import/abr.ts
71
+ var AbrError = class extends Error {
72
+ constructor(message) {
73
+ super(message);
74
+ this.name = "AbrError";
75
+ }
76
+ };
77
+ var MAX_TIP_PIXELS = 5e3 * 5e3;
78
+ var MAX_TIPS = 1e3;
79
+ var Reader = class {
80
+ constructor(bytes, end = bytes.length) {
81
+ this.bytes = bytes;
82
+ this.end = end;
83
+ }
84
+ bytes;
85
+ end;
86
+ pos = 0;
87
+ need(n) {
88
+ if (n < 0 || this.pos + n > this.end) throw new AbrError("The brush file ends early or a size in it is wrong.");
89
+ }
90
+ u8() {
91
+ this.need(1);
92
+ return this.bytes[this.pos++];
93
+ }
94
+ u16() {
95
+ this.need(2);
96
+ const v = this.bytes[this.pos] << 8 | this.bytes[this.pos + 1];
97
+ this.pos += 2;
98
+ return v;
99
+ }
100
+ i16() {
101
+ const v = this.u16();
102
+ return v & 32768 ? v - 65536 : v;
103
+ }
104
+ u32() {
105
+ this.need(4);
106
+ const b = this.bytes;
107
+ const v = (b[this.pos] << 24 | b[this.pos + 1] << 16 | b[this.pos + 2] << 8 | b[this.pos + 3]) >>> 0;
108
+ this.pos += 4;
109
+ return v;
110
+ }
111
+ i32() {
112
+ return this.u32() | 0;
113
+ }
114
+ take(n) {
115
+ this.need(n);
116
+ const out = this.bytes.subarray(this.pos, this.pos + n);
117
+ this.pos += n;
118
+ return out;
119
+ }
120
+ skip(n) {
121
+ this.need(n);
122
+ this.pos += n;
123
+ }
124
+ ascii(n) {
125
+ return String.fromCharCode(...this.take(n));
126
+ }
127
+ };
128
+ function unpackRows(r, height, rowBytes) {
129
+ const lengths = [];
130
+ for (let y = 0; y < height; y++) lengths.push(r.u16());
131
+ const out = new Uint8Array(rowBytes * height);
132
+ for (let y = 0; y < height; y++) {
133
+ const row = new Reader(r.take(lengths[y]));
134
+ let x = 0;
135
+ while (row.pos < row.end && x < rowBytes) {
136
+ const n = row.u8();
137
+ if (n < 128) {
138
+ const run = Math.min(n + 1, rowBytes - x);
139
+ out.set(row.take(run), y * rowBytes + x);
140
+ if (n + 1 > run) row.skip(Math.min(n + 1 - run, row.end - row.pos));
141
+ x += run;
142
+ } else if (n > 128) {
143
+ const v = row.u8();
144
+ const run = Math.min(257 - n, rowBytes - x);
145
+ out.fill(v, y * rowBytes + x, y * rowBytes + x + run);
146
+ x += run;
147
+ }
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+ function readImage(r, width, height, depth, compression) {
153
+ if (!(width > 0 && height > 0)) throw new AbrError(`A tip is ${width}\xD7${height} px.`);
154
+ if (width * height > MAX_TIP_PIXELS) throw new AbrError(`A tip is ${width}\xD7${height} px; tips over ${MAX_TIP_PIXELS} pixels are not read.`);
155
+ if (depth !== 8 && depth !== 16) throw new AbrError(`A tip has ${depth}-bit depth; 8 and 16 bits are read.`);
156
+ const bpp = depth / 8;
157
+ const rowBytes = width * bpp;
158
+ const data = compression === 0 ? r.take(rowBytes * height) : compression === 1 ? unpackRows(r, height, rowBytes) : null;
159
+ if (!data) throw new AbrError(`A tip uses compression ${compression}; raw and PackBits are read.`);
160
+ if (bpp === 1) return Uint8Array.from(data);
161
+ const out = new Uint8Array(width * height);
162
+ for (let i = 0; i < out.length; i++) out[i] = data[i * 2];
163
+ return out;
164
+ }
165
+ var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
166
+ function readV1(r, version, file) {
167
+ const count = r.u16();
168
+ for (let i = 0; i < count && file.tips.length < MAX_TIPS; i++) {
169
+ const type = r.u16();
170
+ const size = r.u32();
171
+ r.need(size);
172
+ const end = r.pos + size;
173
+ const b = new Reader(r.bytes, end);
174
+ b.pos = r.pos;
175
+ try {
176
+ if (type === 1) {
177
+ b.skip(4);
178
+ const spacing = b.u16();
179
+ const diameter = b.u16();
180
+ const roundness = b.u16();
181
+ const angle = b.i16();
182
+ const hardness = b.u16();
183
+ file.tips.push({ kind: "computed", diameter: clamp(diameter, 1, 2500), roundness: clamp(roundness / 100, 0.01, 1), angle: clamp(angle, -180, 180), hardness: clamp(hardness / 100, 0, 1), spacing: clamp(spacing / 100, 0.01, 10) });
184
+ } else if (type === 2) {
185
+ b.skip(4);
186
+ const spacing = b.u16();
187
+ let name;
188
+ if (version === 2) {
189
+ const n = b.u32();
190
+ if (n > 1024) throw new AbrError("A tip name is too long.");
191
+ const chars = [];
192
+ for (let k = 0; k < n; k++) chars.push(b.u16());
193
+ name = String.fromCharCode(...chars).replace(/\0+$/, "").trim() || void 0;
194
+ }
195
+ b.skip(1 + 8);
196
+ const top = b.i32();
197
+ const left = b.i32();
198
+ const bottom = b.i32();
199
+ const right = b.i32();
200
+ const depth = b.u16();
201
+ const compression = b.u8();
202
+ const width = right - left;
203
+ const height = bottom - top;
204
+ file.tips.push({ kind: "sampled", ...name ? { name } : {}, width, height, mask: readImage(b, width, height, depth, compression), ...spacing ? { spacing: clamp(spacing / 100, 0.01, 10) } : {} });
205
+ } else file.unsupported.push(`Brush ${i + 1} is of type ${type}, which is not read.`);
206
+ } catch (e) {
207
+ file.unsupported.push(`Brush ${i + 1} could not be read: ${e.message}`);
208
+ }
209
+ r.pos = end;
210
+ }
211
+ }
212
+ function readDescriptor(bytes) {
213
+ const out = /* @__PURE__ */ new Map();
214
+ const find = (pattern, from) => {
215
+ outer: for (let p = from; p <= bytes.length - pattern.length; p++) {
216
+ for (let k = 0; k < pattern.length; k++) if (bytes[p + k] !== pattern[k]) continue outer;
217
+ return p;
218
+ }
219
+ return -1;
220
+ };
221
+ const code = (s) => [...s].map((c) => c.charCodeAt(0));
222
+ const text = (p) => {
223
+ const r = new Reader(bytes);
224
+ r.pos = p;
225
+ try {
226
+ const n = r.u32();
227
+ if (n > 512) return null;
228
+ const chars = [];
229
+ for (let k = 0; k < n; k++) chars.push(r.u16());
230
+ return String.fromCharCode(...chars).replace(/\0+$/, "");
231
+ } catch {
232
+ return null;
233
+ }
234
+ };
235
+ const NAME = code("Nm TEXT");
236
+ const SAMPLED = code("sampledDataTEXT");
237
+ const SPACING = code("SpcnUntF#Prc");
238
+ let lastName;
239
+ let lastNameAt = -1;
240
+ for (let p = find(SAMPLED, 0); p >= 0; p = find(SAMPLED, p + SAMPLED.length)) {
241
+ for (let q = find(NAME, lastNameAt + 1); q >= 0 && q < p; q = find(NAME, q + NAME.length)) {
242
+ lastName = text(q + NAME.length) ?? void 0;
243
+ lastNameAt = q;
244
+ }
245
+ const uuid = text(p + SAMPLED.length);
246
+ if (!uuid) continue;
247
+ const s = find(SPACING, p);
248
+ let spacing;
249
+ if (s >= 0 && s + SPACING.length + 8 <= bytes.length) {
250
+ const v = new DataView(bytes.buffer, bytes.byteOffset + s + SPACING.length, 8).getFloat64(0);
251
+ if (Number.isFinite(v) && v > 0) spacing = clamp(v / 100, 0.01, 10);
252
+ }
253
+ if (!out.has(uuid)) out.set(uuid, { ...lastName ? { name: lastName.trim() } : {}, ...spacing !== void 0 ? { spacing } : {} });
254
+ }
255
+ return out;
256
+ }
257
+ function readV6(r, subversion, file) {
258
+ const samples = [];
259
+ let presets = /* @__PURE__ */ new Map();
260
+ while (r.pos + 12 <= r.end) {
261
+ const sig = r.ascii(4);
262
+ if (sig !== "8BIM" && sig !== "8B64") break;
263
+ const key = r.ascii(4);
264
+ const len = r.u32();
265
+ r.need(len);
266
+ const end = r.pos + len;
267
+ if (key === "samp") {
268
+ const s = new Reader(r.bytes, end);
269
+ s.pos = r.pos;
270
+ while (s.pos + 4 <= end && samples.length < MAX_TIPS) {
271
+ const size = s.u32();
272
+ const padded = size + (4 - size % 4) % 4;
273
+ const next = s.pos + padded;
274
+ if (s.pos + size > end) break;
275
+ const b = new Reader(r.bytes, s.pos + size);
276
+ b.pos = s.pos;
277
+ try {
278
+ const keyLen = b.u8();
279
+ const uuid = b.ascii(keyLen);
280
+ b.pos = s.pos + (subversion === 1 ? 47 : 301);
281
+ const top = b.i32();
282
+ const left = b.i32();
283
+ const bottom = b.i32();
284
+ const right = b.i32();
285
+ const depth = b.u16();
286
+ const compression = b.u8();
287
+ const width = right - left;
288
+ const height = bottom - top;
289
+ samples.push({ uuid, tip: { kind: "sampled", width, height, mask: readImage(b, width, height, depth, compression) } });
290
+ } catch (e) {
291
+ file.unsupported.push(`Sampled tip ${samples.length + 1} could not be read: ${e.message}`);
292
+ }
293
+ s.pos = Math.min(next, end);
294
+ }
295
+ } else if (key === "desc") presets = readDescriptor(r.bytes.subarray(r.pos, end));
296
+ else if (key === "patt") file.unsupported.push("Patterns (the brushes' textures) are not imported; add a pattern image as a grain source with add_brush_source.");
297
+ r.pos = end + len % 2;
298
+ }
299
+ for (const { uuid, tip } of samples) {
300
+ const p = presets.get(uuid);
301
+ file.tips.push({ ...tip, ...p?.name ? { name: p.name } : {}, ...p?.spacing !== void 0 ? { spacing: p.spacing } : {} });
302
+ }
303
+ if (presets.size) file.unsupported.push("Preset dynamics, textures, dual brushes and colour settings are not mapped; each tip imports with its name and spacing.");
304
+ }
305
+ function readAbr(bytes) {
306
+ const r = new Reader(bytes);
307
+ let version;
308
+ try {
309
+ version = r.u16();
310
+ } catch {
311
+ throw new AbrError("Not a brush file: it is empty.");
312
+ }
313
+ const file = { version, tips: [], unsupported: [] };
314
+ try {
315
+ if (version === 1 || version === 2) readV1(r, version, file);
316
+ else if (version === 6 || version === 7 || version === 10) {
317
+ const subversion = r.u16();
318
+ if (subversion !== 1 && subversion !== 2) throw new AbrError(`ABR version ${version}.${subversion} is not supported.`);
319
+ readV6(r, subversion, file);
320
+ } else throw new AbrError(`Not a Photoshop brush file, or ABR version ${version}, which is not supported (1, 2, 6, 7 and 10 are).`);
321
+ } catch (e) {
322
+ if (!file.tips.length) throw e instanceof AbrError ? e : new AbrError(e.message);
323
+ file.unsupported.push(`The rest of the file could not be read: ${e.message}`);
324
+ }
325
+ return file;
326
+ }
327
+
328
+ // src/brushes/import/bplist.ts
329
+ var PlistError = class extends Error {
330
+ constructor(message) {
331
+ super(message);
332
+ this.name = "PlistError";
333
+ }
334
+ };
335
+ var MAX_OBJECTS = 2e5;
336
+ var MAX_DEPTH = 64;
337
+ function isBinaryPlist(bytes) {
338
+ return bytes.length >= 40 && String.fromCharCode(...bytes.subarray(0, 8)) === "bplist00";
339
+ }
340
+ function readBinaryPlist(bytes) {
341
+ if (!isBinaryPlist(bytes)) throw new PlistError("Not a binary property list.");
342
+ const t = bytes.length - 32;
343
+ const offsetSize = bytes[t + 6];
344
+ const refSize = bytes[t + 7];
345
+ const uint = (p, n) => {
346
+ if (p < 0 || p + n > bytes.length) throw new PlistError("A property list offset is out of bounds.");
347
+ let v = 0;
348
+ for (let i = 0; i < n; i++) v = v * 256 + bytes[p + i];
349
+ return v;
350
+ };
351
+ const count = uint(t + 8, 8);
352
+ const top = uint(t + 16, 8);
353
+ const tableAt = uint(t + 24, 8);
354
+ if (![1, 2, 4, 8].includes(offsetSize) || ![1, 2, 4, 8].includes(refSize)) throw new PlistError("The property list trailer is malformed.");
355
+ if (count > MAX_OBJECTS || top >= count || tableAt + count * offsetSize > t) throw new PlistError("The property list is malformed or too large.");
356
+ const offsetOf = (i) => {
357
+ if (i >= count) throw new PlistError("A property list reference is out of range.");
358
+ const o = uint(tableAt + i * offsetSize, offsetSize);
359
+ if (o >= tableAt) throw new PlistError("A property list object is out of bounds.");
360
+ return o;
361
+ };
362
+ const reading = /* @__PURE__ */ new Set();
363
+ const read = (index, depth) => {
364
+ if (depth > MAX_DEPTH) throw new PlistError("The property list is nested too deeply.");
365
+ if (reading.has(index)) throw new PlistError("The property list refers to itself.");
366
+ let p = offsetOf(index);
367
+ const marker = bytes[p];
368
+ const kind = marker >> 4;
369
+ let info = marker & 15;
370
+ p++;
371
+ const length = () => {
372
+ if (info !== 15) return info;
373
+ const m = bytes[p];
374
+ if (m >> 4 !== 1) throw new PlistError("A property list length is malformed.");
375
+ const n = 1 << (m & 15);
376
+ const v = uint(p + 1, n);
377
+ p += 1 + n;
378
+ return v;
379
+ };
380
+ switch (kind) {
381
+ case 0:
382
+ return info === 8 ? false : info === 9 ? true : null;
383
+ case 1: {
384
+ const n = 1 << info;
385
+ if (n === 8) {
386
+ const v = new DataView(bytes.buffer, bytes.byteOffset + p, 8).getBigInt64(0);
387
+ return Number(v);
388
+ }
389
+ return uint(p, n);
390
+ }
391
+ case 2: {
392
+ const n = 1 << info;
393
+ if (p + n > bytes.length) throw new PlistError("A property list number is out of bounds.");
394
+ const view = new DataView(bytes.buffer, bytes.byteOffset + p, n);
395
+ return n === 4 ? view.getFloat32(0) : n === 8 ? view.getFloat64(0) : 0;
396
+ }
397
+ case 3:
398
+ return p + 8 <= bytes.length ? new DataView(bytes.buffer, bytes.byteOffset + p, 8).getFloat64(0) : 0;
399
+ case 4: {
400
+ const n = length();
401
+ if (p + n > bytes.length) throw new PlistError("Property list data is out of bounds.");
402
+ return bytes.slice(p, p + n);
403
+ }
404
+ case 5: {
405
+ const n = length();
406
+ if (p + n > bytes.length) throw new PlistError("A property list string is out of bounds.");
407
+ return String.fromCharCode(...bytes.subarray(p, p + n));
408
+ }
409
+ case 6: {
410
+ const n = length();
411
+ if (p + n * 2 > bytes.length) throw new PlistError("A property list string is out of bounds.");
412
+ let s = "";
413
+ for (let i = 0; i < n; i++) s += String.fromCharCode(bytes[p + i * 2] << 8 | bytes[p + i * 2 + 1]);
414
+ return s;
415
+ }
416
+ case 8:
417
+ return { uid: uint(p, info + 1) };
418
+ case 10:
419
+ case 12: {
420
+ const n = length();
421
+ reading.add(index);
422
+ const out = [];
423
+ for (let i = 0; i < n; i++) out.push(read(uint(p + i * refSize, refSize), depth + 1));
424
+ reading.delete(index);
425
+ return out;
426
+ }
427
+ case 13: {
428
+ const n = length();
429
+ reading.add(index);
430
+ const out = {};
431
+ for (let i = 0; i < n; i++) {
432
+ const key = read(uint(p + i * refSize, refSize), depth + 1);
433
+ if (typeof key !== "string") throw new PlistError("A property list key is not a string.");
434
+ out[key] = read(uint(p + (n + i) * refSize, refSize), depth + 1);
435
+ }
436
+ reading.delete(index);
437
+ return out;
438
+ }
439
+ default:
440
+ throw new PlistError(`Property list object type ${kind.toString(16)} is not supported.`);
441
+ }
442
+ };
443
+ return read(top, 0);
444
+ }
445
+ function keyedArchiveRoot(plist) {
446
+ const top = plist;
447
+ const objects = Array.isArray(top?.$objects) ? top.$objects : null;
448
+ const root = top?.$top?.root;
449
+ if (!objects || !root || typeof root !== "object" || !("uid" in root)) return null;
450
+ const at = (v) => v && typeof v === "object" && "uid" in v && typeof v.uid === "number" ? objects[v.uid] ?? null : v;
451
+ const obj = at(root);
452
+ if (!obj || typeof obj !== "object" || Array.isArray(obj) || obj instanceof Uint8Array) return null;
453
+ const out = {};
454
+ for (const [k, v] of Object.entries(obj)) {
455
+ const value = at(v);
456
+ out[k] = value === "$null" ? null : value;
457
+ }
458
+ return out;
459
+ }
460
+
461
+ // src/brushes/import/zip.ts
462
+ var ZipError = class extends Error {
463
+ constructor(message) {
464
+ super(message);
465
+ this.name = "ZipError";
466
+ }
467
+ };
468
+ var u16 = (b, p) => b[p] | b[p + 1] << 8;
469
+ var u32 = (b, p) => (b[p] | b[p + 1] << 8 | b[p + 2] << 16 | b[p + 3] << 24) >>> 0;
470
+ function isZip(bytes) {
471
+ return bytes.length >= 22 && bytes[0] === 80 && bytes[1] === 75 && (bytes[2] === 3 || bytes[2] === 5) && (bytes[3] === 4 || bytes[3] === 6);
472
+ }
473
+ function readZip(bytes, limits = {}) {
474
+ const maxEntries = limits.maxEntries ?? 4096;
475
+ const maxEntry = limits.maxEntrySize ?? 64 * 1024 * 1024;
476
+ const maxTotal = limits.maxTotalSize ?? 512 * 1024 * 1024;
477
+ let eocd = -1;
478
+ for (let p2 = bytes.length - 22; p2 >= Math.max(0, bytes.length - 22 - 65535); p2--) {
479
+ if (u32(bytes, p2) === 101010256) {
480
+ eocd = p2;
481
+ break;
482
+ }
483
+ }
484
+ if (eocd < 0) throw new ZipError("Not a ZIP archive: no end of central directory.");
485
+ const count = u16(bytes, eocd + 10);
486
+ const dirSize = u32(bytes, eocd + 12);
487
+ const dirOffset = u32(bytes, eocd + 16);
488
+ if (count === 65535 || dirOffset === 4294967295) throw new ZipError("ZIP64 archives are not supported.");
489
+ if (count > maxEntries) throw new ZipError(`The archive has ${count} entries; at most ${maxEntries} are read.`);
490
+ if (dirOffset + dirSize > eocd) throw new ZipError("The ZIP central directory is out of bounds.");
491
+ const decoder = new TextDecoder("utf-8", { fatal: false });
492
+ const entries = [];
493
+ let total = 0;
494
+ let p = dirOffset;
495
+ for (let i = 0; i < count; i++) {
496
+ if (p + 46 > eocd || u32(bytes, p) !== 33639248) throw new ZipError(`ZIP directory entry ${i} is malformed.`);
497
+ const flags = u16(bytes, p + 8);
498
+ const method = u16(bytes, p + 10);
499
+ const csize = u32(bytes, p + 20);
500
+ const size = u32(bytes, p + 24);
501
+ const nameLen = u16(bytes, p + 28);
502
+ const extraLen = u16(bytes, p + 30);
503
+ const commentLen = u16(bytes, p + 32);
504
+ const local = u32(bytes, p + 42);
505
+ if (p + 46 + nameLen > eocd) throw new ZipError(`ZIP directory entry ${i} is malformed.`);
506
+ const name = decoder.decode(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
507
+ p += 46 + nameLen + extraLen + commentLen;
508
+ if (name.endsWith("/")) continue;
509
+ if (flags & 1) throw new ZipError(`"${name}" is encrypted; encrypted archives are not supported.`);
510
+ if (method !== 0 && method !== 8) throw new ZipError(`"${name}" uses compression method ${method}; only stored and deflated files are read.`);
511
+ if (size > maxEntry) throw new ZipError(`"${name}" is ${size} bytes; files over ${maxEntry} bytes are not read.`);
512
+ total += size;
513
+ if (total > maxTotal) throw new ZipError(`The archive unpacks to more than ${maxTotal} bytes.`);
514
+ if (local + 30 > bytes.length || u32(bytes, local) !== 67324752) throw new ZipError(`"${name}" has no local header.`);
515
+ const start = local + 30 + u16(bytes, local + 26) + u16(bytes, local + 28);
516
+ if (start + csize > bytes.length) throw new ZipError(`"${name}" runs past the end of the archive.`);
517
+ let cached = null;
518
+ entries.push({
519
+ name,
520
+ size,
521
+ read() {
522
+ if (cached) return cached;
523
+ const raw = bytes.subarray(start, start + csize);
524
+ const out = method === 0 ? raw : inflateRaw(raw, { maxOutput: Math.max(size, 1), sizeHint: size });
525
+ if (out.length !== size) throw new ZipError(`"${name}" unpacked to ${out.length} bytes, not the ${size} its directory says.`);
526
+ cached = out;
527
+ return out;
528
+ }
529
+ });
530
+ }
531
+ return entries;
532
+ }
533
+
534
+ // src/brushes/import/pack.ts
535
+ var BRUSH_PACK_FORMATS = ["abr", "brush", "brushset"];
536
+ var MAX_BRUSH_PACK_BYTES = 64 * 1024 * 1024;
537
+ var MAX_PACK_BRUSHES = 300;
538
+ var BrushPackError = class extends Error {
539
+ constructor(message) {
540
+ super(message);
541
+ this.name = "BrushPackError";
542
+ }
543
+ };
544
+ var slug = (s, max = 40) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max).replace(/-+$/, "");
545
+ var stem = (fileName) => fileName.replace(/^.*[\\/]/, "").replace(/\.[a-z0-9]+$/i, "");
546
+ var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
547
+ function brushPackFormat(bytes, fileName, format) {
548
+ if (format) return format;
549
+ const ext = /\.([a-z]+)$/i.exec(fileName)?.[1]?.toLowerCase();
550
+ if (ext === "abr" || ext === "brush" || ext === "brushset") return ext;
551
+ if (isZip(bytes)) return "brushset";
552
+ if (bytes.length >= 2 && [1, 2, 6, 7, 10].includes(bytes[0] << 8 | bytes[1])) return "abr";
553
+ throw new BrushPackError(`"${fileName}" is not a brush pack this reads: give a .abr (Photoshop), .brush or .brushset (Procreate) file.`);
554
+ }
555
+ function idMaker(prefix) {
556
+ const used = /* @__PURE__ */ new Set();
557
+ return (name) => {
558
+ const base = `${prefix}-${slug(name, 20) || "brush"}`.slice(0, 58).replace(/-+$/, "");
559
+ let id = base;
560
+ for (let n = 2; used.has(id) || !BRUSH_ID.test(id); n++) id = `${base}-${n}`;
561
+ used.add(id);
562
+ return id;
563
+ };
564
+ }
565
+ function licenceText(name, bytes) {
566
+ let text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, 64 * 1024));
567
+ if (/\.rtf$/i.test(name)) text = text.replace(/\\[a-z]+-?\d* ?|[{}]/gi, "");
568
+ text = text.replace(/\r\n?/g, "\n").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
569
+ return text ? `${stem(name)}: ${text.slice(0, 4e3)}` : null;
570
+ }
571
+ var LICENCE_FILE = /(^|\/)[^/]*(licen[cs]e|terms|eula|readme|copyright)[^/]*\.(txt|md|rtf)$/i;
572
+ function readAbrPack(bytes, name, prefix, licence, pack) {
573
+ const file = readAbr(bytes);
574
+ pack.unsupported.push(...file.unsupported);
575
+ const id = idMaker(prefix);
576
+ file.tips.slice(0, MAX_PACK_BRUSHES).forEach((tip, i) => {
577
+ const label = tip.name ?? `${name} ${i + 1}`;
578
+ const brushId = id(tip.name ?? String(i + 1));
579
+ const base = { id: brushId, name: label.slice(0, 80), kind: "mark", tags: ["imported"], description: `Imported from ${name}.abr.` };
580
+ if (tip.kind === "computed") {
581
+ pack.brushes.push({ ...base, tip: { shape: tip.roundness < 1 ? "flat" : "round", size: tip.diameter, hardness: tip.hardness, spacing: tip.spacing, roundness: tip.roundness, angle: tip.angle } });
582
+ return;
583
+ }
584
+ try {
585
+ const source = brushSourceFromMask({ id: `${brushId}-shape`, name: label, licence, origin: `${name}.abr` }, tip.width, tip.height, tip.mask);
586
+ pack.sources.push(source);
587
+ pack.brushes.push({ ...base, tip: { shape: "stamp", size: clamp2(Math.max(tip.width, tip.height), 1, 2500), spacing: tip.spacing ?? 0.25, stamps: [brushSourceRef(source.id)] } });
588
+ } catch (e) {
589
+ pack.unsupported.push(`"${label}": ${e.message}`);
590
+ }
591
+ });
592
+ if (file.tips.length > MAX_PACK_BRUSHES) pack.unsupported.push(`Only the first ${MAX_PACK_BRUSHES} of ${file.tips.length} tips were imported.`);
593
+ }
594
+ function readProcreatePack(bytes, name, prefix, licence, pack) {
595
+ let entries;
596
+ try {
597
+ entries = readZip(bytes);
598
+ } catch (e) {
599
+ throw new BrushPackError(`"${name}" can't be read as a Procreate brush pack: ${e.message}`);
600
+ }
601
+ const byName = new Map(entries.map((e) => [e.name, e]));
602
+ const found = [];
603
+ for (const e of entries) {
604
+ if (!LICENCE_FILE.test(e.name) || e.size > 1024 * 1024) continue;
605
+ const text = licenceText(e.name, e.read());
606
+ if (text) found.push(text);
607
+ }
608
+ pack.licence.push(...found);
609
+ const terms = found.length ? found.join("\n\n").slice(0, 2e3) : void 0;
610
+ const dirs = entries.filter((e) => /(^|\/)Brush\.archive$/.test(e.name)).map((e) => e.name.slice(0, -"Brush.archive".length));
611
+ if (!dirs.length) throw new BrushPackError(`"${name}" has no Procreate brushes (no Brush.archive inside).`);
612
+ const id = idMaker(prefix);
613
+ dirs.slice(0, MAX_PACK_BRUSHES).forEach((dir, i) => {
614
+ let settings = null;
615
+ try {
616
+ const archive = byName.get(`${dir}Brush.archive`).read();
617
+ if (isBinaryPlist(archive)) settings = keyedArchiveRoot(readBinaryPlist(archive));
618
+ } catch {
619
+ settings = null;
620
+ }
621
+ const own = typeof settings?.name === "string" && settings.name.trim() ? settings.name.trim() : null;
622
+ const label = (own ?? (dirs.length === 1 ? name : `${name} ${i + 1}`)).slice(0, 80);
623
+ const brushId = id(own ?? String(i + 1));
624
+ const where = `${name}${dir ? `/${dir.replace(/\/$/, "")}` : ""}`;
625
+ const source = (file, kind) => {
626
+ const e = byName.get(`${dir}${file}`);
627
+ if (!e) return null;
628
+ try {
629
+ const s = makeBrushSource({ id: `${brushId}-${kind}`, name: `${label} ${kind}`, image: e.read(), channel: "lightness", licence: [licence, terms].filter(Boolean).join("\n\n") || void 0, origin: `${where}/${file}` });
630
+ pack.sources.push(s);
631
+ return s;
632
+ } catch (err) {
633
+ pack.unsupported.push(`"${label}" ${kind}: ${err.message}`);
634
+ return null;
635
+ }
636
+ };
637
+ const shape = source("Shape.png", "shape");
638
+ const grain = source("Grain.png", "grain");
639
+ const plot = typeof settings?.plotSpacing === "number" && settings.plotSpacing > 0 ? clamp2(settings.plotSpacing, 0.01, 10) : void 0;
640
+ pack.brushes.push({
641
+ id: brushId,
642
+ name: label,
643
+ kind: "mark",
644
+ tags: ["imported", "procreate"],
645
+ description: `Imported from ${name}.`,
646
+ tip: {
647
+ shape: shape ? "stamp" : "round",
648
+ size: 40,
649
+ ...plot !== void 0 ? { spacing: plot } : {},
650
+ ...shape ? { stamps: [brushSourceRef(shape.id)] } : {},
651
+ ...grain ? { grain: { source: brushSourceRef(grain.id), mode: "moving" } } : {}
652
+ }
653
+ });
654
+ if (!settings) pack.unsupported.push(`"${label}": its Brush.archive could not be read; the brush takes its name from the file and default settings.`);
655
+ });
656
+ if (dirs.length > MAX_PACK_BRUSHES) pack.unsupported.push(`Only the first ${MAX_PACK_BRUSHES} of ${dirs.length} brushes were imported.`);
657
+ pack.unsupported.push("Procreate settings other than the name, shape, grain and spacing (dynamics, Apple Pencil curves, taper, wet mix, colour dynamics, rendering modes) are not mapped.");
658
+ }
659
+ function readBrushPack(bytes, options) {
660
+ if (bytes.length > MAX_BRUSH_PACK_BYTES) throw new BrushPackError(`"${options.fileName}" is ${bytes.length} bytes; packs over ${MAX_BRUSH_PACK_BYTES} bytes are not read.`);
661
+ const format = brushPackFormat(bytes, options.fileName, options.format);
662
+ const name = stem(options.fileName) || "Brushes";
663
+ const prefix = slug(options.prefix ?? name) || "pack";
664
+ const licence = options.licence?.trim() ? options.licence.trim().slice(0, 2e3) : void 0;
665
+ const pack = { format, brushes: [], sources: [], unsupported: [], licence: licence ? [licence] : [] };
666
+ try {
667
+ if (format === "abr") readAbrPack(bytes, name, prefix, licence, pack);
668
+ else readProcreatePack(bytes, name, prefix, licence, pack);
669
+ } catch (e) {
670
+ throw e instanceof BrushPackError ? e : new BrushPackError(`"${options.fileName}" can't be read: ${e.message}`);
671
+ }
672
+ if (!pack.brushes.length) throw new BrushPackError(`"${options.fileName}" has no brushes that can be imported. ${pack.unsupported.join(" ")}`.trim());
673
+ return pack;
674
+ }
675
+ async function importBrushPack(library, bytes, options) {
676
+ const pack = readBrushPack(bytes, options);
677
+ const r = await library.importFile({ format: "pixerate-brushes", version: 2, brushes: pack.brushes, sources: pack.sources }, { overwrite: !!options.overwrite });
678
+ const done = /* @__PURE__ */ new Set([...r.added, ...r.replaced]);
679
+ return {
680
+ format: pack.format,
681
+ brushes: pack.brushes.filter((b) => done.has(b.id)).map((b) => ({ id: b.id, name: b.name })),
682
+ sources: r.sources ?? [],
683
+ skipped: r.skipped,
684
+ unsupported: pack.unsupported,
685
+ licence: pack.licence,
686
+ problems: r.problems
687
+ };
688
+ }
689
+ async function importBrushFile(library, file, options = {}) {
690
+ if (/\.json$/i.test(file.name)) {
691
+ let raw;
692
+ try {
693
+ raw = JSON.parse(await file.text());
694
+ } catch {
695
+ throw new BrushPackError(`"${file.name}" is not JSON.`);
696
+ }
697
+ return library.importFile(raw, { overwrite: !!options.overwrite });
698
+ }
699
+ return importBrushPack(library, new Uint8Array(await file.arrayBuffer()), { fileName: file.name, ...options });
700
+ }
701
+
61
702
  // src/brushes/tools.ts
62
703
  var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
63
704
  var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
@@ -402,6 +1043,42 @@ function createBrushLibraryTools(library, options = {}) {
402
1043
  },
403
1044
  annotations: { title: "Import brushes", ...WRITE, destructiveHint: true },
404
1045
  handler: async ({ file, overwrite }) => library.importFile(file, { overwrite: !!overwrite })
1046
+ },
1047
+ {
1048
+ name: "import_brush_pack",
1049
+ title: "Import a brush pack",
1050
+ description: `Imports a brush pack someone already owns: Photoshop .abr (sampled tips become image sources and stamp-tip brushes, computed tips round or flat brushes; preset names and spacing where the file has them) or Procreate .brush / .brushset (each brush's shape becomes its image tip and its grain its tip grain, with its name and spacing). Brushes and sources are added to the library in one import; ids already saved are skipped unless overwrite. Returns format, brushes (id, name, renderable: whether this host, ${profile.name}, can paint it), sources (ids), skipped, unsupported (what was not imported or mapped, in words), licence (the licence given and any licence or terms files found in the pack, also kept on each source) and problems. Packs are often licensed for personal use only: pass the licence they came with.`,
1051
+ inputSchema: {
1052
+ type: "object",
1053
+ properties: {
1054
+ file: { type: "string", minLength: 4, maxLength: Math.ceil(MAX_BRUSH_PACK_BYTES * 4 / 3) + 4, description: `The pack's bytes, base64 (at most ${MAX_BRUSH_PACK_BYTES / 1024 / 1024} MB).` },
1055
+ fileName: { type: "string", minLength: 1, maxLength: 200, description: 'The file name, e.g. "Inky.abr": its extension picks the format and its name prefixes the brush ids and names.' },
1056
+ format: { type: "string", enum: [...BRUSH_PACK_FORMATS], description: "abr, brush or brushset (default: from the file name, then the contents)." },
1057
+ prefix: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{0,39}$", description: "Id prefix for the brushes and sources (default: from the file name)." },
1058
+ licence: { type: "string", maxLength: 2e3, description: "The licence or terms the pack came with, kept on every source it adds." },
1059
+ overwrite: { type: "boolean", description: "Replace saved brushes and sources with the same ids (default false: skip them)." }
1060
+ },
1061
+ required: ["file", "fileName"],
1062
+ additionalProperties: false
1063
+ },
1064
+ annotations: { title: "Import a brush pack", ...WRITE, destructiveHint: true },
1065
+ handler: async ({ file, fileName, format, prefix, licence, overwrite }) => {
1066
+ let bytes;
1067
+ try {
1068
+ bytes = base64ToBytes(file.replace(/^data:[^,]*,/, ""));
1069
+ } catch {
1070
+ throw new BrushLibraryError("invalid", "file is not base64: pass the pack's bytes as base64 (a data: URL works too).");
1071
+ }
1072
+ let r;
1073
+ try {
1074
+ r = await importBrushPack(library, bytes, { fileName, ...format ? { format } : {}, ...prefix ? { prefix } : {}, ...licence ? { licence } : {}, overwrite: !!overwrite });
1075
+ } catch (e) {
1076
+ if (e instanceof BrushLibraryError) throw e;
1077
+ throw new BrushLibraryError("invalid", `${e.message} Nothing was imported.`);
1078
+ }
1079
+ const entries = await Promise.all(r.brushes.map((b) => library.get(b.id)));
1080
+ return { ...r, brushes: r.brushes.map((b, i) => ({ ...b, renderable: entries[i] ? checkBrush(entries[i].brush, profile).ok : false })) };
1081
+ }
405
1082
  }
406
1083
  );
407
1084
  return tools;
@@ -594,6 +1271,16 @@ export {
594
1271
  BRUSH_UNSET_PROPERTY,
595
1272
  mergeBrushPatch,
596
1273
  changedBrushPaths,
1274
+ AbrError,
1275
+ readAbr,
1276
+ BRUSH_PACK_FORMATS,
1277
+ MAX_BRUSH_PACK_BYTES,
1278
+ MAX_PACK_BRUSHES,
1279
+ BrushPackError,
1280
+ brushPackFormat,
1281
+ readBrushPack,
1282
+ importBrushPack,
1283
+ importBrushFile,
597
1284
  createBrushLibraryTools,
598
1285
  createBrushLibraryToolProvider,
599
1286
  BRUSH_SLIDERS,