frontbacked-svg 5.0.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.
package/src/main.ts ADDED
@@ -0,0 +1,1354 @@
1
+
2
+ import { INode, parse, stringify } from "svgson"
3
+
4
+ import "./polyfills/Image.ts"
5
+ import "./polyfills/DOMParser.ts"
6
+ import "./polyfills/File.ts"
7
+ import {
8
+ isVoid, nullOrEmpty, base64ToSvg, genId, base64ToFile, orderOptions, orderByName,
9
+ mergeTemplateData, isUploadedFileUrl, isImageDataUrl, getImage, getImageMask, cleanFilename,
10
+ convertPngBase64ImagesToJpeg, convertPngBase64ImagesWithUPNG, buildImageSelectName, buildImageSelectNameReverse,
11
+ breakName, showField, splitSvgElementId, useDataForRandSeed, valueOfParseValue, actionsStorageKey, fileFieldStorageKey,
12
+ fileDocStorageKey, fileCollectionStorageKey, isBrowser, saveFileFieldFile, getFileFieldFile, moveFileFieldFile,
13
+ deleteFileFieldFile, deleteDataWithKeyPrefix, arrayToMap, isTheSameUrl, expandImagesMap, setR2Host,
14
+ FileFieldsFiles
15
+ } from "./utils.ts"
16
+ import {
17
+ fetchImageForBase64, getUrlUpdatesCount,
18
+ removeUpdateQueryString, compareBase64, removeQueryString, incrementUpdateQueryString,
19
+ mergeImages, generateWatermark
20
+ } from "./images-processor.ts"
21
+ import {
22
+ base64ToImage, analyzeImage
23
+ } from "./imagePassportUtils.ts"
24
+ import {
25
+ AnalyzeResult
26
+ } from "./imagePassportUtils.ts"
27
+ import FILTERS from "./filters.ts"
28
+ import { responsiveFontSize, responsivePathD, responsiveTransform, responsiveViewBox } from "./svgScaler.ts"
29
+ import { getResponsiveX, getResponsiveY } from "./toolsFunc.ts"
30
+ import { OBSCURE_PCT, obscureText, watermark } from "./watermaker.ts"
31
+ import { textGenCodeParser } from "./textGenCodeParser.ts"
32
+ import { getIdentifier, parseAndModifyCSS, parseValueUnit } from "./cssParser.ts"
33
+ import {
34
+ getFontFormat, getFontFamiliesFromSVG, generateFontMap, collectFontFamilies,
35
+ extractFontFamiliesFromCSS, getFontId, createFontThumbnail
36
+ } from "./font-utils.ts"
37
+
38
+ import { downloadSvgAsImage } from "./getSvg.ts"
39
+ import getDefaultFieldsValue from "./get-default-fields-value.ts"
40
+ import {
41
+ timestampToDate, dateToTimestamp, joinTimeSegments, secondsToTimeSegments,
42
+ parsePaymentWindow, timestampToGmt, gmtTimestampToLocal
43
+ } from "./time.ts"
44
+
45
+ import {
46
+ getImageColor, getImageDimension, imageToStamp, isBlankImage, cropImage, reduceBase64Image
47
+ } from "./imageHelper.ts"
48
+
49
+ import {
50
+ Doc, FileMap, FileImage, FilterArgs, Filters, Declaration, CssDeclarations,
51
+ CssAction, CssActions, Mask, Filter, TextSelectSettings, MaskMap, MapWithName, FieldsData, Fields, Field,
52
+ Template, TemplateData, Templates, TemplatesResults, Font, FontsMap, ImageUploadMaskInfo
53
+ } from "./types.ts"
54
+ import { getImageDimensions, resizeBase64Image } from "./base64Image.ts"
55
+ /*
56
+ import { writeFileSync } from "fs"
57
+ import path from "path"
58
+ import { cwd } from "process"
59
+ */
60
+
61
+ export {
62
+ downloadSvgAsImage,
63
+ getDefaultFieldsValue,
64
+ getFontFormat, getFontFamiliesFromSVG, generateFontMap,
65
+ getIdentifier, parseValueUnit, parseAndModifyCSS,
66
+ collectFontFamilies, extractFontFamiliesFromCSS, getFontId, createFontThumbnail,
67
+ timestampToDate, dateToTimestamp, joinTimeSegments, secondsToTimeSegments,
68
+ parsePaymentWindow, timestampToGmt, gmtTimestampToLocal,
69
+ arrayToMap,
70
+ FILTERS
71
+ }
72
+
73
+ export {
74
+ fetchImageForBase64, getUrlUpdatesCount,
75
+ removeUpdateQueryString, compareBase64, removeQueryString, incrementUpdateQueryString,
76
+ mergeImages, generateWatermark
77
+ }
78
+
79
+ export {
80
+ isVoid, nullOrEmpty, base64ToSvg, genId, base64ToFile, orderOptions, orderByName,
81
+ mergeTemplateData, isUploadedFileUrl, isImageDataUrl, getImage, getImageMask, cleanFilename,
82
+ convertPngBase64ImagesToJpeg, convertPngBase64ImagesWithUPNG, buildImageSelectName, buildImageSelectNameReverse,
83
+ breakName, showField, splitSvgElementId, useDataForRandSeed, valueOfParseValue, actionsStorageKey, fileFieldStorageKey,
84
+ fileDocStorageKey, fileCollectionStorageKey, isBrowser, saveFileFieldFile, getFileFieldFile, moveFileFieldFile,
85
+ deleteFileFieldFile, deleteDataWithKeyPrefix
86
+ }
87
+
88
+ export {
89
+ getImageColor, getImageDimension, imageToStamp, isBlankImage, cropImage, reduceBase64Image
90
+ }
91
+
92
+ export {
93
+ base64ToImage, analyzeImage, isTheSameUrl, expandImagesMap, setR2Host
94
+ }
95
+
96
+ export type {
97
+ Doc, FileMap, FileImage, FilterArgs, Filters, Declaration, CssDeclarations,
98
+ CssAction, CssActions, Mask, Filter, TextSelectSettings, MaskMap, MapWithName, FieldsData, Fields, Field,
99
+ Template, TemplateData, Templates, TemplatesResults, Font, FontsMap, ImageUploadMaskInfo, AnalyzeResult
100
+ }
101
+
102
+ const getFieldId = (id: string) => {
103
+ const [ name, ext ] = id.split(".")
104
+ return `${name}.${(ext || "").split("_")[0].trim()}`.trim()
105
+ }
106
+
107
+ const applyFiltersRecursively = async (base64Image: string, filters: (Mask | null)[], excludedFilters?: string[]) => {
108
+ ////console.log("transformImageByTemplate:0", filters[0])
109
+ const result = !filters[0] || (excludedFilters && excludedFilters.includes(filters[0].filter_id))? base64Image : await FILTERS[filters[0].filter_id].filter(base64Image, filters[0].args)
110
+ filters.shift()
111
+ if(filters.length == 0) {
112
+ return result;
113
+
114
+ } else {
115
+ return applyFiltersRecursively(result, filters, excludedFilters)
116
+ }
117
+ }
118
+
119
+ const applyFilters = (base64Image: string, filters: (Mask | null)[], excludedFilters?: string[]): Promise<string> => {
120
+ return new Promise(async (resolve, reject) => {
121
+ try {
122
+
123
+ //console.log("applyFilters.1", base64Image.substring(0, 10))
124
+ const base64 = await applyFiltersRecursively(base64Image, filters, excludedFilters)
125
+
126
+ //console.log("applyFilters.2", base64.substring(0, 10))
127
+ resolve(base64)
128
+
129
+ } catch(e: any) {
130
+ //console.log("applyFilters.error", e?.message)
131
+ reject(e)
132
+ }
133
+ })
134
+ }
135
+
136
+ const imagesToBase64 = async (
137
+ storageHost: string,
138
+ images: FileMap,
139
+ data: FieldsData,
140
+ templateData: TemplateData,
141
+ deviceWidth?: number | null,
142
+ maxRetries: number = 5,
143
+ fileFieldTypesDefaultFiles?: FileFieldsFiles
144
+ ): Promise<FileMap> => {
145
+ const entries = Object.entries(images);
146
+ const successful: FileMap = {};
147
+ let remaining = entries;
148
+
149
+ for (let attempt = 1; attempt <= maxRetries && remaining.length > 0; attempt++) {
150
+ console.log(`🌀 Attempt ${attempt} - ${remaining.length} image(s) remaining`);
151
+
152
+ const failed: typeof remaining = [];
153
+
154
+ const promises = remaining.map(async ([id, url]) => {
155
+ try {
156
+ const fieldId = getFieldId(id);
157
+ let base64: string | null = null;
158
+
159
+ if (["image_upload", "faceshot", "sign"].includes(templateData.fields[fieldId]?.type)) {
160
+ const img = data[fieldId] && isImageDataUrl(data[fieldId])
161
+ ? data[fieldId]
162
+ : getFileFieldFile("other_tools_data", data.id, fieldId, fileFieldTypesDefaultFiles);
163
+ base64 = img;
164
+ } else if (!id.endsWith("_thumbnail")) {
165
+ const base64Url = getImage(id, images, deviceWidth ? `_${deviceWidth}` : null);
166
+ if (base64Url) {
167
+ base64 = await fetchImageForBase64(storageHost, base64Url);
168
+ }
169
+ }
170
+
171
+ if(base64 && isImageDataUrl(base64) && base64.startsWith('data:img/')) {
172
+ base64 = `data:image/${base64.substring('data:img/'.length)}`
173
+ }
174
+
175
+ if (base64 && isImageDataUrl(base64) && templateData.masks) {
176
+ const maskId = templateData.fields[fieldId]?.type === "image_select" ? fieldId : id;
177
+ const mask = templateData.masks[maskId];
178
+ if (mask) {
179
+ const filters = Object.values(mask);
180
+ if (["sign", "image_upload"].includes(templateData.fields[fieldId]?.type)) {
181
+ base64 = await applyFilters(base64, filters, ["ImageTransform"]);
182
+ } else {
183
+ base64 = await applyFilters(base64, filters);
184
+ }
185
+ }
186
+ }
187
+
188
+ successful[id] = base64 || "";
189
+ return;
190
+ } catch (err: any) {
191
+ console.warn(`⚠️ Failed to fetch image for "${id}" on attempt ${attempt}:`, err.message);
192
+ failed.push([id, url]);
193
+ }
194
+ });
195
+
196
+ await Promise.all(promises);
197
+ remaining = failed;
198
+ }
199
+
200
+ if (remaining.length > 0) {
201
+ throw new Error(`❌ Failed to fetch ${remaining.length} image(s) after ${maxRetries} attempts.`);
202
+ }
203
+
204
+ return successful;
205
+ };
206
+
207
+ export function rotateTransformValue(degree: number, x: number, y: number, width: number, height: number) {
208
+ const cx = x + width / 2;
209
+ const cy = y + height / 2;
210
+ return `rotate(${degree} ${cx} ${cy})`;
211
+ }
212
+
213
+ const FRACTION_DIGITS = 3
214
+ const scaleSvg = (
215
+ child: INode,
216
+ preferedWidth: number,
217
+ preferedHeight: number,
218
+ templateWidth: number,
219
+ templateHeight: number,
220
+ textContent?: string | null,
221
+ maxTextBeforeScaleDown?: number | number
222
+ ) => {
223
+
224
+ if(child.attributes.width) {
225
+ child.attributes.width = `${getResponsiveX(Number(child.attributes.width), preferedWidth, templateWidth, FRACTION_DIGITS)}`
226
+ }
227
+ if(child.attributes.height) {
228
+ child.attributes.height = `${getResponsiveY(Number(child.attributes.height), preferedHeight, templateHeight, FRACTION_DIGITS)}`
229
+ }
230
+
231
+ if(child.attributes.x) {
232
+ child.attributes.x = `${getResponsiveX(Number(child.attributes.x), preferedWidth, templateWidth, FRACTION_DIGITS)}`
233
+ }
234
+ if(child.attributes.y) {
235
+ child.attributes.y = `${getResponsiveY(Number(child.attributes.y), preferedHeight, templateHeight, FRACTION_DIGITS)}`
236
+ }
237
+
238
+ if(child.attributes.transform) {
239
+ child.attributes.transform = responsiveTransform(
240
+ child.attributes.transform,
241
+ (v) => getResponsiveX(v, preferedWidth, templateWidth, FRACTION_DIGITS),
242
+ (v) => getResponsiveY(v, preferedHeight, templateHeight, FRACTION_DIGITS),
243
+ textContent, maxTextBeforeScaleDown
244
+ )
245
+ }
246
+
247
+ if(child.name == "path" && child.attributes.d) {
248
+ child.attributes.d = responsivePathD(
249
+ child.attributes.d,
250
+ (v) => getResponsiveX(v, preferedWidth, templateWidth, FRACTION_DIGITS),
251
+ (v) => getResponsiveY(v, preferedHeight, templateHeight, FRACTION_DIGITS)
252
+ )
253
+ }
254
+
255
+ if(child.attributes.dy) {
256
+ child.attributes.dy = `${getResponsiveY(Number(child.attributes.dy), preferedHeight, templateHeight, FRACTION_DIGITS)}`
257
+ }
258
+
259
+ if(child.children.length > 0) {
260
+ for(var i = 0; i < child.children.length; i++) {
261
+ scaleSvg(child.children[i], preferedWidth, preferedHeight, templateWidth, templateHeight, textContent, maxTextBeforeScaleDown)
262
+ }
263
+ }
264
+ }
265
+
266
+ export const getTextAreaMaxLength = (
267
+ textAreaNode: INode,
268
+ textInput?: string,
269
+ tag?: string,
270
+ maxCharsPerLine?: number,
271
+ showWatermark?: boolean | null
272
+ ): {
273
+ maxLines: number,
274
+ defaultInputLines: string[]
275
+ } => {
276
+ let maxLines = 0;
277
+ const textInputLines = textInput ? textInput.split("\n") : [];
278
+ const totalTextInputLines = textInputLines.length;
279
+ const defaultInputLines = [];
280
+
281
+ const breakLineSafely = (line: string, maxLength: number): string[] => {
282
+ const result = [];
283
+ let currentLine = "";
284
+
285
+ for (const word of line.split(" ")) {
286
+ if ((currentLine + word).length > maxLength) {
287
+ if (currentLine.length > 0) {
288
+ result.push(currentLine.trim());
289
+ }
290
+ currentLine = word + " ";
291
+ } else {
292
+ currentLine += word + " ";
293
+ }
294
+ }
295
+
296
+ if (currentLine.trim().length > 0) {
297
+ result.push(currentLine.trim());
298
+ }
299
+
300
+ return result;
301
+ };
302
+
303
+ for (let i = 0; i < textAreaNode.children.length; i++) {
304
+ if (textAreaNode.children[i].value.length > 0) {
305
+ maxLines++;
306
+ defaultInputLines.push(textAreaNode.children[i].value);
307
+
308
+ if (textInputLines.length > 0) {
309
+ let modifiedText = textInputLines[0];
310
+
311
+ if (maxCharsPerLine && modifiedText.length > maxCharsPerLine) {
312
+ const brokenLines = breakLineSafely(modifiedText, maxCharsPerLine);
313
+ modifiedText = brokenLines.shift() || "";
314
+ textInputLines[0] = brokenLines.join("\n");
315
+ } else {
316
+ textInputLines.shift();
317
+ }
318
+
319
+ // Apply text obfuscation before setting the value
320
+ if(showWatermark) {
321
+ textAreaNode.children[i].value = obscureText(modifiedText, OBSCURE_PCT);
322
+
323
+ } else {
324
+ textAreaNode.children[i].value = modifiedText
325
+ }
326
+ } else if (totalTextInputLines > 0) {
327
+ textAreaNode.children[i].value = "";
328
+ }
329
+ }
330
+
331
+ if (textAreaNode.children[i].children.length > 0) {
332
+ for (let j = 0; j < textAreaNode.children[i].children.length; j++) {
333
+ if (textAreaNode.children[i].children[j].value.length > 0) {
334
+ maxLines++;
335
+ defaultInputLines.push(textAreaNode.children[i].children[j].value);
336
+
337
+ if (textInputLines.length > 0) {
338
+ let modifiedText = textInputLines[0];
339
+
340
+ if (maxCharsPerLine && modifiedText.length > maxCharsPerLine) {
341
+ const brokenLines = breakLineSafely(modifiedText, maxCharsPerLine);
342
+ modifiedText = brokenLines.shift() || "";
343
+ textInputLines[0] = brokenLines.join("\n");
344
+ } else {
345
+ textInputLines.shift();
346
+ }
347
+
348
+ // Apply text obfuscation before setting the value
349
+ if(showWatermark) {
350
+ textAreaNode.children[i].children[j].value = obscureText(modifiedText, OBSCURE_PCT);
351
+
352
+ } else {
353
+ textAreaNode.children[i].children[j].value = modifiedText
354
+ }
355
+ } else if (totalTextInputLines > 0) {
356
+ textAreaNode.children[i].children[j].value = "";
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+ return {
364
+ maxLines,
365
+ defaultInputLines
366
+ };
367
+ };
368
+
369
+ const mutateParsedSvg = (
370
+ fields: Fields,
371
+ parsedSvg: INode, id: string, childIndex: number, field: Field,
372
+ images: FileMap, data: FieldsData,
373
+ templateWidth?: number | null, templateHeight?: number | null, width?: number | null,
374
+ showWatermark?: boolean | null,
375
+ mask?: Filter | null
376
+ ): void => {
377
+
378
+ if(!showField(field, fields, data)) {
379
+ parsedSvg.children[childIndex].attributes["class"] = "hide"
380
+ return
381
+ }
382
+
383
+ if(parsedSvg.children[childIndex].name == "image" && !field?.type) {
384
+
385
+
386
+ ////console.log("parsedSvg.mutateParsedSvg ", width, templateWidth, templateHeight, " _SUFFIX_ ", width? `_${width}` : null)
387
+
388
+ const img = getImage(id, images, width? `_${width}` : null)
389
+ parsedSvg.children[childIndex].attributes["xlink:href"] = img
390
+ }
391
+
392
+
393
+ const addFilter = () => {
394
+ ////console.log("addFilter:", mask, mask?.ImageTransform?.filter_id, mask?.ImageTransform?.args)
395
+ if(mask && mask?.ImageTransform?.filter_id && mask?.ImageTransform?.args) {
396
+ const node = parsedSvg.children[childIndex]
397
+ const x = parseFloat(node.attributes["x"]);
398
+ const y = parseFloat(node.attributes["y"]);
399
+ const width = parseFloat(node.attributes["width"]);
400
+ const height = parseFloat(node.attributes["height"]);
401
+
402
+ parsedSvg.children[childIndex].attributes['transform'] = rotateTransformValue(
403
+ mask?.ImageTransform.args?.rotationAngle,
404
+ x, y, width, height
405
+ )
406
+ }
407
+ }
408
+
409
+
410
+ if(parsedSvg.children[childIndex].name != "defs") {
411
+ parsedSvg.children[childIndex].attributes["data-svg-id"] = parsedSvg.children[childIndex].attributes["id"]
412
+ }
413
+
414
+ if(field?.type == "image_select") {
415
+ if(data[field.id]) {
416
+ const imageIndex = data[field.id]
417
+ const img = getImage(imageIndex, images, width? `_${width}` : null)
418
+ parsedSvg.children[childIndex].attributes["xlink:href"] = img
419
+ //console.log("parsedSvg.image_select ", field.type, width, templateWidth, templateHeight, " _WITH_SUFFIX_ ", width? `${id}_${width}` : null, !img? "no_image" : img.substring(0, 10))
420
+ }
421
+
422
+ } else if(["image_upload", "sign", "faceshot"].includes(field?.type)) {
423
+ if(images[id]) {
424
+ parsedSvg.children[childIndex].attributes["xlink:href"] = images[id]
425
+ if(field?.type == "sign") {
426
+ //addFilter()
427
+ ////console.log("parsedSvg.image_upload/sign", id, mask?.ImageTransform)
428
+
429
+ } else if(field?.type == "faceshot") {
430
+ ////console.log("parsedSvg.image_upload/faceshot", id, mask?.ImageTransform)
431
+
432
+ }
433
+ }
434
+
435
+ } else if(field?.type == "checkbox") {
436
+ if(images[id] && data[field.id]) {
437
+ if(parsedSvg.children[childIndex].name == "image") {
438
+ parsedSvg.children[childIndex].attributes["xlink:href"] = getImage(id, images, width? `_${width}` : null)
439
+ }
440
+ }
441
+
442
+ } else if(field?.type == "qrcode") {
443
+ if(images[id]) {
444
+ if(parsedSvg.children[childIndex].name == "image") {
445
+ parsedSvg.children[childIndex].attributes["xlink:href"] = getImage(id, images, width? `_${width}` : null)
446
+ }
447
+ }
448
+
449
+ } else if(["text", "defgen"].includes(field?.type)) {
450
+ var child = parsedSvg.children[childIndex].children[0]
451
+ var prevChild = null
452
+ while (child.children && child.children.length > 0) {
453
+ prevChild = child
454
+ child = child.children[0]
455
+ }
456
+ //child.value = data[field.id] || `No ${field.name} entered`
457
+ if(data[field.id]) {
458
+ if(showWatermark) {
459
+ child.value = obscureText(data[field.id], OBSCURE_PCT);//escapeHtmlEntities(data[field.id])//
460
+
461
+ } else {
462
+ child.value = data[field.id];//escapeHtmlEntities(data[field.id])//
463
+ }
464
+ }
465
+
466
+ } else if(["textarea"].includes(field?.type)) {
467
+ getTextAreaMaxLength(parsedSvg.children[childIndex], data[field.id], field.id, field?.maxCharsPerLine, showWatermark)
468
+
469
+ } else if(["gen", "date"].includes(field?.type)) {
470
+ var child = parsedSvg.children[childIndex].children[0]
471
+ var prevChild = null
472
+ while (child.children && child.children.length > 0) {
473
+ prevChild = child
474
+ child = child.children[0]
475
+ }
476
+ ////console.log("textGenCodeParser.1", data, field)
477
+ child.value = textGenCodeParser(field.code, data, useDataForRandSeed, (dataKey, dataAsKey) => {
478
+ ////console.log("textGenCodeParser.2", data?.id, dataKey)
479
+ if(fields[dataKey]?.type == "text_select") {
480
+ return ((fields[dataKey].selections || {})[dataAsKey]?.value || "").trim()
481
+ }
482
+ return dataAsKey
483
+ })
484
+
485
+ } else if(["text_select"].includes(field?.type)) {
486
+ var child = parsedSvg.children[childIndex].children[0]
487
+ var prevChild = null
488
+ while (child.children && child.children.length > 0) {
489
+ prevChild = child
490
+ child = child.children[0]
491
+ }
492
+ if(data[field.id]) {
493
+ child.value = ((field.selections || {})[data[field.id]]?.value || "").trim()
494
+
495
+ }
496
+
497
+ }
498
+ }
499
+
500
+ const CUSTOMER_CSS_VALUE_PROCESSORS: { [x: string]: (value: string) => string } = {
501
+ "letter-spacing": (propertyValue: string) => {
502
+ const { value, unit } = parseValueUnit(propertyValue)
503
+ let updatedValue;
504
+ if(unit == "psd") {
505
+ //Convert psd value to em
506
+ updatedValue = `${parseFloat(value) / 1000}em`
507
+
508
+ } else {
509
+ updatedValue = propertyValue
510
+ }
511
+
512
+ return updatedValue
513
+ }
514
+ }
515
+ const updateCustomCssValues = (cssAction: CssAction) => {
516
+ const updatedCssAction = { ...cssAction }
517
+ for(const [key, value] of Object.entries(cssAction)) {
518
+ const updatedDeclarations = [ ...value.declarations ]
519
+ for(var i = 0; i < updatedDeclarations.length; i++) {
520
+ const updatedValueFunc = CUSTOMER_CSS_VALUE_PROCESSORS[value.declarations[i].property]
521
+ if(updatedValueFunc) {
522
+ updatedDeclarations[i] = {
523
+ ...updatedDeclarations[i],
524
+ value: updatedValueFunc(value.declarations[i].value)
525
+ }
526
+ }
527
+ }
528
+ updatedCssAction[key].declarations = updatedDeclarations
529
+ }
530
+
531
+ return updatedCssAction;
532
+ }
533
+
534
+ const updateCustomCssPropertiesValuesToCssValues = (cssActions?: CssActions | null) => {
535
+ if(!cssActions) return null
536
+ const updatedCssActions = { ...cssActions }
537
+ if(updatedCssActions.if_selector) {
538
+ updatedCssActions.if_selector = updateCustomCssValues(updatedCssActions.if_selector)
539
+ }
540
+
541
+ if(updatedCssActions.if_property) {
542
+ updatedCssActions.if_property = updateCustomCssValues(updatedCssActions.if_property)
543
+ }
544
+
545
+ if(updatedCssActions.if_property_and_value) {
546
+ updatedCssActions.if_property_and_value = updateCustomCssValues(updatedCssActions.if_property_and_value)
547
+ }
548
+
549
+ return updatedCssActions
550
+ }
551
+
552
+ function getOcclusionId(a: Record<string, string>, b: Record<string, string>, tag?: string): string | null {
553
+ // Calculate the boundaries of both objects
554
+ const aLeft = Number(a.x);
555
+ const aRight = aLeft + Number(a.width);
556
+ const aTop = Number(a.y);
557
+ const aBottom = aTop + Number(a.height);
558
+
559
+ const pct = 5;
560
+
561
+ var bLeft = Number(b.x);
562
+ var bRight = bLeft + Number(b.width);
563
+ var bTop = Number(b.y);
564
+ var bBottom = bTop + Number(b.height);
565
+
566
+ var bLeftPct = (bLeft * pct) / 100;
567
+ var bRightPct = (bRight * pct) / 100;
568
+ var bTopPct = (bTop * pct) / 100;
569
+ var bBottomPct = (bBottom * pct) / 100;
570
+
571
+ // Check if object 'b' is completely behind object 'a'
572
+ const isOccluded = (
573
+ bLeft + bLeftPct >= aLeft && bRight - bRightPct <= aRight && // 'b' horizontally inside 'a'
574
+ bTop + bTopPct >= aTop && bBottom - bBottomPct <= aBottom // 'b' vertically inside 'a'
575
+ );
576
+
577
+ /*
578
+ //console.log("getOcclusionId5:", tag, `
579
+ aLeft: ${aLeft} | aRight: ${aRight} | aTop: ${aTop} | aBottom: ${aBottom} |
580
+ bLeft: ${bLeft} | bRight: ${bRight} | bTop: ${bTop} | bBottom: ${bBottom} |
581
+ `)*/
582
+
583
+ // If 'b' is occluded by 'a', return the id of 'b'. Otherwise, return false.
584
+ return isOccluded ? b.id : null;
585
+ }
586
+
587
+ const calculateElementId = (element: INode, elementIndex: number, allElements: INode[]): string[] => {
588
+ const occlusionIdList = [element.attributes.id]
589
+ for(var i = allElements.length - 1; i >= 0; i--) {
590
+ if(elementIndex != i && allElements[i].name == "image" && allElements[i].attributes) {
591
+ const occlusionId = getOcclusionId(element.attributes, allElements[i].attributes, `${element.attributes.id}:${allElements[i].attributes.id}`)
592
+ if(occlusionId) occlusionIdList.push(occlusionId)
593
+ }
594
+ }
595
+ return occlusionIdList
596
+ }
597
+
598
+ export const getSvg = (
599
+ storageHost: string,
600
+ data: FieldsData, templateData: TemplateData, fonts?: FontsMap | null, showWatermark?: boolean | null,
601
+ width?: number | "max" | null,
602
+ tempMask?: MaskMap | null,
603
+ fileFieldTypesDefaultFiles?: FileFieldsFiles
604
+ ): Promise<string> => {
605
+ ////console.log("formatDate:3.w", width)
606
+
607
+ return new Promise(async (resolve, reject) => {
608
+
609
+ parse(templateData.svg)
610
+ .then(async parsedSvg => {
611
+ const fieldsKeys = Object.keys(templateData.fields)
612
+ const templateWidth = Number(`${parsedSvg.attributes.width || 0}`)
613
+ const templateHeight = Number(`${parsedSvg.attributes.height || 0}`)
614
+
615
+ var preferedWidth = !width || width == "max"? templateWidth : width
616
+ //if(preferedWidth && preferedWidth > 728 && !isHighQuality) preferedWidth = 728
617
+
618
+ const preferedHeight = ((preferedWidth / templateWidth) * templateHeight)
619
+ let images = await imagesToBase64(storageHost, templateData.images, data, templateData, preferedWidth, 5, fileFieldTypesDefaultFiles)
620
+ //writeFileSync(path.join(cwd(), "./images.json"), JSON.stringify(images, null, "\t"))
621
+ //console.log("parsedSvg.images", images)
622
+ //resolve(stringify(parsedSvg))/*
623
+
624
+ let maskImageWidth = 0, maskImageHeight = 0, maskImageX = 0, maskImageY = 0;
625
+ var defIndex = -1
626
+
627
+ //Scale down the SVG
628
+ parsedSvg.attributes.width = `${preferedWidth}`
629
+ parsedSvg.attributes.height = `${preferedHeight}`
630
+
631
+ if(parsedSvg.attributes.viewBox) {
632
+ parsedSvg.attributes.viewBox = responsiveViewBox(
633
+ parsedSvg.attributes.viewBox,
634
+ (v) => getResponsiveX(v, preferedWidth, templateWidth, FRACTION_DIGITS),
635
+ (v) => getResponsiveY(v, preferedHeight, templateHeight, FRACTION_DIGITS)
636
+ )
637
+ }
638
+
639
+ for(var i = parsedSvg.children.length - 1; i >= 0; i--) {
640
+ const child = parsedSvg.children[i]
641
+
642
+ const { name, type, selectIndex, typeAndSelectIndex } = splitSvgElementId(child.attributes.id)
643
+ const key = `${name}.${type}`
644
+
645
+ //Scale down the SVG child
646
+ const field = templateData.fields[key]
647
+ if(child.attributes.id == "Company_Name.text") {
648
+ ////console.log("valueArray:1", key, child.attributes.id, field?.id, field, data[field?.id || ""], data)
649
+ }
650
+ scaleSvg(child, preferedWidth, preferedHeight, templateWidth, templateHeight, data[field?.id || ""], field?.maxTextBeforeScaleDown)
651
+
652
+ //console.info("parsedSvg.otherz.0", key, child.attributes.x, child.attributes.y)
653
+ if(child.name == "defs") {
654
+ defIndex = i
655
+
656
+ } else if(fieldsKeys.includes(key) || child.name == "image") {
657
+
658
+ var fieldId = getFieldId(child.attributes.id)//id.split("_")[0]
659
+ const imageId = type == "image_select"? fieldId : child.attributes.id
660
+
661
+ mutateParsedSvg(templateData.fields,
662
+ parsedSvg, child.attributes.id, i, templateData.fields[key], images, data,
663
+ templateWidth, templateHeight, preferedWidth, showWatermark,
664
+ (tempMask || {})[imageId] || (templateData?.masks || {})[imageId]
665
+ )
666
+
667
+ ////console.log("parsedSvg.otherz!!", imageId, tempMask, (tempMask || {})[imageId], ((tempMask || {})[imageId] || {}).ImageTransform)
668
+ if(((tempMask || {})[imageId] || {}).ImageTransform) {
669
+ ////console.log("parsedSvg.otherz!", key, child.attributes.x, child.attributes.y)
670
+ maskImageWidth = parseFloat(child.attributes.width)
671
+ maskImageHeight = parseFloat(child.attributes.height)
672
+ maskImageX = parseFloat(child.attributes.x)
673
+ maskImageY = parseFloat(child.attributes.y)
674
+ ////console.log("parsedSvg.otherz!.2", key, maskImageX, maskImageY, tempMask)
675
+ }
676
+ }
677
+ }
678
+
679
+ var calulatedIdList: string[] = []
680
+ for(var i = parsedSvg.children.length - 1; i >= 0; i--) {
681
+ const child = parsedSvg.children[i]
682
+ if(child.name == "image") {
683
+ // Add a custom data attribute to identify the clickable element
684
+ const calculatedId = calculateElementId(child, i, parsedSvg.children).filter(id => !calulatedIdList.includes(id));
685
+ calulatedIdList = calulatedIdList.concat(calculatedId)
686
+ parsedSvg.children[i].attributes['data-click-element-id'] =
687
+ calculatedId.length == 0?
688
+ parsedSvg.children[i].attributes.id : calculatedId.join();
689
+ }
690
+ }
691
+
692
+ //Add fonts and widgets styles
693
+ if(defIndex == -1) {
694
+ //defIndex = 0
695
+ }
696
+
697
+ if(defIndex > -1) {
698
+ const el = parsedSvg.children[defIndex]
699
+ ////console.log("parsedSvg:defs", el)
700
+ // Check for <defs> and <style> elements
701
+ if (el.children) {
702
+ el.children.forEach(child => {
703
+ if (child.name === 'style' && child.children && child.children.length > 0) {
704
+ const cssContent = child.children[0].value;
705
+
706
+ const updatedCssContent = parseAndModifyCSS(cssContent, (selector, declarations) => {
707
+
708
+ const css = updateCustomCssPropertiesValuesToCssValues(templateData.cssActions)
709
+ const ifSelector = css?.if_selector || {}
710
+ const ifProperty = css?.if_property || {}
711
+ const ifpropertyAndValue = css?.if_property_and_value || {}
712
+
713
+ var updatedDeclarations = [ ...declarations ]
714
+
715
+ //Update the selector actions first, then the property action, then the property and value.
716
+ //This will allow property and value action to have precedence over the property action, and the
717
+ //property action over the selector action
718
+
719
+ const ifSelectorActionAll = ifSelector["*"]
720
+ const ifSelectorAction = ifSelector[getIdentifier(selector)]
721
+
722
+ if( ifSelectorActionAll ) {
723
+ if(ifSelectorActionAll.shouldReplace) {
724
+ updatedDeclarations = ifSelectorActionAll.declarations
725
+
726
+ } else {
727
+ updatedDeclarations = [ ...updatedDeclarations, ...ifSelectorActionAll.declarations ]
728
+ }
729
+ }
730
+
731
+ if( ifSelectorAction ) {
732
+ if(ifSelectorAction.shouldReplace) {
733
+ updatedDeclarations = ifSelectorAction.declarations
734
+
735
+ } else {
736
+ updatedDeclarations = [ ...updatedDeclarations, ...ifSelectorAction.declarations ]
737
+ }
738
+ }
739
+
740
+ for(var i = 0; i < declarations.length; i++) {
741
+ const decl = declarations[i]
742
+ const ifPropertyActionAll = ifProperty["*"]
743
+ const ifPropertyAction = ifProperty[getIdentifier(decl.property)]
744
+
745
+ const ifPropertyAndValueActionAll = ifpropertyAndValue["*"]
746
+ const ifPropertyAndValueAction = ifpropertyAndValue[getIdentifier(decl.property, decl.value)]
747
+
748
+ if( ifPropertyActionAll ) {
749
+ if(ifPropertyActionAll.shouldReplace) {
750
+ updatedDeclarations = ifPropertyActionAll.declarations
751
+
752
+ } else {
753
+ updatedDeclarations = [ ...updatedDeclarations, ...ifPropertyActionAll.declarations ]
754
+ }
755
+ }
756
+
757
+ if( ifPropertyAction ) {
758
+ if(ifPropertyAction.shouldReplace) {
759
+ updatedDeclarations = ifPropertyAction.declarations
760
+
761
+ } else {
762
+ updatedDeclarations = [ ...updatedDeclarations, ...ifPropertyAction.declarations ]
763
+ }
764
+ }
765
+
766
+ if( ifPropertyAndValueActionAll ) {
767
+ if(ifPropertyAndValueActionAll.shouldReplace) {
768
+ updatedDeclarations = ifPropertyAndValueActionAll.declarations
769
+
770
+ } else {
771
+ updatedDeclarations = [ ...updatedDeclarations, ...ifPropertyAndValueActionAll.declarations ]
772
+ }
773
+ }
774
+
775
+ if( ifPropertyAndValueAction ) {
776
+ if(ifPropertyAndValueAction.shouldReplace) {
777
+ updatedDeclarations = ifPropertyAndValueAction.declarations
778
+
779
+ } else {
780
+ updatedDeclarations = [ ...updatedDeclarations, ...ifPropertyAndValueAction.declarations ]
781
+ }
782
+ }
783
+
784
+ if (decl.property === 'font-size') {
785
+ updatedDeclarations.push(
786
+ {
787
+ property: 'font-size',
788
+ value: responsiveFontSize(
789
+ decl.value,
790
+ templateWidth,
791
+ templateHeight,
792
+ preferedWidth,
793
+ preferedHeight
794
+ )
795
+ }
796
+ )
797
+ }
798
+ }
799
+
800
+ return updatedDeclarations;
801
+ })
802
+ child.children[0].value = updatedCssContent;
803
+ ////console.log("parsedSvg:defs.css.init ", cssContent)
804
+ ////console.log("parsedSvg:defs.css.final ", updatedCssContent)
805
+ }
806
+ });
807
+ }
808
+
809
+ //Sets styles
810
+ const styles = []
811
+ //Sets style for hiding elements
812
+ styles.push({
813
+ "name": "style",
814
+ "type": "element",
815
+ "value": "",
816
+ "attributes": {},
817
+ "children": [
818
+ {
819
+ "name": "",
820
+ "type": "text",
821
+ "value": `.hide { opacity: 0 !important;}`,
822
+ "attributes": {},
823
+ "children": []
824
+ }
825
+ ]
826
+ } as INode)
827
+ //Sets styles for filter widgets
828
+ styles.push({
829
+ "name": "style",
830
+ "type": "element",
831
+ "value": "",
832
+ "attributes": {},
833
+ "children": [
834
+ {
835
+ "name": "",
836
+ "type": "text",
837
+ "value": `#softbaker-slider-handle, #softbaker-button-background, #softbaker-button-text, #softbaker-cancel-background, #softbaker-cancel-text, #softbaker-slider-track { cursor: pointer !important;}`,
838
+ "attributes": {},
839
+ "children": []
840
+ }
841
+ ]
842
+ } as INode)
843
+ parsedSvg.children[defIndex].children = styles.concat(parsedSvg.children[defIndex].children)
844
+
845
+ if(fonts) {
846
+ //Add fonts
847
+ const fontsElements = []
848
+
849
+ for(const font of Object.values(fonts)) {
850
+ if(font.dataUrl && font.ext) {
851
+ fontsElements.push({
852
+ "name": "style",
853
+ "type": "element",
854
+ "value": "",
855
+ "attributes": {},
856
+ "children": [
857
+ {
858
+ "name": "",
859
+ "type": "text",
860
+ "value": `\n @font-face {\n font-family: "${font.name}";\n src: url(${font.dataUrl}) format(${getFontFormat(font.ext)});\n }`,
861
+ "attributes": {},
862
+ "children": []
863
+ }
864
+ ]
865
+ } as INode)
866
+ }
867
+ }
868
+ if(fontsElements.length > 0) {
869
+ parsedSvg.children[defIndex].children = fontsElements.concat(parsedSvg.children[defIndex].children)
870
+ }
871
+ }
872
+
873
+ }
874
+
875
+ //Add Rotate widgets
876
+ if(tempMask) {
877
+
878
+ const maskKey = Object.keys(tempMask)[0]
879
+ const maskValue = ((tempMask || {})[maskKey] || {}).ImageTransform
880
+
881
+ ////console.log("parsedSvg.otherz.tempMask", tempMask, "maskValue:", maskValue, maskImageX, maskImageY, maskImageWidth, maskImageHeight)
882
+
883
+ const colorAccent = "#dd6b20"
884
+ //Rect for slider
885
+ const rect = {
886
+ "name": "rect",
887
+ "type": "element",
888
+ "value": "",
889
+ "attributes": {
890
+ "id": "softbaker-slider-track",
891
+ "x": maskImageX + "",
892
+ "y": maskImageY + maskImageHeight + 10 + "",
893
+ "width": maskImageWidth + "",
894
+ "height": "10",
895
+ "fill": "#2a2a2a"
896
+ },
897
+ "children": []
898
+ }
899
+
900
+ //Circle for slider
901
+ const degree = maskValue?.args?.rotationAngle || 0//(newX / rect.width) * 360;
902
+ const cx = (degree * parseFloat(rect.attributes.width)) / 360
903
+ const circle = {
904
+ "name": "circle",
905
+ "type": "element",
906
+ "value": "",
907
+ "attributes": {
908
+ "id": "softbaker-slider-handle",
909
+ "cx": maskImageX + cx + "",
910
+ "cy": parseFloat(rect.attributes.y) + (parseFloat(rect.attributes.height) / 2) + "",
911
+ "r": parseFloat(rect.attributes.height) + "",
912
+ "fill": colorAccent
913
+ },
914
+ "children": []
915
+ }
916
+
917
+ //Background for submit
918
+ const bg = {
919
+ "name": "rect",
920
+ "type": "element",
921
+ "value": "",
922
+ "attributes": {
923
+ "id": "softbaker-button-background",
924
+ "x": maskImageX + "",
925
+ "y": parseFloat(rect.attributes.y) + parseFloat(rect.attributes.height) + 10 + "",
926
+ "width": "40",
927
+ "height": "20",
928
+ "rx": "10",
929
+ "ry": "10",
930
+ "fill": colorAccent
931
+ },
932
+ "children": []
933
+ }
934
+
935
+ //Text for submit
936
+ const text = {
937
+ "name": "text",
938
+ "type": "element",
939
+ "value": "",
940
+ "attributes": {
941
+ "id": "softbaker-button-text",
942
+ "x": maskImageX + (parseFloat(bg.attributes.width) / 2) + "",
943
+ "y": parseFloat(bg.attributes.y) + (parseFloat(bg.attributes.height) / 2) + 5 + "",
944
+ "font-size": "14",
945
+ "text-anchor": "middle",
946
+ "fill": "#FFFFFF"
947
+ },
948
+ "children": [
949
+ {
950
+ "name": "",
951
+ "type": "text",
952
+ "value": "✔",
953
+ "parent": null,
954
+ "attributes": {},
955
+ "children": []
956
+ }
957
+ ]
958
+ }
959
+
960
+ //Background for cancel
961
+ const submitCancelSpace = 50
962
+ const cancelBg = {
963
+ "name": "rect",
964
+ "type": "element",
965
+ "value": "",
966
+ "attributes": {
967
+ "id": "softbaker-cancel-background",
968
+ "x": maskImageX + submitCancelSpace + "",
969
+ "y": parseFloat(rect.attributes.y) + parseFloat(rect.attributes.height) + 10 + "",
970
+ "width": "40",
971
+ "height": "20",
972
+ "rx": "10",
973
+ "ry": "10",
974
+ "stroke": colorAccent
975
+ },
976
+ "children": []
977
+ }
978
+
979
+ //Text for cancel
980
+ const cancelText = {
981
+ "name": "text",
982
+ "type": "element",
983
+ "value": "",
984
+ "attributes": {
985
+ "id": "softbaker-cancel-text",
986
+ "x": maskImageX + submitCancelSpace + (parseFloat(bg.attributes.width) / 2) + "",
987
+ "y": parseFloat(bg.attributes.y) + (parseFloat(bg.attributes.height) / 2) + 5 + "",
988
+ "font-size": "14",
989
+ "text-anchor": "middle",
990
+ "fill": "#FFFFFF"
991
+ },
992
+ "children": [
993
+ {
994
+ "name": "",
995
+ "type": "text",
996
+ "value": "❌",
997
+ "parent": null,
998
+ "attributes": {},
999
+ "children": []
1000
+ }
1001
+ ]
1002
+ }
1003
+
1004
+ parsedSvg.children = parsedSvg.children.concat(
1005
+ [rect, circle, bg, text, cancelBg, cancelText]
1006
+ )
1007
+ }
1008
+
1009
+ //if(width == 1024) //console.log("formatDate:3.1", parsedSvg)
1010
+ //if(width == 1024) //console.log("formatDate:3.2", stringify(parsedSvg))
1011
+ //Add watermark
1012
+ ////console.log("DataIsFreemium:", data.is_freemium)
1013
+ if(showWatermark) {
1014
+ watermark(stringify(parsedSvg), templateWidth, templateHeight)
1015
+ .then(watermark => {
1016
+ parsedSvg.children.push({
1017
+ "name": "image",
1018
+ "type": "element",
1019
+ "value": "",
1020
+ "attributes": {
1021
+ "id": "watermark-" + genId(8),
1022
+ "width": templateWidth + "",
1023
+ "height": templateHeight + "",
1024
+ "xlink:href": watermark
1025
+ },
1026
+ "children": []
1027
+ })
1028
+ //if(width == 1024) //console.log("formatDate:3.3", stringify(parsedSvg))
1029
+ resolve(stringify(parsedSvg))
1030
+ })
1031
+ .catch((e) => {
1032
+ //if(width == 1024) //console.log("formatDate:3.e", e.message)
1033
+ reject(e)
1034
+ })
1035
+
1036
+ } else {
1037
+ //if(width == 1024) //console.log("formatDate:3.4", stringify(parsedSvg))
1038
+ resolve(stringify(parsedSvg))
1039
+ }//*/
1040
+
1041
+ })
1042
+ .catch(e => {
1043
+ //if(width == 1024) //console.log("formatDate:4", e.message)
1044
+ reject(e)
1045
+ })
1046
+ })
1047
+ }
1048
+
1049
+ // Utility function to remove underscore(_) at the ends of an id which is the photoshop version of white spaces at the ends
1050
+ export const trimId = (name: string) => {
1051
+ return name.replace(/^_+|_+$/g, '');
1052
+ }
1053
+
1054
+ export const splitElementNameWithDirective = (nameWithDirective: string) => {
1055
+ const [ name, directive ] = nameWithDirective.split("@")
1056
+
1057
+ return { name, directive }
1058
+ }
1059
+
1060
+ const getPlaceholder = (element: INode) => {
1061
+ var child = element.children[0]
1062
+ var prevChild = null
1063
+ while (child.children && child.children.length > 0) {
1064
+ prevChild = child
1065
+ child = child.children[0]
1066
+ }
1067
+ const placeholder = child.value
1068
+ return nullOrEmpty(placeholder)? null : placeholder
1069
+ }
1070
+ const updateFields = (fields: Fields, id: string, element: INode) => {
1071
+ const elementName = element.name
1072
+ const { name, type, selectIndex, typeAndSelectIndex } = splitSvgElementId(id)
1073
+
1074
+ if(!typeAndSelectIndex || !type) return { field: null, updatedFields: fields }
1075
+
1076
+ const field: Field = { } as Field
1077
+ switch (type) {
1078
+ case "select"://Covers image select, text select, textarea select,...
1079
+ field.id = `${name}.${type}`
1080
+ field.name = breakName(name)
1081
+ field.type = `${elementName}_${type}`
1082
+ if(elementName == "image") {
1083
+ const option = {
1084
+ name: `${breakName(name)} ${selectIndex}`,
1085
+ id: id,
1086
+ index: selectIndex || 0,
1087
+ type: elementName
1088
+ }
1089
+ field.options = orderOptions({...(fields[field.id]?.options || {}), [option.id]: option})
1090
+ }
1091
+ break;
1092
+ case "upload":
1093
+ field.id = `${name}.${type}`
1094
+ field.name = breakName(name)
1095
+ field.type = `${elementName}_${type}`
1096
+ break;
1097
+ case "faceshot":
1098
+ field.id = `${name}.${type}`
1099
+ field.name = breakName(name)
1100
+ field.type = type
1101
+ break;
1102
+ case "text":
1103
+ case "gen":
1104
+ case "defgen":
1105
+ case "date":
1106
+ field.id = `${name}.${type}`
1107
+ field.name = breakName(name)
1108
+ field.type = type
1109
+ field.placeholder = getPlaceholder(element) || field.name
1110
+ break;
1111
+ case "textarea":
1112
+ field.id = `${name}.${type}`
1113
+ field.name = breakName(name)
1114
+ field.type = type
1115
+ field.placeholder = getTextAreaMaxLength(element).defaultInputLines.join("\n")
1116
+ break;
1117
+ default://Covers text, textarea, and others
1118
+ field.id = `${name}.${type}`
1119
+ field.name = breakName(name)
1120
+ field.type = type
1121
+ break;
1122
+ }
1123
+ if(Object.keys(field).length > 0) fields[field.id] = field
1124
+
1125
+ return {
1126
+ field,
1127
+ updatedFields: fields
1128
+ }
1129
+ }
1130
+
1131
+ // Function to convert PSD to SVG and placeholders
1132
+ export const buildTemplateDataFromSvg = (base64?: string | null): Promise<TemplateData | null> => {
1133
+ return new Promise(async (resolve, reject) => {
1134
+ try {
1135
+
1136
+ if(base64) {
1137
+ parse(base64ToSvg(base64))
1138
+ .then(async parsedSvg => {
1139
+ const annotationErrors: string[] = []
1140
+ const images: FileMap = { }
1141
+ var imagesArray: string[] = []
1142
+ var imagesIdArray: string[] = []
1143
+ const idListOfImagesWithThumbnails: string[] = []
1144
+ var fields: Fields = { }
1145
+ const children = []
1146
+ const processedChoiceFields: string[] = []
1147
+ const masks: MaskMap = {}
1148
+ //Used to check if the title and description directives are declared
1149
+ var title, desc
1150
+ const imageAnalysisPromises: (Promise<ImageUploadMaskInfo>)[] = []
1151
+ for(const el of parsedSvg.children) {
1152
+ const id = trimId(el.attributes["data-name"] || el.attributes.id || genId(8))
1153
+ el.attributes.id = id
1154
+ const { field, updatedFields } = updateFields(fields, id, el)
1155
+ fields = updatedFields
1156
+ //console.log("parsedSvg2:ID", el.attributes["data-name"], id, field?.type)
1157
+
1158
+ if(el.name == "image") {
1159
+ if(!field?.type || !["image_upload", "sign", "faceshot"].includes((field?.type || ""))) {
1160
+ //If the image is a copy of an image already added,
1161
+ if(imagesArray.includes(el.attributes["xlink:href"])) {
1162
+ //set the image as a reference to the already added copy
1163
+ const imageIndex = imagesArray.indexOf(el.attributes["xlink:href"])
1164
+ images[id] = imagesIdArray[imageIndex]
1165
+
1166
+ } else {
1167
+ //add the image
1168
+ images[id] = el.attributes["xlink:href"]
1169
+ imagesArray.push(el.attributes["xlink:href"])
1170
+ imagesIdArray.push(id)
1171
+ }
1172
+
1173
+ } else {
1174
+ //Since the user will be the one to provide it, then no need to upload the placeholder image
1175
+ //Just asign an empty value so the image can be detected has an image to be processed
1176
+ //especially during the application of filter on the image.
1177
+ /*
1178
+ const zRotation = await getZRotationDegree(el.attributes["xlink:href"])
1179
+ if(zRotation != 0) {
1180
+ masks[id] = {filter_id: "Rotate", args: { degree: zRotation }}
1181
+ }*/
1182
+
1183
+ imageAnalysisPromises.push(new Promise(async (resolve, reject) => {
1184
+ const imageEl = await base64ToImage(el.attributes["xlink:href"]);
1185
+ try {
1186
+ const analysisResult = await analyzeImage(imageEl, true);
1187
+ //rotationAngle is way off on analysis done on signatures, and most signature angles are close to 0
1188
+ //if(field?.type == "sign") analysisResult.rotationAngle = 0
1189
+ resolve({
1190
+ imageId: id,
1191
+ filterId: "ImageTransform",
1192
+ mask: {filter_id: "ImageTransform", args: analysisResult }
1193
+ })
1194
+
1195
+ } catch(e: any) {
1196
+ //console.log("annotationErrors:e", e.message)
1197
+ annotationErrors.push(`${el.attributes["id"]} has image annotation error: ${e.message}`)
1198
+ reject(e)
1199
+ }
1200
+ }))
1201
+ images[id] = ""
1202
+ }
1203
+
1204
+ el.attributes["xlink:href"] = ""
1205
+ if(field && field.type == "image_select") {
1206
+ idListOfImagesWithThumbnails.push(id)
1207
+ }
1208
+ }
1209
+
1210
+ if(field) {
1211
+ if(["image_select", "text_select", "textarea_select"].includes(field?.type)) {
1212
+ el.attributes.id = field.id
1213
+ if(!processedChoiceFields.includes(el.attributes.id)) {
1214
+ children.push(el)
1215
+ processedChoiceFields.push(el.attributes.id)
1216
+ }
1217
+
1218
+ } else {
1219
+ const { name } = splitSvgElementId(id)
1220
+ const { directive } = splitElementNameWithDirective(name)
1221
+ if(directive && directive == "name") {
1222
+ title = id
1223
+
1224
+ } else if(directive && directive == "desc") {
1225
+ desc = id
1226
+
1227
+ }
1228
+ children.push(el)
1229
+ //processedChoiceFields.push(field.type)
1230
+ }
1231
+
1232
+ } else {
1233
+ children.push(el)
1234
+ }
1235
+
1236
+ }
1237
+
1238
+ let imageAnalysisPromisesError
1239
+ if(imageAnalysisPromises.length > 0) {
1240
+ try {
1241
+ const all = await Promise.all(imageAnalysisPromises)
1242
+ for(const analysisResult of all) {
1243
+ masks[analysisResult.imageId] = {
1244
+ [analysisResult.filterId]: analysisResult.mask
1245
+ }
1246
+ }
1247
+
1248
+ } catch(e) {
1249
+ imageAnalysisPromisesError = e
1250
+ }
1251
+ }
1252
+
1253
+ if(annotationErrors.length > 0) {
1254
+ return reject(new Error(annotationErrors.join(" ")))
1255
+ }
1256
+
1257
+ if(imageAnalysisPromisesError) return (imageAnalysisPromisesError)
1258
+
1259
+ if(!title || !desc) {
1260
+ let error
1261
+ if(!title && !desc) {
1262
+ error = "Make sure that at least @name and @desc is declared on some text fields. @small is optional."
1263
+
1264
+ } else if(!title) {
1265
+ error = "Make sure that at least @name is declared on a text field. @small is optional."
1266
+
1267
+ } else {
1268
+ error = "Make sure that at least @desc is declared on a text field. @small is optional."
1269
+
1270
+ }
1271
+ return reject(new Error(error))
1272
+ }
1273
+
1274
+ imagesArray = []
1275
+ imagesIdArray = []
1276
+ parsedSvg.children = children
1277
+
1278
+ //console.info("parsedSvg: ", parsedSvg)
1279
+ //console.info("parsedSvg.fields ", fields)
1280
+ //console.info("parsedSvg:images ", "images")
1281
+ // Resolve with SVG content and placeholders description
1282
+
1283
+ const expandedImages = await expandImagesMap(images, async (id, image) => {
1284
+ const { width } = await getImageDimensions(image)
1285
+ var returnMap: FileMap = { }
1286
+ //console.info("parsedSvg:baseWidth2: ", width)
1287
+ returnMap = {
1288
+ [`${id}`]: image
1289
+ }
1290
+
1291
+ if(width >= 728) {
1292
+ returnMap = {
1293
+ ...returnMap,
1294
+ [`${id}`]: image,
1295
+ [`${id}_512`]: await resizeBase64Image(image, 512)
1296
+ }
1297
+ }
1298
+
1299
+ if(width >= 1024) {
1300
+ returnMap = {
1301
+ ...returnMap,
1302
+ [`${id}`]: image,
1303
+ [`${id}_728`]: await resizeBase64Image(image, 728)
1304
+ }
1305
+ }
1306
+
1307
+ if(width >= 2047) {
1308
+ returnMap = {
1309
+ ...returnMap,
1310
+ [`${id}`]: image,
1311
+ [`${id}_1024`]: await resizeBase64Image(image, 1024)
1312
+ }
1313
+ }
1314
+
1315
+ if(idListOfImagesWithThumbnails.includes(id)) {
1316
+ returnMap[`${id}_thumbnail`] = await resizeBase64Image(image, 70)
1317
+ }
1318
+ if(Object.keys(returnMap).length > 0) {
1319
+ return returnMap
1320
+ }
1321
+
1322
+ return null
1323
+ })
1324
+
1325
+ //console.log("parsedSvg2:expandedImages ", expandedImages)
1326
+
1327
+ resolve({
1328
+ svg: stringify(parsedSvg),
1329
+ fields: fields,
1330
+ images,//: await convertPngBase64ImagesWithUPNG(images),
1331
+ masks,
1332
+ cssActions: {
1333
+ if_selector: {
1334
+ "*": {
1335
+ declarations: [{
1336
+ property: "white-space", value: "pre"
1337
+ }],
1338
+ }
1339
+ }
1340
+ }
1341
+ });
1342
+ })
1343
+ .catch(e => {
1344
+ reject(e)
1345
+ })
1346
+
1347
+ } else {
1348
+ resolve(null)
1349
+ }
1350
+ } catch (e) {
1351
+ reject(e);
1352
+ }
1353
+ });
1354
+ };