@pilio/gemini-watermark-remover 1.0.10
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 +22 -0
- package/README.md +371 -0
- package/README_zh.md +371 -0
- package/bin/gwr.mjs +12 -0
- package/package.json +76 -0
- package/skills/gemini-watermark-remover/SKILL.md +28 -0
- package/skills/gemini-watermark-remover/agents/openai.yaml +3 -0
- package/skills/gemini-watermark-remover/references/inputs-and-outputs.md +9 -0
- package/skills/gemini-watermark-remover/references/limitations.md +5 -0
- package/skills/gemini-watermark-remover/references/usage.md +19 -0
- package/skills/gemini-watermark-remover/scripts/run.mjs +153 -0
- package/src/cli/gwrCli.js +17 -0
- package/src/cli/gwrRemoveCommand.js +317 -0
- package/src/core/adaptiveDetector.js +488 -0
- package/src/core/alphaMap.js +30 -0
- package/src/core/blendModes.js +70 -0
- package/src/core/candidateSelector.js +1446 -0
- package/src/core/canvasBlob.js +26 -0
- package/src/core/embeddedAlphaMaps.js +49 -0
- package/src/core/geminiSizeCatalog.js +239 -0
- package/src/core/multiPassRemoval.js +93 -0
- package/src/core/previewAlphaCalibration.js +822 -0
- package/src/core/restorationMetrics.js +251 -0
- package/src/core/selectionDebug.js +47 -0
- package/src/core/watermarkConfig.js +146 -0
- package/src/core/watermarkDecisionPolicy.js +163 -0
- package/src/core/watermarkDisplay.js +60 -0
- package/src/core/watermarkEngine.js +150 -0
- package/src/core/watermarkPresence.js +12 -0
- package/src/core/watermarkProcessor.js +875 -0
- package/src/core/workerClient.js +114 -0
- package/src/sdk/browser.d.ts +13 -0
- package/src/sdk/browser.js +29 -0
- package/src/sdk/image-data.d.ts +14 -0
- package/src/sdk/image-data.js +55 -0
- package/src/sdk/index.d.ts +144 -0
- package/src/sdk/index.js +8 -0
- package/src/sdk/node.d.ts +42 -0
- package/src/sdk/node.js +77 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runRemoveCommand } from './gwrRemoveCommand.js';
|
|
2
|
+
|
|
3
|
+
export async function main(argv, io) {
|
|
4
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
5
|
+
io.stdout.write('Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json]\n');
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const [command, ...rest] = argv;
|
|
10
|
+
|
|
11
|
+
if (command !== 'remove') {
|
|
12
|
+
io.stderr.write(`Unknown command: ${command}\n`);
|
|
13
|
+
return 2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return runRemoveCommand(rest, io);
|
|
17
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { inferMimeTypeFromPath, removeWatermarkFromFile } from '../sdk/node.js';
|
|
5
|
+
|
|
6
|
+
const REMOVE_USAGE =
|
|
7
|
+
'Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json]';
|
|
8
|
+
|
|
9
|
+
export async function runRemoveCommand(argv, io) {
|
|
10
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
11
|
+
io.stdout.write(`${REMOVE_USAGE}\n`);
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const options = parseRemoveArgs(argv);
|
|
16
|
+
if (!options.ok) {
|
|
17
|
+
io.stderr.write(`${options.error}\n`);
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const codec = await resolveCodecOptions(options).catch((error) => {
|
|
22
|
+
io.stderr.write(`${error.message}\n`);
|
|
23
|
+
return null;
|
|
24
|
+
});
|
|
25
|
+
if (!codec) {
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const commandOptions = { ...options, codec };
|
|
30
|
+
const inputStats = await stat(commandOptions.input).catch(() => null);
|
|
31
|
+
if (!inputStats) {
|
|
32
|
+
io.stderr.write(`Input not found: ${commandOptions.input}\n`);
|
|
33
|
+
return 3;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (inputStats.isDirectory() && commandOptions.output) {
|
|
37
|
+
io.stderr.write('Directory input requires --out-dir and does not support --output.\n');
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let results = null;
|
|
42
|
+
try {
|
|
43
|
+
results = inputStats.isDirectory()
|
|
44
|
+
? await processDirectory(commandOptions)
|
|
45
|
+
: [await processOneFile(commandOptions.input, resolveSingleFileOutput(commandOptions), commandOptions)];
|
|
46
|
+
} catch (error) {
|
|
47
|
+
io.stderr.write(`${error.message}\n`);
|
|
48
|
+
return 4;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (commandOptions.json) {
|
|
52
|
+
io.stdout.write(`${JSON.stringify(results.length === 1 ? results[0] : results)}\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseRemoveArgs(argv) {
|
|
59
|
+
const options = {
|
|
60
|
+
ok: true,
|
|
61
|
+
input: null,
|
|
62
|
+
output: null,
|
|
63
|
+
outDir: null,
|
|
64
|
+
overwrite: false,
|
|
65
|
+
json: false,
|
|
66
|
+
decoder: null,
|
|
67
|
+
encoder: null
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let parseOptions = true;
|
|
71
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
72
|
+
const token = argv[index];
|
|
73
|
+
|
|
74
|
+
if (parseOptions && token === '--') {
|
|
75
|
+
parseOptions = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!parseOptions || !token.startsWith('--')) {
|
|
80
|
+
if (options.input) {
|
|
81
|
+
return { ok: false, error: `Unexpected argument: ${token}` };
|
|
82
|
+
}
|
|
83
|
+
options.input = token;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (token === '--output') {
|
|
88
|
+
const parsed = parseOptionValue(argv, index, '--output');
|
|
89
|
+
if (!parsed.ok) return parsed;
|
|
90
|
+
options.output = parsed.value;
|
|
91
|
+
index = parsed.index;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (token === '--out-dir') {
|
|
96
|
+
const parsed = parseOptionValue(argv, index, '--out-dir');
|
|
97
|
+
if (!parsed.ok) return parsed;
|
|
98
|
+
options.outDir = parsed.value;
|
|
99
|
+
index = parsed.index;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (token === '--decoder') {
|
|
104
|
+
const parsed = parseOptionValue(argv, index, '--decoder');
|
|
105
|
+
if (!parsed.ok) return parsed;
|
|
106
|
+
options.decoder = parsed.value;
|
|
107
|
+
index = parsed.index;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (token === '--encoder') {
|
|
112
|
+
const parsed = parseOptionValue(argv, index, '--encoder');
|
|
113
|
+
if (!parsed.ok) return parsed;
|
|
114
|
+
options.encoder = parsed.value;
|
|
115
|
+
index = parsed.index;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (token === '--overwrite') {
|
|
120
|
+
options.overwrite = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (token === '--json') {
|
|
125
|
+
options.json = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { ok: false, error: `Unknown option: ${token}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!options.input) {
|
|
133
|
+
return { ok: false, error: 'Missing required argument: <input>' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.output && options.outDir) {
|
|
137
|
+
return { ok: false, error: 'Use either --output or --out-dir, not both.' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!options.output && !options.outDir) {
|
|
141
|
+
return { ok: false, error: 'Missing output target. Use --output <file> or --out-dir <dir>.' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ((options.decoder && !options.encoder) || (!options.decoder && options.encoder)) {
|
|
145
|
+
return { ok: false, error: '--decoder and --encoder must be used together.' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return options;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseOptionValue(argv, optionIndex, optionName) {
|
|
152
|
+
const nextToken = argv[optionIndex + 1];
|
|
153
|
+
if (!nextToken) {
|
|
154
|
+
return { ok: false, error: `Missing value for ${optionName}` };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (nextToken === '--') {
|
|
158
|
+
const escapedValue = argv[optionIndex + 2];
|
|
159
|
+
if (!escapedValue) {
|
|
160
|
+
return { ok: false, error: `Missing value for ${optionName}` };
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, value: escapedValue, index: optionIndex + 2 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (nextToken.startsWith('--')) {
|
|
166
|
+
return { ok: false, error: `Missing value for ${optionName}` };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { ok: true, value: nextToken, index: optionIndex + 1 };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function resolveCodecOptions(options) {
|
|
173
|
+
if (!options.decoder && !options.encoder) {
|
|
174
|
+
return createSharpCodec();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const decoderPreset = resolveCodecPreset(options.decoder);
|
|
178
|
+
const encoderPreset = resolveCodecPreset(options.encoder);
|
|
179
|
+
|
|
180
|
+
if (!decoderPreset || !decoderPreset.decodeImageData) {
|
|
181
|
+
throw new Error(`Unsupported decoder preset: ${options.decoder}`);
|
|
182
|
+
}
|
|
183
|
+
if (!encoderPreset || !encoderPreset.encodeImageData) {
|
|
184
|
+
throw new Error(`Unsupported encoder preset: ${options.encoder}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
decodeImageData: decoderPreset.decodeImageData,
|
|
189
|
+
encodeImageData: encoderPreset.encodeImageData
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveCodecPreset(name) {
|
|
194
|
+
if (name === 'synthetic') {
|
|
195
|
+
return {
|
|
196
|
+
decodeImageData(buffer) {
|
|
197
|
+
const payload = JSON.parse(Buffer.from(buffer).toString('utf8'));
|
|
198
|
+
return {
|
|
199
|
+
width: payload.width,
|
|
200
|
+
height: payload.height,
|
|
201
|
+
data: Uint8ClampedArray.from(payload.data)
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
encodeImageData(imageData) {
|
|
205
|
+
return Buffer.from(
|
|
206
|
+
JSON.stringify({
|
|
207
|
+
width: imageData.width,
|
|
208
|
+
height: imageData.height,
|
|
209
|
+
data: Array.from(imageData.data)
|
|
210
|
+
}),
|
|
211
|
+
'utf8'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function createSharpCodec() {
|
|
220
|
+
let sharpModule = null;
|
|
221
|
+
try {
|
|
222
|
+
sharpModule = await import('sharp');
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Image codec is unavailable. Failed to load "sharp": ${reason}`,
|
|
227
|
+
{ cause: error }
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sharp = sharpModule.default ?? sharpModule;
|
|
232
|
+
return {
|
|
233
|
+
async decodeImageData(buffer) {
|
|
234
|
+
const { data, info } = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
235
|
+
return {
|
|
236
|
+
width: info.width,
|
|
237
|
+
height: info.height,
|
|
238
|
+
data: Uint8ClampedArray.from(data)
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
async encodeImageData(imageData, context = {}) {
|
|
242
|
+
const format = resolveOutputFormat(context.mimeType, context.filePath);
|
|
243
|
+
let encoder = sharp(Buffer.from(imageData.data), {
|
|
244
|
+
raw: {
|
|
245
|
+
width: imageData.width,
|
|
246
|
+
height: imageData.height,
|
|
247
|
+
channels: 4
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (format === 'jpeg') {
|
|
252
|
+
encoder = encoder.jpeg({ quality: 95 });
|
|
253
|
+
} else if (format === 'webp') {
|
|
254
|
+
encoder = encoder.webp({ quality: 95 });
|
|
255
|
+
} else {
|
|
256
|
+
encoder = encoder.png();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return encoder.toBuffer();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveOutputFormat(mimeType = '', filePath = '') {
|
|
265
|
+
if (mimeType === 'image/jpeg') return 'jpeg';
|
|
266
|
+
if (mimeType === 'image/webp') return 'webp';
|
|
267
|
+
|
|
268
|
+
const ext = path.extname(filePath || '').toLowerCase();
|
|
269
|
+
if (ext === '.jpg' || ext === '.jpeg') return 'jpeg';
|
|
270
|
+
if (ext === '.webp') return 'webp';
|
|
271
|
+
return 'png';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveSingleFileOutput(options) {
|
|
275
|
+
if (options.output) return options.output;
|
|
276
|
+
return path.join(options.outDir, path.basename(options.input));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function processDirectory(options) {
|
|
280
|
+
await mkdir(options.outDir, { recursive: true });
|
|
281
|
+
const entries = await readdir(options.input, { withFileTypes: true });
|
|
282
|
+
const files = entries
|
|
283
|
+
.filter((entry) => entry.isFile())
|
|
284
|
+
.map((entry) => entry.name)
|
|
285
|
+
.sort((a, b) => a.localeCompare(b));
|
|
286
|
+
|
|
287
|
+
const results = [];
|
|
288
|
+
for (const fileName of files) {
|
|
289
|
+
const inputPath = path.join(options.input, fileName);
|
|
290
|
+
const outputPath = path.join(options.outDir, fileName);
|
|
291
|
+
results.push(await processOneFile(inputPath, outputPath, options));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return results;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function processOneFile(inputPath, outputPath, options) {
|
|
298
|
+
const outputStats = await stat(outputPath).catch(() => null);
|
|
299
|
+
if (outputStats && !options.overwrite) {
|
|
300
|
+
throw new Error(`Output already exists: ${outputPath}. Use --overwrite to replace it.`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
304
|
+
const mimeType = inferMimeTypeFromPath(outputPath);
|
|
305
|
+
const result = await removeWatermarkFromFile(inputPath, {
|
|
306
|
+
outputPath,
|
|
307
|
+
mimeType,
|
|
308
|
+
decodeImageData: options.codec.decodeImageData,
|
|
309
|
+
encodeImageData: options.codec.encodeImageData
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
input: inputPath,
|
|
314
|
+
output: outputPath,
|
|
315
|
+
meta: result.meta
|
|
316
|
+
};
|
|
317
|
+
}
|