@pixerate/editor 0.89.0 → 0.90.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.
@@ -271,8 +271,8 @@ function whiteBalanceFromSample(rgb) {
271
271
  if (lin[0] + lin[1] + lin[2] <= 1e-6) return { temperature: 0, tint: 0 };
272
272
  const src = linearSrgbToXy(applyMatrix3(AS_SHOT_WB_INV, lin));
273
273
  const { kelvin, tint } = xyToTempTint(src);
274
- const clamp2 = (v) => Math.max(-100, Math.min(100, v));
275
- return { temperature: clamp2(kelvinToTemperature(kelvin)), tint: clamp2(tint) };
274
+ const clamp3 = (v) => Math.max(-100, Math.min(100, v));
275
+ return { temperature: clamp3(kelvinToTemperature(kelvin)), tint: clamp3(tint) };
276
276
  }
277
277
  var TONE_LUT_SIZE = 4096;
278
278
  var GREY = 0.18;
@@ -1001,20 +1001,20 @@ function distanceTransform(inside, width, height) {
1001
1001
  for (let y = 0; y < height; y++) {
1002
1002
  const row = y * width;
1003
1003
  let count = 0;
1004
- for (let q = 0; q < width; q++) {
1005
- const dq = vertical[row + q];
1004
+ for (let q2 = 0; q2 < width; q2++) {
1005
+ const dq = vertical[row + q2];
1006
1006
  if (dq === NONE) continue;
1007
1007
  const fq = dq * dq;
1008
1008
  let start = -Infinity;
1009
1009
  while (count > 0) {
1010
1010
  const p = sites[count - 1];
1011
1011
  const dp = vertical[row + p];
1012
- start = (fq - dp * dp) / (2 * (q - p)) + (q + p) * 0.5;
1012
+ start = (fq - dp * dp) / (2 * (q2 - p)) + (q2 + p) * 0.5;
1013
1013
  if (start > starts[count - 1]) break;
1014
1014
  count--;
1015
1015
  }
1016
1016
  if (count === 0) start = -Infinity;
1017
- sites[count] = q;
1017
+ sites[count] = q2;
1018
1018
  starts[count] = start;
1019
1019
  count++;
1020
1020
  }
@@ -1551,9 +1551,9 @@ function levelsValue(ch, v, quantum) {
1551
1551
  const range = Math.max(1e-6, ch.inWhite - ch.inBlack);
1552
1552
  let u;
1553
1553
  if (quantum !== void 0 && Number.isFinite(quantum) && quantum >= 1) {
1554
- const q = quantum;
1555
- const vq = Math.round(v * q) / q;
1556
- u = Math.floor(clamp01((vq - ch.inBlack) / range) * q + 0.5 + 1e-3) / q;
1554
+ const q2 = quantum;
1555
+ const vq = Math.round(v * q2) / q2;
1556
+ u = Math.floor(clamp01((vq - ch.inBlack) / range) * q2 + 0.5 + 1e-3) / q2;
1557
1557
  } else {
1558
1558
  u = clamp01((v - ch.inBlack) / range);
1559
1559
  }
@@ -1587,13 +1587,13 @@ function rgbToHsl(c) {
1587
1587
  }
1588
1588
  function hslToRgb(h, s, l) {
1589
1589
  if (s <= 0) return [l, l, l];
1590
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1591
- const p = 2 * l - q;
1590
+ const q2 = l < 0.5 ? l * (1 + s) : l + s - l * s;
1591
+ const p = 2 * l - q2;
1592
1592
  const f = (t) => {
1593
1593
  t = remEuclid(t, 1);
1594
- if (t < 1 / 6) return p + (q - p) * 6 * t;
1595
- if (t < 0.5) return q;
1596
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1594
+ if (t < 1 / 6) return p + (q2 - p) * 6 * t;
1595
+ if (t < 0.5) return q2;
1596
+ if (t < 2 / 3) return p + (q2 - p) * (2 / 3 - t) * 6;
1597
1597
  return p;
1598
1598
  };
1599
1599
  return [f(h + 1 / 3), f(h), f(h - 1 / 3)];
@@ -1671,10 +1671,10 @@ function vibranceLinear(c, v) {
1671
1671
  t2 = t * (1 - a / 4) * (1 - a + a * t * (1 + t) / 2);
1672
1672
  v2 = mx - a * shape;
1673
1673
  } else {
1674
- const [c0, c1, p, q, r0, ra, rt] = VIBRANCE_BOOST;
1674
+ const [c0, c1, p, q2, r0, ra, rt] = VIBRANCE_BOOST;
1675
1675
  const w = vibranceSkin(hsvHue(c, mx, mn));
1676
1676
  const a = v * (1 - w) + 0.64 * Math.pow(v, 1.6) * w;
1677
- const boost = (c0 * a + c1 * a * a) * Math.pow(t, p) * Math.pow(1 - t, q) * Math.exp((r0 + ra * a + rt * (1 - t)) * Math.max(0, 1 - mx));
1677
+ const boost = (c0 * a + c1 * a * a) * Math.pow(t, p) * Math.pow(1 - t, q2) * Math.exp((r0 + ra * a + rt * (1 - t)) * Math.max(0, 1 - mx));
1678
1678
  t2 = Math.min(1, t * (1 + boost));
1679
1679
  v2 = mx + v / 4 * shape;
1680
1680
  }
@@ -3961,7 +3961,7 @@ function polygonCoverage(points, width, height, antiAlias = true) {
3961
3961
  if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });
3962
3962
  else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });
3963
3963
  }
3964
- edges.sort((p, q) => p.top - q.top);
3964
+ edges.sort((p, q2) => p.top - q2.top);
3965
3965
  let next = 0;
3966
3966
  let active = [];
3967
3967
  const xs = [];
@@ -4425,13 +4425,13 @@ function refineCoverage(guide, m, w, h, params = {}) {
4425
4425
  if (any) {
4426
4426
  const dist = distanceTransform(boundary, w, h);
4427
4427
  const r = gfRadius(p);
4428
- const q = guidedFilterColor(guide, m, w, h, r);
4428
+ const q2 = guidedFilterColor(guide, m, w, h, r);
4429
4429
  const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
4430
4430
  for (let i = 0; i < n; i++) {
4431
4431
  const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
4432
4432
  const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
4433
4433
  if (wgt > 0) {
4434
- const qi = Math.min(1, Math.max(0, q[i]));
4434
+ const qi = Math.min(1, Math.max(0, q2[i]));
4435
4435
  a[i] = m[i] + (qi - m[i]) * wgt;
4436
4436
  }
4437
4437
  }
@@ -4583,6 +4583,53 @@ function serializeLayers(layers) {
4583
4583
  };
4584
4584
  return { layers: layers.map((l) => serializeLayer(l, ctx)), assets };
4585
4585
  }
4586
+ var externalRefs = /* @__PURE__ */ new WeakMap();
4587
+ async function serializeLayersExternal(layers, store) {
4588
+ const inline = serializeLayers(layers);
4589
+ let refs = externalRefs.get(store);
4590
+ if (!refs) {
4591
+ refs = /* @__PURE__ */ new Map();
4592
+ externalRefs.set(store, refs);
4593
+ }
4594
+ const assets = {};
4595
+ for (const [id, url] of Object.entries(inline.assets)) {
4596
+ let ref = refs.get(url);
4597
+ if (!ref) {
4598
+ const parsed = parseDataUrl(url);
4599
+ const png = decodePngHeader(parsed.bytes);
4600
+ ref = await store.put(new Blob([parsed.bytes], { type: "image/png" }), { id, width: png.width, height: png.height });
4601
+ if (typeof ref !== "string" || !ref) throw new Error(`The asset store returned no reference for "${id}".`);
4602
+ refs.set(url, ref);
4603
+ }
4604
+ assets[id] = ref;
4605
+ }
4606
+ return { layers: inline.layers, assets };
4607
+ }
4608
+ function decodePngHeader(bytes) {
4609
+ const u32 = (o) => (bytes[o] << 24 | bytes[o + 1] << 16 | bytes[o + 2] << 8 | bytes[o + 3]) >>> 0;
4610
+ return { width: u32(16), height: u32(20) };
4611
+ }
4612
+ async function resolveExternalAssets(assets, store, problems) {
4613
+ const out = {};
4614
+ for (const [id, ref] of Object.entries(assets)) {
4615
+ if (ref.startsWith("data:")) {
4616
+ out[id] = ref;
4617
+ continue;
4618
+ }
4619
+ if (!store) {
4620
+ problems.push(`Asset "${id}" is external (${ref}) and no asset store was given.`);
4621
+ continue;
4622
+ }
4623
+ try {
4624
+ const blob = await store.get(ref);
4625
+ const bytes = new Uint8Array(await blob.arrayBuffer());
4626
+ out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;
4627
+ } catch (err) {
4628
+ problems.push(`Asset "${id}" (${ref}) could not be fetched: ${err.message}`);
4629
+ }
4630
+ }
4631
+ return out;
4632
+ }
4586
4633
  function decodePngDataUrl(url, channels) {
4587
4634
  const parsed = parseDataUrl(url);
4588
4635
  if (!parsed) throw new Error("Asset is not a data URL.");
@@ -4976,6 +5023,2070 @@ function unionRect(a, b) {
4976
5023
  return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
4977
5024
  }
4978
5025
 
5026
+ // src/image-editor/psd/binary.ts
5027
+ var PsdError = class extends Error {
5028
+ constructor(message) {
5029
+ super(message);
5030
+ this.name = "PsdError";
5031
+ }
5032
+ };
5033
+ var PsdReader = class _PsdReader {
5034
+ constructor(data, start = 0, end = data.length) {
5035
+ this.data = data;
5036
+ this.start = start;
5037
+ this.end = end;
5038
+ this.pos = start;
5039
+ }
5040
+ data;
5041
+ start;
5042
+ end;
5043
+ pos;
5044
+ get remaining() {
5045
+ return this.end - this.pos;
5046
+ }
5047
+ get offset() {
5048
+ return this.pos - this.start;
5049
+ }
5050
+ need(n, what) {
5051
+ if (n < 0 || this.pos + n > this.end) throw new PsdError(`Unexpected end of data reading ${what} at byte ${this.pos}.`);
5052
+ }
5053
+ u8() {
5054
+ this.need(1, "a byte");
5055
+ return this.data[this.pos++];
5056
+ }
5057
+ i8() {
5058
+ const v = this.u8();
5059
+ return v > 127 ? v - 256 : v;
5060
+ }
5061
+ u16() {
5062
+ this.need(2, "a 16-bit value");
5063
+ const v = this.data[this.pos] << 8 | this.data[this.pos + 1];
5064
+ this.pos += 2;
5065
+ return v;
5066
+ }
5067
+ i16() {
5068
+ const v = this.u16();
5069
+ return v > 32767 ? v - 65536 : v;
5070
+ }
5071
+ u32() {
5072
+ this.need(4, "a 32-bit value");
5073
+ const d = this.data;
5074
+ const p = this.pos;
5075
+ this.pos += 4;
5076
+ return (d[p] << 24 | d[p + 1] << 16 | d[p + 2] << 8 | d[p + 3]) >>> 0;
5077
+ }
5078
+ i32() {
5079
+ return this.u32() | 0;
5080
+ }
5081
+ u64() {
5082
+ const hi = this.u32();
5083
+ const lo = this.u32();
5084
+ const v = hi * 2 ** 32 + lo;
5085
+ if (!Number.isSafeInteger(v)) throw new PsdError("A 64-bit length is too large.");
5086
+ return v;
5087
+ }
5088
+ i64() {
5089
+ const hi = this.i32();
5090
+ const lo = this.u32();
5091
+ return hi * 2 ** 32 + lo;
5092
+ }
5093
+ f32() {
5094
+ this.need(4, "a float");
5095
+ const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 4).getFloat32(0, false);
5096
+ this.pos += 4;
5097
+ return v;
5098
+ }
5099
+ f64() {
5100
+ this.need(8, "a double");
5101
+ const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 8).getFloat64(0, false);
5102
+ this.pos += 8;
5103
+ return v;
5104
+ }
5105
+ /** A length field: 8 bytes in PSB where the format says so, else 4. */
5106
+ len(long2) {
5107
+ return long2 ? this.u64() : this.u32();
5108
+ }
5109
+ bytes(n) {
5110
+ this.need(n, `${n} bytes`);
5111
+ const out = this.data.subarray(this.pos, this.pos + n);
5112
+ this.pos += n;
5113
+ return out;
5114
+ }
5115
+ ascii(n) {
5116
+ return String.fromCharCode(...this.bytes(n));
5117
+ }
5118
+ skip(n) {
5119
+ this.need(n, `${n} bytes`);
5120
+ this.pos += n;
5121
+ }
5122
+ /** A reader over the next `n` bytes (and skips them here). */
5123
+ sub(n) {
5124
+ this.need(n, `a ${n}-byte section`);
5125
+ const r = new _PsdReader(this.data, this.pos, this.pos + n);
5126
+ this.pos += n;
5127
+ return r;
5128
+ }
5129
+ peek(n) {
5130
+ return this.data.subarray(this.pos, Math.min(this.end, this.pos + n));
5131
+ }
5132
+ rest() {
5133
+ const out = this.data.subarray(this.pos, this.end);
5134
+ this.pos = this.end;
5135
+ return out;
5136
+ }
5137
+ /** Refuses counts that could not fit in what is left (each item at least `size` bytes). */
5138
+ checkCount(n, size, what) {
5139
+ if (n * size > this.remaining) throw new PsdError(`${what}: count ${n} does not fit in the remaining ${this.remaining} bytes.`);
5140
+ }
5141
+ /** Pascal string padded to a multiple of `align` (length byte included). */
5142
+ pascal(align) {
5143
+ const n = this.u8();
5144
+ const s = this.bytes(n);
5145
+ const pad = (align - (1 + n) % align) % align;
5146
+ this.skip(Math.min(pad, this.remaining));
5147
+ return s;
5148
+ }
5149
+ /** Photoshop Unicode string: u32 code-unit count, UTF-16BE units (trailing NULs dropped). */
5150
+ unicode() {
5151
+ const n = this.u32();
5152
+ this.checkCount(n, 2, "Unicode string");
5153
+ let s = "";
5154
+ for (let i = 0; i < n; i++) s += String.fromCharCode(this.u16());
5155
+ return s.replace(/\u0000+$/, "");
5156
+ }
5157
+ };
5158
+ var PsdWriter = class {
5159
+ buf = new Uint8Array(1 << 16);
5160
+ length = 0;
5161
+ ensure(n) {
5162
+ if (this.length + n <= this.buf.length) return;
5163
+ let size = this.buf.length * 2;
5164
+ while (size < this.length + n) size *= 2;
5165
+ const next = new Uint8Array(size);
5166
+ next.set(this.buf.subarray(0, this.length));
5167
+ this.buf = next;
5168
+ }
5169
+ u8(v) {
5170
+ this.ensure(1);
5171
+ this.buf[this.length++] = v & 255;
5172
+ }
5173
+ u16(v) {
5174
+ this.ensure(2);
5175
+ this.buf[this.length++] = v >>> 8 & 255;
5176
+ this.buf[this.length++] = v & 255;
5177
+ }
5178
+ i16(v) {
5179
+ this.u16(v < 0 ? v + 65536 : v);
5180
+ }
5181
+ u32(v) {
5182
+ this.ensure(4);
5183
+ this.buf[this.length++] = v >>> 24 & 255;
5184
+ this.buf[this.length++] = v >>> 16 & 255;
5185
+ this.buf[this.length++] = v >>> 8 & 255;
5186
+ this.buf[this.length++] = v & 255;
5187
+ }
5188
+ i32(v) {
5189
+ this.u32(v >>> 0);
5190
+ }
5191
+ u64(v) {
5192
+ this.u32(Math.floor(v / 2 ** 32));
5193
+ this.u32(v >>> 0);
5194
+ }
5195
+ f64(v) {
5196
+ this.ensure(8);
5197
+ new DataView(this.buf.buffer, this.length, 8).setFloat64(0, v, false);
5198
+ this.length += 8;
5199
+ }
5200
+ bytes(b) {
5201
+ this.ensure(b.length);
5202
+ this.buf.set(b, this.length);
5203
+ this.length += b.length;
5204
+ }
5205
+ ascii(s) {
5206
+ for (let i = 0; i < s.length; i++) this.u8(s.charCodeAt(i));
5207
+ }
5208
+ zeros(n) {
5209
+ this.ensure(n);
5210
+ this.buf.fill(0, this.length, this.length + n);
5211
+ this.length += n;
5212
+ }
5213
+ /** Reserves a length field; `endLen` fills it with the bytes written since. */
5214
+ beginLen(long2) {
5215
+ const at = this.length;
5216
+ if (long2) this.u64(0);
5217
+ else this.u32(0);
5218
+ return { at, long: long2 };
5219
+ }
5220
+ endLen(mark) {
5221
+ const n = this.length - mark.at - (mark.long ? 8 : 4);
5222
+ const save = this.length;
5223
+ this.length = mark.at;
5224
+ if (mark.long) this.u64(n);
5225
+ else this.u32(n);
5226
+ this.length = save;
5227
+ return n;
5228
+ }
5229
+ /** Pascal string padded to a multiple of `align`. */
5230
+ pascal(s, align) {
5231
+ const n = Math.min(255, s.length);
5232
+ this.u8(n);
5233
+ this.bytes(s.subarray(0, n));
5234
+ this.zeros((align - (1 + n) % align) % align);
5235
+ }
5236
+ unicode(s, nul = false) {
5237
+ const n = s.length + (nul ? 1 : 0);
5238
+ this.u32(n);
5239
+ for (let i = 0; i < s.length; i++) this.u16(s.charCodeAt(i));
5240
+ if (nul) this.u16(0);
5241
+ }
5242
+ result() {
5243
+ return this.buf.slice(0, this.length);
5244
+ }
5245
+ };
5246
+ function decodeLegacyName(b) {
5247
+ try {
5248
+ return new TextDecoder("utf-8", { fatal: true }).decode(b);
5249
+ } catch {
5250
+ return String.fromCharCode(...b);
5251
+ }
5252
+ }
5253
+ function encodeLegacyName(s) {
5254
+ const out = [];
5255
+ for (const ch of s) {
5256
+ const c = ch.codePointAt(0);
5257
+ out.push(c >= 32 && c < 127 ? c : 63);
5258
+ if (out.length >= 255) break;
5259
+ }
5260
+ return Uint8Array.from(out);
5261
+ }
5262
+
5263
+ // src/image-editor/psd/descriptor.ts
5264
+ var MAX_DEPTH = 64;
5265
+ var STRING_IDS = /* @__PURE__ */ new Set(["warp", "list", "time"]);
5266
+ function descriptor(classId, items = []) {
5267
+ return { name: "", classId, items };
5268
+ }
5269
+ function descGet(d, key) {
5270
+ return d.items.find(([k]) => k === key)?.[1];
5271
+ }
5272
+ function descNumber(d, key) {
5273
+ const v = descGet(d, key);
5274
+ if (!v) return void 0;
5275
+ if (v.type === "long" || v.type === "doub" || v.type === "UntF" || v.type === "comp") return v.value;
5276
+ return void 0;
5277
+ }
5278
+ function descBool(d, key) {
5279
+ const v = descGet(d, key);
5280
+ return v?.type === "bool" ? v.value : void 0;
5281
+ }
5282
+ function descObject(d, key) {
5283
+ const v = descGet(d, key);
5284
+ return v && (v.type === "Objc" || v.type === "GlbO") ? v.value : void 0;
5285
+ }
5286
+ function readId(r) {
5287
+ const n = r.u32();
5288
+ return n === 0 ? r.ascii(4) : r.ascii(n);
5289
+ }
5290
+ function writeId(w, id) {
5291
+ if (id.length === 4 && !STRING_IDS.has(id)) {
5292
+ w.u32(0);
5293
+ w.ascii(id);
5294
+ } else {
5295
+ w.u32(id.length);
5296
+ w.ascii(id);
5297
+ }
5298
+ }
5299
+ function readClass(r) {
5300
+ return { name: r.unicode(), classId: readId(r) };
5301
+ }
5302
+ function writeClass(w, c) {
5303
+ w.unicode(c.name);
5304
+ writeId(w, c.classId);
5305
+ }
5306
+ function readReferenceItem(r) {
5307
+ const type = r.ascii(4);
5308
+ switch (type) {
5309
+ case "prop":
5310
+ return { type, class: readClass(r), key: readId(r) };
5311
+ case "Clss":
5312
+ return { type, class: readClass(r) };
5313
+ case "Enmr":
5314
+ return { type, class: readClass(r), typeId: readId(r), value: readId(r) };
5315
+ case "rele":
5316
+ return { type, class: readClass(r), offset: r.i32() };
5317
+ case "Idnt":
5318
+ return { type, class: readClass(r), id: r.u32() };
5319
+ case "indx":
5320
+ return { type, class: readClass(r), index: r.u32() };
5321
+ case "name":
5322
+ return { type, class: readClass(r), name: r.unicode() };
5323
+ default:
5324
+ throw new PsdError(`Unsupported descriptor reference item "${type}".`);
5325
+ }
5326
+ }
5327
+ function writeReferenceItem(w, it) {
5328
+ w.ascii(it.type);
5329
+ writeClass(w, it.class);
5330
+ switch (it.type) {
5331
+ case "prop":
5332
+ writeId(w, it.key);
5333
+ break;
5334
+ case "Enmr":
5335
+ writeId(w, it.typeId);
5336
+ writeId(w, it.value);
5337
+ break;
5338
+ case "rele":
5339
+ w.i32(it.offset);
5340
+ break;
5341
+ case "Idnt":
5342
+ w.u32(it.id);
5343
+ break;
5344
+ case "indx":
5345
+ w.u32(it.index);
5346
+ break;
5347
+ case "name":
5348
+ w.unicode(it.name);
5349
+ break;
5350
+ }
5351
+ }
5352
+ function readValue(r, depth) {
5353
+ if (depth > MAX_DEPTH) throw new PsdError("Descriptor nesting is too deep.");
5354
+ const type = r.ascii(4);
5355
+ switch (type) {
5356
+ case "obj ": {
5357
+ const n = r.u32();
5358
+ r.checkCount(n, 8, "Descriptor reference");
5359
+ const items = [];
5360
+ for (let i = 0; i < n; i++) items.push(readReferenceItem(r));
5361
+ return { type, items };
5362
+ }
5363
+ case "Objc":
5364
+ case "GlbO":
5365
+ return { type, value: readDescriptorBody(r, depth + 1) };
5366
+ case "VlLs": {
5367
+ const n = r.u32();
5368
+ r.checkCount(n, 4, "Descriptor list");
5369
+ const items = [];
5370
+ for (let i = 0; i < n; i++) items.push(readValue(r, depth + 1));
5371
+ return { type, items };
5372
+ }
5373
+ case "doub":
5374
+ return { type, value: r.f64() };
5375
+ case "UntF":
5376
+ return { type, unit: r.ascii(4), value: r.f64() };
5377
+ case "UnFl": {
5378
+ const unit = r.ascii(4);
5379
+ const n = r.u32();
5380
+ r.checkCount(n, 8, "Unit float list");
5381
+ const values = [];
5382
+ for (let i = 0; i < n; i++) values.push(r.f64());
5383
+ return { type, unit, values };
5384
+ }
5385
+ case "TEXT":
5386
+ return { type, value: r.unicode() };
5387
+ case "enum":
5388
+ return { type, typeId: readId(r), value: readId(r) };
5389
+ case "long":
5390
+ return { type, value: r.i32() };
5391
+ case "comp":
5392
+ return { type, value: r.i64() };
5393
+ case "bool":
5394
+ return { type, value: r.u8() !== 0 };
5395
+ case "type":
5396
+ case "GlbC":
5397
+ return { type, class: readClass(r) };
5398
+ case "alis":
5399
+ case "Pth ":
5400
+ case "tdta":
5401
+ return { type, data: r.bytes(r.u32()) };
5402
+ case "ObAr":
5403
+ return { type, prefix: r.u32(), body: readDescriptorBody(r, depth + 1) };
5404
+ default:
5405
+ throw new PsdError(`Unsupported descriptor value type "${type}".`);
5406
+ }
5407
+ }
5408
+ function writeValue(w, v) {
5409
+ w.ascii(v.type);
5410
+ switch (v.type) {
5411
+ case "obj ":
5412
+ w.u32(v.items.length);
5413
+ for (const it of v.items) writeReferenceItem(w, it);
5414
+ break;
5415
+ case "Objc":
5416
+ case "GlbO":
5417
+ writeDescriptorBody(w, v.value);
5418
+ break;
5419
+ case "VlLs":
5420
+ w.u32(v.items.length);
5421
+ for (const it of v.items) writeValue(w, it);
5422
+ break;
5423
+ case "doub":
5424
+ w.f64(v.value);
5425
+ break;
5426
+ case "UntF":
5427
+ w.ascii(v.unit);
5428
+ w.f64(v.value);
5429
+ break;
5430
+ case "UnFl":
5431
+ w.ascii(v.unit);
5432
+ w.u32(v.values.length);
5433
+ for (const x of v.values) w.f64(x);
5434
+ break;
5435
+ case "TEXT":
5436
+ w.unicode(v.value, true);
5437
+ break;
5438
+ case "enum":
5439
+ writeId(w, v.typeId);
5440
+ writeId(w, v.value);
5441
+ break;
5442
+ case "long":
5443
+ w.i32(v.value);
5444
+ break;
5445
+ case "comp":
5446
+ w.u32(Math.floor(v.value / 2 ** 32) >>> 0);
5447
+ w.u32(v.value >>> 0);
5448
+ break;
5449
+ case "bool":
5450
+ w.u8(v.value ? 1 : 0);
5451
+ break;
5452
+ case "type":
5453
+ case "GlbC":
5454
+ writeClass(w, v.class);
5455
+ break;
5456
+ case "alis":
5457
+ case "Pth ":
5458
+ case "tdta":
5459
+ w.u32(v.data.length);
5460
+ w.bytes(v.data);
5461
+ break;
5462
+ case "ObAr":
5463
+ w.u32(v.prefix);
5464
+ writeDescriptorBody(w, v.body);
5465
+ break;
5466
+ }
5467
+ }
5468
+ function readDescriptorBody(r, depth) {
5469
+ if (depth > MAX_DEPTH) throw new PsdError("Descriptor nesting is too deep.");
5470
+ const name = r.unicode();
5471
+ const classId = readId(r);
5472
+ const n = r.u32();
5473
+ r.checkCount(n, 10, "Descriptor items");
5474
+ const items = [];
5475
+ for (let i = 0; i < n; i++) {
5476
+ const key = readId(r);
5477
+ items.push([key, readValue(r, depth + 1)]);
5478
+ }
5479
+ return { name, classId, items };
5480
+ }
5481
+ function writeDescriptorBody(w, d) {
5482
+ w.unicode(d.name, true);
5483
+ writeId(w, d.classId);
5484
+ w.u32(d.items.length);
5485
+ for (const [k, v] of d.items) {
5486
+ writeId(w, k);
5487
+ writeValue(w, v);
5488
+ }
5489
+ }
5490
+ function readVersionedDescriptor(r) {
5491
+ const version = r.u32();
5492
+ if (version !== 16) throw new PsdError(`Descriptor version ${version}, expected 16.`);
5493
+ return readDescriptorBody(r, 0);
5494
+ }
5495
+ function writeVersionedDescriptor(w, d) {
5496
+ w.u32(16);
5497
+ writeDescriptorBody(w, d);
5498
+ }
5499
+ function parseVersionedDescriptor(bytes) {
5500
+ return readVersionedDescriptor(new PsdReader(bytes));
5501
+ }
5502
+ function versionedDescriptorBytes(d) {
5503
+ const w = new PsdWriter();
5504
+ writeVersionedDescriptor(w, d);
5505
+ return w.result();
5506
+ }
5507
+
5508
+ // src/image-editor/psd/adjust.ts
5509
+ var ADJUSTMENT_KEYS = ["levl", "curv", "hue2", "brit", "nvrt", "thrs", "post", "expA", "vibA", "blnc", "mixr", "grdm", "phfl", "selc", "blwh", "clrL"];
5510
+ var SUPPORTED_ADJUSTMENT_KEYS = {
5511
+ levl: "levels",
5512
+ curv: "curves",
5513
+ hue2: "hueSaturation",
5514
+ vibA: "vibrance",
5515
+ blnc: "colorBalance",
5516
+ blwh: "blackWhite",
5517
+ nvrt: "invert",
5518
+ post: "posterize",
5519
+ thrs: "threshold",
5520
+ phfl: "photoFilter"
5521
+ };
5522
+ var LABELS = {
5523
+ levl: "Levels",
5524
+ curv: "Curves",
5525
+ hue2: "Hue/Saturation",
5526
+ brit: "Brightness/Contrast",
5527
+ nvrt: "Invert",
5528
+ thrs: "Threshold",
5529
+ post: "Posterize",
5530
+ expA: "Exposure",
5531
+ vibA: "Vibrance",
5532
+ blnc: "Color Balance",
5533
+ mixr: "Channel Mixer",
5534
+ grdm: "Gradient Map",
5535
+ phfl: "Photo Filter",
5536
+ selc: "Selective Color",
5537
+ blwh: "Black & White",
5538
+ clrL: "Color Lookup"
5539
+ };
5540
+ function adjustmentLabel(key) {
5541
+ return LABELS[key] ?? "adjustment";
5542
+ }
5543
+ var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
5544
+ var toHex = (rgb) => "#" + rgb.map((v) => Math.round(clamp2(v, 0, 1) * 255).toString(16).padStart(2, "0")).join("");
5545
+ function canonicalBounds(b) {
5546
+ if (b.some((v) => !Number.isFinite(v))) return [-45, -15, 15, 45];
5547
+ const rem = (v) => (v % 360 + 360) % 360;
5548
+ const out = [b[0], b[0], b[0], b[0]];
5549
+ for (let k = 1; k < 4; k++) out[k] = Math.min(out[k - 1] + rem(b[k] - out[k - 1]), out[0] + 359);
5550
+ const centre = (out[1] + out[2]) / 2;
5551
+ const shift = rem(centre + 30) - 30 - centre;
5552
+ return out.map((v) => v + shift);
5553
+ }
5554
+ function levelsRecord(r) {
5555
+ const inBlack = r.u16();
5556
+ const inWhite = r.u16();
5557
+ const outBlack = r.u16();
5558
+ const outWhite = r.u16();
5559
+ const gamma = r.u16() / 100;
5560
+ return {
5561
+ inBlack: clamp2(inBlack, 0, 255),
5562
+ inWhite: clamp2(inWhite, 0, 255),
5563
+ gamma: clamp2(gamma || 1, 0.1, 9.99),
5564
+ outBlack: clamp2(outBlack, 0, 255),
5565
+ outWhite: clamp2(outWhite, 0, 255)
5566
+ };
5567
+ }
5568
+ function curveFromMap(map) {
5569
+ const pts = [];
5570
+ for (let i = 0; i <= 255; i += 15) pts.push({ input: i, output: map[i] });
5571
+ return pts;
5572
+ }
5573
+ function parseCurves(d) {
5574
+ const r = new PsdReader(d);
5575
+ const pencil = r.u8() !== 0;
5576
+ const version = r.u16();
5577
+ if (version !== 1 && version !== 4) return null;
5578
+ const bits = r.u32();
5579
+ const params = defaultAdjustmentParams("curves");
5580
+ const order = ["master", "red", "green", "blue"];
5581
+ for (let bit = 0; bit < 32; bit++) {
5582
+ if (!(bits & 1 << bit)) continue;
5583
+ let pts;
5584
+ if (pencil) {
5585
+ pts = curveFromMap(r.bytes(256));
5586
+ } else {
5587
+ const n = r.u16();
5588
+ r.checkCount(n, 4, "Curve points");
5589
+ pts = [];
5590
+ for (let i = 0; i < n; i++) {
5591
+ const output = r.u16();
5592
+ const input = r.u16();
5593
+ pts.push({ input: clamp2(input, 0, 255), output: clamp2(output, 0, 255) });
5594
+ }
5595
+ pts.sort((a, b) => a.input - b.input);
5596
+ }
5597
+ if (bit < 4 && pts.length >= 2) params[order[bit]] = pts;
5598
+ }
5599
+ return { kind: "curves", params, approximated: pencil ? "its pencil curve is approximated by points" : void 0 };
5600
+ }
5601
+ function parseHueSaturation(d) {
5602
+ const r = new PsdReader(d);
5603
+ r.u16();
5604
+ const colorize = r.u8() !== 0;
5605
+ r.u8();
5606
+ const triple = () => [r.i16(), r.i16(), r.i16()];
5607
+ const col = triple();
5608
+ const master = triple();
5609
+ const ranges = [];
5610
+ for (let i = 0; i < 6; i++) {
5611
+ if (r.remaining < 14) {
5612
+ ranges.push(neutralHueRange(i));
5613
+ continue;
5614
+ }
5615
+ const b = [r.u16(), r.u16(), r.u16(), r.u16()];
5616
+ const [hue2, saturation2, lightness2] = triple();
5617
+ ranges.push({ hue: hue2, saturation: saturation2, lightness: lightness2, bounds: canonicalBounds(b) });
5618
+ }
5619
+ const [hue, saturation, lightness] = colorize ? col : master;
5620
+ return { kind: "hueSaturation", params: { hue, saturation, lightness, colorize, ranges } };
5621
+ }
5622
+ var BW_KEYS = [
5623
+ ["reds", "Rd "],
5624
+ ["yellows", "Yllw"],
5625
+ ["greens", "Grn "],
5626
+ ["cyans", "Cyn "],
5627
+ ["blues", "Bl "],
5628
+ ["magentas", "Mgnt"]
5629
+ ];
5630
+ var BW_TINT = [225 / 255, 211 / 255, 179 / 255];
5631
+ function descRgb(d) {
5632
+ if (!d) return null;
5633
+ if (descNumber(d, "redFloat") !== void 0) {
5634
+ return [descNumber(d, "redFloat"), descNumber(d, "greenFloat") ?? 0, descNumber(d, "blueFloat") ?? 0].map((v) => clamp2(v, 0, 1));
5635
+ }
5636
+ const r = descNumber(d, "Rd ");
5637
+ const g = descNumber(d, "Grn ");
5638
+ const b = descNumber(d, "Bl ");
5639
+ if (r === void 0 || g === void 0 || b === void 0) return null;
5640
+ return [r, g, b].map((v) => clamp2(v / 255, 0, 1));
5641
+ }
5642
+ function parseBlackWhite(d) {
5643
+ const desc = parseVersionedDescriptor(d);
5644
+ const params = defaultAdjustmentParams("blackWhite");
5645
+ for (const [field, key] of BW_KEYS) {
5646
+ const v = descNumber(desc, key);
5647
+ if (v !== void 0) params[field] = clamp2(v, -200, 300);
5648
+ }
5649
+ params.tint = descBool(desc, "useTint") ? toHex(descRgb(descObject(desc, "tintColor")) ?? BW_TINT) : null;
5650
+ return { kind: "blackWhite", params };
5651
+ }
5652
+ function labToRgb(l, a, b) {
5653
+ const fy = (l + 16) / 116;
5654
+ const fx = fy + a / 500;
5655
+ const fz = fy - b / 200;
5656
+ const f = (t) => t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29);
5657
+ const xyz = [f(fx) * D50[0], f(fy) * D50[1], f(fz) * D50[2]];
5658
+ return XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp2(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));
5659
+ }
5660
+ function decodePsdColor(space, c) {
5661
+ const n = (v) => v / 65535;
5662
+ switch (space) {
5663
+ case 0:
5664
+ return [n(c[0]), n(c[1]), n(c[2])];
5665
+ case 1: {
5666
+ const h = n(c[0]) * 360;
5667
+ const s = n(c[1]);
5668
+ const v = n(c[2]);
5669
+ const k = (m) => (m + h / 60) % 6;
5670
+ const f = (m) => v - v * s * Math.max(0, Math.min(k(m), 4 - k(m), 1));
5671
+ return [f(5), f(3), f(1)];
5672
+ }
5673
+ case 2: {
5674
+ const [cy, m, y, k] = c.map(n);
5675
+ return [cy * k, m * k, y * k];
5676
+ }
5677
+ case 7: {
5678
+ const signed = (v) => v << 16 >> 16;
5679
+ return labToRgb(Math.min(c[0], 1e4) / 100, signed(c[1]) / 100, signed(c[2]) / 100);
5680
+ }
5681
+ case 8: {
5682
+ const v = 1 - Math.min(c[0], 1e4) / 1e4;
5683
+ return [v, v, v];
5684
+ }
5685
+ default:
5686
+ return null;
5687
+ }
5688
+ }
5689
+ function parsePhotoFilter(d) {
5690
+ const r = new PsdReader(d);
5691
+ const version = r.u16();
5692
+ let rgb;
5693
+ if (version === 2) {
5694
+ const space = r.u16();
5695
+ const c = [r.u16(), r.u16(), r.u16(), r.u16()];
5696
+ rgb = [0, 2, 7, 8].includes(space) ? decodePsdColor(space, c) : null;
5697
+ } else if (version === 3) {
5698
+ const rel = [r.u32(), r.u32(), r.u32()].map((v) => v / 32768);
5699
+ if (rel.some((v) => v > 2)) return null;
5700
+ const xyz = [rel[0] * D50[0], rel[1], rel[2] * D50[2]];
5701
+ rgb = XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp2(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));
5702
+ } else {
5703
+ return null;
5704
+ }
5705
+ if (!rgb) return null;
5706
+ const density = clamp2(r.u32(), 0, 100);
5707
+ const preserveLuminosity = r.u8() !== 0;
5708
+ return { kind: "photoFilter", params: { color: toHex(rgb), density, preserveLuminosity } };
5709
+ }
5710
+ function parseAdjustmentBlock(key, data) {
5711
+ try {
5712
+ const r = new PsdReader(data);
5713
+ switch (key) {
5714
+ case "nvrt":
5715
+ return { kind: "invert", params: {} };
5716
+ case "thrs":
5717
+ return { kind: "threshold", params: { level: clamp2(r.u16(), 1, 255) } };
5718
+ case "post":
5719
+ return { kind: "posterize", params: { levels: clamp2(r.u16(), 2, 255) } };
5720
+ case "levl": {
5721
+ r.u16();
5722
+ const master = levelsRecord(r);
5723
+ const red = levelsRecord(r);
5724
+ const green = levelsRecord(r);
5725
+ const blue = levelsRecord(r);
5726
+ return { kind: "levels", params: { master, red, green, blue } };
5727
+ }
5728
+ case "curv":
5729
+ return parseCurves(data);
5730
+ case "hue2":
5731
+ return parseHueSaturation(data);
5732
+ case "vibA": {
5733
+ const desc = parseVersionedDescriptor(data);
5734
+ const get = (k) => clamp2(descNumber(desc, k) ?? 0, -100, 100);
5735
+ return { kind: "vibrance", params: { vibrance: get("vibrance"), saturation: get("Strt") } };
5736
+ }
5737
+ case "blnc": {
5738
+ const tone2 = () => [r.i16(), r.i16(), r.i16()].map((v) => clamp2(v, -100, 100));
5739
+ const shadows = tone2();
5740
+ const midtones = tone2();
5741
+ const highlights = tone2();
5742
+ return { kind: "colorBalance", params: { shadows, midtones, highlights, preserveLuminosity: r.u8() !== 0 } };
5743
+ }
5744
+ case "blwh":
5745
+ return parseBlackWhite(data);
5746
+ case "phfl":
5747
+ return parsePhotoFilter(data);
5748
+ default:
5749
+ return null;
5750
+ }
5751
+ } catch {
5752
+ return null;
5753
+ }
5754
+ }
5755
+ var q = (v) => Math.round(clamp2(v, 0, 255));
5756
+ var i16 = (v) => Math.round(clamp2(v, -32768, 32767));
5757
+ function writeLevels(w, c) {
5758
+ w.u16(q(c.inBlack));
5759
+ w.u16(q(c.inWhite));
5760
+ w.u16(q(c.outBlack));
5761
+ w.u16(q(c.outWhite));
5762
+ w.u16(Math.round(clamp2(c.gamma * 100, 1, 999)));
5763
+ }
5764
+ var long = (value) => ({ type: "long", value: i16(value) });
5765
+ function writeAdjustmentBlock(kind, params) {
5766
+ const w = new PsdWriter();
5767
+ switch (kind) {
5768
+ case "invert":
5769
+ return { key: "nvrt", data: new Uint8Array(0) };
5770
+ case "threshold": {
5771
+ const p = params;
5772
+ w.u16(Math.max(1, q(p.level)));
5773
+ w.u16(0);
5774
+ return { key: "thrs", data: w.result() };
5775
+ }
5776
+ case "posterize": {
5777
+ const p = params;
5778
+ w.u16(Math.round(clamp2(p.levels, 2, 255)));
5779
+ w.u16(0);
5780
+ return { key: "post", data: w.result() };
5781
+ }
5782
+ case "levels": {
5783
+ const p = params;
5784
+ w.u16(2);
5785
+ for (const c of [p.master, p.red, p.green, p.blue]) writeLevels(w, c);
5786
+ const ident = { inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 };
5787
+ for (let i = 4; i < 29; i++) writeLevels(w, ident);
5788
+ return { key: "levl", data: w.result() };
5789
+ }
5790
+ case "curves": {
5791
+ const p = params;
5792
+ w.u8(0);
5793
+ w.u16(1);
5794
+ w.u32(15);
5795
+ for (const c of [p.master, p.red, p.green, p.blue]) {
5796
+ const pts = c.slice(0, 19);
5797
+ w.u16(pts.length);
5798
+ for (const pt of pts) {
5799
+ w.u16(q(pt.output));
5800
+ w.u16(q(pt.input));
5801
+ }
5802
+ }
5803
+ return { key: "curv", data: w.result() };
5804
+ }
5805
+ case "hueSaturation": {
5806
+ const p = params;
5807
+ w.u16(2);
5808
+ w.u8(p.colorize ? 1 : 0);
5809
+ w.u8(0);
5810
+ for (let k = 0; k < 2; k++) for (const x of [p.hue, p.saturation, p.lightness]) w.i16(i16(x));
5811
+ for (let i = 0; i < 6; i++) {
5812
+ const r = p.ranges[i] ?? neutralHueRange(i);
5813
+ for (const b of r.bounds) w.u16(Math.round((b % 360 + 360) % 360) % 360);
5814
+ for (const x of [r.hue, r.saturation, r.lightness]) w.i16(i16(x));
5815
+ }
5816
+ return { key: "hue2", data: w.result() };
5817
+ }
5818
+ case "vibrance": {
5819
+ const p = params;
5820
+ const d = descriptor("null", [
5821
+ ["vibrance", long(clamp2(p.vibrance, -100, 100))],
5822
+ ["Strt", long(clamp2(p.saturation, -100, 100))]
5823
+ ]);
5824
+ return { key: "vibA", data: versionedDescriptorBytes(d) };
5825
+ }
5826
+ case "colorBalance": {
5827
+ const p = params;
5828
+ for (const x of [...p.shadows, ...p.midtones, ...p.highlights]) w.i16(i16(clamp2(x, -100, 100)));
5829
+ w.u8(p.preserveLuminosity ? 1 : 0);
5830
+ w.u8(0);
5831
+ return { key: "blnc", data: w.result() };
5832
+ }
5833
+ case "blackWhite": {
5834
+ const p = params;
5835
+ const tint = p.tint ? hexToRgb(p.tint) : BW_TINT;
5836
+ const dbl = (v) => ({ type: "doub", value: clamp2(v, 0, 1) * 255 });
5837
+ const items = BW_KEYS.map(([field, key]) => [key, long(clamp2(p[field], -200, 300))]);
5838
+ items.push(
5839
+ ["useTint", { type: "bool", value: p.tint !== null }],
5840
+ ["tintColor", { type: "Objc", value: descriptor("RGBC", [["Rd ", dbl(tint[0])], ["Grn ", dbl(tint[1])], ["Bl ", dbl(tint[2])]]) }],
5841
+ ["bwPresetKind", { type: "long", value: 1 }],
5842
+ ["blackAndWhitePresetFileName", { type: "TEXT", value: "" }]
5843
+ );
5844
+ return { key: "blwh", data: versionedDescriptorBytes(descriptor("null", items)) };
5845
+ }
5846
+ case "photoFilter": {
5847
+ const p = params;
5848
+ const rgb = hexToRgb(p.color);
5849
+ w.u16(2);
5850
+ w.u16(0);
5851
+ for (const x of rgb) w.u16(Math.round(clamp2(x, 0, 1) * 65535));
5852
+ w.u16(0);
5853
+ w.u32(Math.round(clamp2(p.density, 0, 100)));
5854
+ w.u8(p.preserveLuminosity ? 1 : 0);
5855
+ w.zeros(3);
5856
+ return { key: "phfl", data: w.result() };
5857
+ }
5858
+ }
5859
+ throw new Error(`Unknown adjustment kind "${kind}".`);
5860
+ }
5861
+
5862
+ // src/image-editor/psd/compression.ts
5863
+ var MAX_DECODED_BYTES = 2 ** 31;
5864
+ function rowBytes(width, depth) {
5865
+ return depth === 1 ? Math.ceil(width / 8) : width * Math.max(1, depth >> 3);
5866
+ }
5867
+ function decodedLength(l) {
5868
+ const total = l.planes * l.height * rowBytes(l.width, l.depth);
5869
+ if (total > MAX_DECODED_BYTES) throw new PsdError(`Decoded channel data of ${total} bytes is larger than the ${MAX_DECODED_BYTES}-byte limit.`);
5870
+ return total;
5871
+ }
5872
+ function packbitsBound(n) {
5873
+ return n + Math.ceil(n / 128);
5874
+ }
5875
+ function packbitsEncode(src, out, at) {
5876
+ const n = src.length;
5877
+ let i = 0;
5878
+ while (i < n) {
5879
+ let run = 1;
5880
+ while (i + run < n && run < 128 && src[i + run] === src[i]) run++;
5881
+ if (run >= 2) {
5882
+ out[at++] = 257 - run & 255;
5883
+ out[at++] = src[i];
5884
+ i += run;
5885
+ continue;
5886
+ }
5887
+ const start = i;
5888
+ let len = 0;
5889
+ while (i < n && len < 128) {
5890
+ if (i + 2 < n && src[i] === src[i + 1] && src[i] === src[i + 2]) break;
5891
+ i++;
5892
+ len++;
5893
+ }
5894
+ out[at++] = len - 1;
5895
+ out.set(src.subarray(start, start + len), at);
5896
+ at += len;
5897
+ }
5898
+ return at;
5899
+ }
5900
+ function packbitsDecodeInto(src, expected, out, at) {
5901
+ const target = at + expected;
5902
+ let i = 0;
5903
+ while (at < target) {
5904
+ if (i >= src.length) throw new PsdError("PackBits row ended early.");
5905
+ const h = src[i++];
5906
+ if (h < 128) {
5907
+ const len = h + 1;
5908
+ if (i + len > src.length) throw new PsdError("PackBits literal is truncated.");
5909
+ if (at + len > target) throw new PsdError("PackBits literal overflows its row.");
5910
+ out.set(src.subarray(i, i + len), at);
5911
+ at += len;
5912
+ i += len;
5913
+ } else if (h !== 128) {
5914
+ const len = 257 - h;
5915
+ if (i >= src.length) throw new PsdError("PackBits run is truncated.");
5916
+ const b = src[i++];
5917
+ if (at + len > target) throw new PsdError("PackBits run overflows its row.");
5918
+ out.fill(b, at, at + len);
5919
+ at += len;
5920
+ }
5921
+ }
5922
+ return at;
5923
+ }
5924
+ function inflateChecked(data, expected) {
5925
+ let out;
5926
+ try {
5927
+ out = inflate(data, { maxOutput: expected + (1 << 20), sizeHint: expected, verify: false });
5928
+ } catch (e) {
5929
+ throw new PsdError(`ZIP channel data: ${e instanceof ZlibError || e instanceof Error ? e.message : String(e)}`);
5930
+ }
5931
+ if (out.length < expected) throw new PsdError(`ZIP stream produced ${out.length} of ${expected} bytes.`);
5932
+ return out.length === expected ? out : out.subarray(0, expected);
5933
+ }
5934
+ function unpredict(buf, l) {
5935
+ const rb = rowBytes(l.width, l.depth);
5936
+ if (rb === 0) return;
5937
+ const w = l.width;
5938
+ const scratch = l.depth === 32 ? new Uint8Array(rb) : null;
5939
+ for (let o = 0; o + rb <= buf.length; o += rb) {
5940
+ if (l.depth === 1 || l.depth === 8) {
5941
+ for (let i = 1; i < rb; i++) buf[o + i] = buf[o + i] + buf[o + i - 1] & 255;
5942
+ } else if (l.depth === 16) {
5943
+ for (let i = 1; i < w; i++) {
5944
+ const a = buf[o + 2 * i] << 8 | buf[o + 2 * i + 1];
5945
+ const b = buf[o + 2 * i - 2] << 8 | buf[o + 2 * i - 1];
5946
+ const v = a + b & 65535;
5947
+ buf[o + 2 * i] = v >> 8;
5948
+ buf[o + 2 * i + 1] = v & 255;
5949
+ }
5950
+ } else if (l.depth === 32) {
5951
+ for (let i = 1; i < rb; i++) buf[o + i] = buf[o + i] + buf[o + i - 1] & 255;
5952
+ scratch.set(buf.subarray(o, o + rb));
5953
+ for (let i = 0; i < w; i++) for (let k = 0; k < 4; k++) buf[o + 4 * i + k] = scratch[k * w + i];
5954
+ } else {
5955
+ throw new PsdError(`ZIP prediction is not defined for depth ${l.depth}.`);
5956
+ }
5957
+ }
5958
+ }
5959
+ function decodeRle(r, l, total) {
5960
+ const rows = l.planes * l.height;
5961
+ const rb = rowBytes(l.width, l.depth);
5962
+ r.checkCount(rows, l.psb ? 4 : 2, "RLE row counts");
5963
+ const counts = new Array(rows);
5964
+ for (let i = 0; i < rows; i++) counts[i] = l.psb ? r.u32() : r.u16();
5965
+ if (total > r.remaining * 130) throw new PsdError("RLE data is too short for its declared size.");
5966
+ const out = new Uint8Array(total);
5967
+ let at = 0;
5968
+ for (const c of counts) at = packbitsDecodeInto(r.bytes(c), rb, out, at);
5969
+ return out;
5970
+ }
5971
+ function decodePlanes(compression, data, l) {
5972
+ const total = decodedLength(l);
5973
+ switch (compression) {
5974
+ case 0:
5975
+ if (data.length < total) throw new PsdError(`Raw channel data has ${data.length} of ${total} bytes.`);
5976
+ return data.slice(0, total);
5977
+ case 1:
5978
+ return decodeRle(new PsdReader(data), l, total);
5979
+ case 2:
5980
+ return inflateChecked(data, total).slice();
5981
+ case 3: {
5982
+ const v = inflateChecked(data, total).slice();
5983
+ unpredict(v, l);
5984
+ return v;
5985
+ }
5986
+ default:
5987
+ throw new PsdError(`Unsupported compression method ${compression}.`);
5988
+ }
5989
+ }
5990
+ function encodeRle(decoded2, l) {
5991
+ const rows = l.planes * l.height;
5992
+ const rb = rowBytes(l.width, l.depth);
5993
+ const cs = l.psb ? 4 : 2;
5994
+ const packed = new Uint8Array(rows * packbitsBound(rb));
5995
+ let at = 0;
5996
+ const counts = new Uint8Array(rows * cs);
5997
+ for (let row = 0; row < rows; row++) {
5998
+ const start = at;
5999
+ at = packbitsEncode(decoded2.subarray(row * rb, (row + 1) * rb), packed, at);
6000
+ const n = at - start;
6001
+ if (cs === 2) {
6002
+ if (n > 65535) throw new PsdError("An RLE row is longer than 65535 bytes; write a PSB instead.");
6003
+ counts[row * 2] = n >> 8;
6004
+ counts[row * 2 + 1] = n & 255;
6005
+ } else {
6006
+ counts[row * 4] = n >>> 24;
6007
+ counts[row * 4 + 1] = n >>> 16 & 255;
6008
+ counts[row * 4 + 2] = n >>> 8 & 255;
6009
+ counts[row * 4 + 3] = n & 255;
6010
+ }
6011
+ }
6012
+ const out = new Uint8Array(counts.length + at);
6013
+ out.set(counts);
6014
+ out.set(packed.subarray(0, at), counts.length);
6015
+ return out;
6016
+ }
6017
+
6018
+ // src/image-editor/psd/file.ts
6019
+ var PSD_MAX_DIMENSION = 3e4;
6020
+ var PSB_MAX_DIMENSION = 3e5;
6021
+ var MAX_CHANNELS = 56;
6022
+ var MAX_LAYER_CHANNELS = 64;
6023
+ var COLOR_MODES = {
6024
+ 0: "bitmap",
6025
+ 1: "grayscale",
6026
+ 2: "indexed",
6027
+ 3: "rgb",
6028
+ 4: "cmyk",
6029
+ 7: "multichannel",
6030
+ 8: "duotone",
6031
+ 9: "lab"
6032
+ };
6033
+ var PSB_LONG_KEYS = /* @__PURE__ */ new Set([
6034
+ "LMsk",
6035
+ "Lr16",
6036
+ "Lr32",
6037
+ "Layr",
6038
+ "Mt16",
6039
+ "Mt32",
6040
+ "Mtrn",
6041
+ "Alph",
6042
+ "FMsk",
6043
+ "lnk2",
6044
+ "FEid",
6045
+ "FXid",
6046
+ "PxSD",
6047
+ "lnk3",
6048
+ "lnkE",
6049
+ "pths",
6050
+ "extd",
6051
+ "extn",
6052
+ "FELS",
6053
+ "cinf",
6054
+ "artd"
6055
+ ]);
6056
+ var CHANNEL_ALPHA = -1;
6057
+ var CHANNEL_USER_MASK = -2;
6058
+ var CHANNEL_REAL_USER_MASK = -3;
6059
+ var LAYER_FLAG_TRANSPARENCY_PROTECTED = 1;
6060
+ var LAYER_FLAG_HIDDEN = 2;
6061
+ var MASK_FLAG_DISABLED = 2;
6062
+ var MASK_FLAG_PARAMETERS = 16;
6063
+ var SECTION_OTHER = 0;
6064
+ var SECTION_OPEN = 1;
6065
+ var SECTION_CLOSED = 2;
6066
+ var SECTION_DIVIDER = 3;
6067
+ function readRect(r) {
6068
+ return { top: r.i32(), left: r.i32(), bottom: r.i32(), right: r.i32() };
6069
+ }
6070
+ function rectSize(rect) {
6071
+ const width = rect.right - rect.left;
6072
+ const height = rect.bottom - rect.top;
6073
+ if (width < 0 || height < 0) throw new PsdError("A layer rectangle has a negative size.");
6074
+ if (width > PSB_MAX_DIMENSION || height > PSB_MAX_DIMENSION) throw new PsdError("A layer is larger than 300000 pixels.");
6075
+ return { width, height };
6076
+ }
6077
+ function readHeader(r) {
6078
+ if (r.ascii(4) !== "8BPS") throw new PsdError('Not a PSD file: the signature is not "8BPS".');
6079
+ const version = r.u16();
6080
+ if (version !== 1 && version !== 2) throw new PsdError(`Unsupported PSD version ${version} (1 is PSD, 2 is PSB).`);
6081
+ r.skip(6);
6082
+ const h = { psb: version === 2, channels: r.u16(), height: r.u32(), width: r.u32(), depth: r.u16(), colorMode: r.u16() };
6083
+ if (h.channels < 1 || h.channels > MAX_CHANNELS) throw new PsdError(`Channel count ${h.channels} is outside 1 to ${MAX_CHANNELS}.`);
6084
+ if (h.width < 1 || h.height < 1) throw new PsdError(`Image size ${h.width} \xD7 ${h.height} must be at least 1 \xD7 1.`);
6085
+ const max = h.psb ? PSB_MAX_DIMENSION : PSD_MAX_DIMENSION;
6086
+ if (h.width > max || h.height > max) throw new PsdError(`Image size ${h.width} \xD7 ${h.height} exceeds ${max} pixels.`);
6087
+ if (![1, 8, 16, 32].includes(h.depth)) throw new PsdError(`Unsupported bit depth ${h.depth}.`);
6088
+ return h;
6089
+ }
6090
+ function readResources(r) {
6091
+ const s = r.sub(r.u32());
6092
+ const out = [];
6093
+ while (s.remaining >= 12) {
6094
+ s.ascii(4);
6095
+ const id = s.u16();
6096
+ const name = decodeLegacyName(s.pascal(2));
6097
+ const len = s.u32();
6098
+ const data = s.bytes(len);
6099
+ if (len % 2 && s.remaining) s.skip(1);
6100
+ out.push({ id, name, data });
6101
+ }
6102
+ return out;
6103
+ }
6104
+ var isSig = (r, at = 0) => {
6105
+ const b = r.data.subarray(r.pos + at, r.pos + at + 4);
6106
+ if (b.length < 4) return false;
6107
+ const s = String.fromCharCode(b[0], b[1], b[2], b[3]);
6108
+ return s === "8BIM" || s === "8B64";
6109
+ };
6110
+ function readBlocks(r, psb, alignment) {
6111
+ const out = [];
6112
+ while (r.remaining >= 12 && isSig(r)) {
6113
+ r.skip(4);
6114
+ const key = r.ascii(4);
6115
+ const len = r.len(psb && PSB_LONG_KEYS.has(key));
6116
+ const data = r.bytes(len);
6117
+ let pad = -1;
6118
+ for (let k = 0; k <= 3; k++) {
6119
+ if (k === r.remaining || k < r.remaining && isSig(r, k)) {
6120
+ pad = k;
6121
+ break;
6122
+ }
6123
+ }
6124
+ if (pad < 0) {
6125
+ const def = (alignment - len % alignment) % alignment;
6126
+ pad = r.peek(def).every((b) => b === 0) && r.remaining >= def ? def : 0;
6127
+ }
6128
+ r.skip(pad);
6129
+ out.push({ key, data });
6130
+ }
6131
+ return out;
6132
+ }
6133
+ function maskParameterBytes(pf) {
6134
+ return (pf & 1 ? 1 : 0) + (pf & 2 ? 8 : 0) + (pf & 4 ? 1 : 0) + (pf & 8 ? 8 : 0);
6135
+ }
6136
+ function realBeforeParameters(d) {
6137
+ const fits = (at) => {
6138
+ if (at >= d.length) return false;
6139
+ const end = at + 1 + maskParameterBytes(d[at]);
6140
+ return d.length >= end && d.length <= end + 3;
6141
+ };
6142
+ if (fits(18)) return false;
6143
+ if (!fits(36)) return false;
6144
+ const known = (pf) => (pf & 240) === 0;
6145
+ const bgOk = (at) => at + 1 < d.length && (d[at + 1] === 0 || d[at + 1] === 255);
6146
+ const photoshop = bgOk(18) && known(d[36]);
6147
+ const realAt = 19 + maskParameterBytes(d[18]);
6148
+ const spec = known(d[18]) && d.length >= realAt + 18 && d.length <= realAt + 21 && bgOk(realAt);
6149
+ return photoshop || !spec;
6150
+ }
6151
+ function parseMask(d) {
6152
+ try {
6153
+ const r = new PsdReader(d);
6154
+ const m = { rect: readRect(r), defaultColor: r.u8(), flags: r.u8() };
6155
+ const readReal = () => r.remaining >= 18 ? { flags: r.u8(), background: r.u8(), rect: readRect(r) } : void 0;
6156
+ const params = (m.flags & MASK_FLAG_PARAMETERS) !== 0;
6157
+ const realFirst = params && realBeforeParameters(d);
6158
+ if (realFirst) m.real = readReal();
6159
+ if (params) {
6160
+ const pf = r.u8();
6161
+ if (pf & 1) m.userDensity = r.u8();
6162
+ if (pf & 2) m.userFeather = r.f64();
6163
+ if (pf & 4) m.vectorDensity = r.u8();
6164
+ if (pf & 8) m.vectorFeather = r.f64();
6165
+ }
6166
+ if (!realFirst) m.real = readReal();
6167
+ return m;
6168
+ } catch {
6169
+ return void 0;
6170
+ }
6171
+ }
6172
+ function readLayerRecord(r, psb) {
6173
+ const rect = readRect(r);
6174
+ const n = r.u16();
6175
+ if (n > MAX_LAYER_CHANNELS) throw new PsdError(`A layer has ${n} channels (at most ${MAX_LAYER_CHANNELS}).`);
6176
+ const channels = [];
6177
+ const lengths = [];
6178
+ for (let i = 0; i < n; i++) {
6179
+ channels.push({ id: r.i16(), compression: null, data: new Uint8Array(0) });
6180
+ lengths.push(r.len(psb));
6181
+ }
6182
+ if (r.ascii(4) !== "8BIM") throw new PsdError('A layer record has no "8BIM" blend-mode signature.');
6183
+ const blendKey = r.ascii(4);
6184
+ const opacity = r.u8();
6185
+ const clipping = r.u8();
6186
+ const flags = r.u8();
6187
+ r.u8();
6188
+ const x = r.sub(r.u32());
6189
+ const maskBytes = x.bytes(x.u32());
6190
+ const mask = maskBytes.length ? parseMask(maskBytes) : void 0;
6191
+ x.skip(x.u32());
6192
+ const legacyName = decodeLegacyName(x.pascal(4));
6193
+ const blocks = readBlocks(x, psb, 2);
6194
+ return { record: { rect, channels, blendKey, opacity, clipping, flags, mask, legacyName, blocks }, lengths };
6195
+ }
6196
+ function readLayerInfo(data, psb) {
6197
+ const r = new PsdReader(data);
6198
+ const count = r.i16();
6199
+ const n = Math.abs(count);
6200
+ r.checkCount(n, 34, "Layer records");
6201
+ const read = [];
6202
+ for (let i = 0; i < n; i++) read.push(readLayerRecord(r, psb));
6203
+ for (const { record, lengths } of read) {
6204
+ record.channels.forEach((ch, i) => {
6205
+ const len = lengths[i];
6206
+ if (len === 0) return;
6207
+ if (len === 1) throw new PsdError("A channel has a data length of 1.");
6208
+ ch.compression = r.u16();
6209
+ ch.data = r.bytes(len - 2);
6210
+ });
6211
+ }
6212
+ return { layers: read.map((x) => x.record), mergedAlpha: count < 0 };
6213
+ }
6214
+ function readPsd(bytes) {
6215
+ const r = new PsdReader(bytes);
6216
+ const header = readHeader(r);
6217
+ const { psb } = header;
6218
+ const colorModeData = r.bytes(r.u32());
6219
+ const resources = readResources(r);
6220
+ const s = r.sub(r.len(psb));
6221
+ let layers = [];
6222
+ let mergedAlpha = false;
6223
+ let globalBlocks = [];
6224
+ if (s.remaining) {
6225
+ const liLen = s.len(psb);
6226
+ if (liLen > 0) ({ layers, mergedAlpha } = readLayerInfo(s.bytes(liLen), psb));
6227
+ if (s.remaining >= 4) {
6228
+ s.skip(s.u32());
6229
+ globalBlocks = readBlocks(s, psb, 4);
6230
+ const key = header.depth === 16 ? "Lr16" : header.depth === 32 ? "Lr32" : null;
6231
+ const block = globalBlocks.find((b) => key ? b.key === key : !layers.length && ["Layr", "Lr16", "Lr32"].includes(b.key));
6232
+ if (block) {
6233
+ ({ layers, mergedAlpha } = readLayerInfo(block.data, psb));
6234
+ globalBlocks = globalBlocks.filter((b) => b !== block);
6235
+ }
6236
+ }
6237
+ }
6238
+ const imageCompression = r.remaining >= 2 ? r.u16() : 0;
6239
+ return { header, colorModeData, resources, layers, mergedAlpha, globalBlocks, imageCompression, imageData: r.rest() };
6240
+ }
6241
+ function layerBlock(rec, key) {
6242
+ return rec.blocks.find((b) => b.key === key)?.data;
6243
+ }
6244
+ function layerName(rec) {
6245
+ const luni = layerBlock(rec, "luni");
6246
+ if (luni) {
6247
+ try {
6248
+ return new PsdReader(luni).unicode();
6249
+ } catch {
6250
+ }
6251
+ }
6252
+ return rec.legacyName;
6253
+ }
6254
+ function sectionDivider(rec) {
6255
+ const d = layerBlock(rec, "lsct") ?? layerBlock(rec, "lsdk");
6256
+ if (!d || d.length < 4) return void 0;
6257
+ const r = new PsdReader(d);
6258
+ const kind = r.u32();
6259
+ if (r.remaining >= 8 && r.ascii(4) === "8BIM") return { kind, blendKey: r.ascii(4) };
6260
+ return { kind };
6261
+ }
6262
+ function u32Block(rec, key) {
6263
+ const d = layerBlock(rec, key);
6264
+ return d && d.length >= 4 ? new PsdReader(d).u32() : void 0;
6265
+ }
6266
+ function u8Block(rec, key) {
6267
+ const d = layerBlock(rec, key);
6268
+ return d && d.length >= 1 ? d[0] : void 0;
6269
+ }
6270
+ function layerTree(file) {
6271
+ const stack = [[]];
6272
+ file.layers.forEach((rec, index) => {
6273
+ const kind = sectionDivider(rec)?.kind ?? SECTION_OTHER;
6274
+ if (kind === SECTION_DIVIDER) {
6275
+ stack.push([]);
6276
+ } else if (kind === SECTION_OPEN || kind === SECTION_CLOSED) {
6277
+ const children = stack.length > 1 ? stack.pop() : [];
6278
+ stack[stack.length - 1].push({ index, children });
6279
+ } else {
6280
+ stack[stack.length - 1].push({ index });
6281
+ }
6282
+ });
6283
+ while (stack.length > 1) {
6284
+ const children = stack.pop();
6285
+ stack[stack.length - 1].push(...children);
6286
+ }
6287
+ return stack[0];
6288
+ }
6289
+ function channelRect(rec, id) {
6290
+ if (id === CHANNEL_USER_MASK && rec.mask) return rec.mask.rect;
6291
+ if (id === CHANNEL_REAL_USER_MASK && rec.mask) return rec.mask.real?.rect ?? rec.mask.rect;
6292
+ return rec.rect;
6293
+ }
6294
+ function decodeLayerChannel(file, rec, id) {
6295
+ const ch = rec.channels.find((c) => c.id === id);
6296
+ if (!ch) return void 0;
6297
+ const { width, height } = rectSize(channelRect(rec, id));
6298
+ if (ch.compression === null) {
6299
+ if (width * height === 0) return new Uint8Array(0);
6300
+ throw new PsdError(`Channel ${id} of layer "${layerName(rec)}" has no data.`);
6301
+ }
6302
+ return decodePlanes(ch.compression, ch.data, { planes: 1, width, height, depth: file.header.depth, psb: file.header.psb });
6303
+ }
6304
+ function decodeMergedImage(file) {
6305
+ const { channels, width, height, depth, psb } = file.header;
6306
+ return decodePlanes(file.imageCompression, file.imageData, { planes: channels, width, height, depth, psb });
6307
+ }
6308
+ function resource(file, id) {
6309
+ return file.resources.find((r) => r.id === id)?.data;
6310
+ }
6311
+ function resolution(file) {
6312
+ const d = resource(file, 1005);
6313
+ if (!d || d.length < 4) return void 0;
6314
+ return new PsdReader(d).u32() / 65536;
6315
+ }
6316
+ function writeRect(w, r) {
6317
+ w.i32(r.top);
6318
+ w.i32(r.left);
6319
+ w.i32(r.bottom);
6320
+ w.i32(r.right);
6321
+ }
6322
+ function writeBlocks(w, blocks, psb, alignment) {
6323
+ for (const b of blocks) {
6324
+ const long2 = psb && PSB_LONG_KEYS.has(b.key);
6325
+ w.ascii(long2 ? "8B64" : "8BIM");
6326
+ w.ascii(b.key);
6327
+ const pad = (alignment - b.data.length % alignment) % alignment;
6328
+ if (long2) w.u64(b.data.length + pad);
6329
+ else w.u32(b.data.length + pad);
6330
+ w.bytes(b.data);
6331
+ w.zeros(pad);
6332
+ }
6333
+ }
6334
+ function writeMask(w, m) {
6335
+ writeRect(w, m.rect);
6336
+ w.u8(m.defaultColor);
6337
+ const pf = (m.userDensity !== void 0 ? 1 : 0) | (m.userFeather !== void 0 ? 2 : 0);
6338
+ w.u8(pf ? m.flags | MASK_FLAG_PARAMETERS : m.flags & ~MASK_FLAG_PARAMETERS);
6339
+ if (pf) {
6340
+ w.u8(pf);
6341
+ if (m.userDensity !== void 0) w.u8(m.userDensity);
6342
+ if (m.userFeather !== void 0) w.f64(m.userFeather);
6343
+ }
6344
+ const len = 18 + (pf ? 1 + maskParameterBytes(pf) : 0);
6345
+ w.zeros(Math.max(0, 20 - len) + (4 - Math.max(20, len) % 4) % 4);
6346
+ }
6347
+ function writeLayerRecord(w, rec, psb, nameBytes) {
6348
+ writeRect(w, rec.rect);
6349
+ w.u16(rec.channels.length);
6350
+ for (const c of rec.channels) {
6351
+ w.i16(c.id);
6352
+ const len = c.compression === null ? 0 : 2 + c.data.length;
6353
+ if (psb) w.u64(len);
6354
+ else w.u32(len);
6355
+ }
6356
+ if (rec.blendKey.length !== 4) throw new PsdError(`Blend key "${rec.blendKey}" must be 4 characters.`);
6357
+ w.ascii("8BIM");
6358
+ w.ascii(rec.blendKey);
6359
+ w.u8(rec.opacity);
6360
+ w.u8(rec.clipping);
6361
+ w.u8(rec.flags);
6362
+ w.u8(0);
6363
+ const extra = w.beginLen(false);
6364
+ const mask = w.beginLen(false);
6365
+ if (rec.mask) writeMask(w, rec.mask);
6366
+ w.endLen(mask);
6367
+ w.u32(40);
6368
+ for (let i = 0; i < 5; i++) w.bytes([0, 0, 255, 255, 0, 0, 255, 255]);
6369
+ w.pascal(nameBytes, 4);
6370
+ writeBlocks(w, rec.blocks, psb, 2);
6371
+ w.endLen(extra);
6372
+ }
6373
+ function writePsd(file, encodeName) {
6374
+ const { header } = file;
6375
+ const { psb } = header;
6376
+ const w = new PsdWriter();
6377
+ w.ascii("8BPS");
6378
+ w.u16(psb ? 2 : 1);
6379
+ w.zeros(6);
6380
+ w.u16(header.channels);
6381
+ w.u32(header.height);
6382
+ w.u32(header.width);
6383
+ w.u16(header.depth);
6384
+ w.u16(header.colorMode);
6385
+ w.u32(file.colorModeData.length);
6386
+ w.bytes(file.colorModeData);
6387
+ const res = w.beginLen(false);
6388
+ for (const r of file.resources) {
6389
+ w.ascii("8BIM");
6390
+ w.u16(r.id);
6391
+ w.pascal(encodeName(r.name), 2);
6392
+ w.u32(r.data.length);
6393
+ w.bytes(r.data);
6394
+ if (r.data.length % 2) w.u8(0);
6395
+ }
6396
+ w.endLen(res);
6397
+ const lm = w.beginLen(psb);
6398
+ if (file.layers.length || file.globalBlocks.length) {
6399
+ const li = w.beginLen(psb);
6400
+ if (file.layers.length) {
6401
+ const start = w.length;
6402
+ w.i16(file.mergedAlpha ? -file.layers.length : file.layers.length);
6403
+ for (const rec of file.layers) writeLayerRecord(w, rec, psb, encodeName(rec.legacyName));
6404
+ for (const rec of file.layers) {
6405
+ for (const c of rec.channels) {
6406
+ if (c.compression === null) continue;
6407
+ w.u16(c.compression);
6408
+ w.bytes(c.data);
6409
+ }
6410
+ }
6411
+ if ((w.length - start) % 2) w.u8(0);
6412
+ }
6413
+ w.endLen(li);
6414
+ w.u32(0);
6415
+ writeBlocks(w, file.globalBlocks, psb, 4);
6416
+ }
6417
+ w.endLen(lm);
6418
+ w.u16(file.imageCompression);
6419
+ w.bytes(file.imageData);
6420
+ return w.result();
6421
+ }
6422
+
6423
+ // src/image-editor/psd/pixels.ts
6424
+ var MODE_INDEXED = 2;
6425
+ var MODE_RGB = 3;
6426
+ var MODE_CMYK = 4;
6427
+ var MODE_MULTICHANNEL = 7;
6428
+ var MODE_LAB = 9;
6429
+ function colorChannelCount(mode, headerChannels) {
6430
+ switch (mode) {
6431
+ case MODE_RGB:
6432
+ case MODE_LAB:
6433
+ return 3;
6434
+ case MODE_CMYK:
6435
+ return 4;
6436
+ case MODE_MULTICHANNEL:
6437
+ return Math.min(3, headerChannels);
6438
+ default:
6439
+ return 1;
6440
+ }
6441
+ }
6442
+ var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
6443
+ function sampler(plane, depth, width, encode, fill) {
6444
+ if (!plane) return () => fill;
6445
+ switch (depth) {
6446
+ case 8:
6447
+ return (i) => plane[i] / 255;
6448
+ case 16:
6449
+ return (i) => (plane[2 * i] << 8 | plane[2 * i + 1]) / 65535;
6450
+ case 32: {
6451
+ const view = new DataView(plane.buffer, plane.byteOffset, plane.byteLength);
6452
+ return (i) => {
6453
+ const v = clamp012(view.getFloat32(4 * i, false) || 0);
6454
+ return encode ? linearToSrgb(v) : v;
6455
+ };
6456
+ }
6457
+ case 1: {
6458
+ const rb = Math.ceil(width / 8);
6459
+ return (i) => {
6460
+ const x = i % width;
6461
+ const y = (i - x) / width;
6462
+ return plane[y * rb + (x >> 3)] >> 7 - (x & 7) & 1 ? 0 : 1;
6463
+ };
6464
+ }
6465
+ default:
6466
+ throw new PsdError(`Unsupported bit depth ${depth}.`);
6467
+ }
6468
+ }
6469
+ function labToSrgb(l, a, b) {
6470
+ const fy = (l + 16) / 116;
6471
+ const f = (t) => t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29);
6472
+ const x = f(fy + a / 500) * D50[0];
6473
+ const y = f(fy) * D50[1];
6474
+ const z = f(fy - b / 200) * D50[2];
6475
+ const m = XYZ_D50_TO_SRGB;
6476
+ return [
6477
+ linearToSrgb(clamp012(m[0][0] * x + m[0][1] * y + m[0][2] * z)),
6478
+ linearToSrgb(clamp012(m[1][0] * x + m[1][1] * y + m[1][2] * z)),
6479
+ linearToSrgb(clamp012(m[2][0] * x + m[2][1] * y + m[2][2] * z))
6480
+ ];
6481
+ }
6482
+ function toRgba8(set) {
6483
+ const { width, height, depth, mode } = set;
6484
+ const n = width * height;
6485
+ const out = new Uint8ClampedArray(n * 4);
6486
+ const linear = depth === 32;
6487
+ const isLab = mode === MODE_LAB;
6488
+ const cmyk = mode === MODE_CMYK;
6489
+ const planes = set.color.map((p) => sampler(p, depth, width, linear && !isLab && !cmyk, cmyk ? 1 : 0));
6490
+ const alpha = sampler(set.alpha, depth, width, false, 1);
6491
+ const fast8 = depth === 8 && mode === MODE_RGB && set.color.every(Boolean);
6492
+ if (fast8) {
6493
+ const [r, g, b] = set.color;
6494
+ const a = set.alpha;
6495
+ for (let i = 0, o = 0; i < n; i++, o += 4) {
6496
+ out[o] = r[i];
6497
+ out[o + 1] = g[i];
6498
+ out[o + 2] = b[i];
6499
+ out[o + 3] = a ? a[i] : 255;
6500
+ }
6501
+ return out;
6502
+ }
6503
+ const palette = set.palette;
6504
+ for (let i = 0, o = 0; i < n; i++, o += 4) {
6505
+ let r;
6506
+ let g;
6507
+ let b;
6508
+ switch (mode) {
6509
+ case MODE_RGB:
6510
+ case MODE_MULTICHANNEL:
6511
+ r = planes[0](i);
6512
+ g = planes[1] ? planes[1](i) : r;
6513
+ b = planes[2] ? planes[2](i) : r;
6514
+ break;
6515
+ case MODE_CMYK: {
6516
+ const k = planes[3] ? planes[3](i) : 1;
6517
+ r = planes[0](i) * k;
6518
+ g = planes[1](i) * k;
6519
+ b = planes[2](i) * k;
6520
+ break;
6521
+ }
6522
+ case MODE_LAB:
6523
+ [r, g, b] = labToSrgb(planes[0](i) * 100, planes[1](i) * 255 - 128, planes[2](i) * 255 - 128);
6524
+ break;
6525
+ case MODE_INDEXED: {
6526
+ const idx = Math.round(planes[0](i) * 255);
6527
+ if (palette && palette.length >= 768) {
6528
+ out[o] = palette[idx];
6529
+ out[o + 1] = palette[256 + idx];
6530
+ out[o + 2] = palette[512 + idx];
6531
+ out[o + 3] = Math.round(alpha(i) * 255);
6532
+ continue;
6533
+ }
6534
+ r = g = b = idx / 255;
6535
+ break;
6536
+ }
6537
+ default:
6538
+ r = g = b = planes[0](i);
6539
+ }
6540
+ out[o] = Math.round(r * 255);
6541
+ out[o + 1] = Math.round(g * 255);
6542
+ out[o + 2] = Math.round(b * 255);
6543
+ out[o + 3] = Math.round(alpha(i) * 255);
6544
+ }
6545
+ return out;
6546
+ }
6547
+ function toMask8(plane, width, height, depth) {
6548
+ if (depth === 8) return new Uint8ClampedArray(plane.buffer, plane.byteOffset, width * height);
6549
+ const s = sampler(plane, depth, width, false, 0);
6550
+ const out = new Uint8ClampedArray(width * height);
6551
+ for (let i = 0; i < out.length; i++) out[i] = Math.round(s(i) * 255);
6552
+ return out;
6553
+ }
6554
+ function rgbaToPlanes(data, n) {
6555
+ const planes = [new Uint8Array(n), new Uint8Array(n), new Uint8Array(n), new Uint8Array(n)];
6556
+ for (let i = 0, o = 0; i < n; i++, o += 4) {
6557
+ planes[0][i] = data[o];
6558
+ planes[1][i] = data[o + 1];
6559
+ planes[2][i] = data[o + 2];
6560
+ planes[3][i] = data[o + 3];
6561
+ }
6562
+ return planes;
6563
+ }
6564
+ function unmatteWhite(data) {
6565
+ for (let i = 0; i < data.length; i += 4) {
6566
+ const a = data[i + 3];
6567
+ if (a === 255 || a === 0) continue;
6568
+ const k = 255 - a;
6569
+ for (let c = 0; c < 3; c++) data[i + c] = Math.round((data[i + c] - k) * 255 / a);
6570
+ }
6571
+ }
6572
+ function matteWhite(data) {
6573
+ const out = Uint8ClampedArray.from(data);
6574
+ for (let i = 0; i < out.length; i += 4) {
6575
+ const a = out[i + 3];
6576
+ if (a === 255) continue;
6577
+ for (let c = 0; c < 3; c++) out[i + c] = Math.round((out[i + c] * a + 255 * (255 - a)) / 255);
6578
+ }
6579
+ return out;
6580
+ }
6581
+
6582
+ // src/image-editor/psd/import.ts
6583
+ var PSD_BLEND_KEYS = {
6584
+ pass: "pass-through",
6585
+ norm: "normal",
6586
+ dark: "darken",
6587
+ "mul ": "multiply",
6588
+ idiv: "color-burn",
6589
+ lbrn: "linear-burn",
6590
+ dkCl: "darker-color",
6591
+ lite: "lighten",
6592
+ scrn: "screen",
6593
+ "div ": "color-dodge",
6594
+ lddg: "linear-dodge",
6595
+ lgCl: "lighter-color",
6596
+ over: "overlay",
6597
+ sLit: "soft-light",
6598
+ hLit: "hard-light",
6599
+ vLit: "vivid-light",
6600
+ lLit: "linear-light",
6601
+ pLit: "pin-light",
6602
+ hMix: "hard-mix",
6603
+ diff: "difference",
6604
+ smud: "exclusion",
6605
+ fsub: "subtract",
6606
+ fdiv: "divide",
6607
+ "hue ": "hue",
6608
+ "sat ": "saturation",
6609
+ colr: "color",
6610
+ "lum ": "luminosity"
6611
+ };
6612
+ var BLEND_TO_PSD = Object.fromEntries(
6613
+ Object.entries(PSD_BLEND_KEYS).map(([k, v]) => [v, k])
6614
+ );
6615
+ var FILL_KEYS = ["SoCo", "GdFl", "PtFl"];
6616
+ var TYPE_KEYS = ["TySh", "tySh"];
6617
+ var SMART_KEYS = ["SoLd", "SoLE", "PlLd"];
6618
+ var VECTOR_KEYS = ["vmsk", "vsms"];
6619
+ var EFFECT_KEYS = ["lfx2", "lfxs", "lmfx"];
6620
+ var percent = (v) => Math.round(v / 255 * 100) / 100;
6621
+ var Warnings = class {
6622
+ groups = /* @__PURE__ */ new Map();
6623
+ extra = [];
6624
+ add(message, layer) {
6625
+ const list = this.groups.get(message) ?? [];
6626
+ if (!list.includes(layer)) list.push(layer);
6627
+ this.groups.set(message, list);
6628
+ }
6629
+ list() {
6630
+ const out = [...this.extra];
6631
+ for (const [message, layers] of this.groups) {
6632
+ const names = layers.map((n) => `"${n}"`);
6633
+ const shown = names.length > 6 ? `${names.slice(0, 6).join(", ")} and ${names.length - 6} more` : names.join(", ");
6634
+ out.push(`${message} (${layers.length === 1 ? "layer" : "layers"} ${shown}).`);
6635
+ }
6636
+ return out;
6637
+ }
6638
+ };
6639
+ function psdKind(rec) {
6640
+ if (rec.blocks.some((b) => TYPE_KEYS.includes(b.key))) return "type";
6641
+ if (rec.blocks.some((b) => SMART_KEYS.includes(b.key))) return "smart-object";
6642
+ const fill = rec.blocks.some((b) => FILL_KEYS.includes(b.key));
6643
+ const vector = rec.blocks.some((b) => VECTOR_KEYS.includes(b.key) || b.key === "vstk");
6644
+ if (fill && vector) return "shape";
6645
+ if (fill) return "fill";
6646
+ const adj = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));
6647
+ if (adj) return adjustmentLabel(adj.key);
6648
+ return "pixel";
6649
+ }
6650
+ function locksOf(rec) {
6651
+ const v = u32Block(rec, "lspf");
6652
+ if (!v) return void 0;
6653
+ const locks = {};
6654
+ if (v & 2147483648) locks.all = true;
6655
+ if (v & 1) locks.transparency = true;
6656
+ if (v & 2) locks.pixels = true;
6657
+ if (v & 4) locks.position = true;
6658
+ return Object.keys(locks).length ? locks : void 0;
6659
+ }
6660
+ function decodeOr(ctx, rec, id, name) {
6661
+ try {
6662
+ return decodeLayerChannel(ctx.file, rec, id);
6663
+ } catch (e) {
6664
+ ctx.warn.add(`A channel could not be decoded (${e instanceof Error ? e.message : String(e)}) and the layer is treated as empty`, name);
6665
+ return void 0;
6666
+ }
6667
+ }
6668
+ function safeSize(ctx, rect, name) {
6669
+ try {
6670
+ return rectSize(rect);
6671
+ } catch (e) {
6672
+ ctx.warn.add(`A malformed rectangle (${e instanceof Error ? e.message : String(e)}) left the layer or its mask empty`, name);
6673
+ return { width: 0, height: 0 };
6674
+ }
6675
+ }
6676
+ function recordPixels(ctx, rec, name) {
6677
+ const { width, height } = safeSize(ctx, rec.rect, name);
6678
+ if (!width || !height || ctx.skipPixels) return emptySurface(4);
6679
+ const ids = [...Array.from({ length: ctx.colorCount }, (_, i) => i), CHANNEL_ALPHA];
6680
+ const planes = ids.map((id) => rec.channels.some((c) => c.id === id) ? decodeOr(ctx, rec, id, name) ?? null : void 0);
6681
+ if (planes.some((p) => p === null)) return emptySurface(4);
6682
+ if (planes.every((p) => p === void 0)) return emptySurface(4);
6683
+ const data = toRgba8({
6684
+ width,
6685
+ height,
6686
+ depth: ctx.file.header.depth,
6687
+ mode: ctx.file.header.colorMode,
6688
+ color: planes.slice(0, ctx.colorCount),
6689
+ alpha: planes[ctx.colorCount],
6690
+ palette: ctx.palette
6691
+ });
6692
+ return surfaceFromPixels({ width, height, data }, 4, rec.rect.left, rec.rect.top);
6693
+ }
6694
+ function recordMask(ctx, rec, name) {
6695
+ const m = rec.mask;
6696
+ if (!m) return void 0;
6697
+ const useReal = rec.channels.some((c) => c.id === CHANNEL_REAL_USER_MASK) && m.real;
6698
+ const id = useReal ? CHANNEL_REAL_USER_MASK : CHANNEL_USER_MASK;
6699
+ if (!rec.channels.some((c) => c.id === id)) return void 0;
6700
+ const rect = useReal ? m.real.rect : m.rect;
6701
+ const def = (useReal ? m.real.background : m.defaultColor) >= 128 ? 255 : 0;
6702
+ const flags = useReal ? m.real.flags : m.flags;
6703
+ let surface = emptySurface(1, def);
6704
+ const { width, height } = safeSize(ctx, rect, name);
6705
+ if (width && height && !ctx.skipPixels) {
6706
+ const plane = decodeOr(ctx, rec, id, name);
6707
+ if (plane) surface = surfaceFromPixels({ width, height, data: toMask8(plane, width, height, ctx.file.header.depth) }, 1, rect.left, rect.top, def);
6708
+ }
6709
+ const mask = { surface, defaultValue: def };
6710
+ if (m.userDensity !== void 0 && m.userDensity < 255) mask.density = percent(m.userDensity);
6711
+ if (m.userFeather !== void 0 && m.userFeather > 0) mask.feather = Math.min(1e3, m.userFeather);
6712
+ if (flags & MASK_FLAG_DISABLED) mask.enabled = false;
6713
+ return mask;
6714
+ }
6715
+ function baseProps(ctx, rec, name, notes) {
6716
+ let blend = PSD_BLEND_KEYS[rec.blendKey];
6717
+ if (rec.blendKey === "diss") {
6718
+ ctx.warn.add("Dissolve is not supported and became Normal", name);
6719
+ notes.push("Dissolve became Normal");
6720
+ } else if (!blend) {
6721
+ ctx.warn.add(`An unknown blend mode became Normal`, name);
6722
+ notes.push(`unknown blend mode "${rec.blendKey}"`);
6723
+ }
6724
+ if (!blend || blend === "pass-through") blend = "normal";
6725
+ const props = {
6726
+ name,
6727
+ visible: (rec.flags & LAYER_FLAG_HIDDEN) === 0,
6728
+ opacity: percent(rec.opacity),
6729
+ blend
6730
+ };
6731
+ if (rec.clipping) props.clipped = true;
6732
+ const locks = locksOf(rec);
6733
+ if (locks) props.locks = locks;
6734
+ if (rec.blocks.some((b) => EFFECT_KEYS.includes(b.key))) {
6735
+ ctx.warn.add("Layer effects (lfx2) are not rendered", name);
6736
+ notes.push("layer effects are not rendered");
6737
+ }
6738
+ if (rec.blocks.some((b) => b.key === "artb" || b.key === "artd" || b.key === "abdd")) {
6739
+ ctx.warn.add("Artboards are not supported; they came in as plain groups, without their background colour and clipping", name);
6740
+ notes.push("artboard imported as a group");
6741
+ }
6742
+ if (rec.blocks.some((b) => VECTOR_KEYS.includes(b.key)) && psdKind(rec) === "pixel") {
6743
+ ctx.warn.add("Vector masks are not supported and were ignored", name);
6744
+ notes.push("vector mask ignored");
6745
+ }
6746
+ return props;
6747
+ }
6748
+ function importNode(ctx, node, depth) {
6749
+ const rec = ctx.file.layers[node.index];
6750
+ const name = layerName(rec);
6751
+ const notes = [];
6752
+ const kind = psdKind(rec);
6753
+ const entry = { name, type: null, status: "mapped", psdKind: node.children ? "group" : kind, notes, depth };
6754
+ ctx.summary.push(entry);
6755
+ if (node.children) {
6756
+ const props2 = baseProps(ctx, rec, name, notes);
6757
+ const divider = sectionDivider(rec);
6758
+ const key = divider?.blendKey ?? rec.blendKey;
6759
+ const groupBlend = key === "pass" ? "pass-through" : PSD_BLEND_KEYS[key] ?? "normal";
6760
+ const children = [];
6761
+ for (const child of node.children) {
6762
+ const layer2 = importNode(ctx, child, depth + 1);
6763
+ if (layer2) children.push(layer2);
6764
+ }
6765
+ const group = { type: "group", id: ctx.newId(name), ...props2, blend: groupBlend, children };
6766
+ if (divider?.kind === SECTION_CLOSED) group.collapsed = true;
6767
+ const mask2 = recordMask(ctx, rec, name);
6768
+ if (mask2) group.mask = mask2;
6769
+ entry.type = "group";
6770
+ entry.id = group.id;
6771
+ entry.status = notes.length ? "approximated" : "mapped";
6772
+ return group;
6773
+ }
6774
+ const adjBlock = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));
6775
+ if (adjBlock) {
6776
+ const parsed = parseAdjustmentBlock(adjBlock.key, adjBlock.data);
6777
+ if (!parsed) {
6778
+ const label = adjustmentLabel(adjBlock.key);
6779
+ ctx.warn.add(`${label} adjustment layers are not supported and were left out`, name);
6780
+ notes.push(`${label} is not supported`);
6781
+ entry.status = "dropped";
6782
+ return null;
6783
+ }
6784
+ const props2 = baseProps(ctx, rec, name, notes);
6785
+ if (parsed.approximated) {
6786
+ ctx.warn.add(`A ${adjustmentLabel(adjBlock.key)} layer was approximated: ${parsed.approximated}`, name);
6787
+ notes.push(parsed.approximated);
6788
+ }
6789
+ const layer2 = { type: "adjustment", id: ctx.newId(name), ...props2, kind: parsed.kind, params: parsed.params };
6790
+ const mask2 = recordMask(ctx, rec, name);
6791
+ if (mask2) layer2.mask = mask2;
6792
+ entry.type = "adjustment";
6793
+ entry.id = layer2.id;
6794
+ entry.status = notes.length ? "approximated" : "mapped";
6795
+ return layer2;
6796
+ }
6797
+ const props = baseProps(ctx, rec, name, notes);
6798
+ if (kind !== "pixel") {
6799
+ const what = kind === "smart-object" ? "Smart objects" : kind === "type" ? "Type layers" : kind === "shape" ? "Shape layers" : "Fill layers";
6800
+ ctx.warn.add(`${what} came in as pixel layers (their stored pixels)`, name);
6801
+ notes.push(`${kind} layer imported as pixels`);
6802
+ }
6803
+ const layer = { type: "pixel", id: ctx.newId(name), ...props, surface: recordPixels(ctx, rec, name), offset: { x: 0, y: 0 } };
6804
+ const fill = u8Block(rec, "iOpa");
6805
+ if (fill !== void 0 && fill < 255) layer.fillOpacity = percent(fill);
6806
+ const mask = recordMask(ctx, rec, name);
6807
+ if (mask) layer.mask = mask;
6808
+ entry.type = "pixel";
6809
+ entry.id = layer.id;
6810
+ entry.status = notes.length ? "approximated" : "mapped";
6811
+ return layer;
6812
+ }
6813
+ function mergedLayer(ctx) {
6814
+ const { header } = ctx.file;
6815
+ const { width, height } = header;
6816
+ let planes;
6817
+ try {
6818
+ planes = decodeMergedImage(ctx.file);
6819
+ } catch (e) {
6820
+ ctx.warn.extra.push(`The merged image could not be decoded (${e instanceof Error ? e.message : String(e)}).`);
6821
+ return null;
6822
+ }
6823
+ const size = rowBytes(width, header.depth) * height;
6824
+ const plane = (i) => i < header.channels ? planes.subarray(i * size, (i + 1) * size) : void 0;
6825
+ const cc = ctx.colorCount;
6826
+ const hasAlpha = header.channels > cc && (ctx.file.layers.length === 0 || ctx.file.mergedAlpha);
6827
+ const data = toRgba8({
6828
+ width,
6829
+ height,
6830
+ depth: header.depth,
6831
+ mode: header.colorMode,
6832
+ color: Array.from({ length: cc }, (_, i) => plane(i)),
6833
+ alpha: hasAlpha ? plane(cc) : void 0,
6834
+ palette: ctx.palette
6835
+ });
6836
+ if (hasAlpha) unmatteWhite(data);
6837
+ return {
6838
+ type: "pixel",
6839
+ id: ctx.newId("Background"),
6840
+ name: "Background",
6841
+ visible: true,
6842
+ opacity: 1,
6843
+ blend: "normal",
6844
+ surface: surfaceFromPixels({ width, height, data }, 4),
6845
+ offset: { x: 0, y: 0 }
6846
+ };
6847
+ }
6848
+ function psdToLayers(file, options = {}) {
6849
+ const { header } = file;
6850
+ const warn = new Warnings();
6851
+ let n = 0;
6852
+ const ctx = {
6853
+ file,
6854
+ warn,
6855
+ newId: options.newId ?? (() => `psd-${++n}`),
6856
+ skipPixels: options.skipPixels ?? false,
6857
+ summary: [],
6858
+ colorCount: colorChannelCount(header.colorMode, header.channels),
6859
+ palette: header.colorMode === MODE_INDEXED ? file.colorModeData : void 0
6860
+ };
6861
+ const mode = COLOR_MODES[header.colorMode] ?? `mode ${header.colorMode}`;
6862
+ if (mode !== "rgb" || header.depth !== 8) {
6863
+ warn.extra.push(`The file is ${header.depth}-bit ${mode}; it was converted to 8-bit RGB.`);
6864
+ }
6865
+ if (resource(file, 1039)) warn.extra.push("The embedded ICC profile is ignored; colours are read as sRGB.");
6866
+ let layers = [];
6867
+ if (file.layers.length) {
6868
+ const tree = layerTree(file);
6869
+ const built = [];
6870
+ for (const node of tree) {
6871
+ const layer = importNode(ctx, node, 0);
6872
+ if (layer) built.push(layer);
6873
+ }
6874
+ layers = built;
6875
+ if (!layers.length && !ctx.skipPixels) {
6876
+ const bg = mergedLayer(ctx);
6877
+ if (bg) {
6878
+ layers = [bg];
6879
+ warn.extra.push("No layer could be imported, so the merged image was opened as the Background.");
6880
+ }
6881
+ }
6882
+ } else if (!ctx.skipPixels) {
6883
+ const bg = mergedLayer(ctx);
6884
+ if (bg) layers = [bg];
6885
+ }
6886
+ return {
6887
+ width: header.width,
6888
+ height: header.height,
6889
+ layers,
6890
+ warnings: warn.list(),
6891
+ summary: ctx.summary,
6892
+ resolution: resolution(file),
6893
+ info: { psb: header.psb, depth: header.depth, colorMode: mode, channels: header.channels }
6894
+ };
6895
+ }
6896
+ function importPsd(bytes, options = {}) {
6897
+ if (bytes.length < 26) throw new PsdError("Not a PSD file: it is shorter than a PSD header.");
6898
+ return psdToLayers(readPsd(bytes), options);
6899
+ }
6900
+
6901
+ // src/image-editor/psd/export.ts
6902
+ var FLAG_PIXELS_IRRELEVANT = 8 | 16;
6903
+ var EMPTY_RECT = { top: 0, left: 0, bottom: 0, right: 0 };
6904
+ function unicodeBlock(s) {
6905
+ const w = new PsdWriter();
6906
+ w.unicode(s);
6907
+ return w.result();
6908
+ }
6909
+ function u32Bytes(v) {
6910
+ const w = new PsdWriter();
6911
+ w.u32(v);
6912
+ return w.result();
6913
+ }
6914
+ function sectionBlock(kind, blendKey) {
6915
+ const w = new PsdWriter();
6916
+ w.u32(kind);
6917
+ if (blendKey) {
6918
+ w.ascii("8BIM");
6919
+ w.ascii(blendKey);
6920
+ }
6921
+ return w.result();
6922
+ }
6923
+ var toRect = (r) => ({ top: r.y, left: r.x, bottom: r.y + r.height, right: r.x + r.width });
6924
+ function rleChannel(id, plane, width, height, psb) {
6925
+ return { id, compression: 1, data: encodeRle(plane, { planes: 1, width, height, depth: 8, psb }) };
6926
+ }
6927
+ var emptyChannel = (id) => ({ id, compression: 0, data: new Uint8Array(0) });
6928
+ function maskChannel(mask, ox, oy, psb) {
6929
+ if (mask.source) throw new PsdError("A parametric mask must be baked before export.");
6930
+ const bounds = contentBounds(mask.surface);
6931
+ const def = mask.defaultValue;
6932
+ const m = { rect: EMPTY_RECT, defaultColor: def, flags: mask.enabled === false ? MASK_FLAG_DISABLED : 0 };
6933
+ if ((mask.density ?? 1) < 1) m.userDensity = Math.round(Math.max(0, mask.density ?? 1) * 255);
6934
+ if ((mask.feather ?? 0) > 0) m.userFeather = mask.feather;
6935
+ if (!bounds) return { channel: emptyChannel(CHANNEL_USER_MASK), mask: m };
6936
+ const plane = readRegion(mask.surface, bounds);
6937
+ m.rect = toRect({ ...bounds, x: bounds.x + ox, y: bounds.y + oy });
6938
+ return { channel: rleChannel(CHANNEL_USER_MASK, new Uint8Array(plane.buffer, plane.byteOffset, plane.length), bounds.width, bounds.height, psb), mask: m };
6939
+ }
6940
+ function lockBits(layer) {
6941
+ const l = layer.locks;
6942
+ if (!l) return 0;
6943
+ return (l.all ? 2147483648 : 0) | (l.transparency ? 1 : 0) | (l.pixels ? 2 : 0) | (l.position ? 4 : 0);
6944
+ }
6945
+ function exportPsd(input) {
6946
+ const { width, height } = input;
6947
+ if (width < 1 || height < 1) throw new PsdError("The document is empty.");
6948
+ const psb = width > PSD_MAX_DIMENSION || height > PSD_MAX_DIMENSION;
6949
+ const records = [];
6950
+ let nextId = 0;
6951
+ const common = (layer, extra = []) => {
6952
+ const name = layer.name || (layer.type === "adjustment" ? adjustmentKindLabel(layer.kind) : "");
6953
+ const blocks = [{ key: "luni", data: unicodeBlock(name) }, { key: "lyid", data: u32Bytes(++nextId) }, ...extra];
6954
+ const locks = lockBits(layer);
6955
+ if (locks) blocks.push({ key: "lspf", data: u32Bytes(locks >>> 0) });
6956
+ return {
6957
+ opacity: Math.round(Math.min(1, Math.max(0, layer.opacity)) * 255),
6958
+ clipping: layer.clipped ? 1 : 0,
6959
+ flags: (layer.visible ? 0 : LAYER_FLAG_HIDDEN) | (layer.locks?.transparency ? LAYER_FLAG_TRANSPARENCY_PROTECTED : 0),
6960
+ legacyName: name,
6961
+ blocks
6962
+ };
6963
+ };
6964
+ const write = (layer) => {
6965
+ switch (layer.type) {
6966
+ case "group": {
6967
+ records.push({
6968
+ rect: EMPTY_RECT,
6969
+ channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),
6970
+ blendKey: "norm",
6971
+ opacity: 255,
6972
+ clipping: 0,
6973
+ flags: FLAG_PIXELS_IRRELEVANT | (layer.visible ? 0 : LAYER_FLAG_HIDDEN),
6974
+ legacyName: "</Layer group>",
6975
+ blocks: [
6976
+ { key: "luni", data: unicodeBlock("</Layer group>") },
6977
+ { key: "lsct", data: sectionBlock(SECTION_DIVIDER) }
6978
+ ]
6979
+ });
6980
+ for (const child of layer.children) write(child);
6981
+ const blendKey = BLEND_TO_PSD[layer.blend] ?? "norm";
6982
+ const base = common(layer, [{ key: "lsct", data: sectionBlock(layer.collapsed ? SECTION_CLOSED : SECTION_OPEN, blendKey) }]);
6983
+ const rec = {
6984
+ ...base,
6985
+ rect: EMPTY_RECT,
6986
+ channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),
6987
+ blendKey,
6988
+ flags: base.flags | FLAG_PIXELS_IRRELEVANT
6989
+ };
6990
+ if (layer.mask) {
6991
+ const m = maskChannel(layer.mask, 0, 0, psb);
6992
+ rec.channels.push(m.channel);
6993
+ rec.mask = m.mask;
6994
+ }
6995
+ records.push(rec);
6996
+ return;
6997
+ }
6998
+ case "adjustment": {
6999
+ const block = writeAdjustmentBlock(layer.kind, layer.params);
7000
+ const rec = {
7001
+ ...common(layer, [{ key: block.key, data: block.data }]),
7002
+ rect: EMPTY_RECT,
7003
+ channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),
7004
+ blendKey: BLEND_TO_PSD[layer.blend] ?? "norm"
7005
+ };
7006
+ if (layer.mask) {
7007
+ const m = maskChannel(layer.mask, 0, 0, psb);
7008
+ rec.channels.push(m.channel);
7009
+ rec.mask = m.mask;
7010
+ }
7011
+ records.push(rec);
7012
+ return;
7013
+ }
7014
+ case "pixel": {
7015
+ if (layer.source === "document") throw new PsdError(`Layer "${layer.name}" reads the document image; resolve its pixels before export.`);
7016
+ if (layer.filters?.some(isActiveFilter)) throw new PsdError(`Layer "${layer.name}" has live filters; bake them before export.`);
7017
+ const extra = [];
7018
+ if (layer.fillOpacity !== void 0 && layer.fillOpacity < 1) {
7019
+ extra.push({ key: "iOpa", data: Uint8Array.from([Math.round(Math.max(0, layer.fillOpacity) * 255), 0, 0, 0]) });
7020
+ }
7021
+ const rec = { ...common(layer, extra), rect: EMPTY_RECT, channels: [], blendKey: BLEND_TO_PSD[layer.blend] ?? "norm" };
7022
+ const bounds = contentBounds(layer.surface);
7023
+ if (bounds) {
7024
+ const n2 = bounds.width * bounds.height;
7025
+ const [r2, g2, b2, a2] = rgbaToPlanes(readRegion(layer.surface, bounds), n2);
7026
+ rec.rect = toRect({ ...bounds, x: bounds.x + layer.offset.x, y: bounds.y + layer.offset.y });
7027
+ rec.channels = [
7028
+ rleChannel(CHANNEL_ALPHA, a2, bounds.width, bounds.height, psb),
7029
+ rleChannel(0, r2, bounds.width, bounds.height, psb),
7030
+ rleChannel(1, g2, bounds.width, bounds.height, psb),
7031
+ rleChannel(2, b2, bounds.width, bounds.height, psb)
7032
+ ];
7033
+ } else {
7034
+ rec.channels = [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel);
7035
+ }
7036
+ if (layer.mask) {
7037
+ const m = maskChannel(layer.mask, layer.offset.x, layer.offset.y, psb);
7038
+ rec.channels.push(m.channel);
7039
+ rec.mask = m.mask;
7040
+ }
7041
+ records.push(rec);
7042
+ return;
7043
+ }
7044
+ case "annotation":
7045
+ throw new PsdError(`Annotation layer "${layer.name}" must be rasterized before export.`);
7046
+ }
7047
+ };
7048
+ for (const layer of input.layers) write(layer);
7049
+ if (records.length > 32767) throw new PsdError("A PSD holds at most 32767 layer records.");
7050
+ const n = width * height;
7051
+ if (input.merged.length < n * 4) throw new PsdError(`The merged image has ${input.merged.length} bytes, expected ${n * 4}.`);
7052
+ const [r, g, b, a] = rgbaToPlanes(matteWhite(input.merged), n);
7053
+ const transparent = a.some((v) => v !== 255);
7054
+ const planes = transparent ? [r, g, b, a] : [r, g, b];
7055
+ const merged = new Uint8Array(n * planes.length);
7056
+ planes.forEach((p, i) => merged.set(p, i * n));
7057
+ const res = Math.max(1, input.resolution ?? 72);
7058
+ const rw = new PsdWriter();
7059
+ for (let k = 0; k < 2; k++) {
7060
+ rw.u32(Math.round(res * 65536));
7061
+ rw.u16(1);
7062
+ rw.u16(1);
7063
+ }
7064
+ const version = new PsdWriter();
7065
+ version.u32(1);
7066
+ version.u8(1);
7067
+ version.unicode("Pixerate Editor");
7068
+ version.unicode("Pixerate Editor");
7069
+ version.u32(1);
7070
+ const resources = [
7071
+ { id: 1005, name: "", data: rw.result() },
7072
+ { id: 1057, name: "", data: version.result() }
7073
+ ];
7074
+ const bytes = writePsd(
7075
+ {
7076
+ header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },
7077
+ colorModeData: new Uint8Array(0),
7078
+ resources,
7079
+ layers: records,
7080
+ mergedAlpha: transparent && records.length > 0,
7081
+ globalBlocks: [],
7082
+ imageCompression: 1,
7083
+ imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb })
7084
+ },
7085
+ encodeLegacyName
7086
+ );
7087
+ return { bytes, psb, records: records.length };
7088
+ }
7089
+
4979
7090
  // src/image-editor/controller.ts
4980
7091
  var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
4981
7092
  function paintCoverageInto(base, coverage, apply, rects) {
@@ -6137,8 +8248,8 @@ var ImageEditorController = class {
6137
8248
  const cur = this.state.paint;
6138
8249
  const next = {
6139
8250
  brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,
6140
- foreground: options.foreground !== void 0 ? toHex(parseColor(options.foreground)) : cur.foreground,
6141
- background: options.background !== void 0 ? toHex(parseColor(options.background)) : cur.background
8251
+ foreground: options.foreground !== void 0 ? toHex2(parseColor(options.foreground)) : cur.foreground,
8252
+ background: options.background !== void 0 ? toHex2(parseColor(options.background)) : cur.background
6142
8253
  };
6143
8254
  this.state.paint = next;
6144
8255
  this.notify();
@@ -6652,9 +8763,18 @@ var ImageEditorController = class {
6652
8763
  if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;
6653
8764
  return null;
6654
8765
  }
6655
- if (!options.sampleMerged) return base;
8766
+ const legacy = legacyView(this.stack(), width, height, null).exact;
8767
+ if (options.sample === "active-layer") {
8768
+ const layer = this.getActiveLayer();
8769
+ if (!layer || layer.type !== "pixel") {
8770
+ throw new LayerOperationError("Sampling the active layer needs an active pixel layer; select one, or sample the composite.");
8771
+ }
8772
+ const surface = layer.source === "document" && options.pixels ? surfaceFromPixels(options.pixels, 4) : this.surfaceOf(layer);
8773
+ return { width, height, data: readRegion(surface, { x: -layer.offset.x, y: -layer.offset.y, width, height }) };
8774
+ }
8775
+ if (legacy && !options.sampleMerged) return base;
6656
8776
  this.syncViews();
6657
- if (legacyView(this.stack(), width, height, null).exact) {
8777
+ if (legacy && !options.pixels) {
6658
8778
  const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data) };
6659
8779
  applyAdjustments(copy, legacyView(this.stack(), width, height, null).adjustments);
6660
8780
  return copy;
@@ -6666,7 +8786,9 @@ var ImageEditorController = class {
6666
8786
  surfaceOf: (l) => l.source === "document" && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l),
6667
8787
  version: pixels ? "pixels" : this.sourceVersion()
6668
8788
  };
6669
- const data = new LayerCompositor().composite(withoutAnnotations(this.state.layers), { x: 0, y: 0, width, height }, sources);
8789
+ const stack = withoutAnnotations(this.state.layers);
8790
+ const layers = options.sampleMerged ? stack : pixelsOnly(stack);
8791
+ const data = new LayerCompositor().composite(layers, { x: 0, y: 0, width, height }, sources);
6670
8792
  return { width, height, data };
6671
8793
  }
6672
8794
  async requirePixels(options) {
@@ -6914,8 +9036,24 @@ var ImageEditorController = class {
6914
9036
  this.notify();
6915
9037
  }
6916
9038
  // --- Rendering & Export ---
6917
- async render(targetCanvas, options) {
9039
+ blankSource = null;
9040
+ /**
9041
+ * What the renderer draws from: the source image, or, for a document without one (an opened
9042
+ * PSD, a v3 document loaded on its own), a transparent canvas of the document size, since every
9043
+ * pixel then comes from the layers. `null` when there is nothing to draw.
9044
+ */
9045
+ drawSource() {
6918
9046
  const source = this.imageElement || this.state.sourceUrl;
9047
+ if (source) return source;
9048
+ if (!this.isSourceless()) return null;
9049
+ const { width, height } = this.naturalSize;
9050
+ if (!this.blankSource || this.blankSource.width !== width || this.blankSource.height !== height) {
9051
+ this.blankSource = this.renderer.createCanvas(width, height);
9052
+ }
9053
+ return this.blankSource;
9054
+ }
9055
+ async render(targetCanvas, options) {
9056
+ const source = this.drawSource();
6919
9057
  if (!source) return;
6920
9058
  this.syncViews();
6921
9059
  const dirty = this.pendingDirty;
@@ -6930,7 +9068,7 @@ var ImageEditorController = class {
6930
9068
  }
6931
9069
  }
6932
9070
  async renderExport(options) {
6933
- const source = this.imageElement || this.state.sourceUrl;
9071
+ const source = this.drawSource();
6934
9072
  if (!source) {
6935
9073
  return this.renderer.createCanvas(1, 1);
6936
9074
  }
@@ -6949,7 +9087,7 @@ var ImageEditorController = class {
6949
9087
  * Returns `null` when no image is loaded.
6950
9088
  */
6951
9089
  async renderSourceView(options = {}) {
6952
- const source = this.imageElement || this.state.sourceUrl;
9090
+ const source = this.drawSource();
6953
9091
  if (!source) return null;
6954
9092
  this.syncViews();
6955
9093
  const { width, height } = this.state.imageDimensions;
@@ -7021,23 +9159,270 @@ var ImageEditorController = class {
7021
9159
  ctx.drawImage(img, 0, 0, width, height);
7022
9160
  return canvas;
7023
9161
  }
9162
+ /**
9163
+ * The image an AI hook should edit: the source (`toSourceDataURL`) when results replace the
9164
+ * Background, or the composite of the layers without annotations, in the source frame, when
9165
+ * they go to a new layer or mask (see `applyInpaintedImage`).
9166
+ */
9167
+ async toAIInputDataURL(options = {}) {
9168
+ const target = options.target ?? this.defaultAITarget();
9169
+ if (target === "replace") return this.toSourceDataURL();
9170
+ const canvas = await this.renderSourceView({ includeAnnotations: false });
9171
+ return canvas ? canvas.toDataURL("image/png") : this.toSourceDataURL();
9172
+ }
7024
9173
  async toSourceDataURL(options = {}) {
7025
9174
  const canvas = await this.renderSource();
7026
9175
  return canvas.toDataURL(options.format || "image/png", options.quality ?? 0.92);
7027
9176
  }
7028
9177
  /**
7029
- * Replaces the source image with an inpainted result (natural image space, see
7030
- * `toSourceDataURL`) and clears the mask annotations, as ONE undoable step.
9178
+ * Whether AI results go to layers: the document has more than one pixel layer or group (adjustment
9179
+ * and annotation layers are edits of the image, not images of their own).
9180
+ */
9181
+ /** `new-layer` in multi-layer documents and documents without a source image, else `replace`. */
9182
+ defaultAITarget() {
9183
+ return this.isMultiLayer || this.isSourceless() ? "new-layer" : "replace";
9184
+ }
9185
+ /** No source image, and no layer reading one (an opened PSD): the layers are the whole document. */
9186
+ isSourceless() {
9187
+ if (this.imageElement || this.state.sourceUrl || !this.state.layers.length) return false;
9188
+ return !flattenLayers(this.state.layers).some((l) => l.type === "pixel" && l.source === "document");
9189
+ }
9190
+ get isMultiLayer() {
9191
+ return flattenLayers(this.state.layers).filter((l) => l.type === "pixel" || l.type === "group").length > 1;
9192
+ }
9193
+ /**
9194
+ * Applies an inpainted result (natural image space, see `toSourceDataURL`) as ONE undoable step.
9195
+ * In a document with several layers (see `isMultiLayer`), or with `target: 'new-layer'`, the
9196
+ * result goes to a new pixel layer above the active layer, limited to the selection. Otherwise
9197
+ * (`target: 'replace'`, the single-layer default) it replaces the Background's pixels and clears
9198
+ * the mask annotations, as before. Resolves to the new layer's id in new-layer mode.
9199
+ */
9200
+ async applyInpaintedImage(newDataUrl, options = {}) {
9201
+ if ((options.target ?? this.defaultAITarget()) === "replace") {
9202
+ await this.replaceSource(newDataUrl, true);
9203
+ return;
9204
+ }
9205
+ const { width, height } = this.naturalSize;
9206
+ let px = await this.decodeImage(newDataUrl);
9207
+ if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);
9208
+ const data = new Uint8ClampedArray(px.data);
9209
+ const sel = this.state.selection?.bounds ? this.state.selection : null;
9210
+ if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round(data[i] * sel.data[p] / 255);
9211
+ return this.addPixelLayer({ name: "Inpaint", pixels: { width, height, data } });
9212
+ }
9213
+ /**
9214
+ * Applies a background-removed result as ONE undoable step. In a document with several layers
9215
+ * (or with `target: 'new-layer'`) the result's alpha becomes the mask of the active pixel layer
9216
+ * (or the Background), replacing its mask. Otherwise (`target: 'replace'`, the single-layer
9217
+ * default) it replaces the Background's pixels; crop and annotations are kept (rescaled if the
9218
+ * new image has different dimensions). Resolves to the masked layer's id in mask mode.
9219
+ */
9220
+ async applyBackgroundRemovedImage(newDataUrl, options = {}) {
9221
+ if ((options.target ?? this.defaultAITarget()) === "replace") {
9222
+ await this.replaceSource(newDataUrl, false);
9223
+ return;
9224
+ }
9225
+ const target = this.developLayer();
9226
+ if (!target) throw new LayerOperationError("There is no pixel layer to mask.");
9227
+ requireUnlocked(target, "all", "mask it");
9228
+ const { width, height } = this.naturalSize;
9229
+ let px = await this.decodeImage(newDataUrl);
9230
+ if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);
9231
+ const alpha = new Uint8ClampedArray(width * height);
9232
+ for (let p = 0; p < alpha.length; p++) alpha[p] = px.data[p * 4 + 3];
9233
+ let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);
9234
+ if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);
9235
+ this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
9236
+ return target.id;
9237
+ }
9238
+ /**
9239
+ * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and
9240
+ * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo
9241
+ * step. Returns the layer id.
9242
+ */
9243
+ async rasterizeLayer(id) {
9244
+ const layer = requireLayer(this.state.layers, id).layer;
9245
+ requireUnlocked(layer, "all", "rasterize it");
9246
+ const { width, height } = this.naturalSize;
9247
+ const canvas = { x: 0, y: 0, width, height };
9248
+ let next;
9249
+ if (layer.type === "annotation") {
9250
+ const blend = layer.blend === "normal" && layer.annotation.blend ? layer.annotation.blend : layer.blend;
9251
+ const content = { ...layer, visible: true, opacity: 1, blend: "normal", annotation: { ...layer.annotation, blend: void 0 } };
9252
+ delete content.mask;
9253
+ delete content.clipped;
9254
+ const surface = await this.compositeToSurface([content], this.paintBounds(layer));
9255
+ next = { type: "pixel", id: layer.id, name: layer.name, visible: layer.visible, opacity: layer.opacity, blend, surface, offset: { x: 0, y: 0 } };
9256
+ if (layer.clipped) next.clipped = true;
9257
+ if (layer.mask) next.mask = layer.mask;
9258
+ if (layer.locks) next.locks = layer.locks;
9259
+ if (this.state.selectedAnnotationId === layer.annotation.id) this.state.selectedAnnotationId = null;
9260
+ } else if (layer.type === "pixel") {
9261
+ if (!layer.filters?.length) throw new LayerOperationError(`Layer "${layer.name || id}" is already pixels with no filters to bake.`);
9262
+ requireUnlocked(layer, "pixels", "rasterize it");
9263
+ const spatial = layer.filters.some((f) => f.enabled && f.kind === "blur");
9264
+ const content = { ...layer, visible: true, opacity: 1, blend: "normal" };
9265
+ delete content.mask;
9266
+ delete content.clipped;
9267
+ delete content.fillOpacity;
9268
+ const rect = spatial ? canvas : this.getLayerBounds(id) ?? canvas;
9269
+ const surface = await this.compositeToSurface([content], rect);
9270
+ next = { ...layer, surface, offset: { x: 0, y: 0 } };
9271
+ delete next.filters;
9272
+ delete next.source;
9273
+ if (layer.mask && (layer.offset.x || layer.offset.y)) {
9274
+ next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };
9275
+ }
9276
+ } else {
9277
+ throw new LayerOperationError(`Only annotation layers and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
9278
+ }
9279
+ this.commitLayers(mapLayer(this.state.layers, id, () => next));
9280
+ return id;
9281
+ }
9282
+ // --- PSD ---
9283
+ /** Pixels per inch of the last opened PSD (written back by `toPsd`). */
9284
+ psdResolution;
9285
+ /**
9286
+ * Opens PSD / PSB bytes as the document (one undo step): the layers replace the stack and the
9287
+ * document has no source image afterwards (every pixel comes from the layers). What the model
9288
+ * cannot hold is approximated or left out; `warnings` says what, naming the layers.
9289
+ * Throws `PsdError` for files that cannot be read at all.
7031
9290
  */
7032
- async applyInpaintedImage(newDataUrl) {
7033
- await this.replaceSource(newDataUrl, true);
9291
+ loadPsd(bytes) {
9292
+ const result = importPsd(bytes, { newId: () => newLayerId() });
9293
+ this.recordHistory();
9294
+ const s = this.state;
9295
+ this.imageElement = null;
9296
+ s.sourceUrl = null;
9297
+ s.imageDimensions = { width: result.width, height: result.height };
9298
+ if (!this.samplePixelsOverride) this.samplePixels = null;
9299
+ s.layers = result.layers;
9300
+ s.crop = null;
9301
+ s.transform = { ...DEFAULT_TRANSFORM_VALUES };
9302
+ s.outputOpacity = 1;
9303
+ delete s.legacyOutputBlur;
9304
+ s.selection = null;
9305
+ s.selectedAnnotationId = null;
9306
+ const pixels = flattenLayers(result.layers).filter((l) => l.type === "pixel");
9307
+ s.activeLayerId = pixels.length ? pixels[pixels.length - 1].id : null;
9308
+ s.activeTarget = "pixels";
9309
+ this.psdResolution = result.resolution;
9310
+ this.lastLoadProblems = result.warnings;
9311
+ this.notify();
9312
+ return {
9313
+ layerIds: flattenLayers(result.layers).map((l) => l.id),
9314
+ warnings: result.warnings,
9315
+ width: result.width,
9316
+ height: result.height,
9317
+ summary: result.summary
9318
+ };
7034
9319
  }
7035
9320
  /**
7036
- * Replaces the source image with a background-removed result as ONE undoable step.
7037
- * Crop and annotations are kept (rescaled if the new image has different dimensions).
9321
+ * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment
9322
+ * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked
9323
+ * into pixels and masks, annotation layers are rasterized (in a browser; elsewhere they are left
9324
+ * out), and the merged image is our composite. `warnings` lists every such change.
7038
9325
  */
7039
- async applyBackgroundRemovedImage(newDataUrl) {
7040
- await this.replaceSource(newDataUrl, false);
9326
+ async toPsd() {
9327
+ const { width, height } = this.naturalSize;
9328
+ const canvas = { x: 0, y: 0, width, height };
9329
+ const warnings = [];
9330
+ const note = /* @__PURE__ */ new Map();
9331
+ const add = (msg, name) => note.set(msg, [...note.get(msg) ?? [], name]);
9332
+ const canRasterize = this.renderer.canRasterize();
9333
+ await this.renderer.prepareLayerAssets(this.state.layers, width, height);
9334
+ const sources = this.renderer.compositorSources(width, height);
9335
+ const compositor = new LayerCompositor();
9336
+ const bakeMask = (layer) => {
9337
+ const mask = layer.mask;
9338
+ if (!mask?.source) return mask;
9339
+ add("Parametric (depth) masks were baked into pixel masks", layer.name);
9340
+ const plain3 = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } };
9341
+ const ox = layer.type === "pixel" ? layer.offset.x : 0;
9342
+ const oy = layer.type === "pixel" ? layer.offset.y : 0;
9343
+ const coverage = compositor.maskCoverage(plain3, canvas, sources);
9344
+ const bytes = new Uint8ClampedArray(width * height);
9345
+ if (coverage) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(coverage[i] * 255);
9346
+ else bytes.fill(255);
9347
+ const baked = { surface: surfaceFromPixels({ width, height, data: bytes }, 1, -ox, -oy), defaultValue: 0 };
9348
+ if (mask.density !== void 0 && mask.density < 1) baked.density = mask.density;
9349
+ if (mask.feather) baked.feather = mask.feather;
9350
+ if (mask.enabled === false) baked.enabled = false;
9351
+ return baked;
9352
+ };
9353
+ const prepare = async (layer) => {
9354
+ if (layer.type === "group") {
9355
+ const children = [];
9356
+ for (const c of layer.children) {
9357
+ const p = await prepare(c);
9358
+ if (p) children.push(p);
9359
+ }
9360
+ return { ...layer, children, mask: bakeMask(layer) };
9361
+ }
9362
+ if (layer.type === "adjustment") return { ...layer, mask: bakeMask(layer) };
9363
+ if (layer.type === "annotation") {
9364
+ if (!canRasterize) {
9365
+ add("Annotation layers need a canvas (a browser) to rasterize and were left out", layer.name);
9366
+ return null;
9367
+ }
9368
+ add("Annotation layers were written as pixel layers", layer.name);
9369
+ const blend = layer.blend === "normal" && layer.annotation.blend ? layer.annotation.blend : layer.blend;
9370
+ const content = { ...layer, visible: true, opacity: 1, blend: "normal", annotation: { ...layer.annotation, blend: void 0 } };
9371
+ delete content.mask;
9372
+ delete content.clipped;
9373
+ const data = compositor.composite([content], canvas, sources);
9374
+ const out2 = {
9375
+ type: "pixel",
9376
+ id: layer.id,
9377
+ name: layer.name,
9378
+ visible: layer.visible,
9379
+ opacity: layer.opacity,
9380
+ blend,
9381
+ surface: surfaceFromPixels({ width, height, data }, 4),
9382
+ offset: { x: 0, y: 0 }
9383
+ };
9384
+ if (layer.clipped) out2.clipped = true;
9385
+ if (layer.locks) out2.locks = layer.locks;
9386
+ const mask2 = bakeMask(layer);
9387
+ if (mask2) out2.mask = mask2;
9388
+ return out2;
9389
+ }
9390
+ let out = { ...layer };
9391
+ if (layer.source === "document") {
9392
+ out.surface = this.surfaceOf(layer);
9393
+ delete out.source;
9394
+ }
9395
+ const active = layer.filters?.filter(isActiveFilter) ?? [];
9396
+ if (active.length) {
9397
+ add("Layer filters (develop, blur) were baked into the pixels", layer.name);
9398
+ const content = { ...out, visible: true, opacity: 1, blend: "normal" };
9399
+ delete content.mask;
9400
+ delete content.clipped;
9401
+ delete content.fillOpacity;
9402
+ const data = compositor.composite([content], canvas, sources);
9403
+ out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };
9404
+ }
9405
+ if (layer.filters?.some((f) => !f.enabled)) add("Disabled layer filters were left out", layer.name);
9406
+ delete out.filters;
9407
+ const mask = bakeMask(layer);
9408
+ if (mask) out.mask = mask;
9409
+ else delete out.mask;
9410
+ return out;
9411
+ };
9412
+ const prepared = [];
9413
+ for (const l of this.state.layers) {
9414
+ const p = await prepare(l);
9415
+ if (p) prepared.push(p);
9416
+ }
9417
+ const s = this.state;
9418
+ if (s.crop) warnings.push("The crop is not applied; the PSD is the uncropped document.");
9419
+ if (s.transform.rotate || s.transform.flipH || s.transform.flipV) warnings.push("Rotation and flips are not applied; the PSD is in the source orientation.");
9420
+ if (s.outputOpacity < 1) warnings.push("The document opacity is not written; layers keep their own opacity.");
9421
+ if (s.legacyOutputBlur) warnings.push("The document-level blur of an old document is not written.");
9422
+ for (const [msg, names] of note) warnings.push(`${msg} (${names.length === 1 ? "layer" : "layers"} ${names.map((n) => `"${n}"`).join(", ")}).`);
9423
+ const merged = compositor.composite(prepared, canvas, { ...sources, version: `psd:${Date.now()}` });
9424
+ const result = exportPsd({ width, height, layers: prepared, merged, resolution: this.psdResolution });
9425
+ return { bytes: result.bytes, warnings, psb: result.psb, width, height };
7041
9426
  }
7042
9427
  async replaceSource(src, clearAnnotations) {
7043
9428
  const next = await this.resolveSource(src);
@@ -7084,17 +9469,26 @@ var ImageEditorController = class {
7084
9469
  this.state.layers = layers;
7085
9470
  this.notify();
7086
9471
  }
7087
- // --- Serialization ---
7088
- /**
7089
- * The serializable state. A document whose layers still have the v1 / v2 shape (the
7090
- * Background with its sliders and depth mask, adjustment layers, annotations) is written as
7091
- * v1 / v2 exactly as before; anything else is written as v3 with the layer pixels as PNG
7092
- * assets. `selection` is included (run-length encoded) only when something is selected.
7093
- */
7094
- toJSON() {
9472
+ toJSON(options) {
7095
9473
  const out = this.serializeState();
7096
9474
  if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
7097
- return out;
9475
+ if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;
9476
+ return serializeLayersExternal(this.state.layers, options.store).then(({ layers, assets }) => ({ ...out, layers, assets }));
9477
+ }
9478
+ /**
9479
+ * Loads a stored state whose v3 assets may live in an asset store (references other than data
9480
+ * URLs are fetched with `store.get`), as one undo step. Assets that cannot be fetched are left
9481
+ * out and listed in `lastLoadProblems`.
9482
+ */
9483
+ async loadJSONAsync(data, options = {}) {
9484
+ if (data.version !== LAYERED_SCHEMA_VERSION || !data.assets) {
9485
+ this.loadJSON(data);
9486
+ return;
9487
+ }
9488
+ const problems = [];
9489
+ const assets = await resolveExternalAssets(data.assets, options.store, problems);
9490
+ this.loadJSON({ ...data, assets });
9491
+ this.lastLoadProblems = [...problems, ...this.lastLoadProblems];
7098
9492
  }
7099
9493
  /** Loads a stored state of any version (one undo step). */
7100
9494
  loadJSON(data) {
@@ -7309,6 +9703,43 @@ function collectStateTiles(layers, into) {
7309
9703
  if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
7310
9704
  }
7311
9705
  }
9706
+ function pixelsOnly(layers) {
9707
+ const out = [];
9708
+ for (const l of layers) {
9709
+ if (l.type === "adjustment") continue;
9710
+ if (l.type === "group") out.push({ ...l, children: pixelsOnly(l.children) });
9711
+ else if (l.type === "pixel" && l.filters?.length) {
9712
+ const p = { ...l };
9713
+ delete p.filters;
9714
+ out.push(p);
9715
+ } else out.push(l);
9716
+ }
9717
+ return out;
9718
+ }
9719
+ function resizePixels(px, width, height) {
9720
+ const src = px.data;
9721
+ const out = new Uint8ClampedArray(width * height * 4);
9722
+ const kx = px.width / width;
9723
+ const ky = px.height / height;
9724
+ for (let y = 0; y < height; y++) {
9725
+ const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));
9726
+ const y0 = Math.floor(fy);
9727
+ const y1 = Math.min(px.height - 1, y0 + 1);
9728
+ const ty = fy - y0;
9729
+ for (let x = 0; x < width; x++) {
9730
+ const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));
9731
+ const x0 = Math.floor(fx);
9732
+ const x1 = Math.min(px.width - 1, x0 + 1);
9733
+ const tx = fx - x0;
9734
+ for (let c = 0; c < 4; c++) {
9735
+ const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;
9736
+ const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;
9737
+ out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;
9738
+ }
9739
+ }
9740
+ }
9741
+ return { width, height, data: out };
9742
+ }
7312
9743
  function withoutAnnotations(layers) {
7313
9744
  const out = [];
7314
9745
  for (const l of layers) {
@@ -7331,7 +9762,7 @@ function parseColor(value) {
7331
9762
  const n = (i) => parseInt(hex.slice(i, i + 2), 16);
7332
9763
  return [n(0), n(2), n(4), hex.length === 8 ? n(6) : 255];
7333
9764
  }
7334
- function toHex(c) {
9765
+ function toHex2(c) {
7335
9766
  return "#" + [c[0], c[1], c[2]].map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
7336
9767
  }
7337
9768
  function normalizeSelection(mask) {
@@ -7622,7 +10053,7 @@ var tone = (prefix, label) => [
7622
10053
  decimals: 0,
7623
10054
  track: { kind: "gradient", from, to }
7624
10055
  }));
7625
- var levelsRecord = (ch, label) => [
10056
+ var levelsRecord2 = (ch, label) => [
7626
10057
  { id: `${ch}.inBlack`, label: `${label} input black`, section: "layer", min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },
7627
10058
  { id: `${ch}.gamma`, label: `${label} gamma`, section: "layer", min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },
7628
10059
  { id: `${ch}.inWhite`, label: `${label} input white`, section: "layer", min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },
@@ -7633,10 +10064,10 @@ var RANGE_NAMES = ["Reds", "Yellows", "Greens", "Cyans", "Blues", "Magentas"];
7633
10064
  var ADJUSTMENT_LAYER_CONTROLS = {
7634
10065
  curves: [],
7635
10066
  levels: [
7636
- ...levelsRecord("master", "RGB"),
7637
- ...levelsRecord("red", "Red"),
7638
- ...levelsRecord("green", "Green"),
7639
- ...levelsRecord("blue", "Blue")
10067
+ ...levelsRecord2("master", "RGB"),
10068
+ ...levelsRecord2("red", "Red"),
10069
+ ...levelsRecord2("green", "Green"),
10070
+ ...levelsRecord2("blue", "Blue")
7640
10071
  ],
7641
10072
  hueSaturation: [
7642
10073
  { id: "hue", label: "Hue", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: "rainbow" }, unit: "\xB0" },
@@ -7899,13 +10330,22 @@ function createImageSelectionTools(controller) {
7899
10330
  contiguous: { type: "boolean", description: "Only the connected region around (x, y) (default true)." },
7900
10331
  fuzziness: { type: "number", minimum: 0, maximum: 200, description: "Color Range fuzziness (default 40)." },
7901
10332
  sampleMerged: { type: "boolean", description: "Sample the adjusted image rather than the source." },
10333
+ sample: {
10334
+ type: "string",
10335
+ enum: ["composite", "active-layer"],
10336
+ description: "composite (default): the whole image; active-layer: only the active pixel layer (see set_active_layer)."
10337
+ },
7902
10338
  mode: modeSchema
7903
10339
  },
7904
10340
  additionalProperties: false
7905
10341
  },
7906
10342
  annotations: writeAnnotations("Select by colour", true),
7907
- handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, mode }) => {
7908
- const common = { mode: parseSelectionMode(mode), sampleMerged: sampleMerged === true };
10343
+ handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, sample, mode }) => {
10344
+ const common = {
10345
+ mode: parseSelectionMode(mode),
10346
+ sampleMerged: sampleMerged === true,
10347
+ sample: sample ?? "composite"
10348
+ };
7909
10349
  if (typeof color === "string") {
7910
10350
  await controller.selectColorRange([{ color }], { ...common, fuzziness: num2(fuzziness, 40) });
7911
10351
  } else if (typeof x === "number" && typeof y === "number") {
@@ -8762,6 +11202,14 @@ function createImageLayerTools(controller) {
8762
11202
  return { layer: describe(controller.layerViaCopy({ layerId, cut: cut === true, name })) };
8763
11203
  }
8764
11204
  };
11205
+ const rasterize = {
11206
+ name: "rasterize_layer",
11207
+ title: "Rasterize a layer",
11208
+ description: "Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
11209
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, required: ["layerId"], additionalProperties: false },
11210
+ annotations: { title: "Rasterize a layer", ...DESTRUCTIVE },
11211
+ handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) })
11212
+ };
8765
11213
  const filterArgs = {
8766
11214
  enabled: { type: "boolean", description: "false keeps the filter but skips it." },
8767
11215
  radius: { type: "number", minimum: 0, maximum: 50, description: "blur: box radius in px (default 2)." },
@@ -8848,6 +11296,7 @@ function createImageLayerTools(controller) {
8848
11296
  removeMask,
8849
11297
  maskProps,
8850
11298
  viaCopy,
11299
+ rasterize,
8851
11300
  addFilter,
8852
11301
  updateFilter,
8853
11302
  removeFilter
@@ -8966,6 +11415,82 @@ function createImagePaintTools(controller) {
8966
11415
  return [paintStroke, fill, clear];
8967
11416
  }
8968
11417
 
11418
+ // src/image-editor/psd-tools.ts
11419
+ var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11420
+ var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11421
+ var DATA = {
11422
+ type: "string",
11423
+ minLength: 1,
11424
+ description: "The PSD or PSB file as base64 (a `data:` URL prefix is accepted)."
11425
+ };
11426
+ function psdBytesFromBase64(data) {
11427
+ if (typeof data !== "string" || !data.trim()) throw new PsdError("data is required: the PSD file as base64.");
11428
+ const body = data.trim().replace(/^data:[^,]*;base64,/i, "");
11429
+ if (!/^[A-Za-z0-9+/=_\s-]+$/.test(body)) throw new PsdError("data is not base64; send the PSD file bytes base64-encoded.");
11430
+ const bytes = base64ToBytes(body);
11431
+ if (bytes.length < 26) throw new PsdError(`data decodes to ${bytes.length} bytes, too short for a PSD (at least 26).`);
11432
+ return bytes;
11433
+ }
11434
+ function createImagePsdTools(controller) {
11435
+ const inspect = {
11436
+ name: "inspect_psd",
11437
+ title: "Inspect a PSD",
11438
+ description: "Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.",
11439
+ inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11440
+ annotations: { title: "Inspect a PSD", ...READ3 },
11441
+ handler: ({ data }) => {
11442
+ const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
11443
+ return {
11444
+ width: res.width,
11445
+ height: res.height,
11446
+ ...res.info,
11447
+ resolution: res.resolution ?? null,
11448
+ layers: res.summary.map(({ id: _id, ...s }) => s),
11449
+ warnings: res.warnings
11450
+ };
11451
+ }
11452
+ };
11453
+ const open = {
11454
+ name: "import_psd",
11455
+ title: "Open a PSD as the document",
11456
+ description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility and locks map directly; type, smart-object, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).",
11457
+ inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11458
+ annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE3 },
11459
+ handler: ({ data }) => {
11460
+ const res = controller.loadPsd(psdBytesFromBase64(data));
11461
+ const s = controller.getState();
11462
+ return {
11463
+ width: res.width,
11464
+ height: res.height,
11465
+ layerIds: res.layerIds,
11466
+ layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),
11467
+ activeLayerId: s.activeLayerId,
11468
+ warnings: res.warnings
11469
+ };
11470
+ }
11471
+ };
11472
+ const save = {
11473
+ name: "export_psd",
11474
+ title: "Export the document as a PSD",
11475
+ description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers and masks, in the source frame (crop and rotation are not applied). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
11476
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11477
+ annotations: { title: "Export the document as a PSD", ...READ3 },
11478
+ handler: async () => {
11479
+ const res = await controller.toPsd();
11480
+ return {
11481
+ data: bytesToBase64(res.bytes),
11482
+ mimeType: "image/vnd.adobe.photoshop",
11483
+ size: res.bytes.length,
11484
+ psb: res.psb,
11485
+ width: res.width,
11486
+ height: res.height,
11487
+ warnings: res.warnings
11488
+ };
11489
+ }
11490
+ };
11491
+ return [inspect, open, save];
11492
+ }
11493
+
8969
11494
  // src/image-editor/tools.ts
8970
11495
  var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
8971
11496
  function adjustmentsSummary(adjustments) {
@@ -9203,7 +11728,8 @@ function createImageEditorToolProvider(controller, options = {}) {
9203
11728
  ...createImageSelectionTools(controller),
9204
11729
  ...createImageEditTools(controller),
9205
11730
  ...createImageLayerTools(controller),
9206
- ...createImagePaintTools(controller)
11731
+ ...createImagePaintTools(controller),
11732
+ ...createImagePsdTools(controller)
9207
11733
  ]
9208
11734
  };
9209
11735
  }
@@ -9242,14 +11768,14 @@ function hitTestCurvePoint(points, x, y, width, height, radius = 8) {
9242
11768
  }
9243
11769
  function insertCurvePoint(points, p) {
9244
11770
  const point = { input: clampLevel(p.input), output: clampLevel(p.output) };
9245
- const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));
9246
- let index = out.findIndex((q) => q.input > point.input);
11771
+ const out = points.filter((q2) => q2.input !== point.input).map((q2) => ({ ...q2 }));
11772
+ let index = out.findIndex((q2) => q2.input > point.input);
9247
11773
  if (index === -1) index = out.length;
9248
11774
  out.splice(index, 0, point);
9249
11775
  return { points: out, index };
9250
11776
  }
9251
11777
  function moveCurvePoint(points, index, p) {
9252
- const out = points.map((q) => ({ ...q }));
11778
+ const out = points.map((q2) => ({ ...q2 }));
9253
11779
  if (index < 0 || index >= out.length) return out;
9254
11780
  const lo = index > 0 ? out[index - 1].input + 1 : 0;
9255
11781
  const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;
@@ -9257,8 +11783,8 @@ function moveCurvePoint(points, index, p) {
9257
11783
  return out;
9258
11784
  }
9259
11785
  function removeCurvePoint(points, index) {
9260
- if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));
9261
- return points.filter((_, i) => i !== index).map((q) => ({ ...q }));
11786
+ if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q2) => ({ ...q2 }));
11787
+ return points.filter((_, i) => i !== index).map((q2) => ({ ...q2 }));
9262
11788
  }
9263
11789
  var round22 = (v) => Math.round(v * 100) / 100;
9264
11790
 
@@ -9313,6 +11839,7 @@ var SelectionGesture = class {
9313
11839
  antiAlias: options.antiAlias,
9314
11840
  distance: options.distance,
9315
11841
  sampleMerged: options.sampleMerged,
11842
+ sample: options.sample,
9316
11843
  mode: selectionModeFromModifiers(modifiers, options.mode)
9317
11844
  });
9318
11845
  } catch (err) {
@@ -9575,6 +12102,7 @@ function layerMenu(controller) {
9575
12102
  { action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
9576
12103
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
9577
12104
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
12105
+ { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
9578
12106
  { action: "toggle-clip", label: active?.clipped ? "Release Clipping Mask" : "Create Clipping Mask", enabled: Boolean(loc && loc.index > 0) }
9579
12107
  ];
9580
12108
  }
@@ -9629,6 +12157,9 @@ async function runLayerMenuAction(controller, action) {
9629
12157
  case "toggle-mask":
9630
12158
  if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });
9631
12159
  break;
12160
+ case "rasterize":
12161
+ if (id) await controller.rasterizeLayer(id);
12162
+ break;
9632
12163
  case "toggle-clip":
9633
12164
  if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });
9634
12165
  break;
@@ -9777,7 +12308,41 @@ var PaintGesture = class {
9777
12308
  this.move = null;
9778
12309
  }
9779
12310
  };
12311
+
12312
+ // src/image-editor/psd/ui.ts
12313
+ var PSD_FILE_ACCEPT = ".psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop";
12314
+ function blobBytes(file) {
12315
+ if (typeof file.arrayBuffer === "function") return file.arrayBuffer().then((b) => new Uint8Array(b));
12316
+ return new Promise((resolve, reject) => {
12317
+ const reader = new FileReader();
12318
+ reader.onload = () => resolve(new Uint8Array(reader.result));
12319
+ reader.onerror = () => reject(reader.error ?? new Error("The file could not be read."));
12320
+ reader.readAsArrayBuffer(file);
12321
+ });
12322
+ }
12323
+ async function openPsdFile(controller, file) {
12324
+ return controller.loadPsd(await blobBytes(file)).warnings;
12325
+ }
12326
+ function psdFileName(name, psb) {
12327
+ const base = name.replace(/\.[a-z0-9]{2,4}$/i, "") || "image";
12328
+ return `${base}.${psb ? "psb" : "psd"}`;
12329
+ }
12330
+ async function downloadPsd(controller, name = "image") {
12331
+ const res = await controller.toPsd();
12332
+ const fileName = psdFileName(name, res.psb);
12333
+ if (typeof document !== "undefined" && typeof URL !== "undefined" && URL.createObjectURL) {
12334
+ const blob = new Blob([res.bytes], { type: "image/vnd.adobe.photoshop" });
12335
+ const url = URL.createObjectURL(blob);
12336
+ const a = document.createElement("a");
12337
+ a.href = url;
12338
+ a.download = fileName;
12339
+ a.click();
12340
+ setTimeout(() => URL.revokeObjectURL(url), 0);
12341
+ }
12342
+ return { fileName, warnings: res.warnings };
12343
+ }
9780
12344
  export {
12345
+ ADJUSTMENT_KEYS,
9781
12346
  ADJUSTMENT_KINDS,
9782
12347
  ADJUSTMENT_LAYER_CONTROLS,
9783
12348
  ADJUSTMENT_LAYER_OPACITY_CONTROL,
@@ -9789,6 +12354,7 @@ export {
9789
12354
  BLUR_FILTER_ID,
9790
12355
  BRUSH_OPTION_CONTROLS,
9791
12356
  CoverageMap,
12357
+ D50,
9792
12358
  D65,
9793
12359
  DEFAULT_ADJUSTMENTS,
9794
12360
  DEFAULT_BRUSH,
@@ -9820,13 +12386,17 @@ export {
9820
12386
  MIN_KELVIN,
9821
12387
  MIRED_PER_TEMPERATURE_UNIT,
9822
12388
  PAINT_TOOLS,
12389
+ PSD_BLEND_KEYS,
12390
+ PSD_FILE_ACCEPT,
9823
12391
  PaintGesture,
9824
12392
  PaintStrokeSession,
12393
+ PsdError,
9825
12394
  REFINE_EDGE_CONTROLS,
9826
12395
  SELECTION_MODES,
9827
12396
  SELECTION_MODE_LABELS,
9828
12397
  SELECTION_MODIFY_OPS,
9829
12398
  SELECTION_TOOLS,
12399
+ SUPPORTED_ADJUSTMENT_KEYS,
9830
12400
  SelectionGesture,
9831
12401
  StrokeCoverage,
9832
12402
  SurfaceDraft,
@@ -9837,6 +12407,7 @@ export {
9837
12407
  TONE_RANGE_PRESETS,
9838
12408
  TiledSurface,
9839
12409
  VIBRANCE_GREY,
12410
+ XYZ_D50_TO_SRGB,
9840
12411
  ZlibError,
9841
12412
  activeAdjustmentLayers,
9842
12413
  addLayerFilter,
@@ -9904,6 +12475,7 @@ export {
9904
12475
  createImageEditorToolProvider,
9905
12476
  createImageLayerTools,
9906
12477
  createImagePaintTools,
12478
+ createImagePsdTools,
9907
12479
  createImageSelectionTools,
9908
12480
  createSelectionMask,
9909
12481
  curveLut,
@@ -9912,7 +12484,10 @@ export {
9912
12484
  curveSvgPath,
9913
12485
  dabCoverage,
9914
12486
  dabRect,
12487
+ decodeLayerChannel,
9915
12488
  decodeLayerMask,
12489
+ decodeMergedImage,
12490
+ decodePlanes,
9916
12491
  decodePng,
9917
12492
  decodePngDataUrl,
9918
12493
  decodeSelectionRle,
@@ -9930,6 +12505,7 @@ export {
9930
12505
  developFromAdjustments,
9931
12506
  developTarget,
9932
12507
  distanceTransform,
12508
+ downloadPsd,
9933
12509
  dropPlacement,
9934
12510
  dropZone,
9935
12511
  duplicateLayer,
@@ -9939,10 +12515,12 @@ export {
9939
12515
  emptyHistogram,
9940
12516
  emptySurface,
9941
12517
  encodePng,
12518
+ encodeRle,
9942
12519
  encodeSelectionRle,
9943
12520
  encodeSurface,
9944
12521
  evaluateAdjustmentLayers,
9945
12522
  expandCoverage,
12523
+ exportPsd,
9946
12524
  featherCoverage,
9947
12525
  fillRect,
9948
12526
  findAdjustmentControl,
@@ -9981,6 +12559,7 @@ export {
9981
12559
  identityLevels,
9982
12560
  imageEditorStateMigrations,
9983
12561
  imageToCanvasPoint,
12562
+ importPsd,
9984
12563
  inflate,
9985
12564
  inflateRaw,
9986
12565
  insertCurvePoint,
@@ -10008,7 +12587,9 @@ export {
10008
12587
  layerMaskFromSelection,
10009
12588
  layerMaskKey,
10010
12589
  layerMenu,
12590
+ layerName,
10011
12591
  layerPanelRows,
12592
+ layerTree,
10012
12593
  layerTypeLabel,
10013
12594
  layersFromLegacy,
10014
12595
  legacyAdjustmentLayer,
@@ -10043,11 +12624,14 @@ export {
10043
12624
  normalizeRefineEdgeParams,
10044
12625
  normalizeStoredAdjustments,
10045
12626
  offsetLayer,
12627
+ openPsdFile,
10046
12628
  packRgba,
12629
+ parseAdjustmentBlock,
10047
12630
  parseBlend,
10048
12631
  parseColor,
10049
12632
  parseDataUrl,
10050
12633
  parseSelectionMode,
12634
+ parseVersionedDescriptor,
10051
12635
  photoFilterMatrix,
10052
12636
  pixelLayerBounds,
10053
12637
  pixelLayers,
@@ -10061,12 +12645,16 @@ export {
10061
12645
  projectImageRect,
10062
12646
  projectMaskCoverage,
10063
12647
  projectQuarterTurn,
12648
+ psdBytesFromBase64,
12649
+ psdFileName,
12650
+ psdToLayers,
10064
12651
  quickMaskPixels,
10065
12652
  rangeLightness,
10066
12653
  rasterMaskFromLegacy,
10067
12654
  rasterizeDab,
10068
12655
  readAdjustmentControls,
10069
12656
  readPixel,
12657
+ readPsd,
10070
12658
  readRegion,
10071
12659
  rectSelection,
10072
12660
  refineCoverage,
@@ -10079,7 +12667,9 @@ export {
10079
12667
  renderLayeredBase,
10080
12668
  requireLayer,
10081
12669
  requireUnlocked,
12670
+ resizePixels,
10082
12671
  resizeSelection,
12672
+ resolveExternalAssets,
10083
12673
  resolveMosaicRegionBounds,
10084
12674
  rgbToGray,
10085
12675
  rgbToHsl,
@@ -10096,6 +12686,7 @@ export {
10096
12686
  selectionValueAt,
10097
12687
  serializeLayer,
10098
12688
  serializeLayers,
12689
+ serializeLayersExternal,
10099
12690
  serializeSelection,
10100
12691
  setControlPath,
10101
12692
  setLayerMask,
@@ -10121,7 +12712,7 @@ export {
10121
12712
  tipFalloff,
10122
12713
  tipReach,
10123
12714
  tipShape,
10124
- toHex,
12715
+ toHex2 as toHex,
10125
12716
  toneCurve,
10126
12717
  toneLuts,
10127
12718
  toneRange,
@@ -10133,6 +12724,7 @@ export {
10133
12724
  updateLayerFilter,
10134
12725
  updateLayerMaskProps,
10135
12726
  uvToXy,
12727
+ versionedDescriptorBytes,
10136
12728
  vibranceLinear,
10137
12729
  vibrancePixel,
10138
12730
  walkDabs,
@@ -10140,6 +12732,8 @@ export {
10140
12732
  whiteBalanceFromSample,
10141
12733
  whiteBalanceMatrix,
10142
12734
  withNewIds,
12735
+ writeAdjustmentBlock,
12736
+ writePsd,
10143
12737
  writeRegion,
10144
12738
  xyToTempTint,
10145
12739
  xyToUv