@pixerate/editor 0.101.0 → 0.102.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/THIRD_PARTY_NOTICES.md +1 -0
- package/dist/{chunk-WIR377F6.js → chunk-67FRIOPZ.js} +15 -12
- package/dist/chunk-67FRIOPZ.js.map +1 -0
- package/dist/{chunk-WT6ZAWJP.cjs → chunk-WSTEXEKD.cjs} +27 -24
- package/dist/chunk-WSTEXEKD.cjs.map +1 -0
- package/dist/image-editor/index.cjs +2843 -1050
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +644 -16
- package/dist/image-editor/index.d.ts +644 -16
- package/dist/image-editor/index.js +2487 -694
- package/dist/image-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +2 -2
- package/dist/raster/index.d.cts +99 -91
- package/dist/raster/index.d.ts +99 -91
- package/dist/raster/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +69 -11
- package/dist/chunk-WIR377F6.js.map +0 -1
- package/dist/chunk-WT6ZAWJP.cjs.map +0 -1
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
validateLayerEffect,
|
|
69
69
|
validateLayerEffects,
|
|
70
70
|
warpSurface
|
|
71
|
-
} from "../chunk-
|
|
71
|
+
} from "../chunk-67FRIOPZ.js";
|
|
72
72
|
import {
|
|
73
73
|
affineFromHomography,
|
|
74
74
|
applyHomography,
|
|
@@ -166,6 +166,7 @@ import {
|
|
|
166
166
|
identityWarpMesh,
|
|
167
167
|
invertMatrix,
|
|
168
168
|
isValidWarpMesh,
|
|
169
|
+
mapWarpMeshPoints,
|
|
169
170
|
matrixAbout,
|
|
170
171
|
multiplyMatrices,
|
|
171
172
|
pullWarpMesh,
|
|
@@ -2429,7 +2430,13 @@ function pixelLayerBounds(layer, surface = layer.surface) {
|
|
|
2429
2430
|
return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height };
|
|
2430
2431
|
}
|
|
2431
2432
|
function layerTypeLabel(type) {
|
|
2432
|
-
return type === "pixel" ? "Pixel layer" : type === "adjustment" ? "Adjustment layer" : type === "group" ? "Group" : "Annotation";
|
|
2433
|
+
return type === "pixel" ? "Pixel layer" : type === "adjustment" ? "Adjustment layer" : type === "group" ? "Group" : type === "smart" ? "Smart object" : "Annotation";
|
|
2434
|
+
}
|
|
2435
|
+
function hasSurface(layer) {
|
|
2436
|
+
return layer.type === "pixel" || layer.type === "smart";
|
|
2437
|
+
}
|
|
2438
|
+
function smartObjectLayers(layers) {
|
|
2439
|
+
return flattenLayers(layers).filter((l) => l.type === "smart");
|
|
2433
2440
|
}
|
|
2434
2441
|
|
|
2435
2442
|
// src/image-editor/layers/compositor.ts
|
|
@@ -2485,6 +2492,13 @@ function depthMaskAlpha(depth, options) {
|
|
|
2485
2492
|
v += lo;
|
|
2486
2493
|
return v < 0 ? 0 : v > 255 ? 255 : v;
|
|
2487
2494
|
}
|
|
2495
|
+
function runLayerFilters(data, width, height, filters) {
|
|
2496
|
+
for (const f of filters) {
|
|
2497
|
+
if (!isActiveFilter(f)) continue;
|
|
2498
|
+
if (f.kind === "develop") applyPixelFilters(data, [f]);
|
|
2499
|
+
else if (f.kind === "blur") applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2488
2502
|
var filterSpecCache = /* @__PURE__ */ new WeakMap();
|
|
2489
2503
|
function pixelFilterSpec(layer) {
|
|
2490
2504
|
const list2 = layer.filters;
|
|
@@ -2497,12 +2511,7 @@ function pixelFilterSpec(layer) {
|
|
|
2497
2511
|
spec = {
|
|
2498
2512
|
key: JSON.stringify(filters),
|
|
2499
2513
|
spatial,
|
|
2500
|
-
apply: (data, width, height) =>
|
|
2501
|
-
for (const f of filters) {
|
|
2502
|
-
if (f.kind === "develop") applyPixelFilters(data, [f]);
|
|
2503
|
-
else if (f.kind === "blur") applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2514
|
+
apply: (data, width, height) => runLayerFilters(data, width, height, filters)
|
|
2506
2515
|
};
|
|
2507
2516
|
}
|
|
2508
2517
|
filterSpecCache.set(list2, spec);
|
|
@@ -2519,6 +2528,8 @@ var PHOTO_PROFILE = {
|
|
|
2519
2528
|
contentBlend: (l) => l.type === "annotation" && l.blend === "normal" && l.annotation.blend ? l.annotation.blend : l.blend,
|
|
2520
2529
|
layerKey: (l) => l.type === "pixel" ? [l.filters ?? null, l.source ?? null] : l.type === "adjustment" ? [l.kind, l.params] : l.type === "annotation" ? l.annotation : void 0,
|
|
2521
2530
|
pixelFilters: (l) => l.type === "pixel" ? pixelFilterSpec(l) : null,
|
|
2531
|
+
/** A smart object draws its rendered pixels (contents → placement → smart filters) like a pixel layer. */
|
|
2532
|
+
isPixelLike: (l) => l.type === "smart",
|
|
2522
2533
|
maskSourcePlane: (source, W, H, sources) => {
|
|
2523
2534
|
const opts = source;
|
|
2524
2535
|
const depth = sources.depthMap?.(opts) ?? null;
|
|
@@ -4607,330 +4618,162 @@ function refineSelection(pixels, mask, params = {}) {
|
|
|
4607
4618
|
return createSelectionMask(mask.width, mask.height, out);
|
|
4608
4619
|
}
|
|
4609
4620
|
|
|
4610
|
-
// src/image-editor/
|
|
4611
|
-
var
|
|
4612
|
-
|
|
4613
|
-
|
|
4621
|
+
// src/image-editor/transform.ts
|
|
4622
|
+
var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
|
|
4623
|
+
var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
|
|
4624
|
+
var ANCHOR_FRACTION = {
|
|
4625
|
+
"top-left": [0, 0],
|
|
4626
|
+
top: [0.5, 0],
|
|
4627
|
+
"top-right": [1, 0],
|
|
4628
|
+
left: [0, 0.5],
|
|
4629
|
+
center: [0.5, 0.5],
|
|
4630
|
+
right: [1, 0.5],
|
|
4631
|
+
"bottom-left": [0, 1],
|
|
4632
|
+
bottom: [0.5, 1],
|
|
4633
|
+
"bottom-right": [1, 1]
|
|
4634
|
+
};
|
|
4635
|
+
function isTransformAnchor(value) {
|
|
4636
|
+
return typeof value === "string" && value in ANCHOR_FRACTION;
|
|
4614
4637
|
}
|
|
4615
|
-
function
|
|
4616
|
-
|
|
4617
|
-
const out = [];
|
|
4618
|
-
for (const p of raw) {
|
|
4619
|
-
if (!p || typeof p !== "object" || typeof p.id !== "string" || typeof p.png !== "string") continue;
|
|
4620
|
-
try {
|
|
4621
|
-
const png = decodePngDataUrl(p.png, 4);
|
|
4622
|
-
out.push({ id: p.id, name: typeof p.name === "string" ? p.name : p.id, width: png.width, height: png.height, data: png.data });
|
|
4623
|
-
} catch (e) {
|
|
4624
|
-
problems.push(`Pattern "${p.name ?? p.id}" could not be decoded (${e instanceof Error ? e.message : String(e)}).`);
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
return out;
|
|
4638
|
+
function identityTransform(box) {
|
|
4639
|
+
return { box: { ...box }, corners: boxQuad(box) };
|
|
4628
4640
|
}
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
const b = contentBounds(surface);
|
|
4633
|
-
let out = null;
|
|
4634
|
-
if (b) {
|
|
4635
|
-
const data = readRegion(surface, b);
|
|
4636
|
-
out = { url: pngDataUrl(b.width, b.height, data, surface.channels), x: b.x, y: b.y };
|
|
4637
|
-
}
|
|
4638
|
-
encodedCache.set(surface, out);
|
|
4639
|
-
return out;
|
|
4641
|
+
function cornersFromMatrix(box, m) {
|
|
4642
|
+
const q2 = boxQuad(box);
|
|
4643
|
+
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
4640
4644
|
}
|
|
4641
|
-
function
|
|
4642
|
-
const
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
blend: layer.blend
|
|
4648
|
-
};
|
|
4649
|
-
if (layer.clipped) out.clipped = true;
|
|
4650
|
-
if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
|
|
4651
|
-
if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
|
|
4652
|
-
if (layer.effects) out.effects = serializeEffects(layer.effects);
|
|
4653
|
-
return out;
|
|
4645
|
+
function matrixFromCorners(box, corners, eps = 1e-6) {
|
|
4646
|
+
const [tl, tr, br, bl] = corners;
|
|
4647
|
+
const size = 1 + Math.max(box.width, box.height);
|
|
4648
|
+
if (Math.abs(tl.x + br.x - tr.x - bl.x) > eps * size || Math.abs(tl.y + br.y - tr.y - bl.y) > eps * size) return null;
|
|
4649
|
+
const h = rectToQuad(box, corners);
|
|
4650
|
+
return h ? affineFromHomography(h, 1e-9) : null;
|
|
4654
4651
|
}
|
|
4655
|
-
function
|
|
4656
|
-
const
|
|
4657
|
-
if (
|
|
4658
|
-
|
|
4659
|
-
return out;
|
|
4652
|
+
function transformHomography(geometry) {
|
|
4653
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
4654
|
+
if (!h) return null;
|
|
4655
|
+
return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;
|
|
4660
4656
|
}
|
|
4661
|
-
function
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
try {
|
|
4668
|
-
psd = { key: p.key, data: base64ToBytes(p.data) };
|
|
4669
|
-
} catch {
|
|
4670
|
-
psd = void 0;
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
return normalizeLayerEffects({ ...r, psd });
|
|
4657
|
+
function hasWarp(geometry) {
|
|
4658
|
+
const w = geometry.warp;
|
|
4659
|
+
if (!w) return false;
|
|
4660
|
+
const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);
|
|
4661
|
+
if (id.points.length !== w.points.length || id.us.some((u, i) => Math.abs(u - w.us[i]) > 1e-12) || id.vs.some((v, i) => Math.abs(v - w.vs[i]) > 1e-12)) return true;
|
|
4662
|
+
return w.points.some((p, i) => Math.abs(p.x - id.points[i].x) > 1e-6 || Math.abs(p.y - id.points[i].y) > 1e-6);
|
|
4674
4663
|
}
|
|
4675
|
-
function
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
if (enc) {
|
|
4680
|
-
out.asset = ctx.addAsset(enc.url, hint);
|
|
4681
|
-
out.x = enc.x;
|
|
4682
|
-
out.y = enc.y;
|
|
4683
|
-
}
|
|
4684
|
-
} else {
|
|
4685
|
-
out.source = { ...mask.source, depthRange: [...mask.source.depthRange] };
|
|
4686
|
-
}
|
|
4687
|
-
if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
|
|
4688
|
-
if (mask.feather) out.feather = mask.feather;
|
|
4689
|
-
if (mask.enabled === false) out.enabled = false;
|
|
4690
|
-
if (mask.linked === false) out.linked = false;
|
|
4691
|
-
return out;
|
|
4664
|
+
function isIdentityTransform(geometry) {
|
|
4665
|
+
if (hasWarp(geometry)) return false;
|
|
4666
|
+
const q2 = boxQuad(geometry.box);
|
|
4667
|
+
return geometry.corners.every((c, i) => Math.abs(c.x - q2[i].x) < 1e-6 && Math.abs(c.y - q2[i].y) < 1e-6);
|
|
4692
4668
|
}
|
|
4693
|
-
function
|
|
4694
|
-
const
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
const enc = encodeSurface(layer.surface);
|
|
4701
|
-
if (enc) {
|
|
4702
|
-
out.asset = ctx.addAsset(enc.url, layer.id);
|
|
4703
|
-
out.x = enc.x;
|
|
4704
|
-
out.y = enc.y;
|
|
4705
|
-
}
|
|
4706
|
-
}
|
|
4707
|
-
if (layer.fillOpacity !== void 0 && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;
|
|
4708
|
-
if (layer.filters?.length) out.filters = JSON.parse(JSON.stringify(layer.filters));
|
|
4709
|
-
return out;
|
|
4710
|
-
}
|
|
4711
|
-
case "adjustment":
|
|
4712
|
-
return { ...base, type: "adjustment", kind: layer.kind, params: JSON.parse(JSON.stringify(layer.params)) };
|
|
4713
|
-
case "group": {
|
|
4714
|
-
const out = { ...base, type: "group", children: layer.children.map((c) => serializeLayer(c, ctx)) };
|
|
4715
|
-
if (layer.collapsed) out.collapsed = true;
|
|
4716
|
-
return out;
|
|
4717
|
-
}
|
|
4718
|
-
case "annotation":
|
|
4719
|
-
return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify(layer.annotation)) };
|
|
4720
|
-
}
|
|
4669
|
+
function transformMap(geometry) {
|
|
4670
|
+
const h = transformHomography(geometry);
|
|
4671
|
+
if (!h) return null;
|
|
4672
|
+
const { box } = geometry;
|
|
4673
|
+
const warp = hasWarp(geometry) ? geometry.warp : null;
|
|
4674
|
+
if (!warp) return (p) => applyHomography(h, p);
|
|
4675
|
+
return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));
|
|
4721
4676
|
}
|
|
4722
|
-
function
|
|
4723
|
-
const
|
|
4724
|
-
const
|
|
4725
|
-
|
|
4726
|
-
let id = hint;
|
|
4727
|
-
for (let n = 2; id in assets && assets[id] !== url; n++) id = `${hint}-${n}`;
|
|
4728
|
-
assets[id] = url;
|
|
4729
|
-
return id;
|
|
4730
|
-
}
|
|
4731
|
-
};
|
|
4732
|
-
return { layers: layers.map((l) => serializeLayer(l, ctx)), assets };
|
|
4677
|
+
function transformedPoint(geometry, fx, fy) {
|
|
4678
|
+
const f = transformMap(geometry);
|
|
4679
|
+
const { box } = geometry;
|
|
4680
|
+
return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;
|
|
4733
4681
|
}
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
const
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4682
|
+
function anchorPoint(geometry, anchor) {
|
|
4683
|
+
const [fx, fy] = ANCHOR_FRACTION[anchor];
|
|
4684
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
4685
|
+
const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };
|
|
4686
|
+
return (h && applyHomography(h, p)) ?? p;
|
|
4687
|
+
}
|
|
4688
|
+
function transformedBox(geometry) {
|
|
4689
|
+
const f = transformMap(geometry);
|
|
4690
|
+
if (!f) return null;
|
|
4691
|
+
const { box } = geometry;
|
|
4692
|
+
const pts = [];
|
|
4693
|
+
const n = hasWarp(geometry) ? 16 : 1;
|
|
4694
|
+
for (let i = 0; i <= n; i++) {
|
|
4695
|
+
const t = i / n;
|
|
4696
|
+
for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {
|
|
4697
|
+
const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });
|
|
4698
|
+
if (!p) return null;
|
|
4699
|
+
pts.push(p);
|
|
4751
4700
|
}
|
|
4752
|
-
assets[id] = ref;
|
|
4753
4701
|
}
|
|
4754
|
-
|
|
4702
|
+
const xs = pts.map((p) => p.x);
|
|
4703
|
+
const ys = pts.map((p) => p.y);
|
|
4704
|
+
const x = Math.min(...xs);
|
|
4705
|
+
const y = Math.min(...ys);
|
|
4706
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
4755
4707
|
}
|
|
4756
|
-
function
|
|
4757
|
-
|
|
4758
|
-
|
|
4708
|
+
function referencePoint(box, reference) {
|
|
4709
|
+
if (reference && typeof reference === "object") return { x: reference.x, y: reference.y };
|
|
4710
|
+
const [fx, fy] = ANCHOR_FRACTION[reference ?? "center"];
|
|
4711
|
+
return { x: box.x + fx * box.width, y: box.y + fy * box.height };
|
|
4759
4712
|
}
|
|
4760
|
-
|
|
4761
|
-
const
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
problems.push(`Asset "${id}" is external (${ref}) and no asset store was given.`);
|
|
4769
|
-
continue;
|
|
4770
|
-
}
|
|
4771
|
-
try {
|
|
4772
|
-
const blob = await store.get(ref);
|
|
4773
|
-
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
4774
|
-
out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;
|
|
4775
|
-
} catch (err) {
|
|
4776
|
-
problems.push(`Asset "${id}" (${ref}) could not be fetched: ${err.message}`);
|
|
4777
|
-
}
|
|
4778
|
-
}
|
|
4779
|
-
return out;
|
|
4713
|
+
function matrixFromParams(box, params) {
|
|
4714
|
+
const sx = (params.scaleX ?? 100) / 100;
|
|
4715
|
+
const sy = (params.scaleY ?? 100) / 100;
|
|
4716
|
+
const tx = Math.tan((params.skewX ?? 0) * Math.PI / 180);
|
|
4717
|
+
const ty = Math.tan((params.skewY ?? 0) * Math.PI / 180);
|
|
4718
|
+
const ref = referencePoint(box, params.reference);
|
|
4719
|
+
const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));
|
|
4720
|
+
return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));
|
|
4780
4721
|
}
|
|
4781
|
-
function
|
|
4782
|
-
const
|
|
4783
|
-
|
|
4784
|
-
const
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
const
|
|
4788
|
-
|
|
4789
|
-
out2[p] = Math.round((0.299 * png.data[i] + 0.587 * png.data[i + 1] + 0.114 * png.data[i + 2]) * png.data[i + 3] / 255);
|
|
4790
|
-
}
|
|
4791
|
-
return { width: png.width, height: png.height, data: out2 };
|
|
4792
|
-
}
|
|
4793
|
-
const out = new Uint8ClampedArray(png.width * png.height * 4);
|
|
4794
|
-
for (let p = 0; p < png.width * png.height; p++) {
|
|
4795
|
-
out[p * 4] = out[p * 4 + 1] = out[p * 4 + 2] = png.data[p];
|
|
4796
|
-
out[p * 4 + 3] = 255;
|
|
4722
|
+
function describeTransform(geometry, reference = "center") {
|
|
4723
|
+
const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);
|
|
4724
|
+
const ref = referencePoint(geometry.box, reference);
|
|
4725
|
+
const bounds = transformedBox(geometry);
|
|
4726
|
+
if (!m) {
|
|
4727
|
+
const h = rectToQuad(geometry.box, geometry.corners);
|
|
4728
|
+
const at2 = (h && applyHomography(h, ref)) ?? ref;
|
|
4729
|
+
return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at2.x, y: at2.y, bounds, corners: geometry.corners };
|
|
4797
4730
|
}
|
|
4798
|
-
|
|
4731
|
+
const [a, b, c, d] = m;
|
|
4732
|
+
const sxAbs = Math.hypot(a, b);
|
|
4733
|
+
const rot = Math.atan2(b, a) * 180 / Math.PI;
|
|
4734
|
+
const shear = sxAbs ? (a * c + b * d) / sxAbs : 0;
|
|
4735
|
+
const sy = sxAbs ? (a * d - b * c) / sxAbs : 0;
|
|
4736
|
+
const skew = sxAbs ? Math.atan(shear / sxAbs) * 180 / Math.PI : 0;
|
|
4737
|
+
const at = applyMatrix(m, ref);
|
|
4738
|
+
const clean = (v) => Math.abs(v) < 1e-9 ? 0 : v;
|
|
4739
|
+
return {
|
|
4740
|
+
affine: true,
|
|
4741
|
+
scaleX: clean(sxAbs * 100),
|
|
4742
|
+
scaleY: clean(sy * 100),
|
|
4743
|
+
rotate: clean(rot === -180 ? 180 : rot),
|
|
4744
|
+
skewX: clean(skew),
|
|
4745
|
+
x: at.x,
|
|
4746
|
+
y: at.y,
|
|
4747
|
+
bounds,
|
|
4748
|
+
corners: geometry.corners
|
|
4749
|
+
};
|
|
4799
4750
|
}
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
const
|
|
4804
|
-
|
|
4805
|
-
ctx.problems.push(`${what}: asset "${ref}" is missing`);
|
|
4806
|
-
return emptySurface(channels, defaultValue);
|
|
4807
|
-
}
|
|
4808
|
-
try {
|
|
4809
|
-
const px2 = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);
|
|
4810
|
-
return surfaceFromPixels(px2, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);
|
|
4811
|
-
} catch (err) {
|
|
4812
|
-
ctx.problems.push(`${what}: asset "${ref}" could not be decoded (${err.message})`);
|
|
4813
|
-
return emptySurface(channels, defaultValue);
|
|
4814
|
-
}
|
|
4751
|
+
function applyParams(geometry, params) {
|
|
4752
|
+
const ref = params.reference && typeof params.reference === "object" ? params.reference : anchorPoint(geometry, params.reference ?? "center");
|
|
4753
|
+
const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });
|
|
4754
|
+
const c = geometry.corners;
|
|
4755
|
+
return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];
|
|
4815
4756
|
}
|
|
4816
|
-
function
|
|
4817
|
-
|
|
4818
|
-
const
|
|
4819
|
-
const
|
|
4820
|
-
|
|
4821
|
-
surface: r.source ? emptySurface(1, 255) : readSurface(r.asset, r.x, r.y, 1, defaultValue, ctx, `${what} mask`),
|
|
4822
|
-
defaultValue: r.source ? 255 : defaultValue
|
|
4823
|
-
};
|
|
4824
|
-
if (typeof r.density === "number" && r.density !== 1) mask.density = num(r.density, 0, 1, 1);
|
|
4825
|
-
if (typeof r.feather === "number" && r.feather > 0) mask.feather = num(r.feather, 0, 1e3, 0);
|
|
4826
|
-
if (r.enabled === false) mask.enabled = false;
|
|
4827
|
-
if (r.linked === false) mask.linked = false;
|
|
4828
|
-
if (r.source && typeof r.source === "object") {
|
|
4829
|
-
const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...r.source };
|
|
4830
|
-
mask.source = { ...s, depthRange: [...s.depthRange] };
|
|
4831
|
-
}
|
|
4832
|
-
return mask;
|
|
4757
|
+
function flipCorners(geometry, axis) {
|
|
4758
|
+
const c = anchorPoint(geometry, "center");
|
|
4759
|
+
const m = axis === "horizontal" ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];
|
|
4760
|
+
const q2 = geometry.corners;
|
|
4761
|
+
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
4833
4762
|
}
|
|
4834
|
-
function
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
if (!f || typeof f !== "object") continue;
|
|
4839
|
-
const r = f;
|
|
4840
|
-
const id = typeof r.id === "string" && r.id ? r.id : newLayerId("flt");
|
|
4841
|
-
const enabled = r.enabled !== false;
|
|
4842
|
-
if (r.kind === "blur") out.push({ id, kind: "blur", enabled, radius: num(r.radius, 0, 50, 0) });
|
|
4843
|
-
else if (r.kind === "develop") {
|
|
4844
|
-
out.push({
|
|
4845
|
-
id,
|
|
4846
|
-
kind: "develop",
|
|
4847
|
-
enabled,
|
|
4848
|
-
brightness: num(r.brightness, -100, 100, 0),
|
|
4849
|
-
contrast: num(r.contrast, -100, 100, 0),
|
|
4850
|
-
saturation: num(r.saturation, -100, 100, 0),
|
|
4851
|
-
exposure: num(r.exposure, -100, 100, 0),
|
|
4852
|
-
temperature: num(r.temperature, -100, 100, 0),
|
|
4853
|
-
tint: num(r.tint, -100, 100, 0),
|
|
4854
|
-
process: normalizeProcessVersion(r.process)
|
|
4855
|
-
});
|
|
4856
|
-
}
|
|
4857
|
-
}
|
|
4858
|
-
return out.length ? out : void 0;
|
|
4763
|
+
function boxPointOf(geometry, p) {
|
|
4764
|
+
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
4765
|
+
if (m) return applyMatrix(invertMatrix(m), p);
|
|
4766
|
+
return null;
|
|
4859
4767
|
}
|
|
4860
|
-
function
|
|
4861
|
-
|
|
4862
|
-
const r = raw;
|
|
4863
|
-
const id = typeof r.id === "string" && r.id ? r.id : newLayerId();
|
|
4864
|
-
const base = {
|
|
4865
|
-
id,
|
|
4866
|
-
name: typeof r.name === "string" ? r.name : "",
|
|
4867
|
-
visible: r.visible !== false,
|
|
4868
|
-
opacity: num(r.opacity, 0, 1, 1),
|
|
4869
|
-
blend: normalizeBlendMode(r.blend)
|
|
4870
|
-
};
|
|
4871
|
-
if (r.clipped === true) base.clipped = true;
|
|
4872
|
-
const mask = readMask(r.mask, ctx, `Layer "${base.name || id}"`);
|
|
4873
|
-
if (mask) base.mask = mask;
|
|
4874
|
-
if (r.locks && typeof r.locks === "object") {
|
|
4875
|
-
const locks = {};
|
|
4876
|
-
for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
|
|
4877
|
-
if (Object.keys(locks).length) base.locks = locks;
|
|
4878
|
-
}
|
|
4879
|
-
const effects = deserializeEffects(r.effects);
|
|
4880
|
-
if (effects && r.type !== "adjustment") base.effects = effects;
|
|
4881
|
-
switch (r.type) {
|
|
4882
|
-
case "pixel": {
|
|
4883
|
-
const layer = {
|
|
4884
|
-
...base,
|
|
4885
|
-
type: "pixel",
|
|
4886
|
-
surface: r.source === "document" ? emptySurface(4) : readSurface(r.asset, r.x, r.y, 4, 0, ctx, `Layer "${base.name || id}"`),
|
|
4887
|
-
offset: { x: Math.round(num(r.offset?.x, -1e7, 1e7, 0)), y: Math.round(num(r.offset?.y, -1e7, 1e7, 0)) }
|
|
4888
|
-
};
|
|
4889
|
-
if (r.source === "document") layer.source = "document";
|
|
4890
|
-
if (typeof r.fillOpacity === "number" && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);
|
|
4891
|
-
const filters = readFilters(r.filters);
|
|
4892
|
-
if (filters) layer.filters = filters;
|
|
4893
|
-
return layer;
|
|
4894
|
-
}
|
|
4895
|
-
case "adjustment": {
|
|
4896
|
-
const norm = normalizeAdjustmentLayer({ id, kind: r.kind, params: r.params });
|
|
4897
|
-
if (!norm) {
|
|
4898
|
-
ctx.problems.push(`Layer "${base.name || id}": unknown adjustment kind "${r.kind}"`);
|
|
4899
|
-
return null;
|
|
4900
|
-
}
|
|
4901
|
-
return { ...base, type: "adjustment", kind: norm.kind, params: norm.params };
|
|
4902
|
-
}
|
|
4903
|
-
case "group": {
|
|
4904
|
-
const children = Array.isArray(r.children) ? r.children.map((c) => deserializeLayer(c, ctx)).filter(Boolean) : [];
|
|
4905
|
-
const blend = r.blend === "pass-through" ? "pass-through" : isBlendMode(r.blend) ? r.blend : "pass-through";
|
|
4906
|
-
const group = { ...base, type: "group", blend, children };
|
|
4907
|
-
if (r.collapsed === true) group.collapsed = true;
|
|
4908
|
-
return group;
|
|
4909
|
-
}
|
|
4910
|
-
case "annotation": {
|
|
4911
|
-
const a = r.annotation;
|
|
4912
|
-
if (!a || typeof a !== "object" || typeof a.type !== "string") {
|
|
4913
|
-
ctx.problems.push(`Layer "${base.name || id}": annotation is missing`);
|
|
4914
|
-
return null;
|
|
4915
|
-
}
|
|
4916
|
-
return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify({ ...a, id: typeof a.id === "string" ? a.id : id })) };
|
|
4917
|
-
}
|
|
4918
|
-
default:
|
|
4919
|
-
ctx.problems.push(`Layer "${base.name || id}": unknown layer type "${r.type}"`);
|
|
4920
|
-
return null;
|
|
4921
|
-
}
|
|
4768
|
+
function presetWarp(box, preset) {
|
|
4769
|
+
return warpPresetMesh(preset, box);
|
|
4922
4770
|
}
|
|
4923
|
-
function
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
seen.add(next.id);
|
|
4930
|
-
if (next.type === "group") next = { ...next, children: dedupe(next.children) };
|
|
4931
|
-
return next;
|
|
4932
|
-
});
|
|
4933
|
-
return dedupe(raw.map((l) => deserializeLayer(l, ctx)).filter(Boolean));
|
|
4771
|
+
function isValidWarp(mesh) {
|
|
4772
|
+
return isValidWarpMesh(mesh);
|
|
4773
|
+
}
|
|
4774
|
+
function geometryHomography(geometry) {
|
|
4775
|
+
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
4776
|
+
return m ? homographyFromAffine(m) : transformHomography(geometry);
|
|
4934
4777
|
}
|
|
4935
4778
|
|
|
4936
4779
|
// src/image-editor/layers/ops.ts
|
|
@@ -4991,6 +4834,10 @@ function withNewIds(layer) {
|
|
|
4991
4834
|
const next = { ...layer, id: newLayerId(layer.type === "adjustment" ? "adj" : layer.type) };
|
|
4992
4835
|
if (next.type === "group") next.children = next.children.map(withNewIds);
|
|
4993
4836
|
if (next.type === "annotation") next.annotation = { ...next.annotation, id: next.id };
|
|
4837
|
+
if (next.type === "smart" && next.psd?.placed) {
|
|
4838
|
+
next.psd = { ...next.psd };
|
|
4839
|
+
delete next.psd.placed;
|
|
4840
|
+
}
|
|
4994
4841
|
return next;
|
|
4995
4842
|
}
|
|
4996
4843
|
function duplicateLayer(layers, id, name) {
|
|
@@ -5016,7 +4863,7 @@ function setLayerProperties(layers, id, props) {
|
|
|
5016
4863
|
next.opacity = props.opacity;
|
|
5017
4864
|
}
|
|
5018
4865
|
if (props.fillOpacity !== void 0) {
|
|
5019
|
-
if (layer.type !== "pixel") fail(`fillOpacity applies to pixel layers; "${id}" is a ${layer.type} layer.`);
|
|
4866
|
+
if (layer.type !== "pixel" && layer.type !== "smart") fail(`fillOpacity applies to pixel layers and smart objects; "${id}" is a ${layer.type} layer.`);
|
|
5020
4867
|
if (!(typeof props.fillOpacity === "number" && props.fillOpacity >= 0 && props.fillOpacity <= 1)) fail("fillOpacity must be a number from 0 to 1.");
|
|
5021
4868
|
const p = next;
|
|
5022
4869
|
if (props.fillOpacity === 1) delete p.fillOpacity;
|
|
@@ -5165,89 +5012,1198 @@ function removeLayerFilter(layers, id, filterId) {
|
|
|
5165
5012
|
else delete next.filters;
|
|
5166
5013
|
return mapLayer(layers, id, () => next);
|
|
5167
5014
|
}
|
|
5168
|
-
function requireEffectLayer(layers, id, action) {
|
|
5169
|
-
const layer = requireLayer(layers, id).layer;
|
|
5170
|
-
if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
|
|
5171
|
-
requireUnlocked(layer, "all", action);
|
|
5172
|
-
return layer;
|
|
5015
|
+
function requireEffectLayer(layers, id, action) {
|
|
5016
|
+
const layer = requireLayer(layers, id).layer;
|
|
5017
|
+
if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
|
|
5018
|
+
requireUnlocked(layer, "all", action);
|
|
5019
|
+
return layer;
|
|
5020
|
+
}
|
|
5021
|
+
function layerEffectsOf(layer) {
|
|
5022
|
+
return layer.effects ?? { enabled: true, items: [] };
|
|
5023
|
+
}
|
|
5024
|
+
function guard(fn) {
|
|
5025
|
+
try {
|
|
5026
|
+
return fn();
|
|
5027
|
+
} catch (e) {
|
|
5028
|
+
if (e instanceof LayerEffectError) fail(e.message);
|
|
5029
|
+
throw e;
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
function setLayerEffects(layers, id, effects) {
|
|
5033
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5034
|
+
const next = { ...layer };
|
|
5035
|
+
if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
|
|
5036
|
+
guard(() => validateLayerEffects(effects));
|
|
5037
|
+
next.effects = effects;
|
|
5038
|
+
} else delete next.effects;
|
|
5039
|
+
return mapLayer(layers, id, () => next);
|
|
5040
|
+
}
|
|
5041
|
+
function addLayerEffect(layers, id, kind, params = {}, index) {
|
|
5042
|
+
if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
|
|
5043
|
+
const layer = requireEffectLayer(layers, id, "add effects");
|
|
5044
|
+
const current = layerEffectsOf(layer);
|
|
5045
|
+
const ids = new Set(current.items.map((e) => e.id));
|
|
5046
|
+
let effectId = newEffectId();
|
|
5047
|
+
while (ids.has(effectId)) effectId = newEffectId();
|
|
5048
|
+
const { id: _ignored, kind: _kind, ...rest } = params;
|
|
5049
|
+
const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
|
|
5050
|
+
const items = [...current.items];
|
|
5051
|
+
const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
|
|
5052
|
+
items.splice(at, 0, effect);
|
|
5053
|
+
return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
|
|
5054
|
+
}
|
|
5055
|
+
function requireEffect(layer, effectId) {
|
|
5056
|
+
const items = layer.effects?.items ?? [];
|
|
5057
|
+
const i = items.findIndex((e) => e.id === effectId);
|
|
5058
|
+
if (i === -1) fail(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
|
|
5059
|
+
return i;
|
|
5060
|
+
}
|
|
5061
|
+
function updateLayerEffect(layers, id, effectId, changes) {
|
|
5062
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5063
|
+
const i = requireEffect(layer, effectId);
|
|
5064
|
+
const current = layer.effects;
|
|
5065
|
+
const items = [...current.items];
|
|
5066
|
+
items[i] = guard(() => mergeLayerEffect(items[i], changes));
|
|
5067
|
+
return setLayerEffects(layers, id, { ...current, items });
|
|
5068
|
+
}
|
|
5069
|
+
function removeLayerEffect(layers, id, effectId) {
|
|
5070
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5071
|
+
requireEffect(layer, effectId);
|
|
5072
|
+
const current = layer.effects;
|
|
5073
|
+
return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
|
|
5074
|
+
}
|
|
5075
|
+
function setLayerEffectsEnabled(layers, id, enabled, scale) {
|
|
5076
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5077
|
+
if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
|
|
5078
|
+
const current = layerEffectsOf(layer);
|
|
5079
|
+
const next = { ...current, enabled };
|
|
5080
|
+
if (scale !== void 0) {
|
|
5081
|
+
if (scale === 1) delete next.scale;
|
|
5082
|
+
else next.scale = scale;
|
|
5083
|
+
}
|
|
5084
|
+
return setLayerEffects(layers, id, next);
|
|
5085
|
+
}
|
|
5086
|
+
function parseBlend(value, allowPassThrough = false) {
|
|
5087
|
+
if (value === void 0) return void 0;
|
|
5088
|
+
if (value === "pass-through" && allowPassThrough) return value;
|
|
5089
|
+
if (!isBlendMode(value)) fail(`Unknown blend mode "${String(value)}".`);
|
|
5090
|
+
return value;
|
|
5091
|
+
}
|
|
5092
|
+
function unionRect(a, b) {
|
|
5093
|
+
if (!a) return b;
|
|
5094
|
+
if (!b) return a;
|
|
5095
|
+
const x = Math.min(a.x, b.x);
|
|
5096
|
+
const y = Math.min(a.y, b.y);
|
|
5097
|
+
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 };
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
// src/image-editor/layers/smart.ts
|
|
5101
|
+
var fail2 = (message) => {
|
|
5102
|
+
throw new LayerOperationError(message);
|
|
5103
|
+
};
|
|
5104
|
+
var SMART_RENDER_MARGIN = MAX_EFFECT_REACH;
|
|
5105
|
+
function newContentsId() {
|
|
5106
|
+
return newLayerId("so");
|
|
5107
|
+
}
|
|
5108
|
+
function contentsBox(contents) {
|
|
5109
|
+
return { x: 0, y: 0, width: contents.width, height: contents.height };
|
|
5110
|
+
}
|
|
5111
|
+
function placementGeometry(layer) {
|
|
5112
|
+
return { box: contentsBox(layer.contents), corners: layer.placement.corners, warp: layer.placement.warp ?? null };
|
|
5113
|
+
}
|
|
5114
|
+
function placementAt(contents, x, y) {
|
|
5115
|
+
return { corners: boxQuad({ x, y, width: contents.width, height: contents.height }) };
|
|
5116
|
+
}
|
|
5117
|
+
function isContentsDecoded(contents) {
|
|
5118
|
+
return contents.kind === "raster" ? Boolean(contents.raster) : Boolean(contents.document);
|
|
5119
|
+
}
|
|
5120
|
+
function requireDecoded(layer, action) {
|
|
5121
|
+
if (isContentsDecoded(layer.contents)) return;
|
|
5122
|
+
const what = layer.contents.file?.type ? `a ${layer.contents.file.type.toUpperCase()} file` : "a file";
|
|
5123
|
+
fail2(
|
|
5124
|
+
`The contents of smart object "${layer.name || layer.id}" are ${what} that could not be decoded here, so it cannot ${action}. Run prepareSmartObjects() in a browser first, or rasterize it.`
|
|
5125
|
+
);
|
|
5126
|
+
}
|
|
5127
|
+
function requireSmartObject(layers, id) {
|
|
5128
|
+
const layer = requireLayer(layers, id).layer;
|
|
5129
|
+
if (layer.type !== "smart") fail2(`Layer "${layer.name || id}" is a ${layer.type} layer, not a smart object; convert it with convert_to_smart_object.`);
|
|
5130
|
+
return layer;
|
|
5131
|
+
}
|
|
5132
|
+
var contentsPixels = /* @__PURE__ */ new WeakMap();
|
|
5133
|
+
function contentsSurface(contents) {
|
|
5134
|
+
let s = contentsPixels.get(contents);
|
|
5135
|
+
if (s) return s;
|
|
5136
|
+
if (contents.kind === "raster") {
|
|
5137
|
+
s = contents.raster ?? emptySurface(4);
|
|
5138
|
+
} else {
|
|
5139
|
+
const doc = contents.document;
|
|
5140
|
+
if (!doc) return emptySurface(4);
|
|
5141
|
+
const W = contents.width;
|
|
5142
|
+
const H = contents.height;
|
|
5143
|
+
const data = new LayerCompositor().composite(doc.layers, { x: 0, y: 0, width: W, height: H }, {
|
|
5144
|
+
width: W,
|
|
5145
|
+
height: H,
|
|
5146
|
+
globalLight: doc.globalLight,
|
|
5147
|
+
pattern: (id) => doc.patterns?.find((p) => p.id === id) ?? null
|
|
5148
|
+
});
|
|
5149
|
+
s = surfaceFromPixels({ width: W, height: H, data }, 4, 0, 0);
|
|
5150
|
+
}
|
|
5151
|
+
contentsPixels.set(contents, s);
|
|
5152
|
+
return s;
|
|
5153
|
+
}
|
|
5154
|
+
function rasterContents(pixels, name, id = newContentsId()) {
|
|
5155
|
+
if (!(pixels.width > 0 && pixels.height > 0) || pixels.data.length < pixels.width * pixels.height * 4) fail2("Contents pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
|
|
5156
|
+
return { id, name, width: pixels.width, height: pixels.height, kind: "raster", raster: surfaceFromPixels(pixels, 4, 0, 0) };
|
|
5157
|
+
}
|
|
5158
|
+
function documentContents(document2, width, height, name, id = newContentsId()) {
|
|
5159
|
+
if (!(Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0)) fail2("Contents width and height must be positive whole numbers.");
|
|
5160
|
+
return { id, name, width, height, kind: "document", document: document2 };
|
|
5161
|
+
}
|
|
5162
|
+
var placedCache = /* @__PURE__ */ new WeakMap();
|
|
5163
|
+
var filteredCache = /* @__PURE__ */ new WeakMap();
|
|
5164
|
+
function placementKey(p, canvas) {
|
|
5165
|
+
return JSON.stringify([p.corners, p.warp ?? null, p.interpolation ?? "bicubic", canvas.width, canvas.height]);
|
|
5166
|
+
}
|
|
5167
|
+
function integerMove(layer) {
|
|
5168
|
+
const g = placementGeometry(layer);
|
|
5169
|
+
if (hasWarp(g)) return null;
|
|
5170
|
+
const q2 = boxQuad(g.box);
|
|
5171
|
+
const dx = g.corners[0].x - q2[0].x;
|
|
5172
|
+
const dy = g.corners[0].y - q2[0].y;
|
|
5173
|
+
if (Math.abs(dx - Math.round(dx)) > 1e-9 || Math.abs(dy - Math.round(dy)) > 1e-9) return null;
|
|
5174
|
+
const same = g.corners.every((c, i) => Math.abs(c.x - q2[i].x - dx) < 1e-9 && Math.abs(c.y - q2[i].y - dy) < 1e-9);
|
|
5175
|
+
return same ? { x: Math.round(dx), y: Math.round(dy) } : null;
|
|
5176
|
+
}
|
|
5177
|
+
function placeContents(layer, canvas, options = {}) {
|
|
5178
|
+
const { contents, placement } = layer;
|
|
5179
|
+
const preview = Boolean(options.step && options.step > 1) || Boolean(options.clip);
|
|
5180
|
+
const key = placementKey(placement, canvas);
|
|
5181
|
+
if (!preview) {
|
|
5182
|
+
const hit = placedCache.get(contents)?.get(key);
|
|
5183
|
+
if (hit) return hit;
|
|
5184
|
+
}
|
|
5185
|
+
const src = contentsSurface(contents);
|
|
5186
|
+
const m = SMART_RENDER_MARGIN;
|
|
5187
|
+
const area = { x: -m, y: -m, width: canvas.width + 2 * m, height: canvas.height + 2 * m };
|
|
5188
|
+
let clip = area;
|
|
5189
|
+
if (options.clip) {
|
|
5190
|
+
const c = options.clip;
|
|
5191
|
+
const x0 = Math.max(area.x, Math.floor(c.x));
|
|
5192
|
+
const y0 = Math.max(area.y, Math.floor(c.y));
|
|
5193
|
+
const x1 = Math.min(area.x + area.width, Math.ceil(c.x + c.width));
|
|
5194
|
+
const y1 = Math.min(area.y + area.height, Math.ceil(c.y + c.height));
|
|
5195
|
+
clip = { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
|
|
5196
|
+
}
|
|
5197
|
+
let out;
|
|
5198
|
+
const move = integerMove(layer);
|
|
5199
|
+
if (move && !options.clip) {
|
|
5200
|
+
out = translateSurface(src, move.x, move.y);
|
|
5201
|
+
} else {
|
|
5202
|
+
const g = placementGeometry(layer);
|
|
5203
|
+
const interpolation = preview ? placement.interpolation === "nearest" ? "nearest" : "bilinear" : placement.interpolation ?? "bicubic";
|
|
5204
|
+
const step = options.step && options.step > 1 ? options.step : 1;
|
|
5205
|
+
let source = src;
|
|
5206
|
+
let k = 1;
|
|
5207
|
+
if (step > 1) {
|
|
5208
|
+
k = step;
|
|
5209
|
+
let r = options.reduced?.get(k);
|
|
5210
|
+
if (!r) {
|
|
5211
|
+
r = reduceSurface(src, k);
|
|
5212
|
+
options.reduced?.set(k, r);
|
|
5213
|
+
}
|
|
5214
|
+
source = r;
|
|
5215
|
+
}
|
|
5216
|
+
const box = { x: 0, y: 0, width: Math.ceil(contents.width / k), height: Math.ceil(contents.height / k) };
|
|
5217
|
+
try {
|
|
5218
|
+
if (hasWarp(g)) {
|
|
5219
|
+
const f = transformMap(g);
|
|
5220
|
+
if (!f) fail2("The placement folds the contents over itself or through the horizon; keep the corners a convex quad.");
|
|
5221
|
+
out = warpSurface(source, (p) => f({ x: p.x * k, y: p.y * k }), { interpolation, source: box, clip, step: step > 1 ? step : void 0 });
|
|
5222
|
+
} else {
|
|
5223
|
+
const h = transformHomography(g);
|
|
5224
|
+
if (!h) fail2("The placement folds the contents over itself or through the horizon; keep the corners a convex quad.");
|
|
5225
|
+
const hk = multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], h);
|
|
5226
|
+
out = transformSurface(source, hk, { interpolation, source: box, clip, step: step > 1 ? step : void 0 });
|
|
5227
|
+
}
|
|
5228
|
+
} catch (e) {
|
|
5229
|
+
if (e instanceof TransformTooLargeError) fail2(e.message);
|
|
5230
|
+
throw e;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
if (!preview) {
|
|
5234
|
+
let byKey = placedCache.get(contents);
|
|
5235
|
+
if (!byKey) {
|
|
5236
|
+
byKey = /* @__PURE__ */ new Map();
|
|
5237
|
+
placedCache.set(contents, byKey);
|
|
5238
|
+
}
|
|
5239
|
+
byKey.set(key, out);
|
|
5240
|
+
while (byKey.size > 4) byKey.delete(byKey.keys().next().value);
|
|
5241
|
+
}
|
|
5242
|
+
return out;
|
|
5243
|
+
}
|
|
5244
|
+
function activeSmartFilters(stack) {
|
|
5245
|
+
if (!stack?.enabled) return [];
|
|
5246
|
+
return stack.filters.filter((f) => f.kind !== "psd" && isActiveFilter(f) && f.opacity > 0);
|
|
5247
|
+
}
|
|
5248
|
+
function filterMaskCoverage(mask, canvas) {
|
|
5249
|
+
if (!mask || mask.enabled === false || mask.source) return null;
|
|
5250
|
+
const W = canvas.width;
|
|
5251
|
+
const H = canvas.height;
|
|
5252
|
+
const bytes = readRegion(mask.surface, { x: 0, y: 0, width: W, height: H });
|
|
5253
|
+
let v = new Float32Array(W * H);
|
|
5254
|
+
let all = true;
|
|
5255
|
+
for (let i = 0; i < v.length; i++) {
|
|
5256
|
+
v[i] = bytes[i] / 255;
|
|
5257
|
+
if (bytes[i] !== 255) all = false;
|
|
5258
|
+
}
|
|
5259
|
+
const feather = mask.feather ?? 0;
|
|
5260
|
+
if (all && !(feather > 0)) return null;
|
|
5261
|
+
if (feather > 0) v = gaussianBlur(v, W, H, feather, "clamp");
|
|
5262
|
+
const density = mask.density ?? 1;
|
|
5263
|
+
if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);
|
|
5264
|
+
return v;
|
|
5265
|
+
}
|
|
5266
|
+
function applySmartFilters(placed, stack, canvas) {
|
|
5267
|
+
const active = activeSmartFilters(stack);
|
|
5268
|
+
if (!active.length || !(canvas.width > 0 && canvas.height > 0)) return placed;
|
|
5269
|
+
const W = canvas.width;
|
|
5270
|
+
const H = canvas.height;
|
|
5271
|
+
const key = JSON.stringify([W, H, active.map((f) => ({ ...f, id: void 0 }))]);
|
|
5272
|
+
let entry = filteredCache.get(placed);
|
|
5273
|
+
if (!entry || entry.key !== key) {
|
|
5274
|
+
const base = readRegion(placed, { x: 0, y: 0, width: W, height: H });
|
|
5275
|
+
let acc = new Uint8ClampedArray(base);
|
|
5276
|
+
for (const f of active) {
|
|
5277
|
+
const out = new Uint8ClampedArray(acc);
|
|
5278
|
+
runLayerFilters(out, W, H, [f]);
|
|
5279
|
+
if (f.blend === "normal") {
|
|
5280
|
+
if (f.opacity >= 1) acc = out;
|
|
5281
|
+
else lerpInto(acc, out, f.opacity, null);
|
|
5282
|
+
} else {
|
|
5283
|
+
blendTile(acc, out, f.blend, f.opacity, null);
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
entry = { key, base, filtered: acc };
|
|
5287
|
+
filteredCache.set(placed, entry);
|
|
5288
|
+
}
|
|
5289
|
+
const cov = filterMaskCoverage(stack.mask, canvas);
|
|
5290
|
+
let result = entry.filtered;
|
|
5291
|
+
if (cov) {
|
|
5292
|
+
result = new Uint8ClampedArray(entry.base);
|
|
5293
|
+
lerpInto(result, entry.filtered, 1, cov);
|
|
5294
|
+
}
|
|
5295
|
+
return surfaceFromPixels({ width: W, height: H, data: result }, 4, 0, 0);
|
|
5296
|
+
}
|
|
5297
|
+
function lerpInto(dst, src, k, coverage) {
|
|
5298
|
+
for (let i = 0, p = 0; i < dst.length; i += 4, p++) {
|
|
5299
|
+
const t = coverage ? k * coverage[p] : k;
|
|
5300
|
+
if (t <= 0) continue;
|
|
5301
|
+
if (t >= 1) {
|
|
5302
|
+
dst[i] = src[i];
|
|
5303
|
+
dst[i + 1] = src[i + 1];
|
|
5304
|
+
dst[i + 2] = src[i + 2];
|
|
5305
|
+
dst[i + 3] = src[i + 3];
|
|
5306
|
+
continue;
|
|
5307
|
+
}
|
|
5308
|
+
for (let c = 0; c < 4; c++) dst[i + c] = Math.round(dst[i + c] + (src[i + c] - dst[i + c]) * t);
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
function renderSmartObject(layer, canvas, options = {}) {
|
|
5312
|
+
const placed = placeContents(layer, canvas, options);
|
|
5313
|
+
return options.skipFilters ? placed : applySmartFilters(placed, layer.smartFilters, canvas);
|
|
5314
|
+
}
|
|
5315
|
+
function rerender(layer, canvas, action = "change") {
|
|
5316
|
+
requireDecoded(layer, action);
|
|
5317
|
+
return { ...layer, surface: renderSmartObject(layer, canvas) };
|
|
5318
|
+
}
|
|
5319
|
+
function normalizePlacement(raw, contents) {
|
|
5320
|
+
const c = raw?.corners;
|
|
5321
|
+
if (!Array.isArray(c) || c.length !== 4 || !c.every((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y))) {
|
|
5322
|
+
fail2("corners must be four finite points [top-left, top-right, bottom-right, bottom-left] in document pixels.");
|
|
5323
|
+
}
|
|
5324
|
+
const out = { corners: c.map((p) => ({ x: p.x, y: p.y })) };
|
|
5325
|
+
if (raw.warp) {
|
|
5326
|
+
if (!isValidWarpMesh(raw.warp)) fail2("warp must be a mesh with us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points in contents pixels.");
|
|
5327
|
+
out.warp = { us: [...raw.warp.us], vs: [...raw.warp.vs], points: raw.warp.points.map((p) => ({ x: p.x, y: p.y })) };
|
|
5328
|
+
}
|
|
5329
|
+
if (raw.interpolation !== void 0) {
|
|
5330
|
+
if (!isInterpolation(raw.interpolation)) fail2(`Unknown interpolation "${String(raw.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
5331
|
+
if (raw.interpolation !== "bicubic") out.interpolation = raw.interpolation;
|
|
5332
|
+
}
|
|
5333
|
+
const g = { box: { x: 0, y: 0, width: contents.width, height: contents.height }, corners: out.corners, warp: out.warp ?? null };
|
|
5334
|
+
if (!transformHomography(g)) fail2("These corners fold the contents over themselves or through the horizon; keep the quad convex.");
|
|
5335
|
+
if (out.warp && !hasWarp(g)) delete out.warp;
|
|
5336
|
+
return out;
|
|
5337
|
+
}
|
|
5338
|
+
function placementHomography(layer) {
|
|
5339
|
+
const g = placementGeometry(layer);
|
|
5340
|
+
return hasWarp(g) ? null : transformHomography(g);
|
|
5341
|
+
}
|
|
5342
|
+
function followMask(mask, before, after) {
|
|
5343
|
+
if (!mask || mask.linked === false || mask.source) return mask;
|
|
5344
|
+
const h0 = placementHomography(before);
|
|
5345
|
+
const h1 = placementHomography(after);
|
|
5346
|
+
if (!h0 || !h1) return mask;
|
|
5347
|
+
const inv = invertHomography(h0);
|
|
5348
|
+
if (!inv) return mask;
|
|
5349
|
+
const delta = multiplyHomographies(inv, h1);
|
|
5350
|
+
const surface = transformSurface(mask.surface, delta, { interpolation: "bilinear" });
|
|
5351
|
+
return { ...mask, surface };
|
|
5352
|
+
}
|
|
5353
|
+
function setSmartPlacement(layers, id, placement, canvas) {
|
|
5354
|
+
const layer = requireSmartObject(layers, id);
|
|
5355
|
+
requireUnlocked(layer, "position", "transform it");
|
|
5356
|
+
requireDecoded(layer, "be transformed");
|
|
5357
|
+
const clean = normalizePlacement(placement, layer.contents);
|
|
5358
|
+
let next = { ...layer, placement: clean };
|
|
5359
|
+
let masksMoved = false;
|
|
5360
|
+
if (layer.mask) {
|
|
5361
|
+
const m = followMask(layer.mask, layer, next);
|
|
5362
|
+
masksMoved ||= m !== layer.mask;
|
|
5363
|
+
next.mask = m;
|
|
5364
|
+
}
|
|
5365
|
+
if (next.smartFilters?.mask) {
|
|
5366
|
+
const m = followMask(next.smartFilters.mask, layer, next);
|
|
5367
|
+
masksMoved ||= m !== next.smartFilters.mask;
|
|
5368
|
+
next.smartFilters = { ...next.smartFilters, mask: m };
|
|
5369
|
+
}
|
|
5370
|
+
next = rerender(next, canvas, "be transformed");
|
|
5371
|
+
return { layers: mapLayer(layers, id, () => next), layer: next, masksMoved };
|
|
5372
|
+
}
|
|
5373
|
+
function offsetSmartObject(layers, id, dx, dy, canvas) {
|
|
5374
|
+
const layer = requireSmartObject(layers, id);
|
|
5375
|
+
requireUnlocked(layer, "position", "move it");
|
|
5376
|
+
const mx = Math.round(dx);
|
|
5377
|
+
const my = Math.round(dy);
|
|
5378
|
+
if (!mx && !my) return layers;
|
|
5379
|
+
const corners = layer.placement.corners.map((p) => ({ x: p.x + mx, y: p.y + my }));
|
|
5380
|
+
let next = { ...layer, placement: { ...layer.placement, corners } };
|
|
5381
|
+
const moveMask = (m2) => m2 && m2.linked !== false && !m2.source ? { ...m2, surface: translateSurface(m2.surface, mx, my) } : m2;
|
|
5382
|
+
if (layer.mask) next.mask = moveMask(layer.mask);
|
|
5383
|
+
if (layer.smartFilters?.mask) next.smartFilters = { ...layer.smartFilters, mask: moveMask(layer.smartFilters.mask) };
|
|
5384
|
+
const spatial = activeSmartFilters(layer.smartFilters).some((f) => f.kind === "blur");
|
|
5385
|
+
const b = contentBounds(layer.surface);
|
|
5386
|
+
const m = SMART_RENDER_MARGIN;
|
|
5387
|
+
const cut = b && (b.x <= -m || b.y <= -m || b.x + b.width >= canvas.width + m || b.y + b.height >= canvas.height + m);
|
|
5388
|
+
if (!spatial && !cut && !(next.smartFilters?.mask && activeSmartFilters(next.smartFilters).length && next.smartFilters.mask.linked === false)) {
|
|
5389
|
+
next = { ...next, surface: translateSurface(layer.surface, mx, my) };
|
|
5390
|
+
} else {
|
|
5391
|
+
next = rerender(next, canvas, "move");
|
|
5392
|
+
}
|
|
5393
|
+
return mapLayer(layers, id, () => next);
|
|
5394
|
+
}
|
|
5395
|
+
function translateLayer(layer, dx, dy) {
|
|
5396
|
+
if (!dx && !dy) return layer;
|
|
5397
|
+
const moveMask = (m) => {
|
|
5398
|
+
if (!m) return m;
|
|
5399
|
+
if (m.source) fail2(`Layer "${layer.name || layer.id}" has a depth mask, which cannot move into a smart object; bake or remove it first.`);
|
|
5400
|
+
return { ...m, surface: translateSurface(m.surface, dx, dy) };
|
|
5401
|
+
};
|
|
5402
|
+
switch (layer.type) {
|
|
5403
|
+
case "pixel":
|
|
5404
|
+
return { ...layer, offset: { x: layer.offset.x + dx, y: layer.offset.y + dy } };
|
|
5405
|
+
case "smart": {
|
|
5406
|
+
const next = {
|
|
5407
|
+
...layer,
|
|
5408
|
+
placement: { ...layer.placement, corners: layer.placement.corners.map((p) => ({ x: p.x + dx, y: p.y + dy })) },
|
|
5409
|
+
surface: translateSurface(layer.surface, dx, dy),
|
|
5410
|
+
mask: moveMask(layer.mask)
|
|
5411
|
+
};
|
|
5412
|
+
if (!next.mask) delete next.mask;
|
|
5413
|
+
if (layer.smartFilters?.mask) next.smartFilters = { ...layer.smartFilters, mask: moveMask(layer.smartFilters.mask) };
|
|
5414
|
+
return next;
|
|
5415
|
+
}
|
|
5416
|
+
case "group": {
|
|
5417
|
+
const next = { ...layer, children: layer.children.map((c) => translateLayer(c, dx, dy)), mask: moveMask(layer.mask) };
|
|
5418
|
+
if (!next.mask) delete next.mask;
|
|
5419
|
+
return next;
|
|
5420
|
+
}
|
|
5421
|
+
case "adjustment": {
|
|
5422
|
+
const next = { ...layer, mask: moveMask(layer.mask) };
|
|
5423
|
+
if (!next.mask) delete next.mask;
|
|
5424
|
+
return next;
|
|
5425
|
+
}
|
|
5426
|
+
case "annotation":
|
|
5427
|
+
return fail2(`Annotation layer "${layer.name || layer.id}" cannot go into a smart object; rasterize it first.`);
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
function layerExtent(layer, surfaceOf, canvas) {
|
|
5431
|
+
const full = { x: 0, y: 0, width: canvas.width, height: canvas.height };
|
|
5432
|
+
let b = null;
|
|
5433
|
+
if (layer.type === "pixel" || layer.type === "smart") {
|
|
5434
|
+
const s = layer.type === "pixel" ? surfaceOf(layer) : layer.surface;
|
|
5435
|
+
if (s.defaultValue !== 0) b = full;
|
|
5436
|
+
else {
|
|
5437
|
+
const c = contentBounds(s);
|
|
5438
|
+
b = c ? { x: c.x + layer.offset.x, y: c.y + layer.offset.y, width: c.width, height: c.height } : null;
|
|
5439
|
+
}
|
|
5440
|
+
} else if (layer.type === "group") {
|
|
5441
|
+
for (const c of layer.children) b = union(b, layerExtent(c, surfaceOf, canvas));
|
|
5442
|
+
} else {
|
|
5443
|
+
b = full;
|
|
5444
|
+
}
|
|
5445
|
+
const m = effectsMargin(layer.effects);
|
|
5446
|
+
if (b && m > 0) b = { x: b.x - Math.ceil(m), y: b.y - Math.ceil(m), width: b.width + 2 * Math.ceil(m), height: b.height + 2 * Math.ceil(m) };
|
|
5447
|
+
return b;
|
|
5448
|
+
}
|
|
5449
|
+
function union(a, b) {
|
|
5450
|
+
if (!a) return b;
|
|
5451
|
+
if (!b) return a;
|
|
5452
|
+
const x = Math.min(a.x, b.x);
|
|
5453
|
+
const y = Math.min(a.y, b.y);
|
|
5454
|
+
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 };
|
|
5455
|
+
}
|
|
5456
|
+
function asSmartFilters(filters) {
|
|
5457
|
+
if (!filters?.length) return void 0;
|
|
5458
|
+
return { enabled: true, filters: filters.map((f) => ({ ...f, blend: "normal", opacity: 1 })) };
|
|
5459
|
+
}
|
|
5460
|
+
function convertToSmartObject(layers, ids, canvas, options) {
|
|
5461
|
+
if (!ids.length) fail2("Give at least one layer id to convert.");
|
|
5462
|
+
const locs = [...new Set(ids)].map((id) => requireLayer(layers, id));
|
|
5463
|
+
const parent = locs[0].parent?.id ?? null;
|
|
5464
|
+
if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail2("Layers to convert must share one parent; move them next to each other first.");
|
|
5465
|
+
for (const l of locs) requireUnlocked(l.layer, "all", "convert it");
|
|
5466
|
+
const ordered = locs.sort((a, b) => a.index - b.index);
|
|
5467
|
+
const top = ordered[ordered.length - 1];
|
|
5468
|
+
let smart;
|
|
5469
|
+
const single = ordered.length === 1 ? ordered[0].layer : null;
|
|
5470
|
+
if (single && single.type === "pixel") {
|
|
5471
|
+
const surface = options.surfaceOf(single);
|
|
5472
|
+
if (surface.defaultValue !== 0) fail2(`Layer "${single.name || single.id}" has no transparent area to crop to; it cannot be converted.`);
|
|
5473
|
+
const b = contentBounds(surface);
|
|
5474
|
+
if (!b) fail2(`Layer "${single.name || single.id}" has no pixels to convert; paint something first.`);
|
|
5475
|
+
const contents = {
|
|
5476
|
+
id: newContentsId(),
|
|
5477
|
+
name: `${single.name || "Layer"}.png`,
|
|
5478
|
+
width: b.width,
|
|
5479
|
+
height: b.height,
|
|
5480
|
+
kind: "raster",
|
|
5481
|
+
raster: translateSurface(surface, -b.x, -b.y)
|
|
5482
|
+
};
|
|
5483
|
+
smart = {
|
|
5484
|
+
type: "smart",
|
|
5485
|
+
id: single.id,
|
|
5486
|
+
name: single.name,
|
|
5487
|
+
visible: single.visible,
|
|
5488
|
+
opacity: single.opacity,
|
|
5489
|
+
blend: single.blend,
|
|
5490
|
+
contents,
|
|
5491
|
+
placement: placementAt(contents, b.x + single.offset.x, b.y + single.offset.y),
|
|
5492
|
+
surface: emptySurface(4),
|
|
5493
|
+
offset: { x: 0, y: 0 }
|
|
5494
|
+
};
|
|
5495
|
+
if (single.clipped) smart.clipped = true;
|
|
5496
|
+
if (single.fillOpacity !== void 0) smart.fillOpacity = single.fillOpacity;
|
|
5497
|
+
if (single.effects) smart.effects = single.effects;
|
|
5498
|
+
if (single.mask) {
|
|
5499
|
+
smart.mask = single.mask.source ? single.mask : { ...single.mask, surface: translateSurface(single.mask.surface, single.offset.x, single.offset.y) };
|
|
5500
|
+
}
|
|
5501
|
+
const filters = asSmartFilters(single.filters);
|
|
5502
|
+
if (filters) smart.smartFilters = filters;
|
|
5503
|
+
} else {
|
|
5504
|
+
let b = null;
|
|
5505
|
+
for (const l of ordered) b = union(b, layerExtent(l.layer, options.surfaceOf, canvas));
|
|
5506
|
+
if (!b || !(b.width > 0 && b.height > 0)) fail2("The layers have no pixels to convert; paint something first.");
|
|
5507
|
+
const nested = ordered.map((l) => {
|
|
5508
|
+
let layer = l.layer;
|
|
5509
|
+
if (layer.type === "pixel" && layer.source === "document") {
|
|
5510
|
+
const resolved = { ...layer, surface: options.surfaceOf(layer) };
|
|
5511
|
+
delete resolved.source;
|
|
5512
|
+
layer = resolved;
|
|
5513
|
+
}
|
|
5514
|
+
if (flattenLayers([layer]).some((x) => x.type === "pixel" && x.source === "document")) fail2(`Layer "${layer.name || layer.id}" holds the document image inside a group; convert the Background on its own.`);
|
|
5515
|
+
return translateLayer(layer, -b.x, -b.y);
|
|
5516
|
+
});
|
|
5517
|
+
if (nested[0].clipped) {
|
|
5518
|
+
const first = { ...nested[0] };
|
|
5519
|
+
delete first.clipped;
|
|
5520
|
+
nested[0] = first;
|
|
5521
|
+
}
|
|
5522
|
+
const name = options.name?.trim() || top.layer.name || "Smart Object";
|
|
5523
|
+
const contents = documentContents({ layers: nested }, b.width, b.height, `${name}.psb`);
|
|
5524
|
+
smart = {
|
|
5525
|
+
type: "smart",
|
|
5526
|
+
id: ordered.length === 1 ? ordered[0].layer.id : newLayerId("smart"),
|
|
5527
|
+
name,
|
|
5528
|
+
visible: true,
|
|
5529
|
+
opacity: 1,
|
|
5530
|
+
blend: "normal",
|
|
5531
|
+
contents,
|
|
5532
|
+
placement: placementAt(contents, b.x, b.y),
|
|
5533
|
+
surface: emptySurface(4),
|
|
5534
|
+
offset: { x: 0, y: 0 }
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
5537
|
+
if (options.name?.trim()) smart.name = options.name.trim();
|
|
5538
|
+
smart = { ...smart, surface: renderSmartObject(smart, canvas) };
|
|
5539
|
+
const below = ordered.filter((l) => l.index < top.index).length;
|
|
5540
|
+
let out = layers;
|
|
5541
|
+
for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);
|
|
5542
|
+
return { layers: insertLayer(out, smart, parent, top.index - below), layer: smart };
|
|
5543
|
+
}
|
|
5544
|
+
function rasterizeSmartObject(layers, id) {
|
|
5545
|
+
const layer = requireSmartObject(layers, id);
|
|
5546
|
+
requireUnlocked(layer, "all", "rasterize it");
|
|
5547
|
+
const out = {
|
|
5548
|
+
type: "pixel",
|
|
5549
|
+
id: layer.id,
|
|
5550
|
+
name: layer.name,
|
|
5551
|
+
visible: layer.visible,
|
|
5552
|
+
opacity: layer.opacity,
|
|
5553
|
+
blend: layer.blend,
|
|
5554
|
+
surface: layer.surface,
|
|
5555
|
+
offset: { x: 0, y: 0 }
|
|
5556
|
+
};
|
|
5557
|
+
if (layer.clipped) out.clipped = true;
|
|
5558
|
+
if (layer.mask) out.mask = layer.mask;
|
|
5559
|
+
if (layer.locks) out.locks = layer.locks;
|
|
5560
|
+
if (layer.effects) out.effects = layer.effects;
|
|
5561
|
+
if (layer.fillOpacity !== void 0) out.fillOpacity = layer.fillOpacity;
|
|
5562
|
+
return { layers: mapLayer(layers, id, () => out), layer: out };
|
|
5563
|
+
}
|
|
5564
|
+
function copySmartObject(layers, id, linked, name) {
|
|
5565
|
+
const loc = requireLayer(layers, id);
|
|
5566
|
+
const layer = requireSmartObject(layers, id);
|
|
5567
|
+
const copy = { ...layer, id: newLayerId("smart"), name: name?.trim() || `${layer.name || "Smart Object"} copy` };
|
|
5568
|
+
delete copy.locks;
|
|
5569
|
+
delete copy.psd;
|
|
5570
|
+
if (!linked) copy.contents = { ...layer.contents, id: newContentsId() };
|
|
5571
|
+
if (copy.smartFilters) copy.smartFilters = { ...copy.smartFilters, filters: copy.smartFilters.filters.map((f) => ({ ...f, id: newLayerId("flt") })) };
|
|
5572
|
+
return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), layer: copy };
|
|
5573
|
+
}
|
|
5574
|
+
function instancesOf(layers, contentsId) {
|
|
5575
|
+
return flattenLayers(layers).filter((l) => l.type === "smart" && l.contents.id === contentsId).map((l) => l.id);
|
|
5576
|
+
}
|
|
5577
|
+
function replaceContents(layers, contentsId, contents, canvas) {
|
|
5578
|
+
const ids = instancesOf(layers, contentsId);
|
|
5579
|
+
if (!ids.length) fail2(`No smart object uses contents "${contentsId}".`);
|
|
5580
|
+
for (const id of ids) requireUnlocked(findLayer(layers, id).layer, "all", "replace its contents");
|
|
5581
|
+
const next = { ...contents, id: contentsId };
|
|
5582
|
+
let out = layers;
|
|
5583
|
+
for (const id of ids) {
|
|
5584
|
+
out = mapLayer(out, id, (l) => {
|
|
5585
|
+
const layer = l;
|
|
5586
|
+
return rerender({ ...layer, contents: next, placement: refitPlacement(layer, next) }, canvas, "take the new contents");
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
return { layers: out, ids };
|
|
5590
|
+
}
|
|
5591
|
+
function refitPlacement(layer, contents) {
|
|
5592
|
+
const W0 = layer.contents.width;
|
|
5593
|
+
const H0 = layer.contents.height;
|
|
5594
|
+
const W1 = contents.width;
|
|
5595
|
+
const H1 = contents.height;
|
|
5596
|
+
if (W0 === W1 && H0 === H1) return layer.placement;
|
|
5597
|
+
const g = placementGeometry(layer);
|
|
5598
|
+
const h = transformHomography(g);
|
|
5599
|
+
if (!h) return placementAt(contents, 0, 0);
|
|
5600
|
+
const shift = [1, 0, (W0 - W1) / 2, 0, 1, (H0 - H1) / 2, 0, 0, 1];
|
|
5601
|
+
const hn = multiplyHomographies(shift, h);
|
|
5602
|
+
const pts = boxQuad({ x: 0, y: 0, width: W1, height: H1 }).map((p) => {
|
|
5603
|
+
const w = hn[6] * p.x + hn[7] * p.y + hn[8];
|
|
5604
|
+
return { x: (hn[0] * p.x + hn[1] * p.y + hn[2]) / w, y: (hn[3] * p.x + hn[4] * p.y + hn[5]) / w };
|
|
5605
|
+
});
|
|
5606
|
+
const out = { corners: pts };
|
|
5607
|
+
if (layer.placement.interpolation) out.interpolation = layer.placement.interpolation;
|
|
5608
|
+
if (layer.placement.warp) out.warp = mapWarpMeshPoints(layer.placement.warp, (p) => ({ x: p.x * W1 / W0, y: p.y * H1 / H0 }));
|
|
5609
|
+
return out;
|
|
5610
|
+
}
|
|
5611
|
+
function normalizeSmartFilter(raw, id = newLayerId("flt")) {
|
|
5612
|
+
if (raw.kind === "psd") fail2("Photoshop filters can only come from a PSD; add develop or blur.");
|
|
5613
|
+
const base = normalizeFilter(raw, id);
|
|
5614
|
+
const blend = raw.blend ?? "normal";
|
|
5615
|
+
if (!isBlendMode(blend) || blend === "pigment") fail2(`Unknown blend mode "${String(blend)}" for a smart filter.`);
|
|
5616
|
+
const opacity = raw.opacity ?? 1;
|
|
5617
|
+
if (!(typeof opacity === "number" && opacity >= 0 && opacity <= 1)) fail2("opacity must be a number from 0 to 1.");
|
|
5618
|
+
return { ...base, blend, opacity };
|
|
5619
|
+
}
|
|
5620
|
+
function withStack(layer, stack, canvas) {
|
|
5621
|
+
const next = { ...layer };
|
|
5622
|
+
if (stack && (stack.filters.length || stack.mask || !stack.enabled)) next.smartFilters = stack;
|
|
5623
|
+
else delete next.smartFilters;
|
|
5624
|
+
return rerender(next, canvas, "change its smart filters");
|
|
5625
|
+
}
|
|
5626
|
+
function stackOf(layer) {
|
|
5627
|
+
return layer.smartFilters ?? { enabled: true, filters: [] };
|
|
5628
|
+
}
|
|
5629
|
+
function requireFilter(layer, filterId) {
|
|
5630
|
+
const list2 = layer.smartFilters?.filters ?? [];
|
|
5631
|
+
const i = list2.findIndex((f) => f.id === filterId);
|
|
5632
|
+
if (i === -1) fail2(`Smart object "${layer.name || layer.id}" has no smart filter "${filterId}". Filters: ${list2.map((f) => `${f.id} (${f.kind})`).join(", ") || "none"}; call get_smart_object.`);
|
|
5633
|
+
return i;
|
|
5634
|
+
}
|
|
5635
|
+
function editableSmart(layers, id) {
|
|
5636
|
+
const layer = requireSmartObject(layers, id);
|
|
5637
|
+
requireUnlocked(layer, "all", "change its smart filters");
|
|
5638
|
+
return layer;
|
|
5639
|
+
}
|
|
5640
|
+
function addSmartFilter(layers, id, filter, canvas, index) {
|
|
5641
|
+
const layer = editableSmart(layers, id);
|
|
5642
|
+
const stack = stackOf(layer);
|
|
5643
|
+
const filters = [...stack.filters];
|
|
5644
|
+
const at = index === void 0 ? filters.length : Math.max(0, Math.min(filters.length, Math.floor(index)));
|
|
5645
|
+
filters.splice(at, 0, filter);
|
|
5646
|
+
const next = withStack(layer, { ...stack, filters }, canvas);
|
|
5647
|
+
return { layers: mapLayer(layers, id, () => next), layer: next, filter };
|
|
5648
|
+
}
|
|
5649
|
+
function updateSmartFilter(layers, id, filterId, changes, canvas) {
|
|
5650
|
+
const layer = editableSmart(layers, id);
|
|
5651
|
+
const i = requireFilter(layer, filterId);
|
|
5652
|
+
const current = layer.smartFilters.filters[i];
|
|
5653
|
+
if ("kind" in changes && changes.kind !== current.kind) fail2("A smart filter kind cannot change; remove it and add another.");
|
|
5654
|
+
let merged;
|
|
5655
|
+
if (current.kind === "psd") {
|
|
5656
|
+
const extra = Object.keys(changes).filter((k) => !["enabled", "blend", "opacity", "kind"].includes(k));
|
|
5657
|
+
if (extra.length) fail2(`"${current.name}" is a Photoshop filter we keep but do not model; only enabled, blend and opacity change. Unknown: ${extra.join(", ")}.`);
|
|
5658
|
+
const blend = changes.blend ?? current.blend;
|
|
5659
|
+
if (!isBlendMode(blend) || blend === "pigment") fail2(`Unknown blend mode "${String(blend)}" for a smart filter.`);
|
|
5660
|
+
const opacity = changes.opacity ?? current.opacity;
|
|
5661
|
+
if (!(typeof opacity === "number" && opacity >= 0 && opacity <= 1)) fail2("opacity must be a number from 0 to 1.");
|
|
5662
|
+
merged = { ...current, enabled: changes.enabled === void 0 ? current.enabled : Boolean(changes.enabled), blend, opacity };
|
|
5663
|
+
} else {
|
|
5664
|
+
merged = normalizeSmartFilter({ ...current, ...changes, kind: current.kind }, current.id);
|
|
5665
|
+
}
|
|
5666
|
+
const filters = [...layer.smartFilters.filters];
|
|
5667
|
+
filters[i] = merged;
|
|
5668
|
+
const next = withStack(layer, { ...layer.smartFilters, filters }, canvas);
|
|
5669
|
+
return { layers: mapLayer(layers, id, () => next), layer: next, filter: merged };
|
|
5670
|
+
}
|
|
5671
|
+
function removeSmartFilter(layers, id, filterId, canvas) {
|
|
5672
|
+
const layer = editableSmart(layers, id);
|
|
5673
|
+
requireFilter(layer, filterId);
|
|
5674
|
+
const stack = layer.smartFilters;
|
|
5675
|
+
const filters = stack.filters.filter((f) => f.id !== filterId);
|
|
5676
|
+
const next = withStack(layer, filters.length ? { ...stack, filters } : stack.enabled ? void 0 : { enabled: false, filters }, canvas);
|
|
5677
|
+
return { layers: mapLayer(layers, id, () => next), layer: next };
|
|
5678
|
+
}
|
|
5679
|
+
function moveSmartFilter(layers, id, filterId, index, canvas) {
|
|
5680
|
+
const layer = editableSmart(layers, id);
|
|
5681
|
+
const i = requireFilter(layer, filterId);
|
|
5682
|
+
const list2 = layer.smartFilters.filters;
|
|
5683
|
+
if (!Number.isInteger(index) || index < 0 || index >= list2.length) fail2(`index must be a whole number from 0 to ${list2.length - 1}.`);
|
|
5684
|
+
const filters = [...list2];
|
|
5685
|
+
const [f] = filters.splice(i, 1);
|
|
5686
|
+
filters.splice(index, 0, f);
|
|
5687
|
+
const next = withStack(layer, { ...layer.smartFilters, filters }, canvas);
|
|
5688
|
+
return { layers: mapLayer(layers, id, () => next), layer: next };
|
|
5689
|
+
}
|
|
5690
|
+
function setSmartFiltersEnabled(layers, id, enabled, canvas) {
|
|
5691
|
+
const layer = editableSmart(layers, id);
|
|
5692
|
+
const next = withStack(layer, { ...stackOf(layer), enabled: Boolean(enabled) }, canvas);
|
|
5693
|
+
return { layers: mapLayer(layers, id, () => next), layer: next };
|
|
5694
|
+
}
|
|
5695
|
+
function setSmartFilterMask(layers, id, mask, canvas) {
|
|
5696
|
+
const layer = editableSmart(layers, id);
|
|
5697
|
+
if (mask?.source) fail2("A smart filter mask is painted, not generated; use a raster mask.");
|
|
5698
|
+
const stack = { ...stackOf(layer) };
|
|
5699
|
+
if (mask) stack.mask = mask;
|
|
5700
|
+
else delete stack.mask;
|
|
5701
|
+
const next = withStack(layer, stack, canvas);
|
|
5702
|
+
return { layers: mapLayer(layers, id, () => next), layer: next };
|
|
5703
|
+
}
|
|
5704
|
+
function withFilterMaskSurface(layer, surface, canvas) {
|
|
5705
|
+
const stack = stackOf(layer);
|
|
5706
|
+
if (!stack.mask) fail2(`Smart object "${layer.name || layer.id}" has no filter mask; add one with set_smart_filter_mask.`);
|
|
5707
|
+
return withStack(layer, { ...stack, mask: { ...stack.mask, surface } }, canvas);
|
|
5708
|
+
}
|
|
5709
|
+
function placementBounds(layer) {
|
|
5710
|
+
return transformedBox(placementGeometry(layer));
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
// src/image-editor/layers/serialize.ts
|
|
5714
|
+
var LAYERED_SCHEMA_VERSION = 3;
|
|
5715
|
+
function serializePatterns(patterns) {
|
|
5716
|
+
return patterns.map((p) => ({ id: p.id, name: p.name, png: pngDataUrl(p.width, p.height, p.data) }));
|
|
5717
|
+
}
|
|
5718
|
+
function deserializePatterns(raw, problems = []) {
|
|
5719
|
+
if (!Array.isArray(raw)) return [];
|
|
5720
|
+
const out = [];
|
|
5721
|
+
for (const p of raw) {
|
|
5722
|
+
if (!p || typeof p !== "object" || typeof p.id !== "string" || typeof p.png !== "string") continue;
|
|
5723
|
+
try {
|
|
5724
|
+
const png = decodePngDataUrl(p.png, 4);
|
|
5725
|
+
out.push({ id: p.id, name: typeof p.name === "string" ? p.name : p.id, width: png.width, height: png.height, data: png.data });
|
|
5726
|
+
} catch (e) {
|
|
5727
|
+
problems.push(`Pattern "${p.name ?? p.id}" could not be decoded (${e instanceof Error ? e.message : String(e)}).`);
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
return out;
|
|
5731
|
+
}
|
|
5732
|
+
var encodedCache = /* @__PURE__ */ new WeakMap();
|
|
5733
|
+
function encodeSurface(surface) {
|
|
5734
|
+
if (encodedCache.has(surface)) return encodedCache.get(surface);
|
|
5735
|
+
const b = contentBounds(surface);
|
|
5736
|
+
let out = null;
|
|
5737
|
+
if (b) {
|
|
5738
|
+
const data = readRegion(surface, b);
|
|
5739
|
+
out = { url: pngDataUrl(b.width, b.height, data, surface.channels), x: b.x, y: b.y };
|
|
5740
|
+
}
|
|
5741
|
+
encodedCache.set(surface, out);
|
|
5742
|
+
return out;
|
|
5743
|
+
}
|
|
5744
|
+
function baseFields(layer, ctx) {
|
|
5745
|
+
const out = {
|
|
5746
|
+
id: layer.id,
|
|
5747
|
+
name: layer.name,
|
|
5748
|
+
visible: layer.visible,
|
|
5749
|
+
opacity: layer.opacity,
|
|
5750
|
+
blend: layer.blend
|
|
5751
|
+
};
|
|
5752
|
+
if (layer.clipped) out.clipped = true;
|
|
5753
|
+
if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
|
|
5754
|
+
if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
|
|
5755
|
+
if (layer.effects) out.effects = serializeEffects(layer.effects);
|
|
5756
|
+
return out;
|
|
5757
|
+
}
|
|
5758
|
+
function serializeEffects(effects) {
|
|
5759
|
+
const out = { enabled: effects.enabled, items: JSON.parse(JSON.stringify(effects.items)) };
|
|
5760
|
+
if (effects.scale !== void 0 && effects.scale !== 1) out.scale = effects.scale;
|
|
5761
|
+
if (effects.psd) out.psd = { key: effects.psd.key, data: bytesToBase64(effects.psd.data) };
|
|
5762
|
+
return out;
|
|
5763
|
+
}
|
|
5764
|
+
function deserializeEffects(raw) {
|
|
5765
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
5766
|
+
const r = raw;
|
|
5767
|
+
let psd;
|
|
5768
|
+
const p = r.psd;
|
|
5769
|
+
if (p && typeof p.key === "string" && typeof p.data === "string") {
|
|
5770
|
+
try {
|
|
5771
|
+
psd = { key: p.key, data: base64ToBytes(p.data) };
|
|
5772
|
+
} catch {
|
|
5773
|
+
psd = void 0;
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
return normalizeLayerEffects({ ...r, psd });
|
|
5777
|
+
}
|
|
5778
|
+
function serializeMask(mask, hint, ctx) {
|
|
5779
|
+
const out = { defaultValue: mask.defaultValue };
|
|
5780
|
+
if (!mask.source) {
|
|
5781
|
+
const enc = encodeSurface(mask.surface);
|
|
5782
|
+
if (enc) {
|
|
5783
|
+
out.asset = ctx.addAsset(enc.url, hint);
|
|
5784
|
+
out.x = enc.x;
|
|
5785
|
+
out.y = enc.y;
|
|
5786
|
+
}
|
|
5787
|
+
} else {
|
|
5788
|
+
out.source = { ...mask.source, depthRange: [...mask.source.depthRange] };
|
|
5789
|
+
}
|
|
5790
|
+
if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
|
|
5791
|
+
if (mask.feather) out.feather = mask.feather;
|
|
5792
|
+
if (mask.enabled === false) out.enabled = false;
|
|
5793
|
+
if (mask.linked === false) out.linked = false;
|
|
5794
|
+
return out;
|
|
5795
|
+
}
|
|
5796
|
+
function serializeLayer(layer, ctx) {
|
|
5797
|
+
const base = baseFields(layer, ctx);
|
|
5798
|
+
switch (layer.type) {
|
|
5799
|
+
case "pixel": {
|
|
5800
|
+
const out = { ...base, type: "pixel", offset: { ...layer.offset } };
|
|
5801
|
+
if (layer.source === "document") out.source = "document";
|
|
5802
|
+
else {
|
|
5803
|
+
const enc = encodeSurface(layer.surface);
|
|
5804
|
+
if (enc) {
|
|
5805
|
+
out.asset = ctx.addAsset(enc.url, layer.id);
|
|
5806
|
+
out.x = enc.x;
|
|
5807
|
+
out.y = enc.y;
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
if (layer.fillOpacity !== void 0 && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;
|
|
5811
|
+
if (layer.filters?.length) out.filters = JSON.parse(JSON.stringify(layer.filters));
|
|
5812
|
+
return out;
|
|
5813
|
+
}
|
|
5814
|
+
case "adjustment":
|
|
5815
|
+
return { ...base, type: "adjustment", kind: layer.kind, params: JSON.parse(JSON.stringify(layer.params)) };
|
|
5816
|
+
case "group": {
|
|
5817
|
+
const out = { ...base, type: "group", children: layer.children.map((c) => serializeLayer(c, ctx)) };
|
|
5818
|
+
if (layer.collapsed) out.collapsed = true;
|
|
5819
|
+
return out;
|
|
5820
|
+
}
|
|
5821
|
+
case "annotation":
|
|
5822
|
+
return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify(layer.annotation)) };
|
|
5823
|
+
case "smart":
|
|
5824
|
+
return serializeSmartObject(layer, base, ctx);
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
function serializeSmartFilters(stack, layerId, ctx) {
|
|
5828
|
+
const out = {
|
|
5829
|
+
enabled: stack.enabled,
|
|
5830
|
+
filters: stack.filters.map((f) => f.kind === "psd" ? { ...f, data: bytesToBase64(f.data) } : JSON.parse(JSON.stringify(f)))
|
|
5831
|
+
};
|
|
5832
|
+
if (stack.mask) out.mask = serializeMask(stack.mask, `${layerId}-filter-mask`, ctx);
|
|
5833
|
+
return out;
|
|
5834
|
+
}
|
|
5835
|
+
function serializeSmartObject(layer, base, ctx) {
|
|
5836
|
+
if (!ctx.addContents) throw new Error("Smart objects need a contents table to be written.");
|
|
5837
|
+
ctx.addContents(layer.contents);
|
|
5838
|
+
const p = layer.placement;
|
|
5839
|
+
const out = {
|
|
5840
|
+
...base,
|
|
5841
|
+
type: "smart",
|
|
5842
|
+
contents: layer.contents.id,
|
|
5843
|
+
placement: { corners: p.corners.map((c) => ({ x: c.x, y: c.y })) }
|
|
5844
|
+
};
|
|
5845
|
+
if (p.warp) out.placement.warp = JSON.parse(JSON.stringify(p.warp));
|
|
5846
|
+
if (p.interpolation && p.interpolation !== "bicubic") out.placement.interpolation = p.interpolation;
|
|
5847
|
+
if (layer.smartFilters) out.smartFilters = serializeSmartFilters(layer.smartFilters, layer.id, ctx);
|
|
5848
|
+
const enc = encodeSurface(layer.surface);
|
|
5849
|
+
if (enc) {
|
|
5850
|
+
out.asset = ctx.addAsset(enc.url, layer.id);
|
|
5851
|
+
out.x = enc.x;
|
|
5852
|
+
out.y = enc.y;
|
|
5853
|
+
}
|
|
5854
|
+
if (layer.fillOpacity !== void 0 && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;
|
|
5855
|
+
if (layer.psd) {
|
|
5856
|
+
out.psd = { key: layer.psd.key, data: bytesToBase64(layer.psd.data) };
|
|
5857
|
+
if (layer.psd.placed) out.psd.placed = layer.psd.placed;
|
|
5858
|
+
}
|
|
5859
|
+
return out;
|
|
5860
|
+
}
|
|
5861
|
+
function serializeContents(c, ctx) {
|
|
5862
|
+
const out = { id: c.id, name: c.name, width: c.width, height: c.height, kind: c.kind };
|
|
5863
|
+
if (c.kind === "raster" && c.raster) {
|
|
5864
|
+
const enc = encodeSurface(c.raster);
|
|
5865
|
+
if (enc) {
|
|
5866
|
+
if (enc.x !== 0 || enc.y !== 0) {
|
|
5867
|
+
const data = readRegion(c.raster, { x: 0, y: 0, width: c.width, height: c.height });
|
|
5868
|
+
out.asset = ctx.addAsset(pngDataUrl(c.width, c.height, data, 4), `contents-${c.id}`);
|
|
5869
|
+
} else out.asset = ctx.addAsset(enc.url, `contents-${c.id}`);
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
if (c.kind === "document" && c.document) {
|
|
5873
|
+
out.layers = c.document.layers.map((l) => serializeLayer(l, ctx));
|
|
5874
|
+
if (c.document.globalLight) out.globalLight = { ...c.document.globalLight };
|
|
5875
|
+
if (c.document.patterns?.length) out.patterns = serializePatterns(c.document.patterns);
|
|
5876
|
+
}
|
|
5877
|
+
if (c.file) out.file = { type: c.file.type, data: bytesToBase64(c.file.bytes) };
|
|
5878
|
+
if (c.linked) out.linked = { path: c.linked.path };
|
|
5879
|
+
return out;
|
|
5880
|
+
}
|
|
5881
|
+
function serializeLayers(layers) {
|
|
5882
|
+
const assets = {};
|
|
5883
|
+
const contents = /* @__PURE__ */ new Map();
|
|
5884
|
+
const ctx = {
|
|
5885
|
+
addAsset(url, hint) {
|
|
5886
|
+
let id = hint;
|
|
5887
|
+
for (let n = 2; id in assets && assets[id] !== url; n++) id = `${hint}-${n}`;
|
|
5888
|
+
assets[id] = url;
|
|
5889
|
+
return id;
|
|
5890
|
+
},
|
|
5891
|
+
addContents(c) {
|
|
5892
|
+
if (contents.has(c.id)) return;
|
|
5893
|
+
contents.set(c.id, null);
|
|
5894
|
+
contents.set(c.id, serializeContents(c, ctx));
|
|
5895
|
+
}
|
|
5896
|
+
};
|
|
5897
|
+
const out = layers.map((l) => serializeLayer(l, ctx));
|
|
5898
|
+
const list2 = [...contents.values()].filter(Boolean);
|
|
5899
|
+
return list2.length ? { layers: out, assets, smartContents: list2 } : { layers: out, assets };
|
|
5900
|
+
}
|
|
5901
|
+
var externalRefs = /* @__PURE__ */ new WeakMap();
|
|
5902
|
+
async function serializeLayersExternal(layers, store) {
|
|
5903
|
+
const inline = serializeLayers(layers);
|
|
5904
|
+
let refs = externalRefs.get(store);
|
|
5905
|
+
if (!refs) {
|
|
5906
|
+
refs = /* @__PURE__ */ new Map();
|
|
5907
|
+
externalRefs.set(store, refs);
|
|
5908
|
+
}
|
|
5909
|
+
const assets = {};
|
|
5910
|
+
for (const [id, url] of Object.entries(inline.assets)) {
|
|
5911
|
+
let ref = refs.get(url);
|
|
5912
|
+
if (!ref) {
|
|
5913
|
+
const parsed = parseDataUrl(url);
|
|
5914
|
+
const png = decodePngHeader(parsed.bytes);
|
|
5915
|
+
ref = await store.put(new Blob([parsed.bytes], { type: "image/png" }), { id, width: png.width, height: png.height });
|
|
5916
|
+
if (typeof ref !== "string" || !ref) throw new Error(`The asset store returned no reference for "${id}".`);
|
|
5917
|
+
refs.set(url, ref);
|
|
5918
|
+
}
|
|
5919
|
+
assets[id] = ref;
|
|
5920
|
+
}
|
|
5921
|
+
return inline.smartContents ? { layers: inline.layers, assets, smartContents: inline.smartContents } : { layers: inline.layers, assets };
|
|
5922
|
+
}
|
|
5923
|
+
function decodePngHeader(bytes) {
|
|
5924
|
+
const u32 = (o) => (bytes[o] << 24 | bytes[o + 1] << 16 | bytes[o + 2] << 8 | bytes[o + 3]) >>> 0;
|
|
5925
|
+
return { width: u32(16), height: u32(20) };
|
|
5926
|
+
}
|
|
5927
|
+
async function resolveExternalAssets(assets, store, problems) {
|
|
5928
|
+
const out = {};
|
|
5929
|
+
for (const [id, ref] of Object.entries(assets)) {
|
|
5930
|
+
if (ref.startsWith("data:")) {
|
|
5931
|
+
out[id] = ref;
|
|
5932
|
+
continue;
|
|
5933
|
+
}
|
|
5934
|
+
if (!store) {
|
|
5935
|
+
problems.push(`Asset "${id}" is external (${ref}) and no asset store was given.`);
|
|
5936
|
+
continue;
|
|
5937
|
+
}
|
|
5938
|
+
try {
|
|
5939
|
+
const blob = await store.get(ref);
|
|
5940
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
5941
|
+
out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;
|
|
5942
|
+
} catch (err) {
|
|
5943
|
+
problems.push(`Asset "${id}" (${ref}) could not be fetched: ${err.message}`);
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
return out;
|
|
5947
|
+
}
|
|
5948
|
+
function decodePngDataUrl(url, channels) {
|
|
5949
|
+
const parsed = parseDataUrl(url);
|
|
5950
|
+
if (!parsed) throw new Error("Asset is not a data URL.");
|
|
5951
|
+
const png = decodePng(parsed.bytes, { keepGray: channels === 1 });
|
|
5952
|
+
if (png.channels === channels) return png;
|
|
5953
|
+
if (channels === 1) {
|
|
5954
|
+
const out2 = new Uint8ClampedArray(png.width * png.height);
|
|
5955
|
+
for (let i = 0, p = 0; p < out2.length; i += 4, p++) {
|
|
5956
|
+
out2[p] = Math.round((0.299 * png.data[i] + 0.587 * png.data[i + 1] + 0.114 * png.data[i + 2]) * png.data[i + 3] / 255);
|
|
5957
|
+
}
|
|
5958
|
+
return { width: png.width, height: png.height, data: out2 };
|
|
5959
|
+
}
|
|
5960
|
+
const out = new Uint8ClampedArray(png.width * png.height * 4);
|
|
5961
|
+
for (let p = 0; p < png.width * png.height; p++) {
|
|
5962
|
+
out[p * 4] = out[p * 4 + 1] = out[p * 4 + 2] = png.data[p];
|
|
5963
|
+
out[p * 4 + 3] = 255;
|
|
5964
|
+
}
|
|
5965
|
+
return { width: png.width, height: png.height, data: out };
|
|
5966
|
+
}
|
|
5967
|
+
var num = (v, lo, hi, fallback) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : fallback;
|
|
5968
|
+
function readSurface(ref, x, y, channels, defaultValue, ctx, what) {
|
|
5969
|
+
if (typeof ref !== "string" || !ref) return emptySurface(channels, defaultValue);
|
|
5970
|
+
const url = ctx.assets[ref];
|
|
5971
|
+
if (typeof url !== "string") {
|
|
5972
|
+
ctx.problems.push(`${what}: asset "${ref}" is missing`);
|
|
5973
|
+
return emptySurface(channels, defaultValue);
|
|
5974
|
+
}
|
|
5975
|
+
try {
|
|
5976
|
+
const px2 = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);
|
|
5977
|
+
return surfaceFromPixels(px2, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);
|
|
5978
|
+
} catch (err) {
|
|
5979
|
+
ctx.problems.push(`${what}: asset "${ref}" could not be decoded (${err.message})`);
|
|
5980
|
+
return emptySurface(channels, defaultValue);
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
function readMask(raw, ctx, what) {
|
|
5984
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
5985
|
+
const r = raw;
|
|
5986
|
+
const defaultValue = r.defaultValue === 255 ? 255 : 0;
|
|
5987
|
+
const mask = {
|
|
5988
|
+
surface: r.source ? emptySurface(1, 255) : readSurface(r.asset, r.x, r.y, 1, defaultValue, ctx, `${what} mask`),
|
|
5989
|
+
defaultValue: r.source ? 255 : defaultValue
|
|
5990
|
+
};
|
|
5991
|
+
if (typeof r.density === "number" && r.density !== 1) mask.density = num(r.density, 0, 1, 1);
|
|
5992
|
+
if (typeof r.feather === "number" && r.feather > 0) mask.feather = num(r.feather, 0, 1e3, 0);
|
|
5993
|
+
if (r.enabled === false) mask.enabled = false;
|
|
5994
|
+
if (r.linked === false) mask.linked = false;
|
|
5995
|
+
if (r.source && typeof r.source === "object") {
|
|
5996
|
+
const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...r.source };
|
|
5997
|
+
mask.source = { ...s, depthRange: [...s.depthRange] };
|
|
5998
|
+
}
|
|
5999
|
+
return mask;
|
|
6000
|
+
}
|
|
6001
|
+
function readFilters(raw) {
|
|
6002
|
+
if (!Array.isArray(raw)) return void 0;
|
|
6003
|
+
const out = [];
|
|
6004
|
+
for (const f of raw) {
|
|
6005
|
+
if (!f || typeof f !== "object") continue;
|
|
6006
|
+
const r = f;
|
|
6007
|
+
const id = typeof r.id === "string" && r.id ? r.id : newLayerId("flt");
|
|
6008
|
+
const enabled = r.enabled !== false;
|
|
6009
|
+
if (r.kind === "blur") out.push({ id, kind: "blur", enabled, radius: num(r.radius, 0, 50, 0) });
|
|
6010
|
+
else if (r.kind === "develop") {
|
|
6011
|
+
out.push({
|
|
6012
|
+
id,
|
|
6013
|
+
kind: "develop",
|
|
6014
|
+
enabled,
|
|
6015
|
+
brightness: num(r.brightness, -100, 100, 0),
|
|
6016
|
+
contrast: num(r.contrast, -100, 100, 0),
|
|
6017
|
+
saturation: num(r.saturation, -100, 100, 0),
|
|
6018
|
+
exposure: num(r.exposure, -100, 100, 0),
|
|
6019
|
+
temperature: num(r.temperature, -100, 100, 0),
|
|
6020
|
+
tint: num(r.tint, -100, 100, 0),
|
|
6021
|
+
process: normalizeProcessVersion(r.process)
|
|
6022
|
+
});
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
6025
|
+
return out.length ? out : void 0;
|
|
6026
|
+
}
|
|
6027
|
+
function deserializeLayer(raw, ctx) {
|
|
6028
|
+
if (!raw || typeof raw !== "object") return null;
|
|
6029
|
+
const r = raw;
|
|
6030
|
+
const id = typeof r.id === "string" && r.id ? r.id : newLayerId();
|
|
6031
|
+
const base = {
|
|
6032
|
+
id,
|
|
6033
|
+
name: typeof r.name === "string" ? r.name : "",
|
|
6034
|
+
visible: r.visible !== false,
|
|
6035
|
+
opacity: num(r.opacity, 0, 1, 1),
|
|
6036
|
+
blend: normalizeBlendMode(r.blend)
|
|
6037
|
+
};
|
|
6038
|
+
if (r.clipped === true) base.clipped = true;
|
|
6039
|
+
const mask = readMask(r.mask, ctx, `Layer "${base.name || id}"`);
|
|
6040
|
+
if (mask) base.mask = mask;
|
|
6041
|
+
if (r.locks && typeof r.locks === "object") {
|
|
6042
|
+
const locks = {};
|
|
6043
|
+
for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
|
|
6044
|
+
if (Object.keys(locks).length) base.locks = locks;
|
|
6045
|
+
}
|
|
6046
|
+
const effects = deserializeEffects(r.effects);
|
|
6047
|
+
if (effects && r.type !== "adjustment") base.effects = effects;
|
|
6048
|
+
switch (r.type) {
|
|
6049
|
+
case "pixel": {
|
|
6050
|
+
const layer = {
|
|
6051
|
+
...base,
|
|
6052
|
+
type: "pixel",
|
|
6053
|
+
surface: r.source === "document" ? emptySurface(4) : readSurface(r.asset, r.x, r.y, 4, 0, ctx, `Layer "${base.name || id}"`),
|
|
6054
|
+
offset: { x: Math.round(num(r.offset?.x, -1e7, 1e7, 0)), y: Math.round(num(r.offset?.y, -1e7, 1e7, 0)) }
|
|
6055
|
+
};
|
|
6056
|
+
if (r.source === "document") layer.source = "document";
|
|
6057
|
+
if (typeof r.fillOpacity === "number" && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);
|
|
6058
|
+
const filters = readFilters(r.filters);
|
|
6059
|
+
if (filters) layer.filters = filters;
|
|
6060
|
+
return layer;
|
|
6061
|
+
}
|
|
6062
|
+
case "adjustment": {
|
|
6063
|
+
const norm = normalizeAdjustmentLayer({ id, kind: r.kind, params: r.params });
|
|
6064
|
+
if (!norm) {
|
|
6065
|
+
ctx.problems.push(`Layer "${base.name || id}": unknown adjustment kind "${r.kind}"`);
|
|
6066
|
+
return null;
|
|
6067
|
+
}
|
|
6068
|
+
return { ...base, type: "adjustment", kind: norm.kind, params: norm.params };
|
|
6069
|
+
}
|
|
6070
|
+
case "group": {
|
|
6071
|
+
const children = Array.isArray(r.children) ? r.children.map((c) => deserializeLayer(c, ctx)).filter(Boolean) : [];
|
|
6072
|
+
const blend = r.blend === "pass-through" ? "pass-through" : isBlendMode(r.blend) ? r.blend : "pass-through";
|
|
6073
|
+
const group = { ...base, type: "group", blend, children };
|
|
6074
|
+
if (r.collapsed === true) group.collapsed = true;
|
|
6075
|
+
return group;
|
|
6076
|
+
}
|
|
6077
|
+
case "annotation": {
|
|
6078
|
+
const a = r.annotation;
|
|
6079
|
+
if (!a || typeof a !== "object" || typeof a.type !== "string") {
|
|
6080
|
+
ctx.problems.push(`Layer "${base.name || id}": annotation is missing`);
|
|
6081
|
+
return null;
|
|
6082
|
+
}
|
|
6083
|
+
return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify({ ...a, id: typeof a.id === "string" ? a.id : id })) };
|
|
6084
|
+
}
|
|
6085
|
+
case "smart":
|
|
6086
|
+
return readSmartObject(r, base, ctx);
|
|
6087
|
+
default:
|
|
6088
|
+
ctx.problems.push(`Layer "${base.name || id}": unknown layer type "${r.type}"`);
|
|
6089
|
+
return null;
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
function readContents(id, ctx) {
|
|
6093
|
+
const memo = ctx.contentsMemo ??= /* @__PURE__ */ new Map();
|
|
6094
|
+
if (memo.has(id)) {
|
|
6095
|
+
const c2 = memo.get(id);
|
|
6096
|
+
if (c2 === null) ctx.problems.push(`Smart object contents "${id}" contain themselves; the inner instance was dropped`);
|
|
6097
|
+
return c2 ?? null;
|
|
6098
|
+
}
|
|
6099
|
+
const raw = (ctx.smartContents ?? []).find((c2) => c2 && typeof c2 === "object" && c2.id === id);
|
|
6100
|
+
if (!raw) {
|
|
6101
|
+
ctx.problems.push(`Smart object contents "${id}" are missing`);
|
|
6102
|
+
return null;
|
|
6103
|
+
}
|
|
6104
|
+
const width = Math.round(num(raw.width, 0, 3e5, 0));
|
|
6105
|
+
const height = Math.round(num(raw.height, 0, 3e5, 0));
|
|
6106
|
+
if (!(width > 0 && height > 0)) {
|
|
6107
|
+
ctx.problems.push(`Smart object contents "${id}" have no size`);
|
|
6108
|
+
return null;
|
|
6109
|
+
}
|
|
6110
|
+
memo.set(id, null);
|
|
6111
|
+
const c = { id, name: typeof raw.name === "string" ? raw.name : "Contents", width, height, kind: raw.kind === "document" ? "document" : "raster" };
|
|
6112
|
+
if (raw.file && typeof raw.file.type === "string" && typeof raw.file.data === "string") {
|
|
6113
|
+
try {
|
|
6114
|
+
c.file = { type: raw.file.type, bytes: base64ToBytes(raw.file.data) };
|
|
6115
|
+
} catch {
|
|
6116
|
+
ctx.problems.push(`Smart object contents "${c.name}": the embedded file could not be read`);
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
if (raw.linked && typeof raw.linked.path === "string") c.linked = { path: raw.linked.path };
|
|
6120
|
+
if (c.kind === "raster") {
|
|
6121
|
+
if (typeof raw.asset === "string") c.raster = readSurface(raw.asset, 0, 0, 4, 0, ctx, `Smart object contents "${c.name}"`);
|
|
6122
|
+
else if (!c.file) c.raster = emptySurface(4);
|
|
6123
|
+
} else {
|
|
6124
|
+
const outer = ctx.canvas;
|
|
6125
|
+
ctx.canvas = { width, height };
|
|
6126
|
+
const layers = deserializeLayers(raw.layers, ctx);
|
|
6127
|
+
ctx.canvas = outer;
|
|
6128
|
+
c.document = { layers };
|
|
6129
|
+
if (raw.globalLight && typeof raw.globalLight === "object") c.document.globalLight = { angle: num(raw.globalLight.angle, -360, 360, 120), altitude: num(raw.globalLight.altitude, 0, 90, 30) };
|
|
6130
|
+
const patterns = deserializePatterns(raw.patterns, ctx.problems);
|
|
6131
|
+
if (patterns.length) c.document.patterns = patterns;
|
|
6132
|
+
}
|
|
6133
|
+
memo.set(id, c);
|
|
6134
|
+
return c;
|
|
5173
6135
|
}
|
|
5174
|
-
function
|
|
5175
|
-
|
|
6136
|
+
function readSmartFilters(raw, ctx, what) {
|
|
6137
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
6138
|
+
const r = raw;
|
|
6139
|
+
const filters = [];
|
|
6140
|
+
const list2 = Array.isArray(r.filters) ? r.filters : [];
|
|
6141
|
+
const plain3 = readFilters(list2.filter((f) => f && f.kind !== "psd")) ?? [];
|
|
6142
|
+
let k = 0;
|
|
6143
|
+
for (const f of list2) {
|
|
6144
|
+
if (!f || typeof f !== "object") continue;
|
|
6145
|
+
const blend = isBlendMode(f.blend) && f.blend !== "pigment" ? f.blend : "normal";
|
|
6146
|
+
const opacity = num(f.opacity, 0, 1, 1);
|
|
6147
|
+
if (f.kind === "psd") {
|
|
6148
|
+
try {
|
|
6149
|
+
filters.push({ id: typeof f.id === "string" ? f.id : newLayerId("flt"), kind: "psd", enabled: f.enabled !== false, name: typeof f.name === "string" ? f.name : "Filter", data: base64ToBytes(String(f.data ?? "")), blend, opacity });
|
|
6150
|
+
} catch {
|
|
6151
|
+
ctx.problems.push(`${what}: a Photoshop smart filter could not be read`);
|
|
6152
|
+
}
|
|
6153
|
+
} else if (f.kind === "develop" || f.kind === "blur") {
|
|
6154
|
+
const base = plain3[k++];
|
|
6155
|
+
if (base) filters.push({ ...base, blend, opacity });
|
|
6156
|
+
}
|
|
6157
|
+
}
|
|
6158
|
+
const stack = { enabled: r.enabled !== false, filters };
|
|
6159
|
+
const mask = readMask(r.mask, ctx, `${what} filter`);
|
|
6160
|
+
if (mask) stack.mask = mask;
|
|
6161
|
+
return stack;
|
|
5176
6162
|
}
|
|
5177
|
-
function
|
|
6163
|
+
function readSmartObject(r, base, ctx) {
|
|
6164
|
+
const what = `Layer "${base.name || base.id}"`;
|
|
6165
|
+
const contents = typeof r.contents === "string" ? readContents(r.contents, ctx) : null;
|
|
6166
|
+
if (!contents) {
|
|
6167
|
+
ctx.problems.push(`${what}: its smart object contents are missing; the layer was dropped`);
|
|
6168
|
+
return null;
|
|
6169
|
+
}
|
|
6170
|
+
const p = r.placement ?? {};
|
|
6171
|
+
let placement;
|
|
5178
6172
|
try {
|
|
5179
|
-
|
|
6173
|
+
const raw = { corners: p.corners };
|
|
6174
|
+
if (p.warp && isValidWarpMesh(p.warp)) raw.warp = p.warp;
|
|
6175
|
+
if (isInterpolation(p.interpolation)) raw.interpolation = p.interpolation;
|
|
6176
|
+
placement = normalizePlacement(raw, contents);
|
|
5180
6177
|
} catch (e) {
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
}
|
|
5184
|
-
}
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
if (
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
}
|
|
5194
|
-
function addLayerEffect(layers, id, kind, params = {}, index) {
|
|
5195
|
-
if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
|
|
5196
|
-
const layer = requireEffectLayer(layers, id, "add effects");
|
|
5197
|
-
const current = layerEffectsOf(layer);
|
|
5198
|
-
const ids = new Set(current.items.map((e) => e.id));
|
|
5199
|
-
let effectId = newEffectId();
|
|
5200
|
-
while (ids.has(effectId)) effectId = newEffectId();
|
|
5201
|
-
const { id: _ignored, kind: _kind, ...rest } = params;
|
|
5202
|
-
const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
|
|
5203
|
-
const items = [...current.items];
|
|
5204
|
-
const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
|
|
5205
|
-
items.splice(at, 0, effect);
|
|
5206
|
-
return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
|
|
5207
|
-
}
|
|
5208
|
-
function requireEffect(layer, effectId) {
|
|
5209
|
-
const items = layer.effects?.items ?? [];
|
|
5210
|
-
const i = items.findIndex((e) => e.id === effectId);
|
|
5211
|
-
if (i === -1) fail(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
|
|
5212
|
-
return i;
|
|
5213
|
-
}
|
|
5214
|
-
function updateLayerEffect(layers, id, effectId, changes) {
|
|
5215
|
-
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5216
|
-
const i = requireEffect(layer, effectId);
|
|
5217
|
-
const current = layer.effects;
|
|
5218
|
-
const items = [...current.items];
|
|
5219
|
-
items[i] = guard(() => mergeLayerEffect(items[i], changes));
|
|
5220
|
-
return setLayerEffects(layers, id, { ...current, items });
|
|
5221
|
-
}
|
|
5222
|
-
function removeLayerEffect(layers, id, effectId) {
|
|
5223
|
-
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5224
|
-
requireEffect(layer, effectId);
|
|
5225
|
-
const current = layer.effects;
|
|
5226
|
-
return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
|
|
5227
|
-
}
|
|
5228
|
-
function setLayerEffectsEnabled(layers, id, enabled, scale) {
|
|
5229
|
-
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5230
|
-
if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
|
|
5231
|
-
const current = layerEffectsOf(layer);
|
|
5232
|
-
const next = { ...current, enabled };
|
|
5233
|
-
if (scale !== void 0) {
|
|
5234
|
-
if (scale === 1) delete next.scale;
|
|
5235
|
-
else next.scale = scale;
|
|
6178
|
+
ctx.problems.push(`${what}: its placement is invalid (${e instanceof Error ? e.message : String(e)}); it was placed at the top left`);
|
|
6179
|
+
placement = { corners: [{ x: 0, y: 0 }, { x: contents.width, y: 0 }, { x: contents.width, y: contents.height }, { x: 0, y: contents.height }] };
|
|
6180
|
+
}
|
|
6181
|
+
const layer = { ...base, type: "smart", contents, placement, surface: emptySurface(4), offset: { x: 0, y: 0 } };
|
|
6182
|
+
const filters = readSmartFilters(r.smartFilters, ctx, what);
|
|
6183
|
+
if (filters) layer.smartFilters = filters;
|
|
6184
|
+
if (typeof r.fillOpacity === "number" && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);
|
|
6185
|
+
if (r.psd && typeof r.psd.key === "string" && typeof r.psd.data === "string") {
|
|
6186
|
+
try {
|
|
6187
|
+
layer.psd = { key: r.psd.key, data: base64ToBytes(r.psd.data) };
|
|
6188
|
+
if (typeof r.psd.placed === "string") layer.psd.placed = r.psd.placed;
|
|
6189
|
+
} catch {
|
|
6190
|
+
}
|
|
5236
6191
|
}
|
|
5237
|
-
|
|
5238
|
-
}
|
|
5239
|
-
|
|
5240
|
-
if (value === void 0) return void 0;
|
|
5241
|
-
if (value === "pass-through" && allowPassThrough) return value;
|
|
5242
|
-
if (!isBlendMode(value)) fail(`Unknown blend mode "${String(value)}".`);
|
|
5243
|
-
return value;
|
|
6192
|
+
if (typeof r.asset === "string") layer.surface = readSurface(r.asset, r.x, r.y, 4, 0, ctx, what);
|
|
6193
|
+
else if (contents.raster || contents.document) layer.surface = renderSmartObject(layer, ctx.canvas ?? { width: 1, height: 1 });
|
|
6194
|
+
return layer;
|
|
5244
6195
|
}
|
|
5245
|
-
function
|
|
5246
|
-
if (!
|
|
5247
|
-
|
|
5248
|
-
const
|
|
5249
|
-
|
|
5250
|
-
|
|
6196
|
+
function deserializeLayers(raw, ctx) {
|
|
6197
|
+
if (!Array.isArray(raw)) return [];
|
|
6198
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6199
|
+
const dedupe = (list2) => list2.map((l) => {
|
|
6200
|
+
let next = l;
|
|
6201
|
+
if (seen.has(l.id)) next = { ...l, id: newLayerId() };
|
|
6202
|
+
seen.add(next.id);
|
|
6203
|
+
if (next.type === "group") next = { ...next, children: dedupe(next.children) };
|
|
6204
|
+
return next;
|
|
6205
|
+
});
|
|
6206
|
+
return dedupe(raw.map((l) => deserializeLayer(l, ctx)).filter(Boolean));
|
|
5251
6207
|
}
|
|
5252
6208
|
|
|
5253
6209
|
// src/image-editor/psd/binary.ts
|
|
@@ -8165,202 +9121,44 @@ function exportPsd(input) {
|
|
|
8165
9121
|
{ id: 1057, name: "", data: version.result() }
|
|
8166
9122
|
];
|
|
8167
9123
|
if (input.globalLight) {
|
|
8168
|
-
const i32 = (v) => {
|
|
8169
|
-
const w = new PsdWriter();
|
|
8170
|
-
w.i32(Math.round(v));
|
|
8171
|
-
return w.result();
|
|
8172
|
-
};
|
|
8173
|
-
resources.push({ id: 1037, name: "", data: i32(input.globalLight.angle) }, { id: 1049, name: "", data: i32(input.globalLight.altitude) });
|
|
8174
|
-
}
|
|
8175
|
-
const globalBlocks = [];
|
|
8176
|
-
if (patternIds2.size) {
|
|
8177
|
-
const byId = new Map((input.patterns ?? []).map((p) => [p.id, p]));
|
|
8178
|
-
const embed = [];
|
|
8179
|
-
for (const id of patternIds2) {
|
|
8180
|
-
const p = byId.get(id) ?? builtinPattern(id);
|
|
8181
|
-
if (p) embed.push(p);
|
|
8182
|
-
else note(`Pattern "${id}" is not in the document and was not embedded`, id);
|
|
8183
|
-
}
|
|
8184
|
-
if (embed.length) globalBlocks.push({ key: "Patt", data: writePatternBlock(embed) });
|
|
8185
|
-
}
|
|
8186
|
-
const bytes = writePsd(
|
|
8187
|
-
{
|
|
8188
|
-
header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },
|
|
8189
|
-
colorModeData: new Uint8Array(0),
|
|
8190
|
-
resources,
|
|
8191
|
-
layers: records,
|
|
8192
|
-
mergedAlpha: transparent && records.length > 0,
|
|
8193
|
-
globalBlocks,
|
|
8194
|
-
imageCompression: 1,
|
|
8195
|
-
imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb })
|
|
8196
|
-
},
|
|
8197
|
-
encodeLegacyName
|
|
8198
|
-
);
|
|
8199
|
-
const warnings = [...notes].map(([msg, names]) => `${msg} (${names.map((n2) => `"${n2}"`).join(", ")}).`);
|
|
8200
|
-
return { bytes, psb, records: records.length, warnings };
|
|
8201
|
-
}
|
|
8202
|
-
|
|
8203
|
-
// src/image-editor/transform.ts
|
|
8204
|
-
var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
|
|
8205
|
-
var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
|
|
8206
|
-
var ANCHOR_FRACTION = {
|
|
8207
|
-
"top-left": [0, 0],
|
|
8208
|
-
top: [0.5, 0],
|
|
8209
|
-
"top-right": [1, 0],
|
|
8210
|
-
left: [0, 0.5],
|
|
8211
|
-
center: [0.5, 0.5],
|
|
8212
|
-
right: [1, 0.5],
|
|
8213
|
-
"bottom-left": [0, 1],
|
|
8214
|
-
bottom: [0.5, 1],
|
|
8215
|
-
"bottom-right": [1, 1]
|
|
8216
|
-
};
|
|
8217
|
-
function isTransformAnchor(value) {
|
|
8218
|
-
return typeof value === "string" && value in ANCHOR_FRACTION;
|
|
8219
|
-
}
|
|
8220
|
-
function identityTransform(box) {
|
|
8221
|
-
return { box: { ...box }, corners: boxQuad(box) };
|
|
8222
|
-
}
|
|
8223
|
-
function cornersFromMatrix(box, m) {
|
|
8224
|
-
const q2 = boxQuad(box);
|
|
8225
|
-
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
8226
|
-
}
|
|
8227
|
-
function matrixFromCorners(box, corners, eps = 1e-6) {
|
|
8228
|
-
const [tl, tr, br, bl] = corners;
|
|
8229
|
-
const size = 1 + Math.max(box.width, box.height);
|
|
8230
|
-
if (Math.abs(tl.x + br.x - tr.x - bl.x) > eps * size || Math.abs(tl.y + br.y - tr.y - bl.y) > eps * size) return null;
|
|
8231
|
-
const h = rectToQuad(box, corners);
|
|
8232
|
-
return h ? affineFromHomography(h, 1e-9) : null;
|
|
8233
|
-
}
|
|
8234
|
-
function transformHomography(geometry) {
|
|
8235
|
-
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8236
|
-
if (!h) return null;
|
|
8237
|
-
return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;
|
|
8238
|
-
}
|
|
8239
|
-
function hasWarp(geometry) {
|
|
8240
|
-
const w = geometry.warp;
|
|
8241
|
-
if (!w) return false;
|
|
8242
|
-
const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);
|
|
8243
|
-
if (id.points.length !== w.points.length || id.us.some((u, i) => Math.abs(u - w.us[i]) > 1e-12) || id.vs.some((v, i) => Math.abs(v - w.vs[i]) > 1e-12)) return true;
|
|
8244
|
-
return w.points.some((p, i) => Math.abs(p.x - id.points[i].x) > 1e-6 || Math.abs(p.y - id.points[i].y) > 1e-6);
|
|
8245
|
-
}
|
|
8246
|
-
function isIdentityTransform(geometry) {
|
|
8247
|
-
if (hasWarp(geometry)) return false;
|
|
8248
|
-
const q2 = boxQuad(geometry.box);
|
|
8249
|
-
return geometry.corners.every((c, i) => Math.abs(c.x - q2[i].x) < 1e-6 && Math.abs(c.y - q2[i].y) < 1e-6);
|
|
8250
|
-
}
|
|
8251
|
-
function transformMap(geometry) {
|
|
8252
|
-
const h = transformHomography(geometry);
|
|
8253
|
-
if (!h) return null;
|
|
8254
|
-
const { box } = geometry;
|
|
8255
|
-
const warp = hasWarp(geometry) ? geometry.warp : null;
|
|
8256
|
-
if (!warp) return (p) => applyHomography(h, p);
|
|
8257
|
-
return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));
|
|
8258
|
-
}
|
|
8259
|
-
function transformedPoint(geometry, fx, fy) {
|
|
8260
|
-
const f = transformMap(geometry);
|
|
8261
|
-
const { box } = geometry;
|
|
8262
|
-
return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;
|
|
8263
|
-
}
|
|
8264
|
-
function anchorPoint(geometry, anchor) {
|
|
8265
|
-
const [fx, fy] = ANCHOR_FRACTION[anchor];
|
|
8266
|
-
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8267
|
-
const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };
|
|
8268
|
-
return (h && applyHomography(h, p)) ?? p;
|
|
8269
|
-
}
|
|
8270
|
-
function transformedBox(geometry) {
|
|
8271
|
-
const f = transformMap(geometry);
|
|
8272
|
-
if (!f) return null;
|
|
8273
|
-
const { box } = geometry;
|
|
8274
|
-
const pts = [];
|
|
8275
|
-
const n = hasWarp(geometry) ? 16 : 1;
|
|
8276
|
-
for (let i = 0; i <= n; i++) {
|
|
8277
|
-
const t = i / n;
|
|
8278
|
-
for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {
|
|
8279
|
-
const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });
|
|
8280
|
-
if (!p) return null;
|
|
8281
|
-
pts.push(p);
|
|
8282
|
-
}
|
|
9124
|
+
const i32 = (v) => {
|
|
9125
|
+
const w = new PsdWriter();
|
|
9126
|
+
w.i32(Math.round(v));
|
|
9127
|
+
return w.result();
|
|
9128
|
+
};
|
|
9129
|
+
resources.push({ id: 1037, name: "", data: i32(input.globalLight.angle) }, { id: 1049, name: "", data: i32(input.globalLight.altitude) });
|
|
8283
9130
|
}
|
|
8284
|
-
const
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
}
|
|
8295
|
-
function matrixFromParams(box, params) {
|
|
8296
|
-
const sx = (params.scaleX ?? 100) / 100;
|
|
8297
|
-
const sy = (params.scaleY ?? 100) / 100;
|
|
8298
|
-
const tx = Math.tan((params.skewX ?? 0) * Math.PI / 180);
|
|
8299
|
-
const ty = Math.tan((params.skewY ?? 0) * Math.PI / 180);
|
|
8300
|
-
const ref = referencePoint(box, params.reference);
|
|
8301
|
-
const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));
|
|
8302
|
-
return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));
|
|
8303
|
-
}
|
|
8304
|
-
function describeTransform(geometry, reference = "center") {
|
|
8305
|
-
const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);
|
|
8306
|
-
const ref = referencePoint(geometry.box, reference);
|
|
8307
|
-
const bounds = transformedBox(geometry);
|
|
8308
|
-
if (!m) {
|
|
8309
|
-
const h = rectToQuad(geometry.box, geometry.corners);
|
|
8310
|
-
const at2 = (h && applyHomography(h, ref)) ?? ref;
|
|
8311
|
-
return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at2.x, y: at2.y, bounds, corners: geometry.corners };
|
|
9131
|
+
const globalBlocks = [];
|
|
9132
|
+
if (patternIds2.size) {
|
|
9133
|
+
const byId = new Map((input.patterns ?? []).map((p) => [p.id, p]));
|
|
9134
|
+
const embed = [];
|
|
9135
|
+
for (const id of patternIds2) {
|
|
9136
|
+
const p = byId.get(id) ?? builtinPattern(id);
|
|
9137
|
+
if (p) embed.push(p);
|
|
9138
|
+
else note(`Pattern "${id}" is not in the document and was not embedded`, id);
|
|
9139
|
+
}
|
|
9140
|
+
if (embed.length) globalBlocks.push({ key: "Patt", data: writePatternBlock(embed) });
|
|
8312
9141
|
}
|
|
8313
|
-
const
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
y: at.y,
|
|
8329
|
-
bounds,
|
|
8330
|
-
corners: geometry.corners
|
|
8331
|
-
};
|
|
8332
|
-
}
|
|
8333
|
-
function applyParams(geometry, params) {
|
|
8334
|
-
const ref = params.reference && typeof params.reference === "object" ? params.reference : anchorPoint(geometry, params.reference ?? "center");
|
|
8335
|
-
const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });
|
|
8336
|
-
const c = geometry.corners;
|
|
8337
|
-
return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];
|
|
8338
|
-
}
|
|
8339
|
-
function flipCorners(geometry, axis) {
|
|
8340
|
-
const c = anchorPoint(geometry, "center");
|
|
8341
|
-
const m = axis === "horizontal" ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];
|
|
8342
|
-
const q2 = geometry.corners;
|
|
8343
|
-
return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
|
|
8344
|
-
}
|
|
8345
|
-
function boxPointOf(geometry, p) {
|
|
8346
|
-
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
8347
|
-
if (m) return applyMatrix(invertMatrix(m), p);
|
|
8348
|
-
return null;
|
|
8349
|
-
}
|
|
8350
|
-
function presetWarp(box, preset) {
|
|
8351
|
-
return warpPresetMesh(preset, box);
|
|
8352
|
-
}
|
|
8353
|
-
function isValidWarp(mesh) {
|
|
8354
|
-
return isValidWarpMesh(mesh);
|
|
8355
|
-
}
|
|
8356
|
-
function geometryHomography(geometry) {
|
|
8357
|
-
const m = matrixFromCorners(geometry.box, geometry.corners);
|
|
8358
|
-
return m ? homographyFromAffine(m) : transformHomography(geometry);
|
|
9142
|
+
const bytes = writePsd(
|
|
9143
|
+
{
|
|
9144
|
+
header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },
|
|
9145
|
+
colorModeData: new Uint8Array(0),
|
|
9146
|
+
resources,
|
|
9147
|
+
layers: records,
|
|
9148
|
+
mergedAlpha: transparent && records.length > 0,
|
|
9149
|
+
globalBlocks,
|
|
9150
|
+
imageCompression: 1,
|
|
9151
|
+
imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb })
|
|
9152
|
+
},
|
|
9153
|
+
encodeLegacyName
|
|
9154
|
+
);
|
|
9155
|
+
const warnings = [...notes].map(([msg, names]) => `${msg} (${names.map((n2) => `"${n2}"`).join(", ")}).`);
|
|
9156
|
+
return { bytes, psb, records: records.length, warnings };
|
|
8359
9157
|
}
|
|
8360
9158
|
|
|
8361
9159
|
// src/image-editor/free-transform.ts
|
|
8362
9160
|
var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
|
|
8363
|
-
var
|
|
9161
|
+
var fail3 = (message) => {
|
|
8364
9162
|
throw new LayerOperationError(message);
|
|
8365
9163
|
};
|
|
8366
9164
|
var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
|
|
@@ -8419,17 +9217,20 @@ function liftMask(mask, sel, o) {
|
|
|
8419
9217
|
var FreeTransformSession = class {
|
|
8420
9218
|
constructor(host, init = {}) {
|
|
8421
9219
|
this.host = host;
|
|
8422
|
-
if (init.interpolation !== void 0 && !isInterpolation(init.interpolation))
|
|
9220
|
+
if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail3(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
8423
9221
|
this.interp = init.interpolation ?? "bicubic";
|
|
8424
9222
|
const sel = host.selection();
|
|
8425
9223
|
const selected = Boolean(sel?.bounds);
|
|
8426
9224
|
let content = init.content ?? "auto";
|
|
8427
9225
|
if (!["auto", "layer", "selection", "selection-copy", "selection-outline"].includes(content)) {
|
|
8428
|
-
|
|
9226
|
+
fail3(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
|
|
8429
9227
|
}
|
|
8430
|
-
|
|
9228
|
+
const targetId = init.layerId ?? host.activeLayerId();
|
|
9229
|
+
const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
|
|
9230
|
+
const smartTarget = target?.type === "smart" && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
|
|
9231
|
+
if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
|
|
8431
9232
|
this.content = content;
|
|
8432
|
-
if (content !== "layer" && !selected)
|
|
9233
|
+
if (content !== "layer" && !selected) fail3(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
|
|
8433
9234
|
const layers = host.layers();
|
|
8434
9235
|
this.startLayers = layers;
|
|
8435
9236
|
this.startSelection = sel;
|
|
@@ -8444,25 +9245,39 @@ var FreeTransformSession = class {
|
|
|
8444
9245
|
box = { ...sel.bounds };
|
|
8445
9246
|
} else {
|
|
8446
9247
|
const id = init.layerId ?? host.activeLayerId();
|
|
8447
|
-
if (!id)
|
|
9248
|
+
if (!id) fail3("No layer given and none is active; pass layerId (see list_layers).");
|
|
8448
9249
|
const layer = requireLayer(layers, id).layer;
|
|
8449
9250
|
this.layerId = layer.id;
|
|
8450
9251
|
const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
|
|
8451
9252
|
const channel = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : "pixels");
|
|
8452
|
-
if (channel !== "pixels" && channel !== "mask")
|
|
9253
|
+
if (channel !== "pixels" && channel !== "mask") fail3(`Unknown channel "${String(channel)}"; use pixels or mask.`);
|
|
8453
9254
|
const linked = mask?.linked !== false;
|
|
8454
9255
|
const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
|
|
8455
|
-
if (channel === "mask" && !mask)
|
|
8456
|
-
if (!maskAlone && layer.type !== "pixel")
|
|
8457
|
-
if (!layer.visible && content !== "selection-copy")
|
|
9256
|
+
if (channel === "mask" && !mask) fail3(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
|
|
9257
|
+
if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart") fail3(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel layers, smart objects, layer masks (channel mask) and selections.`);
|
|
9258
|
+
if (!layer.visible && content !== "selection-copy") fail3(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
|
|
8458
9259
|
if (content !== "selection-copy") {
|
|
8459
9260
|
requireUnlocked(layer, "position", "transform it");
|
|
8460
9261
|
if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
|
|
8461
9262
|
}
|
|
8462
9263
|
const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
8463
9264
|
const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
|
|
9265
|
+
if (layer.type === "smart" && !maskAlone) {
|
|
9266
|
+
if (content !== "layer") fail3(`Layer "${layer.name || id}" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
|
|
9267
|
+
if (!isContentsDecoded(layer.contents)) fail3(`The contents of "${layer.name || id}" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);
|
|
9268
|
+
this.smart = layer;
|
|
9269
|
+
this.smartObject = true;
|
|
9270
|
+
this.startGeometry = placementGeometry(layer);
|
|
9271
|
+
if (init.interpolation === void 0 && layer.placement.interpolation) this.interp = layer.placement.interpolation;
|
|
9272
|
+
this.startInterp = this.interp;
|
|
9273
|
+
this.box = contentsBox(layer.contents);
|
|
9274
|
+
this.moves = mask && linked ? ["pixels", "mask"] : ["pixels"];
|
|
9275
|
+
this.geometryValue = this.startGeometry;
|
|
9276
|
+
this.shown = this.startLayers;
|
|
9277
|
+
return;
|
|
9278
|
+
}
|
|
8464
9279
|
if (maskAlone) {
|
|
8465
|
-
if (content === "selection-copy")
|
|
9280
|
+
if (content === "selection-copy") fail3("Layer via Copy copies pixels; transform a mask with content layer or selection.");
|
|
8466
9281
|
if (content === "selection") {
|
|
8467
9282
|
const { lifted, rest } = liftMask(mask.surface, sel, o);
|
|
8468
9283
|
this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
|
|
@@ -8517,11 +9332,13 @@ var FreeTransformSession = class {
|
|
|
8517
9332
|
moves.push("selection");
|
|
8518
9333
|
}
|
|
8519
9334
|
}
|
|
8520
|
-
if (!box && moves[0] === "mask")
|
|
8521
|
-
if (!box)
|
|
9335
|
+
if (!box && moves[0] === "mask") fail3("The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).");
|
|
9336
|
+
if (!box) fail3(content === "layer" ? "The layer has no pixels to transform." : "The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).");
|
|
8522
9337
|
this.box = box;
|
|
8523
9338
|
this.moves = moves;
|
|
8524
|
-
this.
|
|
9339
|
+
this.startGeometry = identityTransform(box);
|
|
9340
|
+
this.startInterp = this.interp;
|
|
9341
|
+
this.geometryValue = this.startGeometry;
|
|
8525
9342
|
this.shown = this.startLayers;
|
|
8526
9343
|
if (this.copyId) host.show(this.startLayers, sel, this.copyId);
|
|
8527
9344
|
this.shown = host.layers();
|
|
@@ -8536,7 +9353,19 @@ var FreeTransformSession = class {
|
|
|
8536
9353
|
box;
|
|
8537
9354
|
/** What moves. */
|
|
8538
9355
|
moves;
|
|
9356
|
+
/**
|
|
9357
|
+
* The geometry the transform starts from: the identity over `box`, or for a smart object its
|
|
9358
|
+
* placement (`box` is then the contents' rectangle in contents pixels, and the transform edits
|
|
9359
|
+
* the placement without resampling anything twice).
|
|
9360
|
+
*/
|
|
9361
|
+
startGeometry;
|
|
9362
|
+
/** True when the layer is a smart object (the transform sets its placement). */
|
|
9363
|
+
smartObject = false;
|
|
8539
9364
|
geometryValue;
|
|
9365
|
+
smart = null;
|
|
9366
|
+
smartReduced = /* @__PURE__ */ new Map();
|
|
9367
|
+
startInterp;
|
|
9368
|
+
smartMasksMoved = false;
|
|
8540
9369
|
interp;
|
|
8541
9370
|
parts = [];
|
|
8542
9371
|
before;
|
|
@@ -8557,10 +9386,10 @@ var FreeTransformSession = class {
|
|
|
8557
9386
|
return !this.closed;
|
|
8558
9387
|
}
|
|
8559
9388
|
check() {
|
|
8560
|
-
if (this.closed)
|
|
9389
|
+
if (this.closed) fail3("This transform has already been committed or cancelled; begin a new one.");
|
|
8561
9390
|
if (this.host.layers() !== this.shown) {
|
|
8562
9391
|
this.closed = true;
|
|
8563
|
-
|
|
9392
|
+
fail3("The document changed while the transform was open, so it was abandoned; begin a new one.");
|
|
8564
9393
|
}
|
|
8565
9394
|
}
|
|
8566
9395
|
/**
|
|
@@ -8570,7 +9399,7 @@ var FreeTransformSession = class {
|
|
|
8570
9399
|
update(changes, preview = {}) {
|
|
8571
9400
|
this.check();
|
|
8572
9401
|
if (changes.interpolation !== void 0) {
|
|
8573
|
-
if (!isInterpolation(changes.interpolation))
|
|
9402
|
+
if (!isInterpolation(changes.interpolation)) fail3(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
8574
9403
|
this.interp = changes.interpolation;
|
|
8575
9404
|
}
|
|
8576
9405
|
const next = {
|
|
@@ -8578,14 +9407,15 @@ var FreeTransformSession = class {
|
|
|
8578
9407
|
corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
|
|
8579
9408
|
warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
|
|
8580
9409
|
};
|
|
8581
|
-
if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)))
|
|
8582
|
-
if (!transformMap(next))
|
|
9410
|
+
if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail3("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
|
|
9411
|
+
if (!transformMap(next)) fail3("These corners fold the frame over itself or through the horizon; keep the quad convex.");
|
|
8583
9412
|
this.geometryValue = next;
|
|
8584
9413
|
if (preview !== false) this.render(preview);
|
|
8585
9414
|
return next;
|
|
8586
9415
|
}
|
|
8587
9416
|
/** Resamples every part for the current geometry and shows it (or returns it, for commit). */
|
|
8588
9417
|
resample(final, preview = {}) {
|
|
9418
|
+
if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
|
|
8589
9419
|
const g = this.geometryValue;
|
|
8590
9420
|
const identity = isIdentityTransform(g);
|
|
8591
9421
|
const warp = hasWarp(g);
|
|
@@ -8625,7 +9455,7 @@ var FreeTransformSession = class {
|
|
|
8625
9455
|
moved = transformSurface(src, hl, opts);
|
|
8626
9456
|
}
|
|
8627
9457
|
} catch (e) {
|
|
8628
|
-
if (e instanceof TransformTooLargeError)
|
|
9458
|
+
if (e instanceof TransformTooLargeError) fail3(e.message);
|
|
8629
9459
|
throw e;
|
|
8630
9460
|
}
|
|
8631
9461
|
}
|
|
@@ -8647,6 +9477,35 @@ var FreeTransformSession = class {
|
|
|
8647
9477
|
}
|
|
8648
9478
|
return { layers, selection };
|
|
8649
9479
|
}
|
|
9480
|
+
/** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */
|
|
9481
|
+
resampleSmart(final, preview) {
|
|
9482
|
+
const layer = this.smart;
|
|
9483
|
+
const g = this.geometryValue;
|
|
9484
|
+
const placement = { corners: g.corners, warp: g.warp ?? null, interpolation: this.interp };
|
|
9485
|
+
const canvas = this.host.canvas();
|
|
9486
|
+
try {
|
|
9487
|
+
if (final) {
|
|
9488
|
+
const res = setSmartPlacement(this.startLayers, layer.id, placement, canvas);
|
|
9489
|
+
this.smartMasksMoved = res.masksMoved;
|
|
9490
|
+
return res.layers;
|
|
9491
|
+
}
|
|
9492
|
+
const step = previewStep(preview.scale);
|
|
9493
|
+
const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });
|
|
9494
|
+
return mapLayer(this.startLayers, layer.id, (l) => ({ ...l, placement, surface }));
|
|
9495
|
+
} catch (e) {
|
|
9496
|
+
if (e instanceof TransformTooLargeError) fail3(e.message);
|
|
9497
|
+
throw e;
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
/** Whether the geometry (or the interpolation of a smart object) differs from where it started. */
|
|
9501
|
+
isChanged() {
|
|
9502
|
+
const g = this.geometryValue;
|
|
9503
|
+
if (!this.smart) return !isIdentityTransform(g);
|
|
9504
|
+
const a = this.startGeometry;
|
|
9505
|
+
const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);
|
|
9506
|
+
const warp = (w, box) => w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : "";
|
|
9507
|
+
return !same || warp(g.warp, g.box) !== warp(a.warp, a.box) || this.interp !== this.startInterp;
|
|
9508
|
+
}
|
|
8650
9509
|
render(preview) {
|
|
8651
9510
|
const { layers, selection } = this.resample(false, preview);
|
|
8652
9511
|
this.host.show(layers, selection, this.copyId ?? this.startActive);
|
|
@@ -8655,8 +9514,7 @@ var FreeTransformSession = class {
|
|
|
8655
9514
|
/** Finishes the transform with full-quality resampling, as one undo step. */
|
|
8656
9515
|
commit() {
|
|
8657
9516
|
this.check();
|
|
8658
|
-
const
|
|
8659
|
-
const changed = !isIdentityTransform(g) || Boolean(this.copyId);
|
|
9517
|
+
const changed = this.isChanged() || Boolean(this.copyId);
|
|
8660
9518
|
if (!changed) {
|
|
8661
9519
|
this.closed = true;
|
|
8662
9520
|
this.host.restore(this.before);
|
|
@@ -8679,12 +9537,13 @@ var FreeTransformSession = class {
|
|
|
8679
9537
|
const id = this.copyId ?? this.layerId;
|
|
8680
9538
|
const layer = id ? findLayer(layers, id)?.layer : void 0;
|
|
8681
9539
|
if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
|
|
9540
|
+
else if (this.moves.includes("pixels") && layer?.type === "smart") bounds = contentBounds(layer.surface);
|
|
8682
9541
|
else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
|
|
8683
9542
|
else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
|
|
8684
9543
|
return {
|
|
8685
9544
|
layerId: this.copyId ?? this.layerId,
|
|
8686
9545
|
content: this.content,
|
|
8687
|
-
moved: [...this.moves],
|
|
9546
|
+
moved: this.smart ? this.moves.filter((m) => m !== "mask" || this.smartMasksMoved) : [...this.moves],
|
|
8688
9547
|
box: { ...this.box },
|
|
8689
9548
|
corners: this.geometryValue.corners,
|
|
8690
9549
|
bounds,
|
|
@@ -8694,6 +9553,39 @@ var FreeTransformSession = class {
|
|
|
8694
9553
|
};
|
|
8695
9554
|
|
|
8696
9555
|
// src/image-editor/controller.ts
|
|
9556
|
+
function prepareNestedForPsd(layers) {
|
|
9557
|
+
const out = [];
|
|
9558
|
+
for (const l of layers) {
|
|
9559
|
+
if (l.type === "annotation") continue;
|
|
9560
|
+
let next = l;
|
|
9561
|
+
if (l.type === "group") next = { ...l, children: prepareNestedForPsd(l.children) };
|
|
9562
|
+
else if (l.type === "smart") next = rasterizeSmartObject([l], l.id).layer;
|
|
9563
|
+
else if (l.type === "pixel" && l.filters?.length) {
|
|
9564
|
+
const p = { ...l };
|
|
9565
|
+
if (l.filters.some(isActiveFilter)) {
|
|
9566
|
+
const b = l.surface.bounds;
|
|
9567
|
+
if (b) {
|
|
9568
|
+
const r = { x: b.x + l.offset.x - 64, y: b.y + l.offset.y - 64, width: b.width + 128, height: b.height + 128 };
|
|
9569
|
+
const plain3 = { ...l, visible: true, opacity: 1, blend: "normal" };
|
|
9570
|
+
delete plain3.mask;
|
|
9571
|
+
delete plain3.clipped;
|
|
9572
|
+
delete plain3.effects;
|
|
9573
|
+
delete plain3.fillOpacity;
|
|
9574
|
+
const data = new LayerCompositor().composite([plain3], r, { width: r.x + r.width, height: r.y + r.height });
|
|
9575
|
+
p.surface = surfaceFromPixels({ width: r.width, height: r.height, data }, 4, r.x - l.offset.x, r.y - l.offset.y);
|
|
9576
|
+
}
|
|
9577
|
+
}
|
|
9578
|
+
delete p.filters;
|
|
9579
|
+
next = p;
|
|
9580
|
+
}
|
|
9581
|
+
if (next.mask?.source) {
|
|
9582
|
+
next = { ...next };
|
|
9583
|
+
delete next.mask;
|
|
9584
|
+
}
|
|
9585
|
+
out.push(next);
|
|
9586
|
+
}
|
|
9587
|
+
return out;
|
|
9588
|
+
}
|
|
8697
9589
|
var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
|
|
8698
9590
|
function paintCoverageInto(base, coverage, apply, rects) {
|
|
8699
9591
|
const tiles = new Map(base.tiles);
|
|
@@ -8994,6 +9886,7 @@ var ImageEditorController = class {
|
|
|
8994
9886
|
* for undoable source replacement.
|
|
8995
9887
|
*/
|
|
8996
9888
|
async loadImage(src) {
|
|
9889
|
+
this.abandonContents();
|
|
8997
9890
|
if (typeof src === "string") this.state.sourceUrl = src;
|
|
8998
9891
|
const source = await this.resolveSource(src);
|
|
8999
9892
|
const prev = this.state.imageDimensions;
|
|
@@ -9283,7 +10176,7 @@ var ImageEditorController = class {
|
|
|
9283
10176
|
setActiveLayer(id, target = "pixels") {
|
|
9284
10177
|
if (id !== null && !this.getLayer(id)) return false;
|
|
9285
10178
|
const layer = id ? this.getLayer(id) : void 0;
|
|
9286
|
-
const t = target === "mask" && layer?.mask ? "mask" : "pixels";
|
|
10179
|
+
const t = target === "mask" && layer?.mask ? "mask" : target === "filter-mask" && layer?.type === "smart" && layer.smartFilters?.mask ? "filter-mask" : "pixels";
|
|
9287
10180
|
if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;
|
|
9288
10181
|
this.state.activeLayerId = id;
|
|
9289
10182
|
this.state.activeTarget = t;
|
|
@@ -9296,7 +10189,7 @@ var ImageEditorController = class {
|
|
|
9296
10189
|
*/
|
|
9297
10190
|
getLayerPixels(id) {
|
|
9298
10191
|
const layer = this.getLayer(id);
|
|
9299
|
-
if (!layer || layer.type !== "pixel") return null;
|
|
10192
|
+
if (!layer || layer.type !== "pixel" && layer.type !== "smart") return null;
|
|
9300
10193
|
const surface = this.surfaceOf(layer);
|
|
9301
10194
|
const b = contentBounds(surface);
|
|
9302
10195
|
if (!b) return null;
|
|
@@ -9309,7 +10202,7 @@ var ImageEditorController = class {
|
|
|
9309
10202
|
*/
|
|
9310
10203
|
getLayerBounds(id) {
|
|
9311
10204
|
const layer = this.getLayer(id);
|
|
9312
|
-
if (!layer || layer.type !== "pixel") return null;
|
|
10205
|
+
if (!layer || layer.type !== "pixel" && layer.type !== "smart") return null;
|
|
9313
10206
|
const surface = this.surfaceOf(layer);
|
|
9314
10207
|
let b = this.boundsCache.get(surface);
|
|
9315
10208
|
if (b === void 0) {
|
|
@@ -9358,9 +10251,9 @@ var ImageEditorController = class {
|
|
|
9358
10251
|
}
|
|
9359
10252
|
return String(id);
|
|
9360
10253
|
}
|
|
9361
|
-
/** The pixels a pixel layer composites (the decoded source for the Background). */
|
|
10254
|
+
/** The pixels a pixel layer or smart object composites (the decoded source for the Background). */
|
|
9362
10255
|
surfaceOf(layer) {
|
|
9363
|
-
if (layer.source !== "document") return layer.surface;
|
|
10256
|
+
if (layer.type === "smart" || layer.source !== "document") return layer.surface;
|
|
9364
10257
|
return this.sourceSurface() ?? layer.surface;
|
|
9365
10258
|
}
|
|
9366
10259
|
/**
|
|
@@ -9604,6 +10497,15 @@ var ImageEditorController = class {
|
|
|
9604
10497
|
*/
|
|
9605
10498
|
offsetLayer(id, move, commitToHistory = true) {
|
|
9606
10499
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
10500
|
+
if (layer.type === "smart") {
|
|
10501
|
+
const tl = layer.placement.corners[0];
|
|
10502
|
+
const dx = move.x !== void 0 ? move.x - tl.x : move.dx ?? 0;
|
|
10503
|
+
const dy = move.y !== void 0 ? move.y - tl.y : move.dy ?? 0;
|
|
10504
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError("x and y must be numbers.");
|
|
10505
|
+
this.commitLayers(offsetSmartObject(this.state.layers, id, dx, dy, this.canvasSize), void 0, commitToHistory);
|
|
10506
|
+
const moved = findLayer(this.state.layers, id).layer;
|
|
10507
|
+
return { offset: { x: Math.round(moved.placement.corners[0].x), y: Math.round(moved.placement.corners[0].y) }, bounds: this.getLayerBounds(id) };
|
|
10508
|
+
}
|
|
9607
10509
|
const current = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
9608
10510
|
const x = move.x ?? current.x + (move.dx ?? 0);
|
|
9609
10511
|
const y = move.y ?? current.y + (move.dy ?? 0);
|
|
@@ -9810,78 +10712,476 @@ var ImageEditorController = class {
|
|
|
9810
10712
|
const [a, b, c, d] = spec.matrix;
|
|
9811
10713
|
if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError("matrix is singular (it collapses the layer to a line); its determinant a\xB7d \u2212 b\xB7c must not be 0.");
|
|
9812
10714
|
}
|
|
9813
|
-
const session = this.beginFreeTransform(spec);
|
|
9814
|
-
try {
|
|
9815
|
-
const g =
|
|
9816
|
-
const corners = spec.matrix ?
|
|
9817
|
-
let warp = null;
|
|
9818
|
-
if (spec.warp) {
|
|
9819
|
-
if ("style" in spec.warp) {
|
|
9820
|
-
if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
|
|
9821
|
-
warp = presetWarp(session.box, spec.warp);
|
|
9822
|
-
} else {
|
|
9823
|
-
if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points, row by row.");
|
|
9824
|
-
warp = spec.warp;
|
|
9825
|
-
}
|
|
10715
|
+
const session = this.beginFreeTransform(spec);
|
|
10716
|
+
try {
|
|
10717
|
+
const g = session.startGeometry;
|
|
10718
|
+
const corners = spec.matrix ? g.corners.map((p) => applyMatrix(spec.matrix, p)) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;
|
|
10719
|
+
let warp = spec.warp === void 0 ? g.warp ?? null : null;
|
|
10720
|
+
if (spec.warp) {
|
|
10721
|
+
if ("style" in spec.warp) {
|
|
10722
|
+
if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
|
|
10723
|
+
warp = presetWarp(session.box, spec.warp);
|
|
10724
|
+
} else {
|
|
10725
|
+
if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points, row by row.");
|
|
10726
|
+
warp = spec.warp;
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
10729
|
+
session.update({ corners, warp }, false);
|
|
10730
|
+
return session.commit();
|
|
10731
|
+
} catch (e) {
|
|
10732
|
+
session.cancel();
|
|
10733
|
+
throw e;
|
|
10734
|
+
}
|
|
10735
|
+
}
|
|
10736
|
+
/** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */
|
|
10737
|
+
flipLayer(axis, init = {}) {
|
|
10738
|
+
if (axis !== "horizontal" && axis !== "vertical") throw new LayerOperationError(`axis must be horizontal or vertical, got "${String(axis)}".`);
|
|
10739
|
+
const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
|
|
10740
|
+
try {
|
|
10741
|
+
session.update({ corners: flipCorners(session.startGeometry, axis) }, false);
|
|
10742
|
+
return session.commit();
|
|
10743
|
+
} catch (e) {
|
|
10744
|
+
session.cancel();
|
|
10745
|
+
throw e;
|
|
10746
|
+
}
|
|
10747
|
+
}
|
|
10748
|
+
/**
|
|
10749
|
+
* Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's
|
|
10750
|
+
* centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).
|
|
10751
|
+
*/
|
|
10752
|
+
rotateLayer90(turn, init = {}) {
|
|
10753
|
+
if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
|
|
10754
|
+
const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
|
|
10755
|
+
try {
|
|
10756
|
+
const b = session.smartObject ? transformedBox(session.startGeometry) ?? session.box : session.box;
|
|
10757
|
+
const cx = b.x + b.width / 2;
|
|
10758
|
+
const cy = b.y + b.height / 2;
|
|
10759
|
+
const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
|
|
10760
|
+
const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);
|
|
10761
|
+
const m = turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];
|
|
10762
|
+
session.update({ corners: session.startGeometry.corners.map((p) => applyMatrix(m, p)) }, false);
|
|
10763
|
+
return session.commit();
|
|
10764
|
+
} catch (e) {
|
|
10765
|
+
session.cancel();
|
|
10766
|
+
throw e;
|
|
10767
|
+
}
|
|
10768
|
+
}
|
|
10769
|
+
/** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */
|
|
10770
|
+
addLayerFilter(id, filter) {
|
|
10771
|
+
const f = normalizeFilter(filter);
|
|
10772
|
+
const next = addLayerFilter(this.state.layers, id, f);
|
|
10773
|
+
this.commitLayers(next);
|
|
10774
|
+
return findLayer(next, id).layer.filters ?? [];
|
|
10775
|
+
}
|
|
10776
|
+
/** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */
|
|
10777
|
+
updateLayerFilter(id, filterId, changes, commitToHistory = true) {
|
|
10778
|
+
const next = updateLayerFilter(this.state.layers, id, filterId, changes);
|
|
10779
|
+
this.commitLayers(next, void 0, commitToHistory);
|
|
10780
|
+
return findLayer(next, id).layer.filters ?? [];
|
|
10781
|
+
}
|
|
10782
|
+
/** Removes one filter (one undo step). Returns the remaining filters. */
|
|
10783
|
+
removeLayerFilter(id, filterId) {
|
|
10784
|
+
const next = removeLayerFilter(this.state.layers, id, filterId);
|
|
10785
|
+
this.commitLayers(next);
|
|
10786
|
+
return findLayer(next, id).layer.filters ?? [];
|
|
10787
|
+
}
|
|
10788
|
+
// --- Smart objects ---
|
|
10789
|
+
get canvasSize() {
|
|
10790
|
+
return this.naturalSize;
|
|
10791
|
+
}
|
|
10792
|
+
/**
|
|
10793
|
+
* Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the
|
|
10794
|
+
* active one) become one smart object at the topmost one's position. A single pixel layer keeps
|
|
10795
|
+
* its id and properties and its filters become smart filters (it renders exactly as before);
|
|
10796
|
+
* anything else nests as a layer document. Returns the smart object's id.
|
|
10797
|
+
*/
|
|
10798
|
+
convertToSmartObject(ids, options = {}) {
|
|
10799
|
+
const list2 = ids?.length ? ids : this.state.activeLayerId ? [this.state.activeLayerId] : [];
|
|
10800
|
+
if (!list2.length) throw new LayerOperationError("No layers given and none is active; pass layerIds (see list_layers).");
|
|
10801
|
+
const res = convertToSmartObject(this.state.layers, list2, this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l), name: options.name });
|
|
10802
|
+
this.commitLayers(res.layers, () => {
|
|
10803
|
+
this.state.activeLayerId = res.layer.id;
|
|
10804
|
+
this.state.activeTarget = "pixels";
|
|
10805
|
+
});
|
|
10806
|
+
return res.layer.id;
|
|
10807
|
+
}
|
|
10808
|
+
/** Layer › Smart Objects › Rasterize (one undo step): the smart object becomes a pixel layer holding what it renders. */
|
|
10809
|
+
rasterizeSmartObject(id) {
|
|
10810
|
+
const res = rasterizeSmartObject(this.state.layers, id);
|
|
10811
|
+
this.commitLayers(res.layers, () => {
|
|
10812
|
+
if (this.state.activeLayerId === id && this.state.activeTarget === "filter-mask") this.state.activeTarget = "pixels";
|
|
10813
|
+
});
|
|
10814
|
+
return id;
|
|
10815
|
+
}
|
|
10816
|
+
/** A smart object's contents, placement and smart filters as plain data (see `SmartObjectInfo`). */
|
|
10817
|
+
getSmartObject(id) {
|
|
10818
|
+
const layer = requireSmartObject(this.state.layers, id);
|
|
10819
|
+
const c = layer.contents;
|
|
10820
|
+
const g = placementGeometry(layer);
|
|
10821
|
+
const readout = describeTransform(g);
|
|
10822
|
+
const stack = layer.smartFilters;
|
|
10823
|
+
return {
|
|
10824
|
+
layerId: layer.id,
|
|
10825
|
+
contents: {
|
|
10826
|
+
id: c.id,
|
|
10827
|
+
name: c.name,
|
|
10828
|
+
kind: c.kind,
|
|
10829
|
+
width: c.width,
|
|
10830
|
+
height: c.height,
|
|
10831
|
+
decoded: isContentsDecoded(c),
|
|
10832
|
+
fileType: c.file?.type ?? null,
|
|
10833
|
+
linkedPath: c.linked?.path ?? null,
|
|
10834
|
+
instances: instancesOf(this.state.layers, c.id),
|
|
10835
|
+
layerCount: c.document ? flattenLayers(c.document.layers).length : 0
|
|
10836
|
+
},
|
|
10837
|
+
placement: {
|
|
10838
|
+
corners: g.corners.map((p) => ({ x: p.x, y: p.y })),
|
|
10839
|
+
warp: layer.placement.warp ?? null,
|
|
10840
|
+
interpolation: layer.placement.interpolation ?? "bicubic",
|
|
10841
|
+
affine: readout.affine,
|
|
10842
|
+
scaleX: readout.scaleX,
|
|
10843
|
+
scaleY: readout.scaleY,
|
|
10844
|
+
rotate: readout.rotate,
|
|
10845
|
+
skewX: readout.skewX,
|
|
10846
|
+
center: anchorPoint(g, "center"),
|
|
10847
|
+
bounds: transformedBox(g)
|
|
10848
|
+
},
|
|
10849
|
+
smartFilters: {
|
|
10850
|
+
enabled: stack?.enabled ?? true,
|
|
10851
|
+
filters: (stack?.filters ?? []).map((f) => f.kind === "psd" ? { id: f.id, kind: "psd", name: f.name, enabled: f.enabled, blend: f.blend, opacity: f.opacity } : { ...f }),
|
|
10852
|
+
mask: stack?.mask ? { enabled: stack.mask.enabled !== false, linked: stack.mask.linked !== false, density: stack.mask.density ?? 1, feather: stack.mask.feather ?? 0, bounds: contentBounds(stack.mask.surface), defaultValue: stack.mask.defaultValue } : null
|
|
10853
|
+
},
|
|
10854
|
+
bounds: this.getLayerBounds(id)
|
|
10855
|
+
};
|
|
10856
|
+
}
|
|
10857
|
+
/**
|
|
10858
|
+
* Sets a smart object's placement (one undo step, or none with `commitToHistory = false` during a
|
|
10859
|
+
* drag): where the contents' corners land, an optional warp in contents pixels and the
|
|
10860
|
+
* interpolation. The contents render again from the source; a linked mask follows a warp-free
|
|
10861
|
+
* change. Returns the new placement summary.
|
|
10862
|
+
*/
|
|
10863
|
+
setSmartObjectPlacement(id, placement, commitToHistory = true) {
|
|
10864
|
+
const res = setSmartPlacement(this.state.layers, id, placement, this.canvasSize);
|
|
10865
|
+
this.commitLayers(res.layers, void 0, commitToHistory);
|
|
10866
|
+
return { ...this.getSmartObject(id).placement, masksMoved: res.masksMoved };
|
|
10867
|
+
}
|
|
10868
|
+
/**
|
|
10869
|
+
* Sets a smart object's transform absolutely (one undo step): exactly one of `corners` (where the
|
|
10870
|
+
* contents' corners land), `matrix` (contents pixels → document) or the numbers (`scaleX`,
|
|
10871
|
+
* `scaleY` in percent of the contents' own size, `rotate`, `skewX`, `skewY`, and `x`, `y` where
|
|
10872
|
+
* the contents' centre goes, default where it is now); plus, optionally, a warp (a preset, a mesh
|
|
10873
|
+
* in contents pixels, or `null` to remove it) and the interpolation. Unlike `transformLayer`,
|
|
10874
|
+
* nothing is relative to the current transform.
|
|
10875
|
+
*/
|
|
10876
|
+
setSmartObjectTransform(id, spec) {
|
|
10877
|
+
const layer = requireSmartObject(this.state.layers, id);
|
|
10878
|
+
const g = placementGeometry(layer);
|
|
10879
|
+
const numeric = ["scaleX", "scaleY", "rotate", "skewX", "skewY", "x", "y"].filter((k) => spec[k] !== void 0);
|
|
10880
|
+
const ways = [spec.corners !== void 0, spec.matrix !== void 0, numeric.length > 0].filter(Boolean).length;
|
|
10881
|
+
if (ways > 1) throw new LayerOperationError("Give the transform only one way: corners, matrix, or the numbers (scaleX, scaleY, rotate, skewX, skewY, x, y).");
|
|
10882
|
+
let corners = g.corners;
|
|
10883
|
+
if (spec.corners) corners = spec.corners;
|
|
10884
|
+
else if (spec.matrix) {
|
|
10885
|
+
if (!(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) throw new LayerOperationError("matrix must be six finite numbers [a, b, c, d, e, f] (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f).");
|
|
10886
|
+
const [a, b, c, d] = spec.matrix;
|
|
10887
|
+
if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError("matrix is singular (it collapses the contents to a line); a\xB7d \u2212 b\xB7c must not be 0.");
|
|
10888
|
+
corners = cornersFromMatrix(g.box, spec.matrix);
|
|
10889
|
+
} else if (numeric.length) {
|
|
10890
|
+
if (spec.scaleX === 0 || spec.scaleY === 0) throw new LayerOperationError("scaleX and scaleY must not be 0 (that collapses the contents to a line).");
|
|
10891
|
+
for (const k of numeric) if (!Number.isFinite(spec[k])) throw new LayerOperationError(`${k} must be a finite number.`);
|
|
10892
|
+
const now = describeTransform(g);
|
|
10893
|
+
const centre = anchorPoint(g, "center");
|
|
10894
|
+
const params = {
|
|
10895
|
+
scaleX: spec.scaleX ?? (now.affine ? now.scaleX : 100),
|
|
10896
|
+
scaleY: spec.scaleY ?? (now.affine ? now.scaleY : 100),
|
|
10897
|
+
rotate: spec.rotate ?? (now.affine ? now.rotate : 0),
|
|
10898
|
+
skewX: spec.skewX ?? (now.affine ? now.skewX : 0),
|
|
10899
|
+
skewY: spec.skewY ?? 0
|
|
10900
|
+
};
|
|
10901
|
+
const m = matrixFromParams(g.box, { ...params, reference: "center" });
|
|
10902
|
+
const cx = g.box.width / 2;
|
|
10903
|
+
const cy = g.box.height / 2;
|
|
10904
|
+
const to = { x: spec.x ?? centre.x, y: spec.y ?? centre.y };
|
|
10905
|
+
corners = cornersFromMatrix(g.box, multiplyMatrices(m, translateMatrix(to.x - cx, to.y - cy)));
|
|
10906
|
+
}
|
|
10907
|
+
let warp = layer.placement.warp ?? null;
|
|
10908
|
+
if (spec.warp === null) warp = null;
|
|
10909
|
+
else if (spec.warp) {
|
|
10910
|
+
if ("style" in spec.warp) {
|
|
10911
|
+
if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
|
|
10912
|
+
warp = presetWarp(g.box, spec.warp);
|
|
10913
|
+
} else {
|
|
10914
|
+
if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points in contents pixels, row by row.");
|
|
10915
|
+
warp = spec.warp;
|
|
10916
|
+
}
|
|
10917
|
+
}
|
|
10918
|
+
const next = { corners, warp, interpolation: spec.interpolation ?? layer.placement.interpolation };
|
|
10919
|
+
return this.setSmartObjectPlacement(id, next);
|
|
10920
|
+
}
|
|
10921
|
+
/**
|
|
10922
|
+
* Layer › Smart Objects › Replace Contents (one undo step): new contents for the smart object and
|
|
10923
|
+
* every instance linked to it, from RGBA pixels, PSD bytes (a layer document) or contents. Each
|
|
10924
|
+
* instance keeps its scale and rotation about the contents' centre. Returns the contents id and
|
|
10925
|
+
* the layers that changed.
|
|
10926
|
+
*/
|
|
10927
|
+
replaceSmartObjectContents(id, source) {
|
|
10928
|
+
const layer = requireSmartObject(this.state.layers, id);
|
|
10929
|
+
let contents;
|
|
10930
|
+
if ("contents" in source) contents = source.contents;
|
|
10931
|
+
else if ("pixels" in source) contents = rasterContents(source.pixels, source.name ?? layer.contents.name.replace(/\.\w+$/, ".png"));
|
|
10932
|
+
else {
|
|
10933
|
+
const psd = importPsd(source.psd, { newId: () => newLayerId() });
|
|
10934
|
+
contents = documentContents({ layers: psd.layers, globalLight: psd.globalLight, patterns: psd.patterns.length ? psd.patterns : void 0 }, psd.width, psd.height, source.name ?? `${layer.name || "Contents"}.psb`);
|
|
10935
|
+
contents.file = { bytes: source.psd.slice(), type: psd.info.psb ? "psb" : "psd" };
|
|
10936
|
+
}
|
|
10937
|
+
const res = replaceContents(this.state.layers, layer.contents.id, contents, this.canvasSize);
|
|
10938
|
+
this.commitLayers(res.layers);
|
|
10939
|
+
return { contentsId: layer.contents.id, width: contents.width, height: contents.height, layerIds: res.ids };
|
|
10940
|
+
}
|
|
10941
|
+
/** `replaceSmartObjectContents` from an image (PNG data URL or base64 decodes anywhere; other formats need a browser). */
|
|
10942
|
+
async replaceSmartObjectContentsFromImage(id, image, name) {
|
|
10943
|
+
const pixels = await this.decodeImage(image);
|
|
10944
|
+
return this.replaceSmartObjectContents(id, { pixels, name });
|
|
10945
|
+
}
|
|
10946
|
+
/**
|
|
10947
|
+
* Layer › Smart Objects › Export Contents: the file the contents came from while unedited, else a
|
|
10948
|
+
* PNG (raster contents) or a PSD / PSB of the nested document. Returns the file name, MIME type
|
|
10949
|
+
* and bytes.
|
|
10950
|
+
*/
|
|
10951
|
+
exportSmartObjectContents(id) {
|
|
10952
|
+
const layer = requireSmartObject(this.state.layers, id);
|
|
10953
|
+
const c = layer.contents;
|
|
10954
|
+
const base = c.name.replace(/\.\w+$/, "") || "Contents";
|
|
10955
|
+
if (c.file) {
|
|
10956
|
+
const mime = { psd: "image/vnd.adobe.photoshop", psb: "image/vnd.adobe.photoshop", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", tif: "image/tiff", tiff: "image/tiff", gif: "image/gif" };
|
|
10957
|
+
const ext = c.file.type.toLowerCase();
|
|
10958
|
+
return { name: /\.\w+$/.test(c.name) ? c.name : `${base}.${ext}`, mimeType: mime[ext] ?? "application/octet-stream", bytes: c.file.bytes };
|
|
10959
|
+
}
|
|
10960
|
+
if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of "${layer.name || id}" are not available here.`);
|
|
10961
|
+
if (c.kind === "raster") {
|
|
10962
|
+
const data = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });
|
|
10963
|
+
return { name: `${base}.png`, mimeType: "image/png", bytes: encodePng(c.width, c.height, data) };
|
|
10964
|
+
}
|
|
10965
|
+
const doc = c.document;
|
|
10966
|
+
const merged = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });
|
|
10967
|
+
const prepared = prepareNestedForPsd(doc.layers);
|
|
10968
|
+
const res = exportPsd({ width: c.width, height: c.height, layers: prepared, merged, patterns: doc.patterns, globalLight: doc.globalLight });
|
|
10969
|
+
return { name: `${base}.${res.psb ? "psb" : "psd"}`, mimeType: "image/vnd.adobe.photoshop", bytes: res.bytes };
|
|
10970
|
+
}
|
|
10971
|
+
/**
|
|
10972
|
+
* Layer › Smart Objects › New Smart Object via Copy (`linked: false`: independent contents) or a
|
|
10973
|
+
* linked duplicate (`linked: true`: edits to the contents show in both), directly above, as one
|
|
10974
|
+
* undo step. Returns the copy's id.
|
|
10975
|
+
*/
|
|
10976
|
+
copySmartObject(id, linked = false, name) {
|
|
10977
|
+
const res = copySmartObject(this.state.layers, id, linked, name);
|
|
10978
|
+
this.commitLayers(res.layers, () => {
|
|
10979
|
+
this.state.activeLayerId = res.layer.id;
|
|
10980
|
+
this.state.activeTarget = "pixels";
|
|
10981
|
+
});
|
|
10982
|
+
return res.layer.id;
|
|
10983
|
+
}
|
|
10984
|
+
/** Adds a smart filter (`develop` or `blur`, with `blend` and `opacity`) at `index` (default the top), as one undo step. */
|
|
10985
|
+
addSmartFilter(id, filter, index) {
|
|
10986
|
+
const res = addSmartFilter(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);
|
|
10987
|
+
this.commitLayers(res.layers);
|
|
10988
|
+
return res.filter;
|
|
10989
|
+
}
|
|
10990
|
+
/** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */
|
|
10991
|
+
updateSmartFilter(id, filterId, changes, commitToHistory = true) {
|
|
10992
|
+
const res = updateSmartFilter(this.state.layers, id, filterId, changes, this.canvasSize);
|
|
10993
|
+
this.commitLayers(res.layers, void 0, commitToHistory);
|
|
10994
|
+
return res.filter;
|
|
10995
|
+
}
|
|
10996
|
+
/** Removes one smart filter (one undo step). Returns the remaining filters. */
|
|
10997
|
+
removeSmartFilter(id, filterId) {
|
|
10998
|
+
const res = removeSmartFilter(this.state.layers, id, filterId, this.canvasSize);
|
|
10999
|
+
this.commitLayers(res.layers);
|
|
11000
|
+
return res.layer.smartFilters?.filters ?? [];
|
|
11001
|
+
}
|
|
11002
|
+
/** Moves one smart filter to `index` (0 = bottom, applied first), as one undo step. Returns the filters. */
|
|
11003
|
+
moveSmartFilter(id, filterId, index) {
|
|
11004
|
+
const res = moveSmartFilter(this.state.layers, id, filterId, index, this.canvasSize);
|
|
11005
|
+
this.commitLayers(res.layers);
|
|
11006
|
+
return res.layer.smartFilters?.filters ?? [];
|
|
11007
|
+
}
|
|
11008
|
+
/** Turns a smart object's whole filter stack on or off (one undo step). */
|
|
11009
|
+
setSmartFiltersEnabled(id, enabled) {
|
|
11010
|
+
const res = setSmartFiltersEnabled(this.state.layers, id, enabled, this.canvasSize);
|
|
11011
|
+
this.commitLayers(res.layers);
|
|
11012
|
+
return res.layer.smartFilters?.enabled ?? true;
|
|
11013
|
+
}
|
|
11014
|
+
/**
|
|
11015
|
+
* Sets the smart filter mask (one undo step): `reveal-all`, `hide-all`, `selection` (the
|
|
11016
|
+
* selection's coverage), or `none` to remove it; `props` sets density, feather, enabled and linked.
|
|
11017
|
+
* Painting it targets channel `filter-mask`. Returns the mask, or `null` when removed.
|
|
11018
|
+
*/
|
|
11019
|
+
setSmartFilterMask(id, from, props = {}, commitToHistory = true) {
|
|
11020
|
+
const layer = requireSmartObject(this.state.layers, id);
|
|
11021
|
+
let mask;
|
|
11022
|
+
if (from === "none") mask = null;
|
|
11023
|
+
else if (from === "keep") {
|
|
11024
|
+
if (!layer.smartFilters?.mask) throw new LayerOperationError(`Smart object "${layer.name || id}" has no filter mask; add one with from reveal-all, hide-all or selection.`);
|
|
11025
|
+
mask = withMaskProps(layer.smartFilters.mask, props);
|
|
11026
|
+
} else if (from === "selection") {
|
|
11027
|
+
const sel = this.state.selection;
|
|
11028
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, or use reveal-all / hide-all.");
|
|
11029
|
+
mask = this.maskFromSelection(sel, props);
|
|
11030
|
+
} else {
|
|
11031
|
+
const v = from === "reveal-all" ? 255 : 0;
|
|
11032
|
+
mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);
|
|
11033
|
+
}
|
|
11034
|
+
const res = setSmartFilterMask(this.state.layers, id, mask, this.canvasSize);
|
|
11035
|
+
this.commitLayers(res.layers, () => {
|
|
11036
|
+
if (this.state.activeLayerId !== id) return;
|
|
11037
|
+
if (mask && from !== "keep") this.state.activeTarget = "filter-mask";
|
|
11038
|
+
else if (!mask && this.state.activeTarget === "filter-mask") this.state.activeTarget = "pixels";
|
|
11039
|
+
}, commitToHistory);
|
|
11040
|
+
return res.layer.smartFilters?.mask ?? null;
|
|
11041
|
+
}
|
|
11042
|
+
/**
|
|
11043
|
+
* Decodes smart object contents that came as files the synchronous decoders cannot read (a JPEG
|
|
11044
|
+
* or TIFF embedded in a PSD), through the browser's image decoder. No undo step: the pixels on
|
|
11045
|
+
* screen do not change. Returns the contents ids still undecoded.
|
|
11046
|
+
*/
|
|
11047
|
+
async prepareSmartObjects() {
|
|
11048
|
+
const pending = /* @__PURE__ */ new Map();
|
|
11049
|
+
for (const l of flattenLayers(this.state.layers)) if (l.type === "smart" && !isContentsDecoded(l.contents) && l.contents.file) pending.set(l.contents.id, l.contents);
|
|
11050
|
+
const left = [];
|
|
11051
|
+
for (const [cid, c] of pending) {
|
|
11052
|
+
try {
|
|
11053
|
+
const url = `data:${c.file.type === "png" ? "image/png" : c.file.type === "gif" ? "image/gif" : c.file.type === "tif" || c.file.type === "tiff" ? "image/tiff" : "image/jpeg"};base64,${bytesToBase64(c.file.bytes)}`;
|
|
11054
|
+
const px2 = await this.decodeImage(url);
|
|
11055
|
+
const decoded2 = { ...c, kind: "raster", raster: rasterContents(px2, c.name, cid).raster, width: px2.width, height: px2.height };
|
|
11056
|
+
const swap = (layers) => {
|
|
11057
|
+
let out = layers;
|
|
11058
|
+
for (const l of flattenLayers(layers)) if (l.type === "smart" && l.contents.id === cid) out = mapLayer(out, l.id, (x) => ({ ...x, contents: decoded2 }));
|
|
11059
|
+
return out;
|
|
11060
|
+
};
|
|
11061
|
+
this.state.layers = swap(this.state.layers);
|
|
11062
|
+
for (const e of [...this.undoStack, ...this.redoStack]) e.layers = swap(e.layers);
|
|
11063
|
+
} catch {
|
|
11064
|
+
left.push(cid);
|
|
9826
11065
|
}
|
|
9827
|
-
session.update({ corners, warp }, false);
|
|
9828
|
-
return session.commit();
|
|
9829
|
-
} catch (e) {
|
|
9830
|
-
session.cancel();
|
|
9831
|
-
throw e;
|
|
9832
11066
|
}
|
|
11067
|
+
if (pending.size) this.notify();
|
|
11068
|
+
return left;
|
|
9833
11069
|
}
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
session.update({ corners: flipCorners(identityTransform(session.box), axis) }, false);
|
|
9840
|
-
return session.commit();
|
|
9841
|
-
} catch (e) {
|
|
9842
|
-
session.cancel();
|
|
9843
|
-
throw e;
|
|
9844
|
-
}
|
|
11070
|
+
// --- Smart object contents (Edit Contents) ---
|
|
11071
|
+
contentsStack = [];
|
|
11072
|
+
/** The smart objects whose contents are open (outermost first); empty when the document itself is shown. */
|
|
11073
|
+
getEditingContents() {
|
|
11074
|
+
return this.contentsStack.map((e) => ({ layerId: e.layerId, name: e.name }));
|
|
9845
11075
|
}
|
|
9846
11076
|
/**
|
|
9847
|
-
*
|
|
9848
|
-
*
|
|
11077
|
+
* Layer › Smart Objects › Edit Contents: shows the smart object's contents as a document of their
|
|
11078
|
+
* own (a layer document, or raster contents as one pixel layer), with their own undo history.
|
|
11079
|
+
* Every editor operation and tool then works on the contents until `closeSmartObjectContents`.
|
|
11080
|
+
* Contents can nest. Returns the contents' size and layer ids.
|
|
9849
11081
|
*/
|
|
9850
|
-
|
|
9851
|
-
if (
|
|
9852
|
-
const
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
}
|
|
11082
|
+
editSmartObjectContents(id) {
|
|
11083
|
+
if (this.getFreeTransform()) throw new LayerOperationError("A Free Transform is open; commit or cancel it first.");
|
|
11084
|
+
const layer = requireSmartObject(this.state.layers, id);
|
|
11085
|
+
const c = layer.contents;
|
|
11086
|
+
if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of "${layer.name || id}" are a ${c.file?.type ?? "file"} that could not be decoded here; run prepareSmartObjects() in a browser first.`);
|
|
11087
|
+
const saved = {
|
|
11088
|
+
state: { ...this.state },
|
|
11089
|
+
undo: this.undoStack,
|
|
11090
|
+
redo: this.redoStack,
|
|
11091
|
+
initial: this.initialSnapshot,
|
|
11092
|
+
element: this.imageElement,
|
|
11093
|
+
samplePixels: this.samplePixels,
|
|
11094
|
+
samplePixelsOverride: this.samplePixelsOverride,
|
|
11095
|
+
psdResolution: this.psdResolution
|
|
11096
|
+
};
|
|
11097
|
+
const layers = c.kind === "document" ? c.document.layers : [{ type: "pixel", id: newLayerId(), name: "Layer 0", visible: true, opacity: 1, blend: "normal", surface: c.raster, offset: { x: 0, y: 0 } }];
|
|
11098
|
+
this.contentsStack.push({ layerId: layer.id, contentsId: c.id, name: layer.name || c.name, saved });
|
|
11099
|
+
const s = this.state;
|
|
11100
|
+
this.state = {
|
|
11101
|
+
...s,
|
|
11102
|
+
sourceUrl: null,
|
|
11103
|
+
imageDimensions: { width: c.width, height: c.height },
|
|
11104
|
+
crop: null,
|
|
11105
|
+
transform: { ...DEFAULT_TRANSFORM_VALUES },
|
|
11106
|
+
layers,
|
|
11107
|
+
activeLayerId: layers.length ? layers[layers.length - 1].id : null,
|
|
11108
|
+
activeTarget: "pixels",
|
|
11109
|
+
selection: null,
|
|
11110
|
+
selectedAnnotationId: null,
|
|
11111
|
+
outputOpacity: 1,
|
|
11112
|
+
zoom: 1,
|
|
11113
|
+
pan: { x: 0, y: 0 },
|
|
11114
|
+
editingContents: this.getEditingContents()
|
|
11115
|
+
};
|
|
11116
|
+
delete this.state.legacyOutputBlur;
|
|
11117
|
+
if (c.document?.globalLight) this.state.globalLight = c.document.globalLight;
|
|
11118
|
+
else delete this.state.globalLight;
|
|
11119
|
+
if (c.document?.patterns?.length) this.state.patterns = c.document.patterns;
|
|
11120
|
+
else delete this.state.patterns;
|
|
11121
|
+
this.imageElement = null;
|
|
11122
|
+
this.samplePixels = null;
|
|
11123
|
+
this.samplePixelsOverride = false;
|
|
11124
|
+
this.undoStack = [];
|
|
11125
|
+
this.redoStack = [];
|
|
11126
|
+
this.viewKey = [];
|
|
11127
|
+
this.initialSnapshot = this.captureHistoryEntry();
|
|
11128
|
+
this.notify();
|
|
11129
|
+
return { layerId: layer.id, depth: this.contentsStack.length, width: c.width, height: c.height, layerIds: flattenLayers(layers).map((l) => l.id) };
|
|
9866
11130
|
}
|
|
9867
|
-
/**
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
11131
|
+
/**
|
|
11132
|
+
* Closes the innermost open contents: with `save` (default) the edited layers become the smart
|
|
11133
|
+
* object's contents, and it and every linked instance render again (one undo step in the
|
|
11134
|
+
* document); without, the edits are dropped. Returns the smart object and the layers that changed.
|
|
11135
|
+
*/
|
|
11136
|
+
closeSmartObjectContents(save = true) {
|
|
11137
|
+
const top = this.contentsStack.pop();
|
|
11138
|
+
if (!top) throw new LayerOperationError("No smart object contents are open; open them with edit_smart_object_contents.");
|
|
11139
|
+
if (this.openTransform?.isOpen) this.openTransform.cancel();
|
|
11140
|
+
this.openTransform = null;
|
|
11141
|
+
const edited = this.state;
|
|
11142
|
+
const changed = save && this.undoStack.length > 0;
|
|
11143
|
+
let next = null;
|
|
11144
|
+
if (changed) {
|
|
11145
|
+
const layer = findLayer(top.saved.state.layers, top.layerId)?.layer;
|
|
11146
|
+
const old = layer?.contents;
|
|
11147
|
+
const { width, height } = edited.imageDimensions;
|
|
11148
|
+
const only = edited.layers.length === 1 ? edited.layers[0] : null;
|
|
11149
|
+
const plain3 = only?.type === "pixel" && only.visible && only.opacity === 1 && only.blend === "normal" && !only.mask && !only.effects && !only.filters?.length && !only.clipped && (only.fillOpacity ?? 1) === 1 && only.source !== "document";
|
|
11150
|
+
if (old?.kind === "raster" && plain3) {
|
|
11151
|
+
const px2 = readRegion(only.surface, { x: -only.offset.x, y: -only.offset.y, width, height });
|
|
11152
|
+
next = rasterContents({ width, height, data: px2 }, old.name, old.id);
|
|
11153
|
+
} else {
|
|
11154
|
+
const doc = { layers: edited.layers, globalLight: edited.globalLight, patterns: edited.patterns };
|
|
11155
|
+
next = documentContents(doc, width, height, old?.kind === "document" ? old.name : `${(old?.name ?? "Contents").replace(/\.\w+$/, "")}.psb`, top.contentsId);
|
|
11156
|
+
}
|
|
11157
|
+
}
|
|
11158
|
+
const saved = top.saved;
|
|
11159
|
+
this.state = saved.state;
|
|
11160
|
+
this.undoStack = saved.undo;
|
|
11161
|
+
this.redoStack = saved.redo;
|
|
11162
|
+
this.initialSnapshot = saved.initial;
|
|
11163
|
+
this.imageElement = saved.element;
|
|
11164
|
+
this.samplePixels = saved.samplePixels;
|
|
11165
|
+
this.samplePixelsOverride = saved.samplePixelsOverride;
|
|
11166
|
+
this.psdResolution = saved.psdResolution;
|
|
11167
|
+
this.viewKey = [];
|
|
11168
|
+
if (this.contentsStack.length) this.state.editingContents = this.getEditingContents();
|
|
11169
|
+
else delete this.state.editingContents;
|
|
11170
|
+
if (!next) {
|
|
11171
|
+
this.notify();
|
|
11172
|
+
return { layerId: top.layerId, changed: false, layerIds: [] };
|
|
11173
|
+
}
|
|
11174
|
+
const res = replaceContents(this.state.layers, top.contentsId, next, this.canvasSize);
|
|
11175
|
+
this.commitLayers(res.layers);
|
|
11176
|
+
return { layerId: top.layerId, changed: true, layerIds: res.ids };
|
|
9873
11177
|
}
|
|
9874
|
-
/**
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
this.commitLayers(next, void 0, commitToHistory);
|
|
9878
|
-
return findLayer(next, id).layer.filters ?? [];
|
|
11178
|
+
/** The document's own layers (not the contents open for editing). */
|
|
11179
|
+
rootLayers() {
|
|
11180
|
+
return this.contentsStack[0]?.saved.state.layers ?? this.state.layers;
|
|
9879
11181
|
}
|
|
9880
|
-
/**
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
this.commitLayers(next);
|
|
9884
|
-
return findLayer(next, id).layer.filters ?? [];
|
|
11182
|
+
/** Drops every open contents edit (loading another document). */
|
|
11183
|
+
abandonContents() {
|
|
11184
|
+
while (this.contentsStack.length) this.closeSmartObjectContents(false);
|
|
9885
11185
|
}
|
|
9886
11186
|
// --- Layer effects ---
|
|
9887
11187
|
/** A layer's effects (an empty, enabled list when it has none). */
|
|
@@ -10073,7 +11373,7 @@ var ImageEditorController = class {
|
|
|
10073
11373
|
const k = Math.min(1, side / Math.max(width, height));
|
|
10074
11374
|
const tw = Math.max(1, Math.round(width * k));
|
|
10075
11375
|
const th = Math.max(1, Math.round(height * k));
|
|
10076
|
-
const surface = options.mask ? layer.mask && !layer.mask.source ? layer.mask.surface : null : layer.type === "pixel" ? this.surfaceOf(layer) : null;
|
|
11376
|
+
const surface = options.mask ? layer.mask && !layer.mask.source ? layer.mask.surface : null : layer.type === "pixel" || layer.type === "smart" ? this.surfaceOf(layer) : null;
|
|
10077
11377
|
if (!surface) return null;
|
|
10078
11378
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
10079
11379
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
@@ -10160,19 +11460,27 @@ var ImageEditorController = class {
|
|
|
10160
11460
|
if (!layer.mask) throw new LayerOperationError(`Layer "${layer.name || id}" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);
|
|
10161
11461
|
if (layer.mask.source) throw new LayerOperationError(`Layer "${layer.name || id}" has a depth mask, which is generated, not painted.`);
|
|
10162
11462
|
requireUnlocked(layer, "all", action);
|
|
11463
|
+
} else if (ch === "filter-mask") {
|
|
11464
|
+
if (layer.type !== "smart" || !layer.smartFilters?.mask) {
|
|
11465
|
+
throw new LayerOperationError(`Layer "${layer.name || id}" has no smart filter mask to ${action}; add one with set_smart_filter_mask, or target pixels or mask.`);
|
|
11466
|
+
}
|
|
11467
|
+
if (!isContentsDecoded(layer.contents)) throw new LayerOperationError(`The contents of "${layer.name || id}" are not decoded here, so its filter mask cannot be painted; run prepareSmartObjects() first.`);
|
|
11468
|
+
requireUnlocked(layer, "all", action);
|
|
10163
11469
|
} else {
|
|
11470
|
+
if (layer.type === "smart") throw new LayerOperationError(`Layer "${layer.name || id}" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);
|
|
10164
11471
|
if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);
|
|
10165
11472
|
requireUnlocked(layer, "pixels", action);
|
|
10166
11473
|
}
|
|
10167
11474
|
if (!layer.visible) throw new LayerOperationError(`Layer "${layer.name || id}" is hidden; show it before you ${action}.`);
|
|
10168
11475
|
const offset = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
10169
|
-
const surface = ch === "mask" ? layer.mask.surface : this.surfaceOf(layer);
|
|
11476
|
+
const surface = ch === "mask" ? layer.mask.surface : ch === "filter-mask" ? layer.smartFilters.mask.surface : this.surfaceOf(layer);
|
|
10170
11477
|
const lockTransparency = ch === "pixels" && Boolean(layer.locks?.transparency);
|
|
10171
11478
|
return { id, layer, channel: ch, offset, surface, lockTransparency };
|
|
10172
11479
|
}
|
|
10173
11480
|
/** The layer with its pixels (or mask) replaced by `surface`. */
|
|
10174
11481
|
withSurface(layer, channel, surface) {
|
|
10175
11482
|
if (channel === "mask") return { ...layer, mask: { ...layer.mask, surface } };
|
|
11483
|
+
if (channel === "filter-mask") return withFilterMaskSurface(layer, surface, this.canvasSize);
|
|
10176
11484
|
const next = { ...layer, surface };
|
|
10177
11485
|
delete next.source;
|
|
10178
11486
|
return next;
|
|
@@ -10187,7 +11495,7 @@ var ImageEditorController = class {
|
|
|
10187
11495
|
const tool = init.tool ?? "brush";
|
|
10188
11496
|
if (tool !== "brush" && tool !== "eraser" && tool !== "mixer") throw new LayerOperationError(`Unknown paint tool "${String(tool)}"; use brush, eraser or mixer.`);
|
|
10189
11497
|
const target = this.paintTarget(init.layerId, init.channel, tool === "eraser" ? "erase" : "paint");
|
|
10190
|
-
if (tool === "mixer" && target.channel
|
|
11498
|
+
if (tool === "mixer" && target.channel !== "pixels") {
|
|
10191
11499
|
throw new LayerOperationError("The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.");
|
|
10192
11500
|
}
|
|
10193
11501
|
const brush = normalizeBrush({ ...this.state.paint.brush, ...init.brush ?? {} });
|
|
@@ -10199,10 +11507,10 @@ var ImageEditorController = class {
|
|
|
10199
11507
|
};
|
|
10200
11508
|
const apply = {
|
|
10201
11509
|
mode: tool === "eraser" ? "erase" : "paint",
|
|
10202
|
-
color: target.channel
|
|
10203
|
-
background: target.channel
|
|
11510
|
+
color: target.channel !== "pixels" ? toMaskGrey(color) : color,
|
|
11511
|
+
background: target.channel !== "pixels" ? toMaskGrey(background) : background,
|
|
10204
11512
|
opacity: brush.opacity,
|
|
10205
|
-
blend: target.channel
|
|
11513
|
+
blend: target.channel !== "pixels" ? "normal" : brush.blend,
|
|
10206
11514
|
lockTransparency: target.lockTransparency,
|
|
10207
11515
|
selection: this.state.selection?.bounds ? this.state.selection : null,
|
|
10208
11516
|
offset: target.offset
|
|
@@ -10230,7 +11538,7 @@ var ImageEditorController = class {
|
|
|
10230
11538
|
/** @internal Shows a stroke's live surface (no history). */
|
|
10231
11539
|
_strokeUpdate(layerId, channel, surface, rects) {
|
|
10232
11540
|
this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
|
|
10233
|
-
this.notify(rects);
|
|
11541
|
+
this.notify(channel === "filter-mask" ? void 0 : rects);
|
|
10234
11542
|
}
|
|
10235
11543
|
/** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */
|
|
10236
11544
|
_strokeCommit(before, layerId, channel, surface) {
|
|
@@ -10302,10 +11610,10 @@ var ImageEditorController = class {
|
|
|
10302
11610
|
};
|
|
10303
11611
|
const apply = {
|
|
10304
11612
|
mode,
|
|
10305
|
-
color: target.channel
|
|
10306
|
-
background: target.channel
|
|
11613
|
+
color: target.channel !== "pixels" ? grey(color) : color,
|
|
11614
|
+
background: target.channel !== "pixels" ? grey(background) : background,
|
|
10307
11615
|
opacity,
|
|
10308
|
-
blend: target.channel
|
|
11616
|
+
blend: target.channel !== "pixels" ? "normal" : blend,
|
|
10309
11617
|
lockTransparency: target.lockTransparency,
|
|
10310
11618
|
selection: null,
|
|
10311
11619
|
offset: target.offset
|
|
@@ -10691,7 +11999,7 @@ var ImageEditorController = class {
|
|
|
10691
11999
|
const sources = {
|
|
10692
12000
|
width,
|
|
10693
12001
|
height,
|
|
10694
|
-
surfaceOf: (l) => l.source === "document" && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l),
|
|
12002
|
+
surfaceOf: (l) => l.type === "pixel" && l.source === "document" && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l),
|
|
10695
12003
|
version: pixels ? "pixels" : this.sourceVersion()
|
|
10696
12004
|
};
|
|
10697
12005
|
const stack = withoutAnnotations(this.state.layers);
|
|
@@ -11181,8 +12489,10 @@ var ImageEditorController = class {
|
|
|
11181
12489
|
if (layer.mask && (layer.offset.x || layer.offset.y)) {
|
|
11182
12490
|
next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };
|
|
11183
12491
|
}
|
|
12492
|
+
} else if (layer.type === "smart") {
|
|
12493
|
+
return this.rasterizeSmartObject(id);
|
|
11184
12494
|
} else {
|
|
11185
|
-
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.`);
|
|
12495
|
+
throw new LayerOperationError(`Only annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
|
|
11186
12496
|
}
|
|
11187
12497
|
this.commitLayers(mapLayer(this.state.layers, id, () => next));
|
|
11188
12498
|
return id;
|
|
@@ -11198,6 +12508,7 @@ var ImageEditorController = class {
|
|
|
11198
12508
|
*/
|
|
11199
12509
|
loadPsd(bytes) {
|
|
11200
12510
|
const result = importPsd(bytes, { newId: () => newLayerId() });
|
|
12511
|
+
this.abandonContents();
|
|
11201
12512
|
this.recordHistory();
|
|
11202
12513
|
const s = this.state;
|
|
11203
12514
|
this.imageElement = null;
|
|
@@ -11306,6 +12617,13 @@ var ImageEditorController = class {
|
|
|
11306
12617
|
if (mask2) out2.mask = mask2;
|
|
11307
12618
|
return out2;
|
|
11308
12619
|
}
|
|
12620
|
+
if (layer.type === "smart") {
|
|
12621
|
+
add2("Smart objects were written as pixel layers (their rendered pixels)", layer.name);
|
|
12622
|
+
const px2 = rasterizeSmartObject([layer], layer.id).layer;
|
|
12623
|
+
const mask2 = bakeMask(px2);
|
|
12624
|
+
if (mask2) px2.mask = mask2;
|
|
12625
|
+
return px2;
|
|
12626
|
+
}
|
|
11309
12627
|
let out = { ...layer };
|
|
11310
12628
|
if (layer.source === "document") {
|
|
11311
12629
|
out.surface = this.surfaceOf(layer);
|
|
@@ -11391,10 +12709,27 @@ var ImageEditorController = class {
|
|
|
11391
12709
|
this.notify();
|
|
11392
12710
|
}
|
|
11393
12711
|
toJSON(options) {
|
|
11394
|
-
const
|
|
11395
|
-
|
|
12712
|
+
const root = this.contentsStack[0]?.saved;
|
|
12713
|
+
const current = this.state;
|
|
12714
|
+
const element = this.imageElement;
|
|
12715
|
+
if (root) {
|
|
12716
|
+
this.state = root.state;
|
|
12717
|
+
this.imageElement = root.element;
|
|
12718
|
+
this.viewKey = [];
|
|
12719
|
+
}
|
|
12720
|
+
let out;
|
|
12721
|
+
try {
|
|
12722
|
+
out = this.serializeState();
|
|
12723
|
+
if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
|
|
12724
|
+
} finally {
|
|
12725
|
+
if (root) {
|
|
12726
|
+
this.state = current;
|
|
12727
|
+
this.imageElement = element;
|
|
12728
|
+
this.viewKey = [];
|
|
12729
|
+
}
|
|
12730
|
+
}
|
|
11396
12731
|
if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;
|
|
11397
|
-
return serializeLayersExternal(this.
|
|
12732
|
+
return serializeLayersExternal(this.rootLayers(), options.store).then(({ layers, assets, smartContents }) => smartContents ? { ...out, layers, assets, smartContents } : { ...out, layers, assets });
|
|
11398
12733
|
}
|
|
11399
12734
|
/**
|
|
11400
12735
|
* Loads a stored state whose v3 assets may live in an asset store (references other than data
|
|
@@ -11413,6 +12748,7 @@ var ImageEditorController = class {
|
|
|
11413
12748
|
}
|
|
11414
12749
|
/** Loads a stored state of any version (one undo step). */
|
|
11415
12750
|
loadJSON(data) {
|
|
12751
|
+
this.abandonContents();
|
|
11416
12752
|
this.recordHistory();
|
|
11417
12753
|
this.applyStored(data);
|
|
11418
12754
|
let selection = null;
|
|
@@ -11435,7 +12771,8 @@ var ImageEditorController = class {
|
|
|
11435
12771
|
this.lastLoadProblems = [];
|
|
11436
12772
|
if (isLayered(data)) {
|
|
11437
12773
|
const problems = [];
|
|
11438
|
-
|
|
12774
|
+
const canvas = typeof data.width === "number" && data.width > 0 && typeof data.height === "number" && data.height > 0 ? { width: data.width, height: data.height } : this.naturalSize;
|
|
12775
|
+
s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems, smartContents: Array.isArray(data.smartContents) ? data.smartContents : [], canvas });
|
|
11439
12776
|
this.lastLoadProblems = problems;
|
|
11440
12777
|
s.crop = data.crop ? { ...data.crop } : null;
|
|
11441
12778
|
s.transform = { ...data.transform };
|
|
@@ -11487,7 +12824,7 @@ var ImageEditorController = class {
|
|
|
11487
12824
|
annotations: JSON.parse(JSON.stringify(view.annotations))
|
|
11488
12825
|
};
|
|
11489
12826
|
}
|
|
11490
|
-
const { layers, assets } = serializeLayers(this.state.layers);
|
|
12827
|
+
const { layers, assets, smartContents } = serializeLayers(this.state.layers);
|
|
11491
12828
|
const out = {
|
|
11492
12829
|
version: LAYERED_SCHEMA_VERSION,
|
|
11493
12830
|
width,
|
|
@@ -11502,6 +12839,7 @@ var ImageEditorController = class {
|
|
|
11502
12839
|
if (this.state.legacyOutputBlur !== void 0) out.legacyOutputBlur = this.state.legacyOutputBlur;
|
|
11503
12840
|
if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };
|
|
11504
12841
|
if (this.state.patterns?.length) out.patterns = serializePatterns(this.state.patterns);
|
|
12842
|
+
if (smartContents) out.smartContents = smartContents;
|
|
11505
12843
|
return out;
|
|
11506
12844
|
}
|
|
11507
12845
|
};
|
|
@@ -11636,6 +12974,12 @@ function collectStateTiles(layers, into) {
|
|
|
11636
12974
|
for (const l of flattenLayers(layers)) {
|
|
11637
12975
|
if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
|
|
11638
12976
|
if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
|
|
12977
|
+
if (l.type === "smart") {
|
|
12978
|
+
collectTiles(l.surface, into);
|
|
12979
|
+
if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);
|
|
12980
|
+
if (l.contents.raster) collectTiles(l.contents.raster, into);
|
|
12981
|
+
if (l.contents.document) collectStateTiles(l.contents.document.layers, into);
|
|
12982
|
+
}
|
|
11639
12983
|
}
|
|
11640
12984
|
}
|
|
11641
12985
|
function pixelsOnly(layers) {
|
|
@@ -11868,7 +13212,7 @@ function loadImageEditorDocument(input) {
|
|
|
11868
13212
|
if (state.selection === void 0) delete state.selection;
|
|
11869
13213
|
return { state, report: result.report, unknown: result.unknown };
|
|
11870
13214
|
}
|
|
11871
|
-
var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation"]);
|
|
13215
|
+
var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart"]);
|
|
11872
13216
|
function cleanLayers(value, dropped) {
|
|
11873
13217
|
const out = [];
|
|
11874
13218
|
for (const raw of value) {
|
|
@@ -11934,6 +13278,13 @@ function loadLayeredDocument(doc) {
|
|
|
11934
13278
|
return { width: mask.width, height: mask.height, rle: v.rle };
|
|
11935
13279
|
},
|
|
11936
13280
|
defaults: void 0
|
|
13281
|
+
},
|
|
13282
|
+
smartContents: {
|
|
13283
|
+
parse: (value) => {
|
|
13284
|
+
if (!Array.isArray(value)) throw new Error("smartContents must be an array");
|
|
13285
|
+
return value.filter((c) => isObject(c) && typeof c.id === "string").map((c) => JSON.parse(JSON.stringify(c)));
|
|
13286
|
+
},
|
|
13287
|
+
defaults: void 0
|
|
11937
13288
|
}
|
|
11938
13289
|
},
|
|
11939
13290
|
{ currentVersion: 3, steps: {} }
|
|
@@ -11947,6 +13298,7 @@ function loadLayeredDocument(doc) {
|
|
|
11947
13298
|
const state = { version: 3, ...result.value };
|
|
11948
13299
|
if (state.selection === void 0) delete state.selection;
|
|
11949
13300
|
if (state.legacyOutputBlur === void 0) delete state.legacyOutputBlur;
|
|
13301
|
+
if (state.smartContents === void 0) delete state.smartContents;
|
|
11950
13302
|
return { state, report: result.report, unknown: result.unknown };
|
|
11951
13303
|
}
|
|
11952
13304
|
|
|
@@ -12766,6 +14118,13 @@ function describeLayer(layer, activeId, boundsOf) {
|
|
|
12766
14118
|
out.collapsed = Boolean(layer.collapsed);
|
|
12767
14119
|
out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));
|
|
12768
14120
|
break;
|
|
14121
|
+
case "smart":
|
|
14122
|
+
out.bounds = boundsOf(layer);
|
|
14123
|
+
out.fillOpacity = round3(layer.fillOpacity ?? 1);
|
|
14124
|
+
out.contents = { id: layer.contents.id, name: layer.contents.name, kind: layer.contents.kind, width: layer.contents.width, height: layer.contents.height };
|
|
14125
|
+
out.smartFilters = (layer.smartFilters?.filters ?? []).map((f) => ({ id: f.id, kind: f.kind, enabled: f.enabled }));
|
|
14126
|
+
out.filterMask = maskSummary(layer.smartFilters?.mask);
|
|
14127
|
+
break;
|
|
12769
14128
|
}
|
|
12770
14129
|
return out;
|
|
12771
14130
|
}
|
|
@@ -13735,7 +15094,11 @@ var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: fals
|
|
|
13735
15094
|
var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
13736
15095
|
var TARGET = {
|
|
13737
15096
|
layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
|
|
13738
|
-
channel: {
|
|
15097
|
+
channel: {
|
|
15098
|
+
type: "string",
|
|
15099
|
+
enum: ["pixels", "mask", "filter-mask"],
|
|
15100
|
+
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals."
|
|
15101
|
+
}
|
|
13739
15102
|
};
|
|
13740
15103
|
var BRUSH = {
|
|
13741
15104
|
type: "object",
|
|
@@ -14072,7 +15435,7 @@ function createImageTransformTools(controller) {
|
|
|
14072
15435
|
const getFrame = {
|
|
14073
15436
|
name: "get_transform_frame",
|
|
14074
15437
|
title: "Get the Free Transform frame",
|
|
14075
|
-
description: "Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.",
|
|
15438
|
+
description: "Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. For a smart object (smartObject true) the frame is the contents\u2019 rectangle in contents pixels and the corners are where they land now. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.",
|
|
14076
15439
|
inputSchema: {
|
|
14077
15440
|
type: "object",
|
|
14078
15441
|
properties: {
|
|
@@ -14094,9 +15457,10 @@ function createImageTransformTools(controller) {
|
|
|
14094
15457
|
content: args.content === "selection-copy" ? "selection-copy" : s.content,
|
|
14095
15458
|
moved: [...s.moves],
|
|
14096
15459
|
frame: { ...s.box },
|
|
14097
|
-
corners:
|
|
14098
|
-
center: roundPoint(
|
|
15460
|
+
corners: s.startGeometry.corners.map(roundPoint),
|
|
15461
|
+
center: roundPoint(anchorPoint(s.startGeometry, "center"))
|
|
14099
15462
|
};
|
|
15463
|
+
if (s.smartObject) out.smartObject = true;
|
|
14100
15464
|
s.cancel();
|
|
14101
15465
|
if (args.meshColumns !== void 0) {
|
|
14102
15466
|
const cols = args.meshColumns;
|
|
@@ -14225,6 +15589,366 @@ function createImageTransformTools(controller) {
|
|
|
14225
15589
|
return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];
|
|
14226
15590
|
}
|
|
14227
15591
|
|
|
15592
|
+
// src/image-editor/smart-tools.ts
|
|
15593
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15594
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
15595
|
+
var WRITE_IDEMPOTENT4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15596
|
+
var DESTRUCTIVE5 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
15597
|
+
var round5 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
|
|
15598
|
+
var roundPoint2 = (p) => ({ x: round5(p.x), y: round5(p.y) });
|
|
15599
|
+
var LAYER_ID4 = { type: "string", description: "Smart object layer id from list_layers (type smart)." };
|
|
15600
|
+
var FILTER_ID = { type: "string", description: "Smart filter id from get_smart_object or add_smart_filter." };
|
|
15601
|
+
var POINT2 = {
|
|
15602
|
+
type: "object",
|
|
15603
|
+
properties: { x: { type: "number", description: "Document pixels from the left." }, y: { type: "number", description: "Document pixels from the top." } },
|
|
15604
|
+
required: ["x", "y"],
|
|
15605
|
+
additionalProperties: false
|
|
15606
|
+
};
|
|
15607
|
+
var FILTER_BLENDS = BLEND_MODES.map((m) => m.id).filter((m) => m !== "pigment");
|
|
15608
|
+
var FILTER_SETTINGS = {
|
|
15609
|
+
enabled: { type: "boolean", description: "false keeps the filter but skips it (the eye in the Layers panel; default true)." },
|
|
15610
|
+
blend: { type: "string", enum: FILTER_BLENDS, description: "How the filter\u2019s result blends with the result below it (Blending Options; default normal)." },
|
|
15611
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Strength of the filter\u2019s result over the result below it, 0..1 (default 1)." },
|
|
15612
|
+
radius: { type: "number", minimum: 0, maximum: 50, description: "blur: box radius in px (default 2)." },
|
|
15613
|
+
brightness: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
|
|
15614
|
+
contrast: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
|
|
15615
|
+
saturation: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
|
|
15616
|
+
exposure: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
|
|
15617
|
+
temperature: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 blue to yellow (default 0)." },
|
|
15618
|
+
tint: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 green to magenta (default 0)." }
|
|
15619
|
+
};
|
|
15620
|
+
function settingsFrom(args) {
|
|
15621
|
+
const out = {};
|
|
15622
|
+
for (const k of Object.keys(FILTER_SETTINGS)) if (args[k] !== void 0) out[k] = args[k];
|
|
15623
|
+
return out;
|
|
15624
|
+
}
|
|
15625
|
+
function imageArg(image) {
|
|
15626
|
+
if (typeof image !== "string" || !image.trim()) throw new LayerOperationError("image is required: a PNG as a data URL or base64 (other formats need a browser).");
|
|
15627
|
+
return image.trim();
|
|
15628
|
+
}
|
|
15629
|
+
function createImageSmartObjectTools(controller) {
|
|
15630
|
+
const smartId = (id) => {
|
|
15631
|
+
if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a smart object id from list_layers).");
|
|
15632
|
+
requireSmartObject(controller.getLayers(), id);
|
|
15633
|
+
return id;
|
|
15634
|
+
};
|
|
15635
|
+
const info = (id) => {
|
|
15636
|
+
const i = controller.getSmartObject(id);
|
|
15637
|
+
return {
|
|
15638
|
+
...i,
|
|
15639
|
+
placement: { ...i.placement, corners: i.placement.corners.map(roundPoint2), center: roundPoint2(i.placement.center), scaleX: round5(i.placement.scaleX), scaleY: round5(i.placement.scaleY), rotate: round5(i.placement.rotate), skewX: round5(i.placement.skewX) }
|
|
15640
|
+
};
|
|
15641
|
+
};
|
|
15642
|
+
const layerSummary = (id) => {
|
|
15643
|
+
const s = controller.getState();
|
|
15644
|
+
const loc = findLayer(s.layers, id);
|
|
15645
|
+
return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;
|
|
15646
|
+
};
|
|
15647
|
+
const getSmart = {
|
|
15648
|
+
name: "get_smart_object",
|
|
15649
|
+
title: "Read a smart object",
|
|
15650
|
+
description: "Returns a smart object\u2019s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents\u2019 own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.",
|
|
15651
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
|
|
15652
|
+
annotations: { title: "Read a smart object", ...READ7 },
|
|
15653
|
+
handler: ({ layerId }) => info(smartId(layerId))
|
|
15654
|
+
};
|
|
15655
|
+
const convert = {
|
|
15656
|
+
name: "convert_to_smart_object",
|
|
15657
|
+
title: "Convert to Smart Object",
|
|
15658
|
+
description: "Layer \u203A Smart Objects \u203A Convert to Smart Object: sibling layers become one smart object at the topmost one\u2019s position, so later transforms and filters are non-destructive. One pixel layer keeps its id, name, opacity, blend, fill, mask and effects, and its filters become smart filters (it looks exactly the same); several layers, a group or an adjustment become a nested layer document (edit it with edit_smart_object_contents). One undo step. Returns the new layer and its contents.",
|
|
15659
|
+
inputSchema: {
|
|
15660
|
+
type: "object",
|
|
15661
|
+
properties: {
|
|
15662
|
+
layerIds: { type: "array", items: { type: "string" }, minItems: 1, description: "Sibling layer ids from list_layers (default the active layer)." },
|
|
15663
|
+
name: { type: "string", description: "Name of the smart object (default the top layer\u2019s name)." }
|
|
15664
|
+
},
|
|
15665
|
+
additionalProperties: false
|
|
15666
|
+
},
|
|
15667
|
+
annotations: { title: "Convert to Smart Object", ...WRITE6 },
|
|
15668
|
+
handler: ({ layerIds, name }) => {
|
|
15669
|
+
for (const id2 of layerIds ?? []) requireLayer(controller.getLayers(), id2);
|
|
15670
|
+
const id = controller.convertToSmartObject(layerIds, { name });
|
|
15671
|
+
return { layer: layerSummary(id), smartObject: info(id) };
|
|
15672
|
+
}
|
|
15673
|
+
};
|
|
15674
|
+
const rasterize = {
|
|
15675
|
+
name: "rasterize_smart_object",
|
|
15676
|
+
title: "Rasterize a smart object",
|
|
15677
|
+
description: "Layer \u203A Smart Objects \u203A Rasterize: the smart object becomes a pixel layer holding what it shows now (smart filters baked; same id, mask, effects, opacity, blend and fill). The contents, placement and filters are gone, so later transforms resample the pixels. One undo step. Returns the layer.",
|
|
15678
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
|
|
15679
|
+
annotations: { title: "Rasterize a smart object", ...DESTRUCTIVE5 },
|
|
15680
|
+
handler: ({ layerId }) => ({ layer: layerSummary(controller.rasterizeSmartObject(smartId(layerId))) })
|
|
15681
|
+
};
|
|
15682
|
+
const setTransform = {
|
|
15683
|
+
name: "set_smart_object_transform",
|
|
15684
|
+
title: "Set a smart object\u2019s transform",
|
|
15685
|
+
description: "Sets where a smart object\u2019s contents land, absolutely (not relative to the current transform), and renders it again from the contents, so nothing degrades however often it is transformed. Give at most one of: corners (Distort / Perspective), matrix (contents pixels \u2192 document), or the numbers scaleX / scaleY (percent of the contents\u2019 own size), rotate, skewX, skewY and x, y (where the contents\u2019 centre goes; unset numbers keep their current values). warp sets a preset or a mesh in contents pixels (null removes it); with nothing else given the corners stay. A linked mask follows a warp-free change. For a relative nudge use transform_layer. One undo step. Returns the placement and bounds.",
|
|
15686
|
+
inputSchema: {
|
|
15687
|
+
type: "object",
|
|
15688
|
+
properties: {
|
|
15689
|
+
layerId: LAYER_ID4,
|
|
15690
|
+
corners: { type: "array", items: POINT2, minItems: 4, maxItems: 4, description: "Where the contents\u2019 corners land: [top-left, top-right, bottom-right, bottom-left] in document pixels. The quad must stay convex." },
|
|
15691
|
+
matrix: { type: "array", items: { type: "number" }, minItems: 6, maxItems: 6, description: "Affine map from contents pixels to document pixels [a, b, c, d, e, f] (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f)." },
|
|
15692
|
+
scaleX: { type: "number", description: "Width in percent of the contents\u2019 own width (negative flips)." },
|
|
15693
|
+
scaleY: { type: "number", description: "Height in percent of the contents\u2019 own height (negative flips)." },
|
|
15694
|
+
rotate: { type: "number", description: "Rotation in degrees, clockwise." },
|
|
15695
|
+
skewX: { type: "number", minimum: -89, maximum: 89, description: "Horizontal skew in degrees." },
|
|
15696
|
+
skewY: { type: "number", minimum: -89, maximum: 89, description: "Vertical skew in degrees." },
|
|
15697
|
+
x: { type: "number", description: "Document x of the contents\u2019 centre (default where it is now)." },
|
|
15698
|
+
y: { type: "number", description: "Document y of the contents\u2019 centre (default where it is now)." },
|
|
15699
|
+
warp: {
|
|
15700
|
+
description: "A warp over the contents: a preset {style, bend, horizontal, vertical, orientation}, a mesh {columns, rows, points} in contents pixels, or null to remove the warp. Omit to keep the current warp.",
|
|
15701
|
+
anyOf: [
|
|
15702
|
+
{ type: "null" },
|
|
15703
|
+
{
|
|
15704
|
+
type: "object",
|
|
15705
|
+
properties: {
|
|
15706
|
+
style: { type: "string", enum: WARP_STYLES, description: "Preset (Photoshop\u2019s menu)." },
|
|
15707
|
+
bend: { type: "number", minimum: -100, maximum: 100, description: "Bend in percent (default 50)." },
|
|
15708
|
+
horizontal: { type: "number", minimum: -100, maximum: 100, description: "Horizontal distortion in percent (default 0)." },
|
|
15709
|
+
vertical: { type: "number", minimum: -100, maximum: 100, description: "Vertical distortion in percent (default 0)." },
|
|
15710
|
+
orientation: { type: "string", enum: ["horizontal", "vertical"], description: "Bend axis (default horizontal)." }
|
|
15711
|
+
},
|
|
15712
|
+
required: ["style"],
|
|
15713
|
+
additionalProperties: false
|
|
15714
|
+
},
|
|
15715
|
+
{
|
|
15716
|
+
type: "object",
|
|
15717
|
+
properties: {
|
|
15718
|
+
columns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns." },
|
|
15719
|
+
rows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows." },
|
|
15720
|
+
points: { type: "array", items: POINT2, minItems: 16, description: "The (3\xB7columns + 1) \xD7 (3\xB7rows + 1) control points row by row, in contents pixels (0..width \xD7 0..height is no warp)." }
|
|
15721
|
+
},
|
|
15722
|
+
required: ["columns", "rows", "points"],
|
|
15723
|
+
additionalProperties: false
|
|
15724
|
+
}
|
|
15725
|
+
]
|
|
15726
|
+
},
|
|
15727
|
+
interpolation: { type: "string", enum: INTERPOLATIONS, description: "Resampling: nearest, bilinear or bicubic (default: as before, bicubic for new smart objects)." }
|
|
15728
|
+
},
|
|
15729
|
+
required: ["layerId"],
|
|
15730
|
+
additionalProperties: false
|
|
15731
|
+
},
|
|
15732
|
+
annotations: { title: "Set a smart object\u2019s transform", ...WRITE_IDEMPOTENT4 },
|
|
15733
|
+
handler: (args) => {
|
|
15734
|
+
const id = smartId(args.layerId);
|
|
15735
|
+
let warp;
|
|
15736
|
+
const w = args.warp;
|
|
15737
|
+
if (w === null) warp = null;
|
|
15738
|
+
else if (w && "style" in w) warp = { style: w.style, bend: w.bend ?? 50, horizontal: w.horizontal, vertical: w.vertical, orientation: w.orientation };
|
|
15739
|
+
else if (w) {
|
|
15740
|
+
const m = w;
|
|
15741
|
+
const want = (3 * m.columns + 1) * (3 * m.rows + 1);
|
|
15742
|
+
if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} \xD7 ${m.rows} mesh needs (3\xB7${m.columns} + 1) \xD7 (3\xB7${m.rows} + 1) = ${want} points, got ${m.points.length}.`);
|
|
15743
|
+
const knots = (n) => Array.from({ length: n + 1 }, (_, i) => i / n);
|
|
15744
|
+
warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };
|
|
15745
|
+
}
|
|
15746
|
+
const keys = ["corners", "matrix", "scaleX", "scaleY", "rotate", "skewX", "skewY", "x", "y", "warp", "interpolation"];
|
|
15747
|
+
if (!keys.some((k) => args[k] !== void 0)) throw new LayerOperationError(`Give something to set: ${keys.join(", ")}.`);
|
|
15748
|
+
const res = controller.setSmartObjectTransform(id, {
|
|
15749
|
+
corners: args.corners,
|
|
15750
|
+
matrix: args.matrix,
|
|
15751
|
+
scaleX: args.scaleX,
|
|
15752
|
+
scaleY: args.scaleY,
|
|
15753
|
+
rotate: args.rotate,
|
|
15754
|
+
skewX: args.skewX,
|
|
15755
|
+
skewY: args.skewY,
|
|
15756
|
+
x: args.x,
|
|
15757
|
+
y: args.y,
|
|
15758
|
+
warp,
|
|
15759
|
+
interpolation: args.interpolation
|
|
15760
|
+
});
|
|
15761
|
+
return { layerId: id, placement: info(id).placement, masksMoved: res.masksMoved, bounds: controller.getLayerBounds(id) };
|
|
15762
|
+
}
|
|
15763
|
+
};
|
|
15764
|
+
const replace = {
|
|
15765
|
+
name: "replace_smart_object_contents",
|
|
15766
|
+
title: "Replace a smart object\u2019s contents",
|
|
15767
|
+
description: "Layer \u203A Smart Objects \u203A Replace Contents: new raster contents from an image for the smart object and every linked instance (same contents id). Each instance keeps its scale and rotation about the contents\u2019 centre and its smart filters, and renders again. One undo step. Returns the contents id, the new size and the layers that changed.",
|
|
15768
|
+
inputSchema: {
|
|
15769
|
+
type: "object",
|
|
15770
|
+
properties: {
|
|
15771
|
+
layerId: LAYER_ID4,
|
|
15772
|
+
image: { type: "string", description: "The new contents: a PNG as a data URL or base64 (other formats and URLs need a browser)." },
|
|
15773
|
+
name: { type: "string", description: "File name of the contents (default the old name with .png)." }
|
|
15774
|
+
},
|
|
15775
|
+
required: ["layerId", "image"],
|
|
15776
|
+
additionalProperties: false
|
|
15777
|
+
},
|
|
15778
|
+
annotations: { title: "Replace a smart object\u2019s contents", ...DESTRUCTIVE5 },
|
|
15779
|
+
handler: async ({ layerId, image, name }) => controller.replaceSmartObjectContentsFromImage(smartId(layerId), imageArg(image), name)
|
|
15780
|
+
};
|
|
15781
|
+
const exportContents = {
|
|
15782
|
+
name: "export_smart_object_contents",
|
|
15783
|
+
title: "Export a smart object\u2019s contents",
|
|
15784
|
+
description: "Layer \u203A Smart Objects \u203A Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG\u2026), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.",
|
|
15785
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
|
|
15786
|
+
annotations: { title: "Export a smart object\u2019s contents", ...READ7 },
|
|
15787
|
+
handler: ({ layerId }) => {
|
|
15788
|
+
const f = controller.exportSmartObjectContents(smartId(layerId));
|
|
15789
|
+
return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };
|
|
15790
|
+
}
|
|
15791
|
+
};
|
|
15792
|
+
const copy = {
|
|
15793
|
+
name: "copy_smart_object",
|
|
15794
|
+
title: "Copy a smart object",
|
|
15795
|
+
description: "Duplicates a smart object directly above it. linked true (Duplicate Layer) shares the contents, so editing or replacing them changes both; linked false (Layer \u203A Smart Objects \u203A New Smart Object via Copy) gives the copy its own contents. Placement, filters, mask and effects are copied either way. One undo step. Returns the copy.",
|
|
15796
|
+
inputSchema: {
|
|
15797
|
+
type: "object",
|
|
15798
|
+
properties: {
|
|
15799
|
+
layerId: LAYER_ID4,
|
|
15800
|
+
linked: { type: "boolean", description: "true shares the contents with the original; false (default) makes independent contents." },
|
|
15801
|
+
name: { type: "string", description: 'Name of the copy (default "<name> copy").' }
|
|
15802
|
+
},
|
|
15803
|
+
required: ["layerId"],
|
|
15804
|
+
additionalProperties: false
|
|
15805
|
+
},
|
|
15806
|
+
annotations: { title: "Copy a smart object", ...WRITE6 },
|
|
15807
|
+
handler: ({ layerId, linked, name }) => {
|
|
15808
|
+
const id = controller.copySmartObject(smartId(layerId), linked === true, name);
|
|
15809
|
+
return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };
|
|
15810
|
+
}
|
|
15811
|
+
};
|
|
15812
|
+
const editContents = {
|
|
15813
|
+
name: "edit_smart_object_contents",
|
|
15814
|
+
title: "Edit a smart object\u2019s contents",
|
|
15815
|
+
description: 'Layer \u203A Smart Objects \u203A Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer "Layer 0") with their own undo history. Every other tool (list_layers, paint, adjustments\u2026) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents\u2019 size and layers.',
|
|
15816
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
|
|
15817
|
+
annotations: { title: "Edit a smart object\u2019s contents", ...WRITE6 },
|
|
15818
|
+
handler: ({ layerId }) => {
|
|
15819
|
+
const res = controller.editSmartObjectContents(smartId(layerId));
|
|
15820
|
+
const s = controller.getState();
|
|
15821
|
+
return { ...res, editing: controller.getEditingContents(), layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))) };
|
|
15822
|
+
}
|
|
15823
|
+
};
|
|
15824
|
+
const closeContents = {
|
|
15825
|
+
name: "close_smart_object_contents",
|
|
15826
|
+
title: "Close a smart object\u2019s contents",
|
|
15827
|
+
description: "Closes the contents opened with edit_smart_object_contents. save true (default) writes the edits back: the smart object and every linked instance render again, as one undo step in the document; save false drops them. Returns the smart object, whether it changed, the layers that changed and what is still open.",
|
|
15828
|
+
inputSchema: {
|
|
15829
|
+
type: "object",
|
|
15830
|
+
properties: { save: { type: "boolean", description: "true (default) saves the edits into the smart object; false discards them." } },
|
|
15831
|
+
additionalProperties: false
|
|
15832
|
+
},
|
|
15833
|
+
annotations: { title: "Close a smart object\u2019s contents", ...WRITE6 },
|
|
15834
|
+
handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() })
|
|
15835
|
+
};
|
|
15836
|
+
const addFilter = {
|
|
15837
|
+
name: "add_smart_filter",
|
|
15838
|
+
title: "Add a smart filter",
|
|
15839
|
+
description: "Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id) and the stack.",
|
|
15840
|
+
inputSchema: {
|
|
15841
|
+
type: "object",
|
|
15842
|
+
properties: {
|
|
15843
|
+
layerId: LAYER_ID4,
|
|
15844
|
+
kind: { type: "string", enum: ["develop", "blur"], description: "Filter kind." },
|
|
15845
|
+
index: { type: "integer", minimum: 0, description: "Position in the stack, 0 = bottom, applied first (default the top)." },
|
|
15846
|
+
...FILTER_SETTINGS
|
|
15847
|
+
},
|
|
15848
|
+
required: ["layerId", "kind"],
|
|
15849
|
+
additionalProperties: false
|
|
15850
|
+
},
|
|
15851
|
+
annotations: { title: "Add a smart filter", ...WRITE6 },
|
|
15852
|
+
handler: (args) => {
|
|
15853
|
+
const id = smartId(args.layerId);
|
|
15854
|
+
const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index);
|
|
15855
|
+
return { layerId: id, filter, smartFilters: info(id).smartFilters };
|
|
15856
|
+
}
|
|
15857
|
+
};
|
|
15858
|
+
const updateFilter = {
|
|
15859
|
+
name: "update_smart_filter",
|
|
15860
|
+
title: "Change a smart filter",
|
|
15861
|
+
description: "Changes one smart filter: its settings, enabled, blend or opacity (only the given values; the kind cannot change). A Photoshop filter kept from a PSD (kind psd) only takes enabled, blend and opacity. One undo step. Returns the filter and the stack.",
|
|
15862
|
+
inputSchema: {
|
|
15863
|
+
type: "object",
|
|
15864
|
+
properties: { layerId: LAYER_ID4, filterId: FILTER_ID, ...FILTER_SETTINGS },
|
|
15865
|
+
required: ["layerId", "filterId"],
|
|
15866
|
+
additionalProperties: false
|
|
15867
|
+
},
|
|
15868
|
+
annotations: { title: "Change a smart filter", ...WRITE_IDEMPOTENT4 },
|
|
15869
|
+
handler: (args) => {
|
|
15870
|
+
const id = smartId(args.layerId);
|
|
15871
|
+
const changes = settingsFrom(args);
|
|
15872
|
+
if (!Object.keys(changes).length) throw new LayerOperationError("Give at least one value to change.");
|
|
15873
|
+
const filter = controller.updateSmartFilter(id, args.filterId, changes);
|
|
15874
|
+
return { layerId: id, filter, smartFilters: info(id).smartFilters };
|
|
15875
|
+
}
|
|
15876
|
+
};
|
|
15877
|
+
const removeFilter = {
|
|
15878
|
+
name: "remove_smart_filter",
|
|
15879
|
+
title: "Remove a smart filter",
|
|
15880
|
+
description: "Removes one smart filter (the filter mask goes with the last one). One undo step. Returns the remaining stack.",
|
|
15881
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID4, filterId: FILTER_ID }, required: ["layerId", "filterId"], additionalProperties: false },
|
|
15882
|
+
annotations: { title: "Remove a smart filter", ...DESTRUCTIVE5 },
|
|
15883
|
+
handler: ({ layerId, filterId }) => {
|
|
15884
|
+
const id = smartId(layerId);
|
|
15885
|
+
controller.removeSmartFilter(id, filterId);
|
|
15886
|
+
return { layerId: id, smartFilters: info(id).smartFilters };
|
|
15887
|
+
}
|
|
15888
|
+
};
|
|
15889
|
+
const moveFilter = {
|
|
15890
|
+
name: "move_smart_filter",
|
|
15891
|
+
title: "Reorder a smart filter",
|
|
15892
|
+
description: "Moves one smart filter to a position in the stack (0 = bottom, applied first), as dragging it in the Layers panel. One undo step. Returns the stack in its new order.",
|
|
15893
|
+
inputSchema: {
|
|
15894
|
+
type: "object",
|
|
15895
|
+
properties: { layerId: LAYER_ID4, filterId: FILTER_ID, index: { type: "integer", minimum: 0, description: "New position, 0 = bottom (applied first)." } },
|
|
15896
|
+
required: ["layerId", "filterId", "index"],
|
|
15897
|
+
additionalProperties: false
|
|
15898
|
+
},
|
|
15899
|
+
annotations: { title: "Reorder a smart filter", ...WRITE_IDEMPOTENT4 },
|
|
15900
|
+
handler: ({ layerId, filterId, index }) => {
|
|
15901
|
+
const id = smartId(layerId);
|
|
15902
|
+
controller.moveSmartFilter(id, filterId, index);
|
|
15903
|
+
return { layerId: id, smartFilters: info(id).smartFilters };
|
|
15904
|
+
}
|
|
15905
|
+
};
|
|
15906
|
+
const setEnabled = {
|
|
15907
|
+
name: "set_smart_filters_enabled",
|
|
15908
|
+
title: "Turn smart filters on or off",
|
|
15909
|
+
description: "Layer \u203A Smart Filter \u203A Disable / Enable Smart Filters: switches the whole stack; each filter keeps its own enabled flag. One undo step. Returns the stack.",
|
|
15910
|
+
inputSchema: {
|
|
15911
|
+
type: "object",
|
|
15912
|
+
properties: { layerId: LAYER_ID4, enabled: { type: "boolean", description: "false hides every smart filter; true shows them again." } },
|
|
15913
|
+
required: ["layerId", "enabled"],
|
|
15914
|
+
additionalProperties: false
|
|
15915
|
+
},
|
|
15916
|
+
annotations: { title: "Turn smart filters on or off", ...WRITE_IDEMPOTENT4 },
|
|
15917
|
+
handler: ({ layerId, enabled }) => {
|
|
15918
|
+
const id = smartId(layerId);
|
|
15919
|
+
controller.setSmartFiltersEnabled(id, enabled === true);
|
|
15920
|
+
return { layerId: id, smartFilters: info(id).smartFilters };
|
|
15921
|
+
}
|
|
15922
|
+
};
|
|
15923
|
+
const setMask = {
|
|
15924
|
+
name: "set_smart_filter_mask",
|
|
15925
|
+
title: "Set the smart filter mask",
|
|
15926
|
+
description: "Sets the one filter mask of a smart object\u2019s smart filters, which says where the filtered result shows over the unfiltered contents: from reveal-all (white), hide-all (black), selection (the selection\u2019s coverage) or none (remove it); keep changes only its properties. Paint it with paint_stroke channel filter-mask. One undo step. Returns the mask summary.",
|
|
15927
|
+
inputSchema: {
|
|
15928
|
+
type: "object",
|
|
15929
|
+
properties: {
|
|
15930
|
+
layerId: LAYER_ID4,
|
|
15931
|
+
from: { type: "string", enum: ["reveal-all", "hide-all", "selection", "none", "keep"], description: "What the mask starts from; none removes it; keep only changes the properties below." },
|
|
15932
|
+
density: { type: "number", minimum: 0, maximum: 1, description: "How much hidden areas hide, 0..1 (default 1)." },
|
|
15933
|
+
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Gaussian softening in px (default 0)." },
|
|
15934
|
+
enabled: { type: "boolean", description: "false keeps the mask but ignores it." },
|
|
15935
|
+
linked: { type: "boolean", description: "false keeps the mask in place when the smart object is transformed or moved (default linked)." }
|
|
15936
|
+
},
|
|
15937
|
+
required: ["layerId", "from"],
|
|
15938
|
+
additionalProperties: false
|
|
15939
|
+
},
|
|
15940
|
+
annotations: { title: "Set the smart filter mask", ...WRITE6 },
|
|
15941
|
+
handler: (args) => {
|
|
15942
|
+
const id = smartId(args.layerId);
|
|
15943
|
+
const props = {};
|
|
15944
|
+
for (const k of ["density", "feather", "enabled", "linked"]) if (args[k] !== void 0) props[k] = args[k];
|
|
15945
|
+
const mask = controller.setSmartFilterMask(id, args.from, props);
|
|
15946
|
+
return { layerId: id, filterMask: maskSummary(mask ?? void 0) };
|
|
15947
|
+
}
|
|
15948
|
+
};
|
|
15949
|
+
return [getSmart, convert, rasterize, setTransform, replace, exportContents, copy, editContents, closeContents, addFilter, updateFilter, removeFilter, moveFilter, setEnabled, setMask];
|
|
15950
|
+
}
|
|
15951
|
+
|
|
14228
15952
|
// src/image-editor/tools.ts
|
|
14229
15953
|
var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
|
|
14230
15954
|
function adjustmentsSummary(adjustments) {
|
|
@@ -14463,6 +16187,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14463
16187
|
...createImageEditTools(controller),
|
|
14464
16188
|
...createImageLayerTools(controller),
|
|
14465
16189
|
...createImageTransformTools(controller),
|
|
16190
|
+
...createImageSmartObjectTools(controller),
|
|
14466
16191
|
...createImageEffectTools(controller),
|
|
14467
16192
|
...createImagePaintTools(controller),
|
|
14468
16193
|
...createPigmentTools(),
|
|
@@ -14801,6 +16526,8 @@ function layerBadge(layer) {
|
|
|
14801
16526
|
return "grp";
|
|
14802
16527
|
case "annotation":
|
|
14803
16528
|
return layer.annotation.type === "text" ? "T" : "ann";
|
|
16529
|
+
case "smart":
|
|
16530
|
+
return "so";
|
|
14804
16531
|
}
|
|
14805
16532
|
}
|
|
14806
16533
|
function dropZone(fraction, isGroup) {
|
|
@@ -14840,7 +16567,11 @@ function layerMenu(controller) {
|
|
|
14840
16567
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
14841
16568
|
{ action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
|
|
14842
16569
|
{ action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
|
|
14843
|
-
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
16570
|
+
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
16571
|
+
{ action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
|
|
16572
|
+
{ action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
|
|
16573
|
+
{ action: "smart-via-copy", label: "New Smart Object via Copy", enabled: active?.type === "smart" },
|
|
16574
|
+
{ action: "add-filter-mask", label: "Add Smart Filter Mask", enabled: active?.type === "smart" && !active.smartFilters?.mask },
|
|
14844
16575
|
{ action: "toggle-clip", label: active?.clipped ? "Release Clipping Mask" : "Create Clipping Mask", enabled: Boolean(loc && loc.index > 0) }
|
|
14845
16576
|
];
|
|
14846
16577
|
}
|
|
@@ -14901,6 +16632,18 @@ async function runLayerMenuAction(controller, action) {
|
|
|
14901
16632
|
case "rasterize":
|
|
14902
16633
|
if (id) await controller.rasterizeLayer(id);
|
|
14903
16634
|
break;
|
|
16635
|
+
case "convert-smart":
|
|
16636
|
+
if (id) controller.convertToSmartObject([id]);
|
|
16637
|
+
break;
|
|
16638
|
+
case "edit-contents":
|
|
16639
|
+
if (id) controller.editSmartObjectContents(id);
|
|
16640
|
+
break;
|
|
16641
|
+
case "smart-via-copy":
|
|
16642
|
+
if (id) controller.copySmartObject(id, false);
|
|
16643
|
+
break;
|
|
16644
|
+
case "add-filter-mask":
|
|
16645
|
+
if (id) controller.setSmartFilterMask(id, "reveal-all");
|
|
16646
|
+
break;
|
|
14904
16647
|
case "toggle-clip":
|
|
14905
16648
|
if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });
|
|
14906
16649
|
break;
|
|
@@ -14910,6 +16653,17 @@ async function runLayerMenuAction(controller, action) {
|
|
|
14910
16653
|
return err instanceof Error ? err.message : String(err);
|
|
14911
16654
|
}
|
|
14912
16655
|
}
|
|
16656
|
+
function smartFilterRows(layer) {
|
|
16657
|
+
if (layer.type !== "smart") return [];
|
|
16658
|
+
const list2 = layer.smartFilters?.filters ?? [];
|
|
16659
|
+
const rows = list2.map((f, index) => ({
|
|
16660
|
+
filter: f,
|
|
16661
|
+
index,
|
|
16662
|
+
drawn: f.kind !== "psd",
|
|
16663
|
+
label: f.kind === "psd" ? f.name.replace(/&/g, "").replace(/\.\.\.$/, "") : f.kind === "blur" ? `Blur (${f.radius} px)` : "Develop"
|
|
16664
|
+
}));
|
|
16665
|
+
return rows.reverse();
|
|
16666
|
+
}
|
|
14913
16667
|
|
|
14914
16668
|
// src/image-editor/layers/effects-ui.ts
|
|
14915
16669
|
function layerEffectsPanel(layer) {
|
|
@@ -15267,7 +17021,7 @@ var FreeTransformTool = class {
|
|
|
15267
17021
|
this.corners = s.geometry.corners;
|
|
15268
17022
|
this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };
|
|
15269
17023
|
this.modeValue = "scale";
|
|
15270
|
-
this.mesh = null;
|
|
17024
|
+
this.mesh = s.geometry.warp ?? null;
|
|
15271
17025
|
this.warpSettings = { style: "custom", bend: 50, horizontal: 0, vertical: 0, orientation: "horizontal", columns: 1, rows: 1 };
|
|
15272
17026
|
this.typed = {};
|
|
15273
17027
|
this.error = null;
|
|
@@ -15770,6 +17524,7 @@ export {
|
|
|
15770
17524
|
SELECTION_MODE_LABELS,
|
|
15771
17525
|
SELECTION_MODIFY_OPS,
|
|
15772
17526
|
SELECTION_TOOLS,
|
|
17527
|
+
SMART_RENDER_MARGIN,
|
|
15773
17528
|
SUPPORTED_ADJUSTMENT_KEYS,
|
|
15774
17529
|
SelectionGesture,
|
|
15775
17530
|
StrokeCoverage,
|
|
@@ -15790,8 +17545,10 @@ export {
|
|
|
15790
17545
|
XYZ_D50_TO_SRGB,
|
|
15791
17546
|
ZlibError,
|
|
15792
17547
|
activeAdjustmentLayers,
|
|
17548
|
+
activeSmartFilters,
|
|
15793
17549
|
addLayerEffect,
|
|
15794
17550
|
addLayerFilter,
|
|
17551
|
+
addSmartFilter,
|
|
15795
17552
|
addToHistogram,
|
|
15796
17553
|
adjustmentKindLabel,
|
|
15797
17554
|
adjustmentLayersKey,
|
|
@@ -15814,6 +17571,7 @@ export {
|
|
|
15814
17571
|
applyProcess2Adjustments,
|
|
15815
17572
|
applyRegionBlur,
|
|
15816
17573
|
applyRegionMosaic,
|
|
17574
|
+
applySmartFilters,
|
|
15817
17575
|
applyStrokeToTile,
|
|
15818
17576
|
backgroundLayer,
|
|
15819
17577
|
base64ToBytes,
|
|
@@ -15853,9 +17611,13 @@ export {
|
|
|
15853
17611
|
computeHistogram,
|
|
15854
17612
|
computeSelectionBounds,
|
|
15855
17613
|
contentBounds,
|
|
17614
|
+
contentsBox,
|
|
17615
|
+
contentsSurface,
|
|
15856
17616
|
contractCoverage,
|
|
15857
17617
|
controlPosition,
|
|
15858
17618
|
controlTrackBackground,
|
|
17619
|
+
convertToSmartObject,
|
|
17620
|
+
copySmartObject,
|
|
15859
17621
|
cornersFromMatrix,
|
|
15860
17622
|
coverageTileCoord,
|
|
15861
17623
|
coverageToByte,
|
|
@@ -15869,6 +17631,7 @@ export {
|
|
|
15869
17631
|
createImagePaintTools,
|
|
15870
17632
|
createImagePsdTools,
|
|
15871
17633
|
createImageSelectionTools,
|
|
17634
|
+
createImageSmartObjectTools,
|
|
15872
17635
|
createImageTransformTools,
|
|
15873
17636
|
createPigment,
|
|
15874
17637
|
createPigmentTools,
|
|
@@ -15906,6 +17669,7 @@ export {
|
|
|
15906
17669
|
developFromAdjustments,
|
|
15907
17670
|
developTarget,
|
|
15908
17671
|
distanceTransform,
|
|
17672
|
+
documentContents,
|
|
15909
17673
|
downloadPsd,
|
|
15910
17674
|
dragWarpMeshPoint,
|
|
15911
17675
|
dropPlacement,
|
|
@@ -15932,6 +17696,7 @@ export {
|
|
|
15932
17696
|
exportPsd,
|
|
15933
17697
|
featherCoverage,
|
|
15934
17698
|
fillRect,
|
|
17699
|
+
filterMaskCoverage,
|
|
15935
17700
|
findAdjustmentControl,
|
|
15936
17701
|
findLayer,
|
|
15937
17702
|
findLayerControl,
|
|
@@ -15956,6 +17721,7 @@ export {
|
|
|
15956
17721
|
handlePaintShortcut,
|
|
15957
17722
|
hasActiveEffects,
|
|
15958
17723
|
hasResampledLegacyMask,
|
|
17724
|
+
hasSurface,
|
|
15959
17725
|
hasWarp,
|
|
15960
17726
|
hashMaskContent,
|
|
15961
17727
|
hashNoise,
|
|
@@ -15982,12 +17748,14 @@ export {
|
|
|
15982
17748
|
inflateRaw,
|
|
15983
17749
|
insertCurvePoint,
|
|
15984
17750
|
insertLayer,
|
|
17751
|
+
instancesOf,
|
|
15985
17752
|
invertHomography,
|
|
15986
17753
|
invertMatrix3,
|
|
15987
17754
|
invertSelection,
|
|
15988
17755
|
isActiveFilter,
|
|
15989
17756
|
isAdjustmentKind,
|
|
15990
17757
|
isBlendMode,
|
|
17758
|
+
isContentsDecoded,
|
|
15991
17759
|
isDefaultDepthMask,
|
|
15992
17760
|
isIdentityTransform,
|
|
15993
17761
|
isInterpolation,
|
|
@@ -16049,9 +17817,11 @@ export {
|
|
|
16049
17817
|
modifySelection,
|
|
16050
17818
|
moveCurvePoint,
|
|
16051
17819
|
moveLayer,
|
|
17820
|
+
moveSmartFilter,
|
|
16052
17821
|
mulMatrix3,
|
|
16053
17822
|
needsLegacyBase,
|
|
16054
17823
|
neutralHueRange,
|
|
17824
|
+
newContentsId,
|
|
16055
17825
|
newEffectId,
|
|
16056
17826
|
newLayerId,
|
|
16057
17827
|
normalizeAdjustmentLayer,
|
|
@@ -16063,11 +17833,14 @@ export {
|
|
|
16063
17833
|
normalizeLayerEffects,
|
|
16064
17834
|
normalizeLayerMask,
|
|
16065
17835
|
normalizeMixer,
|
|
17836
|
+
normalizePlacement,
|
|
16066
17837
|
normalizeProcessVersion,
|
|
16067
17838
|
normalizeRect,
|
|
16068
17839
|
normalizeRefineEdgeParams,
|
|
17840
|
+
normalizeSmartFilter,
|
|
16069
17841
|
normalizeStoredAdjustments,
|
|
16070
17842
|
offsetLayer,
|
|
17843
|
+
offsetSmartObject,
|
|
16071
17844
|
openPsdFile,
|
|
16072
17845
|
packRgba,
|
|
16073
17846
|
parseAdjustmentBlock,
|
|
@@ -16083,7 +17856,11 @@ export {
|
|
|
16083
17856
|
photoFilterMatrix,
|
|
16084
17857
|
pixelLayerBounds,
|
|
16085
17858
|
pixelLayers,
|
|
17859
|
+
placeContents,
|
|
16086
17860
|
placeLayer,
|
|
17861
|
+
placementAt,
|
|
17862
|
+
placementBounds,
|
|
17863
|
+
placementGeometry,
|
|
16087
17864
|
pngDataUrl,
|
|
16088
17865
|
pointerPressure,
|
|
16089
17866
|
polygonCoverage,
|
|
@@ -16102,8 +17879,10 @@ export {
|
|
|
16102
17879
|
pullWarpMesh,
|
|
16103
17880
|
quickMaskPixels,
|
|
16104
17881
|
rangeLightness,
|
|
17882
|
+
rasterContents,
|
|
16105
17883
|
rasterMaskFromLegacy,
|
|
16106
17884
|
rasterizeDab,
|
|
17885
|
+
rasterizeSmartObject,
|
|
16107
17886
|
readAdjustmentControls,
|
|
16108
17887
|
readPixel,
|
|
16109
17888
|
readPsd,
|
|
@@ -16121,10 +17900,15 @@ export {
|
|
|
16121
17900
|
removeLayer,
|
|
16122
17901
|
removeLayerEffect,
|
|
16123
17902
|
removeLayerFilter,
|
|
17903
|
+
removeSmartFilter,
|
|
16124
17904
|
renderLayerEffects,
|
|
16125
17905
|
renderLayeredBase,
|
|
17906
|
+
renderSmartObject,
|
|
17907
|
+
replaceContents,
|
|
16126
17908
|
requireLayer,
|
|
17909
|
+
requireSmartObject,
|
|
16127
17910
|
requireUnlocked,
|
|
17911
|
+
rerender,
|
|
16128
17912
|
resizePixels,
|
|
16129
17913
|
resizeSelection,
|
|
16130
17914
|
resizeWarpMesh,
|
|
@@ -16134,6 +17918,7 @@ export {
|
|
|
16134
17918
|
rgbToHsl,
|
|
16135
17919
|
rgbToReflectance,
|
|
16136
17920
|
rgbUnitToHex,
|
|
17921
|
+
runLayerFilters,
|
|
16137
17922
|
runLayerMenuAction,
|
|
16138
17923
|
sampleLut,
|
|
16139
17924
|
sampleToneLut,
|
|
@@ -16146,6 +17931,7 @@ export {
|
|
|
16146
17931
|
selectionToCoverage,
|
|
16147
17932
|
selectionToPixels,
|
|
16148
17933
|
selectionValueAt,
|
|
17934
|
+
serializeContents,
|
|
16149
17935
|
serializeEffects,
|
|
16150
17936
|
serializeLayer,
|
|
16151
17937
|
serializeLayers,
|
|
@@ -16159,7 +17945,12 @@ export {
|
|
|
16159
17945
|
setLayerProperties,
|
|
16160
17946
|
setLum,
|
|
16161
17947
|
setPigmentColor,
|
|
17948
|
+
setSmartFilterMask,
|
|
17949
|
+
setSmartFiltersEnabled,
|
|
17950
|
+
setSmartPlacement,
|
|
16162
17951
|
shiftCoverage,
|
|
17952
|
+
smartFilterRows,
|
|
17953
|
+
smartObjectLayers,
|
|
16163
17954
|
smoothCoverage,
|
|
16164
17955
|
smoothPoints,
|
|
16165
17956
|
solidSurface,
|
|
@@ -16197,6 +17988,7 @@ export {
|
|
|
16197
17988
|
updateLayerEffect,
|
|
16198
17989
|
updateLayerFilter,
|
|
16199
17990
|
updateLayerMaskProps,
|
|
17991
|
+
updateSmartFilter,
|
|
16200
17992
|
uvToXy,
|
|
16201
17993
|
validateEffectGradient,
|
|
16202
17994
|
validateLayerEffect,
|
|
@@ -16213,6 +18005,7 @@ export {
|
|
|
16213
18005
|
wbMatrix,
|
|
16214
18006
|
whiteBalanceFromSample,
|
|
16215
18007
|
whiteBalanceMatrix,
|
|
18008
|
+
withFilterMaskSurface,
|
|
16216
18009
|
withNewIds,
|
|
16217
18010
|
writeAdjustmentBlock,
|
|
16218
18011
|
writeLfx2,
|