• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
0

How to get text transform box boundary?

Guide ,
May 18, 2020 May 18, 2020

Copy link to clipboard

Copied

Hello everyone!
In a certain situation, I need to align the text with a different number of lines along the lower border, taking into account the baseline of the text. If i use the values ​​of bounds, boundsNoEffects, boundsNoMask, boundingBox of the text layer, or bounds and boundingBox of the text itself (textKey) (by the way, they are points, I could not adequately convert them to pixels), then they give values ​​that depend on the style of the text. That is, alignment occurs along the boundaries of the letters, but not along the baseline (this causes the text to shift when aligning).

2020-05-19_09-43-39.png

At the same time, i notice that when transforming text layer, we see a transform box that takes into account the baseline of the text (i.e., it does not depend on the style of individual letters).

2020-05-19_09-35-32.png

Is there any way to get these boundaries?

TOPICS
Actions and scripting

Views

5.1K

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 2 Correct answers

Community Expert , May 19, 2020 May 19, 2020

I think you might have been mistaken, for Point Text »bounds« seems to give the dimensions of the »whole« Layer, »boundingBox« the dimensions of the pixel content. 

Albeit in relation to the insertion point. 

(edited the code)

Screenshot 2020-05-19 at 11.17.56.png

Screenshot taken at View > 100%

 

// 2020, use it at your own risk;
if (app.documents.length > 0) {
var ref = new ActionReference();
ref.putProperty(stringIDToTypeID("property"), stringIDToTypeID('textKey'));
ref.putEnumerated( charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), ch
...

Votes

Translate

Translate
People's Champ , May 25, 2020 May 25, 2020

По просьбе топикстартера.

 

"МногаБукав" - поэтому на родном. )

 

Я, лично, не работаю с текстом и мало что про него знаю.

Но полез в интернет и нарыл кое-что.

xx, xy, yx, xx, ty, tx – это элементы матрицы трансформации при 2D aффином преобразовании.

Данный скрипт читает эти данные из текстового слоя.

Элементы tx, ty (смещение точки) скорее всего не используются.

Вместо них нужно использовать координаты textClickPoint. Я их просто складываю.

 

Итого имеем скрипт.

 

 

try 
{

function show_points
...

Votes

Translate

Translate
Adobe
Community Expert ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

Hi,

I have a script that I found long days ago. May be this scipt will help you to find out the correct values. 

// 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
*/

if (typeof COPYCSS !== 'object') {
    COPYCSS = {};
}

$.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);
                    COPYCSS.textProperties[thisKey] = ref.value;
                    break;

                default:
                    $.writeln(thisKey +
                        ": " + ActionDescriptor.dumpValue(this.getFlatType(key)));
                    COPYCSS.textProperties[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 + " <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 COPYCSS.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(COPYCSS.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();
COPYCSS.textProperties = {};
//new Array();

COPYCSS.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) { }
}

COPYCSS.reset();

// Call Photoshop to copy text to the system clipboard
COPYCSS.copyTextToClipboard = function (txt) {
    var testStrDesc = new ActionDescriptor();

    testStrDesc.putString(keyTextData, txt);
    executeAction(ktextToClipboardStr, testStrDesc, DialogModes.NO);
}

COPYCSS.copyCSSToClipboard = function () {
    this.logToHeadlights("Copy to CSS invoked");
    this.copyTextToClipboard(this.cssText);
}

COPYCSS.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.
COPYCSS.setCurrentLayer = function (layer) {
    this.currentLayer = layer;
    this.currentPSLayerInfo = new PSLayerInfo(layer.itemIndex - this.documentIndexOffset, layer.isBackgroundLayer);
}

COPYCSS.getCurrentLayer = function () {
    if (!this.currentLayer)
        this.setCurrentLayer(app.activeDocument.activeLayer);
    return this.currentLayer;
}

// These shims connect the original cssToClip with the new PSLayerInfo object.
COPYCSS.getLayerAttr = function (keyString, layerDesc) {
    return this.currentPSLayerInfo.getLayerAttr(keyString, layerDesc);
}

COPYCSS.getLayerBounds = function (ignoreEffects) {
    return this.currentPSLayerInfo.getBounds(ignoreEffects);
}

COPYCSS.descToCSSColor = function (colorDesc, colorPath) {
    return this.currentPSLayerInfo.descToCSSColor(colorDesc, colorPath);
}

// Like getLayerAttr, but returns an app attribute.  No caching.
COPYCSS.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);
}

COPYCSS.getAppAttr = function (keyStr) {
    return this.getPSAttr(keyStr, classApplication);
}

COPYCSS.getDocAttr = function (keyStr) {
    return this.getPSAttr(keyStr, classDocument);
}

COPYCSS.pushIndent = function () {
    this.indentSpaces += "  ";
}

COPYCSS.popIndent = function () {
    if (this.indentSpaces.length < 2)
        alert("Error - indent underflow");
    this.indentSpaces = this.indentSpaces.slice(0, -2);
}

COPYCSS.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
}

COPYCSS.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
COPYCSS.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
COPYCSS.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.
COPYCSS.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%, ... );
COPYCSS.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.
COPYCSS.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 = COPYCSS.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(",") + ";");
    }
}

COPYCSS.addOpacity = function (opacity) {
    opacity = (typeof opacity == "number") ? opacity : this.getLayerAttr("opacity");
    if ((typeof opacity == "number") && (opacity < 255))
        this.addText("opacity: " + round1k(opacity / 255) + ";");
}

COPYCSS.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;
}

COPYCSS.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.
COPYCSS.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");

            COPYCSS.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.
COPYCSS.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);
            }
        }
    }
}

COPYCSS.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).
COPYCSS.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
COPYCSS.countGroupLayers = function (layerGroup, memberTest) {
    if (!memberTest)
        memberTest = COPYCSS.isCSSLayerKind;
    var currentLayer = new PSLayerInfo(layerGroup.itemIndex - COPYCSS.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.
COPYCSS.pushGroupLevel = function () {
    if (this.groupLevel == 0) {
        var numSteps = this.countGroupLayers(this.getCurrentLayer()) + 1;
        this.groupProgress.totalProgressSteps = numSteps;
    }
    this.groupLevel++;
}

COPYCSS.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;
}

COPYCSS.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.
COPYCSS.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
COPYCSS.copyLayerCSSToClipboard = function () {
    var resultObj = new Object();

    app.doProgress(localize("$$$/Photoshop/Progress/CopyCSSProgress=Copying CSS..."), "this.copyLayerCSSToClipboardWithProgress(resultObj)");

    return resultObj.msg;
}

COPYCSS.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);
    }

    COPYCSS.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 COPYCSS.getLayerAttr().
COPYCSS.dumpLayerAttr = function (keyName) {
    // COPYCSS.textProperties = {};
    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
COPYCSS.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.  
COPYCSS.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.
COPYCSS.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" : COPYCSS.getLayerAttr("layerID");
        $.writeln("Layer[" + COPYCSS.getLayerAttr("itemIndex") + "] ID=" + layerID + " name: " + COPYCSS.getLayerAttr("name"));
    }
}

COPYCSS.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; COPYCSS.dumpLayers();

// Print out some interesting objects
runCopyCSSFromScript = true; COPYCSS.dumpLayerAttr( "AGMStrokeStyleInfo" );
runCopyCSSFromScript = true; COPYCSS.dumpLayerAttr( "adjustment" );  // Gradient, etc.
runCopyCSSFromScript = true; COPYCSS.dumpLayerAttr( "layerEffects" );  // Layer FX, drop shadow, etc.
//~ runCopyCSSFromScript = true;
COPYCSS.dumpLayerAttr("textKey");
runCopyCSSFromScript = true;
COPYCSS.dumpLayerAttr("bounds");

// Backdoor to allow using this script as a library; 
// if ((typeof (runCopyCSSFromScript) == 'undefined') ||
//     (runCopyCSSFromScript == false))
//COPYCSS.copyLayerCSSToClipboard();     

 

Let us know if this helps. Or if you can attach the psd file, I will check once.

 

 

Best regards

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

Thanks! But this code simply displays information from the layer descriptor. I can get it without his help. The problem is that all the borders there also describe the symbols themselves rather than the bounding box of the transformation.

2020-05-19_10-34-15.png
There are some offsets in the descriptor that may affect the display of the bounding box transformation, but I can’t figure out how to compare them with other values to get the coordinates needed:
textKey.transform.yy: 6.73553805098192
textKey.textShape [0] .transform.yy: 1
.textStyle.markYDistFromBaseline: 100pt

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

I think you might have been mistaken, for Point Text »bounds« seems to give the dimensions of the »whole« Layer, »boundingBox« the dimensions of the pixel content. 

Albeit in relation to the insertion point. 

(edited the code)

Screenshot 2020-05-19 at 11.17.56.png

Screenshot taken at View > 100%

 

// 2020, use it at your own risk;
if (app.documents.length > 0) {
var ref = new ActionReference();
ref.putProperty(stringIDToTypeID("property"), stringIDToTypeID('textKey'));
ref.putEnumerated( charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
var layerDesc = executeActionGet(ref);
var textDesc = layerDesc.getObjectValue(stringIDToTypeID('textKey'));
var theBounds = textDesc.getObjectValue(stringIDToTypeID("bounds"));
var theBoundingBox = textDesc.getObjectValue(stringIDToTypeID("boundingBox"));
var thePoint = textDesc.getObjectValue(stringIDToTypeID("textClickPoint"));
var theBounds = checkDesc2 (theBounds);
var theBoundingBox = checkDesc2 (theBoundingBox);
var point = checkDesc2 (thePoint);
alert ("the point\n"+point+"\nbounds\n"+theBounds+"\nbounding box\n"+theBoundingBox)
};
//////
////// based on code by michael l hale //////
function checkDesc2 (theDesc) {
var c = theDesc.count;
var str = '';
for(var i=0;i<c;i++){ //enumerate descriptor's keys
	str = str + 'Key '+i+' = '+typeIDToStringID(theDesc.getKey(i))+': '+theDesc.getType(theDesc.getKey(i))+'\n'+getValues (theDesc, i)+'\n';
	};
//alert("desc\n\n"+str);
return str
};
////// check //////
function getValues (theDesc, theNumber) {
switch (theDesc.getType(theDesc.getKey(theNumber))) {
case DescValueType.ALIASTYPE:
return theDesc.getPath(theDesc.getKey(theNumber));
break;
case DescValueType.BOOLEANTYPE:
return theDesc.getBoolean(theDesc.getKey(theNumber));
break;
case DescValueType.CLASSTYPE:
return theDesc.getClass(theDesc.getKey(theNumber));
break;
case DescValueType.DOUBLETYPE:
return theDesc.getDouble(theDesc.getKey(theNumber));
break;
case DescValueType.ENUMERATEDTYPE:
return (typeIDToStringID(theDesc.getEnumerationValue(theDesc.getKey(theNumber)))+"_"+typeIDToStringID(theDesc.getEnumerationType(theDesc.getKey(theNumber))));
break;
case DescValueType.INTEGERTYPE:
return theDesc.getInteger(theDesc.getKey(theNumber));
break;
case DescValueType.LISTTYPE:
return theDesc.getList(theDesc.getKey(theNumber));
break;
case DescValueType.OBJECTTYPE:
return (theDesc.getObjectValue(theDesc.getKey(theNumber))+"_"+typeIDToStringID(theDesc.getObjectType(theDesc.getKey(theNumber))));
break;
case DescValueType.RAWTYPE:
return theDesc.getReference(theDesc.getData(theNumber));
break;
case DescValueType.REFERENCETYPE:
return theDesc.getReference(theDesc.getKey(theNumber));
break;
case DescValueType.STRINGTYPE:
return theDesc.getString(theDesc.getKey(theNumber));
break;
case DescValueType.UNITDOUBLE:
return (theDesc.getUnitDoubleValue(theDesc.getKey(theNumber))+"_"+typeIDToStringID(theDesc.getUnitDoubleType(theDesc.getKey(theNumber))));
break;
default: 
break;
};
};

 

 

 

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

c_pfaffenbichler, thanks! I thought in the same direction, but I had difficulty translating points into pixels (I had never used these units of measurement before).

I understand correctly - I need to get bounds of textKey, then find the coordinates of textClickPoint, recalculate bounds in pixels, recalculate the offsets of bounds from textClickPoint?

textClickPoint is indicated as a percentage - is it a percentage of the width and height of the page, respectively?

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

Yes, as far as I can see that would be the necessary approach. Edit: At least for point text. 

If you resize the image to 72ppi without resampling this might make the calculations easier. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

What am I doing wrong? 😞

(psd with this layer in the message above)

2020-05-19_23-13-09.png

 

 

 

#target photoshop;

s2t = stringIDToTypeID;
(r = new ActionReference()).putProperty(s2t('property'), p = s2t('resolution'))
r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
var docRes = executeActionGet(r).getUnitDoubleValue(p);

(d = new ActionDescriptor()).putUnitDouble(s2t('resolution'), s2t('densityUnit'), 72);
executeAction(s2t("imageSize"), d, DialogModes.NO);

(r = new ActionReference()).putProperty(s2t('property'), p = s2t('height'))
r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
var docHeight = executeActionGet(r).getUnitDoubleValue(p);

(r = new ActionReference()).putProperty(s2t('property'), p = s2t('width'))
r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
var docWidth = executeActionGet(r).getUnitDoubleValue(p);

(r = new ActionReference()).putProperty(s2t('property'), p = s2t('textKey'))
r.putEnumerated(s2t('layer'), s2t('ordinal'), s2t('targetEnum'));
var textKey = executeActionGet(r).getObjectValue(p);

alert (typeIDToStringID(textKey.getList(s2t ('textShape')).getObjectValue(0).getEnumerationValue(s2t ('textType'))));

var clickPointH = textKey.getObjectValue(s2t('textClickPoint')).getUnitDoubleValue(s2t('horizontal')),
    clickPointV = textKey.getObjectValue(s2t('textClickPoint')).getUnitDoubleValue(s2t('vertical')),
    clickPointHPx = Math.round(clickPointH / 100 * docWidth),
    clickPointVPx = Math.round(clickPointV / 100 * docHeight);

makeGuide(clickPointHPx, 'vertical')
makeGuide(clickPointVPx, 'horizontal')

var bounds = textKey.getObjectValue(s2t('bounds')),
left = bounds.getUnitDoubleValue(s2t('left')),
top = bounds.getUnitDoubleValue(s2t('top')),
right = bounds.getUnitDoubleValue(s2t('right')),
bottom = bounds.getUnitDoubleValue(s2t('bottom'));

makeGuide(left + clickPointHPx, 'vertical');
makeGuide(top + clickPointVPx, 'horizontal');
makeGuide(right + clickPointHPx, 'vertical');
makeGuide(bottom + clickPointVPx, 'horizontal');

(d = new ActionDescriptor()).putUnitDouble(s2t('resolution'), s2t('densityUnit'), docRes);
executeAction(s2t("imageSize"), d, DialogModes.NO);

function makeGuide(position, orientation) {
    (d1 = new ActionDescriptor()).putUnitDouble(s2t("position"), s2t("pixelsUnit"), position);
    d1.putEnumerated(s2t("orientation"), s2t("orientation"), s2t(orientation));
    d1.putEnumerated(s2t("kind"), s2t("kind"), s2t("document"));
    (d = new ActionDescriptor()).putObject(s2t("new"), s2t("guide"), d1);
    (r = new ActionReference()).putClass(s2t("guide"));
    d.putReference(s2t("null"), r);
    executeAction(s2t("make"), d, DialogModes.NO);
}

 

 

 

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

UPD:

I noticed that the coordinates of the bounding box are in principle correct, just for some reason go beyond the bounds of the document in the descriptor itself. If they are reduced, say, with a coefficient of 0.45 (for a specific document), then they quite accurately describe the bounding box of the transformation. I could assume that I wasn’t converting pt to px correctly, but the descriptor explicitly states that for a document 600 pt high, the lower bound of the bounding box of the text object is 921 (!!!) pt. (and that’s before adjusting for click point coordinates). I do not understand how this can be.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

Apparently you transformed the Type Layer and the bounds refer to the untransformed Type Layer: 

Screenshot 2020-05-20 at 08.54.05.png

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 20, 2020 May 20, 2020

Copy link to clipboard

Copied

Really! I completely forgot that the layer was transformed. That is, when recalculating the coordinates, the transformation coefficient must also be taken into account.

2020-05-20_09-57-59.png

Thank you so much!

 

By the way, I noticed that in Photoshop settings there are two types of pt dimension. Apparently, this moment should be taken into account.

2020-05-20_09-59-18.png

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

Could you please attach the PSD files?

Best regards

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

test psd: Untitled-1.psd

2020-05-19_14-06-16.png

For example, there are two text objects (for simplicity - one style and font size). Objects have a different number of lines. I need to align them differently than the align command does:

2020-05-19_13-59-46.png

and so that their transform boxes coincide on the bottom edge:

2020-05-19_14-00-45.png

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
May 19, 2020 May 19, 2020

Copy link to clipboard

Copied

»by the way, they are points, I could not adequately convert them to pixels«

You can just have the Script save the resolution, resize the image to 72ppi resolution without resampling, do whatever and at the end resize the image to its original resolution without resampling again. 

In the meantime I think pt and px should be interchangable. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 21, 2020 May 21, 2020

Copy link to clipboard

Copied

As a result, I wrote such code to get the rectangle of the transformation of the text layer. It works fine, but only for layers with a simple transformation (xx yy):

 

#target photoshop;

s2t = stringIDToTypeID;
(r = new ActionReference()).putProperty(s2t('property'), p = s2t('resolution'));
r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
var docRes = executeActionGet(r).getUnitDoubleValue(p);

(r = new ActionReference()).putProperty(s2t('property'), p = s2t('unitsPrefs'));
r.putEnumerated(s2t("application"), s2t("ordinal"), s2t("targetEnum"));
var exactPoints = executeActionGet(r).getObjectValue(p).getBoolean(s2t('exactPoints')) ? 72.27 : 72;

(d = new ActionDescriptor()).putUnitDouble(s2t('resolution'), s2t('densityUnit'), exactPoints);
executeAction(s2t("imageSize"), d, DialogModes.NO);

(r = new ActionReference()).putProperty(s2t('property'), p = s2t('height'))
r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
var docHeight = executeActionGet(r).getUnitDoubleValue(p);

(r = new ActionReference()).putProperty(s2t('property'), p = s2t('width'))
r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
var docWidth = executeActionGet(r).getUnitDoubleValue(p);

(r = new ActionReference()).putProperty(s2t('property'), p = s2t('textKey'))
r.putEnumerated(s2t('layer'), s2t('ordinal'), s2t('targetEnum'));
var textKey = executeActionGet(r).getObjectValue(p);

var clickPointH = textKey.getObjectValue(s2t('textClickPoint')).getUnitDoubleValue(s2t('horizontal')),
    clickPointV = textKey.getObjectValue(s2t('textClickPoint')).getUnitDoubleValue(s2t('vertical')),
    clickPointHPx = Math.round(clickPointH / 100 * docWidth),
    clickPointVPx = Math.round(clickPointV / 100 * docHeight);

makeGuide(clickPointHPx, 'vertical')
makeGuide(clickPointVPx, 'horizontal')

if (typeIDToStringID(textKey.getList(s2t('textShape')).getObjectValue(0).getEnumerationValue(s2t('textType'))) != 'box') {
    var bounds = textKey.getObjectValue(s2t('bounds')),
        left = bounds.getUnitDoubleValue(s2t('left')),
        top = bounds.getUnitDoubleValue(s2t('top')),
        right = bounds.getUnitDoubleValue(s2t('right')),
        bottom = bounds.getUnitDoubleValue(s2t('bottom')),
        transform = textKey.hasKey(s2t('transform')) ? textKey.getObjectValue(s2t('transform')) : null,
        xx = transform != null ? transform.getDouble(s2t('xx')) : 1,
        yy = transform != null ? transform.getDouble(s2t('yy')) : 1,
        xy = transform != null ? transform.getDouble(s2t('xy')) : 1,
        yx = transform != null ? transform.getDouble(s2t('xy')) : 1;

    makeGuide(left * xx + clickPointHPx, 'vertical');
    makeGuide(top * yy + clickPointVPx, 'horizontal');
    makeGuide(right * xx + clickPointHPx, 'vertical');
    makeGuide(bottom * yy + clickPointVPx, 'horizontal');
}

(d = new ActionDescriptor()).putUnitDouble(s2t('resolution'), s2t('densityUnit'), docRes);
executeAction(s2t("imageSize"), d, DialogModes.NO);

function makeGuide(position, orientation) {
    (d1 = new ActionDescriptor()).putUnitDouble(s2t("position"), s2t("pixelsUnit"), position);
    d1.putEnumerated(s2t("orientation"), s2t("orientation"), s2t(orientation));
    d1.putEnumerated(s2t("kind"), s2t("kind"), s2t("document"));
    (d = new ActionDescriptor()).putObject(s2t("new"), s2t("guide"), d1);
    (r = new ActionReference()).putClass(s2t("guide"));
    d.putReference(s2t("null"), r);
    executeAction(s2t("make"), d, DialogModes.NO);
}

 

For more complex transformations, as I understand it, displacements along each axis are additionally indicated (by the way, I could not make such a transformation so that tx and ty change - what is it?):

 

xx = 0.35280340604376
xy = -0.26049157198293
yx = 0.09027891302419
yy = 0.47847952594196
tx = 0
ty = 0

 

I approximately understood the principle and even was able to process these displacements for simple cases such as a 90-degree turn, but so far I can’t get any further: I need to somehow take into account all the displacements so that in the end they give the correct transformation box. If someone can help, i would be grateful.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
People's Champ ,
May 25, 2020 May 25, 2020

Copy link to clipboard

Copied

По просьбе топикстартера.

 

"МногаБукав" - поэтому на родном. )

 

Я, лично, не работаю с текстом и мало что про него знаю.

Но полез в интернет и нарыл кое-что.

xx, xy, yx, xx, ty, tx – это элементы матрицы трансформации при 2D aффином преобразовании.

Данный скрипт читает эти данные из текстового слоя.

Элементы tx, ty (смещение точки) скорее всего не используются.

Вместо них нужно использовать координаты textClickPoint. Я их просто складываю.

 

Итого имеем скрипт.

 

 

try 
{

function show_points(p1, p2)
    {
    try {
        var d = new ActionDescriptor();
        var r = new ActionReference();
        r.putProperty(stringIDToTypeID("path"), stringIDToTypeID("workPath"));
        d.putReference(stringIDToTypeID("null"), r);
        var list = new ActionList();
        var d1 = new ActionDescriptor();
        d1.putEnumerated(stringIDToTypeID("shapeOperation"), stringIDToTypeID("shapeOperation"), stringIDToTypeID("add"));
        var list1 = new ActionList();
        var d2 = new ActionDescriptor();
        d2.putBoolean(stringIDToTypeID("closedSubpath"), true);
        var list2 = new ActionList();

        for (var i = 0; i < 4; i++)
            {
            var d3 = new ActionDescriptor();
            var d4 = new ActionDescriptor();
            d4.putUnitDouble(stringIDToTypeID("horizontal"), stringIDToTypeID("distanceUnit"), p1[i][0]);
            d4.putUnitDouble(stringIDToTypeID("vertical"), stringIDToTypeID("distanceUnit"), p1[i][1]);
            d3.putObject(stringIDToTypeID("anchor"), stringIDToTypeID("point"), d4);
            list2.putObject(stringIDToTypeID("pathPoint"), d3);
            }                

        d2.putList(stringIDToTypeID("points"), list2);


        list1.putObject(stringIDToTypeID("subpathsList"), d2);
        d1.putList(stringIDToTypeID("subpathListKey"), list1);
        list.putObject(stringIDToTypeID("pathComponent"), d1);
        var d11 = new ActionDescriptor();
        d11.putEnumerated(stringIDToTypeID("shapeOperation"), stringIDToTypeID("shapeOperation"), stringIDToTypeID("add"));
        var list3 = new ActionList();
        var d12 = new ActionDescriptor();
        d12.putBoolean(stringIDToTypeID("closedSubpath"), true);
        var list4 = new ActionList();

        for (var i = 0; i < 4; i++)
            {
            var d3 = new ActionDescriptor();
            var d4 = new ActionDescriptor();
            d4.putUnitDouble(stringIDToTypeID("horizontal"), stringIDToTypeID("distanceUnit"), p2[i][0]);
            d4.putUnitDouble(stringIDToTypeID("vertical"), stringIDToTypeID("distanceUnit"), p2[i][1]);
            d3.putObject(stringIDToTypeID("anchor"), stringIDToTypeID("point"), d4);
            list4.putObject(stringIDToTypeID("pathPoint"), d3);
            }

        d12.putList(stringIDToTypeID("points"), list4);

        list3.putObject(stringIDToTypeID("subpathsList"), d12);
        d11.putList(stringIDToTypeID("subpathListKey"), list3);
        list.putObject(stringIDToTypeID("pathComponent"), d11);
        d.putList(stringIDToTypeID("to"), list);
        executeAction(stringIDToTypeID("set"), d, DialogModes.NO);
        }
    catch (e) { throw(e); }
    }

function tranform(p, xx, xy, yx, yy, tx, ty)
    {
    try {
        var x = p[0];
        var y = p[1];

        p[0] = xx*x + yx*y + tx;
        p[1] = xy*x + yy*y + ty;
        }
    catch (e) { alert(e); }
    }

var doc = app.activeDocument;

var r = new ActionReference();
r.putProperty(stringIDToTypeID("property"), stringIDToTypeID("width"));
r.putEnumerated(stringIDToTypeID("document"), stringIDToTypeID("ordinal"), stringIDToTypeID("targetEnum"));
var w = executeActionGet(r).getUnitDoubleValue(stringIDToTypeID("width"));

var r = new ActionReference();
r.putProperty(stringIDToTypeID("property"), stringIDToTypeID("height"));
r.putEnumerated(stringIDToTypeID("document"), stringIDToTypeID("ordinal"), stringIDToTypeID("targetEnum"));
var h = executeActionGet(r).getUnitDoubleValue(stringIDToTypeID("height"));

var r = new ActionReference();
r.putProperty(stringIDToTypeID("property"), stringIDToTypeID("textKey"));
r.putEnumerated(stringIDToTypeID("layer"), stringIDToTypeID("ordinal"), stringIDToTypeID("targetEnum"));

var tkey = executeActionGet(r).getObjectValue(stringIDToTypeID("textKey"));

var xx = 1;
var xy = 0;
var yx = 0;
var yy = 1;
var tx = 0;
var ty = 0;
          
if (tkey.hasKey(stringIDToTypeID("transform")))
    {
    xx = tkey.getObjectValue(stringIDToTypeID("transform")).getDouble(stringIDToTypeID("xx"));
    xy = tkey.getObjectValue(stringIDToTypeID("transform")).getDouble(stringIDToTypeID("xy"));
    yx = tkey.getObjectValue(stringIDToTypeID("transform")).getDouble(stringIDToTypeID("yx"));
    yy = tkey.getObjectValue(stringIDToTypeID("transform")).getDouble(stringIDToTypeID("yy"));
    tx = tkey.getObjectValue(stringIDToTypeID("transform")).getDouble(stringIDToTypeID("tx")); // not used
    ty = tkey.getObjectValue(stringIDToTypeID("transform")).getDouble(stringIDToTypeID("ty")); // not used
    }    
            
var x0 = tkey.getObjectValue(stringIDToTypeID("bounds")).getUnitDoubleValue(stringIDToTypeID("left"));
var y0 = tkey.getObjectValue(stringIDToTypeID("bounds")).getUnitDoubleValue(stringIDToTypeID("top"));  
var x1 = tkey.getObjectValue(stringIDToTypeID("bounds")).getUnitDoubleValue(stringIDToTypeID("right"));  
var y1 = tkey.getObjectValue(stringIDToTypeID("bounds")).getUnitDoubleValue(stringIDToTypeID("bottom"));  

var p1 = [[x0,y0],[x1,y0],[x1,y1],[x0,y1]];

var ch = tkey.getObjectValue(stringIDToTypeID("textClickPoint")).getUnitDoubleValue(stringIDToTypeID("horizontal"));
var cv = tkey.getObjectValue(stringIDToTypeID("textClickPoint")).getUnitDoubleValue(stringIDToTypeID("vertical"));

tx += w*ch/100;
ty += h*cv/100;

tranform(p1[0], xx, xy, yx, yy, tx, ty);
tranform(p1[1], xx, xy, yx, yy, tx, ty);
tranform(p1[2], xx, xy, yx, yy, tx, ty);
tranform(p1[3], xx, xy, yx, yy, tx, ty);


var l = Math.min(p1[0][0], p1[1][0], p1[2][0], p1[3][0]);
var t = Math.min(p1[0][1], p1[1][1], p1[2][1], p1[3][1]);
var r = Math.max(p1[0][0], p1[1][0], p1[2][0], p1[3][0]);
var b = Math.max(p1[0][1], p1[1][1], p1[2][1], p1[3][1]);

var p2 = [[l,t], [r,t], [r,b], [l,b]];

show_points(p1, p2);


}
catch (e) { alert(e); }

 

 

Изначальные координаты границы текста находятся в bounds для textKey.

Их них формируется начальные точки – массив P1.

 

Функция tranform(p, xx, xy, yx, yy, tx, ty)

Преобразует точки согласно матрице

[x']    [xx yx tx]    [x]    [xx*x + yx*y + tx ]

[y'] = [xy yy ty] * [y] = [xy*x + yy*y + ty]

[1 ]    [0    0   1]    [1]    [           1               ] 

После этого P1 содержит координаты точек трансформированной границы.

Это та граница, которая появляется если во время редактирования текста нажить Ctrl.

 

Массив точек P2 формируется из минимумов и максимумов координат точек, т.е. представляет собой прямоугольник, в который вписан параллелограмм P1.

 

P2 совпадает со второй границей, которая появляется при трансформации текста через Ctrl+T.

 

Функция show_points(p1, p2)

 

Показывает нужные фигуры в виде Work Path.

 

Данный скрипт не учитывает включение функции Warp Text.

 

Как прикрутить сюда Warp Text я ещё не допёр.

Нет ни желания, ни времени ковырять.

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
May 25, 2020 May 25, 2020

Copy link to clipboard

Copied

Thanks for the sample code! I think that it will come in handy for many, since the information on the transformation of smart objects is stored in the same way

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
People's Champ ,
May 26, 2020 May 26, 2020

Copy link to clipboard

Copied

LATEST
As far as I know in a smart object everything is a little different.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines