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/LICENSE +191 -0
- package/README.md +41 -0
- package/dist/main.cjs +3785 -0
- package/dist/main.d.cts +316 -0
- package/dist/main.d.ts +316 -0
- package/dist/main.mjs +3681 -0
- package/package.json +103 -0
- package/src/base64Image.ts +101 -0
- package/src/cssParser.ts +59 -0
- package/src/filters.ts +226 -0
- package/src/font-utils.ts +204 -0
- package/src/get-default-fields-value.ts +64 -0
- package/src/getSvg.ts +284 -0
- package/src/imageHelper.ts +293 -0
- package/src/imagePassportUtils.ts +718 -0
- package/src/images-processor.ts +186 -0
- package/src/main.ts +1354 -0
- package/src/polyfills/DOMParser.ts +28 -0
- package/src/polyfills/File.ts +20 -0
- package/src/polyfills/Image.ts +90 -0
- package/src/svgScaler.ts +179 -0
- package/src/textGenCodeParser.ts +319 -0
- package/src/time.ts +147 -0
- package/src/toolsFunc.ts +79 -0
- package/src/types.ts +177 -0
- package/src/utils.ts +758 -0
- package/src/watermaker.ts +190 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
|
|
2
|
+
import { Doc, Field, Fields, FieldsData, FileMap, MapWithName, MaskMap, TemplateData } from "./types.ts";
|
|
3
|
+
import { mergeImages, removeUpdateQueryString } from "./images-processor.ts";
|
|
4
|
+
import { textGenCodeParser } from "./textGenCodeParser.ts";
|
|
5
|
+
import { getCanvas } from "./imageHelper.ts";
|
|
6
|
+
|
|
7
|
+
const isNode = typeof window === 'undefined';
|
|
8
|
+
|
|
9
|
+
export const isVoid = (value: any) => {
|
|
10
|
+
return value === undefined || value === null
|
|
11
|
+
}
|
|
12
|
+
export const nullOrEmpty = (value: any) => {
|
|
13
|
+
return isVoid(value) || value.length == 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts a Base64-encoded SVG string to a plain SVG string using modern APIs.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} base64String - The Base64-encoded SVG string.
|
|
20
|
+
* @returns {string} The decoded SVG string.
|
|
21
|
+
*/
|
|
22
|
+
export function base64ToSvg(base64String: string): string {
|
|
23
|
+
// Check if the base64 string starts with the SVG data URI scheme
|
|
24
|
+
const svgPrefix = 'data:image/svg+xml;base64,';
|
|
25
|
+
if (base64String.startsWith(svgPrefix)) {
|
|
26
|
+
base64String = base64String.slice(svgPrefix.length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Decode the base64 string to a byte array
|
|
30
|
+
const byteCharacters = atobLegacy(base64String);
|
|
31
|
+
|
|
32
|
+
// Convert the byte array to a typed array
|
|
33
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
34
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
35
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
36
|
+
}
|
|
37
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
38
|
+
|
|
39
|
+
// Convert the byte array to a string using TextDecoder
|
|
40
|
+
const decoder = new TextDecoder('utf-8');
|
|
41
|
+
const svgString = decoder.decode(byteArray);
|
|
42
|
+
|
|
43
|
+
return svgString;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Legacy atob function to convert Base64-encoded string to binary string.
|
|
48
|
+
* This is used for compatibility with older browsers.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} base64String - The Base64-encoded string.
|
|
51
|
+
* @returns {string} The binary string.
|
|
52
|
+
*/
|
|
53
|
+
function atobLegacy(base64String: string): string {
|
|
54
|
+
if (typeof Buffer !== 'undefined') {
|
|
55
|
+
// Node.js environment
|
|
56
|
+
return Buffer.from(base64String, 'base64').toString('binary');
|
|
57
|
+
} else if (typeof window !== 'undefined' && typeof window.atob === 'function') {
|
|
58
|
+
// Browser environment
|
|
59
|
+
return window.atob(base64String);
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error('Base64 decoding not supported in this environment');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generates a random alphanumeric string of the specified length.
|
|
67
|
+
*
|
|
68
|
+
* @param {number} length - The desired length of the generated string.
|
|
69
|
+
* @returns {string} A random alphanumeric string of the specified length.
|
|
70
|
+
*/
|
|
71
|
+
export function genId(length: number): string {
|
|
72
|
+
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
73
|
+
let result = '';
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < length; i++) {
|
|
76
|
+
const randomIndex = Math.floor(Math.random() * characters.length);
|
|
77
|
+
result += characters.charAt(randomIndex);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Converts a Base64 string to a File (Browser) or Buffer (Node.js).
|
|
85
|
+
* @param base64String - The Base64 encoded string.
|
|
86
|
+
* @param fileName - The name of the output file.
|
|
87
|
+
* @returns File (Browser) or Buffer (Node.js)
|
|
88
|
+
*/
|
|
89
|
+
export function base64ToFile(id: string, base64String: string): File | Buffer {
|
|
90
|
+
const matches = base64String.match(/^data:(.+);base64,(.+)$/);
|
|
91
|
+
if (!matches) {
|
|
92
|
+
throw new Error('Invalid Base64 string: ' + base64String? base64String.substring(0, base64String.length > 10? 9 : base64String.length - 1) : "NO base64String");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const mimeType = matches[1]; // Extract MIME type
|
|
96
|
+
const buffer = Buffer.from(matches[2], 'base64');
|
|
97
|
+
|
|
98
|
+
if (typeof window !== 'undefined') {
|
|
99
|
+
// Browser: Create a File object
|
|
100
|
+
// Create File object from Blob
|
|
101
|
+
const fileName = `${cleanFilename(id)}.${mimeType.split('/')[1]}`;
|
|
102
|
+
return new File([buffer], fileName, { type: mimeType });
|
|
103
|
+
} else {
|
|
104
|
+
// Node.js: Return Buffer
|
|
105
|
+
return buffer;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Converts an array of objects to a map using a specified attribute as the key.
|
|
111
|
+
*
|
|
112
|
+
* @param {Array<Object>} arr - The array of objects to be converted.
|
|
113
|
+
* @param {string} attr - The attribute to use as the key for the mapping.
|
|
114
|
+
* @returns {Object} - An object mapping each unique attribute value to its corresponding item.
|
|
115
|
+
*/
|
|
116
|
+
export function arrayToMap(attr: string, arr: {[x: string]: any}[], keyParser?: (key: string) => string): {[x: string]: {[x: string]: any}} {
|
|
117
|
+
return arr.reduce((map, item) => {
|
|
118
|
+
var key = item[attr];
|
|
119
|
+
if(keyParser) key = keyParser(key)
|
|
120
|
+
if (key !== undefined) {
|
|
121
|
+
map[key] = item;
|
|
122
|
+
}
|
|
123
|
+
return map;
|
|
124
|
+
}, {});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Converts a JSON object into a File object.
|
|
129
|
+
*
|
|
130
|
+
* @param jsonObject - The JSON object to convert.
|
|
131
|
+
* @param fileName - The desired name for the File. Defaults to 'data.json'.
|
|
132
|
+
* @returns The File object containing the JSON data.
|
|
133
|
+
*/
|
|
134
|
+
function jsonToFile(jsonObject: object, fileName: string = 'data.json'): File {
|
|
135
|
+
// Convert JSON object to string
|
|
136
|
+
const jsonString: string = JSON.stringify(jsonObject, null, 2);
|
|
137
|
+
|
|
138
|
+
// Create a Blob with the JSON string
|
|
139
|
+
const blob: Blob = new Blob([jsonString], { type: 'application/json' });
|
|
140
|
+
|
|
141
|
+
// Create a File from the Blob
|
|
142
|
+
const file: File = new File([blob], fileName, { type: 'application/json' });
|
|
143
|
+
|
|
144
|
+
return file;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Function to order an object with Field values
|
|
148
|
+
export function orderOptions(obj: { [x: string]: Field }): { [x: string]: Field } {
|
|
149
|
+
// Convert object to an array of [key, value] pairs
|
|
150
|
+
const entries = Object.entries(obj);
|
|
151
|
+
|
|
152
|
+
// Sort the entries array based on the 'index' property of the values
|
|
153
|
+
entries.sort(([, a], [, b]) => {
|
|
154
|
+
const indexAStr = (a.index || "").toString();
|
|
155
|
+
const indexBStr = (b.index || "").toString();
|
|
156
|
+
return indexAStr.localeCompare(indexBStr);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Convert the sorted entries back to an object
|
|
160
|
+
const sortedObj: { [x: string]: Field } = {};
|
|
161
|
+
for (const [key, value] of entries) {
|
|
162
|
+
sortedObj[key] = value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return sortedObj;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const orderByName = (dataMap: MapWithName): MapWithName => {
|
|
169
|
+
// Convert the map into an array of key-value pairs
|
|
170
|
+
const entries = Object.entries(dataMap);
|
|
171
|
+
|
|
172
|
+
// Sort the array by the `name` attribute of the values
|
|
173
|
+
entries.sort((a, b) => {
|
|
174
|
+
const nameA = a[1].name.toUpperCase(); // Ignore case while sorting
|
|
175
|
+
const nameB = b[1].name.toUpperCase(); // Ignore case while sorting
|
|
176
|
+
|
|
177
|
+
if (nameA < nameB) {
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
if (nameA > nameB) {
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
return 0; // Names must be equal
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Convert the sorted array back into an object
|
|
187
|
+
const sortedDataMap: MapWithName = {};
|
|
188
|
+
entries.forEach(([key, value]) => {
|
|
189
|
+
sortedDataMap[key] = value;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return sortedDataMap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Updates the query string in a URL, adding or incrementing the 'updates' parameter.
|
|
197
|
+
* @param {string} url - The URL to update.
|
|
198
|
+
* @returns {string} - The updated URL.
|
|
199
|
+
*/
|
|
200
|
+
export function isTheSameUrl(url?: string, url2?: string): boolean {
|
|
201
|
+
if(!url || !url2) return false
|
|
202
|
+
return isUploadedFileUrl(url) && removeUpdateQueryString(url).toLowerCase() === removeUpdateQueryString(url2).toLowerCase()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function expandImagesMap(
|
|
206
|
+
imagesMap: FileMap,
|
|
207
|
+
onFile: (id: string, image: string) => Promise<FileMap | null>
|
|
208
|
+
): Promise<FileMap> {
|
|
209
|
+
return new Promise(async (resolve, reject) => {
|
|
210
|
+
try {
|
|
211
|
+
const expandedMap: FileMap = { ...imagesMap }; // Start with a copy of the original map
|
|
212
|
+
|
|
213
|
+
for (const [id, image] of Object.entries(imagesMap)) {
|
|
214
|
+
if (image && isImageDataUrl(image)) {
|
|
215
|
+
const newFiles = await onFile(id, image);
|
|
216
|
+
if (newFiles) {
|
|
217
|
+
Object.assign(expandedMap, newFiles);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
resolve(expandedMap)
|
|
223
|
+
} catch(e) {
|
|
224
|
+
reject(e)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const mergeTemplateData = (storageHost: string, newTemplateData: TemplateData, oldTemplateData?: TemplateData | null): Promise<TemplateData> => {
|
|
230
|
+
return new Promise(async (resolve, reject) => {
|
|
231
|
+
//If this is not an update to an existing template. Such as in a new template upload.
|
|
232
|
+
if(!oldTemplateData) return resolve(newTemplateData)
|
|
233
|
+
|
|
234
|
+
const data: TemplateData = { } as TemplateData
|
|
235
|
+
data.svg = newTemplateData.svg
|
|
236
|
+
|
|
237
|
+
const errors: string[] = []
|
|
238
|
+
|
|
239
|
+
//Checking for fields merge errors
|
|
240
|
+
for(const [id, field] of Object.entries(oldTemplateData.fields)) {
|
|
241
|
+
const newField = newTemplateData.fields[id]
|
|
242
|
+
//Error when a field is missing in the new template
|
|
243
|
+
if(!newField) {
|
|
244
|
+
errors.push(`The ${field.type.split("_").join(" ")} field, "${field.name}" is missing in the new template.`)
|
|
245
|
+
|
|
246
|
+
} //Error when a field type has changed in the new template
|
|
247
|
+
else if(newField.type != field.type) {
|
|
248
|
+
errors.push(`The ${field.type.split("_").join(" ")} field, "${field.name}" is misrepresented as "${newField.type.split("_").join(" ")}" in the new template.`)
|
|
249
|
+
|
|
250
|
+
} //Error when a field is a select field, but it's missing options in the new template
|
|
251
|
+
else if(field.options && !newField?.options) {
|
|
252
|
+
errors.push(`The ${field.type.split("_").join(" ")} field, "${field.name}" is missing options in the new template.`)
|
|
253
|
+
|
|
254
|
+
} //Validating each option in the new template
|
|
255
|
+
else if(field.options && newField.options) {
|
|
256
|
+
for(const option of Object.values(field.options)) {
|
|
257
|
+
//Error when an option is missing in the new template
|
|
258
|
+
if(!newField.options[option.id]) {
|
|
259
|
+
errors.push(`The ${field.type.split("_").join(" ")} ${field.name} option, "${option.name}" is missing in the new template.`)
|
|
260
|
+
|
|
261
|
+
} //Error when a option type has changed in the new template
|
|
262
|
+
else if(newField.options[option.id].type != option.type) {
|
|
263
|
+
errors.push(`The ${field.type.split("_").join(" ")} ${field.name} option, "${option.name}" is misrepresented as "${newField.options[option.id].type.split("_").join(" ")}" in the new template.`)
|
|
264
|
+
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
//Checking for images merge errors
|
|
272
|
+
if(Object.keys(oldTemplateData.images || {}).length > 0 && Object.keys(newTemplateData.images || {}).length == 0) {
|
|
273
|
+
errors.push(`Images are missing in missing in the new template.`)
|
|
274
|
+
|
|
275
|
+
} else if(Object.keys(oldTemplateData.images || {}).length > 0) {
|
|
276
|
+
for(const id of Object.keys(oldTemplateData.images)) {
|
|
277
|
+
//Error when a an image is missing in the new template, and the is of the missing image is not a select option,
|
|
278
|
+
//since a new image might have been added from the user interface without its existence in the svg
|
|
279
|
+
if(!newTemplateData.images[id] && !id.includes(".select")) {
|
|
280
|
+
errors.push(`The image with the id, ${id} is missing in the new template.`)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
//merge the fields
|
|
287
|
+
data.fields = { ...oldTemplateData.fields, ...newTemplateData.fields }
|
|
288
|
+
//Iterate the merged fields to order each select fields' options by name
|
|
289
|
+
for(const [id, field] of Object.entries(data.fields)) {
|
|
290
|
+
if(field.options) {
|
|
291
|
+
data.fields[id].options = orderOptions(field.options)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//merge the images
|
|
296
|
+
data.images = await mergeImages(storageHost, oldTemplateData.images, { ...oldTemplateData.images, ...newTemplateData.images })
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if(errors.length > 0) {
|
|
300
|
+
return reject(new Error(errors.join("\n")))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
resolve(data)
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const isUploadedFileUrl = ( url: string ) => {
|
|
308
|
+
return url.startsWith("http")
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export const isImageDataUrl = ( url: string ) => {
|
|
312
|
+
return url.startsWith("data:image") || url.startsWith("data:img")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export const getImage = (id: string, images: FileMap, suffix?: string | null) => {
|
|
316
|
+
//suffix examples "_thumbnail", "_728", "_512"
|
|
317
|
+
//console.log("parsedSvg.geImage: ", id, images, suffix)
|
|
318
|
+
if(!id) return ""
|
|
319
|
+
var image = images[id]
|
|
320
|
+
|
|
321
|
+
if(id.includes(".")) {
|
|
322
|
+
//console.log("parsedSvg.randImages:1", id, suffix, image)
|
|
323
|
+
}
|
|
324
|
+
//If the image value is a reference to another image
|
|
325
|
+
if(image && !isUploadedFileUrl(image) && !isImageDataUrl(image)) {
|
|
326
|
+
//set the image as the value of the referenced image
|
|
327
|
+
image = images[`${images[id]}${suffix || ""}`]
|
|
328
|
+
if(id.includes(".")) {
|
|
329
|
+
//console.log("parsedSvg.randImages:2", id, suffix, image)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
} else if(image) {
|
|
333
|
+
image = images[`${id}${suffix || ""}`]
|
|
334
|
+
if(id.includes(".")) {
|
|
335
|
+
//console.log("parsedSvg.randImages:3", id, suffix, `${id}${suffix || ""}`, !image? "--" : image.substring(0, 10))
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
//set the image as the fallback image if the specified resolution does not exist
|
|
340
|
+
if(!image && suffix) {
|
|
341
|
+
const idDeviceNumberSplit = id.split("_")
|
|
342
|
+
const idNumber = idDeviceNumberSplit[idDeviceNumberSplit.length - 1]
|
|
343
|
+
//Only fallback if the id is not a reference to a resolution
|
|
344
|
+
//This is done by checking the number at the end of the id
|
|
345
|
+
//If there's no number, then the id is not a resolution image
|
|
346
|
+
//It it is, it might be the id of a duplicated image. where the number stands for
|
|
347
|
+
// the duplication number instead of the resolution size
|
|
348
|
+
if(isNaN(Number(idNumber.trim())) || Number(idNumber.trim()) < 512) image = images[id]
|
|
349
|
+
}
|
|
350
|
+
if(id.includes(".")) {
|
|
351
|
+
//console.log("parsedSvg.randImages:4", id, suffix, image)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return image
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export const getImageMask = (id: string, masks?: MaskMap | null) => {
|
|
358
|
+
if(!masks) return null
|
|
359
|
+
//suffix examples "_thumbnail", "_728", "_512"
|
|
360
|
+
var mask = masks[id]
|
|
361
|
+
//If the image value is a reference to another image mask
|
|
362
|
+
if(mask && typeof mask === "string") {
|
|
363
|
+
//set the image as the value of the referenced image mask
|
|
364
|
+
mask = masks[mask]
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return mask
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function cleanFilename(text: string) {
|
|
371
|
+
// Remove or replace characters not safe for filenames
|
|
372
|
+
return text
|
|
373
|
+
.trim() // Remove whitespace from the beginning and end
|
|
374
|
+
.replace(/[/\\?%*:|"<>]/g, '') // Remove invalid characters for filenames
|
|
375
|
+
.replace(/\s+/g, '-'); // Replace spaces (or whitespace) with underscores
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Converts a map of base64 images, converting PNG images to JPG format.
|
|
380
|
+
*
|
|
381
|
+
* @param {Map<string, string>} base64Map - A map where the key is a string and the value is a base64 image.
|
|
382
|
+
* @returns {Promise<Map<string, string>>} - A new map with PNG images converted to JPG.
|
|
383
|
+
*/
|
|
384
|
+
export async function convertPngBase64ImagesToJpeg(base64Map: {[x: string]: string}): Promise<{[x: string]: string}> {
|
|
385
|
+
const updatedMap: {[x: string]: string} = {};
|
|
386
|
+
|
|
387
|
+
for (const [key, base64Image] of Object.entries(base64Map)) {
|
|
388
|
+
if (base64Image.startsWith("data:image/png") || base64Image.startsWith("data:img/png")) {
|
|
389
|
+
const jpgBase64 = await convertPngToJpg(base64Image);
|
|
390
|
+
updatedMap[key] = jpgBase64;
|
|
391
|
+
} else {
|
|
392
|
+
updatedMap[key] = base64Image;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return updatedMap;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function convertPngBase64ImagesWithUPNG(base64Map: {[x: string]: string}): Promise<{[x: string]: string}> {
|
|
400
|
+
const updatedMap: {[x: string]: string} = {};
|
|
401
|
+
|
|
402
|
+
for (const [key, base64Image] of Object.entries(base64Map)) {
|
|
403
|
+
if (base64Image.startsWith("data:image/png") || base64Image.startsWith("data:img/png")) {
|
|
404
|
+
const jpgBase64 = await convertPngWithUPNG(base64Image);
|
|
405
|
+
updatedMap[key] = jpgBase64;
|
|
406
|
+
} else {
|
|
407
|
+
updatedMap[key] = base64Image;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return updatedMap;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Converts a base64 PNG image to a base64 JPG image.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} base64Png - The base64 string of the PNG image.
|
|
418
|
+
* @returns {Promise<string>} - A promise resolving to the base64 string of the converted JPG image.
|
|
419
|
+
*/
|
|
420
|
+
function convertPngToJpg(base64Png: string): Promise<string> {
|
|
421
|
+
return new Promise((resolve, reject) => {
|
|
422
|
+
const img = new Image();
|
|
423
|
+
img.onload = () => {
|
|
424
|
+
const canvas = getCanvas(img.width, img.height);
|
|
425
|
+
const ctx = canvas.getContext("2d") as any;
|
|
426
|
+
canvas.width = img.width;
|
|
427
|
+
canvas.height = img.height;
|
|
428
|
+
|
|
429
|
+
if(!ctx) return reject(new Error("No canvas context"))
|
|
430
|
+
|
|
431
|
+
// Fill canvas with a white background to avoid transparency.
|
|
432
|
+
//ctx.fillStyle = "#fff";
|
|
433
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
434
|
+
|
|
435
|
+
// Draw the PNG image onto the canvas.
|
|
436
|
+
ctx.drawImage(img, 0, 0);
|
|
437
|
+
|
|
438
|
+
// Convert the canvas content to JPG base64.
|
|
439
|
+
const jpgBase64 = canvas.toDataURL("image/jpeg", 0.8); // 0.8 for quality adjustment.
|
|
440
|
+
resolve(jpgBase64);
|
|
441
|
+
};
|
|
442
|
+
img.onerror = reject;
|
|
443
|
+
img.src = base64Png;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function convertPngWithUPNG(base64Png: string): Promise<string> {
|
|
448
|
+
return new Promise((resolve, reject) => {
|
|
449
|
+
reject(new Error(""))/*
|
|
450
|
+
const img = new Image();
|
|
451
|
+
img.onload = () => {
|
|
452
|
+
const canvas = document.createElement("canvas");
|
|
453
|
+
const ctx = canvas.getContext("2d");
|
|
454
|
+
canvas.width = img.width;
|
|
455
|
+
canvas.height = img.height;
|
|
456
|
+
|
|
457
|
+
if (!ctx) return reject(new Error("No canvas context"));
|
|
458
|
+
|
|
459
|
+
// Draw the PNG image onto the canvas.
|
|
460
|
+
ctx.drawImage(img, 0, 0);
|
|
461
|
+
|
|
462
|
+
// Extract raw image data from the canvas.
|
|
463
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
464
|
+
const rgbaData = imageData.data;
|
|
465
|
+
|
|
466
|
+
// Encode the raw image data to PNG using UPNG.
|
|
467
|
+
const pngArrayBuffer = UPNG.encode([new Uint8Array(rgbaData.buffer)], canvas.width, canvas.height, 0);
|
|
468
|
+
const pngBase64 = `data:image/png;base64,${btoa(Array.from(new Uint8Array(pngArrayBuffer)).map(byte => String.fromCharCode(byte)).join(""))}`;
|
|
469
|
+
|
|
470
|
+
resolve(pngBase64);
|
|
471
|
+
};
|
|
472
|
+
img.onerror = reject;
|
|
473
|
+
img.src = base64Png;*/
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export const buildImageSelectName = (fieldName: string, name: string) => {
|
|
478
|
+
if(!name) return name
|
|
479
|
+
var optionName = name.substring(`${fieldName} `.length)
|
|
480
|
+
if (isNaN(Number(optionName))) {
|
|
481
|
+
return `${optionName}, ${fieldName}`;
|
|
482
|
+
}
|
|
483
|
+
var lastDigitPos = optionName.substring(optionName.length - 1)
|
|
484
|
+
switch (lastDigitPos) {
|
|
485
|
+
case "1":
|
|
486
|
+
return `${optionName}st ${fieldName}`
|
|
487
|
+
case "2":
|
|
488
|
+
return `${optionName}nd ${fieldName}`
|
|
489
|
+
case "3":
|
|
490
|
+
return `${optionName}rd ${fieldName}`
|
|
491
|
+
default:
|
|
492
|
+
return `${optionName}th ${fieldName}`
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export const buildImageSelectNameReverse = (fieldName: string, formattedName: string) => {
|
|
497
|
+
if (!formattedName) return formattedName;
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
// Handle the case with ", fieldName"
|
|
501
|
+
if (formattedName.endsWith(`, ${fieldName}`)) {
|
|
502
|
+
const index = formattedName.lastIndexOf(`, ${fieldName}`)
|
|
503
|
+
return `${fieldName} ${formattedName.substring(0, index)}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Check if the formattedName ends with the fieldName
|
|
507
|
+
if (formattedName.endsWith(` ${fieldName}`)) {
|
|
508
|
+
const withoutFieldName = formattedName.substring(0, formattedName.length - fieldName.length - 1);
|
|
509
|
+
|
|
510
|
+
// Check for ordinal suffix
|
|
511
|
+
const ordinalMatch = withoutFieldName.match(/(\d+)(st|nd|rd|th)$/);
|
|
512
|
+
if (ordinalMatch) {
|
|
513
|
+
return `${fieldName} ${ordinalMatch[1]}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Handle the default case without ordinal suffix
|
|
517
|
+
return `${fieldName} ${withoutFieldName}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
// If no match, return null or undefined as it doesn't fit the expected format
|
|
522
|
+
return formattedName;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Utility function to determine input type based on the layer name
|
|
526
|
+
export const breakName = (name: string) => {
|
|
527
|
+
return name.replace(/_/g, " ").split("@")[0]
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export const showField = (field: Field, fields: Fields, data: Doc) => {
|
|
531
|
+
const visibilityCode = field?.visibility_code;
|
|
532
|
+
if (visibilityCode && visibilityCode.length > 0 && (visibilityCode.includes("==") || visibilityCode.includes("!="))) {
|
|
533
|
+
let codeAndTarget = visibilityCode.split("==");
|
|
534
|
+
let isNot;
|
|
535
|
+
if(visibilityCode.includes("==")) {
|
|
536
|
+
codeAndTarget = visibilityCode.split("==");
|
|
537
|
+
isNot = false;
|
|
538
|
+
|
|
539
|
+
} else {
|
|
540
|
+
codeAndTarget = visibilityCode.split("!=");
|
|
541
|
+
isNot = true
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const code = codeAndTarget[0].trim();
|
|
545
|
+
const targetExpression = codeAndTarget[1].trim();
|
|
546
|
+
|
|
547
|
+
// Helper function to parse code result
|
|
548
|
+
const parseCodeResult = (parser: (dataKey: string, dataAsKey: string) => string) => {
|
|
549
|
+
return textGenCodeParser(code, data, useDataForRandSeed, parser);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const codeResultForTextSelectName = parseCodeResult((dataKey, dataAsKey) => {
|
|
553
|
+
if (fields[dataKey]?.type === "text_select" && fields[dataKey]?.selections?.[dataAsKey]) {
|
|
554
|
+
return (fields[dataKey].selections[dataAsKey]?.name || "").trim();
|
|
555
|
+
|
|
556
|
+
}
|
|
557
|
+
return dataAsKey;
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const codeResultForTextSelectValue = parseCodeResult((dataKey, dataAsKey) => {
|
|
561
|
+
if (fields[dataKey]?.type === "text_select" && fields[dataKey]?.selections?.[dataAsKey]) {
|
|
562
|
+
return (fields[dataKey].selections[dataAsKey]?.value || "").trim();
|
|
563
|
+
|
|
564
|
+
} else if (fields[dataKey]?.type === "image_select") {
|
|
565
|
+
const optionName = (fields[dataKey]?.options || {})[dataAsKey]?.name || ""
|
|
566
|
+
//console.log("showField", dataKey, "::", dataAsKey, "-1-", optionName)
|
|
567
|
+
return optionName;
|
|
568
|
+
}
|
|
569
|
+
return dataAsKey;
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const codeResultForImageSelectTransformed = parseCodeResult((dataKey, dataAsKey) => {
|
|
573
|
+
if (fields[dataKey]?.type === "image_select") {
|
|
574
|
+
const transformed = buildImageSelectName(fields[dataKey].name, (fields[dataKey]?.options || {})[dataAsKey]?.name || "")
|
|
575
|
+
//console.log("showField", dataKey, "::", dataAsKey, "-2-", transformed)
|
|
576
|
+
return transformed;
|
|
577
|
+
}
|
|
578
|
+
return dataAsKey;
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const codeResult = [codeResultForTextSelectName, codeResultForTextSelectValue, codeResultForImageSelectTransformed];
|
|
582
|
+
|
|
583
|
+
// Evaluate the target expression with logical operators
|
|
584
|
+
const evaluateExpression = (expression: string): boolean => {
|
|
585
|
+
const operands = expression.split(/\s*(\|\||&&)\s*/); // Split by `||` and `&&`
|
|
586
|
+
|
|
587
|
+
if(expression.includes("5th")) {
|
|
588
|
+
//console.log("showField", expression, "-1-", operands)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let result = checkOperand(operands[0]);
|
|
592
|
+
|
|
593
|
+
for (let i = 2; i < operands.length; i++) {
|
|
594
|
+
const currentOperand = checkOperand(operands[i]);
|
|
595
|
+
const operator = operands[i - 1];
|
|
596
|
+
if (operator === "||") {
|
|
597
|
+
result = result || currentOperand;
|
|
598
|
+
} else if (operator === "&&") {
|
|
599
|
+
result = result && currentOperand;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return result;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const checkOperand = (operand: string): boolean => {
|
|
607
|
+
const trimmedOperand = operand.trim();
|
|
608
|
+
|
|
609
|
+
if(!isNot) {
|
|
610
|
+
//return codeResult.some(result => result === trimmedOperand);
|
|
611
|
+
return codeResult.some(result => result.toLowerCase() === trimmedOperand.toLowerCase());
|
|
612
|
+
|
|
613
|
+
} else {
|
|
614
|
+
//return codeResult.some(result => result !== trimmedOperand);
|
|
615
|
+
//console.log("checkOperand", codeResult, `${codeResult[0].toLowerCase()} !== ${trimmedOperand.toLowerCase()}`, codeResult[0].toLowerCase() !== trimmedOperand.toLowerCase())
|
|
616
|
+
return codeResult.some(result => result.toLowerCase() !== trimmedOperand.toLowerCase());
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
return evaluateExpression(targetExpression);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return true;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
export const splitSvgElementId = (id: string) => {
|
|
627
|
+
const [ name, typeAndSelectIndex ] = id.split(".", 2)
|
|
628
|
+
|
|
629
|
+
if(!typeAndSelectIndex) return { name }
|
|
630
|
+
|
|
631
|
+
const [ type, selectIndex ] = [
|
|
632
|
+
typeAndSelectIndex.split("_")[0],
|
|
633
|
+
typeAndSelectIndex.split("_").slice(1).join(" ")
|
|
634
|
+
]
|
|
635
|
+
|
|
636
|
+
return { name, type, selectIndex, typeAndSelectIndex }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export const useDataForRandSeed = (key: string): boolean => {
|
|
640
|
+
const { type } = splitSvgElementId(key)
|
|
641
|
+
return type && ["text", "textarea"].includes(type)? true : false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export const valueOfParseValue = (key: string, value: string, data?: FieldsData, fields?: Fields) => {
|
|
645
|
+
if(fields && data && ["gen", "date"].includes(fields[key]?.type)) {
|
|
646
|
+
value = textGenCodeParser(fields[key].code, data, useDataForRandSeed, (dataKey, dataAsKey) => {
|
|
647
|
+
if(fields[dataKey]?.type == "text_select") {
|
|
648
|
+
return ((fields[dataKey].selections || {})[dataAsKey]?.value || "").trim()
|
|
649
|
+
|
|
650
|
+
}
|
|
651
|
+
return dataAsKey
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
//console.log("formatDate.value:", key, value, data, fields)
|
|
656
|
+
|
|
657
|
+
return value
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export const actionsStorageKey = (collectionName: string, id: string) => {
|
|
661
|
+
return `/${collectionName}/${id}`.toLowerCase()
|
|
662
|
+
}
|
|
663
|
+
export const fileFieldStorageKey = (collectionName: string, id: string, fieldName: string) => {
|
|
664
|
+
return `/${collectionName}/${id}/${fieldName}`.toLowerCase()
|
|
665
|
+
}
|
|
666
|
+
export const fileDocStorageKey = (collectionName: string, id: string) => {
|
|
667
|
+
return `/${collectionName}/${id}`.toLowerCase()
|
|
668
|
+
}
|
|
669
|
+
export const fileCollectionStorageKey = (collectionName: string) => {
|
|
670
|
+
return `/${collectionName}`.toLowerCase()
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export const isBrowser = () => {
|
|
674
|
+
return typeof window !== "undefined"
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export const saveFileFieldFile = (collectionName: string, id: string, fieldName: string, value: string) => {
|
|
678
|
+
if(!isBrowser() || !window?.localStorage) return
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
window?.localStorage.setItem(fileFieldStorageKey(collectionName, id, fieldName), value)
|
|
682
|
+
|
|
683
|
+
} catch(e) {
|
|
684
|
+
window?.localStorage.clear()
|
|
685
|
+
window?.localStorage.setItem(fileFieldStorageKey(collectionName, id, fieldName), value)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export interface FileFieldsFiles {
|
|
690
|
+
[x: string]: string
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export const getFileFieldFile = (collectionName: string, id: string, fieldName: string, fileFieldTypesDefaultFiles?: FileFieldsFiles): string | null => {
|
|
694
|
+
if(!isBrowser() || !window?.localStorage) {
|
|
695
|
+
const { type, name } = splitSvgElementId(fieldName)
|
|
696
|
+
if(!type || !fileFieldTypesDefaultFiles) return null
|
|
697
|
+
|
|
698
|
+
if(type == "upload" && name.toLowerCase().split(" ").includes("logo")) return fileFieldTypesDefaultFiles.logo
|
|
699
|
+
return fileFieldTypesDefaultFiles[`${type}`]
|
|
700
|
+
|
|
701
|
+
}
|
|
702
|
+
return window?.localStorage.getItem(fileFieldStorageKey(collectionName, id, fieldName))
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export const moveFileFieldFile = (collectionName: string, oldId: string, newId: string, fieldName: string, fileFieldTypesDefaultFiles?: FileFieldsFiles): Promise<void> => {
|
|
706
|
+
return new Promise((resolve, reject) => {
|
|
707
|
+
if(!isBrowser() || !window?.localStorage) return resolve()
|
|
708
|
+
const file = getFileFieldFile(collectionName, oldId, fieldName, fileFieldTypesDefaultFiles)
|
|
709
|
+
if(file) {
|
|
710
|
+
saveFileFieldFile(collectionName, newId, fieldName, file)
|
|
711
|
+
window?.localStorage.removeItem(fileFieldStorageKey(collectionName, oldId, fieldName))
|
|
712
|
+
resolve()
|
|
713
|
+
|
|
714
|
+
} else {
|
|
715
|
+
reject(new Error("No file at old ID"))
|
|
716
|
+
}
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export const deleteFileFieldFile = (collectionName: string, id: string, fieldName: string, fileFieldTypesDefaultFiles?: FileFieldsFiles): Promise<void> => {
|
|
721
|
+
return new Promise((resolve, reject) => {
|
|
722
|
+
if(!isBrowser() || !window?.localStorage) return resolve()
|
|
723
|
+
const file = getFileFieldFile(collectionName, id, fieldName, fileFieldTypesDefaultFiles)
|
|
724
|
+
if(file) {
|
|
725
|
+
window?.localStorage.removeItem(fileFieldStorageKey(collectionName, id, fieldName))
|
|
726
|
+
resolve()
|
|
727
|
+
|
|
728
|
+
} else {
|
|
729
|
+
resolve()
|
|
730
|
+
}
|
|
731
|
+
})
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export const deleteDataWithKeyPrefix = (keyPrefix: string) => {
|
|
735
|
+
if(!isBrowser() || !window?.localStorage) return
|
|
736
|
+
for (let key in window?.localStorage) {
|
|
737
|
+
if (key.startsWith(keyPrefix)) {
|
|
738
|
+
window?.localStorage.removeItem(key);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export const setR2Host = (url: string, r2Domain: string) => {
|
|
744
|
+
if(!url) return url
|
|
745
|
+
if (url.startsWith('/')) {
|
|
746
|
+
return `https://${r2Domain}${url}`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
const parsedUrl = new URL(url);
|
|
751
|
+
parsedUrl.host = r2Domain;
|
|
752
|
+
parsedUrl.protocol = 'https:'; // force https
|
|
753
|
+
return parsedUrl.toString();
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.error('Invalid URL:', url);
|
|
756
|
+
return url; // fallback
|
|
757
|
+
}
|
|
758
|
+
};
|