@image-marker/web 0.1.0

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.
Files changed (124) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +100 -0
  3. package/api-contract.json +224 -0
  4. package/lib/commonjs/core/batch.d.ts +32 -0
  5. package/lib/commonjs/core/batch.js +85 -0
  6. package/lib/commonjs/core/content-credentials.d.ts +53 -0
  7. package/lib/commonjs/core/content-credentials.js +96 -0
  8. package/lib/commonjs/core/image-info.d.ts +30 -0
  9. package/lib/commonjs/core/image-info.js +348 -0
  10. package/lib/commonjs/core/index.d.ts +1014 -0
  11. package/lib/commonjs/core/index.js +92 -0
  12. package/lib/commonjs/core/invisible-watermark.d.ts +102 -0
  13. package/lib/commonjs/core/invisible-watermark.js +913 -0
  14. package/lib/commonjs/core/job.d.ts +57 -0
  15. package/lib/commonjs/core/job.js +151 -0
  16. package/lib/commonjs/core/layout.d.ts +19 -0
  17. package/lib/commonjs/core/layout.js +75 -0
  18. package/lib/commonjs/core/marker.d.ts +1 -0
  19. package/lib/commonjs/core/marker.js +11 -0
  20. package/lib/commonjs/core/recipe.d.ts +44 -0
  21. package/lib/commonjs/core/recipe.js +175 -0
  22. package/lib/commonjs/core/result.d.ts +34 -0
  23. package/lib/commonjs/core/result.js +41 -0
  24. package/lib/commonjs/core/trace-runtime.d.ts +43 -0
  25. package/lib/commonjs/core/trace-runtime.js +92 -0
  26. package/lib/commonjs/core/validate.d.ts +4 -0
  27. package/lib/commonjs/core/validate.js +192 -0
  28. package/lib/commonjs/core/web/browser.d.ts +105 -0
  29. package/lib/commonjs/core/web/browser.js +434 -0
  30. package/lib/commonjs/core/web/helpers.d.ts +41 -0
  31. package/lib/commonjs/core/web/helpers.js +264 -0
  32. package/lib/commonjs/core/web/index.d.ts +59 -0
  33. package/lib/commonjs/core/web/index.js +724 -0
  34. package/lib/commonjs/core/web/invisible-worker-client.d.ts +13 -0
  35. package/lib/commonjs/core/web/invisible-worker-client.js +140 -0
  36. package/lib/commonjs/core/web/renderer.d.ts +20 -0
  37. package/lib/commonjs/core/web/renderer.js +665 -0
  38. package/lib/commonjs/core/web/runtime.d.ts +106 -0
  39. package/lib/commonjs/core/web/runtime.js +2 -0
  40. package/lib/commonjs/editor/adapter.d.ts +26 -0
  41. package/lib/commonjs/editor/adapter.js +102 -0
  42. package/lib/commonjs/editor/controller.d.ts +106 -0
  43. package/lib/commonjs/editor/controller.js +919 -0
  44. package/lib/commonjs/editor/core-contract.d.ts +2 -0
  45. package/lib/commonjs/editor/core-contract.js +23 -0
  46. package/lib/commonjs/editor/geometry.d.ts +8 -0
  47. package/lib/commonjs/editor/geometry.js +109 -0
  48. package/lib/commonjs/editor/headless.d.ts +5 -0
  49. package/lib/commonjs/editor/headless.js +22 -0
  50. package/lib/commonjs/editor/presets.d.ts +19 -0
  51. package/lib/commonjs/editor/presets.js +64 -0
  52. package/lib/commonjs/editor/projection.d.ts +12 -0
  53. package/lib/commonjs/editor/projection.js +195 -0
  54. package/lib/commonjs/editor/types.d.ts +166 -0
  55. package/lib/commonjs/editor/types.js +2 -0
  56. package/lib/commonjs/editor-adapter.d.ts +7 -0
  57. package/lib/commonjs/editor-adapter.js +17 -0
  58. package/lib/commonjs/headless.d.ts +2 -0
  59. package/lib/commonjs/headless.js +26 -0
  60. package/lib/commonjs/index.d.ts +5 -0
  61. package/lib/commonjs/index.js +27 -0
  62. package/lib/commonjs/package.json +1 -0
  63. package/lib/module/core/batch.d.ts +32 -0
  64. package/lib/module/core/batch.js +82 -0
  65. package/lib/module/core/content-credentials.d.ts +53 -0
  66. package/lib/module/core/content-credentials.js +92 -0
  67. package/lib/module/core/image-info.d.ts +30 -0
  68. package/lib/module/core/image-info.js +343 -0
  69. package/lib/module/core/index.d.ts +1014 -0
  70. package/lib/module/core/index.js +56 -0
  71. package/lib/module/core/invisible-watermark.d.ts +102 -0
  72. package/lib/module/core/invisible-watermark.js +897 -0
  73. package/lib/module/core/job.d.ts +57 -0
  74. package/lib/module/core/job.js +143 -0
  75. package/lib/module/core/layout.d.ts +19 -0
  76. package/lib/module/core/layout.js +70 -0
  77. package/lib/module/core/marker.d.ts +1 -0
  78. package/lib/module/core/marker.js +4 -0
  79. package/lib/module/core/recipe.d.ts +44 -0
  80. package/lib/module/core/recipe.js +169 -0
  81. package/lib/module/core/result.d.ts +34 -0
  82. package/lib/module/core/result.js +38 -0
  83. package/lib/module/core/trace-runtime.d.ts +43 -0
  84. package/lib/module/core/trace-runtime.js +89 -0
  85. package/lib/module/core/validate.d.ts +4 -0
  86. package/lib/module/core/validate.js +187 -0
  87. package/lib/module/core/web/browser.d.ts +105 -0
  88. package/lib/module/core/web/browser.js +427 -0
  89. package/lib/module/core/web/helpers.d.ts +41 -0
  90. package/lib/module/core/web/helpers.js +251 -0
  91. package/lib/module/core/web/index.d.ts +59 -0
  92. package/lib/module/core/web/index.js +709 -0
  93. package/lib/module/core/web/invisible-worker-client.d.ts +13 -0
  94. package/lib/module/core/web/invisible-worker-client.js +137 -0
  95. package/lib/module/core/web/renderer.d.ts +20 -0
  96. package/lib/module/core/web/renderer.js +661 -0
  97. package/lib/module/core/web/runtime.d.ts +106 -0
  98. package/lib/module/core/web/runtime.js +1 -0
  99. package/lib/module/editor/adapter.d.ts +26 -0
  100. package/lib/module/editor/adapter.js +99 -0
  101. package/lib/module/editor/controller.d.ts +106 -0
  102. package/lib/module/editor/controller.js +915 -0
  103. package/lib/module/editor/core-contract.d.ts +2 -0
  104. package/lib/module/editor/core-contract.js +2 -0
  105. package/lib/module/editor/geometry.d.ts +8 -0
  106. package/lib/module/editor/geometry.js +103 -0
  107. package/lib/module/editor/headless.d.ts +5 -0
  108. package/lib/module/editor/headless.js +4 -0
  109. package/lib/module/editor/presets.d.ts +19 -0
  110. package/lib/module/editor/presets.js +57 -0
  111. package/lib/module/editor/projection.d.ts +12 -0
  112. package/lib/module/editor/projection.js +188 -0
  113. package/lib/module/editor/types.d.ts +166 -0
  114. package/lib/module/editor/types.js +1 -0
  115. package/lib/module/editor-adapter.d.ts +7 -0
  116. package/lib/module/editor-adapter.js +10 -0
  117. package/lib/module/headless.d.ts +2 -0
  118. package/lib/module/headless.js +2 -0
  119. package/lib/module/index.d.ts +5 -0
  120. package/lib/module/index.js +3 -0
  121. package/lib/module/package.json +1 -0
  122. package/lib/source-manifest.json +34 -0
  123. package/lib/worker/invisible-watermark.js +1 -0
  124. package/package.json +85 -0
@@ -0,0 +1,661 @@
1
+ import { createWebCanvas, getCanvasContext, getSecurityErrorMessage, loadWebImage, } from './browser.js';
2
+ import { degreesToRadians, encodeCanvas, fitSizeWithinMax, getExpandedCanvasSize, getRotatedBounds, normalizeOutputFormat, normalizeQuality, resolveAnchoredPosition, resolveSpreadValue, } from './helpers.js';
3
+ import { resolveTilePlacements } from '../layout.js';
4
+ function throwIfAborted(signal) {
5
+ if (!signal?.aborted)
6
+ return;
7
+ const error = new Error('Web canvas operation was aborted.');
8
+ error.name = 'AbortError';
9
+ throw error;
10
+ }
11
+ function assertFinitePositive(value, label) {
12
+ if (!Number.isFinite(value) || value <= 0) {
13
+ throw new Error(`${label} must be a finite number greater than 0.`);
14
+ }
15
+ return value;
16
+ }
17
+ function resolveScale(value, label) {
18
+ return assertFinitePositive(value ?? 1, label);
19
+ }
20
+ function resolveAlpha(value, label) {
21
+ const alpha = value ?? 1;
22
+ if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) {
23
+ throw new Error(`${label} must be a finite number between 0 and 1.`);
24
+ }
25
+ return alpha;
26
+ }
27
+ const WEB_BLEND_MODES = {
28
+ normal: 'source-over',
29
+ multiply: 'multiply',
30
+ screen: 'screen',
31
+ overlay: 'overlay',
32
+ darken: 'darken',
33
+ lighten: 'lighten',
34
+ };
35
+ function resolveBlendMode(value) {
36
+ return WEB_BLEND_MODES[value ?? 'normal'];
37
+ }
38
+ function resolveRotation(value, label) {
39
+ const rotation = value ?? 0;
40
+ if (!Number.isFinite(rotation)) {
41
+ throw new Error(`${label} must be a finite number.`);
42
+ }
43
+ return rotation;
44
+ }
45
+ function escapeFontFamily(fontFamily) {
46
+ return fontFamily.replace(/["\\]/g, '\\$&');
47
+ }
48
+ function createFont(style, fontSize) {
49
+ const italic = style?.italic ? 'italic ' : '';
50
+ const weight = style?.bold ? '700 ' : '400 ';
51
+ const families = [style?.fontName, ...(style?.fontFallbacks ?? [])]
52
+ .filter((family) => Boolean(family))
53
+ .map((family) => `"${escapeFontFamily(family)}"`);
54
+ const family = [...families, 'sans-serif'].join(', ');
55
+ return `${italic}${weight}${fontSize}px ${family}`;
56
+ }
57
+ function resolveFontSize(style, canvasWidth) {
58
+ const fontSize = style?.fontSizeRatio !== undefined
59
+ ? canvasWidth * style.fontSizeRatio
60
+ : style?.fontSize ?? 14;
61
+ return assertFinitePositive(fontSize, 'text font size');
62
+ }
63
+ function metricHeight(metrics, fontSize) {
64
+ const ascent = metrics.actualBoundingBoxAscent ??
65
+ metrics.fontBoundingBoxAscent ??
66
+ fontSize * 0.8;
67
+ const descent = metrics.actualBoundingBoxDescent ??
68
+ metrics.fontBoundingBoxDescent ??
69
+ fontSize * 0.2;
70
+ return Math.max(ascent + descent, fontSize);
71
+ }
72
+ function measureTextWidth(context, text, letterSpacing) {
73
+ const characters = Array.from(text);
74
+ return Math.max(context.measureText(text).width +
75
+ Math.max(characters.length - 1, 0) * letterSpacing, 0);
76
+ }
77
+ function splitOversizedToken(context, token, maxWidth, letterSpacing) {
78
+ const chunks = [];
79
+ let chunk = '';
80
+ for (const character of Array.from(token)) {
81
+ const candidate = chunk + character;
82
+ if (chunk &&
83
+ measureTextWidth(context, candidate, letterSpacing) > maxWidth) {
84
+ chunks.push(chunk);
85
+ chunk = character;
86
+ }
87
+ else {
88
+ chunk = candidate;
89
+ }
90
+ }
91
+ if (chunk || chunks.length === 0) {
92
+ chunks.push(chunk);
93
+ }
94
+ return chunks;
95
+ }
96
+ function wrapParagraph(context, paragraph, maxWidth, wrap, letterSpacing) {
97
+ if (wrap === 'none' ||
98
+ measureTextWidth(context, paragraph, letterSpacing) <= maxWidth) {
99
+ return [paragraph];
100
+ }
101
+ if (wrap === 'character') {
102
+ return splitOversizedToken(context, paragraph, maxWidth, letterSpacing);
103
+ }
104
+ const lines = [];
105
+ const tokens = paragraph.match(/\s+|\S+/gu) ?? [''];
106
+ let current = '';
107
+ for (const token of tokens) {
108
+ if (/^\s+$/u.test(token)) {
109
+ if (current || lines.length === 0) {
110
+ current += token;
111
+ }
112
+ continue;
113
+ }
114
+ const candidate = current + token;
115
+ if (measureTextWidth(context, candidate, letterSpacing) <= maxWidth) {
116
+ current = candidate;
117
+ continue;
118
+ }
119
+ const completedLine = current.trimEnd();
120
+ if (completedLine) {
121
+ lines.push(completedLine);
122
+ }
123
+ current = '';
124
+ if (measureTextWidth(context, token, letterSpacing) <= maxWidth) {
125
+ current = token;
126
+ continue;
127
+ }
128
+ const chunks = splitOversizedToken(context, token, maxWidth, letterSpacing);
129
+ lines.push(...chunks.slice(0, -1));
130
+ current = chunks[chunks.length - 1] ?? '';
131
+ }
132
+ const finalLine = current.trimEnd();
133
+ if (finalLine || lines.length === 0) {
134
+ lines.push(finalLine);
135
+ }
136
+ return lines;
137
+ }
138
+ function ellipsizeLine(context, value, maxWidth, letterSpacing) {
139
+ const ellipsis = '…';
140
+ if (measureTextWidth(context, ellipsis, letterSpacing) > maxWidth) {
141
+ return '';
142
+ }
143
+ let characters = Array.from(value.trimEnd());
144
+ while (characters.length > 0 &&
145
+ measureTextWidth(context, `${characters.join('')}${ellipsis}`, letterSpacing) > maxWidth) {
146
+ characters = characters.slice(0, -1);
147
+ }
148
+ return `${characters.join('')}${ellipsis}`;
149
+ }
150
+ function measureTextLayout(context, text, style, canvasWidth) {
151
+ const fontSize = resolveFontSize(style, canvasWidth);
152
+ context.font = createFont(style, fontSize);
153
+ const maxWidth = assertFinitePositive(resolveSpreadValue(style?.maxWidth, canvasWidth) ?? canvasWidth, 'text maxWidth');
154
+ const letterSpacing = style?.letterSpacing ?? 0;
155
+ const wrap = style?.wrap ?? 'word';
156
+ const rawLines = text
157
+ .split(/\r\n?|\n/)
158
+ .flatMap((line) => wrapParagraph(context, line, maxWidth, wrap, letterSpacing));
159
+ const maxLines = style?.maxLines ?? Number.POSITIVE_INFINITY;
160
+ const omittedLines = rawLines.length > maxLines;
161
+ const visibleLines = rawLines.slice(0, maxLines);
162
+ const lineOverflow = visibleLines.some((line) => measureTextWidth(context, line, letterSpacing) > maxWidth);
163
+ if (style?.overflow === 'ellipsis' &&
164
+ (omittedLines || lineOverflow) &&
165
+ visibleLines.length > 0) {
166
+ const finalIndex = visibleLines.length - 1;
167
+ visibleLines[finalIndex] = ellipsizeLine(context, visibleLines[finalIndex] ?? '', maxWidth, letterSpacing);
168
+ }
169
+ const lines = visibleLines.map((line) => ({
170
+ text: line,
171
+ width: Math.min(measureTextWidth(context, line, letterSpacing), maxWidth),
172
+ }));
173
+ const representative = context.measureText(lines.reduce((widest, line) => (line.width > widest.width ? line : widest), lines[0] ?? { text: '', width: 0 }).text || 'Mg');
174
+ const naturalLineHeight = metricHeight(representative, fontSize);
175
+ const lineHeight = style?.lineHeight ?? naturalLineHeight;
176
+ return {
177
+ lines,
178
+ width: Math.max(...lines.map((line) => line.width), 0),
179
+ height: Math.max(lineHeight * lines.length, 1),
180
+ lineHeight,
181
+ fontSize,
182
+ maxWidth,
183
+ clipped: omittedLines || lineOverflow,
184
+ letterSpacing,
185
+ direction: style?.direction ?? 'auto',
186
+ };
187
+ }
188
+ function parsePaddingTokens(value) {
189
+ return typeof value === 'number'
190
+ ? [String(value)]
191
+ : value.trim().split(/\s+/).filter(Boolean).slice(0, 4);
192
+ }
193
+ function resolveAxisValue(value, relativeTo) {
194
+ const resolved = resolveSpreadValue(value, relativeTo);
195
+ return resolved === undefined ? undefined : Math.max(resolved, 0);
196
+ }
197
+ function resolvePadding(style, canvas) {
198
+ const padding = { top: 0, right: 0, bottom: 0, left: 0 };
199
+ if (style.padding !== undefined) {
200
+ const values = parsePaddingTokens(style.padding);
201
+ const [first = '0', second = first, third = first, fourth = second] = values;
202
+ if (values.length === 1) {
203
+ padding.top = resolveAxisValue(first, canvas.height) ?? 0;
204
+ padding.right = resolveAxisValue(first, canvas.width) ?? 0;
205
+ padding.bottom = resolveAxisValue(first, canvas.height) ?? 0;
206
+ padding.left = resolveAxisValue(first, canvas.width) ?? 0;
207
+ }
208
+ else if (values.length === 2) {
209
+ padding.top = resolveAxisValue(first, canvas.height) ?? 0;
210
+ padding.right = resolveAxisValue(second, canvas.width) ?? 0;
211
+ padding.bottom = padding.top;
212
+ padding.left = padding.right;
213
+ }
214
+ else if (values.length === 3) {
215
+ padding.top = resolveAxisValue(first, canvas.height) ?? 0;
216
+ padding.right = resolveAxisValue(second, canvas.width) ?? 0;
217
+ padding.bottom = resolveAxisValue(third, canvas.height) ?? 0;
218
+ padding.left = padding.right;
219
+ }
220
+ else {
221
+ padding.top = resolveAxisValue(first, canvas.height) ?? 0;
222
+ padding.right = resolveAxisValue(second, canvas.width) ?? 0;
223
+ padding.bottom = resolveAxisValue(third, canvas.height) ?? 0;
224
+ padding.left = resolveAxisValue(fourth, canvas.width) ?? 0;
225
+ }
226
+ }
227
+ const horizontal = resolveAxisValue(style.paddingHorizontal, canvas.width) ??
228
+ resolveAxisValue(style.paddingX, canvas.width);
229
+ const vertical = resolveAxisValue(style.paddingVertical, canvas.height) ??
230
+ resolveAxisValue(style.paddingY, canvas.height);
231
+ if (horizontal !== undefined) {
232
+ padding.left = horizontal;
233
+ padding.right = horizontal;
234
+ }
235
+ if (vertical !== undefined) {
236
+ padding.top = vertical;
237
+ padding.bottom = vertical;
238
+ }
239
+ padding.left =
240
+ resolveAxisValue(style.paddingLeft, canvas.width) ?? padding.left;
241
+ padding.right =
242
+ resolveAxisValue(style.paddingRight, canvas.width) ?? padding.right;
243
+ padding.top =
244
+ resolveAxisValue(style.paddingTop, canvas.height) ?? padding.top;
245
+ padding.bottom =
246
+ resolveAxisValue(style.paddingBottom, canvas.height) ?? padding.bottom;
247
+ return padding;
248
+ }
249
+ function resolveRadiusValue(value, rect) {
250
+ return {
251
+ x: Math.max(resolveSpreadValue(value?.x, rect.width) ?? 0, 0),
252
+ y: Math.max(resolveSpreadValue(value?.y, rect.height) ?? 0, 0),
253
+ };
254
+ }
255
+ function resolveCornerRadii(cornerRadius, rect) {
256
+ const all = resolveRadiusValue(cornerRadius?.all, rect);
257
+ const resolveCorner = (value) => {
258
+ const resolved = value ? resolveRadiusValue(value, rect) : all;
259
+ return {
260
+ x: Math.min(resolved.x, rect.width / 2),
261
+ y: Math.min(resolved.y, rect.height / 2),
262
+ };
263
+ };
264
+ return {
265
+ topLeft: resolveCorner(cornerRadius?.topLeft),
266
+ topRight: resolveCorner(cornerRadius?.topRight),
267
+ bottomRight: resolveCorner(cornerRadius?.bottomRight),
268
+ bottomLeft: resolveCorner(cornerRadius?.bottomLeft),
269
+ };
270
+ }
271
+ function roundedRectPath(context, x, y, width, height, cornerRadius) {
272
+ const radii = resolveCornerRadii(cornerRadius, { width, height });
273
+ context.beginPath();
274
+ context.moveTo(x + radii.topLeft.x, y);
275
+ context.lineTo(x + width - radii.topRight.x, y);
276
+ context.quadraticCurveTo(x + width, y, x + width, y + radii.topRight.y);
277
+ context.lineTo(x + width, y + height - radii.bottomRight.y);
278
+ context.quadraticCurveTo(x + width, y + height, x + width - radii.bottomRight.x, y + height);
279
+ context.lineTo(x + radii.bottomLeft.x, y + height);
280
+ context.quadraticCurveTo(x, y + height, x, y + height - radii.bottomLeft.y);
281
+ context.lineTo(x, y + radii.topLeft.y);
282
+ context.quadraticCurveTo(x, y, x + radii.topLeft.x, y);
283
+ context.closePath();
284
+ }
285
+ function drawTextBackground(context, style, origin, layout, canvas, outlineInset = 0) {
286
+ const padding = resolvePadding(style, canvas);
287
+ let x = origin.x - outlineInset - padding.left;
288
+ let y = origin.y - outlineInset - padding.top;
289
+ let width = layout.width + outlineInset * 2 + padding.left + padding.right;
290
+ let height = layout.height + outlineInset * 2 + padding.top + padding.bottom;
291
+ if (style.type === 'stretchX') {
292
+ x = 0;
293
+ width = canvas.width;
294
+ }
295
+ else if (style.type === 'stretchY') {
296
+ y = 0;
297
+ height = canvas.height;
298
+ }
299
+ context.fillStyle = style.color;
300
+ roundedRectPath(context, x, y, width, height, style.cornerRadius);
301
+ context.fill();
302
+ }
303
+ function getAlignedTextX(alignment, originX, layoutWidth) {
304
+ if (alignment === 'center') {
305
+ return originX + layoutWidth / 2;
306
+ }
307
+ if (alignment === 'right') {
308
+ return originX + layoutWidth;
309
+ }
310
+ return originX;
311
+ }
312
+ function getLineStartX(alignment, textX, lineWidth) {
313
+ if (alignment === 'center') {
314
+ return textX - lineWidth / 2;
315
+ }
316
+ if (alignment === 'right') {
317
+ return textX - lineWidth;
318
+ }
319
+ return textX;
320
+ }
321
+ function drawTextDecorations(context, options, layout, textX, lineIndex, lineWidth) {
322
+ if (!options.style?.underline && !options.style?.strikeThrough) {
323
+ return;
324
+ }
325
+ const lineStart = getLineStartX(options.style.textAlign, textX, lineWidth);
326
+ const top = lineIndex * layout.lineHeight;
327
+ context.beginPath();
328
+ if (options.style.underline) {
329
+ context.moveTo(lineStart, top + layout.fontSize);
330
+ context.lineTo(lineStart + lineWidth, top + layout.fontSize);
331
+ }
332
+ if (options.style.strikeThrough) {
333
+ context.moveTo(lineStart, top + layout.fontSize * 0.52);
334
+ context.lineTo(lineStart + lineWidth, top + layout.fontSize * 0.52);
335
+ }
336
+ const stroke = context;
337
+ stroke.lineWidth = Math.max(layout.fontSize / 16, 1);
338
+ stroke.strokeStyle = options.style.color ?? '#000000';
339
+ stroke.stroke();
340
+ }
341
+ function drawSpacedText(context, method, text, textX, y, alignment, lineWidth, letterSpacing, direction) {
342
+ if (letterSpacing === 0 || 'letterSpacing' in context) {
343
+ if ('letterSpacing' in context) {
344
+ context.letterSpacing = `${letterSpacing}px`;
345
+ }
346
+ context[method](text, textX, y);
347
+ return;
348
+ }
349
+ const previousAlignment = context.textAlign;
350
+ context.textAlign = 'left';
351
+ const characters = Array.from(text);
352
+ if (direction === 'rtl') {
353
+ characters.reverse();
354
+ }
355
+ let cursor = getLineStartX(alignment, textX, lineWidth);
356
+ characters.forEach((character, index) => {
357
+ context[method](character, cursor, y);
358
+ cursor += context.measureText(character).width;
359
+ if (index < characters.length - 1) {
360
+ cursor += letterSpacing;
361
+ }
362
+ });
363
+ context.textAlign = previousAlignment;
364
+ }
365
+ function drawTextAtPosition(context, options, canvas, layout, visualPosition, rotation) {
366
+ const style = options.style;
367
+ const strokeWidth = style?.strokeStyle?.width ?? 0;
368
+ const outlineInset = strokeWidth / 2;
369
+ const visualSize = {
370
+ width: layout.width + outlineInset * 2,
371
+ height: layout.height + outlineInset * 2,
372
+ };
373
+ const position = {
374
+ x: visualPosition.x + outlineInset,
375
+ y: visualPosition.y + outlineInset,
376
+ };
377
+ context.save();
378
+ try {
379
+ if (rotation !== 0) {
380
+ context.translate(visualPosition.x + visualSize.width / 2, visualPosition.y + visualSize.height / 2);
381
+ context.rotate(degreesToRadians(rotation));
382
+ context.translate(-(visualPosition.x + visualSize.width / 2), -(visualPosition.y + visualSize.height / 2));
383
+ }
384
+ if (style?.textBackgroundStyle) {
385
+ drawTextBackground(context, style.textBackgroundStyle, position, layout, canvas, outlineInset);
386
+ }
387
+ context.font = createFont(style, layout.fontSize);
388
+ context.fillStyle = style?.color ?? '#000000';
389
+ context.textAlign = style?.textAlign ?? 'left';
390
+ context.textBaseline = 'top';
391
+ if ('direction' in context) {
392
+ context.direction =
393
+ layout.direction === 'auto' ? 'inherit' : layout.direction;
394
+ }
395
+ context.shadowBlur = style?.shadowStyle?.radius ?? 0;
396
+ context.shadowOffsetX = style?.shadowStyle?.dx ?? 0;
397
+ context.shadowOffsetY = style?.shadowStyle?.dy ?? 0;
398
+ context.shadowColor = style?.shadowStyle?.color ?? 'transparent';
399
+ context.lineJoin = 'round';
400
+ context.lineWidth = strokeWidth;
401
+ context.strokeStyle = style?.strokeStyle?.color ?? '#000000';
402
+ const textX = getAlignedTextX(style?.textAlign, position.x, layout.width);
403
+ if (style?.skewX) {
404
+ context.translate(position.x, position.y);
405
+ context.transform(1, 0, style.skewX, 1, 0, 0);
406
+ context.translate(-position.x, -position.y);
407
+ }
408
+ if (layout.clipped) {
409
+ context.beginPath();
410
+ context.rect(position.x, position.y, layout.width, layout.height);
411
+ context.clip();
412
+ }
413
+ layout.lines.forEach((line, lineIndex) => {
414
+ const lineY = position.y + lineIndex * layout.lineHeight;
415
+ if (strokeWidth > 0) {
416
+ drawSpacedText(context, 'strokeText', line.text, textX, lineY, style?.textAlign, line.width, layout.letterSpacing, layout.direction);
417
+ const shadowColor = context.shadowColor;
418
+ context.shadowColor = 'transparent';
419
+ drawSpacedText(context, 'fillText', line.text, textX, lineY, style?.textAlign, line.width, layout.letterSpacing, layout.direction);
420
+ context.shadowColor = shadowColor;
421
+ }
422
+ else {
423
+ drawSpacedText(context, 'fillText', line.text, textX, lineY, style?.textAlign, line.width, layout.letterSpacing, layout.direction);
424
+ }
425
+ context.save();
426
+ context.translate(position.x, position.y);
427
+ drawTextDecorations(context, options, layout, textX - position.x, lineIndex, line.width);
428
+ context.restore();
429
+ });
430
+ }
431
+ finally {
432
+ context.restore();
433
+ }
434
+ }
435
+ function drawTextLayerContent(context, options, canvas) {
436
+ const style = options.style;
437
+ const textLayout = measureTextLayout(context, options.text, style, canvas.width);
438
+ const rotation = resolveRotation(style?.rotate, 'text rotation');
439
+ const strokeWidth = style?.strokeStyle?.width ?? 0;
440
+ const outlineInset = strokeWidth / 2;
441
+ const visualSize = {
442
+ width: textLayout.width + outlineInset * 2,
443
+ height: textLayout.height + outlineInset * 2,
444
+ };
445
+ if (options.layout?.type === 'tile') {
446
+ const rotatedSize = getRotatedBounds(visualSize, rotation);
447
+ const placements = resolveTilePlacements(options.layout, canvas, rotatedSize);
448
+ const originInset = {
449
+ x: (rotatedSize.width - visualSize.width) / 2,
450
+ y: (rotatedSize.height - visualSize.height) / 2,
451
+ };
452
+ placements.forEach((placement) => {
453
+ drawTextAtPosition(context, options, canvas, textLayout, {
454
+ x: placement.x + originInset.x,
455
+ y: placement.y + originInset.y,
456
+ }, rotation);
457
+ });
458
+ return;
459
+ }
460
+ const position = resolveAnchoredPosition(options.position, canvas, visualSize, 20);
461
+ drawTextAtPosition(context, options, canvas, textLayout, position, rotation);
462
+ }
463
+ function drawTextLayer(context, options, canvas) {
464
+ const previousAlpha = context.globalAlpha;
465
+ const previousBlendMode = context.globalCompositeOperation;
466
+ const alpha = resolveAlpha(options.alpha, 'watermark text alpha');
467
+ context.save();
468
+ try {
469
+ context.globalAlpha = previousAlpha * alpha;
470
+ context.globalCompositeOperation = resolveBlendMode(options.blendMode);
471
+ drawTextLayerContent(context, options, canvas);
472
+ }
473
+ finally {
474
+ context.globalAlpha = previousAlpha;
475
+ context.globalCompositeOperation = previousBlendMode;
476
+ context.restore();
477
+ }
478
+ }
479
+ function fullSourceBounds(image) {
480
+ return { x: 0, y: 0, width: image.width, height: image.height };
481
+ }
482
+ function getVisibleSourceBounds(image, runtime) {
483
+ throwIfAborted(runtime?.signal);
484
+ const canvas = createWebCanvas(image.width, image.height, runtime?.resources);
485
+ const context = getCanvasContext(canvas);
486
+ context.drawImage(image.image, 0, 0);
487
+ let pixels;
488
+ try {
489
+ pixels = context.getImageData(0, 0, image.width, image.height).data;
490
+ }
491
+ catch (error) {
492
+ const name = error && typeof error === 'object' && 'name' in error
493
+ ? String(error.name)
494
+ : '';
495
+ if (name === 'SecurityError') {
496
+ throw new Error(getSecurityErrorMessage('trim transparent padding'));
497
+ }
498
+ throw error;
499
+ }
500
+ let left = image.width;
501
+ let top = image.height;
502
+ let right = -1;
503
+ let bottom = -1;
504
+ for (let y = 0; y < image.height; y += 1) {
505
+ for (let x = 0; x < image.width; x += 1) {
506
+ const alpha = pixels[(y * image.width + x) * 4 + 3] ?? 0;
507
+ if (alpha > 0) {
508
+ left = Math.min(left, x);
509
+ top = Math.min(top, y);
510
+ right = Math.max(right, x);
511
+ bottom = Math.max(bottom, y);
512
+ }
513
+ }
514
+ }
515
+ return right < left || bottom < top
516
+ ? fullSourceBounds(image)
517
+ : {
518
+ x: left,
519
+ y: top,
520
+ width: right - left + 1,
521
+ height: bottom - top + 1,
522
+ };
523
+ }
524
+ async function drawImageLayer(context, options, canvas, maxSize, runtime) {
525
+ const image = await loadWebImage(options.src, runtime?.resources, runtime?.signal);
526
+ try {
527
+ throwIfAborted(runtime?.signal);
528
+ const sourceBounds = options.trimTransparentPadding
529
+ ? getVisibleSourceBounds(image, runtime)
530
+ : fullSourceBounds(image);
531
+ const boundedImageSize = fitSizeWithinMax(image, maxSize);
532
+ const decodeScale = boundedImageSize.width / image.width;
533
+ const scale = resolveScale(options.scale, 'watermark image scale') * decodeScale;
534
+ const alpha = resolveAlpha(options.alpha, 'watermark image alpha');
535
+ const rotation = resolveRotation(options.rotate, 'watermark image rotation');
536
+ const scaledSize = {
537
+ width: sourceBounds.width * scale,
538
+ height: sourceBounds.height * scale,
539
+ };
540
+ const rotatedBounds = getRotatedBounds(scaledSize, rotation);
541
+ const positions = options.layout?.type === 'tile'
542
+ ? resolveTilePlacements(options.layout, canvas, rotatedBounds)
543
+ : [resolveAnchoredPosition(options.position, canvas, rotatedBounds, 0)];
544
+ positions.forEach((position) => {
545
+ const previousAlpha = context.globalAlpha;
546
+ const previousBlendMode = context.globalCompositeOperation;
547
+ context.save();
548
+ try {
549
+ context.globalAlpha = previousAlpha * alpha;
550
+ context.globalCompositeOperation = resolveBlendMode(options.blendMode);
551
+ context.imageSmoothingEnabled = false;
552
+ context.translate(position.x - rotatedBounds.left, position.y - rotatedBounds.top);
553
+ context.rotate(degreesToRadians(rotation));
554
+ context.scale(scale, scale);
555
+ context.drawImage(image.image, sourceBounds.x, sourceBounds.y, sourceBounds.width, sourceBounds.height, 0, 0, sourceBounds.width, sourceBounds.height);
556
+ }
557
+ finally {
558
+ context.globalAlpha = previousAlpha;
559
+ context.globalCompositeOperation = previousBlendMode;
560
+ context.restore();
561
+ }
562
+ });
563
+ }
564
+ finally {
565
+ image.cleanup();
566
+ }
567
+ }
568
+ function validateRotationCanvasMode(mode) {
569
+ const resolved = mode ?? 'expand';
570
+ if (resolved !== 'expand' && resolved !== 'crop') {
571
+ throw new Error(`Unsupported rotation canvas mode: ${resolved}.`);
572
+ }
573
+ return resolved;
574
+ }
575
+ function normalizeMatteColor(value) {
576
+ const resolved = value ?? '#FFFFFF';
577
+ const match = typeof resolved === 'string'
578
+ ? /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(resolved)
579
+ : null;
580
+ const hex = match?.[1];
581
+ if (!hex) {
582
+ throw new Error('matteColor must use #RGB, #RGBA, #RRGGBB, or #RRGGBBAA.');
583
+ }
584
+ const expanded = hex.length <= 4
585
+ ? Array.from(hex)
586
+ .map((character) => character + character)
587
+ .join('')
588
+ : hex;
589
+ return `#${expanded.slice(0, 6).toUpperCase()}`;
590
+ }
591
+ /** Render a complete composition into a browser canvas. */
592
+ export async function renderWebCompositionToCanvas(backgroundImage, layers, output, runtime) {
593
+ throwIfAborted(runtime?.signal);
594
+ if (!backgroundImage?.src) {
595
+ throw new Error('please set image!');
596
+ }
597
+ const format = normalizeOutputFormat(output.saveFormat);
598
+ normalizeQuality(output.quality);
599
+ const matteColor = normalizeMatteColor(output.matteColor);
600
+ const canvasMode = validateRotationCanvasMode(output.rotationCanvasMode);
601
+ const backgroundScale = resolveScale(backgroundImage.scale, 'background image scale');
602
+ const backgroundAlpha = resolveAlpha(backgroundImage.alpha, 'background image alpha');
603
+ const backgroundRotation = resolveRotation(backgroundImage.rotate, 'background image rotation');
604
+ const background = await loadWebImage(backgroundImage.src, runtime?.resources, runtime?.signal);
605
+ try {
606
+ throwIfAborted(runtime?.signal);
607
+ const boundedBackgroundSize = fitSizeWithinMax(background, output.maxSize);
608
+ const compositionSize = {
609
+ width: Math.max(Math.round(boundedBackgroundSize.width * backgroundScale), 1),
610
+ height: Math.max(Math.round(boundedBackgroundSize.height * backgroundScale), 1),
611
+ };
612
+ const outputSize = canvasMode === 'expand'
613
+ ? getExpandedCanvasSize(compositionSize, backgroundRotation)
614
+ : compositionSize;
615
+ const canvas = createWebCanvas(outputSize.width, outputSize.height, runtime?.resources);
616
+ const context = getCanvasContext(canvas);
617
+ if (format === 'jpg') {
618
+ context.fillStyle = matteColor;
619
+ context.fillRect(0, 0, outputSize.width, outputSize.height);
620
+ }
621
+ context.save();
622
+ try {
623
+ if (backgroundRotation !== 0) {
624
+ context.translate(outputSize.width / 2, outputSize.height / 2);
625
+ context.rotate(degreesToRadians(backgroundRotation));
626
+ context.translate(-compositionSize.width / 2, -compositionSize.height / 2);
627
+ }
628
+ context.save();
629
+ try {
630
+ context.globalAlpha = backgroundAlpha;
631
+ context.imageSmoothingEnabled = true;
632
+ context.drawImage(background.image, 0, 0, compositionSize.width, compositionSize.height);
633
+ }
634
+ finally {
635
+ context.restore();
636
+ }
637
+ for (const layer of layers) {
638
+ throwIfAborted(runtime?.signal);
639
+ if (layer.type === 'text') {
640
+ drawTextLayer(context, layer.options, compositionSize);
641
+ }
642
+ else {
643
+ await drawImageLayer(context, layer.options, compositionSize, output.maxSize, runtime);
644
+ }
645
+ }
646
+ }
647
+ finally {
648
+ context.restore();
649
+ }
650
+ return canvas;
651
+ }
652
+ finally {
653
+ background.cleanup();
654
+ }
655
+ }
656
+ /** Render a complete composition into a data URL. */
657
+ export async function renderWebComposition(backgroundImage, layers, output, runtime) {
658
+ const canvas = await renderWebCompositionToCanvas(backgroundImage, layers, output, runtime);
659
+ throwIfAborted(runtime?.signal);
660
+ return encodeCanvas(canvas, output.saveFormat, output.quality);
661
+ }