Skip to content

Guidance Needed for Creating Editable Text Layer Objects with Correct Properties Setting for Photoshop and Correctly Showing the Text Layer as per Given Width, Height, X, and Y Position #257

@aamirsdev

Description

@aamirsdev

Hi, I'm working on PSD file creation using the ag-psd library and encountering issues with creating editable text layers. I need to generate a text layer that is editable in Photoshop (showing properties like font size, color, justification in the Character/Paragraph panels) and renders correctly when opening the file, including proper multi-line text support.
Based on the layer information I have, here's the object structure I'm using:

const layerInfo = {
    name: 'Text Layer',
    text: 'Sample Text',
    fontFamily: 'ArialMT',
    fontSize: 48,
    fontWeight: 400,
    fontStyle: 'normal',
    left: 100,   // x position
    top: 100,    // y position
    tracking: 1,
    color: '#000000',
    justification: 'left',
    width,       // Optional, canvas decides based on text
    height,      // Optional, canvas decides based on text
    backgroundColor: '',
    includeCanvas: true,
    lineHeight: 1.2
    };

Currently, I'm setting all the Photoshop properties in the textData object (including styleRuns, paragraphStyleRuns, bounds, etc.) to make it editable, and I'm creating a canvas object to measure/render the text for bounds.

However, I'm facing problems:

  • -If not using the canvas, the text value is not rendering when opening the file in Photoshop (renders only when clicking on the edit text layer option).
  • -If using the canvas, the canvas is creating the layer not as per given width and height, and also the text is coming below the layer and the layer is showing above the text, but it gets correct when we click on edit the text layer in Photoshop

Here's my current script for the createTextLayer function:


// Function to create a text layer object compatible with ag-psd, using full Photoshop text structure for editability and properties
function createTextLayer(layerInfo) {
    // Default values for optional properties
    const {
        name = 'Text Layer',
        text = 'Sample Text',
        fontFamily = 'ArialMT',
        fontSize = 48,
        fontWeight = 400,
        fontStyle = 'normal',
        left = 100,
        top = 100,
        tracking = 1,
        color = '#000000',
        justification = 'left',
        width,
        height,
        backgroundColor = '',
        includeCanvas = true,
        lineHeight = 1.2
    } = layerInfo;
    // Determine canvas size based on provided width/height or defaults (for bounds calculation)
    const canvasWidth = width || 400; // Default width if not provided
    const canvasHeight = height || 100; // Default height if not provided
    // Create a canvas to measure text bounds accurately (in pixels)
    const canvas = new Canvas(canvasWidth, canvasHeight);
    const ctx = canvas.getContext('2d');
    const canvasFontStyle = fontStyle.toLowerCase() === 'regular' ? 'normal' : fontStyle;
    ctx.font = `${canvasFontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`; // Font size in px for canvas measurement
    const lines = text.split('\n');
    const lineHeightPx = fontSize * lineHeight; // Line height in px
    const textWidth = Math.max(...lines.map(line => ctx.measureText(line).width));
    const textHeight = lines.length * lineHeightPx;
    // Calculate bounds in pixels (ag-psd expects boundingBox in pixels for text layers)
    const boundsLeftPx = 0; // Relative to transform origin
    const boundsTopPx = 0;
    const boundsRightPx = textWidth;
    const boundsBottomPx = textHeight;
    // Layer bounds in pixels (for PSD positioning)
    const layerLeft = left;
    const layerTop = top;
    const layerRight = left + canvasWidth;
    const layerBottom = top + canvasHeight;
    // Convert color to RGB
    const rgbColor = hexToRgb(color);
    const bgColor = backgroundColor ? hexToRgb(backgroundColor) : { r: 255, g: 255, b: 255, a: 0 }; // Default transparent
    // Map justification to Photoshop values
    const psJustification = justification === 'left' ? 'left' : justification === 'center' ? 'centerJustified' : 'right';
    // For multi-line text, create styleRuns and paragraphStyleRuns that cover each line separately if needed
    // But in standard PSD, one run for the whole text works; if issues persist, we can split by lines
    const styleRuns = [];
    const paragraphStyleRuns = [];
    let currentIndex = 0;
    lines.forEach((line, index) => {
        const lineLength = line.length + (index < lines.length - 1 ? 1 : 0); // Include \n except for last line
        if (lineLength > 0) {
            styleRuns.push({
                length: lineLength,
                style: {
                    font: {
                        name: fontFamily,
                        script: 0,
                        type: 0,
                        synthetic: 0
                    },
                    fontSize: fontSize,
                    fauxBold: false,
                    fauxItalic: false,
                    autoLeading: true,
                    leading: 0,
                    backgroundColor: bgColor,
                    horizontalScale: 1,
                    verticalScale: 1,
                    tracking: tracking,
                    autoKerning: true,
                    kerning: 0,
                    baselineShift: 0,
                    fontCaps: 0,
                    fontBaseline: 0,
                    underline: false,
                    strikethrough: false,
                    ligatures: true,
                    dLigatures: false,
                    fillColor: rgbColor,
                    strokeColor: { r: 0, g: 0, b: 0, a: 0 },
                    fillFlag: true,
                    strokeFlag: false,
                    fillFirst: true,
                    yUnderline: 0,
                }
            });
            paragraphStyleRuns.push({
                length: lineLength,
                paragraphStyle: {
                    justification: psJustification,
                    firstLineIndent: 0,
                    startIndent: 0,
                    endIndent: 0,
                    spaceBefore: 0,
                    spaceAfter: 0,
                    autoHyphenate: true,
                    hyphenatedWordSize: 6,
                    preHyphen: 2,
                    postHyphen: 2,
                    consecutiveHyphens: 8,
                    zone: 36,
                    wordSpacing: [0.8, 1, 1.33],
                    letterSpacing: [0, 0, 0],
                    glyphSpacing: [1, 1, 1],
                    autoLeading: lineHeight,
                    leadingType: 0,
                    hanging: false,
                    burasagari: false,
                    kinsokuOrder: 0,
                    everyLineComposer: false
                }
            });
        }
        currentIndex += lineLength;
    });
    // Create the complete text structure that Photoshop expects (adjusted for multi-line with per-line runs)
    const textData = {
        text: text, // Multi-line text with \n; Photoshop interprets this correctly
        transform: [1, 0, 0, 1, left, top], // Baseline origin position
        left: Math.floor(layerLeft),
        top: Math.floor(layerTop),
        right: Math.ceil(layerRight),
        bottom: Math.ceil(layerBottom),
        antiAlias: 'smooth',
        useFractionalGlyphWidths: true,
        orientation: 'horizontal',
        style: {
            font: {
                name: fontFamily,
                script: 0,
                type: 0,
                synthetic: 0,
                fontWeight: fontWeight,
                fontStyle: fontStyle,
                tracking: tracking,
                color: rgbColor,
                fontSize: fontSize,
            },
            fontSize: fontSize,
            fauxBold: false,
            fauxItalic: false,
            autoLeading: true,
            leading: 0,
            horizontalScale: 1,
            backgroundColor: bgColor,
            verticalScale: 1,
            autoKerning: true,
            kerning: 0,
            tracking: tracking,
            baselineShift: 0,
            fontCaps: 0,
            fontBaseline: 0,
            underline: false,
            strikethrough: false,
            ligatures: true,
            dLigatures: false,
            fillColor: rgbColor,
            strokeColor: { r: 0, g: 0, b: 0, a: 0 },
            fillFlag: true,
            strokeFlag: false,
            fillFirst: true,
            yUnderline: 0,
        },
        styleRuns: styleRuns, // Per-line style runs for better multi-line support
        paragraphStyle: {
            justification: psJustification,
            firstLineIndent: 0,
            startIndent: 0,
            endIndent: 0,
            spaceBefore: 0,
            spaceAfter: 0,
            autoHyphenate: true,
            hyphenatedWordSize: 6,
            preHyphen: 2,
            postHyphen: 2,
            consecutiveHyphens: 8,
            zone: 36,
            wordSpacing: [0.8, 1, 1.33],
            letterSpacing: [0, 0, 0],
            glyphSpacing: [1, 1, 1],
            autoLeading: lineHeight,
            leadingType: 0,
            hanging: false,
            burasagari: false,
            kinsokuOrder: 0,
            everyLineComposer: false
        },
        paragraphStyleRuns: paragraphStyleRuns, // Per-line paragraph runs
        bounds: {
            top: { units: 'Pixels', value: boundsTopPx },
            left: { units: 'Pixels', value: boundsLeftPx },
            right: { units: 'Pixels', value: boundsRightPx },
            bottom: { units: 'Pixels', value: boundsBottomPx }
        },
        boundingBox: {
            top: { units: 'Pixels', value: boundsTopPx },
            left: { units: 'Pixels', value: boundsLeftPx },
            right: { units: 'Pixels', value: boundsRightPx },
            bottom: { units: 'Pixels', value: boundsBottomPx }
        },
        warp: {
            style: 'none',
            value: 0,
            perspective: 0,
            perspectiveOther: 0,
            rotate: 'horizontal'
        }
    };
    // Create the layer as a text layer with full Photoshop-compatible text data
    const layer = {
        name: name,
        opacity: 1,
        blendMode: 'normal',
        type: 'text', // Text layer for editability and properties in Photoshop
        left: layerLeft,
        top: layerTop,
        right: layerRight,
        bottom: layerBottom,
        canvas: canvas, // Include canvas for reference (not serialized in PSD)
        text: textData, // Full text structure for Photoshop properties and editability
    };
    return layer;
}

Could you please guide me on how to correctly create the editable text layer object using ag-psd?

Specifically, how to handle positioning, multi-line text rendering, and ensure the text appears when opening the PSD file. Any example code or corrections to my approach would be greatly appreciated!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions