-
Notifications
You must be signed in to change notification settings - Fork 82
Description
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!