6 Replies Latest reply on Sep 8, 2011 6:41 AM by c.pfaffenbichler

    Snap vector shape points to pixels

    BlipAdobe Level 1

      I'm trying to write a JS script in Photoshop CS5 which will snap all the points in a selected vector layer to the nearest whole pixel.  This is extremely useful when producing artwork for web pages, mobile phone apps etc.

       

      I've figured out how to obtain the list of vector points (via doc.pathItems[].subPathItems[].pathPoints[]) and it's easy to quantize these values to the nearest integer.  The bit I can't figure out is how to apply the quantized values back to the document.  I've tried simply writing the value into the obtained pathPoints[] object but this seems to be read only and any changes made are not picked up by the document.

       

      Would anyone have any advice on the last piece of this puzzle?

       

      The scripting guide has example script for creating a new path (rather than modifying an existing one), so another idea would be to construct a brand new (snapped) path and layer based upon a complete copy of the original, and then delete the original layer.  This seems a bit of a long way round though.

        • 1. Re: Snap vector shape points to pixels
          c.pfaffenbichler Level 9
          so another idea would be to construct a brand new (snapped) path and layer based upon a complete copy of the original, and then delete the original layer.  This seems a bit of a long way round though.

          You don’t need to delete the Layer, just its Vector Mask and then apply the newly created Work Path as a Vector Mask.

          As far as I can tell you simply cannot edit an existing Path’s PathPoints individually via Scripting in Photoshop.

          • 2. Re: Snap vector shape points to pixels
            BlipAdobe Level 1

            OK thanks, I was beginning to feel that this was the case.

             

            Would you have any quick pointers on the best way to apply the work path as a vector mask to an existing layer in script?  If not don't worry, I'm sure I'll get there - it's just that from my experience so far I seem to have to try everything in about 4 different ways until I get a solution that both works and is fast!

            • 3. Re: Snap vector shape points to pixels
              c.pfaffenbichler Level 9

              I think this ScriptingListener-code should work:

               

              // =======================================================
              var idMk = charIDToTypeID( "Mk  " );
                  var desc11 = new ActionDescriptor();
                  var idnull = charIDToTypeID( "null" );
                      var ref8 = new ActionReference();
                      var idPath = charIDToTypeID( "Path" );
                      ref8.putClass( idPath );
                  desc11.putReference( idnull, ref8 );
                  var idAt = charIDToTypeID( "At  " );
                      var ref9 = new ActionReference();
                      var idPath = charIDToTypeID( "Path" );
                      var idPath = charIDToTypeID( "Path" );
                      var idvectorMask = stringIDToTypeID( "vectorMask" );
                      ref9.putEnumerated( idPath, idPath, idvectorMask );
                  desc11.putReference( idAt, ref9 );
                  var idUsng = charIDToTypeID( "Usng" );
                      var ref10 = new ActionReference();
                      var idPath = charIDToTypeID( "Path" );
                      var idOrdn = charIDToTypeID( "Ordn" );
                      var idTrgt = charIDToTypeID( "Trgt" );
                      ref10.putEnumerated( idPath, idOrdn, idTrgt );
                  desc11.putReference( idUsng, ref10 );
              executeAction( idMk, desc11, DialogModes.NO );
              

              • 4. Re: Snap vector shape points to pixels
                BlipAdobe Level 1

                Firstly, thanks a million for that code snippet.  I've been off working on something else for a little while so I've only just returned to this problem this morning.

                 

                Here's the final script which hopefully may be of some use to others.  It snaps all points in the selected layer's vector mask to the nearest whole pixel. It works pretty well but as with most things there are a couple of issues/areas for improvement:

                 

                1. It does not support multiple layer selections.  I have some Action Manager script which deals with multiple layer selections but it would make this example messier (I don't understand why Document.activeLayer doesn't simply return an array, or at least there should be a Document.selectedLayers array property).

                 

                2. I found the path for the active layer by checking the doc for paths with a PathKind of VECTORMASK, which works fine.  It's unclear if/when I should be looking for PathKind.WORKINGPATH.  Again, it would make much more sense to me if vector mask paths were simply extra leaves of the PhotoShop DOM rather than having to select a layer and look in the paths palette, but hey!

                 

                 

                #target photoshop
                
                // Constants
                var QUANTIZE_PIXELS = 1;    // The number of whole pixels we wish the path points to be quantized to
                
                app.bringToFront();
                 
                main();
                
                
                // Quantizes all points in the active layer's vector mask to the value specified by QUANTIZE_PIXELS.
                // NOTE : This script does not currently handle multiple layer selections.
                function main()
                {
                    var doc = app.activeDocument;
                    var PathItems = doc.pathItems;
                    
                    // Work in pixels
                    var OrigRulerUnits = app.preferences.rulerUnits;
                    var OrigTypeUnits = app.preferences.typeUnits;
                    app.preferences.rulerUnits = Units.PIXELS;
                    app.preferences.typeUnits = TypeUnits.PIXELS;
                
                    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)
                        {
                            QuantizedPathInfo = BuildPathInfoFromPath(Path, QUANTIZE_PIXELS);
                            Path.remove();
                            break; // for
                        }
                    }
                    
                    // Did we find a path to quantize?
                    if (QuantizedPathInfo == null)
                    {
                        alert("Please select exactly one vector path layer.", "Couldn't get path");
                    }
                    else
                    {
                        var TempQuantizedPath = doc.pathItems.add("TempQuantizedPath", QuantizedPathInfo);
                        
                        // Convert to a vector mask on the current layer
                        pathtoVectorMask();
                
                        // Finished with the path
                        TempQuantizedPath.remove();
                    }
                
                    // Restore original ruler units
                    app.preferences.rulerUnits = OrigRulerUnits;
                    app.preferences.typeUnits = OrigTypeUnits;
                }
                
                
                // 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], zPoint[1] ];
                    else
                        return [ Math.round(zPoint[0] / zQuantizeUnits) * zQuantizeUnits, Math.round(zPoint[1] / zQuantizeUnits) * 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()
                {
                    function cTID(s) { return charIDToTypeID(s); };
                    function sTID(s) { return stringIDToTypeID(s); };
                
                    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 );
                };
                
                
                • 5. Re: Snap vector shape points to pixels
                  BlipAdobe Level 1

                  For the sake of completeness, but at the expense of some readability, here's a version which deals with multiple selected layers.

                   

                  The only final issue I have is that my script adds many operations to the history list.  Does anyone know how I can make my whole script a single atomic history step?

                   

                  #target photoshop
                  
                  // Constants
                  var QUANTIZE_PIXELS = 1;    // The number of whole pixels we wish the path points to be quantized to
                  
                  // Some helpers
                  function cTID(s) { return charIDToTypeID(s); };
                  function sTID(s) { return stringIDToTypeID(s); };
                  
                  
                  app.bringToFront();
                   
                  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], zPoint[1] ];
                      else
                          return [ Math.round(zPoint[0] / zQuantizeUnits) * zQuantizeUnits, Math.round(zPoint[1] / zQuantizeUnits) * 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;
                  }
                  
                  
                  
                  • 6. Re: Snap vector shape points to pixels
                    c.pfaffenbichler Level 9

                    Check out Document.suspendHistory() in the Object Model Viewer.

                    Or search for »suspendHistory« here or at www.ps-scripts.com, you could probably find some examples.