image-edit-tools 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ops/add-text.d.ts.map +1 -1
- package/dist/ops/add-text.js +31 -19
- package/dist/ops/add-text.js.map +1 -1
- package/package.json +1 -1
- package/src/ops/add-text.ts +31 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-text.d.ts","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAyB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"add-text.d.ts","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAyB,MAAM,aAAa,CAAC;AA2DxF,wBAAsB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,SAAS,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CA4HvG"}
|
package/dist/ops/add-text.js
CHANGED
|
@@ -33,25 +33,29 @@ function escapeXml(text) {
|
|
|
33
33
|
.replace(/"/g, '"')
|
|
34
34
|
.replace(/'/g, ''');
|
|
35
35
|
}
|
|
36
|
-
function getAnchorProps(anchor = 'top-left') {
|
|
36
|
+
function getAnchorProps(anchor = 'top-left', fontSize = 24) {
|
|
37
37
|
const parts = anchor.split('-');
|
|
38
38
|
const yAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
|
|
39
39
|
const xAlign = parts.length === 2 ? parts[1] : parts[0] === 'center' ? 'center' : 'left';
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
// librsvg does NOT reliably support dominant-baseline values other than 'auto' (alphabetic).
|
|
41
|
+
// Instead of relying on dominant-baseline, we compute a y-offset to position text correctly.
|
|
42
|
+
// With 'auto' (alphabetic baseline), y = text baseline (bottom of caps).
|
|
43
|
+
// To make y = text top, we shift down by ~0.8 * fontSize.
|
|
44
|
+
// To make y = text middle, we shift down by ~0.35 * fontSize.
|
|
45
|
+
let yOffset = 0;
|
|
46
|
+
if (yAlign === 'top') {
|
|
47
|
+
yOffset = Math.round(fontSize * 0.8);
|
|
48
|
+
}
|
|
49
|
+
else if (yAlign === 'middle' || yAlign === 'center') {
|
|
50
|
+
yOffset = Math.round(fontSize * 0.35);
|
|
51
|
+
}
|
|
52
|
+
// 'bottom' / 'auto' → yOffset = 0 (alphabetic baseline is already at y)
|
|
49
53
|
let textAnchor = 'start';
|
|
50
54
|
if (xAlign === 'center')
|
|
51
55
|
textAnchor = 'middle';
|
|
52
56
|
else if (xAlign === 'right')
|
|
53
57
|
textAnchor = 'end';
|
|
54
|
-
return { textAnchor,
|
|
58
|
+
return { textAnchor, yOffset };
|
|
55
59
|
}
|
|
56
60
|
export async function addText(input, options) {
|
|
57
61
|
try {
|
|
@@ -78,18 +82,21 @@ export async function addText(input, options) {
|
|
|
78
82
|
const lineHeight = layer.lineHeight ?? 1.2;
|
|
79
83
|
const totalHeight = lines.length * fontSize * lineHeight;
|
|
80
84
|
const approxMaxWidth = Math.max(...lines.map(l => l.length * fontSize * 0.6));
|
|
81
|
-
const { textAnchor,
|
|
85
|
+
const { textAnchor, yOffset } = getAnchorProps(layer.anchor, fontSize);
|
|
86
|
+
const renderY = layer.y + yOffset;
|
|
82
87
|
let align = textAnchor;
|
|
83
88
|
if (layer.align) {
|
|
84
89
|
align = layer.align === 'left' ? 'start' : layer.align === 'right' ? 'end' : 'middle';
|
|
85
90
|
}
|
|
86
|
-
|
|
91
|
+
// Always use dominant-baseline: auto (alphabetic) — the only value librsvg reliably supports
|
|
92
|
+
const style = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: auto;`;
|
|
87
93
|
let layerSvg = '';
|
|
88
94
|
if (layer.background) {
|
|
89
95
|
const bg = layer.background;
|
|
90
96
|
const pad = bg.padding ?? 0;
|
|
91
97
|
const bgOpacity = bg.opacity ?? 1.0;
|
|
92
98
|
const radius = bg.borderRadius ?? 0;
|
|
99
|
+
// Background rect is positioned relative to the *intended* y (layer.y), not renderY
|
|
93
100
|
let rectX = layer.x - pad;
|
|
94
101
|
let rectY = layer.y - pad;
|
|
95
102
|
if (textAnchor === 'middle') {
|
|
@@ -98,31 +105,36 @@ export async function addText(input, options) {
|
|
|
98
105
|
else if (textAnchor === 'end') {
|
|
99
106
|
rectX = layer.x - approxMaxWidth - pad;
|
|
100
107
|
}
|
|
101
|
-
|
|
108
|
+
// Adjust for anchor vertical alignment
|
|
109
|
+
const parts = (layer.anchor ?? 'top-left').split('-');
|
|
110
|
+
const vAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
|
|
111
|
+
if (vAlign === 'middle' || vAlign === 'center') {
|
|
102
112
|
rectY = layer.y - (totalHeight / 2) - pad;
|
|
103
113
|
}
|
|
104
|
-
else if (
|
|
114
|
+
else if (vAlign === 'bottom') {
|
|
105
115
|
rectY = layer.y - totalHeight - pad + fontSize;
|
|
106
116
|
}
|
|
107
117
|
layerSvg += `<rect x="${rectX}" y="${rectY}" width="${approxMaxWidth + pad * 2}" height="${totalHeight + pad * 2}" fill="${bg.color}" opacity="${bgOpacity}" rx="${radius}" ry="${radius}" />`;
|
|
108
118
|
}
|
|
109
|
-
layerSvg += `<text x="${layer.x}" y="${
|
|
119
|
+
layerSvg += `<text x="${layer.x}" y="${renderY}" style="${style}">`;
|
|
110
120
|
lines.forEach((line, idx) => {
|
|
111
121
|
let dy = idx === 0 ? 0 : fontSize * lineHeight;
|
|
112
122
|
layerSvg += `<tspan x="${layer.x}" dy="${dy}">${escapeXml(line)}</tspan>`;
|
|
113
123
|
});
|
|
114
124
|
layerSvg += `</text>`;
|
|
115
125
|
svgBody += `<g style="isolation: isolate">${layerSvg}</g>`;
|
|
116
|
-
// Compute bounding box for overflow detection
|
|
126
|
+
// Compute bounding box for overflow detection (using intended y, not renderY)
|
|
117
127
|
let boxX = layer.x;
|
|
118
128
|
let boxY = layer.y;
|
|
119
129
|
if (textAnchor === 'middle')
|
|
120
130
|
boxX -= approxMaxWidth / 2;
|
|
121
131
|
else if (textAnchor === 'end')
|
|
122
132
|
boxX -= approxMaxWidth;
|
|
123
|
-
|
|
133
|
+
const anchorParts = (layer.anchor ?? 'top-left').split('-');
|
|
134
|
+
const vAlignBox = anchorParts.length === 2 ? anchorParts[0] : anchorParts[0] === 'center' ? 'middle' : anchorParts[0];
|
|
135
|
+
if (vAlignBox === 'middle' || vAlignBox === 'center')
|
|
124
136
|
boxY -= totalHeight / 2;
|
|
125
|
-
else if (
|
|
137
|
+
else if (vAlignBox === 'bottom')
|
|
126
138
|
boxY -= totalHeight - fontSize;
|
|
127
139
|
const boxBottom = boxY + totalHeight;
|
|
128
140
|
const boxRight = boxX + approxMaxWidth;
|
package/dist/ops/add-text.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-text.js","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAsC,SAAS,EAAc,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,SAAS,QAAQ,CAAC,IAAY,EAAE,QAAgB,EAAE,QAAiB;IACjE,IAAI,CAAC,QAAQ;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,SAAS,GAAG,QAAQ,GAAG,GAAG,CAAC,CAAC,gBAAgB;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,WAAW,GAAG,EAAE,CAAC;IAErB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;YACzD,WAAW,GAAG,CAAC,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,CAAC;aAAM,CAAC;YACN,IAAI,WAAW;gBAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACzC,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,cAAc,CAAC,SAAqB,UAAU;
|
|
1
|
+
{"version":3,"file":"add-text.js","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/ops/add-text.ts
CHANGED
|
@@ -33,23 +33,29 @@ function escapeXml(text: string): string {
|
|
|
33
33
|
.replace(/'/g, ''');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function getAnchorProps(anchor: TextAnchor = 'top-left'): { textAnchor: string,
|
|
36
|
+
function getAnchorProps(anchor: TextAnchor = 'top-left', fontSize: number = 24): { textAnchor: string, yOffset: number } {
|
|
37
37
|
const parts = anchor.split('-');
|
|
38
38
|
const yAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
|
|
39
39
|
const xAlign = parts.length === 2 ? parts[1] : parts[0] === 'center' ? 'center' : 'left';
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
41
|
+
// librsvg does NOT reliably support dominant-baseline values other than 'auto' (alphabetic).
|
|
42
|
+
// Instead of relying on dominant-baseline, we compute a y-offset to position text correctly.
|
|
43
|
+
// With 'auto' (alphabetic baseline), y = text baseline (bottom of caps).
|
|
44
|
+
// To make y = text top, we shift down by ~0.8 * fontSize.
|
|
45
|
+
// To make y = text middle, we shift down by ~0.35 * fontSize.
|
|
46
|
+
let yOffset = 0;
|
|
47
|
+
if (yAlign === 'top') {
|
|
48
|
+
yOffset = Math.round(fontSize * 0.8);
|
|
49
|
+
} else if (yAlign === 'middle' || yAlign === 'center') {
|
|
50
|
+
yOffset = Math.round(fontSize * 0.35);
|
|
51
|
+
}
|
|
52
|
+
// 'bottom' / 'auto' → yOffset = 0 (alphabetic baseline is already at y)
|
|
47
53
|
|
|
48
54
|
let textAnchor = 'start';
|
|
49
55
|
if (xAlign === 'center') textAnchor = 'middle';
|
|
50
56
|
else if (xAlign === 'right') textAnchor = 'end';
|
|
51
57
|
|
|
52
|
-
return { textAnchor,
|
|
58
|
+
return { textAnchor, yOffset };
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
export async function addText(input: ImageInput, options: { layers: TextLayer[] }): Promise<ImageResult> {
|
|
@@ -82,14 +88,16 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
82
88
|
const totalHeight = lines.length * fontSize * lineHeight;
|
|
83
89
|
const approxMaxWidth = Math.max(...lines.map(l => l.length * fontSize * 0.6));
|
|
84
90
|
|
|
85
|
-
const { textAnchor,
|
|
91
|
+
const { textAnchor, yOffset } = getAnchorProps(layer.anchor, fontSize);
|
|
92
|
+
const renderY = layer.y + yOffset;
|
|
86
93
|
|
|
87
94
|
let align = textAnchor;
|
|
88
95
|
if (layer.align) {
|
|
89
96
|
align = layer.align === 'left' ? 'start' : layer.align === 'right' ? 'end' : 'middle';
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
|
|
99
|
+
// Always use dominant-baseline: auto (alphabetic) — the only value librsvg reliably supports
|
|
100
|
+
const style = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: auto;`;
|
|
93
101
|
|
|
94
102
|
let layerSvg = '';
|
|
95
103
|
|
|
@@ -99,6 +107,7 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
99
107
|
const bgOpacity = bg.opacity ?? 1.0;
|
|
100
108
|
const radius = bg.borderRadius ?? 0;
|
|
101
109
|
|
|
110
|
+
// Background rect is positioned relative to the *intended* y (layer.y), not renderY
|
|
102
111
|
let rectX = layer.x - pad;
|
|
103
112
|
let rectY = layer.y - pad;
|
|
104
113
|
|
|
@@ -108,16 +117,19 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
108
117
|
rectX = layer.x - approxMaxWidth - pad;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
// Adjust for anchor vertical alignment
|
|
121
|
+
const parts = (layer.anchor ?? 'top-left').split('-');
|
|
122
|
+
const vAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
|
|
123
|
+
if (vAlign === 'middle' || vAlign === 'center') {
|
|
112
124
|
rectY = layer.y - (totalHeight / 2) - pad;
|
|
113
|
-
} else if (
|
|
125
|
+
} else if (vAlign === 'bottom') {
|
|
114
126
|
rectY = layer.y - totalHeight - pad + fontSize;
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
layerSvg += `<rect x="${rectX}" y="${rectY}" width="${approxMaxWidth + pad * 2}" height="${totalHeight + pad * 2}" fill="${bg.color}" opacity="${bgOpacity}" rx="${radius}" ry="${radius}" />`;
|
|
118
130
|
}
|
|
119
131
|
|
|
120
|
-
layerSvg += `<text x="${layer.x}" y="${
|
|
132
|
+
layerSvg += `<text x="${layer.x}" y="${renderY}" style="${style}">`;
|
|
121
133
|
lines.forEach((line, idx) => {
|
|
122
134
|
let dy = idx === 0 ? 0 : fontSize * lineHeight;
|
|
123
135
|
layerSvg += `<tspan x="${layer.x}" dy="${dy}">${escapeXml(line)}</tspan>`;
|
|
@@ -126,13 +138,16 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
126
138
|
|
|
127
139
|
svgBody += `<g style="isolation: isolate">${layerSvg}</g>`;
|
|
128
140
|
|
|
129
|
-
// Compute bounding box for overflow detection
|
|
141
|
+
// Compute bounding box for overflow detection (using intended y, not renderY)
|
|
130
142
|
let boxX = layer.x;
|
|
131
143
|
let boxY = layer.y;
|
|
132
144
|
if (textAnchor === 'middle') boxX -= approxMaxWidth / 2;
|
|
133
145
|
else if (textAnchor === 'end') boxX -= approxMaxWidth;
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
|
|
147
|
+
const anchorParts = (layer.anchor ?? 'top-left').split('-');
|
|
148
|
+
const vAlignBox = anchorParts.length === 2 ? anchorParts[0] : anchorParts[0] === 'center' ? 'middle' : anchorParts[0];
|
|
149
|
+
if (vAlignBox === 'middle' || vAlignBox === 'center') boxY -= totalHeight / 2;
|
|
150
|
+
else if (vAlignBox === 'bottom') boxY -= totalHeight - fontSize;
|
|
136
151
|
|
|
137
152
|
const boxBottom = boxY + totalHeight;
|
|
138
153
|
const boxRight = boxX + approxMaxWidth;
|