@pilio/gemini-watermark-remover 1.0.19 → 1.0.21

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,165 @@
1
+ const EPSILON = 1e-8;
2
+
3
+ function clamp(value, min, max) {
4
+ return Math.max(min, Math.min(max, value));
5
+ }
6
+
7
+ function gaussianKernel1D(sigma) {
8
+ if (!Number.isFinite(sigma) || sigma <= 0) return null;
9
+
10
+ const radius = Math.max(1, Math.ceil(sigma * 3));
11
+ const kernel = new Float32Array(radius * 2 + 1);
12
+ let sum = 0;
13
+
14
+ for (let i = -radius; i <= radius; i++) {
15
+ const value = Math.exp(-(i * i) / (2 * sigma * sigma));
16
+ kernel[i + radius] = value;
17
+ sum += value;
18
+ }
19
+
20
+ if (sum <= EPSILON) return null;
21
+
22
+ for (let i = 0; i < kernel.length; i++) {
23
+ kernel[i] /= sum;
24
+ }
25
+
26
+ return { kernel, radius };
27
+ }
28
+
29
+ function blurHorizontal(values, width, height, kernelInfo) {
30
+ const out = new Float32Array(values.length);
31
+ const { kernel, radius } = kernelInfo;
32
+
33
+ for (let y = 0; y < height; y++) {
34
+ const rowBase = y * width;
35
+ for (let x = 0; x < width; x++) {
36
+ let sum = 0;
37
+ for (let k = -radius; k <= radius; k++) {
38
+ const sx = clamp(x + k, 0, width - 1);
39
+ sum += values[rowBase + sx] * kernel[k + radius];
40
+ }
41
+ out[rowBase + x] = sum;
42
+ }
43
+ }
44
+
45
+ return out;
46
+ }
47
+
48
+ function blurVertical(values, width, height, kernelInfo) {
49
+ const out = new Float32Array(values.length);
50
+ const { kernel, radius } = kernelInfo;
51
+
52
+ for (let y = 0; y < height; y++) {
53
+ for (let x = 0; x < width; x++) {
54
+ let sum = 0;
55
+ for (let k = -radius; k <= radius; k++) {
56
+ const sy = clamp(y + k, 0, height - 1);
57
+ sum += values[sy * width + x] * kernel[k + radius];
58
+ }
59
+ out[y * width + x] = sum;
60
+ }
61
+ }
62
+
63
+ return out;
64
+ }
65
+
66
+ function gaussianBlur(values, width, height, sigma) {
67
+ const kernelInfo = gaussianKernel1D(sigma);
68
+ if (!kernelInfo) return new Float32Array(values);
69
+
70
+ return blurVertical(
71
+ blurHorizontal(values, width, height, kernelInfo),
72
+ width,
73
+ height,
74
+ kernelInfo
75
+ );
76
+ }
77
+
78
+ function dilate(values, width, height, radius) {
79
+ if (!Number.isFinite(radius) || radius <= 0) return new Float32Array(values);
80
+
81
+ const roundedRadius = Math.max(1, Math.round(radius));
82
+ const radiusSquared = roundedRadius * roundedRadius;
83
+ const out = new Float32Array(values.length);
84
+
85
+ for (let y = 0; y < height; y++) {
86
+ for (let x = 0; x < width; x++) {
87
+ let maxValue = 0;
88
+ for (let dy = -roundedRadius; dy <= roundedRadius; dy++) {
89
+ for (let dx = -roundedRadius; dx <= roundedRadius; dx++) {
90
+ if (dx * dx + dy * dy > radiusSquared) continue;
91
+ const sx = x + dx;
92
+ const sy = y + dy;
93
+ if (sx < 0 || sy < 0 || sx >= width || sy >= height) continue;
94
+ maxValue = Math.max(maxValue, values[sy * width + sx]);
95
+ }
96
+ }
97
+ out[y * width + x] = maxValue;
98
+ }
99
+ }
100
+
101
+ return out;
102
+ }
103
+
104
+ export function createAlphaGradientMask({
105
+ alphaMap,
106
+ width,
107
+ height = width,
108
+ strength = 1,
109
+ gamma = 0.5,
110
+ dilateRadius = 2,
111
+ blurSigma = 2
112
+ }) {
113
+ if (!alphaMap || width <= 0 || height <= 0 || alphaMap.length < width * height) {
114
+ return new Float32Array(0);
115
+ }
116
+
117
+ const gradient = new Float32Array(width * height);
118
+ let minGradient = Number.POSITIVE_INFINITY;
119
+ let maxGradient = 0;
120
+
121
+ for (let y = 1; y < height - 1; y++) {
122
+ for (let x = 1; x < width - 1; x++) {
123
+ const i = y * width + x;
124
+ const gx =
125
+ -alphaMap[i - width - 1] - 2 * alphaMap[i - 1] - alphaMap[i + width - 1] +
126
+ alphaMap[i - width + 1] + 2 * alphaMap[i + 1] + alphaMap[i + width + 1];
127
+ const gy =
128
+ -alphaMap[i - width - 1] - 2 * alphaMap[i - width] - alphaMap[i - width + 1] +
129
+ alphaMap[i + width - 1] + 2 * alphaMap[i + width] + alphaMap[i + width + 1];
130
+ const value = Math.sqrt(gx * gx + gy * gy);
131
+ gradient[i] = value;
132
+ minGradient = Math.min(minGradient, value);
133
+ maxGradient = Math.max(maxGradient, value);
134
+ }
135
+ }
136
+
137
+ if (!Number.isFinite(minGradient) || maxGradient <= minGradient + EPSILON) {
138
+ return new Float32Array(width * height);
139
+ }
140
+
141
+ const normalized = new Float32Array(width * height);
142
+ const exponent = Number.isFinite(gamma) && gamma > 0 ? gamma : 1;
143
+ for (let i = 0; i < normalized.length; i++) {
144
+ const value = (gradient[i] - minGradient) / (maxGradient - minGradient);
145
+ normalized[i] = Math.pow(clamp(value, 0, 1), exponent);
146
+ }
147
+
148
+ const expanded = dilate(normalized, width, height, dilateRadius);
149
+ const blurred = gaussianBlur(expanded, width, height, blurSigma);
150
+ const safeStrength = Number.isFinite(strength) ? Math.max(0, strength) : 1;
151
+
152
+ for (let i = 0; i < blurred.length; i++) {
153
+ blurred[i] = clamp(blurred[i] * safeStrength, 0, 1);
154
+ }
155
+
156
+ return blurred;
157
+ }
158
+
159
+ export function getAlphaGradientWeight(mask, index, floor = 0.35) {
160
+ if (!mask || index < 0 || index >= mask.length) {
161
+ return clamp(floor, 0, 1);
162
+ }
163
+
164
+ return Math.max(clamp(floor, 0, 1), clamp(mask[index], 0, 1));
165
+ }
@@ -37,11 +37,16 @@ export function removeWatermark(imageData, alphaMap, position, options = {}) {
37
37
  // Calculate index in alpha map
38
38
  const alphaIdx = row * width + col;
39
39
 
40
- // Get alpha value
40
+ // Get alpha value. A negative alpha map marks a dark-polarity
41
+ // watermark: same opacity mask, black logo value.
41
42
  const rawAlpha = alphaMap[alphaIdx];
43
+ const alphaMagnitude = Math.abs(rawAlpha);
44
+ const logoValue = Number.isFinite(options.logoValue)
45
+ ? options.logoValue
46
+ : (rawAlpha < 0 ? 0 : LOGO_VALUE);
42
47
 
43
48
  // Remove low-level alpha noise from compressed background capture.
44
- const signalAlpha = Math.max(0, rawAlpha - ALPHA_NOISE_FLOOR) * alphaGain;
49
+ const signalAlpha = Math.max(0, alphaMagnitude - ALPHA_NOISE_FLOOR) * alphaGain;
45
50
 
46
51
  // Skip very small alpha values (noise)
47
52
  if (signalAlpha < ALPHA_THRESHOLD) {
@@ -49,7 +54,7 @@ export function removeWatermark(imageData, alphaMap, position, options = {}) {
49
54
  }
50
55
 
51
56
  // Use original alpha for inverse solve; use denoised alpha as activation signal.
52
- const alpha = Math.min(rawAlpha * alphaGain, MAX_ALPHA);
57
+ const alpha = Math.min(alphaMagnitude * alphaGain, MAX_ALPHA);
53
58
  const oneMinusAlpha = 1.0 - alpha;
54
59
 
55
60
  // Apply reverse alpha blending to each RGB channel
@@ -57,7 +62,7 @@ export function removeWatermark(imageData, alphaMap, position, options = {}) {
57
62
  const watermarked = imageData.data[imgIdx + c];
58
63
 
59
64
  // Reverse alpha blending formula
60
- const original = (watermarked - alpha * LOGO_VALUE) / oneMinusAlpha;
65
+ const original = (watermarked - alpha * logoValue) / oneMinusAlpha;
61
66
 
62
67
  // Clip to [0, 255] range
63
68
  imageData.data[imgIdx + c] = Math.max(0, Math.min(255, Math.round(original)));