Mini Kabibi Habibi
// (c) Copyright 2011, 2015, 2023. Adobe Systems, Incorporated. All rights reserved.
//
// CAFcropCorners.jsx - Apply Content Aware Fill (CAF) to the corners/sides
// left blank when an image is rotated or extended via a crop operation.
//
// John Peterson, Adobe Systems, 2011, 2015, 2023
//
// on localized builds we pull the $$$/Strings from a .dat file
$.localize = true;
var g_StackScriptFolderPath = app.path + "/"+ localize("$$$/ScriptingSupport/InstalledScripts=Presets/Scripts")
+ "/Stack Scripts Only/";
$.evalFile(g_StackScriptFolderPath + "Geometry.jsx");
$.evalFile(g_StackScriptFolderPath + "Terminology.jsx");
$.evalFile(g_StackScriptFolderPath + "StackSupport.jsx" );
$.evalFile(g_StackScriptFolderPath + "PolyClip.jsx" );
function S(n) { return stringIDToTypeID(n); }
var fillMethod = 'ca-fill'; // or 'gen-expand'
var generativePrompt = "";
var service_version = "";
var cleanupNeeded = false;
var seedValue = null;
var scriptResult = '';
// Convert a JSON string into an action descriptor.
function json2desc(jsonSrc)
{
var desc = new ActionDescriptor();
desc.putString(S("json"), jsonSrc );
var resultDesc = executeAction( S("convertJSONdescriptor"), desc, DialogModes.NO );
return resultDesc.getObjectValue(S("object"));
}
// In developer mode (see Preferences > Plugins), the Actions panel has a flyout menu
// "Copy as JavaScript". The core of resulting code are action descriptors for
// running the commands. By converting the JSON to a string, you can run it here.
function executeJSONstr(jsonStr)
{
// Grab event name from the JSON, and remove it to form the parameter descriptor
var eventRegex = /^{"_obj":"(\w+)",/;
var m = jsonStr.match(eventRegex)
if (m)
{
// Split the event key from the parameters
var event = m[1];
jsonStr = jsonStr.replace( eventRegex, "{");
var desc = json2desc(jsonStr);
return executeAction( S(event), desc, DialogModes.NO );
}
var m = jsonStr.match(/^{"_obj":"(\w+)"}$/); // See if event has no parameters.
if (m)
return executeAction(S(m[1]), new ActionDescriptor(), DialogModes.NO);
alert("event not found?");
}
// This is a secret handshake to allow the script to run in debug mode.
// Possible values for 'shortcutKey' are 'ctrlKey', 'shiftKey, 'altKey', 'metaKey'
// ('metaKey' is the command key on Mac, and the Windows key on Windows)
function secretDebugHandshake(shortcutKey)
{
var isDebugBuild = (app.scriptingVersion.search(/\s+0x80/) > 0)
|| ($.version.search(/debug/) > 0);
if (! shortcutKey)
shortcutKey = 'shiftKey';
var keyValue = ScriptUI.environment.keyboardState[shortcutKey];
return (isDebugBuild && keyValue);
}
// Amount CAF area overlaps the image. This formula was arrived at empirically.
// It needs something that grows slowly with the image area, but reliably encompasses
// enough area that the CAF has some overlap to work with.
function overlapAmount( bounds )
{
if (fillMethod == 'gen-expand')
return 0; // synthetic_fill_controller::pre_dilate_gen_mask() will adjust the overlap
var overlap = Math.log( Math.sqrt(bounds.getArea()) * 5 - 18 );
if (overlap < 5)
overlap = 5;
return overlap;
}
function syntheticFillSelection()
{
var ref = new ActionReference();
ref.putEnumerated( S( "document" ), S( "ordinal" ), S( "targetEnum" ) );
var serviceDesc = new ActionDescriptor( S("clio") );
serviceDesc.putBoolean(S("gi_DILATE"), false);
if (seedValue)
serviceDesc.putInteger(S("gi_SEED"), seedValue);
var serviceContainer = new ActionDescriptor();
serviceContainer.putObject(S("clio"), S("clio"), serviceDesc);
var desc = new ActionDescriptor();
desc.putReference( S( "null" ), ref );
desc.putObject( S("serviceOptionsList"), S("clio"), serviceContainer );
desc.putString( S( "prompt" ), generativePrompt );
if (!secretDebugHandshake("ctrlKey")) // ctrlKey in debug skips waiting for the clio service
desc.putString(S("serviceVersion"), service_version);
var serviceIDstr = "clio";
if (secretDebugHandshake("ctrlKey")) // ctrlKey in debug skips waiting for the clio service
serviceIDstr = "emulator";
desc.putString( S( "serviceID" ), serviceIDstr );
desc.putString( S( "workflow" ), "out_painting" ); // Crop was used, that's Generative Expand, tagged as "out_painting"
desc.putBoolean( S("preDilate"), true );
executeAction( S( "syntheticFill" ), desc, DialogModes.NO );
// If the call to executeAction succeeds, it just returns the 'desc' parameter.
// We only really want scriptResult to record errors; this is handled by the catch
// in doContentAwareFill().
}
// On Debug builds, CAF is *really* slow. So if it's a debug build and the shift key
// is down, we disable CAF and just leave the selection up.
function doContentAwareFill()
{
scriptResult = '';
if (secretDebugHandshake("shiftKey"))
return;
try {
if (fillMethod == 'gen-expand')
syntheticFillSelection();
else
contentAwareFillSelection();
}
catch (err)
{
app.activeDocument.selection.deselect();
// Before exiting, flag we need to clean up the command history.
cleanupNeeded = true;
scriptResult = String(err.number) + " " + err.message;
// Specifically alert for CAF errors:
// errPatchMatchHoleTooSmall = -26330, errPatchMatchHoleTooLarge = -26331,
// errPatchMatchImageTooSmall = -26332, errPatchMatchTargetPointsCollinear = -26333,
// errPatchMatchSourcePointsCollinear = -26334,
// errPatchMatchTargetPointsTooClose = -26335,
// errPatchMatchSourcePointsTooClose = -26336,
// -1 is a General PS error, the other three are (quoting interfaces/PSErrors.h):
//
var CAFerrors = {"-1":1, "-26330":1, "-26331":1, "-26332":1, "-26333":1,
"-26334":1, "-26335":1, "-26336":1};
if (err.number in CAFerrors)
{
// Skip the scary "General PS error" part, if possible
var msgParts = err.message.split("-");
if (msgParts.length > 1)
alert(msgParts[1]);
else
alert(err.message);
}
else if (err.number != 8007) // Skip cancel
throw err;
}
}
// Because the polygon clipping code tends to fail if points lie exactly on edges,
// or edges are parallel & co-incident, we handle those two cases separately.
// If only rotation is applied, and the crop bounds is expanded to exactly
// include the corners of the rotated document, then compute the areas
// to be filled.
//
// AdobePatentID="P5923-US"
CAFCorners = function(angle, orginalWidth, orginalHeight)
{
if (angle % 90.0 == 0.0)
return; // Nothing to do.
angle *= Math.PI/180.0; // We get passed degrees, need radians.
// Functions to find the next/previous point on a list of four points (rectangle),
// wrapping around if necessary
function nextPoint( i, pointList )
{
return pointList[(Number(i) + 1) % 4];
}
function prevPoint( i, pointList )
{
return pointList[(Number(i) == 0) ? 3 : Number(i) - 1];
}
var rotatedPoints, rotatedBounds;
// Given an angle and a rectangle, compute the rotated
// rectangle and a new bounding rectangle for it.
computeRotation = function( angle, rect )
{
// Rotate rect about the origin, and find the bounds of the rotated rectangle.
rect.setCenter( TPoint.kOrigin );
var i;
rotatedPoints = rect.getCornerPoints();
rotatedBounds = new TRect(0,0,0,0);
for (i in rotatedPoints)
{
rotatedPoints[i] = rotatedPoints[i].rotate(angle);
rotatedBounds.extendTo( rotatedPoints[i] );
}
// Move the origin back to top left corner of the enclosing rect,
// the new center is based on that.
rotatedBounds.offset( -rotatedBounds.getTopLeft() );
this.fNewCenter = rotatedBounds.getCenter();
for (i in rotatedPoints)
rotatedPoints[i] += this.fNewCenter;
}
var bounds = new TRect( 0, 0, orginalWidth, orginalHeight );
computeRotation( angle, bounds );
// Avoid extra white margin on the sides
rotatedBounds.extendTo( rotatedBounds.getBottomRight() + new TPoint(1,1) );
// Amount CAF area overlaps the image. Spent too much time thinking about this.
var overlap = overlapAmount( bounds );
// Short hand names; R is the rotated rect's points, B is the enclosing rect's points.
var i, R = rotatedPoints;
var B = rotatedBounds.getCornerPoints();
// For each corner...
for (i = 0; i < 4; ++i)
{
// Depending on the angle, work out the geometry of the corner area left
// exposed when the rectangular image is rotated
if (angle > 0)
{
R[1].fX += 1.0; // Avoid extra white margin on the sides
R[2].fY += 1.0;
var offsetVector = (nextPoint( i, R ) - R[i]).normalize() * overlap;
var selPoints = [ prevPoint( i, R ), B[i], R[i],
R[i] + offsetVector,
prevPoint( i, R ) + offsetVector,
prevPoint( i, R ) ];
}
else
{
R[2].fX += 1.0;
R[3].fY += 1.0;
var offsetVector = (prevPoint( i, R ) - R[i] ).normalize() * overlap;
var selPoints = [ R[i], B[i], nextPoint( i, R ),
nextPoint( i, R ) + offsetVector,
R[i] + offsetVector, R[i] ];
}
createPolygonSelection( selPoints, i > 0 );
}
doContentAwareFill();
}
//
// No rotation, so compute the leftover rectangles to fill.
//
function CAFNoRotate( docRight, docBottom, cropRect )
{
var docRect = new TRect( 0, 0, docRight, docBottom );
// PS-113198 force to pixel boundaries.
cropRect.fLeft = Math.round(cropRect.fLeft);
cropRect.fTop = Math.round(cropRect.fTop);
cropRect.fRight = Math.round(cropRect.fRight);
cropRect.fBottom = Math.round(cropRect.fBottom);
var overlap = overlapAmount( docRect );
var CAFoffset = -cropRect.getTopLeft();
var CAFinset = new TPoint( -overlap, -overlap );
var rectList = cropRect.subtract( docRect );
if (rectList)
{
cropRect.offset( CAFoffset ); // Move cropRect to the same origin the document is in
for (var i = 0; i < rectList.length; ++i)
{
var CAFrect = rectList[i];
CAFrect.offset( CAFoffset );
CAFrect.inset( CAFinset );
if (fillMethod == 'gen-expand')
CAFrect.intersectWith( cropRect ); // CA crop needs the extra area to avoid background bleed
var rectPoints = CAFrect.getCornerPoints();
rectPoints.push( rectPoints[0] ); // Close polygon
createPolygonSelection( rectPoints, i > 0 ); // addTo selection after i=0
}
doContentAwareFill();
}
}
//
// General case - document is rotated and (possibly) cropped against the cropRect
//
//
// AdobePatentID="P5923-US"
function CAFWithRotate( docRight, docBottom, angle, cropPoints )
{
angle *= Math.PI/180.0; // We get passed degrees, need radians.
var i, docRect = new TRect( 0, 0, docRight, docBottom );
var cropCenter = TRect.getBounds( cropPoints ).getCenter();
var docPoints = docRect.getCornerPoints();
// Undo the rotation of the cropRect, and apply it to the docRect
for (i = 0; i < 4; ++i)
{
docPoints[i] = ((docPoints[i] - cropCenter).rotate(angle)) + cropCenter;
cropPoints[i] = ((cropPoints[i] - cropCenter).rotate(angle)) + cropCenter;
}
// Offset so it's in the cropRect coordinates
var cropRect = TRect.getBounds(cropPoints);
var offset = new TPoint( -cropRect.fLeft, -cropRect.fTop );
cropRect.offset( offset );
for (i = 0; i < 4; ++i)
{
docPoints[i] = docPoints[i] + offset;
cropPoints[i] = cropPoints[i] + offset;
}
// Shrink the docRect to produce some overlap.
var diagDist = Math.sqrt(docRight*docRight+docBottom*docBottom);
var shrinkBy = (diagDist - overlapAmount(docRect)*2)/diagDist;
docRect = TRect.getBounds(docPoints);
cropCenter = docRect.getCenter();
for (i = 0; i < 4; ++i)
docPoints[i] = (docPoints[i] - cropCenter) * shrinkBy + cropCenter;
// If the document is completely inside the cropping area, then
// no clipping is required. Just build the clip poly from the two inputs.
if (cropRect.contains( docRect ))
{
docPoints.reverse();
docPoints.push( docPoints[0] );
cropPoints = cropPoints.concat( docPoints );
cropPoints.push( cropPoints[3] );
cropPoints.push( cropPoints[0] );
createPolygonSelection( cropPoints );
doContentAwareFill();
}
else // General case: call polygon clipper
{
var polyList = TPoint.intersectConvexPolygons( docPoints, cropPoints, "minusSubject" );
if (polyList)
{
for (i = 0; i < polyList.length; ++i)
createPolygonSelection( polyList[i], i > 0 );
doContentAwareFill();
}
}
}
function getCmdName()
{
var cmdNames = { "ca-fill": localize("$$$/CropTool/CropOptions/AutoFillOnCrop=Content-Aware Crop"),
"gen-expand": localize("$$$/CropTool/CropOptions/GenerativeExpandScr=Generative Expand")};
return cmdNames[fillMethod];
}
// Cleaning up the command history must be done *after* the suspended history state runs.
function cleanupFailure()
{
if (cleanupNeeded)
{
// Remove the history state of the failed command
executeJSONstr('{"_obj":"select","_target":[{"_property":"currentHistoryState","_ref":"historyState"}]}');
executeJSONstr('{"_obj":"delete","_target":[{"_property":"currentHistoryState","_ref":"historyState"}]}');
}
return scriptResult; // Returns the error set in doContentAwareFill(), if any. Otherwise ''.
}
// In order to get just a single event in the history state, we invoke the
// functions above via these helper functions to suspend History
function runCAFRotateOnly( angle, width, height )
{
var args = [angle, width, height].join(",");
app.activeDocument.suspendHistory( getCmdName(), "CAFCorners(" +args + ");");
return cleanupFailure();
};
// Note cropRect is passed in as a string, and evaluated with the suspendHistory call.
function runCAFNoRotate( dr, db, cropRect )
{
var args = [dr, db, cropRect].join(",");
app.activeDocument.suspendHistory( getCmdName(), "CAFNoRotate(" + args + ");" );
return cleanupFailure();
}
function runCAFRotate( dr, db, cropRect, rotate, cropRectPts )
{
var args = [dr, db, rotate, cropRectPts].join(",");
// $.writeln("CAFWithRotate(" + args + ");");
app.activeDocument.suspendHistory( getCmdName(), "CAFWithRotate(" + args + ");" );
return cleanupFailure();
}
//CAFNoRotate(768,512,new TRect(-36.000000,-32.000000,724.000000,544.000000));
//CAFWithRotate(3072,2048,new TRect(-70.973594,-245.201704,3287.349782,2148.302146),-3.225869,new TPoint(-6.993695,7.630765));
//CAFWithRotate(3072,2048,new TRect(562.709409,-121.149406,3277.244709,2219.612453),-3.600068,new TPoint(-5.290591,175.653866));
// Tests
//runCAF(-2.6574382011561966,3072.0000000000000,2048.0000000000000);
//CAFNoRotate( 3072, 2048, -72, -65.967789, 3168, 2131.960284 )
//CAFWithRotate(3072,2048,-178.820332,-137.560556,3018.96466,2057.560556,-2.48215);
// GenTech/ExpandTest1.psd
//useGenerativeFill = true;
//runCAFNoRotate(1024.000000,768.000000,'new TRect(-281.862372,-216.000000,1024.000000,768.000000)');