@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.
- package/dist/image-editor/index.cjs +2770 -176
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +439 -8
- package/dist/image-editor/index.d.ts +439 -8
- package/dist/image-editor/index.js +2653 -59
- package/dist/image-editor/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
275
|
-
return { temperature:
|
|
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
|
|
1005
|
-
const dq = vertical[row +
|
|
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 * (
|
|
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] =
|
|
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
|
|
1555
|
-
const vq = Math.round(v *
|
|
1556
|
-
u = Math.floor(clamp01((vq - ch.inBlack) / range) *
|
|
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
|
|
1591
|
-
const p = 2 * l -
|
|
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 + (
|
|
1595
|
-
if (t < 0.5) return
|
|
1596
|
-
if (t < 2 / 3) return p + (
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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 ?
|
|
6141
|
-
background: options.background !== void 0 ?
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
7030
|
-
*
|
|
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
|
-
|
|
7033
|
-
|
|
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
|
-
*
|
|
7037
|
-
*
|
|
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
|
|
7040
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
...
|
|
7637
|
-
...
|
|
7638
|
-
...
|
|
7639
|
-
...
|
|
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 = {
|
|
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((
|
|
9246
|
-
let index = out.findIndex((
|
|
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((
|
|
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((
|
|
9261
|
-
return points.filter((_, i) => i !== index).map((
|
|
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
|