Skip to content

Instantly share code, notes, and snippets.

@yorb
Created April 23, 2013 16:52
Show Gist options
  • Save yorb/5445360 to your computer and use it in GitHub Desktop.
Save yorb/5445360 to your computer and use it in GitHub Desktop.
A Photoshop script to snap all path points on selected shape layers to nearest pixels.
// Based on Adobe Community Forums member BlipAdobe's script:
// http://forums.adobe.com/message/3908198#3908198
#target photoshop
// Constants
var QUANTIZE_PIXELS = 1; // The number of whole pixels we wish the path points to be quantized to
var PIXEL_RATIO = app.activeDocument.resolution / 72; // Standardize pixels and points
// Some helpers
function cTID(s) { return charIDToTypeID(s); };
function sTID(s) { return stringIDToTypeID(s); };
app.bringToFront();
app.activeDocument.suspendHistory("Snap To Pixel", "main();");
// Quantizes all points in the active layers' vector masks to the value specified by QUANTIZE_PIXELS.
function main()
{
// Work in pixels
var OrigRulerUnits = app.preferences.rulerUnits;
var OrigTypeUnits = app.preferences.typeUnits;
app.preferences.rulerUnits = Units.PIXELS;
app.preferences.typeUnits = TypeUnits.PIXELS;
// Obtain the action manager indices of all the selected layers
var SelIndices = GetSelectedLayersIdx();
if (SelIndices.length == 1)
{
// Only a single layer is selected
QuantizeVectorMaskForActiveLayer(QUANTIZE_PIXELS);
}
else
{
// More than one layer is selected
for (var i = 0; i < SelIndices.length; ++i)
{
if (MakeActiveByIndex(SelIndices[i], false) != -1)
{
QuantizeVectorMaskForActiveLayer(QUANTIZE_PIXELS);
}
}
}
// Restore original ruler units
app.preferences.rulerUnits = OrigRulerUnits;
app.preferences.typeUnits = OrigTypeUnits;
}
function QuantizeVectorMaskForActiveLayer(zQuantizeUnits)
{
var doc = app.activeDocument;
var PathItems = doc.pathItems;
// Nothing to do if the active layer has no path
if (PathItems.length == 0)
return;
var QuantizedPathInfo = null;
for (var i = 0; i < PathItems.length; ++i)
{
var Path = PathItems[i];
// It would appear that the path for the selected layer is of kind VECTORMASK. If so, when does WORKPATH come into play?
if (Path.kind == PathKind.VECTORMASK)
{
// Build a path info structure by copying the properties of this path, quantizing all anchor and left/right points
var QuantizedPathInfo = BuildPathInfoFromPath(Path, zQuantizeUnits);
if (QuantizedPathInfo.length > 0)
{
// We've now finished with the original path so it can go
Path.remove();
// Add our new path to the doc
var TempQuantizedPath = doc.pathItems.add("TempQuantizedPath", QuantizedPathInfo);
// Convert the new path to a vector mask on the active layer
PathtoVectorMask();
// Finished with the path
TempQuantizedPath.remove();
}
}
}
}
// Copies the specified path to a new path info structure, which is then returned. The function will
// optionally quantize all points to the specified number of whole units. Specify zQuantizeUnits = 0
// if you do not wish to quantize the points.
function BuildPathInfoFromPath(zInputPath, zQuantizeUnits)
{
// The output path will be an array of SubPathInfo objects
var OutputPathInfo = [];
// For each subpath in the input path
for (var SubPathIndex = 0; SubPathIndex < zInputPath.subPathItems.length; ++SubPathIndex)
{
var InputSubPath = zInputPath.subPathItems[SubPathIndex];
var OutputPointInfo = [];
// For each point in the input subpath
var NumPoints = InputSubPath.pathPoints.length;
for (var PointIndex = 0; PointIndex < NumPoints; ++PointIndex)
{
var InputPoint = InputSubPath.pathPoints[PointIndex];
var InputAnchor = InputPoint.anchor;
var InputAnchorQ = QuantizePoint(InputAnchor, zQuantizeUnits);
// Copy all the input point's properties to the output point info
OutputPointInfo[PointIndex] = new PathPointInfo();
OutputPointInfo[PointIndex].kind = InputPoint.kind;
OutputPointInfo[PointIndex].anchor = QuantizePoint(InputPoint.anchor, zQuantizeUnits);
OutputPointInfo[PointIndex].leftDirection = QuantizePoint(InputPoint.leftDirection, zQuantizeUnits);
OutputPointInfo[PointIndex].rightDirection = QuantizePoint(InputPoint.rightDirection, zQuantizeUnits);
}
// Create the SubPathInfo for our output path, and copy properties from the input sub path
OutputPathInfo[SubPathIndex] = new SubPathInfo();
OutputPathInfo[SubPathIndex].closed = InputSubPath.closed;
OutputPathInfo[SubPathIndex].operation = InputSubPath.operation;
OutputPathInfo[SubPathIndex].entireSubPath = OutputPointInfo;
}
return OutputPathInfo;
}
// Quantizes the specified point to the specified number of whole units
function QuantizePoint(zPoint, zQuantizeUnits)
{
// Check for divide by zero (if zQuantizeUnits == 0 we don't quantize)
if (zQuantizeUnits == 0)
return [ zPoint[0]/PIXEL_RATIO, zPoint[1]/PIXEL_RATIO ];
else
return [ Math.round(zPoint[0] / zQuantizeUnits)/PIXEL_RATIO * zQuantizeUnits, Math.round(zPoint[1] / zQuantizeUnits)/PIXEL_RATIO * zQuantizeUnits ];
}
// Converts the current working path to a vector mask on the active layer. This function will fail
// if the active layer already has a vector mask, so you should remove it beforehand.
function PathtoVectorMask()
{
var desc11 = new ActionDescriptor();
var ref8 = new ActionReference();
ref8.putClass( cTID("Path") );
desc11.putReference( cTID("null"), ref8);
var ref9 = new ActionReference();
ref9.putEnumerated(cTID("Path"), cTID("Path"), sTID("vectorMask"));
desc11.putReference(cTID("At "), ref9);
var ref10 = new ActionReference();
ref10.putEnumerated(cTID("Path"), cTID("Ordn"), cTID("Trgt"));
desc11.putReference(cTID("Usng"), ref10);
executeAction(cTID("Mk "), desc11, DialogModes.NO );
}
// Make a layer active by its action manager index
function MakeActiveByIndex(zIndex, zVisible)
{
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putIndex(cTID("Lyr "), zIndex)
desc.putReference(cTID( "null"), ref );
desc.putBoolean(cTID( "MkVs"), zVisible );
executeAction(cTID( "slct"), desc, DialogModes.NO );
}
// Returns the AM index of the selected layers
function GetSelectedLayersIdx()
{
var selectedLayers = new Array;
var ref = new ActionReference();
ref.putEnumerated(cTID('Dcmn'), cTID('Ordn'), cTID('Trgt') );
var desc = executeActionGet(ref);
if (desc.hasKey(sTID('targetLayers')))
{
desc = desc.getList(sTID('targetLayers'));
var c = desc.count
var selectedLayers = new Array();
for(var i = 0; i < c; ++i)
{
selectedLayers.push(desc.getReference(i).getIndex());
}
}
else
{
var ref = new ActionReference();
ref.putProperty(cTID('Prpr'), cTID('ItmI'));
ref.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt'));
selectedLayers.push(executeActionGet(ref).getInteger(cTID('ItmI')));
}
return selectedLayers;
}
@skibulk
Copy link

skibulk commented Apr 19, 2016

When multiple layers are selected, it would make sense to re-select the same layers before exiting the script.

@skibulk
Copy link

skibulk commented Apr 19, 2016

I'm not sure why this keeps shifting the selection by one index - but here's the general idea:

    // Select layers by their action manager indices
    function SelectByIndices(indices)
    {
        var ref = new ActionReference();
        var c = indices.length;
        for(var i = 0; i < c; ++i)
        {
            ref.putIdentifier(cTID('Lyr '), indices[i]);
        }
        var desc = new ActionDescriptor();
        desc.putReference(cTID('null'), ref);
        desc.putBoolean(cTID('MkVs'), false);
        executeAction(cTID('slct'), desc, DialogModes.NO);
   }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment