Created
January 5, 2017 12:21
-
-
Save silviu-bucsa/94844b3ffefaa09018554443b43a4cc4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2012 Adobe Systems Incorporated. All Rights reserved. | |
// IMPORTANT: This file MUST be written out from ESTK with the option to write the UTF-8 | |
// signature turned ON (Edit > Preferences > Documents > UTF-8 Signature). Otherwise, | |
// the script fails when run from Photoshop with "JavaScript code was missing" on | |
// non-English Windows systems. | |
// | |
// Extract CSS from the current layer selection and copy it to the clipboard. | |
// | |
/* | |
@@@BUILDINFO@@@ CopyCSSToClipboard.jsx 1.0.0.0 | |
*/ | |
$.localize = true; | |
// Constants for accessing PS event functionality. In the interests of speed | |
// we're defining just the ones used here, rather than sucking in a general defs file. | |
const classApplication = app.charIDToTypeID('capp'); | |
const classDocument = charIDToTypeID('Dcmn'); | |
const classLayer = app.charIDToTypeID('Lyr '); | |
const classLayerEffects = app.charIDToTypeID('Lefx'); | |
const classProperty = app.charIDToTypeID('Prpr'); | |
const enumTarget = app.charIDToTypeID('Trgt'); | |
const eventGet = app.charIDToTypeID('getd'); | |
const eventHide = app.charIDToTypeID('Hd '); | |
const eventSelect = app.charIDToTypeID('slct'); | |
const eventShow = app.charIDToTypeID('Shw '); | |
const keyItemIndex = app.charIDToTypeID ('ItmI'); | |
const keyLayerID = app.charIDToTypeID('LyrI'); | |
const keyTarget = app.charIDToTypeID('null'); | |
const keyTextData = app.charIDToTypeID('TxtD'); | |
const typeNULL = app.charIDToTypeID('null'); | |
const typeOrdinal = app.charIDToTypeID('Ordn'); | |
const ktextToClipboardStr = app.stringIDToTypeID( "textToClipboard" ); | |
const unitAngle = app.charIDToTypeID('#Ang'); | |
const unitDensity = app.charIDToTypeID('#Rsl'); | |
const unitDistance = app.charIDToTypeID('#Rlt'); | |
const unitNone = app.charIDToTypeID('#Nne'); | |
const unitPercent = app.charIDToTypeID('#Prc'); | |
const unitPixels = app.charIDToTypeID('#Pxl'); | |
const unitMillimeters= app.charIDToTypeID('#Mlm'); | |
const unitPoints = app.charIDToTypeID('#Pnt'); | |
const enumRulerCm = app.charIDToTypeID('RrCm'); | |
const enumRulerInches = app.charIDToTypeID('RrIn'); | |
const enumRulerPercent = app.charIDToTypeID('RrPr'); | |
const enumRulerPicas = app.charIDToTypeID('RrPi'); | |
const enumRulerPixels = app.charIDToTypeID('RrPx'); | |
const enumRulerPoints = app.charIDToTypeID('RrPt'); | |
// SheetKind definitions from USheet.h | |
const kAnySheet = 0; | |
const kPixelSheet = 1; | |
const kAdjustmentSheet = 2; | |
const kTextSheet = 3; | |
const kVectorSheet = 4; | |
const kSmartObjectSheet = 5; | |
const kVideoSheet = 6; | |
const kLayerGroupSheet = 7; | |
const k3DSheet = 8; | |
const kGradientSheet = 9; | |
const kPatternSheet = 10; | |
const kSolidColorSheet = 11; | |
const kBackgroundSheet = 12; | |
const kHiddenSectionBounder = 13; | |
// Tables to convert Photoshop UnitTypes into CSS types | |
var unitIDToCSS = {}; | |
unitIDToCSS[unitAngle] = "deg"; | |
unitIDToCSS[unitDensity] = "DEN "; // Not supported in CSS | |
unitIDToCSS[unitDistance] = "DIST"; // Not supported in CSS | |
unitIDToCSS[unitNone] = ""; // Not supported in CSS | |
unitIDToCSS[unitPercent] = "%"; | |
unitIDToCSS[unitPixels] = "px"; | |
unitIDToCSS[unitMillimeters] = "mm"; | |
unitIDToCSS[unitPoints] = "pt"; | |
unitIDToCSS[enumRulerCm] = "cm"; | |
unitIDToCSS[enumRulerInches] = "in"; | |
unitIDToCSS[enumRulerPercent] = "%"; | |
unitIDToCSS[enumRulerPicas] = "pc"; | |
unitIDToCSS[enumRulerPixels] = "px"; | |
unitIDToCSS[enumRulerPoints] = "pt"; | |
// Pixel units in Photoshop are hardwired to 72 DPI (points), | |
// regardless of the doc resolution. | |
var unitIDToPt = {}; | |
unitIDToPt[unitPixels] = 1; | |
unitIDToPt[enumRulerPixels] = 1; | |
unitIDToPt[Units.PIXELS] = 1; | |
unitIDToPt[unitPoints] = 1; | |
unitIDToPt[enumRulerPoints] = 1; | |
unitIDToPt[Units.POINTS] = 1; | |
unitIDToPt[unitMillimeters] = UnitValue(1, "mm").as('pt'); | |
unitIDToPt[Units.MM] = UnitValue(1, "mm").as('pt'); | |
unitIDToPt[enumRulerCm] = UnitValue(1, "cm").as('pt'); | |
unitIDToPt[Units.CM] = UnitValue(1, "cm").as('pt'); | |
unitIDToPt[enumRulerInches] = UnitValue(1, "in").as('pt'); | |
unitIDToPt[Units.INCHES] = UnitValue(1, "in").as('pt'); | |
unitIDToPt[stringIDToTypeID("inchesUnit")] = UnitValue(1, "in").as('pt'); | |
unitIDToPt[enumRulerPicas] = UnitValue(1, "pc").as('pt'); | |
unitIDToPt[Units.PICAS] = UnitValue(1, "pc").as('pt'); | |
unitIDToPt[unitDistance] = 1; | |
unitIDToPt[unitDensity] = 1; | |
// Fortunately, both CSS and the DOM unit values use the same | |
// unit abbreviations. | |
var DOMunitToCSS = {}; | |
DOMunitToCSS[Units.CM] = "cm"; | |
DOMunitToCSS[Units.INCHES] = "in"; | |
DOMunitToCSS[Units.MM] = "mm"; | |
DOMunitToCSS[Units.PERCENT] = "%"; | |
DOMunitToCSS[Units.PICAS] = "pc"; | |
DOMunitToCSS[Units.PIXELS] = "px"; | |
DOMunitToCSS[Units.POINTS] = "pt"; | |
DOMunitToCSS[TypeUnits.MM] = "mm"; | |
DOMunitToCSS[TypeUnits.PIXELS] = "px"; | |
DOMunitToCSS[TypeUnits.POINTS] = "pt"; | |
// A sample object descriptor path looks like: | |
// AGMStrokeStyleInfo.strokeStyleContent.'Clr '.'Rd ' | |
// This converts either OSType or string IDs. | |
makeID = function( keyStr ) | |
{ | |
if (keyStr[0] == "'") // Keys with single quotes 'ABCD' are charIDs. | |
return app.charIDToTypeID( eval(keyStr) ); | |
else | |
return app.stringIDToTypeID( keyStr ); | |
} | |
// Clean up some pretty noisy FP numbers... | |
function round1k( x ) { return Math.round( x * 1000 ) / 1000; } | |
// Strip off the unit string and return UnitValue as an actual number | |
function stripUnits( x ) { return Number( x.replace(/[^0-9.-]+/g, "") ); } | |
// Convert a "3.0pt" style string or number to a DOM UnitValue | |
function makeUnitVal( v ) | |
{ | |
if (typeof v == "string") | |
return UnitValue( stripUnits( v ), v.replace(/[0-9.-]+/g, "" ) ); | |
if (typeof v == "number") | |
return UnitValue( v, DOMunitToCSS[app.preferences.rulerUnits] ); | |
} | |
// Convert a pixel measurement into a UnitValue in rulerUnits | |
function pixelsToAppUnits( v ) | |
{ | |
if (app.preferences.rulerUnits == Units.PIXELS) | |
return UnitValue( v, "px" ); | |
else | |
{ | |
// Divide by doc's DPI, convert to inch, then convert to ruler units. | |
var appUnits = DOMunitToCSS[app.preferences.rulerUnits]; | |
return UnitValue( (UnitValue( v / app.activeDocument.resolution, "in" )).as(appUnits), appUnits ); | |
} | |
} | |
// Format a DOM UnitValue as a CSS string, using the rulerUnits units. | |
UnitValue.prototype.asCSS = function() | |
{ | |
var cssUnits = DOMunitToCSS[app.preferences.rulerUnits]; | |
return round1k( this.as(cssUnits) ) + cssUnits; | |
} | |
// Return the absolute value of a UnitValue as a UnitValue | |
UnitValue.prototype.abs = function() | |
{ | |
return UnitValue( Math.abs( this.value ), this.type ); | |
} | |
// It turns out no matter what your PS units pref is set to, the DOM/PSEvent | |
// system happily hands you values in whatever whacky units it feels like. | |
// This normalizes the unit output to the ruler setting, for consistency in CSS. | |
// Note: This isn't a method because "desc" can either be an ActionDescriptor | |
// or an ActionList (in which case the "ID" is the index). | |
function getPSUnitValue( desc, ID ) | |
{ | |
var srcUnitsID = desc.getUnitDoubleType( ID ); | |
if (srcUnitsID == unitNone) // Um, unitless unitvalues are just...numbers. | |
return round1k( desc.getUnitDoubleValue( ID )); | |
// Angles and percentages are typically things like gradient parameters, | |
// and should be left as-is. | |
if ((srcUnitsID == unitAngle) || (srcUnitsID == unitPercent)) | |
return round1k(desc.getUnitDoubleValue( ID )) + unitIDToCSS[srcUnitsID]; | |
// Skip conversion if coming and going in pixels | |
if (((srcUnitsID == unitPixels) || (srcUnitsID == enumRulerPixels)) | |
&& (app.preferences.rulerUnits == Units.PIXELS)) | |
return round1k(desc.getUnitDoubleValue( ID )) + "px"; | |
// Other units to pixels must first convert to points, | |
// then expanded by the actual doc resolution (measured in DPI) | |
if (app.preferences.rulerUnits == Units.PIXELS) | |
return round1k( desc.getUnitDoubleValue( ID ) * unitIDToPt[srcUnitsID] | |
* app.activeDocument.resolution / 72 ) + "px"; | |
var DOMunitStr = DOMunitToCSS[app.preferences.rulerUnits]; | |
// Pixels must be explictly converted to other units | |
if ((srcUnitsID == unitPixels) || (srcUnitsID == enumRulerPixels)) | |
return pixelsToAppUnits( desc.getUnitDoubleValue( ID ) ).as(DOMunitStr) + DOMunitStr; | |
// Otherwise, let Photoshop do generic conversion. | |
return round1k( UnitValue( desc.getUnitDoubleValue( ID ), | |
unitIDToCSS[srcUnitsID] | |
).as( DOMunitStr ) ) + DOMunitStr; | |
} | |
// Attempt decoding of reference types. This generates an object with two keys, | |
// "refclass" and "value". So a channel reference looks like: | |
// { refclass:'channel', value: 1 } | |
// Note the dump method compresses this to the text "{ channel: 1 }", but internally | |
// the form above is used. This is because ExtendScript doesn't have a good method | |
// for enumerating keys. | |
function getReference( ref ) | |
{ | |
var v; | |
switch (ref.getForm()) | |
{ | |
case ReferenceFormType.CLASSTYPE: v = typeIDToStringID( ref.getDesiredClass() ); break; | |
case ReferenceFormType.ENUMERATED: v = ref.getEnumeratedValue(); break; | |
case ReferenceFormType.IDENTIFIER: v = ref.getIdentifier(); break; | |
case ReferenceFormType.INDEX: v = ref.getIndex(); break; | |
case ReferenceFormType.NAME: v =ref.getName(); break; | |
case ReferenceFormType.OFFSET: v = ref.getOffset(); break; | |
case ReferenceFormType.PROPERTY: v = ref.getProperty(); break; | |
default: v = null; | |
} | |
return { refclass: typeIDToStringID( ref.getDesiredClass() ), value: v }; | |
} | |
// For non-recursive types, return the value. Note unit types are | |
// returned as strings with the unit suffix, if you want just the number | |
// you'll need to strip off the type and convert it to Number() | |
// Note: This isn't a method because "desc" can either be an ActionDescriptor | |
// or an ActionList (in which case the "ID" is the index). | |
function getFlatType( desc, ID ) | |
{ | |
switch (desc.getType( ID )) | |
{ | |
case DescValueType.BOOLEANTYPE: return desc.getBoolean( ID ); | |
case DescValueType.STRINGTYPE: return desc.getString( ID ); | |
case DescValueType.INTEGERTYPE: return desc.getInteger( ID ); | |
case DescValueType.DOUBLETYPE: return desc.getDouble( ID ); | |
case DescValueType.UNITDOUBLE: return getPSUnitValue( desc, ID ); | |
case DescValueType.ENUMERATEDTYPE: return typeIDToStringID( desc.getEnumerationValue(ID) ); | |
case DescValueType.REFERENCETYPE: return getReference( desc.getReference( ID ) ); | |
case DescValueType.RAWTYPE: return desc.getData( ID ); | |
case DescValueType.ALIASTYPE: return desc.getPath( ID ); | |
case DescValueType.CLASSTYPE: return typeIDToStringID( desc.getClass( ID ) ); | |
default: return desc.getType(ID).toString(); | |
} | |
} | |
//////////////////////////////////// ActionDescriptor ////////////////////////////////////// | |
ActionDescriptor.prototype.getFlatType = function( ID ) | |
{ | |
return getFlatType( this, ID ); | |
} | |
ActionList.prototype.getFlatType = function( index ) | |
{ | |
// Share the ActionDesciptor code via duck typing | |
return getFlatType( this, index ); | |
} | |
// Traverse the object described the string in the current layer. | |
// Objects take the form of the nested descriptor IDs (the code above figures out the types on the fly). | |
// So | |
// AGMStrokeStyleInfo.strokeStyleContent.'Clr '.'Rd ' | |
// translates to doing a eventGet of stringIDToTypeID("AGMStrokeStyleInfo") on the current layer, | |
// then doing: | |
// desc.getObject(s2ID("AGMStrokeStyleInfo")) | |
// .getObject(s2ID("strokeStyleContent)).getObject(c2ID('Clr ')).getDouble('Rd '); | |
// | |
ActionDescriptor.prototype.getVal = function( keyList, firstListItemOnly ) | |
{ | |
if (typeof(keyList) == 'string') // Make keyList an array if not already | |
keyList = keyList.split('.'); | |
if (typeof( firstListItemOnly ) == "undefined") | |
firstListItemOnly = true; | |
// If there are no more keys to traverse, just return this object. | |
if (keyList.length == 0) | |
return this; | |
keyStr = keyList.shift(); | |
keyID = makeID(keyStr); | |
if (this.hasKey( keyID)) | |
switch (this.getType( keyID )) | |
{ | |
case DescValueType.OBJECTTYPE: | |
return this.getObjectValue( keyID ).getVal( keyList, firstListItemOnly ); | |
case DescValueType.LISTTYPE: | |
var xx = this.getList( keyID ); // THIS IS CREEPY - original code below fails in random places on the same document. | |
return /*this.getList( keyID )*/xx.getVal( keyList, firstListItemOnly ); | |
default: return this.getFlatType( keyID ); | |
} | |
else | |
return null; | |
} | |
// Traverse the actionList using the keyList (see below) | |
ActionList.prototype.getVal = function( keyList, firstListItemOnly ) | |
{ | |
if (typeof(keyList) == 'string') // Make keyList an array if not already | |
keyList = keyList.split('.'); | |
if (typeof( firstListItemOnly ) == "undefined") | |
firstListItemOnly = true; | |
// Instead of ID, pass list item #. Duck typing. | |
if (firstListItemOnly) | |
switch (this.getType( 0 )) | |
{ | |
case DescValueType.OBJECTTYPE: | |
return this.getObjectValue( 0 ).getVal( keyList, firstListItemOnly ); | |
case DescValueType.LISTTYPE: | |
return this.getList( 0 ).getVal( keyList, firstListItemOnly ); | |
default: return this.getFlatType( 0 ); | |
} | |
else | |
{ | |
var i, result = []; | |
for (i = 0; i < this.count; ++i) | |
switch (this.getType(i)) | |
{ | |
case DescValueType.OBJECTTYPE: | |
result.push( this.getObjectValue( i ).getVal( keyList, firstListItemOnly )); | |
break; | |
case DescValueType.LISTTYPE: | |
result.push( this.getList( i ).getVal( keyList, firstListItemOnly )); | |
break; | |
default: | |
result.push( this.getFlatType( i ) ); | |
} | |
return result; | |
} | |
} | |
ActionDescriptor.prototype.extractBounds = function() | |
{ | |
function getbnd(desc, key) { return makeUnitVal( desc.getVal( key ) ); } | |
return [getbnd(this,"left"), getbnd(this,"top"), getbnd(this,"right"), getbnd(this,"bottom")]; | |
} | |
ActionDescriptor.dumpValue = function( flatValue ) | |
{ | |
if ((typeof flatValue == "object") && (typeof flatValue.refclass == "string")) | |
return "{ " + flatValue.refclass + ": " + flatValue.value + " }"; | |
else | |
return flatValue; | |
} | |
// Debugging - recursively walk a descriptor and dump out all of the keys | |
// Note we only dump stringIDs. If you look in UActions.cpp:CInitialStringToIDEntry, | |
// there is a table converting most (all?) charIDs into stringIDs. | |
ActionDescriptor.prototype.dumpDesc = function( keyName ) | |
{ | |
var i; | |
if (typeof( keyName ) == "undefined") | |
keyName = ""; | |
for (i = 0; i < this.count; ++i) | |
{ | |
var key = this.getKey(i); | |
var ref; | |
var thisKey = keyName + "." + app.typeIDToStringID( key ) ; | |
switch (this.getType( key )) | |
{ | |
case DescValueType.OBJECTTYPE: | |
this.getObjectValue( key ).dumpDesc( thisKey ); | |
break; | |
case DescValueType.LISTTYPE: | |
this.getList( key ).dumpDesc( thisKey ); | |
break; | |
case DescValueType.REFERENCETYPE: | |
ref = this.getFlatType( key ); | |
$.writeln( thisKey + ":ref:" + ref.refclass + ":" + ref.value ); | |
break; | |
default: | |
$.writeln( thisKey | |
+ ": " + ActionDescriptor.dumpValue( this.getFlatType( key ) ) ); | |
} | |
} | |
} | |
ActionList.prototype.dumpDesc = function( keyName ) | |
{ | |
var i; | |
if (typeof( keyName ) == "undefined") | |
keyName = ""; | |
if (this.count == 0) | |
$.writeln( keyName + " <empty list>" ); | |
else | |
for (i = 0; i < this.count; ++i) | |
{ | |
try { | |
if (this.getType(i) == DescValueType.OBJECTTYPE) | |
this.getObjectValue(i).dumpDesc( keyName + "[" + i + "]" ); | |
else | |
if (this.getType(i) == DescValueType.LISTTYPE) | |
this.getList(i).dumpDesc( keyName + "[" + i + "]" ); | |
else | |
$.writeln( keyName + "[" + i + "]:" | |
+ ActionDescriptor.dumpValue( this.getFlatType( i ) ) ); | |
} | |
catch (err) | |
{ | |
$.writeln("Error "+keyName+"["+i+"]: " + err.message); | |
} | |
} | |
} | |
//////////////////////////////////// ProgressBar ////////////////////////////////////// | |
// "ProgressBar" provides an abstracted interface to the progress bar DOM. It keeps | |
// track of the total steps and number of steps completed so task steps can simply call | |
// nextProgress(). | |
function ProgressBar() | |
{ | |
this.totalProgressSteps = 0; | |
this.currentProgress = 0; | |
} | |
// You must set cssToClip.totalProgressSteps to the total number of | |
// steps to complete before calling this or nextProgress(). | |
// Returns true if aborted. | |
ProgressBar.prototype.updateProgress = function( done ) | |
{ | |
if (this.totalProgressSteps == 0) | |
return false; | |
return !app.updateProgress( done, this.totalProgressSteps ); | |
} | |
// Returns true if aborted. | |
ProgressBar.prototype.nextProgress = function() | |
{ | |
this.currentProgress++; | |
return this.updateProgress( this.currentProgress ); | |
} | |
//////////////////////////////////// PSLayer ////////////////////////////////////// | |
// The overhead for using Photoshop DOM layers is high, and can be | |
// really high if you need to switch the active layer. This class provides | |
// a cache and accessor functions for layers bypassing the DOM. | |
function PSLayerInfo( layerIndex, isBG ) | |
{ | |
this.index = layerIndex; | |
this.boundsCache = null; | |
this.descCache = {}; | |
if (isBG) | |
{ | |
this.layerID = "BG"; | |
this.layerKind = kBackgroundSheet; | |
} | |
else | |
{ | |
// See TLayerElement::Make() to learn how layers are located by PS events. | |
var ref = new ActionReference(); | |
ref.putProperty( classProperty, keyLayerID ); | |
ref.putIndex( classLayer, layerIndex ); | |
this.layerID = executeActionGet( ref ).getVal("layerID"); | |
this.layerKind = this.getLayerAttr("layerKind"); | |
this.visible = this.getLayerAttr("visible"); | |
} | |
} | |
PSLayerInfo.layerIDToIndex = function( layerID ) | |
{ | |
var ref = new ActionReference(); | |
ref.putProperty( classProperty, keyItemIndex ); | |
ref.putIdentifier( classLayer, layerID ); | |
return executeActionGet( ref ).getVal("itemIndex"); | |
} | |
PSLayerInfo.prototype.makeLayerActive = function() | |
{ | |
var desc = new ActionDescriptor(); | |
var ref = new ActionReference(); | |
ref.putIdentifier( classLayer, this.layerID ); | |
desc.putReference( typeNULL, ref ); | |
executeAction( eventSelect, desc, DialogModes.NO ); | |
} | |
PSLayerInfo.prototype.getLayerAttr = function( keyString, layerDesc ) | |
{ | |
var layerDesc; | |
var keyList = keyString.split('.'); | |
if ((typeof(layerDesc) == "undefined") || (layerDesc == null)) | |
{ | |
// Cache the IDs, because some (e.g., Text) take a while to get. | |
if (typeof this.descCache[keyList[0]] == "undefined") | |
{ | |
var ref = new ActionReference(); | |
ref.putProperty( classProperty, makeID(keyList[0])); | |
ref.putIndex( classLayer, this.index ); | |
layerDesc = executeActionGet( ref ); | |
this.descCache[keyList[0]] = layerDesc; | |
} | |
else | |
layerDesc = this.descCache[keyList[0]]; | |
} | |
return layerDesc.getVal( keyList ); | |
} | |
PSLayerInfo.prototype.getBounds = function( ignoreEffects ) | |
{ | |
var boundsDesc; | |
if (typeof ignoreEffects == "undefined") | |
ignoreEffects = false; | |
if (ignoreEffects) | |
boundsDesc = this.getLayerAttr("boundsNoEffects"); | |
else | |
{ | |
if (this.boundsCache) | |
return this.boundsCache; | |
boundsDesc = this.getLayerAttr("bounds"); | |
} | |
if (this.getLayerAttr("artboardEnabled")) | |
boundsDesc = this.getLayerAttr("artboard.artboardRect"); | |
var bounds = boundsDesc.extractBounds(); | |
if (! ignoreEffects) | |
this.boundsCache = bounds; | |
return bounds; | |
} | |
// Get a list of descriptors. Returns NULL if one of them is unavailable. | |
PSLayerInfo.prototype.getLayerAttrList = function( keyString ) | |
{ | |
var i, keyList = keyString.split('.'); | |
var descList = []; | |
// First item from the layer | |
var desc = this.getLayerAttr( keyList[0] ); | |
if (! desc) | |
return null; | |
descList.push( desc ); | |
if (keyList.length == 1) | |
return descList; | |
for (i = 1; i < keyList.length; ++i) | |
{ | |
desc = descList[i-1].getVal( keyList[i] ); | |
if (desc == null) return null; | |
descList.push( desc ); | |
} | |
return descList; | |
} | |
PSLayerInfo.prototype.descToColorList = function( colorDesc, colorPath ) | |
{ | |
function roundColor( x ) { x = Math.round(x); return (x > 255) ? 255 : x; } | |
var i, rgb = ["'Rd '", "'Grn '","'Bl '"]; // Note double quotes around single quotes | |
var rgbTxt = []; | |
// See if the color is really there | |
colorDesc = this.getLayerAttr( colorPath, colorDesc ); | |
if (! colorDesc) | |
return null; | |
for (i in rgb) | |
rgbTxt.push( roundColor(colorDesc.getVal( rgb[i] )) ); | |
return rgbTxt; | |
} | |
// If the desc has a 'Clr ' object, create CSS "rgb( rrr, ggg, bbb )" output from it. | |
PSLayerInfo.prototype.descToCSSColor = function( colorDesc, colorPath ) | |
{ | |
var rgbTxt = this.descToColorList( colorDesc, colorPath ); | |
if (! rgbTxt) | |
return null; | |
return "rgb(" + rgbTxt.join(", ") + ")"; | |
} | |
PSLayerInfo.prototype.descToRGBAColor = function( colorPath, opacity, colorDesc ) | |
{ | |
var rgbTxt = this.descToColorList( colorDesc, colorPath ); | |
rgbTxt = rgbTxt ? rgbTxt : ["0","0","0"]; | |
if (! ((opacity > 0.0) && (opacity < 1.0))) | |
opacity = opacity / 255.0; | |
if (opacity == 1.0) | |
return "rgb(" + rgbTxt.join( ", ") + ")"; | |
else | |
return "rgba(" + rgbTxt.join( ", ") + ", " + round1k( opacity ) + ")"; | |
} | |
function DropShadowInfo( xoff, yoff, dsDesc ) | |
{ | |
this.xoff = xoff; | |
this.yoff = yoff; | |
this.dsDesc = dsDesc; | |
} | |
PSLayerInfo.getEffectOffset = function( fxDesc ) | |
{ | |
var xoff, yoff, angle; | |
// Assumes degrees, PS users aren't into radians. | |
if (fxDesc.getVal( "useGlobalAngle" )) | |
angle = stripUnits( cssToClip.getAppAttr( "globalAngle.globalLightingAngle" ) ) * (Math.PI/180.0); | |
else | |
angle = stripUnits( fxDesc.getVal( "localLightingAngle" ) ) * (Math.PI/180.0); | |
// Photoshop describes the drop shadow in polar coordinates, while CSS uses cartesian coords. | |
var distance = fxDesc.getVal( "distance" ); | |
var distUnits = distance.replace( /[\d.]+/g, "" ); | |
distance = stripUnits( distance ); | |
return [round1k(-Math.cos(angle) * distance) + distUnits, | |
round1k( Math.sin(angle) * distance) + distUnits]; | |
} | |
// New lfx: dropShadowMulti, frameFXMulti, gradientFillMulti, innerShadowMulti, solidFillMulti, | |
PSLayerInfo.prototype.getDropShadowInfo = function( shadowType, boundsInfo, psEffect ) | |
{ | |
psEffect = (typeof psEffect == "undefined") ? "dropShadow" : psEffect; | |
var lfxDesc = this.getLayerAttr( "layerEffects"); | |
var dsDesc = lfxDesc ? lfxDesc.getVal( psEffect ) : null; | |
var lfxOn = this.getLayerAttr( "layerFXVisible" ); | |
// Gather the effect and effectMulti descriptors into a single list. | |
// It will be one or the other | |
var dsDescList = null; | |
if (lfxDesc) | |
dsDescList = dsDesc ? [dsDesc] : lfxDesc.getVal(psEffect + "Multi", false); | |
// If any of the other (non-drop-shadow) layer effects are on, then | |
// flag this so we use the proper bounds calculation. | |
if ((typeof shadowType != "undefined") && (typeof boundsInfo != "undefined") | |
&& (shadowType == "box-shadow") && lfxDesc && lfxOn && !dsDescList) | |
{ | |
var i, fxList = ["dropShadow", "innerShadow", "outerGlow", "innerGlow", | |
"bevelEmboss", "chromeFX", "solidFill", "gradientFill"]; | |
for (i in fxList) | |
if (lfxDesc.getVal( fxList[i] + ".enabled")) | |
{ | |
boundsInfo.hasLayerEffect = true; | |
break; | |
} | |
// Search multis as well | |
if (! boundsInfo.hasLayerEffect) | |
{ | |
var fxMultiList = ["dropShadowMulti", "frameFXMulti", "gradientFillMulti", | |
"innerShadowMulti", "solidFillMulti"]; | |
for (i in fxMultiList) | |
{ | |
var j, fxs = lfxDesc.getVal( fxMultiList[i] ); | |
for (j = 0; j < fxs.length; ++j) | |
if (fxs[j].getVal("enabled")) | |
{ | |
boundsInfo.hasLayerEffect = true; | |
break; | |
} | |
if (boundsInfo.hasLayerEffect) break; | |
} | |
} | |
} | |
// Bail out if effect turned off (no eyeball) | |
if (! dsDescList || !lfxOn) | |
return null; | |
var i, dropShadows = []; | |
for (i = 0; i < dsDescList.length; ++i) | |
if (dsDescList[i].getVal("enabled")) | |
{ | |
var offset = PSLayerInfo.getEffectOffset( dsDescList[i] ); | |
dropShadows.push( new DropShadowInfo( offset[0], offset[1], dsDescList[i] ) ); | |
} | |
return (dropShadows.length > 0) ? dropShadows : null; | |
} | |
// | |
// Return text with substituted descriptors. Note items delimited | |
// in $'s are substituted with values looked up from the layer data | |
// e.g.: | |
// border-width: $AGMStrokeStyleInfo.strokeStyleLineWidth$;" | |
// puts the stroke width into the output. If the descriptor isn't | |
// found, no output is generated. | |
// | |
PSLayerInfo.prototype.replaceDescKey = function( cssText, baseDesc ) | |
{ | |
// Locate any $parameters$ to be substituted. | |
var i, subs = cssText.match(/[$]([^$]+)[$]/g); | |
var replacementFailed = false; | |
function testAndReplace( item ) | |
{ | |
if (item != null) | |
cssText = cssText.replace(/[$]([^$]+)[$]/, item ); | |
else | |
replacementFailed = true; | |
} | |
if (subs) | |
{ | |
// Stupid JS regex leaves whole match in capture group! | |
for (i = 0; i < subs.length; ++i) | |
subs[i] = subs[i].split("$")[1]; | |
if (typeof(baseDesc) == "undefined") | |
baseDesc = null; | |
if (! subs) | |
alert('Missing substitution text in CSS/SVG spec'); | |
for (i = 0; i < subs.length; ++i) | |
{ | |
// Handle color as a special case | |
if (subs[i].match(/'Clr '/)) | |
testAndReplace( this.descToCSSColor( baseDesc, subs[i] ) ); | |
else if (subs[i].match(/(^|[.])color$/)) | |
testAndReplace( this.descToCSSColor( baseDesc, subs[i] ) ); | |
else | |
testAndReplace( this.getLayerAttr( subs[i], baseDesc ) ); | |
} | |
} | |
return [replacementFailed, cssText]; | |
} | |
// If useLayerFX is false, then don't check it. By default it's checked. | |
PSLayerInfo.prototype.gradientDesc = function( useLayerFX ) | |
{ | |
if (typeof useLayerFX == "undefined") | |
useLayerFX = true; | |
var descList = this.getLayerAttr( "adjustment" ); | |
if (descList && descList.getVal("gradient")) | |
{ | |
return descList; | |
} | |
else // If there's no adjustment layer, see if we have one from layerFX... | |
{ | |
if (useLayerFX) | |
descList = this.getLayerAttr( "layerEffects.gradientFill" ); | |
} | |
return descList; | |
} | |
function GradientInfo( gradDesc ) | |
{ | |
this.angle = gradDesc.getVal("angle"); | |
this.opacity = gradDesc.getVal("opacity"); | |
this.opacity = this.opacity ? stripUnits(this.opacity)/100.0 : 1; | |
if (this.angle == null) | |
this.angle = "0deg"; | |
this.type = gradDesc.getVal("type"); | |
// Get rid of the new "gradientType:" prefix | |
this.type = this.type.replace(/^gradientType:/,""); | |
if ((this.type != "linear") && (this.type != "radial")) | |
this.type = "linear"; // punt | |
this.reverse = gradDesc.getVal("reverse") ? true : false; | |
} | |
// Extendscript operator overloading | |
GradientInfo.prototype["=="] = function( src ) | |
{ | |
return (this.angle === src.angle) | |
&& (this.type === src.type) | |
&& (this.reverse === src.reverse); | |
} | |
PSLayerInfo.prototype.gradientInfo = function( useLayerFX ) | |
{ | |
var gradDesc = this.gradientDesc( useLayerFX ); | |
// Make sure null is returned if we aren't using layerFX and there's no adj layer | |
if (! useLayerFX && gradDesc && !gradDesc.getVal("gradient")) | |
return null; | |
return (gradDesc && (!useLayerFX || gradDesc.getVal("enabled"))) ? new GradientInfo( gradDesc ) : null; | |
} | |
// Gradient stop object, made from PS gradient.colors/gradient.transparency descriptor | |
function GradientStop( desc, maxVal ) | |
{ | |
this.r = 0; this.g = 0; this.b = 0; this.m = 100; | |
this.location = 0; this.midPoint = 50; | |
if (typeof desc != "undefined") | |
{ | |
var colorDesc = desc.getVal("color"); | |
if (colorDesc) | |
{ | |
this.r = Math.round(colorDesc.getVal("red")); | |
this.g = Math.round(colorDesc.getVal("green")); | |
this.b = Math.round(colorDesc.getVal("blue")); | |
} | |
var opacity = desc.getVal("opacity"); | |
this.m = opacity ? stripUnits(opacity) : 100; | |
this.location = (desc.getVal("location") / maxVal) * 100; | |
this.midPoint = desc.getVal("midpoint"); | |
} | |
} | |
GradientStop.prototype.copy = function( matte, location ) | |
{ | |
var result = new GradientStop(); | |
result.r = this.r; | |
result.g = this.g; | |
result.b = this.b; | |
result.m = (typeof matte == "undefined") ? this.m : matte; | |
result.location = (typeof location == "undefined") ? this.location : location; | |
result.midPoint = this.midPoint; | |
return result; | |
} | |
GradientStop.prototype["=="] = function( src ) | |
{ | |
return (this.r === src.r) && (this.g === src.g) | |
&& (this.b === src.b) && (this.m === src.m) | |
&& (this.location === src.location) | |
&& (this.midPoint === src.midPoint); | |
} | |
// Lerp ("linear interpolate") | |
GradientStop.lerp = function(t, a, b) | |
{ return Math.round(t * (b - a) + a); } // Same as (1-t)*a + t*b | |
GradientStop.prototype.interpolate = function( dest, t1 ) | |
{ | |
var result = new GradientStop(); | |
result.r = GradientStop.lerp( t1, this.r, dest.r ); | |
result.g = GradientStop.lerp( t1, this.g, dest.g ); | |
result.b = GradientStop.lerp( t1, this.b, dest.b ); | |
result.m = GradientStop.lerp(t1, this.m, dest.m ); | |
return result; | |
} | |
GradientStop.prototype.colorString = function( noTransparency ) | |
{ | |
if (typeof noTransparency == "undefined") | |
noTransparency = false; | |
var compList = (noTransparency || (this.m == 100)) | |
? [this.r, this.g, this.b] | |
: [this.r, this.g, this.b, this.m/100]; | |
var tag = (compList.length == 3) ? "rgb(" : "rgba("; | |
return tag + compList.join(",") + ")"; | |
} | |
GradientStop.prototype.toString = function() | |
{ | |
return this.colorString() + " " + Math.round(this.location) + "%"; | |
} | |
GradientStop.reverseStoplist = function( stopList ) | |
{ | |
stopList.reverse(); | |
// Fix locations to ascending order | |
for (var s in stopList) | |
stopList[s].location = 100 - stopList[s].location; | |
return stopList; | |
} | |
GradientStop.dumpStops = function( stopList ) | |
{ | |
for (var i in stopList) | |
$.writeln( stopList[i] ); | |
} | |
// Gradient format: linear-gradient( <angle>, rgb( rr, gg, bb ) xx%, rgb( rr, gg, bb ), yy%, ... ); | |
PSLayerInfo.prototype.gradientColorStops = function() | |
{ | |
// Create local representation of PS stops | |
function makeStopList( descList, maxVal ) | |
{ | |
var s, stopList = []; | |
for (s in descList) | |
stopList.push( new GradientStop( descList[s], maxVal ) ); | |
// Replace Photoshop "midpoints" with complete new stops | |
for (s = 1; s < stopList.length; ++s) | |
{ | |
if (stopList[s].midPoint != 50) | |
{ | |
var newStop = stopList[s-1].interpolate( stopList[s], 0.5 ); | |
newStop.location = GradientStop.lerp( stopList[s].midPoint/100.0, | |
stopList[s-1].location, | |
stopList[s].location ); | |
stopList.splice( s, 0, newStop ); | |
s += 1; // Skip new stop | |
} | |
} | |
return stopList; | |
} | |
var gdesc = this.gradientDesc(); | |
var psGrad = gdesc ? gdesc.getVal("gradient") : null; | |
if (psGrad) | |
{ | |
// var maxVal = psGrad.getVal( "interpolation" ); // I swear it used to find this. | |
var maxVal = 4096; | |
var c, colorStops = makeStopList( psGrad.getVal( "colors", false ), maxVal ); | |
var m, matteStops = makeStopList( psGrad.getVal( "transparency", false ), maxVal ); | |
// Check to see if any matte stops are active | |
var matteActive = false; | |
for (m in matteStops) | |
if (! matteActive) | |
matteActive = (matteStops[m].m != 100); | |
if (matteActive) | |
{ | |
// First, copy matte values from matching matte stops to the color stops | |
c = 0; | |
for (m in matteStops) | |
{ | |
while ((c < colorStops.length) && (colorStops[c].location < matteStops[m].location)) | |
c++; | |
if ((c < colorStops.length) && (colorStops[c].location == matteStops[m].location)) | |
colorStops[c].m = matteStops[m].m; | |
} | |
// Make sure the end locations match up | |
if (colorStops[colorStops.length-1].location < matteStops[matteStops.length-1].location) | |
colorStops.push( colorStops[colorStops.length-1].copy( colorStops[colorStops.length-1].m, matteStops[matteStops.length-1].location )); | |
// Now weave the lists together | |
m = 0; c = 0; | |
while (c < colorStops.length) | |
{ | |
// Must adjust color stop's matte to interpolate matteStops | |
if (colorStops[c].location < matteStops[m].location) | |
{ | |
var t = (colorStops[c].location - matteStops[m-1].location) | |
/ (matteStops[m].location - matteStops[m-1].location); | |
colorStops[c].m = GradientStop.lerp( t, matteStops[m-1].m, matteStops[m].m ); | |
c++; | |
} | |
// Must add matte stop to color stop list | |
if (matteStops[m].location < colorStops[c].location) | |
{ | |
var t, newStop; | |
// If matte stops exist in front of the 1st color stop | |
if (c < 1) | |
{ | |
newStop = colorStops[0].copy( matteStops[m].m, matteStops[m].location ); | |
} | |
else | |
{ | |
t = (matteStops[m].location - colorStops[c-1].location) | |
/ (colorStops[c].location - colorStops[c-1].location); | |
newStop = colorStops[c-1].interpolate( colorStops[c], t ); | |
newStop.m = matteStops[m].m; | |
newStop.location = matteStops[m].location; | |
} | |
colorStops.splice( c, 0, newStop ); | |
m++; | |
c++; // Step past newly added color stop | |
} | |
// Same, was fixed above | |
if (matteStops[m].location == colorStops[c].location) | |
{ | |
m++; c++; | |
} | |
} | |
// If any matte stops remain, add those too. | |
while (m < matteStops.length) | |
{ | |
var newStop = colorStops[c-1].copy( matteStops[m].m, matteStops[m].location ); | |
colorStops.push( newStop ); | |
m++; | |
} | |
} | |
return colorStops; | |
} | |
else | |
return null; | |
} | |
//////////////////////////////////// CSSToClipboard ////////////////////////////////////// | |
// Base object to scope the rest of the functions in. | |
function CSSToClipboard() | |
{ | |
// Constructor moved to reset(), so it can be called via a script. | |
} | |
cssToClip = new CSSToClipboard(); | |
cssToClip.reset = function() | |
{ | |
this.pluginName = "CSSToClipboard"; | |
this.cssText = ""; | |
this.indentSpaces = ""; | |
this.browserTags = ["-moz-", "-webkit-", "-ms-"]; | |
this.currentLayer = null; | |
this.currentPSLayerInfo = null; | |
this.groupLevel = 0; | |
this.currentLeft = 0; | |
this.currentTop = 0; | |
this.groupProgress = new ProgressBar(); | |
this.aborted = false; | |
// Work-around for screwy layer indexing. | |
this.documentIndexOffset = 0; | |
try { | |
// This throws an error if there's no background | |
if (app.activeDocument.backgroundLayer) | |
this.documentIndexOffset = 1; | |
} | |
catch (err) | |
{} | |
} | |
cssToClip.reset(); | |
// Call Photoshop to copy text to the system clipboard | |
cssToClip.copyTextToClipboard = function( txt ) | |
{ | |
var testStrDesc = new ActionDescriptor(); | |
testStrDesc.putString( keyTextData, txt ); | |
executeAction( ktextToClipboardStr, testStrDesc, DialogModes.NO ); | |
} | |
cssToClip.copyCSSToClipboard = function() | |
{ | |
this.logToHeadlights("Copy to CSS invoked"); | |
this.copyTextToClipboard( this.cssText ); | |
} | |
cssToClip.isCSSLayerKind = function( layerKind ) | |
{ | |
if (typeof layerKind == "undefined") | |
layerKind = this.currentPSLayerInfo.layerKind; | |
switch (layerKind) | |
{ | |
case kVectorSheet: return true; | |
case kTextSheet: return true; | |
case kPixelSheet: return true; | |
case kLayerGroupSheet: return true; | |
} | |
return false | |
} | |
// Listen carefully: When the Photoshop DOM *reports an index to you*, it uses one based | |
// indexing. When *you request* layer info with ref.putIndex( classLayer, index ), | |
// it uses *zero* based indexing. The DOM should probably stick to the zero-based | |
// index, so the adjustment is made here. | |
// Oh god, it gets worse...the indexing is zero based if there's no background layer. | |
cssToClip.setCurrentLayer = function( layer ) | |
{ | |
this.currentLayer = layer; | |
this.currentPSLayerInfo = new PSLayerInfo(layer.itemIndex - this.documentIndexOffset, layer.isBackgroundLayer); | |
} | |
cssToClip.getCurrentLayer = function() | |
{ | |
if (! this.currentLayer) | |
this.setCurrentLayer( app.activeDocument.activeLayer ); | |
return this.currentLayer; | |
} | |
// These shims connect the original cssToClip with the new PSLayerInfo object. | |
cssToClip.getLayerAttr = function( keyString, layerDesc ) | |
{ return this.currentPSLayerInfo.getLayerAttr( keyString, layerDesc ); } | |
cssToClip.getLayerBounds = function( ignoreEffects ) | |
{ return this.currentPSLayerInfo.getBounds( ignoreEffects ); } | |
cssToClip.descToCSSColor = function( colorDesc, colorPath ) | |
{ return this.currentPSLayerInfo.descToCSSColor( colorDesc, colorPath ); } | |
// Like getLayerAttr, but returns an app attribute. No caching. | |
cssToClip.getPSAttr = function( keyStr, objectClass ) | |
{ | |
var keyList = keyStr.split('.'); | |
var ref = new ActionReference(); | |
ref.putProperty( classProperty, makeID( keyList[0] ) ); | |
ref.putEnumerated( objectClass, typeOrdinal, enumTarget ); | |
var resultDesc = executeActionGet( ref ); | |
return resultDesc.getVal( keyList ); | |
} | |
cssToClip.getAppAttr = function( keyStr ) | |
{ return this.getPSAttr( keyStr, classApplication ); } | |
cssToClip.getDocAttr = function( keyStr ) | |
{ return this.getPSAttr( keyStr, classDocument ); } | |
cssToClip.pushIndent = function() | |
{ | |
this.indentSpaces += " "; | |
} | |
cssToClip.popIndent = function() | |
{ | |
if (this.indentSpaces.length < 2) | |
alert("Error - indent underflow"); | |
this.indentSpaces = this.indentSpaces.slice(0,-2); | |
} | |
cssToClip.addText = function( text, browserTagList ) | |
{ | |
var i; | |
if (typeof browserTagList == "undefined") | |
browserTagList = null; | |
if (browserTagList) | |
for (i in browserTagList) | |
this.cssText += (this.indentSpaces + browserTagList[i] + text + "\n"); | |
else | |
this.cssText += (this.indentSpaces + text + "\n"); | |
// $.writeln(text); // debug | |
} | |
cssToClip.addStyleLine = function( cssText, baseDesc, browserTagList ) | |
{ | |
var result = this.currentPSLayerInfo.replaceDescKey( cssText, baseDesc ); | |
var replacementFailed = result[0]; | |
cssText = result[1]; | |
if (! replacementFailed) | |
this.addText( cssText, browserTagList ); | |
return !replacementFailed; | |
} | |
// Text items need to try both the base and the default descriptors | |
cssToClip.addStyleLine2 = function( cssText, baseDesc, backupDesc ) | |
{ | |
if (! this.addStyleLine( cssText, baseDesc ) && backupDesc) | |
this.addStyleLine( cssText, backupDesc ); | |
} | |
// Text is handled as a special case, to take care of rounding issues. | |
// In particular, we're avoiding 30.011 and 29.942, which round1k would miss | |
// Seriously fractional text sizes (as specified by "roundMargin") are left as-is | |
cssToClip.addTextSize = function( baseDesc, backupDesc ) | |
{ | |
var roundMargin = 0.2; // Values outside of this are left un-rounded | |
var sizeText = this.getLayerAttr("size", baseDesc ); | |
if (! sizeText) | |
sizeText = this.getLayerAttr("size", backupDesc ); | |
if (! sizeText) | |
return; | |
var unitRxp = /[\d.-]+\s*(\w+)/g; | |
var units = unitRxp.exec(sizeText); | |
if (! units) return; | |
units = units[1]; | |
var textNum = stripUnits(sizeText); | |
var roundOff = textNum - (textNum|0); | |
if ((roundOff < roundMargin) || (roundOff > (1.0-roundMargin))) | |
this.addText( "font-size: " + Math.round(textNum) + units +";"); | |
else | |
this.addStyleLine2( "font-size: $size$;", baseDesc, backupDesc ); | |
} | |
// Checks the geometry, and returns "ellipse", "roundrect" | |
// or "null" (if the points don't match round rect/ellipse pattern). | |
// NOTE: All of this should go away when the DAG metadata is available | |
// to just tell you what the radius is. | |
// NOTE2: The path for a shape is ONLY visible when that shape is the active | |
// layer. So you must set the shape in question to be the active layer before | |
// calling this function. This really slows down the script, unfortunately. | |
cssToClip.extractShapeGeometry = function() | |
{ | |
// We accept a shape as conforming if the coords are within "magnitude" | |
// of the overall size. | |
function near(a,b, magnitude) | |
{ | |
a = Math.abs(a); b = Math.abs(b); | |
return Math.abs(a-b) < (Math.max(a,b)/magnitude); | |
} | |
function sameCoord( pathPt, xy ) | |
{ | |
return (pathPt.rightDirection[xy] == pathPt.anchor[xy]) | |
&& (pathPt.leftDirection[xy] == pathPt.anchor[xy]); | |
} | |
function dumpPts( pts ) // For debug viewing in Matlab | |
{ | |
function pt2str( pt ) { return "[" + Math.floor(pt[0]) + ", " + Math.floor(pt[1]) + "]"; } | |
var i; | |
for (i = 0; i < pts.length; ++i) | |
$.writeln( "[" + [pt2str(pts[i].rightDirection), pt2str(pts[i].anchor), pt2str(pts[i].leftDirection)].join( "; " ) + "];" ); | |
} | |
// Control point location for Bezier arcs. | |
// See problem 1, http://www.graphics.stanford.edu/courses/cs248-98-fall/Final/q1.html | |
const kEllipseDist = 4*(Math.sqrt(2) - 1)/3; | |
if (app.activeDocument.pathItems.length == 0) | |
return null; // No path | |
// Grab the path name from the layer name (it's auto-generated) | |
var i, pathName = localize("$$$/ShapeLayerPathName=^0 Shape Path"); | |
var path = app.activeDocument.pathItems[pathName.replace(/[^]0/,app.activeDocument.activeLayer.name)]; | |
// If we have a plausible path, walk the geometry and see if it matches a shape we know about. | |
if ((path.kind == PathKind.VECTORMASK) && (path.subPathItems.length == 1)) | |
{ | |
var subPath = path.subPathItems[0]; | |
if (subPath.closed && (subPath.pathPoints.length == 4)) // Ellipse? | |
{ | |
function next(index) { return (index + 1) % 4; } | |
function prev(index) { return (index > 0) ? (index-1) : 3; } | |
var pts = subPath.pathPoints; | |
// dumpPts( pts ); | |
for (i = 0; i < 4; ++i) | |
{ | |
var xy = i % 2; // 0 = x, 1 = y, alternates as we traverse the oval sides | |
if (! sameCoord( pts[i], 1-xy )) return null; | |
if (! near( pts[i].leftDirection[xy] - pts[i].anchor[xy], | |
(pts[next(i)].anchor[xy] - pts[i].anchor[xy]) * kEllipseDist, 100)) return null; | |
if (! near( pts[i].anchor[xy] - pts[i].rightDirection[xy], | |
(pts[prev(i)].anchor[xy] - pts[i].anchor[xy]) * kEllipseDist, 100)) return null; | |
} | |
// Return the X,Y radius | |
return [pts[1].anchor[0] - pts[0].anchor[0], pts[1].anchor[1] - pts[0].anchor[1], "ellipse"]; | |
} | |
else if (subPath.closed && (subPath.pathPoints.length == 8)) // RoundRect? | |
{ | |
var pts = subPath.pathPoints; | |
//dumpPts( pts ); | |
function sameCoord2( pt, xy, io ) | |
{ | |
return (sameCoord( pt, xy ) | |
&& ( ((io == 0) && (pt.rightDirection[1-xy] == pt.anchor[1-xy])) | |
|| ((io == 1) && (pt.leftDirection[1-xy] == pt.anchor[1-xy])) ) ); | |
} | |
function next(index) { return (index + 1) % 8; } | |
function prev(index) { return (index > 0) ? (index-1) : 7; } | |
function arm( pt, xy, io ) { return (io == 0) ? pt.rightDirection[xy] : pt.leftDirection[xy]; } | |
for (i = 0; i < 8; ++i) | |
{ | |
var io = i % 2; // Incoming / Outgoing vector on the anchor point | |
var hv = (i >> 1) % 2; // Horizontal / Vertical side of the round rect | |
if (! sameCoord2( pts[i], 1-hv, 1-io )) return null; | |
if (io == 0) | |
{ | |
if( ! near( arm( pts[i], hv, io ) - pts[i].anchor[hv], | |
(pts[prev(i)].anchor[hv] - pts[i].anchor[hv])*kEllipseDist, 10 ) ) | |
return null; | |
} | |
else | |
{ | |
if( ! near( arm( pts[i], hv, io ) - pts[i].anchor[hv], | |
(pts[next(i)].anchor[hv] - pts[i].anchor[hv])*kEllipseDist, 10 ) ) | |
return null; | |
} | |
} | |
return [pts[2].anchor[0] - pts[1].anchor[0], pts[2].anchor[1] - pts[1].anchor[1], "round rect"]; | |
} | |
} | |
} | |
// Gradient format: linear-gradient( <angle>, rgb( rr, gg, bb ) xx%, rgb( rr, gg, bb ), yy%, ... ); | |
cssToClip.gradientToCSS = function() | |
{ | |
var colorStops = this.currentPSLayerInfo.gradientColorStops(); | |
var gradInfo = this.currentPSLayerInfo.gradientInfo(); | |
if (colorStops && gradInfo) | |
{ | |
if (gradInfo.reverse) | |
colorStops = GradientStop.reverseStoplist( colorStops ); | |
if (gradInfo.type == "linear") | |
return gradInfo.type + "-gradient( " + gradInfo.angle + ", " + colorStops.join(", ") + ");"; | |
// Radial - right now gradient is always centered (50% 50%) | |
if (gradInfo.type == "radial") | |
return gradInfo.type + "-gradient( 50% 50%, circle closest-side, " + colorStops.join(", ") + ");"; | |
} | |
else | |
return null; | |
} | |
// Translate Photoshop drop shadow. May need work with layerEffects.scale, | |
// and need to figure out what's up with the global angle. | |
cssToClip.addDropShadow = function( shadowType, boundsInfo ) | |
{ | |
var dsInfo = this.currentPSLayerInfo.getDropShadowInfo( shadowType, boundsInfo, "dropShadow" ); | |
var isInfo = this.currentPSLayerInfo.getDropShadowInfo( shadowType, boundsInfo, "innerShadow" ); | |
if (! (dsInfo || isInfo)) | |
return; | |
function map( lst, fn ) | |
{ | |
var i, result = []; | |
for (i = 0; i < lst.length; ++i) | |
result.push( fn(lst[i] ) ); | |
return result; | |
} | |
function getShadowCSS( info, skipSpread ) | |
{ | |
// Translate PS parameters to CSS style | |
var opacity = info.dsDesc.getVal("opacity"); | |
// LFX reports "opacity" as a percentage, so convert it to decimal | |
opacity = opacity ? stripUnits(opacity)/100.0 : 1; | |
var colorSpec = cssToClip.currentPSLayerInfo.descToRGBAColor( "color", opacity, info.dsDesc ); | |
var size = stripUnits(info.dsDesc.getVal("blur")); | |
var chokeMatte = stripUnits(info.dsDesc.getVal("chokeMatte")); | |
var spread = size * chokeMatte / 100; | |
var blurRad = size - spread; | |
// Hack - spread is not used for text shadows. | |
var spreadStr = skipSpread ? "" : spread+ "px " | |
return info.xoff+" " + info.yoff + " " | |
+ blurRad + "px " + spreadStr + colorSpec; | |
} | |
function insetShadowCSS( info ) { return "inset " + getShadowCSS( info ); } | |
function textShadowCSS( info ) { return getShadowCSS( info, true ); } | |
// You say CSS was designed by committee? Really? | |
if (shadowType == "box-shadow") | |
{ | |
var i, shadows = []; | |
if (dsInfo) | |
shadows = map( dsInfo, getShadowCSS ); | |
if (isInfo) // push.apply == extend | |
shadows.push.apply( shadows, map( isInfo, insetShadowCSS ) ); | |
this.addText( shadowType + ": " + shadows.join(",") + ";" ); | |
boundsInfo.hasLayerEffect = true; | |
} | |
// CSS doesn't support inner shadow, just drop shadow | |
if (dsInfo && (shadowType == "text-shadow")) { | |
var shadows = map( dsInfo, textShadowCSS ); | |
this.addText(shadowType + ": " + shadows.join(",") + ";" ); | |
} | |
} | |
cssToClip.addOpacity = function( opacity ) | |
{ | |
opacity = (typeof opacity == "number") ? opacity : this.getLayerAttr("opacity"); | |
if ((typeof opacity == "number") && (opacity < 255)) | |
this.addText( "opacity: " + round1k(opacity / 255) + ";" ); | |
} | |
cssToClip.addRGBAColor = function( param, opacity, colorDesc ) | |
{ | |
this.addText( param + ': ' + this.currentPSLayerInfo.descToRGBAColor( "color", opacity, colorDesc ) +';' ); | |
} | |
function BoundsParameters() | |
{ | |
this.borderWidth = 0; | |
this.textOffset = null; | |
this.hasLayerEffect = false; | |
this.textLine = false; | |
this.rawTextBounds = null; | |
this.textHasDecenders = false; | |
this.textFontSize = 0; | |
this.textLineHeight = 1.2; | |
} | |
cssToClip.addObjectBounds = function( boundsInfo ) | |
{ | |
var curLayer = this.getCurrentLayer(); | |
var bounds = this.getLayerBounds( boundsInfo.hasLayerEffect ); | |
if (boundsInfo.rawTextBounds) | |
{ | |
// If the text has been transformed, rawTextBounds is set. We need | |
// to set the CSS bounds to reflect the *un*transformed text, placed about | |
// the center of the transformed text's bounding box. | |
var cenx = bounds[0] + (bounds[2] - bounds[0])/2; | |
var ceny = bounds[1] + (bounds[3] - bounds[1])/2; | |
var txtWidth = boundsInfo.rawTextBounds[2] - boundsInfo.rawTextBounds[0]; | |
var txtHeight= boundsInfo.rawTextBounds[3] - boundsInfo.rawTextBounds[1]; | |
bounds[0] = cenx - (txtWidth/2); | |
bounds[1] = ceny - (txtHeight/2); | |
bounds[2] = bounds[0] + txtWidth; | |
bounds[3] = bounds[1] + txtHeight; | |
} | |
if (boundsInfo.textLine | |
&& !boundsInfo.hasLayerEffect | |
&& (boundsInfo.textFontSize !== 0)) | |
{ | |
var actualTextPixelHeight = (bounds[3] -bounds[1]).as('px'); | |
var textBoxHeight = boundsInfo.textFontSize * boundsInfo.textLineHeight; | |
var correction = (actualTextPixelHeight - textBoxHeight)/2; | |
// If the text doesn't have decenders, then the correction by the PS baseline will | |
// be off (the text is instead centered vertically in the CSS text box). This applies | |
// a different correciton for this case. | |
if (boundsInfo.textOffset) | |
{ | |
if (boundsInfo.textHasDecenders) | |
{ | |
var lineHeightCorrection = (boundsInfo.textFontSize - (boundsInfo.textFontSize * boundsInfo.textLineHeight))/2; | |
boundsInfo.textOffset[1] += lineHeightCorrection; | |
} | |
else | |
boundsInfo.textOffset[1] = UnitValue( correction, 'px' ); | |
} | |
} | |
if ((this.groupLevel == 0) && boundsInfo.textOffset) | |
{ | |
this.addText("position: absolute;" ); | |
this.addText("left: " + (bounds[0] + boundsInfo.textOffset[0]).asCSS() +";"); | |
this.addText("top: " + (bounds[1] + boundsInfo.textOffset[1]).asCSS() + ";"); | |
} | |
else | |
{ | |
// Go through the DOM to ensure we're working in Pixels | |
var left = bounds[0]; | |
var top = bounds[1]; | |
if (boundsInfo.textOffset == null) | |
boundsInfo.textOffset = [0, 0]; | |
// Intuitively you'd think this would be "relative", but you'd be wrong. | |
// "Absolute" coordinates are relative to the container. | |
this.addText("position: absolute;"); | |
this.addText("left: " + (left | |
- this.currentLeft | |
+ boundsInfo.textOffset[0]).asCSS() +";"); | |
this.addText("top: " + (top | |
- this.currentTop | |
+ boundsInfo.textOffset[1]).asCSS() + ";"); | |
} | |
// Go through the DOM to ensure we're working in Pixels | |
var width = bounds[2] - bounds[0]; | |
var height = bounds[3] - bounds[1]; | |
// In CSS, the border width is added to the -outside- of the bounds. In order to match | |
// the default behavior in PS, we adjust it here. | |
if (boundsInfo.borderWidth > 0) | |
{ | |
width -= 2*boundsInfo.borderWidth; | |
height -= 2*boundsInfo.borderWidth; | |
} | |
// Don't generate a width for "line" (paint) style text. | |
if (! boundsInfo.textLine) | |
{ | |
this.addText( "width: " + ((width < 0) ? 0 : width.asCSS()) + ";"); | |
this.addText( "height: " + ((height < 0) ? 0 : height.asCSS()) + ";"); | |
} | |
} | |
// Only called for shape (vector) layers. | |
cssToClip.getShapeLayerCSS = function( boundsInfo ) | |
{ | |
// If we have AGM stroke style info, generate that. | |
var agmDesc = this.getLayerAttr( "AGMStrokeStyleInfo" ); | |
boundsInfo.borderWidth = 0; | |
var opacity = this.getLayerAttr("opacity" ); | |
if (agmDesc && agmDesc.getVal( "strokeEnabled")) | |
{ | |
// Assumes pixels! | |
boundsInfo.borderWidth = makeUnitVal(agmDesc.getVal( "strokeStyleLineWidth" )); | |
this.addStyleLine( "border-width: $strokeStyleLineWidth$;", agmDesc ); | |
this.addStyleLine( "border-color: $strokeStyleContent.color$;", agmDesc ); | |
var cap = agmDesc.getVal( "strokeStyleLineCapType" ); | |
var dashes = agmDesc.getVal( "strokeStyleLineDashSet", false ); | |
if (dashes && dashes.length > 0) | |
{ | |
if ((cap == "strokeStyleRoundCap") && (dashes[0] == 0)) | |
this.addStyleLine("border-style: dotted;" ); | |
if ((cap == "strokeStyleButtCap") && (dashes[0] > 0)) | |
this.addStyleLine("border-style: dashed;"); | |
} | |
else | |
this.addStyleLine("border-style: solid;"); | |
} | |
// Check for layerFX style borders | |
var fxDesc = this.getLayerAttr( "layerEffects.frameFX" ); | |
if (fxDesc && fxDesc.getVal( "enabled" ) | |
&& (fxDesc.getVal( "paintType" ) == "solidColor")) | |
{ | |
opacity = (stripUnits( fxDesc.getVal("opacity") ) / 100) * opacity; | |
boundsInfo.borderWidth = makeUnitVal(fxDesc.getVal( "size" )); // Assumes pixels! | |
this.addStyleLine("border-style: solid;"); | |
this.addStyleLine("border-width: $size$;", fxDesc ); | |
this.addStyleLine("border-color: $color$;", fxDesc ); | |
} | |
// The Path for a shape *only* becomes visible when that shape is the active layer, | |
// so we need to make the current layer active before we extract geometry information. | |
// Yes, I know this is painfully slow, modifying the DOM or PS to behave otherwise is hard. | |
var saveLayer = app.activeDocument.activeLayer; | |
app.activeDocument.activeLayer = this.getCurrentLayer(); | |
var shapeGeom = this.extractShapeGeometry(); | |
app.activeDocument.activeLayer = saveLayer; | |
// We assume path coordinates are in pixels, they're not stored as UnitValues in the DOM. | |
if (shapeGeom) | |
{ | |
// In CSS, the borderRadius needs to be added to the borderWidth, otherwise ovals | |
// turn into rounded rects. | |
if (shapeGeom[2] == "ellipse") | |
this.addText("border-radius: 50%;"); | |
else | |
{ | |
var radius = Math.round((shapeGeom[0]+shapeGeom[1])/2); | |
// Note: path geometry is -always- in points ... unless the ruler type is Pixels. | |
radius = (app.preferences.rulerUnits == Units.PIXELS) | |
? radius = pixelsToAppUnits( radius ) | |
: radius = UnitValue( radius, "pt" ); | |
cssToClip.addText( "border-radius: " + radius.asCSS() +";"); | |
} | |
} | |
var i, gradientCSS = this.gradientToCSS(); | |
if (!agmDesc // If AGM object, only fill if explictly turned on | |
|| (agmDesc && agmDesc.getVal("fillEnabled"))) | |
{ | |
if (gradientCSS) | |
{ | |
for (i in this.browserTags) | |
this.addText( "background-image: " + this.browserTags[i] + gradientCSS); | |
} | |
else | |
{ | |
var fillOpacity = this.getLayerAttr("fillOpacity") / 255.0; | |
if (fillOpacity < 1.0) | |
this.addRGBAColor( "background-color", fillOpacity, this.getLayerAttr( "adjustment" )); | |
else | |
this.addStyleLine( "background-color: $adjustment.color$;" ); | |
} | |
} | |
this.addOpacity( opacity ); | |
this.addDropShadow( "box-shadow", boundsInfo ); | |
} | |
// Only called for text layers. | |
cssToClip.getTextLayerCSS = function( boundsInfo ) | |
{ | |
function isStyleOn( textDesc, defTextDesc, styleKey, onText ) | |
{ | |
var styleText = textDesc.getVal( styleKey ); | |
if (! styleText && defTextDesc) | |
styleText = defTextDesc.getVal( styleKey ); | |
return (styleText && (styleText.search( onText ) >= 0)); | |
} | |
// If the text string is empty, then trying to access the attributes fails, so exit now. | |
var textString = this.getLayerAttr("textKey.textKey"); | |
if (textString.length === 0) | |
return; | |
var cssUnits = DOMunitToCSS[app.preferences.rulerUnits]; | |
boundsInfo.textOffset = [UnitValue( 0, cssUnits ), UnitValue( 0, cssUnits )]; | |
var leadingOffset = 0; | |
var opacity = (this.getLayerAttr("opacity")/255.0) * (this.getLayerAttr("fillOpacity")/255.0); | |
var textDesc = this.getLayerAttr( "textKey.textStyleRange.textStyle" ); | |
var defaultDesc = this.getLayerAttr( "textKey.paragraphStyleRange.paragraphStyle.defaultStyle" ); | |
if (! defaultDesc) | |
defaultDesc = this.getLayerAttr("textKey.textStyleRange.textStyle.baseParentStyle"); | |
if (textDesc) | |
{ | |
// this.addStyleLine2( "font-size: $size$;", textDesc, defaultDesc ); | |
this.addTextSize( textDesc, defaultDesc ); | |
this.addStyleLine2( 'font-family: "$fontName$";', textDesc, defaultDesc ); | |
if (opacity == 1.0) | |
this.addStyleLine2( "color: $color$;", textDesc, defaultDesc ); // Color can just default to black | |
else | |
{ | |
if (textDesc.getVal("color")) | |
this.addRGBAColor( "color" , opacity, textDesc ); | |
else | |
this.addRGBAColor( "color", opacity, defaultDesc ); | |
} | |
// This table is: [PS Style event key ; PS event value keyword to search for ; corresponding CSS] | |
var styleTable = [["fontStyleName", "Bold", "font-weight: bold;"], | |
["fontStyleName", "Italic", "font-style: italic;"], | |
["strikethrough", "StrikethroughOn", "text-decoration: line-through;"], | |
["underline", "underlineOn", "text-decoration: underline;"], | |
// Need RE, otherwise conflicts w/"smallCaps" | |
["fontCaps", /^allCaps/, "text-transform: uppercase;"], | |
["fontCaps", "smallCaps", "font-variant: small-caps;"], | |
// These should probably also modify the font size? | |
["baseline", "superScript", "vertical-align: super;"], | |
["baseline", "subScript", "vertical-align: sub;"]]; | |
var i; | |
for (i in styleTable) | |
if (isStyleOn( textDesc, defaultDesc, styleTable[i][0], styleTable[i][1] )) | |
this.addText( styleTable[i][2] ); | |
// Synthesize the line-height from the "leading" (line spacing) / font-size | |
var fontSize = textDesc.getVal( "size" ); | |
if (! fontSize && defaultDesc) fontSize = defaultDesc.getVal( "size" ); | |
var fontLeading = textDesc.getVal( "leading" ); | |
if (fontSize) | |
fontSize = stripUnits(fontSize); | |
if (fontSize && fontLeading) | |
{ | |
leadingOffset = fontLeading; | |
boundsInfo.textLineHeight = round1k(stripUnits(fontLeading) / fontSize); | |
} | |
this.addText("line-height: " + boundsInfo.textLineHeight + ";"); | |
if (fontSize) | |
boundsInfo.textFontSize = fontSize; | |
var pgraphStyle = this.getLayerAttr( "textKey.paragraphStyleRange.paragraphStyle" ); | |
if (pgraphStyle) | |
{ | |
this.addStyleLine( "text-align: $align$;", pgraphStyle ); | |
var lineIndent = pgraphStyle.getVal( "firstLineIndent" ); | |
if (lineIndent && (stripUnits(lineIndent) != 0)) | |
this.addStyleLine( "text-indent: $firstLineIndent$;", pgraphStyle ); | |
// PS startIndent for whole 'graph, CSS is? | |
} | |
// Update boundsInfo | |
this.addDropShadow( "text-shadow", boundsInfo ); | |
// text-indent text-align letter-spacing line-height | |
var baseDesc = this.getLayerAttr( "textKey" ); | |
function txtBnd( id ) { return makeUnitVal(baseDesc.getVal(id)); } | |
boundsInfo.textOffset = [txtBnd("bounds.left") - txtBnd("boundingBox.left"), | |
txtBnd("bounds.top") - txtBnd("boundingBox.top") + makeUnitVal(leadingOffset)]; | |
if (this.getLayerAttr( "textKey.textShape.char" ) == "paint") | |
boundsInfo.textLine = true; | |
// This seems to be the one reliable indicator that the text has decenders | |
// below the baseline, indicating the positioning in CSS must be handled | |
// differently. | |
if (txtBnd("boundingBox.bottom").as('px') / fontSize > 0.03) | |
boundsInfo.textHasDecenders = true; | |
// Matrix: [xx xy 0; yx yy 0; tx ty 1], if not identiy, then add it. | |
var textXform = this.getLayerAttr( "textKey.transform" ); | |
var vScale = textDesc.getVal("verticalScale"); | |
var hScale = textDesc.getVal("horizontalScale"); | |
vScale = (typeof vScale == "number") ? round1k(vScale/100.0) : 1; | |
hScale = (typeof hScale == "number") ? round1k(hScale/100.0) : 1; | |
if (textXform) | |
{ | |
function xfm(key) { return textXform.getVal( key ); } | |
var xformData = this.currentPSLayerInfo.replaceDescKey("[$xx$, $xy$, $yx$, $yy$, $tx$, $ty$]", textXform); | |
var m = eval(xformData[1]); | |
m[0] *= hScale; | |
m[3] *= vScale; | |
if (! ((m[0] == 1) && (m[1] == 0) | |
&& (m[2] == 0) && (m[3] == 1) | |
&& (m[4] == 0) && (m[5] == 0))) | |
{ | |
boundsInfo.rawTextBounds = baseDesc.getVal("boundingBox").extractBounds(); | |
this.addText("transform: matrix( " + m.join(",") + ");", this.browserTags ); | |
} | |
} | |
else | |
{ | |
// Case for text not otherwise transformed. | |
if ((vScale != 1.0) || (hScale != 1.0)) | |
{ | |
boundsInfo.rawTextBounds = baseDesc.getVal("boundingBox").extractBounds(); | |
this.addText( "transform: scale(" + hScale + ", " + vScale + ");", this.browserTags ); | |
} | |
} | |
} | |
} | |
cssToClip.getPixelLayerCSS = function() | |
{ | |
var name = this.getLayerAttr( "name" ); | |
// If suffix isn't present, add one. Assume file is in same folder as parent. | |
if (name.search( /[.]((\w){3,4})$/ ) < 0) { | |
this.addStyleLine( 'background-image: url("$name$.png");'); | |
} | |
else | |
{ | |
// If the layer has a suffix, assume Generator-style naming conventions | |
var docSuffix = app.activeDocument.name.search(/([.]psd)$/i); | |
var docFolder = (docSuffix < 0) ? app.activeDocument.name | |
: app.activeDocument.name.slice(0, docSuffix); | |
docFolder += "-assets/"; // The "-assets" is not localized. | |
// Weed out any Generator parameters, if present. | |
var m = name.match(/(?:[\dx%? ])*([^.+,\n\r]+)([.]\w+)+$/); | |
if (m) { | |
name = m[1]+m[2]; | |
} | |
this.addText( 'background-image: url("' + docFolder + name + '");'); | |
} | |
var fillOpacity = this.getLayerAttr("fillOpacity")/255.0; | |
this.addOpacity( this.getLayerAttr("opacity") * fillOpacity ); | |
} | |
// This walks the group and outputs all visible items in that group. If the current | |
// layer is not a group, then it walks to the end of the document (i.e., for dumping | |
// the whole document). | |
cssToClip.getGroupLayers = function ( currentLayer, memberTest, processAllLayers) | |
{ | |
processAllLayers = (typeof processAllLayers === "undefined") ? false : processAllLayers; | |
// If processing all of the layers, don't stop at the end of the first group | |
var layerLevel = processAllLayers ? 2 : 1; | |
var visibleLevel = layerLevel; | |
var curIndex = currentLayer.index; | |
var saveGroup = []; | |
if (currentLayer.layerKind === kLayerGroupSheet) | |
{ | |
if (! currentLayer.visible) { | |
return; | |
} | |
curIndex--; // Step to next layer in group so layerLevel is correct | |
} | |
var groupLayers = []; | |
while ((curIndex > 0) && (layerLevel > 0)) | |
{ | |
var nextLayer = new PSLayerInfo(curIndex, false); | |
if (memberTest(nextLayer.layerKind)) | |
{ | |
if (nextLayer.layerKind === kLayerGroupSheet) | |
{ | |
if (nextLayer.visible && (visibleLevel === layerLevel)) { | |
visibleLevel++; | |
// The layers and section bounds must be swapped | |
// in order to process the group's layerFX | |
saveGroup.push(nextLayer); | |
groupLayers.push(kHiddenSectionBounder); | |
} | |
layerLevel++; | |
} | |
else | |
{ | |
if (nextLayer.visible && (visibleLevel === layerLevel)) { | |
groupLayers.push(nextLayer); | |
} | |
} | |
} | |
else | |
if (nextLayer.layerKind === kHiddenSectionBounder) | |
{ | |
layerLevel--; | |
if (layerLevel < visibleLevel) { | |
visibleLevel = layerLevel; | |
if (saveGroup.length > 0) { | |
groupLayers.push(saveGroup.pop()); | |
} | |
} | |
} | |
curIndex--; | |
} | |
return groupLayers; | |
}; | |
// Recursively count the number of layers in the group, for progress bar | |
cssToClip.countGroupLayers = function( layerGroup, memberTest ) | |
{ | |
if (! memberTest) | |
memberTest = cssToClip.isCSSLayerKind; | |
var currentLayer = new PSLayerInfo( layerGroup.itemIndex - cssToClip.documentIndexOffset); | |
var groupLayers = this.getGroupLayers( currentLayer, memberTest ); | |
var i, visLayers = 0; | |
for (i = 0; i < groupLayers.length; ++i) | |
if (typeof groupLayers[i] === "object") | |
visLayers++; | |
return visLayers; | |
} | |
// The CSS for nested DIVs (essentially; what's going on with groups) | |
// are NOT specified hierarchically. So we need to finish this group's | |
// output, then create the CSS for everything in it. | |
cssToClip.pushGroupLevel = function() | |
{ | |
if (this.groupLevel == 0) | |
{ | |
var numSteps = this.countGroupLayers( this.getCurrentLayer() )+1; | |
this.groupProgress.totalProgressSteps = numSteps; | |
} | |
this.groupLevel++; | |
} | |
cssToClip.popGroupLevel = function() | |
{ | |
var i, saveGroupLayer = this.getCurrentLayer(); | |
var saveLeft = this.currentLeft, saveTop = this.currentTop; | |
var bounds = this.getLayerBounds(); | |
this.currentLeft = bounds[0]; | |
this.currentTop = bounds[1]; | |
var notAborted = true; | |
for (i = 0; ((i < saveGroupLayer.layers.length) && notAborted); ++i) | |
{ | |
this.setCurrentLayer( saveGroupLayer.layers[i] ); | |
if (this.isCSSLayerKind()) | |
notAborted = this.gatherLayerCSS(); | |
} | |
this.setCurrentLayer( saveGroupLayer ); | |
this.groupLevel--; | |
this.currentLeft = saveLeft; | |
this.currentTop = saveTop; | |
return notAborted; | |
} | |
cssToClip.layerNameToCSS = function( layerName ) | |
{ | |
const kMaxLayerNameLength = 50; | |
// Remove any user-supplied class/ID delimiter | |
if ((layerName[0] == ".") || (layerName[0] == "#")) | |
layerName = layerName.slice(1); | |
// Remove any other creepy punctuation. | |
var badStuff = /[“”";!.?,'`@’#'$%^&*)(+=|}{><\x2F\s-]/g | |
var layerName = layerName.replace(badStuff, "_"); | |
// Text layer names may be arbitrarily long; keep it real | |
if (layerName.length > kMaxLayerNameLength) | |
layerName = layerName.slice(0, kMaxLayerNameLength-3) ; | |
// Layers can't start with digits, force an _ in front in that case. | |
if (layerName.match(/^[\d].*/)) | |
layerName = "_" + layerName; | |
return layerName; | |
} | |
// Gather the CSS info for the current layer, and add it to this.cssText | |
// Returns FALSE if the process was aborted. | |
cssToClip.gatherLayerCSS = function() | |
{ | |
// Script can't be called from PS context menu unless there is an active layer | |
var curLayer = this.getCurrentLayer(); | |
// Skip invisible or non-css-able layers. | |
var layerKind = this.currentPSLayerInfo.layerKind; | |
if (layerKind === kBackgroundSheet) // Background == pixels. Never in groups. | |
layerKind = kPixelSheet; | |
if ((! this.isCSSLayerKind( layerKind )) || (! curLayer.visible)) | |
return true; | |
var isCSSid = (curLayer.name[0] == '#'); // Flag if generating ID not class | |
var layerName = this.layerNameToCSS( curLayer.name ); | |
this.addText( (isCSSid ? "#" : ".") + layerName + " {" ); | |
this.pushIndent(); | |
var boundsInfo = new BoundsParameters(); | |
switch (layerKind) | |
{ | |
case kLayerGroupSheet: this.pushGroupLevel(); break; | |
case kVectorSheet: this.getShapeLayerCSS( boundsInfo ); break; | |
case kTextSheet: this.getTextLayerCSS( boundsInfo ); break; | |
case kPixelSheet: this.getPixelLayerCSS(); break; | |
} | |
var aborted = false; | |
if (this.groupLevel > 0) | |
aborted = this.groupProgress.nextProgress(); | |
if (aborted) | |
return false; | |
// Use the Opacity tag for groups, so it applies to all descendants. | |
if (layerKind == kLayerGroupSheet) | |
this.addOpacity(); | |
this.addObjectBounds( boundsInfo ); | |
this.addStyleLine( "z-index: $itemIndex$;" ); | |
this.popIndent(); | |
this.addText("}"); | |
var notAborted = true; | |
// If we're processing a group, now is the time to process the member layers. | |
if ((curLayer.typename == "LayerSet") | |
&& (this.groupLevel > 0)) | |
notAborted = this.popGroupLevel(); | |
return notAborted; | |
} | |
// Main entry point | |
cssToClip.copyLayerCSSToClipboard = function() | |
{ | |
var resultObj = new Object(); | |
app.doProgress( localize("$$$/Photoshop/Progress/CopyCSSProgress=Copying CSS..."), "this.copyLayerCSSToClipboardWithProgress(resultObj)"); | |
return resultObj.msg; | |
} | |
cssToClip.copyLayerCSSToClipboardWithProgress = function(outResult) | |
{ | |
this.reset(); | |
var saveUnits = app.preferences.rulerUnits; | |
app.preferences.rulerUnits = Units.PIXELS; // Web dudes want pixels. | |
try { | |
var elapsedTime, then = new Date(); | |
if (! this.gatherLayerCSS()) | |
return; // aborted | |
elapsedTime = new Date() - then; | |
} | |
catch (err) | |
{ | |
// Copy CSS fails if a new doc pops open before it's finished, possible if Cmd-N is selected | |
// before the progress bar is up. This message isn't optimal, but it was too late to get a | |
// proper error message translated, so this was close enough. | |
// MUST USE THIS FOR RELEASE PRIOR TO CS7/PS14 | |
// alert( localize( "$$$/MaskPanel/MaskSelection/NoLayerSelected=No layer selected" ) ); | |
alert( localize( "$$$/Scripts/CopyCSSToClipboard/Error=Internal error creating CSS: " ) + err.message + | |
localize( "$$$/Scripts/CopyCSSToClipboard/ErrorLine= at script line ") + err.line ); | |
} | |
cssToClip.copyCSSToClipboard(); | |
if (saveUnits) | |
app.preferences.rulerUnits = saveUnits; | |
// We can watch this in ESTK without screwing up the app | |
outResult.msg = ("time: " + (elapsedTime / 1000.0) + " sec"); | |
} | |
// ----- End of CopyCSSToClipboard script proper. What follows is test & debugging code ----- | |
// Dump out a layer attribute as text. This is how you learn what attributes are available. | |
// Note this only works for ActionDescriptor or ActionList layer attributes; for simple | |
// types just call cssToClip.getLayerAttr(). | |
cssToClip.dumpLayerAttr = function( keyName ) | |
{ | |
this.setCurrentLayer( app.activeDocument.activeLayer ); | |
var ref = new ActionReference(); | |
ref.putIdentifier( classLayer, app.activeDocument.activeLayer.id ); | |
layerDesc = executeActionGet( ref ); | |
var desc = layerDesc.getVal( keyName, false ); | |
if (! desc) | |
return; | |
if ((desc.typename == "ActionDescriptor") || (desc.typename == "ActionList")) | |
desc.dumpDesc( keyName ); | |
else | |
if ((typeof desc != "string") && (desc.length >= 1)) | |
{ | |
s = [] | |
for (var i in desc) | |
{ | |
if ((typeof desc[i] == "object") | |
&& (desc[i].typename in {"ActionDescriptor":1, "ActionList":1 })) | |
desc[i].dumpDesc( keyName + "[" +i + "]" ); | |
else | |
s.push( desc[i].dumpDesc( keyName ) ) | |
} | |
if (s.length > 0) | |
$.writeln( keyName +": [" + s.join(", ") + "]" ); | |
} | |
else | |
$.writeln(keyName + ": " + ActionDescriptor.dumpValue(desc) ); | |
} | |
// Taken from inspection of ULayerElement.cpp | |
cssToClip.allLayerAttrs = ['AGMStrokeStyleInfo','adjustment','background','bounds', | |
'boundsNoEffects','channelRestrictions','color','count','fillOpacity','filterMaskDensity', | |
'filterMaskFeather','generatorSettings','globalAngle','group','hasFilterMask', | |
'hasUserMask','hasVectorMask','itemIndex','layer3D','layerEffects','layerFXVisible', | |
'layerSection','layerID','layerKind','layerLocking','layerSVGdata','layerSection', | |
'linkedLayerIDs','metadata','mode','name','opacity','preserveTransparency', | |
'smartObject','targetChannels','textKey','useAlignedRendering','useAlignedRendering', | |
'userMaskDensity','userMaskEnabled','userMaskFeather','userMaskLinked', | |
'vectorMaskDensity','vectorMaskFeather','videoLayer','visible','visibleChannels', | |
'XMPMetadataAsUTF8']; | |
// Dump all the available attributes on the layer. | |
cssToClip.dumpAllLayerAttrs = function() | |
{ | |
this.setCurrentLayer( app.activeDocument.activeLayer ); | |
var ref = new ActionReference(); | |
ref.putIndex( classLayer, app.activeDocument.activeLayer.itemIndex ); | |
var desc = executeActionGet( ref ); | |
var i; | |
for (i = 0; i < this.allLayerAttrs.length; ++i) | |
{ | |
var attr = this.allLayerAttrs[i]; | |
var attrDesc = null; | |
try { | |
attrDesc = this.getLayerAttr( attr ); | |
if (attrDesc) | |
this.dumpLayerAttr( attr ); | |
else | |
$.writeln( attr + ": null" ); | |
} | |
catch (err) | |
{ | |
$.writeln( attr + ': ' + err.message ); | |
} | |
} | |
} | |
// Walk the document's layers and describe them. | |
cssToClip.dumpLayers = function( layerSet ) | |
{ | |
var i, layerID; | |
if (typeof layerSet == "undefined") | |
layerSet = app.activeDocument; | |
for (i= 0; i < layerSet.layers.length; ++i) | |
{ | |
if (layerSet.layers[i].typename == "LayerSet") | |
this.dumpLayers( layerSet.layers[i] ); | |
this.setCurrentLayer( layerSet.layers[i] ); | |
layerID = (layerSet.layers[i].isBackground) ? "BG" : cssToClip.getLayerAttr( "layerID" ); | |
$.writeln("Layer[" + cssToClip.getLayerAttr( "itemIndex" ) + "] ID=" + layerID + " name: " + cssToClip.getLayerAttr( "name" ) ); | |
} | |
} | |
cssToClip.logToHeadlights = function(eventRecord) | |
{ | |
var headlightsActionID = stringIDToTypeID("headlightsLog"); | |
var desc = new ActionDescriptor(); | |
desc.putString(stringIDToTypeID("subcategory"), "Export"); | |
desc.putString(stringIDToTypeID("eventRecord"), eventRecord); | |
executeAction(headlightsActionID, desc, DialogModes.NO); | |
} | |
function testProgress() | |
{ | |
app.doProgress( localize("$$$/Photoshop/Progress/CopyCSSProgress=Copying CSS..."),"testProgressTask()" ); | |
} | |
function testProgressTask() | |
{ | |
var i, total = 10; | |
var progBar = new ProgressBar(); | |
progBar.totalProgressSteps = total; | |
for (i = 0; i <= total; ++i) | |
{ | |
// if (progBar.updateProgress( i )) | |
if (progBar.nextProgress()) | |
{ | |
$.writeln('cancelled'); | |
break; | |
} | |
$.sleep(800); | |
} | |
} | |
// Debug. Uncomment one of these lines, and watch the output | |
// in the ESTK "JavaScript Console" panel. | |
// Walk the layers | |
//runCopyCSSFromScript = true; cssToClip.dumpLayers(); | |
// Print out some interesting objects | |
//runCopyCSSFromScript = true; cssToClip.dumpLayerAttr( "AGMStrokeStyleInfo" ); | |
//runCopyCSSFromScript = true; cssToClip.dumpLayerAttr( "adjustment" ); // Gradient, etc. | |
//runCopyCSSFromScript = true; cssToClip.dumpLayerAttr( "layerEffects" ); // Layer FX, drop shadow, etc. | |
//runCopyCSSFromScript = true; cssToClip.dumpLayerAttr( "textKey" ); | |
//runCopyCSSFromScript = true; cssToClip.dumpLayerAttr( "bounds" ); | |
// Some useful individual parameters | |
//runCopyCSSFromScript = true; $.writeln( cssToClip.dumpLayerAttr( "opacity" ) ); | |
//runCopyCSSFromScript = true; $.writeln( cssToClip.dumpLayerAttr( "fillOpacity" ) ); | |
//runCopyCSSFromScript = true; $.writeln( cssToClip.dumpLayerAttr( "name" )); | |
//runCopyCSSFromScript = true; $.writeln( cssToClip.dumpLayerAttr( "itemIndex" )); | |
//runCopyCSSFromScript = true; $.writeln( cssToClip.dumpLayerAttr( "layerFXVisible" )); | |
//runCopyCSSFromScript = true; $.writeln( cssToClip.dumpLayerAttr("layerSVGdata" )); | |
//runCopyCSSFromScript = true; $.writeln( cssToClip.dumpLayerAttr("layerVectorPointData" )); | |
// Debugging tests | |
//runCopyCSSFromScript = true; testProgress(); | |
//runCopyCSSFromScript = true; cssToClip.countGroupLayers( cssToClip.getCurrentLayer() ); | |
// Backdoor to allow using this script as a library; | |
if ((typeof( runCopyCSSFromScript ) == 'undefined') | |
|| (runCopyCSSFromScript == false)) | |
cssToClip.copyLayerCSSToClipboard(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment