Mini Kabibi Habibi
// (c) Copyright 2006-2007. Adobe Systems, Incorporated. All rights reserved.
//
// Photomerge in ExtendScript.
// Translated from the original C++ automation & filter plugins
//
// John Peterson, Adobe Systems, 2006
//
// Adobe Patent or Adobe Patent Pending Invention Included Within this File
/*
@@@BUILDINFO@@@ Photomerge.jsx 3.0.0.2
*/
/*
// BEGIN__HARVEST_EXCEPTION_ZSTRING
<javascriptresource>
<name>$$$/JavaScripts/Photomerge/Menu=Photomerge...</name>
<menu>automate</menu>
<eventid>0f9db13f-a772-4035-9020-840f0e5e2f02</eventid>
<terminology><![CDATA[<< /Version 1
/Events <<
/0f9db13f-a772-4035-9020-840f0e5e2f02 [($$$/AdobePlugin/Photomerge/Name=Photomerge...)]
>>
>> ]]>
</terminology>
</javascriptresource>
// END__HARVEST_EXCEPTION_ZSTRING
*/
// on localized builds we pull the $$$/Strings from a .dat file
$.localize = true;
// Put header files in a "Stack Scripts Only" folder. The "...Only" tells
// PS not to place it in the menu. For that reason, we do -not- localize that
// portion of the folder name.
var g_StackScriptFolderPath = app.path + "/"+ localize("$$$/ScriptingSupport/InstalledScripts=Presets/Scripts") + "/"
+ localize("$$$/private/Exposuremerge/StackScriptOnly=Stack Scripts Only/");
$.evalFile(g_StackScriptFolderPath + "LatteUI.jsx");
$.evalFile(g_StackScriptFolderPath + "StackSupport.jsx");
$.evalFile(g_StackScriptFolderPath + "CreateImageStack.jsx");
$.evalFile(g_StackScriptFolderPath + "Geometry.jsx");
$.evalFile(g_StackScriptFolderPath + "PolyClip.jsx");
// debug level: 0-2 (0:disable, 1:break on error, 2:break at beginning)
// Must leave at zero, otherwise trapping gFileFromBridge fails on QA's debug builds.
$.level = 0; // (Window.version.search("d") != -1) ? 1 : 0;
// debugger; // launch debugger on next line
if (typeof(PMDebug) == 'undefined')
var PMDebug = false;
/************************************************************/
// photomerge routines
// Debug - write the trapezoid in Matlab format
function dumpTrap( name, corners )
{
var i;
if (! PMDebug) return;
$.write( name + "= [" );
for (i in corners)
$.write( ((i > 0) ? "; " : "" ) + corners[i].fX + " " + corners[i].fY );
$.writeln( "];");
}
StackElement.prototype.dumpMLCorners = function()
{
// Weed out file suffix (chokes matlab)
dumpTrap( this.fName.match(/([^.]+)/)[1], this.fCorners );
}
// Set the fCorners of the layer to the bounds of the Photoshop layer.
StackElement.prototype.setCornersToLayerBounds = function( stackDoc )
{
if (typeof(stackDoc) == "undefined")
stackDoc = app.activeDocument;
var bounds = stackDoc.layers[this.fName].bounds;
this.fCorners = new Array();
this.fCorners[0] = new TPoint( bounds[0].as("px"), bounds[1].as("px") );
this.fCorners[2] = new TPoint( bounds[2].as("px"), bounds[3].as("px") );
this.fCorners[1] = new TPoint( this.fCorners[2].fX, this.fCorners[0].fY );
this.fCorners[3] = new TPoint( this.fCorners[0].fX, this.fCorners[2].fY );
}
// Add the corner data to the string of per-stackElement information
// that gets passed to the filter plugin
StackElement.prototype.addPieceData = function()
{
if (typeof(this.fCorners) != "undefined")
{
// Add corners in place of trailing '\n'
this.fString = this.fString.slice(0,-1) + "fCorners=";
for (j = 0; j < 4; j++)
this.fString += " " + this.fCorners[j].fX.toString() + " " + this.fCorners[j].fY.toString();
this.fString += "\t";
if (typeof(this.fScale) != "undefined")
this.fString += ("fScale=" + this.fScale.toString() + "\t");
if ((typeof(this.fConnectedTo) != "undefined") && this.fConnectedTo)
this.fString += "fConnectedTo=" + this.fConnectedTo.fLayerID + "\t";
if (typeof(this.fLayerID) != "undefined")
this.fString += "fLayerID=" + this.fLayerID.toString() + "\t";
this.fString += "\n";
}
else
debugger; // Corner data missing!
}
StackElement.prototype.overlapArea = function( other )
{
if (other == this)
return TPoint.polygonArea( this.fCorners );
var overlapBounds = TRect.intersection( this.fBoundsCache, other.fBoundsCache );
if (overlapBounds.isEmpty())
return 0.0;
var clipPoly = TPoint.intersectConvexPolygons( this.fCorners, other.fCorners );
if (! clipPoly)
return 0.0;
else return TPoint.polygonArea( clipPoly );
}
// Find the points where the two quadrilaterals intersect (yes, eight is a theoretical max)
// Note situations where one piece intersects the same edge twice - special
// case handled by SoftEdgeBlend
StackElement.prototype.findQuadIntersections = function( other, intersections )
{
var i, j;
var curIntersections, numIntersections = 0;
var innerEdgeIntersections = [ 0, 0, 0, 0 ];
var outerEdgeIntersections = [ 0, 0, 0, 0 ];
var thisEdge = false;
var otherEdge = false;
for (i = 0; i < 4; i++)
{
var eb0 = other.fCorners[i];
var eb1 = other.fCorners[(i + 1) > 3 ? 0 : i + 1];
curIntersections = numIntersections;
for (j = 0; j < 4; j++)
{
var ed0 = this.fCorners[j];
var ed1 = this.fCorners[(j + 1) > 3 ? 0 : j + 1];
var cross = TPoint.lineSegmentIntersect( eb0, eb1, ed0, ed1 );
if (cross != TPoint.kInfinite)
{
intersections[numIntersections++] = cross;
innerEdgeIntersections[j]++;
}
}
outerEdgeIntersections[i] = numIntersections - curIntersections;
}
if (numIntersections == 2)
for (i = 0; i < 4; i++)
{
if (innerEdgeIntersections[i] == 2) thisEdge = true;
if (outerEdgeIntersections[i] == 2) otherEdge = true;
}
return {"numIntersections":numIntersections, "thisEdge":thisEdge, "otherEdge":otherEdge};
}
// Look for a point of this that's inside the corners of other.
StackElement.prototype.findSingleInsidePoint = function( other )
{
var i;
var result = new Object();
result.numFound = 0;
for (i = 0; i < 4; i++)
if (this.fCorners[i].pointInQuad( other.fCorners ))
{
result.numFound++;
result.insidePt = this.fCorners[i];
}
return result;
}
// "Adobe patent application tracking # B349, entitled 'Method and apparatus for Layer-based Panorama Adjustment and Editing', inventor: John Peterson"
//
// Because we're dealing with quads, we can't use the simple scheme
// to generate blend rects that the rectangles use - black stuff
// from the rects will seep in. So instead, we use various heuristics to
// figure out how the quads themselves intersect. This isn't too hard
// when there's just two intersection points (usual case), but there are
// some pathological cases where there are many more intersections (the octogon
// from one square 45 deg. off from another is the classic example).
StackElement.prototype.softEdgeBlend = function( other, blendRad )
{
var i, j;
var intersections = new Array(); // Worst case is a square inside a 45 degree rot square
// If there's no distortion, do the blend strictly on the rectangles
/* if ((!IsQuadMapped() && !other->IsQuadMapped())
|| (IsRectilinear() && other->IsRectilinear()))
{
Assert_( other->fWarpedRaster );
fWarpedRaster->SoftEdgeBlendRasters( *(other->fWarpedRaster), blendRad );
return;
}
*/
var bounds = this.getBounds();
// Find the points where the two quadrilaterals intersect
var intResult = this.findQuadIntersections( other, intersections );
var numIntersections = intResult.numIntersections;
var thisEdgeTwice = intResult.thisEdge;
var otherEdgeTwice = intResult.otherEdge;
var thisInsidePt = this.findSingleInsidePoint( other );
var otherInsidePt = other.findSingleInsidePoint( this );
// If quads don't overlap, just bail
if (numIntersections == 0)
return;
// Handle cases where just one point overlaps the other piece
var insidePoint;
if (otherEdgeTwice || ((numIntersections == 2) && (thisInsidePt.numFound == 1)))
{
insidePoint = thisInsidePt.insidePt;
this.makeBlendTrapezoid( intersections[0], intersections[1], insidePoint, blendRad, false );
return;
}
if (thisEdgeTwice /*|| ((numIntersections == 2) && (otherInsidePt.numFound == 1))*/)
{
insidePoint = otherInsidePt.insidePt;
this.makeBlendTrapezoid( intersections[0], intersections[1], insidePoint, blendRad, false );
return;
}
// ...More than two corners overlap, apply heuristics to find reasonable blending
$.bp( numIntersections > 8 );
// If we got more than two points, pick the two furthest apart
if (numIntersections > 2)
{
var max0, max1;
var maxDist = -1.0;
for (i = 0; i < numIntersections - 1; i++)
for (j = i + 1; j < numIntersections; j++)
{
var dist = (intersections[i] - intersections[j]).vectorLength();
if (dist > maxDist)
{
max0 = intersections[i];
max1 = intersections[j];
maxDist = dist;
}
}
$.bp( maxDist <= -1.0 );
intersections[0] = max0;
intersections[1] = max1;
numIntersections = 2;
}
TPoint.clipLineToRect( bounds, intersections[0], intersections[1] );
// The point furthest away from the cut line on the "other" image
// is the one that we blend toward (the "dark" corner).
var maxPoint = 0, maxDist = -1;
for (i = 0; i < 4; i++)
{
var dist = Math.abs( other.fCorners[i].distanceToLine( intersections[0], intersections[1] ) );
if (dist > maxDist)
{
maxDist = dist;
maxPoint = i;
}
}
this.makeBlendTrapezoid( intersections[0], intersections[1], other.fCorners[maxPoint], blendRad, true );
}
// "Adobe patent application tracking # B349, entitled 'Method and apparatus for Layer-based Panorama Adjustment and Editing', inventor: John Peterson"
//
// Create a layer mask that fades out from the edge0,edge1 cutline towards "insidePt"
// If two points are inside, construct the mask fade from the edge of the image.
//
// insidePt
// | * |
// | / \ |
// | / \ |
// insetLine-> | +-/-----\-+ | --
// | |/ \| | | radius
// baseLine-> +-------*---------*------+ --
// /edge0 \edge1
// / \
//
//
StackElement.prototype.makeBlendTrapezoid = function( edge0, edge1, insidePt, radius, useCorners )
{
function sgn(x) { if (x < 0) return -1; if (x > 0) return 1; return 0; }
function wrap4(i, next)
{
i += next;
if (i > 3) return i % 4;
if (i < 0) return 3;
return i;
}
// Create a layer mask
selectOneLayer( app.activeDocument, this.fName );
// app.activeDocument.activeLayer = app.activeDocument.layers[this.fName]; // Broken if multiple layers selected.
createLayerMask(); // Does nothing if the layer already has a mask
// Create vector perpendicular to edge towards insidePt
var edgeDir = edge1 - edge0;
var blendDir = new TPoint( -edgeDir.fY, edgeDir.fX );
blendDir /= blendDir.vectorLength(); // Make unit length
// Make a polygonal selection covering the area
var dist = insidePt.distanceToLine( edge0, edge1 );
var blendOffset = blendDir * dist;
var blendBox;
if (! useCorners)
blendBox = [edge0, edge0+blendOffset, edge1+blendOffset, edge1, edge0];
else
{
// If the cutline slices across the image (two points on each side), then
// - Look for the edge edge0 is on
// - Figure out which corner is on the same side as "insdePt"
// - Construct the blendBox from that.
var i, nextPtInd;
for (i = 0; i < 4; i++)
if (edge0.distanceToLine( this.fCorners[i], this.fCorners[wrap4(i,1)] ) < 0.0001 )
{
if (this.fCorners[i].sideOf( edge0, edge1 ) == sgn( dist ))
nextPtInd = -1;
else
nextPtInd = 1;
blendBox = new Array();
blendBox[0] = edge0;
blendBox[1] = (nextPtInd < 0) ? this.fCorners[i] : this.fCorners[i+1];
blendBox[2] = (nextPtInd < 0) ? this.fCorners[wrap4(i, -1)] : this.fCorners[wrap4(i, 2)];
blendBox[3] = edge1;
break;
}
$.bp( i == 4 ); // Never found edge0?
}
createPolygonSelection( blendBox );
// Fill it.
var midPoint = (edge0 + edge1) * 0.5;
gradientFillLayerMask( midPoint, midPoint + blendDir * radius * sgn(dist) );
app.activeDocument.selection.deselect();
}
//////////////////////////////////////////////////////////////////////////////////
// Photomerge base class
//////////////////////////////////////////////////////////////////////////////////
const kPhotomergeAdvancedBlendingFlag = app.stringIDToTypeID( "PhotomergeAdvancedBlendingFlag" );
photomerge = new ImageStackCreator( localize("$$$/AdobePlugin/Shared/Photomerge/Process/Name=Photomerge"),
localize('$$$/AdobePlugin/Shared/Photomerge/Auto/untitled=Untitled_Panorama' ), null );
// For now, alignment is turned OFF, because we want to
// invoke it independantly.
photomerge.useAlignment = false; // We do the alignment, not PS
photomerge.hideAlignment = true;
photomerge.mustBeSameSize = false;
photomerge.mustBeUnmodifiedRaw = false;
photomerge.mustNotBe32Bit = false;
photomerge.radioButtons = ["_LOauto", "_LOperspective", "_LOcylindrical", "_LOspherical", "_LOcollage", "_LOnormal", "_LOinteractive"];
photomerge.interactiveFlag = false;
photomerge.alignmentKey = "Auto"; // Defaults to perspective
photomerge.compositionFile = null;
photomerge.advancedBlending = true;
photomerge.lensCorrection = false;
photomerge.removeVignette = false;
photomerge.autoFillTrans = false;
try {
// We want to steer people to the advanced blending option,
// so have it be on by default, rather than sticky.
// var desc = app.getCustomOptions("PhotomergeFlags001");
// photomerge.advancedBlending = desc.getBoolean( kPhotomergeAdvancedBlendingFlag );
}
catch (e)
{
}
// Get the bounds of all of the stackElements.
photomerge.getBounds = function()
{
var i;
for (i in this.stackElements)
{
if (i == 0)
this.fBounds = this.stackElements[i].getBounds();
else
this.fBounds.extendTo( this.stackElements[i].getBounds() );
}
return this.fBounds;
}
// Align selected layers by content (uses SIFT registration in Photoshop core)
// This just returns the alignment data, it does not actually transform the layers
// unless doTransform is true
photomerge.getAlignmentInfo = function( stackDoc, doTransform )
{
selectAllLayers(stackDoc, 1);
const kMargin = 10;
function offsetGroup( delta, group )
{
group.bounds.offset( delta );
var k;
for (k = 0; k < group.layers.length; ++k)
{
group.layers[k].offset( delta );
if (doTransform)
{
selectOneLayer( stackDoc, group.layers[k].fName );
// Translate gets broken when document DPI isn't 72 DPI...(PR 1417264)
// activeDocument.activeLayer.translate( UnitValue( delta.fX, "px" ), UnitValue( delta.fY, "px" ) );
translateActiveLayer( delta.fX, delta.fY );
}
}
}
var i, j, alignInfo;
var alignmentFlags = [];
if (this.lensCorrection) alignmentFlags.push(kradialDistortStr);
if (this.removeVignette) alignmentFlags.push(kvignetteStr);
alignInfo = getActiveDocAlignmentInfo( this.alignmentKey, doTransform, alignmentFlags );
// If the alignment fails completely, fake up a plan B...
// For now, just set the images side by side.
if (! alignInfo)
{
if (this.autoFillTrans)
alert(localize("$$$/AdobePlugin/Shared/Photomerge/donotfill=Some images could not be automatically aligned. A fill area could not be created."));
else
alert(localize("$$$/AdobePlugin/Shared/Photomerge/alignbad=Some images could not be automatically aligned"));
var xpos = 0;
for (i in this.stackElements)
{
this.stackElements[i].setCornersToSize();
this.stackElements[i].offset( new TPoint( xpos, 0 ) );
xpos += this.stackElements[i].getBounds().getWidth() + kMargin;
}
this.fGroups = null;
}
else
{
var layerList = alignInfo.layerInfo;
this.fGroups = new Array();
for (i = 0; i < layerList.length; ++i)
{
// Note we depend on stackElement's order matching
// the document's sheet list!
var curGroup = layerList[i].groupNum;
if (!doTransform && (layerList[i].corners.length > 0))
this.stackElements[i].fCorners = layerList[i].corners;
else
this.stackElements[i].setCornersToLayerBounds( stackDoc );
this.stackElements[i].fAlignGroup = curGroup;
this.stackElements[i].fBaseFlag = layerList[i].baseFlag;
if (typeof(this.fGroups[curGroup]) == "undefined")
{
this.fGroups[curGroup] = new Object();
this.fGroups[curGroup].hasCorners = layerList[i].corners.length > 0;
this.fGroups[curGroup].bounds = this.stackElements[i].getBounds();
this.fGroups[curGroup].layers = new Array();
this.fGroups[curGroup].xformType = layerList[i].xformType;
}
else
this.fGroups[curGroup].bounds.extendTo( this.stackElements[i].getBounds() );
this.fGroups[curGroup].layers.push( this.stackElements[i] );
}
// Now move the groups into place
// Note carefully: if the corners were given, then the group is already
// transformed into the proper spot, and we just need to move the corners to
// match. So shut off moving the layer pixels around from here on out.
offsetGroup( -this.fGroups[0].bounds.getTopLeft(), this.fGroups[0] );
for (i = 1; i < alignInfo.numGroups; ++i)
{
var spacing = Math.round(this.fGroups[i-1].bounds.getHeight() / 10.0);
offsetGroup( -this.fGroups[i].bounds.getTopLeft() + new TPoint(0, spacing + Math.round(this.fGroups[i-1].bounds.fBottom)),
this.fGroups[i] );
}
}
this.getBounds();
}
// The original Photomerge plugin needs to have the "connectivity" of the
// pieces when in perspective mode, i.e., a pieces distortion is based
// on the distortion of the one it overlaps most. This takes the
// "base" piece information from the PS core and uses overlap area to
// determine this.
photomerge.setupConnectivity = function()
{
var i;
// See if stackElem is connected to the "base". If "without"
// is given, then the path to the base must not use "without"
function isConnectedToBase( stackElem, without, dbg_count )
{
if (typeof(dbg_count) == "undefined")
dbg_count = 0;
if (typeof(without) == "undefined")
without = null;
$.bp( dbg_count > 150 ); // oops, got stuck in a loop...
if (stackElem == null)
return false;
if (stackElem == without)
return false;
if (stackElem.fConnectedTo == stackElem)
return true; // Already at base
return isConnectedToBase( stackElem.fConnectedTo, without, dbg_count + 1 );
}
// Initialize
for (i in this.stackElements)
{
this.stackElements[i].fBoundsCache = this.stackElements[i].getBounds();
this.stackElements[i].fLayerID = i;
// Bases connect to themselves.
this.stackElements[i].fConnectedTo = this.stackElements[i].fBaseFlag ? this.stackElements[i] : null;
this.stackElements[i].fNeighborOverlap = 0;
}
// Create a connection table based on the overlap of the pieces
var g, i, j, baseInd = -1;
if (this.fGroups)
for (g in this.fGroups)
{
var group = this.fGroups[g];
var connections = new Array();
var numLayers = group.layers.length;
// Ignore orphan images
if (numLayers < 2)
{
group.layers[0].fBaseFlag = false;
group.layers[0].fConnectedTo = null;
continue;
}
// If there's no perspective, there's no connections ("0" is kProjective in UAlignment.h)
if (group.xformType != 0)
{
for (i = 0; i < numLayers; ++i)
{
group.layers[i].fBaseFlag = false;
group.layers[i].fConnectedTo = group.layers[i];
}
continue;
}
for (i = 0; i < numLayers; ++i)
{
if (group.layers[i].fBaseFlag)
baseInd = i;
connections[i] = new Array();
group.layers[i].fGroupIndex = i;
if (i > 0)
for (j = 0; j < i; ++j)
{
connections[i][j] = group.layers[i].overlapArea( group.layers[j] );
connections[j][i] = connections[i][j]; // table is symentric
}
}
$.bp( baseInd == -1 ); // Never found the base?
// Debug - dump the connection table
/* for (i = 0; i < numLayers; ++i)
{
var s = "";
for (j = 0; j < numLayers; ++j)
s += ", " + Math.floor(connections[i][j]);
$.writeln(s);
}
*/
// Connect everything to the base that's connected.
for (i = 0; i < numLayers; ++i)
if ((i != baseInd) && (connections[baseInd][i] > 0))
{
group.layers[i].fConnectedTo = group.layers[baseInd];
group.layers[i].fNeighborOverlap = connections[baseInd][i];
}
// Walk the cconnectivity table and make sure everything is
// "optimally" connected.
var changes = false;
do {
changes = false;
for (i = 0; i < numLayers; ++i)
{
if (i != baseInd)
{
var curLayer = group.layers[i];
for (j = 0; j < numLayers; ++j)
{
if (((j != baseInd) && (j != i))
&& (((connections[i][j] > curLayer.fNeighborOverlap)
&& isConnectedToBase( group.layers[j], curLayer ))))
{
curLayer.fConnectedTo = group.layers[j];
curLayer.fNeighborOverlap = connections[i][j];
changes = true;
}
}
}
}
} while (changes);
// for (i = 0; i < numLayers; ++i)
// $.writeln( group.layers[i].fName + " is connected to " + (group.layers[i].fConnectedTo ? group.layers[i].fConnectedTo.fName : "??") );
}
}
photomerge.offsetStack = function( delta )
{
for (i in this.stackElements)
this.stackElements[i].offset( delta );
this.fBounds.offset( delta );
}
photomerge.scaleStack = function( s )
{
for (i in this.stackElements)
this.stackElements[i].scale( s );
this.getBounds();
}
// This gets executed before a filter plugin is invoked. "desc"
// allows passing parameters to the filter.
photomerge.customPluginArguments = function( desc )
{
var f = new File(this.stackElements[0].fFullName);
var path = File.encode( f.parent.fsName ) + (File.fs == "Windows" ? "\\" : "/");
desc.putString( app.charIDToTypeID('PMfp'), path );
if (this.compositionFile)
{
desc.putString( app.charIDToTypeID('PMrf'), this.compositionFile.fsName );
desc.putString( app.charIDToTypeID('PMcf'), File.encode( this.compositionFile.fsName ) );
}
}
photomerge.callInteractivePlugin = function( stackDoc )
{
// Scale the results to fit the screen
/* var scaleFactor = 1.0, screenSize = primaryScreenDimensions() * 0.75;
if (this.fBounds.getHeight() > this.fBounds.getWidth())
{
if (this.fBounds.getHeight() > screenSize.fY)
scaleFactor = screenSize.fY / this.fBounds.getHeight();
}
else
{
if (this.fBounds.getWidth() > screenSize.fX)
scaleFactor = screenSize.fX / this.fBounds.getWidth();
}
*/
const kMaxPieceSize = 1024; // Must match value in PhotomergeUI.cpp
var i;
// The old plugin insists on eight bit data.
if (stackDoc.bitsPerChannel != BitsPerChannelType.EIGHT)
{
stackDoc.bitsPerChannel = BitsPerChannelType.EIGHT;
this.stackDepthChanged = true;
}
if (this.compositionFile == null)
{
// Make sure the quad coordinates coorespond to the scale used by the UI plugin
var maxPieceSize = 0;
for (i in this.stackElements)
maxPieceSize = Math.max( Math.max( this.stackElements[i].fWidth, this.stackElements[i].fHeight ), maxPieceSize );
var mipLevel = 0;
while (maxPieceSize >> mipLevel > kMaxPieceSize)
mipLevel++;
var imageReduction = 1.0 / (1 << mipLevel);
this.offsetStack( -this.fBounds.getCenter() );
this.scaleStack( imageReduction );
this.offsetStack( -this.fBounds.getTopLeft() );
this.setupConnectivity();
// Add the additional per-piece metadata to pass to the filter plugin
for (i in this.stackElements)
this.stackElements[i].addPieceData();
}
// Make the result layer active
app.activeDocument.activeLayer = app.activeDocument.layers[app.activeDocument.layers.length -1];
// Note: we need an "unmodified" flag, so if no
// changes are made we skip the data recovery step...
var result, err;
try {
result = this.invokeFilterPlugin( "AdobePhotomergeUI4SCRIPT", DialogModes.ALL );
}
catch (err)
{
result = null;
}
if (result == null) // Cancelled / bombed out
{
stackDoc.close(SaveOptions.DONOTSAVECHANGES);
return null;
}
// Extract the data from the plugin
var modifiedPieceInfo = result.getString( app.charIDToTypeID('PSpc') ).split('\n');
for (i in modifiedPieceInfo)
{
// If we loaded a composition (.pmg) file, we won't have corners yet.
if (typeof(this.stackElements[i].fCorners) == "undefined")
this.stackElements[i].fCorners = new Array();
var j, pieceData = modifiedPieceInfo[i].split('\t');
for (j in pieceData)
{
var k, pair = pieceData[j].split('=');
if (pair[0] == 'fUsed')
this.stackElements[i].fUsed = eval(pair[1]);
if (pair[0] == 'fCorners')
{
var coords = pair[1].split(/\s+/).slice(1);
for (k = 0; k < 4; k++)
this.stackElements[i].fCorners[k] = new TPoint( Number(coords[k*2]), Number(coords[k*2+1]) );
}
}
}
// Remove unused photos
for (i = 0; i < this.stackElements.length; ++i)
if (! this.stackElements[i].fUsed)
{
stackDoc.layers[(this.stackElements.length-1)-i].remove();
this.stackElements.splice(i,1);
i--;
}
// Hey...it could happen.
if (this.stackElements.length < 2)
return null;
// If we run the UI, we're restricted to an eight bit stack.
// If the source images were higher, we need to reload the image stack.
if (this.stackDepthChanged)
{
stackDoc.close(SaveOptions.DONOTSAVECHANGES);
stackDoc = this.loadStackLayers();
}
this.getBounds(); // Update w/new corner data
return stackDoc;
}
// "Adobe patent application tracking # B349, entitled 'Method and apparatus for Layer-based Panorama Adjustment and Editing', inventor: John Peterson"
//
// Use the geometry of the overlapping pieces to create
// simple rectangular blend masks.
//
photomerge.quickBlend = function()
{
var i, j;
var blendRadius = Math.round(Math.min(this.stackElements[0].fWidth, this.stackElements[0].fHeight) / 10.0);
if (PMDebug)
for (i in this.stackElements)
this.stackElements[i].dumpMLCorners();
// Set up progress bar for blending
// The progress bar doesn't work - there's know way in ScriptUI to force it to update.
/* var progressWindow = latteUI( g_StackScriptFolderPath + "PMBlendingProgress.exv" );
var num = this.stackElements.length;
var progressBar = progressWindow.findControl('_progress');
progressBar.maxvalue = (num * (num + 1)) / 2;
num = 0;
progressWindow.center();
progressWindow.show();
*/
// Blend the i'th piece against the 0..i-1 pieces below it
for (i = this.stackElements.length-1; i > 0; --i)
for (j = i-1; j >= 0; j--)
{
// num++;
// progressBar.value = num; // ScriptUI bug - there's no way to force this to update.
this.stackElements[i].softEdgeBlend( this.stackElements[j], blendRadius );
}
// progressWindow.close();
}
// Wrap the advancedBlend in a try/catch so errors (i.e., user cancel)
// just stop the blend process.
photomerge.advancedBlend = function( stackDoc )
{
try {
stackDoc.changeMode( ChangeMode.RGB ); // Auto-blend requires RGB
selectAllLayers(stackDoc, 1);
if (this.autoFillTrans && this.fGroups != null)
{
if (this.fGroups.length==1) // do autoFill only if fGroup==1
advancedMergeLayersAndAutoFill();
else
{
// do original but popup an alert
advancedMergeLayers();
alert(localize("$$$/AdobePlugin/Shared/Photomerge/donotfill=Some images could not be automatically aligned. A fill area could not be created."));
}
}
else
{
advancedMergeLayers();
}
}
catch (err)
{
}
}
// With the stack elements specified, this
// portion actually creates the Panorama.
// Returns boolean indicating success/failure
photomerge.doPanorama = function()
{
var i, stackDoc = null;
function primaryScreenDimensions()
{
var i;
for (i in $.screens)
if ($.screens[i].primary)
return new TPoint( $.screens[i].right - $.screens[i].left,
$.screens[i].bottom - $.screens[i].top );
}
function resizeCanvasToFitPano()
{
// Extend the canvas to hold the panorama
var w = UnitValue( photomerge.getBounds().getWidth(), "px" );
var h = UnitValue( photomerge.getBounds().getHeight(), "px" );
app.activeDocument.resizeCanvas( w, h, AnchorPosition.TOPLEFT );
}
if (this.interactiveFlag)
{
// Filter must have eight bit depth and RGB color space
stackDoc = this.loadStackLayers( BitsPerChannelType.EIGHT );
stackDoc.changeMode(ChangeMode.RGB);
}
else
stackDoc = this.loadStackLayers();
if (! stackDoc)
return false;
// Remove spurious last layer (not needed by Photomerge)
if (app.activeDocument.layers[app.activeDocument.layers.length-1].name == this.pluginName)
app.activeDocument.layers[app.activeDocument.layers.length-1].remove();
// The UI needs everything in the top left corner
if (this.interactiveFlag)
for (i = 0; i < stackDoc.layers.length; ++i)
{
var xoff = stackDoc.layers[i].bounds[0].as("px");
var yoff = stackDoc.layers[i].bounds[1].as("px");
if ((xoff != 0) || (yoff != 0))
stackDoc.layers[i].translate( UnitValue( -xoff, "px" ), UnitValue( -yoff, "px" ) );
}
// Sort out exactly what operations we want to do.
if (! this.interactiveFlag)
{
selectAllLayers(stackDoc, 1);
this.getAlignmentInfo( stackDoc, true );
resizeCanvasToFitPano();
if (this.advancedBlending)
{
this.advancedBlend( stackDoc );
}
// The Advanced blending works so well that there's little point
// in having the rectangular gradient blends anymore. Uncomment the
// following two lines if you still want them (see similar code below
// for the interactive case).
// else
// this.quickBlend();
purgeHistoryStates();
return true;
}
// Interactive happens here
if (this.compositionFile == null)
{
this.getAlignmentInfo( stackDoc, false );
// With the corners computed by getAlignmentInfo,
// find the bounds and use that to slide the images
// over so their bounds has origin 0,0 (top left)
this.offsetStack( -this.fBounds.getTopLeft() );
}
if (this.interactiveFlag)
stackDoc = this.callInteractivePlugin( stackDoc );
if (stackDoc == null)
return false;
resizeCanvasToFitPano();
// Now apply the transformation to the pieces
for (i in this.stackElements)
this.stackElements[i].transform();
if (this.advancedBlending)
{
this.advancedBlend( stackDoc );
}
// The new "advanced blending" works well enough that there's
// little point in invoking the rectangular gradient blends anymore.
// If you really want them, you can uncomment the two lines below.
// else
// this.quickBlend();
purgeHistoryStates();
return true;
}
// Extra group breaks the main dialog's radio buttons,
// so we manually implement it here (ScriptUI lossage)
// NOTE: When called, this is a member function of radioControl,
// -not- photomerge. JavaScript voodoo.
photomerge.radioClick = function()
{
var w = this.window;
var i;
// Some of the transforms don't allow lens correction...
var allowLensCor = ((this.button_id != '_LOnormal')
&& (this.button_id != '_LOcollage')
&& (this.button_id != '_LOinteractive'));
w.findControl('_useLensCorrection').enabled = allowLensCor;
w.findControl('_removeVignette').enabled = allowLensCor;
// Be aggressive about it...
if (! allowLensCor)
{
w.findControl('_useLensCorrection').value = false;
w.findControl('_removeVignette').value = false;
// If collage or reposition, uncheck autoFill, not dimmed, so users can still check it if desired
w.findControl('_autoFillTrans').value = false;
}
for (i in photomerge.radioButtons)
{
var b = w.findControl(photomerge.radioButtons[i]);
if (b != this)
b.value = false;
}
}
photomerge.checkboxClick = function()
{
var w = this.window;
var i;
// If "Blend Images Together" is not checked, dim "Fill Transparent Areas"
var allowAutoFill = w.findControl("_advancedBlend").value;
w.findControl('_autoFillTrans').enabled = allowAutoFill;
// Be aggressive about it...
if (! allowAutoFill)
{
w.findControl("_autoFillTrans").value = false;
}
}
// Callback when "Load Composition" is clicked.
// NOTE: When called, this is a member function of buttonControl,
// -not- photomerge. JavaScript voodoo.
photomerge.loadCompositionClick = function()
{
function MacPMGType( f )
{
if (f.type == 'PhMg')
return true;
var match = f.name.match(/.*[.](.+)$/);
var suffix = match != null ? match[1].toLowerCase() : "";
if (suffix == "pmg")
return true;
if (f instanceof Folder)
{
// If the item is an app or a bundle it will be an
// instance of folder. If we return true it will
// appear as an enabled item. While the OS will not
// let the user navigate into it, it is better to
// have it appear as disabled.
if (suffix.match(/app|bundle/i) != null)
{
// Do not navigate folders that end in .app or .bundle
return false;
}
// navigate any other folder
return true;
}
// some unknown file type/suffix
return false;
}
var fileType = File.fs == "Windows" ? localize("$$$/AdobePlugin/Shared/Photomerge/Auto/Win=Photomerge Compositions:*.pmg")
: MacPMGType;
photomerge.compositionFile = File.openDialog( localize("$$$/AdobePlugin/Photomerge/LoadComp=Load Photomerge Composition"), fileType );
if (photomerge.compositionFile == null || !photomerge.compositionFile.open("r"))
return;
var line = photomerge.compositionFile.readln();
if (! line.match(/^VIS/))
{
alert( this.pluginName + localize("$$$/AdobePlugin/Photomerge/BadComp=The Composition file is corrupt"), this.pluginName, true );
return;
}
// Read through the composition file and extract the file paths in it.
var mergeFiles = new Array();
while (! photomerge.compositionFile.eof)
{
line = photomerge.compositionFile.readln();
var f = line.match(/^\s*PATH\s+<([^>]+)/);
if (f)
{
// If no file path delimiters, image paths are assumed relative to the composition file
var relPath = (f[1].indexOf( (File.fs == "Windows") ? "\\" : "/" ) < 0);
f = (relPath ? photomerge.compositionFile.path + "/" : "") + f[1];
mergeFiles.push( new StackElement( new File(f) ) );
}
}
if (mergeFiles.length < 2)
{
alert( this.pluginName + localize("$$$/AdobePlugin/Photomerge/BadComp=The Composition file is corrupt"), this.pluginName, true );
return;
}
photomerge.stackElements = mergeFiles;
photomerge.interactiveFlag = true;
this.window.close(kFilesFromPMLoad);
}
// Set up the radio buttons
photomerge.customDialogSetup = function( w )
{
var i, button;
for (i in this.radioButtons)
{
button = w.findControl(this.radioButtons[i]);
button.onClick = this.radioClick;
// Flag with name so we can identify it in radioClick
button.button_id = this.radioButtons[i];
}
// Missing feature: We should query the selected file's metadata and
// automatically turn on the '_useLensCorrection' checkbox if the
// file has the proper support for it.
w.findControl("_loadcomp").onClick = this.loadCompositionClick;
// Julie didn't like the intro line; nuke it here because stackDialog looks for it.
w.findControl("_intro").parent.remove(['_intro']);
var size = w.findControl("_fileList").size;
size[1] += 20;
w.findControl("_LOauto").value = true; // Set default
w.findControl("_advancedBlend").value = this.advancedBlending;
var chxBox = w.findControl("_advancedBlend");
chxBox.onClick = this.checkboxClick;
chxBox.button_id = '_advancedBlend';
// If the PhotomergeUI or ADM plugins aren't there,
// or we're not 32 bit, don't display the option for it.
if ((app.systemInformation.search(/PhotomergeUI/) < 0)
|| (app.systemInformation.search(/ADM /) < 0)
|| ((File.fs == "Macintosh")
&& (app.systemInformation.split('\r')[0].search(/.+Photoshop.*x32/) < 0)))
{
w.findControl("_PMInteractive").hide();
w.findControl("_loadcomp").hide();
}
}
// Called by the dialog on closing to collect the results
photomerge.customDialogFunction = function( w )
{
if (w.findControl("_LOinteractive").value)
{
this.interactiveFlag = true;
this.alignmentKey = 'interactive';
}
else
{
var i, d = [{k:"_LOauto",v:"Auto"},{k:"_LOnormal",v:"translation"},{k:"_LOperspective",v:"Prsp"},{k:"_LOcylindrical",v:"cylindrical"},{k:"_LOspherical",v:"spherical"},{k:"_LOcollage",v:"sceneCollage"}];
for (i in d)
if (w.findControl(d[i].k).value)
{
this.alignmentKey = d[i].v;
break;
}
}
this.advancedBlending = w.findControl("_advancedBlend").value;
this.lensCorrection = w.findControl("_useLensCorrection").value;
this.removeVignette = w.findControl("_removeVignette").value;
this.autoFillTrans = w.findControl("_autoFillTrans").value;
}
// "Main" execution of Photomerge from the menu
photomerge.doInteractivePano = function ()
{
// Because of the ",true", the dialog is pre-loaded with any bridge files.
this.getFilesFromBridgeOrDialog( localize("$$$/private/Photomerge/PMDialogexv=PMDialog.exv"), true );
var saveUnits = null;
// If the ruler units are set to percent, we must change them while Photomerge runs, since
// percent only makes sense within the context of a single document, not across multiple documents
// (it returns NaA in that case).
if (app.preferences.rulerUnits == Units.PERCENT)
{
saveUnits = Units.PERCENT;
app.preferences.rulerUnits = Units.PIXELS;
}
try {
if (this.stackElements && this.doPanorama())
{
fitViewOnScreen();
var flagDesc = new ActionDescriptor();
flagDesc.putBoolean( kPhotomergeAdvancedBlendingFlag, photomerge.advancedBlending );
app.putCustomOptions( "PhotomergeFlags001", flagDesc, true );
}
}
catch (err)
{
if (this.stackDoc)
this.stackDoc.close(SaveOptions.DONOTSAVECHANGES)
}
if (saveUnits)
app.preferences.rulerUnits = saveUnits;
try{
var playbackDescription = new ActionDescriptor;
// add a fake key then remove so that the internal ActionDescriptor
// actually gets made, we don't want an actual key because
// we don't want the turn down arrow in the actions panel
var fakeID = app.charIDToTypeID('xxxx');
playbackDescription.putInteger(fakeID, 22);
playbackDescription.erase(fakeID);
app.playbackDisplayDialogs = DialogModes.ALL;
app.playbackParameters = playbackDescription;
}catch(e) {
; // do nothing
}
}
// Use this version to call Photomerge from a script.
photomerge.createPanorama = function(filelist, displayDialog)
{
this.interactiveFlag = (typeof(displayDialog) == 'boolean') ? displayDialog : false;
if (filelist.length < 2)
{
alert(localize("$$$/AdobePlugin/Shared/Photomerge/AtLeast2=Photomerge needs at least two files selected."), this.pluginName, true );
return;
}
var j;
this.stackElements = new Array();
for (j in filelist)
{
var f = filelist[j];
this.stackElements.push( new StackElement( (typeof(f) == 'string') ? File(f) : f ) );
}
if (this.stackElements.length > 1)
this.doPanorama();
}
if ((typeof(runphotomergeFromScript) == 'undefined')
|| (runphotomergeFromScript==false))
photomerge.doInteractivePano();