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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { getCanvas } from "./imageHelper.ts";
|
|
2
|
+
|
|
3
|
+
const isNode = typeof window === 'undefined';
|
|
4
|
+
|
|
5
|
+
export const OBSCURE_PCT = 70
|
|
6
|
+
export function obscureText(text: string, visiblePercentage: number): string {
|
|
7
|
+
if (visiblePercentage < 0 || visiblePercentage > 100) {
|
|
8
|
+
throw new Error("Percentage should be between 0 and 100");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const totalChars: number = text.length;
|
|
12
|
+
|
|
13
|
+
// Collect indices of non-space characters
|
|
14
|
+
const nonSpaceIndices: number[] = [];
|
|
15
|
+
for (let i = 0; i < totalChars; i++) {
|
|
16
|
+
if (text[i] !== ' ') {
|
|
17
|
+
nonSpaceIndices.push(i);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const visibleCharsCount: number = Math.round((visiblePercentage / 100) * nonSpaceIndices.length);
|
|
22
|
+
|
|
23
|
+
// Shuffle indices randomly
|
|
24
|
+
for (let i = nonSpaceIndices.length - 1; i > 0; i--) {
|
|
25
|
+
const j: number = Math.floor(Math.random() * (i + 1));
|
|
26
|
+
[nonSpaceIndices[i], nonSpaceIndices[j]] = [nonSpaceIndices[j], nonSpaceIndices[i]];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const visibleIndices: Set<number> = new Set(nonSpaceIndices.slice(0, visibleCharsCount));
|
|
30
|
+
|
|
31
|
+
return text.split('').map((char, index) => {
|
|
32
|
+
if (char === ' ') return char; // Keep spaces unchanged
|
|
33
|
+
return visibleIndices.has(index) ? char : '*';
|
|
34
|
+
}).join('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function watermark(svgString: string, width: number, height: number): Promise<string> {
|
|
38
|
+
/**
|
|
39
|
+
* Extracts the most frequently used text color from an SVG, considering multiple <style> elements.
|
|
40
|
+
*/
|
|
41
|
+
function getDominantTextColor(svgString: string): string {
|
|
42
|
+
const parser = new DOMParser();
|
|
43
|
+
const svgDoc = parser.parseFromString(svgString, "image/svg+xml");
|
|
44
|
+
|
|
45
|
+
// Collect all <style> content
|
|
46
|
+
const styleContent = Array.from(svgDoc.querySelectorAll("style"))
|
|
47
|
+
.map(style => style.textContent || "")
|
|
48
|
+
.join(" ");
|
|
49
|
+
|
|
50
|
+
console.log("styleContent:", styleContent)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extracts fill color from CSS rules.
|
|
54
|
+
*/
|
|
55
|
+
function findFillInCSS(selector: string): string | null {
|
|
56
|
+
const regex = new RegExp(`${selector}\\s*{[^}]*fill:\\s*([^;]+);`, "gi");
|
|
57
|
+
const matches = Array.from(styleContent.matchAll(regex)); // FIXED HERE
|
|
58
|
+
const fill = matches.length > 0 ? matches[matches.length - 1][1].trim() : null;
|
|
59
|
+
|
|
60
|
+
console.log("findFillInCSS:", selector, fill)
|
|
61
|
+
|
|
62
|
+
return fill
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const textElements = svgDoc.querySelectorAll("text");
|
|
66
|
+
const colorMap: Record<string, number> = {};
|
|
67
|
+
|
|
68
|
+
textElements.forEach((textEl) => {
|
|
69
|
+
let color = textEl.getAttribute("fill");
|
|
70
|
+
|
|
71
|
+
// If no fill is found, check CSS styles
|
|
72
|
+
if (!color) {
|
|
73
|
+
const classNames = textEl.getAttribute("class")?.split(/\s+/) || [];
|
|
74
|
+
const id = textEl.getAttribute("id");
|
|
75
|
+
|
|
76
|
+
// Check CSS rules for fill color
|
|
77
|
+
for (const className of classNames) {
|
|
78
|
+
if (!color) color = findFillInCSS(`\\.${className}`);
|
|
79
|
+
}
|
|
80
|
+
if (!color && id) color = findFillInCSS(`#${id}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Normalize color and count occurrences
|
|
84
|
+
if (color) {
|
|
85
|
+
color = color.toLowerCase();
|
|
86
|
+
colorMap[color] = (colorMap[color] || 0) + 1;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Get the most frequently used text color, fallback to white if none found
|
|
91
|
+
return Object.entries(colorMap)
|
|
92
|
+
.sort((a, b) => b[1] - a[1]) // Sort by frequency
|
|
93
|
+
.map(([color]) => color)[0] || "black"; // Default to white
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const textColor = getDominantTextColor(svgString); // Extract text color from SVG styles
|
|
98
|
+
|
|
99
|
+
// Convert SVG to Image
|
|
100
|
+
const img = new Image();
|
|
101
|
+
let svgBtoa
|
|
102
|
+
try {
|
|
103
|
+
svgBtoa = btoa(unescape(encodeURIComponent(svgString))) // btoa(svgString)
|
|
104
|
+
|
|
105
|
+
} catch(e: any) {
|
|
106
|
+
console.log("getDominantTextColor:error", e?.message, svgString)
|
|
107
|
+
return reject(e)
|
|
108
|
+
}
|
|
109
|
+
img.onload = () => {
|
|
110
|
+
const canvas = getCanvas(width, height);
|
|
111
|
+
canvas.width = width;
|
|
112
|
+
canvas.height = height;
|
|
113
|
+
const ctx = canvas.getContext("2d") as any;
|
|
114
|
+
|
|
115
|
+
if (!ctx) {
|
|
116
|
+
resolve("");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ctx.clearRect(0, 0, width, height);
|
|
121
|
+
ctx.font = "20px Arial";
|
|
122
|
+
ctx.globalAlpha = 0.9; // Semi-transparent watermark
|
|
123
|
+
|
|
124
|
+
const text = "Fake Sample";
|
|
125
|
+
const textWidth = ctx.measureText(text).width;
|
|
126
|
+
const textHeight = 20;
|
|
127
|
+
const numTexts = Math.floor((width * height) / (textWidth * textHeight * 3));
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < numTexts; i++) {
|
|
130
|
+
const x = Math.random() * width;
|
|
131
|
+
const y = Math.random() * height;
|
|
132
|
+
const angle = (Math.random() * 60 - 30) * (Math.PI / 180);
|
|
133
|
+
|
|
134
|
+
ctx.save();
|
|
135
|
+
ctx.translate(x, y);
|
|
136
|
+
ctx.rotate(angle);
|
|
137
|
+
ctx.fillStyle = textColor; // Use the extracted text color
|
|
138
|
+
ctx.fillText(text, 0, 0);
|
|
139
|
+
ctx.restore();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// === Add Warning Text with Contrast Fix ===
|
|
143
|
+
const warningText = `Remove watermark to remove "Fake Sample" and show all hidden texts.`;
|
|
144
|
+
|
|
145
|
+
let warningFontSize = Math.floor(width / 30);
|
|
146
|
+
ctx.font = `${warningFontSize}px Arial`;
|
|
147
|
+
ctx.globalAlpha = 1;
|
|
148
|
+
|
|
149
|
+
// Calculate best contrast for warning text
|
|
150
|
+
const avgColor = textColor.match(/\d+/g)?.map(Number) ?? [0, 0, 0];
|
|
151
|
+
const brightness = (avgColor[0] * 0.299 + avgColor[1] * 0.587 + avgColor[2] * 0.114);
|
|
152
|
+
ctx.fillStyle = brightness > 128 ? "black" : "white"; // Adjust contrast
|
|
153
|
+
|
|
154
|
+
let warningTextWidth = ctx.measureText(warningText).width;
|
|
155
|
+
const maxWidth = width * 0.45;
|
|
156
|
+
|
|
157
|
+
while (warningTextWidth > maxWidth && warningFontSize > 10) {
|
|
158
|
+
warningFontSize -= 2;
|
|
159
|
+
ctx.font = `${warningFontSize}px Arial`;
|
|
160
|
+
warningTextWidth = ctx.measureText(warningText).width;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const padding = warningFontSize * 0.8;
|
|
164
|
+
const textX = width * 0.05;
|
|
165
|
+
|
|
166
|
+
const positions = [
|
|
167
|
+
{ x: textX, y: height * 0.15 },
|
|
168
|
+
{ x: textX, y: height * 0.6 }
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
positions.forEach(({ x, y }) => {
|
|
172
|
+
const boxWidth = warningTextWidth + padding * 2;
|
|
173
|
+
const boxHeight = warningFontSize + padding * 1.5;
|
|
174
|
+
const boxX = x - padding * 0.5;
|
|
175
|
+
const boxY = y - warningFontSize - padding;
|
|
176
|
+
|
|
177
|
+
ctx.globalAlpha = 0.5;
|
|
178
|
+
ctx.fillStyle = "black";
|
|
179
|
+
ctx.fillRect(boxX, boxY, boxWidth, boxHeight);
|
|
180
|
+
|
|
181
|
+
ctx.globalAlpha = 1;
|
|
182
|
+
ctx.fillStyle = "white";
|
|
183
|
+
ctx.fillText(warningText, x, y);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
resolve(canvas.toDataURL("image/png"));
|
|
187
|
+
};
|
|
188
|
+
img.src = `data:image/svg+xml;base64,${svgBtoa}`;
|
|
189
|
+
});
|
|
190
|
+
}
|