282 lines
9.8 KiB
React
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();
|
|
}
|
|
}
|