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.
@@ -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
+ }