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/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontbacked-svg",
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "frontbacked svg renderer",
|
|
5
|
+
"types": "dist/main.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin.disable": {
|
|
8
|
+
"frontbacked-svg": "dist/bin/cli.cjs"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/main.d.ts",
|
|
14
|
+
"default": "./dist/main.mjs"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/main.d.cts",
|
|
18
|
+
"default": "./dist/main.cjs"
|
|
19
|
+
},
|
|
20
|
+
"default": "./dist/main.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./dist/*": {
|
|
23
|
+
"types": "./dist/*.d.ts",
|
|
24
|
+
"import": "./dist/*.mjs",
|
|
25
|
+
"require": "./dist/*.cjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.0.0"
|
|
30
|
+
},
|
|
31
|
+
"packageManager": "npm@8.4.0",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"bin"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc && tsup",
|
|
39
|
+
"s": "node dist/bin/cli.cjs",
|
|
40
|
+
"t": "npm run build && npm run s",
|
|
41
|
+
"lint": "eslint . && npm run lint:lockfile",
|
|
42
|
+
"lint:fix": "eslint . --fix",
|
|
43
|
+
"lint:lockfile": "lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm yarn",
|
|
44
|
+
"test": "c8 node --loader ts-node/esm --test __tests__/**",
|
|
45
|
+
"test:watch": "c8 node --loader ts-node/esm --test --watch __tests__/**",
|
|
46
|
+
"coverage:view": "open coverage/lcov-report/index.html",
|
|
47
|
+
"version": "changeset version",
|
|
48
|
+
"release": "changeset publish"
|
|
49
|
+
},
|
|
50
|
+
"author": {
|
|
51
|
+
"name": "feyi-tech",
|
|
52
|
+
"email": "jinminetics@gmail.com",
|
|
53
|
+
"url": "https://github.com/feyi-tech"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"provenance": false,
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"license": "Apache-2.0",
|
|
60
|
+
"keywords": [
|
|
61
|
+
""
|
|
62
|
+
],
|
|
63
|
+
"homepage": "https://github.com/feyi-tech/frontbacked-svg",
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/feyi-tech/frontbacked-svg/issues"
|
|
66
|
+
},
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/feyi-tech/frontbacked-svg.git"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@changesets/changelog-github": "^0.5.0",
|
|
73
|
+
"@changesets/cli": "^2.27.7",
|
|
74
|
+
"@types/jsdom": "^21.1.7",
|
|
75
|
+
"@types/node": "^20.14.10",
|
|
76
|
+
"c8": "^10.1.2",
|
|
77
|
+
"eslint": "^9.6.0",
|
|
78
|
+
"eslint-plugin-security": "^3.0.1",
|
|
79
|
+
"husky": "^9.0.11",
|
|
80
|
+
"lint-staged": "^15.2.7",
|
|
81
|
+
"lockfile-lint": "^4.14.0",
|
|
82
|
+
"neostandard": "^0.11.0",
|
|
83
|
+
"ts-node": "^10.9.2",
|
|
84
|
+
"tsup": "^8.1.0",
|
|
85
|
+
"typescript": "^5.5.3",
|
|
86
|
+
"validate-conventional-commit": "^1.0.4"
|
|
87
|
+
},
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"firebase": ">=8.0.0"
|
|
90
|
+
},
|
|
91
|
+
"lint-staged": {
|
|
92
|
+
"**/*.{js,json}": [
|
|
93
|
+
"npm run lint:fix"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"dependencies": {
|
|
97
|
+
"@types/firebase": "^3.2.1",
|
|
98
|
+
"canvas": "^3.1.0",
|
|
99
|
+
"jsdom": "^26.0.0",
|
|
100
|
+
"jspdf": "^2.5.1",
|
|
101
|
+
"svgson": "^5.3.1"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ImageWrapper } from "./polyfills/Image.ts";
|
|
2
|
+
|
|
3
|
+
const isNode = typeof window === 'undefined';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the dimensions (width and height) of an image from a base64 string.
|
|
7
|
+
*
|
|
8
|
+
* @param base64Image - The base64 string of the image.
|
|
9
|
+
* @returns A Promise that resolves to an object containing the base64 string, width, and height of the image.
|
|
10
|
+
*/
|
|
11
|
+
export function getImageDimensions(base64Image: string): Promise<{ width: number, height: number }> {
|
|
12
|
+
return new Promise<{ width: number, height: number }>((resolve, reject) => {
|
|
13
|
+
const img = new Image();
|
|
14
|
+
|
|
15
|
+
img.onload = () => {
|
|
16
|
+
resolve({
|
|
17
|
+
width: img.width,
|
|
18
|
+
height: img.height,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
img.onerror = (error) => {
|
|
23
|
+
reject(new Error(`Failed to load image: ${error}`));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
img.src = base64Image;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resizes a base64 image string to the given width while maintaining the aspect ratio.
|
|
32
|
+
*
|
|
33
|
+
* @param base64Image - The base64 string of the image.
|
|
34
|
+
* @param targetWidth - The desired width for the resized image.
|
|
35
|
+
* @returns A Promise that resolves to the resized image as a base64 string.
|
|
36
|
+
*/
|
|
37
|
+
export async function resizeBase64Image(base64Image: string, targetWidth: number): Promise<string> {
|
|
38
|
+
return new Promise<string>((resolve, reject) => {
|
|
39
|
+
const img = new Image();
|
|
40
|
+
|
|
41
|
+
// Set up the onload handler to resize the image once it's loaded
|
|
42
|
+
img.onload = () => {
|
|
43
|
+
const aspectRatio = img.width / img.height;
|
|
44
|
+
const targetHeight = targetWidth / aspectRatio;
|
|
45
|
+
|
|
46
|
+
// Create a canvas and draw the resized image on it
|
|
47
|
+
const canvas = document.createElement('canvas');
|
|
48
|
+
canvas.width = targetWidth;
|
|
49
|
+
canvas.height = targetHeight;
|
|
50
|
+
const ctx = canvas.getContext('2d') as any;
|
|
51
|
+
|
|
52
|
+
if (ctx) {
|
|
53
|
+
const realImage = ((img as any) as ImageWrapper).realImage? ((img as any) as ImageWrapper).realImage : img
|
|
54
|
+
ctx.drawImage(realImage, 0, 0, targetWidth, targetHeight);
|
|
55
|
+
|
|
56
|
+
// Convert the canvas to a base64 string and resolve the promise
|
|
57
|
+
const resizedBase64Image = canvas.toDataURL('image/png');
|
|
58
|
+
resolve(resizedBase64Image);
|
|
59
|
+
} else {
|
|
60
|
+
reject(new Error('Failed to get canvas context'));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Set up the onerror handler to reject the promise if the image fails to load
|
|
65
|
+
img.onerror = (error) => {
|
|
66
|
+
reject(new Error(`Failed to load image: ${error}`));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Set the src attribute to the base64 image string to start loading the image
|
|
70
|
+
img.src = base64Image;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function base64UrlToFile(base64Url: string, fileName?: string, fileType?: string): File {
|
|
75
|
+
// Extract base64 data and the MIME type from the header
|
|
76
|
+
const [header, base64Data] = base64Url.split(',');
|
|
77
|
+
|
|
78
|
+
// Infer the file type from the header if not provided
|
|
79
|
+
fileType = fileType || header.match(/:(.*?);/)?.[1];
|
|
80
|
+
|
|
81
|
+
// If the fileName is not provided, generate a random one with an appropriate extension
|
|
82
|
+
if (!fileName) {
|
|
83
|
+
const extension = fileType?.split('/')[1] || 'bin'; // Default to 'bin' if no type is inferred
|
|
84
|
+
fileName = `file_${Date.now()}.${extension}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Decode the base64 string to a binary string
|
|
88
|
+
const binaryString = window.atob(base64Data);
|
|
89
|
+
const len = binaryString.length;
|
|
90
|
+
const bytes = new Uint8Array(len);
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < len; i++) {
|
|
93
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Create a Blob from the decoded data
|
|
97
|
+
const blob = new Blob([bytes], { type: fileType });
|
|
98
|
+
|
|
99
|
+
// Create and return a File from the Blob
|
|
100
|
+
return new File([blob], fileName, { type: fileType });
|
|
101
|
+
}
|
package/src/cssParser.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Declaration } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const getIdentifier = (selectorOrProperty: string, propertyValue?: string) => {
|
|
5
|
+
return `${selectorOrProperty.replace(/\s/g, "")}${propertyValue? `:${propertyValue.replace(/\s/g, "")}` : ""}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseValueUnit(input: string) {
|
|
9
|
+
const match = input.match(/^([-+]?\d*\.?\d+)([a-zA-Z]+)$/);
|
|
10
|
+
if (match) {
|
|
11
|
+
return {
|
|
12
|
+
value: match[1],
|
|
13
|
+
unit: match[2]
|
|
14
|
+
};
|
|
15
|
+
} else {
|
|
16
|
+
throw new Error(`Invalid input format: ${input}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseAndModifyCSS(cssString: string, callback: (selector: string, declarations: Declaration[]) => Declaration[]) {
|
|
21
|
+
const cssObject: {[x: string]: Declaration[]} = {};
|
|
22
|
+
|
|
23
|
+
// Regular expression to match selectors and their declarations
|
|
24
|
+
const regex = /([^{]+)\{([^}]+)\}/g;
|
|
25
|
+
let match;
|
|
26
|
+
|
|
27
|
+
// Parse the CSS content into an object
|
|
28
|
+
while ((match = regex.exec(cssString)) !== null) {
|
|
29
|
+
const selector = match[1].trim();
|
|
30
|
+
const declarations = match[2].trim().split(';').filter(Boolean).map(decl => {
|
|
31
|
+
const [property, value] = decl.split(':').map(item => item.trim());
|
|
32
|
+
return { property, value };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
cssObject[selector] = declarations;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Function to process each selector and its declarations using the callback
|
|
39
|
+
Object.keys(cssObject).forEach(selector => {
|
|
40
|
+
const declarations = cssObject[selector];
|
|
41
|
+
|
|
42
|
+
// Call the callback, passing the selector and its declarations
|
|
43
|
+
const updatedDeclarations = callback(selector, declarations);
|
|
44
|
+
|
|
45
|
+
// Replace the original declarations with the returned value
|
|
46
|
+
cssObject[selector] = updatedDeclarations;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Convert the object back to a CSS string
|
|
50
|
+
let newCssString = '';
|
|
51
|
+
Object.keys(cssObject).forEach(selector => {
|
|
52
|
+
const declarations = cssObject[selector]
|
|
53
|
+
.map(({ property, value }) => `${property}: ${value};`)
|
|
54
|
+
.join(' ');
|
|
55
|
+
newCssString += `${selector} { ${declarations} }\n`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return newCssString;
|
|
59
|
+
}
|
package/src/filters.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { FilterArgs, Filters } from "./types.ts";
|
|
2
|
+
import { AnalyzeResult, transformImageByTemplate } from "./imagePassportUtils.ts";
|
|
3
|
+
import { ImageWrapper } from './polyfills/Image.ts';
|
|
4
|
+
import { getCanvas } from "./imageHelper.ts";
|
|
5
|
+
|
|
6
|
+
const isNode = typeof window === 'undefined';
|
|
7
|
+
// Helper function to apply the filter
|
|
8
|
+
const applyFilter = (base64ImageString: string, filter: (data: ImageData, args?: FilterArgs | null) => void, args?: FilterArgs | null): Promise<string> => {
|
|
9
|
+
//alert(`Filter will apply`)
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const img = new Image();
|
|
12
|
+
|
|
13
|
+
img.onload = () => {
|
|
14
|
+
|
|
15
|
+
const canvas = getCanvas(img.width, img.width);
|
|
16
|
+
//const canvas = document.createElement('canvas');
|
|
17
|
+
const ctx = canvas.getContext('2d') as any;
|
|
18
|
+
if (!ctx) {
|
|
19
|
+
return reject(new Error("Canvas not supported"));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
canvas.width = img.width;
|
|
23
|
+
canvas.height = img.height;
|
|
24
|
+
const realImage = ((img as any) as ImageWrapper).realImage? ((img as any) as ImageWrapper).realImage : img
|
|
25
|
+
ctx.drawImage(realImage, 0, 0);
|
|
26
|
+
|
|
27
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
28
|
+
filter(imageData, args);
|
|
29
|
+
ctx.putImageData(imageData, 0, 0);
|
|
30
|
+
|
|
31
|
+
resolve(canvas.toDataURL());
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
img.src = base64ImageString;
|
|
35
|
+
|
|
36
|
+
img.onerror = reject;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const BLUR_RADII = ["0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7",/* "0.8", "0.9", "1"*/]
|
|
41
|
+
// Define the FILTERS object with filter functions and corresponding React components
|
|
42
|
+
const FILTERS: Filters = {
|
|
43
|
+
Whitescale: {
|
|
44
|
+
id: "Whitescale",
|
|
45
|
+
filter: (base64ImageString) => applyFilter(base64ImageString, (data) => {
|
|
46
|
+
const pixels = data.data;
|
|
47
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
48
|
+
pixels[i] = 255; // R
|
|
49
|
+
pixels[i + 1] = 255; // G
|
|
50
|
+
pixels[i + 2] = 255; // B
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
Blackscale: {
|
|
55
|
+
id: "Blackscale",
|
|
56
|
+
filter: (base64ImageString) => applyFilter(base64ImageString, (data) => {
|
|
57
|
+
const pixels = data.data;
|
|
58
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
59
|
+
pixels[i] = 0; // R
|
|
60
|
+
pixels[i + 1] = 0; // G
|
|
61
|
+
pixels[i + 2] = 0; // B
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
Greyscale: {
|
|
66
|
+
id: "Greyscale",
|
|
67
|
+
filter: (base64ImageString) => applyFilter(base64ImageString, (data) => {
|
|
68
|
+
const pixels = data.data;
|
|
69
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
70
|
+
const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
|
|
71
|
+
pixels[i] = avg; // R
|
|
72
|
+
pixels[i + 1] = avg; // G
|
|
73
|
+
pixels[i + 2] = avg; // B
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
Redscale: {
|
|
78
|
+
id: "Redscale",
|
|
79
|
+
filter: (base64ImageString) => applyFilter(base64ImageString, (data) => {
|
|
80
|
+
const pixels = data.data;
|
|
81
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
82
|
+
pixels[i + 1] = 0; // G
|
|
83
|
+
pixels[i + 2] = 0; // B
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
Greenscale: {
|
|
88
|
+
id: "Greenscale",
|
|
89
|
+
filter: (base64ImageString) => applyFilter(base64ImageString, (data) => {
|
|
90
|
+
const pixels = data.data;
|
|
91
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
92
|
+
pixels[i] = 0; // R
|
|
93
|
+
pixels[i + 2] = 0; // B
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
Bluescale: {
|
|
98
|
+
id: "Bluescale",
|
|
99
|
+
filter: (base64ImageString) => applyFilter(base64ImageString, (data) => {
|
|
100
|
+
const pixels = data.data;
|
|
101
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
102
|
+
pixels[i] = 0; // R
|
|
103
|
+
pixels[i + 1] = 0; // G
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
Sepia: {
|
|
108
|
+
id: "Sepia",
|
|
109
|
+
filter: (base64ImageString, args) => applyFilter(base64ImageString, (data) => {
|
|
110
|
+
// Set default values for sepia transformation or use provided args
|
|
111
|
+
const rMult = args?.rMult ?? 0.393;
|
|
112
|
+
const gMult = args?.gMult ?? 0.769;
|
|
113
|
+
const bMult = args?.bMult ?? 0.189;
|
|
114
|
+
const rAdd = args?.rAdd ?? 0.349;
|
|
115
|
+
const gAdd = args?.gAdd ?? 0.686;
|
|
116
|
+
const bAdd = args?.bAdd ?? 0.272;
|
|
117
|
+
|
|
118
|
+
const pixels = data.data;
|
|
119
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
120
|
+
const r = pixels[i]; // Red
|
|
121
|
+
const g = pixels[i + 1]; // Green
|
|
122
|
+
const b = pixels[i + 2]; // Blue
|
|
123
|
+
|
|
124
|
+
// Apply customizable sepia transformation
|
|
125
|
+
const tr = rMult * r + gMult * g + bMult * b;
|
|
126
|
+
const tg = rAdd * r + gAdd * g + bAdd * b;
|
|
127
|
+
const tb = bAdd * r + bAdd * g + bAdd * b;
|
|
128
|
+
|
|
129
|
+
pixels[i] = tr > 255 ? 255 : tr; // Red
|
|
130
|
+
pixels[i + 1] = tg > 255 ? 255 : tg; // Green
|
|
131
|
+
pixels[i + 2] = tb > 255 ? 255 : tb; // Blue
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
Guassianblur: {
|
|
136
|
+
id: "Guassianblur",
|
|
137
|
+
filter: (base64ImageString, args) => applyFilter(base64ImageString, (data, args) => {
|
|
138
|
+
const minRadius = Number(BLUR_RADII[0])
|
|
139
|
+
const maxRadius = Number(BLUR_RADII[BLUR_RADII.length - 1])
|
|
140
|
+
var radius = args?.radius && !isNaN(args.radius) ? Number(args.radius) : minRadius;// Default radius
|
|
141
|
+
if(radius > maxRadius) radius = minRadius;
|
|
142
|
+
radius *= 10;
|
|
143
|
+
|
|
144
|
+
const pixels = data.data;
|
|
145
|
+
const width = data.width;
|
|
146
|
+
const height = data.height;
|
|
147
|
+
|
|
148
|
+
// Create a copy of the original pixel data
|
|
149
|
+
const copyData = new Uint8ClampedArray(pixels);
|
|
150
|
+
|
|
151
|
+
// Apply separable Gaussian blur: first horizontal, then vertical
|
|
152
|
+
// Horizontal pass
|
|
153
|
+
function blurHorizontal() {
|
|
154
|
+
for (let y = 0; y < height; y++) {
|
|
155
|
+
for (let x = 0; x < width; x++) {
|
|
156
|
+
let r = 0, g = 0, b = 0, a = 0, count = 0;
|
|
157
|
+
|
|
158
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
159
|
+
const nx = x + dx;
|
|
160
|
+
|
|
161
|
+
if (nx >= 0 && nx < width) {
|
|
162
|
+
const i = (y * width + nx) * 4;
|
|
163
|
+
r += copyData[i];
|
|
164
|
+
g += copyData[i + 1];
|
|
165
|
+
b += copyData[i + 2];
|
|
166
|
+
a += copyData[i + 3];
|
|
167
|
+
count++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const idx = (y * width + x) * 4;
|
|
172
|
+
pixels[idx] = r / count;
|
|
173
|
+
pixels[idx + 1] = g / count;
|
|
174
|
+
pixels[idx + 2] = b / count;
|
|
175
|
+
pixels[idx + 3] = a / count;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Vertical pass
|
|
181
|
+
function blurVertical() {
|
|
182
|
+
for (let x = 0; x < width; x++) {
|
|
183
|
+
for (let y = 0; y < height; y++) {
|
|
184
|
+
let r = 0, g = 0, b = 0, a = 0, count = 0;
|
|
185
|
+
|
|
186
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
187
|
+
const ny = y + dy;
|
|
188
|
+
|
|
189
|
+
if (ny >= 0 && ny < height) {
|
|
190
|
+
const i = (ny * width + x) * 4;
|
|
191
|
+
r += copyData[i];
|
|
192
|
+
g += copyData[i + 1];
|
|
193
|
+
b += copyData[i + 2];
|
|
194
|
+
a += copyData[i + 3];
|
|
195
|
+
count++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const idx = (y * width + x) * 4;
|
|
200
|
+
pixels[idx] = r / count;
|
|
201
|
+
pixels[idx + 1] = g / count;
|
|
202
|
+
pixels[idx + 2] = b / count;
|
|
203
|
+
pixels[idx + 3] = a / count;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Apply the separable blur
|
|
209
|
+
blurHorizontal(); // First pass
|
|
210
|
+
blurVertical(); // Second pass
|
|
211
|
+
}, args)
|
|
212
|
+
},/*
|
|
213
|
+
Rotate: {
|
|
214
|
+
id: "Rotate",
|
|
215
|
+
filter: (base64ImageString, args) => new Promise((resolve, reject) => {
|
|
216
|
+
resolve(base64ImageString)
|
|
217
|
+
}),
|
|
218
|
+
render: null
|
|
219
|
+
},*/
|
|
220
|
+
ImageTransform: {
|
|
221
|
+
id: "ImageTransform",
|
|
222
|
+
filter: (base64ImageString, args) => transformImageByTemplate(args as AnalyzeResult, base64ImageString, 2)
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default FILTERS
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { parse, INode } from 'svgson';
|
|
2
|
+
import { Font, FontsMap } from './types.ts';
|
|
3
|
+
import { cleanFilename, isBrowser } from './utils.ts';
|
|
4
|
+
import { getCanvas } from './imageHelper.ts';
|
|
5
|
+
|
|
6
|
+
const isNode = typeof window === 'undefined';
|
|
7
|
+
|
|
8
|
+
// Function to recursively traverse the parsed SVG and collect font-family values
|
|
9
|
+
export function collectFontFamilies(node: INode, fontFamilies: Set<string> = new Set()): Set<string> {
|
|
10
|
+
// Check for direct font-family attribute
|
|
11
|
+
if (node.attributes && node.attributes['font-family']) {
|
|
12
|
+
fontFamilies.add(node.attributes['font-family']);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check for <defs> and <style> elements
|
|
16
|
+
if (node.name === 'defs' && node.children) {
|
|
17
|
+
node.children.forEach(child => {
|
|
18
|
+
if (child.name === 'style' && child.children && child.children.length > 0) {
|
|
19
|
+
const cssContent = child.children[0].value;
|
|
20
|
+
extractFontFamiliesFromCSS(cssContent, fontFamilies);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Recurse into children
|
|
26
|
+
if (node.children && node.children.length > 0) {
|
|
27
|
+
node.children.forEach(child => collectFontFamilies(child, fontFamilies));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return fontFamilies;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Function to extract font-family values from CSS content
|
|
34
|
+
export function extractFontFamiliesFromCSS(cssContent: string, fontFamilies: Set<string>) {
|
|
35
|
+
const fontFamilyRegex = /font-family:\s*([^;]+);/g;
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = fontFamilyRegex.exec(cssContent)) !== null) {
|
|
38
|
+
fontFamilies.add(match[1].trim());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Main export function to parse the SVG and return a list of font-family values
|
|
43
|
+
export async function getFontFamiliesFromSVG(svgString: string): Promise<string[]> {
|
|
44
|
+
const parsedSVG = await parse(svgString);
|
|
45
|
+
const fontFamilies = collectFontFamilies(parsedSVG as INode);
|
|
46
|
+
return Array.from(fontFamilies);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async function fetchFontDataAsDataUrl(url: string): Promise<string> {
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url);
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`HTTP status ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
const blob = await response.blob();
|
|
57
|
+
|
|
58
|
+
if (typeof window !== "undefined" && typeof FileReader !== "undefined") {
|
|
59
|
+
// Browser Environment: Use FileReader
|
|
60
|
+
return new Promise<string>((resolve, reject) => {
|
|
61
|
+
const reader = new FileReader();
|
|
62
|
+
reader.onloadend = () => resolve(reader.result as string);
|
|
63
|
+
reader.onerror = reject;
|
|
64
|
+
reader.readAsDataURL(blob);
|
|
65
|
+
});
|
|
66
|
+
} else if (typeof Buffer !== "undefined") {
|
|
67
|
+
// Node.js Environment: Use Buffer
|
|
68
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
69
|
+
const base64String = Buffer.from(arrayBuffer).toString("base64");
|
|
70
|
+
const mimeType = blob.type || "application/octet-stream";
|
|
71
|
+
return `data:${mimeType};base64,${base64String}`;
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error("Base64 conversion not supported in this environment");
|
|
74
|
+
}
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
throw new Error(`Failed to fetch font data from ${url}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
export function getFontId(fontName: string): string {
|
|
82
|
+
// Convert to lowercase
|
|
83
|
+
let fontId = fontName.toLowerCase();
|
|
84
|
+
fontId = fontId.replace(/["']/g, "")
|
|
85
|
+
|
|
86
|
+
// Replace spaces and special characters with hyphens
|
|
87
|
+
fontId = fontId.replace(/[\s]+/g, '-');
|
|
88
|
+
|
|
89
|
+
// Remove any characters that are not alphanumeric or hyphens
|
|
90
|
+
fontId = fontId.replace(/[^a-z0-9-]+/g, '');
|
|
91
|
+
|
|
92
|
+
// Ensure no multiple consecutive hyphens
|
|
93
|
+
fontId = fontId.replace(/-+/g, '-');
|
|
94
|
+
|
|
95
|
+
// Trim hyphens from start and end
|
|
96
|
+
fontId = fontId.replace(/^-|-$/g, '');
|
|
97
|
+
|
|
98
|
+
return fontId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function generateFontMap(
|
|
102
|
+
fontsLocationDomain: string,
|
|
103
|
+
fontNames: string[],
|
|
104
|
+
fontExtensions = ["ttf", "otf", "woff", "woff2", "eot", "svg"]
|
|
105
|
+
): Promise<FontsMap> {
|
|
106
|
+
return new Promise(async (resolve, reject) => {
|
|
107
|
+
const fontMap: FontsMap = {};
|
|
108
|
+
|
|
109
|
+
for (const fontName of fontNames) {
|
|
110
|
+
const fontInfo: Font = {
|
|
111
|
+
name: fontName.replace(/["']/g, ""),
|
|
112
|
+
id: getFontId(fontName)
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
let fontFound = false;
|
|
116
|
+
|
|
117
|
+
for (const ext of fontExtensions) {
|
|
118
|
+
const url = `https://${fontsLocationDomain}/fonts/${cleanFilename(fontInfo.id)}.${ext}?v=1`;
|
|
119
|
+
try {
|
|
120
|
+
const dataUrl = await fetchFontDataAsDataUrl(url);
|
|
121
|
+
fontInfo.url = url;
|
|
122
|
+
fontInfo.dataUrl = dataUrl;
|
|
123
|
+
fontInfo.ext = ext;
|
|
124
|
+
fontFound = true;
|
|
125
|
+
break;
|
|
126
|
+
} catch (error: any) {
|
|
127
|
+
fontInfo.readError = error.message;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!fontFound) {
|
|
132
|
+
fontInfo.readError = `No valid font found for ${fontName} with extensions: ${fontExtensions.join(
|
|
133
|
+
', '
|
|
134
|
+
)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fontMap[fontInfo.id] = fontInfo;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
resolve(fontMap)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getFontFormat(extension: string) {
|
|
145
|
+
const formatMap: {[x: string]: string} = {
|
|
146
|
+
'ttf': 'truetype',
|
|
147
|
+
'otf': 'opentype',
|
|
148
|
+
'woff': 'woff',
|
|
149
|
+
'woff2': 'woff2',
|
|
150
|
+
'eot': 'embedded-opentype',
|
|
151
|
+
'svg': 'svg'
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return formatMap[extension.toLowerCase()] || 'unknown';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createFontThumbnail(fontBase64: string, fontFormat: string): Promise<string> {
|
|
158
|
+
const mimeTypeMap: {[x: string]: string} = {
|
|
159
|
+
'ttf': 'font/ttf',
|
|
160
|
+
'otf': 'font/otf',
|
|
161
|
+
'woff': 'font/woff',
|
|
162
|
+
'woff2': 'font/woff2',
|
|
163
|
+
'eot': 'font/eot',
|
|
164
|
+
'svg': 'svg'
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const mimeType = mimeTypeMap[fontFormat.toLowerCase()];
|
|
168
|
+
if (!mimeType) {
|
|
169
|
+
return Promise.reject(new Error('Unsupported font format'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
//return resolve("")
|
|
174
|
+
//const fontFace = new FontFace('CustomFont', `url(data:${mimeType};base64,${fontBase64})`);
|
|
175
|
+
|
|
176
|
+
if(!isBrowser() || !document?.fonts) return resolve("")
|
|
177
|
+
const fontFace = new FontFace('CustomFont', `url(${fontBase64})`);
|
|
178
|
+
|
|
179
|
+
fontFace.load().then((loadedFontFace) => {
|
|
180
|
+
(document.fonts as any).add(loadedFontFace);
|
|
181
|
+
|
|
182
|
+
const canvas = getCanvas(100, 100);
|
|
183
|
+
//const canvas = document.createElement('canvas');
|
|
184
|
+
canvas.width = 100; // Set canvas width
|
|
185
|
+
canvas.height = 100; // Set canvas height
|
|
186
|
+
|
|
187
|
+
const ctx = canvas.getContext('2d') as any;
|
|
188
|
+
if(!ctx) return
|
|
189
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
190
|
+
ctx.fillStyle = '#FFFFFF'; // Set background color
|
|
191
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
192
|
+
ctx.fillStyle = '#000000'; // Set text color
|
|
193
|
+
ctx.font = '50px CustomFont'; // Use the loaded font
|
|
194
|
+
ctx.textAlign = 'center';
|
|
195
|
+
ctx.textBaseline = 'middle';
|
|
196
|
+
ctx.fillText('Aa', canvas.width / 2, canvas.height / 2); // Draw the text
|
|
197
|
+
|
|
198
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
199
|
+
resolve(dataUrl);
|
|
200
|
+
}).catch(error => {
|
|
201
|
+
reject(error);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|