Files
photoshop-ai-edit/cep/host/host.jsx
T
2026-08-03 19:37:45 +08:00

282 lines
9.8 KiB
React

// ExtendScript host for the AI Edit CEP panel.
// ASCII only: ExtendScript guesses this file's encoding, so all localized
// strings are passed in from the panel instead of written here.
function aiEditHasDocument() {
try {
return app.documents.length > 0 ? "yes" : "no";
} catch (e) {
return "no";
}
}
function aiEditShowComposite(doc) {
try {
doc.activeChannels = doc.componentChannels;
} catch (e) {
try {
doc.activeChannels = [doc.channels[0]];
} catch (fallbackError) {}
}
}
function aiEditPickImages(title, filter) {
try {
var picked = File.openDialog(title, filter, true);
if (!picked) return "";
if (!(picked instanceof Array)) picked = [picked];
var paths = [];
for (var i = 0; i < picked.length; i++) paths.push(picked[i].fsName);
return paths.join("\n");
} catch (e) {
return "";
}
}
// A native window is the reliable way to type CJK: the CEF panel does not
// always receive IME composition events.
function aiEditPromptDialog(title, label, okText, cancelText, initial) {
try {
var dialog = new Window("dialog", title);
dialog.alignChildren = "fill";
dialog.margins = 14;
dialog.spacing = 10;
dialog.add("statictext", undefined, label);
var field = dialog.add("edittext", undefined, initial || "", {
multiline: true,
wantReturn: true
});
field.preferredSize = [420, 150];
var row = dialog.add("group");
row.alignment = "right";
row.spacing = 8;
var cancel = row.add("button", undefined, cancelText, { name: "cancel" });
var confirm = row.add("button", undefined, okText, { name: "ok" });
var text = null;
confirm.onClick = function () {
text = field.text;
dialog.close(1);
};
cancel.onClick = function () {
dialog.close(0);
};
field.active = true;
if (dialog.show() !== 1 || text === null) return "cancel";
// escape() keeps the result ASCII so evalScript can carry it back.
return "ok:" + escape(text);
} catch (e) {
return "cancel";
}
}
function aiEditGetGenerationSize() {
try {
if (app.documents.length === 0) return "";
var doc = app.activeDocument;
var width = doc.width.as("px");
var height = doc.height.as("px");
try {
var bounds = doc.selection.bounds;
width = bounds[2].as("px") - bounds[0].as("px");
height = bounds[3].as("px") - bounds[1].as("px");
} catch (noSelection) {}
return Math.max(1, Math.round(width)) + "x" + Math.max(1, Math.round(height));
} catch (e) {
return "";
}
}
function aiEditCaptureSelection() {
var doc = null;
var reference = null;
try {
if (app.documents.length === 0) return "none";
doc = app.activeDocument;
aiEditShowComposite(doc);
var bounds;
try {
bounds = doc.selection.bounds;
} catch (selectionError) {
return "none";
}
var token = "AI Edit Selection " + new Date().getTime();
var channel = doc.channels.add();
channel.name = token;
doc.selection.store(channel);
aiEditShowComposite(doc);
var width = Math.max(1, Math.round(bounds[2].as("px") - bounds[0].as("px")));
var height = Math.max(1, Math.round(bounds[3].as("px") - bounds[1].as("px")));
var boundsText = [
Math.round(bounds[0].as("px")),
Math.round(bounds[1].as("px")),
width,
height
].join(",");
// Avoid copy/copyMerged: Photoshop reports that command as unavailable
// for some active layers and documents. A duplicate keeps the merged
// visible pixels and can be cropped without using the clipboard.
reference = doc.duplicate("AI Edit Reference", false);
reference.flatten();
// Do not call selection.clear here. Photoshop rejects that command
// when the duplicated document resolves to a locked background layer.
// Cropping to the captured bounds keeps the selected image usable as
// a reference without changing pixels through the clipboard/delete UI.
reference.crop(bounds);
var file = new File(Folder.temp.fsName + "/ai-edit-selection-" + new Date().getTime() + ".png");
var options = new PNGSaveOptions();
reference.saveAs(file, options, true, Extension.LOWERCASE);
reference.close(SaveOptions.DONOTSAVECHANGES);
app.activeDocument = doc;
aiEditShowComposite(doc);
return "ok:" + token + "|" + file.fsName + "|" + boundsText;
} catch (e) {
try {
if (reference) reference.close(SaveOptions.DONOTSAVECHANGES);
if (doc) {
app.activeDocument = doc;
aiEditShowComposite(doc);
}
} catch (closeError) {}
return "error:" + e.toString();
}
}
function aiEditAddLayerMask(hasSelection) {
var classicMask = new ActionDescriptor();
classicMask.putClass(charIDToTypeID("Nw "), charIDToTypeID("Chnl"));
var classicTarget = new ActionReference();
classicTarget.putEnumerated(
charIDToTypeID("Chnl"),
charIDToTypeID("Chnl"),
charIDToTypeID("Msk ")
);
classicMask.putReference(charIDToTypeID("At "), classicTarget);
classicMask.putEnumerated(
charIDToTypeID("Usng"),
charIDToTypeID("UsrM"),
charIDToTypeID(hasSelection ? "RvlS" : "RvlA")
);
try {
executeAction(charIDToTypeID("Mk "), classicMask, DialogModes.NO);
return;
} catch (classicError) {}
var modernMask = new ActionDescriptor();
var newChannel = new ActionReference();
newChannel.putClass(stringIDToTypeID("channel"));
modernMask.putReference(charIDToTypeID("null"), newChannel);
var currentLayer = new ActionReference();
currentLayer.putEnumerated(
stringIDToTypeID("layer"),
stringIDToTypeID("ordinal"),
stringIDToTypeID("targetEnum")
);
modernMask.putReference(charIDToTypeID("At "), currentLayer);
modernMask.putEnumerated(
stringIDToTypeID("using"),
stringIDToTypeID("userMaskEnabled"),
stringIDToTypeID(hasSelection ? "revealSelection" : "revealAll")
);
executeAction(charIDToTypeID("Mk "), modernMask, DialogModes.NO);
}
function aiEditPlaceImage(filePath, layerName, channelName, boundsText) {
try {
if (app.documents.length === 0) return "error:missing-document";
var file = new File(filePath);
if (!file.exists) return "error:missing-file";
var doc = app.activeDocument;
var selectionChannel = null;
var restoreChannel = null;
var hadCurrentSelection = false;
var hasSelection = false;
if (channelName) {
try {
try {
doc.selection.bounds;
hadCurrentSelection = true;
restoreChannel = doc.channels.add();
restoreChannel.name = "AI Edit Restore " + new Date().getTime();
doc.selection.store(restoreChannel);
} catch (noCurrentSelection) {}
selectionChannel = doc.channels.getByName(channelName);
doc.selection.load(selectionChannel);
hasSelection = true;
} catch (channelError) {}
}
var place = new ActionDescriptor();
place.putPath(charIDToTypeID("null"), file);
place.putEnumerated(charIDToTypeID("FTcs"), charIDToTypeID("QCSt"), charIDToTypeID("Qcsa"));
var offset = new ActionDescriptor();
offset.putUnitDouble(charIDToTypeID("Hrzn"), charIDToTypeID("#Pxl"), 0);
offset.putUnitDouble(charIDToTypeID("Vrtc"), charIDToTypeID("#Pxl"), 0);
place.putObject(charIDToTypeID("Ofst"), charIDToTypeID("Ofst"), offset);
place.putBoolean(stringIDToTypeID("linked"), false);
executeAction(charIDToTypeID("Plc "), place, DialogModes.NO);
var placedLayer = app.activeDocument.activeLayer;
if (layerName) placedLayer.name = layerName;
if (boundsText) {
var boundsValues = boundsText.split(",");
if (boundsValues.length === 4) {
var targetLeft = Number(boundsValues[0]);
var targetTop = Number(boundsValues[1]);
var targetWidth = Number(boundsValues[2]);
var targetHeight = Number(boundsValues[3]);
var placedBounds = placedLayer.bounds;
var placedWidth = Math.max(1, placedBounds[2].as("px") - placedBounds[0].as("px"));
var placedHeight = Math.max(1, placedBounds[3].as("px") - placedBounds[1].as("px"));
placedLayer.resize(
targetWidth / placedWidth * 100,
targetHeight / placedHeight * 100,
AnchorPosition.TOPLEFT
);
placedBounds = placedLayer.bounds;
placedLayer.translate(
targetLeft - placedBounds[0].as("px"),
targetTop - placedBounds[1].as("px")
);
}
}
app.activeDocument.activeLayer = placedLayer;
aiEditAddLayerMask(hasSelection);
if (selectionChannel) {
if (hadCurrentSelection && restoreChannel) doc.selection.load(restoreChannel);
else doc.selection.deselect();
if (restoreChannel) restoreChannel.remove();
}
aiEditShowComposite(doc);
return "ok";
} catch (e) {
try {
aiEditShowComposite(app.activeDocument);
} catch (restoreViewError) {}
return "error:" + e.toString();
}
}