// 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) { try { 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 ) ) ); } } catch (err) { $.writeln("Error "+keyName+"["+i+"]: " + err.message); } } } ActionList.prototype.dumpDesc = function( keyName ) { var i; if (typeof( keyName ) == "undefined") keyName = ""; if (this.count == 0) $.writeln( keyName + " " ); 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( , 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( , 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();