@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.
Files changed (39) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +371 -0
  3. package/README_zh.md +371 -0
  4. package/bin/gwr.mjs +12 -0
  5. package/package.json +76 -0
  6. package/skills/gemini-watermark-remover/SKILL.md +28 -0
  7. package/skills/gemini-watermark-remover/agents/openai.yaml +3 -0
  8. package/skills/gemini-watermark-remover/references/inputs-and-outputs.md +9 -0
  9. package/skills/gemini-watermark-remover/references/limitations.md +5 -0
  10. package/skills/gemini-watermark-remover/references/usage.md +19 -0
  11. package/skills/gemini-watermark-remover/scripts/run.mjs +153 -0
  12. package/src/cli/gwrCli.js +17 -0
  13. package/src/cli/gwrRemoveCommand.js +317 -0
  14. package/src/core/adaptiveDetector.js +488 -0
  15. package/src/core/alphaMap.js +30 -0
  16. package/src/core/blendModes.js +70 -0
  17. package/src/core/candidateSelector.js +1446 -0
  18. package/src/core/canvasBlob.js +26 -0
  19. package/src/core/embeddedAlphaMaps.js +49 -0
  20. package/src/core/geminiSizeCatalog.js +239 -0
  21. package/src/core/multiPassRemoval.js +93 -0
  22. package/src/core/previewAlphaCalibration.js +822 -0
  23. package/src/core/restorationMetrics.js +251 -0
  24. package/src/core/selectionDebug.js +47 -0
  25. package/src/core/watermarkConfig.js +146 -0
  26. package/src/core/watermarkDecisionPolicy.js +163 -0
  27. package/src/core/watermarkDisplay.js +60 -0
  28. package/src/core/watermarkEngine.js +150 -0
  29. package/src/core/watermarkPresence.js +12 -0
  30. package/src/core/watermarkProcessor.js +875 -0
  31. package/src/core/workerClient.js +114 -0
  32. package/src/sdk/browser.d.ts +13 -0
  33. package/src/sdk/browser.js +29 -0
  34. package/src/sdk/image-data.d.ts +14 -0
  35. package/src/sdk/image-data.js +55 -0
  36. package/src/sdk/index.d.ts +144 -0
  37. package/src/sdk/index.js +8 -0
  38. package/src/sdk/node.d.ts +42 -0
  39. package/src/sdk/node.js +77 -0
@@ -0,0 +1,1446 @@
1
+ import { removeWatermark } from './blendModes.js';
2
+ import {
3
+ computeRegionGradientCorrelation,
4
+ computeRegionSpatialCorrelation,
5
+ detectAdaptiveWatermarkRegion,
6
+ interpolateAlphaMap,
7
+ shouldAttemptAdaptiveFallback,
8
+ warpAlphaMap
9
+ } from './adaptiveDetector.js';
10
+ import {
11
+ assessReferenceTextureAlignment,
12
+ assessReferenceTextureAlignmentFromStats,
13
+ calculateNearBlackRatio,
14
+ cloneImageData,
15
+ getRegionTextureStats,
16
+ scoreRegion
17
+ } from './restorationMetrics.js';
18
+ import {
19
+ hasReliableAdaptiveWatermarkSignal,
20
+ hasReliableStandardWatermarkSignal
21
+ } from './watermarkPresence.js';
22
+ import {
23
+ matchOfficialGeminiImageSize,
24
+ resolveGeminiWatermarkSearchConfigs
25
+ } from './geminiSizeCatalog.js';
26
+
27
+ const MAX_NEAR_BLACK_RATIO_INCREASE = 0.05;
28
+ const VALIDATION_MIN_IMPROVEMENT = 0.08;
29
+ const VALIDATION_TARGET_RESIDUAL = 0.22;
30
+ const VALIDATION_MAX_GRADIENT_INCREASE = 0.04;
31
+ const VALIDATION_MIN_CONFIDENCE_FOR_ADAPTIVE_TRIAL = 0.25;
32
+ const STANDARD_FAST_PATH_RESIDUAL_THRESHOLD = 0.22;
33
+ const STANDARD_FAST_PATH_GRADIENT_THRESHOLD = 0.08;
34
+ const STANDARD_NEARBY_SEARCH_RESIDUAL_THRESHOLD = 0.18;
35
+ const STANDARD_NEARBY_SEARCH_GRADIENT_THRESHOLD = 0.05;
36
+ const STANDARD_LOCAL_SHIFT_STRONG_BASE_GRADIENT_SCORE = 0.35;
37
+ const STANDARD_LOCAL_SHIFT_STRONG_BASE_SPATIAL_SCORE = 0.8;
38
+ const STANDARD_LOCAL_SHIFT_WEAK_CANDIDATE_GRADIENT_SCORE = 0.12;
39
+ const STANDARD_LOCAL_SHIFT_WEAK_CANDIDATE_SPATIAL_SCORE = 0.65;
40
+ const STANDARD_LOCAL_SHIFT_MIN_VALIDATION_ADVANTAGE = 0.3;
41
+ const STANDARD_LOCAL_SHIFT_SKIP_PROCESSED_GRADIENT_THRESHOLD = 0.02;
42
+ const STANDARD_LOCAL_SHIFT_PRESERVE_CLEAN_BASE_GRADIENT_THRESHOLD = 0.02;
43
+ const STANDARD_LOCAL_SHIFT_MAX_CANDIDATE_GRADIENT_FOR_CLEAN_BASE = 0.03;
44
+ const TEMPLATE_ALIGN_SHIFTS = [-0.5, -0.25, 0, 0.25, 0.5];
45
+ const TEMPLATE_ALIGN_SCALES = [0.99, 1, 1.01];
46
+ const STANDARD_NEARBY_SHIFTS = [-12, -8, -4, 0, 4, 8, 12];
47
+ const STANDARD_FINE_LOCAL_SHIFTS = [-2, -1, 0, 1, 2];
48
+ const STANDARD_SIZE_JITTERS = [-12, -10, -8, -6, -4, -2, 2, 4, 6, 8, 10, 12];
49
+ const PREVIEW_ANCHOR_MIN_SIZE = 24;
50
+ const PREVIEW_ANCHOR_MAX_SIZE_RATIO = 1.05;
51
+ const PREVIEW_ANCHOR_MIN_SIZE_RATIO = 0.55;
52
+ const PREVIEW_ANCHOR_MARGIN_WINDOW = 16;
53
+ const PREVIEW_ANCHOR_MARGIN_EXTENSION = 8;
54
+ const PREVIEW_ANCHOR_SIZE_STEP = 2;
55
+ const PREVIEW_ANCHOR_MARGIN_STEP = 2;
56
+ const PREVIEW_ANCHOR_TOP_K = 8;
57
+ const PREVIEW_ANCHOR_MIN_SCORE = 0.2;
58
+ const PREVIEW_ANCHOR_LOCAL_DELTAS = [-1, 0, 1];
59
+ const PREVIEW_TEMPLATE_ALIGN_SHIFTS = [-1, -0.5, 0, 0.5, 1];
60
+ const PREVIEW_TEMPLATE_ALIGN_SCALES = [0.985, 1, 1.015];
61
+ const PREVIEW_ANCHOR_GAIN_SKIP_RESIDUAL_THRESHOLD = 0.22;
62
+ const PREVIEW_ANCHOR_GAIN_SKIP_GRADIENT_THRESHOLD = 0.24;
63
+
64
+ export { assessReferenceTextureAlignment, calculateNearBlackRatio, scoreRegion } from './restorationMetrics.js';
65
+
66
+ const ORIGIN_REGION = Object.freeze({ x: 0, y: 0 });
67
+
68
+ function mergeCandidateProvenance(...provenanceParts) {
69
+ const merged = {};
70
+ for (const provenance of provenanceParts) {
71
+ if (!provenance || typeof provenance !== 'object') continue;
72
+ Object.assign(merged, provenance);
73
+ }
74
+
75
+ return Object.keys(merged).length > 0 ? merged : null;
76
+ }
77
+
78
+ function buildStandardCandidateSeeds({
79
+ originalImageData,
80
+ config,
81
+ position,
82
+ alpha48,
83
+ alpha96,
84
+ getAlphaMap,
85
+ resolveAlphaMap = null,
86
+ includeCatalogVariants = true
87
+ }) {
88
+ const configs = includeCatalogVariants
89
+ ? resolveGeminiWatermarkSearchConfigs(
90
+ originalImageData.width,
91
+ originalImageData.height,
92
+ config
93
+ )
94
+ : [config];
95
+ const seeds = [];
96
+
97
+ for (const candidateConfig of configs) {
98
+ const candidatePosition = candidateConfig === config
99
+ ? position
100
+ : {
101
+ x: originalImageData.width - candidateConfig.marginRight - candidateConfig.logoSize,
102
+ y: originalImageData.height - candidateConfig.marginBottom - candidateConfig.logoSize,
103
+ width: candidateConfig.logoSize,
104
+ height: candidateConfig.logoSize
105
+ };
106
+ if (
107
+ candidatePosition.x < 0 ||
108
+ candidatePosition.y < 0 ||
109
+ candidatePosition.x + candidatePosition.width > originalImageData.width ||
110
+ candidatePosition.y + candidatePosition.height > originalImageData.height
111
+ ) {
112
+ continue;
113
+ }
114
+
115
+ const alphaMap = typeof resolveAlphaMap === 'function'
116
+ ? resolveAlphaMap(candidateConfig.logoSize)
117
+ : resolveAlphaMapForSize(candidateConfig.logoSize, {
118
+ alpha48,
119
+ alpha96,
120
+ getAlphaMap
121
+ });
122
+ if (!alphaMap) continue;
123
+
124
+ seeds.push({
125
+ config: candidateConfig,
126
+ position: candidatePosition,
127
+ alphaMap,
128
+ source: candidateConfig === config ? 'standard' : 'standard+catalog',
129
+ provenance: candidateConfig === config ? null : { catalogVariant: true }
130
+ });
131
+ }
132
+
133
+ return seeds;
134
+ }
135
+
136
+ function inferDecisionTier(candidate, { directMatch = false } = {}) {
137
+ if (!candidate) return 'insufficient';
138
+ if (directMatch) return 'direct-match';
139
+ if (candidate.source?.includes('validated')) return 'validated-match';
140
+ if (candidate.accepted) return 'validated-match';
141
+ return 'safe-removal';
142
+ }
143
+
144
+ function shouldEscalateSearch(candidate) {
145
+ if (!candidate) return true;
146
+
147
+ return Math.abs(candidate.processedSpatialScore) > STANDARD_FAST_PATH_RESIDUAL_THRESHOLD ||
148
+ Math.max(0, candidate.processedGradientScore) > STANDARD_FAST_PATH_GRADIENT_THRESHOLD;
149
+ }
150
+
151
+ function shouldSearchNearbyStandardCandidate(candidate, originalImageData) {
152
+ if (!candidate) return true;
153
+
154
+ return Number(candidate.position?.width) >= 72 &&
155
+ Number(originalImageData?.height) > Number(originalImageData?.width) * 1.25 &&
156
+ (
157
+ Math.abs(candidate.processedSpatialScore) > STANDARD_NEARBY_SEARCH_RESIDUAL_THRESHOLD ||
158
+ Math.max(0, candidate.processedGradientScore) > STANDARD_NEARBY_SEARCH_GRADIENT_THRESHOLD
159
+ );
160
+ }
161
+
162
+ function resolveAlphaMapForSize(size, { alpha48, alpha96, getAlphaMap } = {}) {
163
+ if (size === 48) return alpha48;
164
+ if (size === 96) return alpha96;
165
+
166
+ const provided = typeof getAlphaMap === 'function' ? getAlphaMap(size) : null;
167
+ if (provided) return provided;
168
+
169
+ return alpha96 ? interpolateAlphaMap(alpha96, 96, size) : null;
170
+ }
171
+
172
+ function createAlphaMapResolver({ alpha48, alpha96, getAlphaMap }) {
173
+ const cache = new Map();
174
+
175
+ return (size) => {
176
+ if (cache.has(size)) {
177
+ return cache.get(size);
178
+ }
179
+
180
+ const resolved = resolveAlphaMapForSize(size, {
181
+ alpha48,
182
+ alpha96,
183
+ getAlphaMap
184
+ });
185
+ cache.set(size, resolved);
186
+ return resolved;
187
+ };
188
+ }
189
+
190
+ function isPreviewAnchorGainSearchRequired(candidate) {
191
+ if (!candidate) return true;
192
+
193
+ return Math.abs(candidate.processedSpatialScore) > PREVIEW_ANCHOR_GAIN_SKIP_RESIDUAL_THRESHOLD ||
194
+ Math.max(0, candidate.processedGradientScore) > PREVIEW_ANCHOR_GAIN_SKIP_GRADIENT_THRESHOLD;
195
+ }
196
+
197
+ export function evaluateRestorationCandidate({
198
+ originalImageData,
199
+ alphaMap,
200
+ position,
201
+ source,
202
+ config,
203
+ baselineNearBlackRatio,
204
+ adaptiveConfidence = null,
205
+ alphaGain = 1,
206
+ provenance = null,
207
+ includeImageData = true
208
+ }) {
209
+ if (!alphaMap || !position) return null;
210
+
211
+ const originalScores = scoreRegion(originalImageData, alphaMap, position);
212
+ const regionImageData = createCandidateRegionImageData({
213
+ originalImageData,
214
+ alphaMap,
215
+ position,
216
+ alphaGain
217
+ });
218
+ const regionPosition = {
219
+ x: ORIGIN_REGION.x,
220
+ y: ORIGIN_REGION.y,
221
+ width: position.width,
222
+ height: position.height
223
+ };
224
+ const processedScores = scoreRegion(regionImageData, alphaMap, regionPosition);
225
+ const nearBlackRatio = calculateNearBlackRatio(regionImageData, regionPosition);
226
+ const nearBlackIncrease = nearBlackRatio - baselineNearBlackRatio;
227
+ // Signed suppression keeps legitimate "slight overshoot" restores eligible.
228
+ const improvement = originalScores.spatialScore - processedScores.spatialScore;
229
+ const gradientIncrease = processedScores.gradientScore - originalScores.gradientScore;
230
+ const textureAssessment = assessReferenceTextureAlignmentFromStats({
231
+ originalImageData,
232
+ referenceImageData: originalImageData,
233
+ candidateTextureStats: getRegionTextureStats(regionImageData, regionPosition),
234
+ position
235
+ });
236
+ const texturePenalty = textureAssessment.texturePenalty;
237
+ const accepted =
238
+ textureAssessment.hardReject !== true &&
239
+ nearBlackIncrease <= MAX_NEAR_BLACK_RATIO_INCREASE &&
240
+ improvement >= VALIDATION_MIN_IMPROVEMENT &&
241
+ (
242
+ Math.abs(processedScores.spatialScore) <= VALIDATION_TARGET_RESIDUAL ||
243
+ gradientIncrease <= VALIDATION_MAX_GRADIENT_INCREASE
244
+ );
245
+
246
+ return {
247
+ accepted,
248
+ source,
249
+ config,
250
+ position,
251
+ alphaMap,
252
+ adaptiveConfidence,
253
+ alphaGain,
254
+ provenance: mergeCandidateProvenance(provenance),
255
+ imageData: includeImageData
256
+ ? materializeCandidateImageData(originalImageData, alphaMap, position, alphaGain)
257
+ : null,
258
+ originalSpatialScore: originalScores.spatialScore,
259
+ originalGradientScore: originalScores.gradientScore,
260
+ processedSpatialScore: processedScores.spatialScore,
261
+ processedGradientScore: processedScores.gradientScore,
262
+ improvement,
263
+ nearBlackRatio,
264
+ nearBlackIncrease,
265
+ gradientIncrease,
266
+ tooDark: textureAssessment.tooDark,
267
+ tooFlat: textureAssessment.tooFlat,
268
+ hardReject: textureAssessment.hardReject,
269
+ texturePenalty,
270
+ validationCost:
271
+ Math.abs(processedScores.spatialScore) +
272
+ Math.max(0, processedScores.gradientScore) * 0.6 +
273
+ Math.max(0, nearBlackIncrease) * 3 +
274
+ texturePenalty
275
+ };
276
+ }
277
+
278
+ function pickBestValidatedCandidate(candidates) {
279
+ const accepted = candidates.filter((candidate) => candidate?.accepted);
280
+ if (accepted.length === 0) return null;
281
+
282
+ accepted.sort((a, b) => {
283
+ if (a.validationCost !== b.validationCost) {
284
+ return a.validationCost - b.validationCost;
285
+ }
286
+
287
+ return b.improvement - a.improvement;
288
+ });
289
+
290
+ return accepted[0];
291
+ }
292
+
293
+ function createCandidateRegionImageData({
294
+ originalImageData,
295
+ alphaMap,
296
+ position,
297
+ alphaGain
298
+ }) {
299
+ const regionImageData = {
300
+ width: position.width,
301
+ height: position.height,
302
+ data: new Uint8ClampedArray(position.width * position.height * 4)
303
+ };
304
+
305
+ for (let row = 0; row < position.height; row++) {
306
+ const srcStart = ((position.y + row) * originalImageData.width + position.x) * 4;
307
+ const srcEnd = srcStart + position.width * 4;
308
+ const destStart = row * position.width * 4;
309
+ regionImageData.data.set(originalImageData.data.subarray(srcStart, srcEnd), destStart);
310
+ }
311
+
312
+ removeWatermark(regionImageData, alphaMap, {
313
+ x: 0,
314
+ y: 0,
315
+ width: position.width,
316
+ height: position.height
317
+ }, { alphaGain });
318
+
319
+ return regionImageData;
320
+ }
321
+
322
+ function materializeCandidateImageData(originalImageData, alphaMap, position, alphaGain) {
323
+ const candidateImageData = cloneImageData(originalImageData);
324
+ removeWatermark(candidateImageData, alphaMap, position, { alphaGain });
325
+ return candidateImageData;
326
+ }
327
+
328
+ function ensureCandidateImageData(candidate, originalImageData) {
329
+ if (!candidate) return candidate;
330
+ if (candidate.imageData) return candidate;
331
+
332
+ return {
333
+ ...candidate,
334
+ imageData: materializeCandidateImageData(
335
+ originalImageData,
336
+ candidate.alphaMap,
337
+ candidate.position,
338
+ candidate.alphaGain ?? 1
339
+ )
340
+ };
341
+ }
342
+
343
+ export function pickBetterCandidate(currentBest, candidate, minCostDelta = 0.005) {
344
+ if (!candidate?.accepted) return currentBest;
345
+ if (!currentBest) return candidate;
346
+ if (shouldPreserveStrongStandardAnchor(currentBest, candidate)) {
347
+ return currentBest;
348
+ }
349
+ if (shouldPreferPreviewAnchorCandidate(currentBest, candidate)) {
350
+ return candidate;
351
+ }
352
+ if (shouldPreferPreviewAnchorCandidate(candidate, currentBest)) {
353
+ return currentBest;
354
+ }
355
+ if (candidate.validationCost < currentBest.validationCost - minCostDelta) {
356
+ return candidate;
357
+ }
358
+ if (Math.abs(candidate.validationCost - currentBest.validationCost) <= minCostDelta &&
359
+ candidate.improvement > currentBest.improvement + 0.01) {
360
+ return candidate;
361
+ }
362
+ return currentBest;
363
+ }
364
+
365
+ function isStandardCandidateSource(candidate) {
366
+ return typeof candidate?.source === 'string' && candidate.source.startsWith('standard');
367
+ }
368
+
369
+ function isDriftedStandardCandidate(candidate) {
370
+ return isStandardCandidateSource(candidate) &&
371
+ (
372
+ candidate?.provenance?.localShift === true ||
373
+ candidate?.provenance?.sizeJitter === true ||
374
+ String(candidate?.source || '').includes('+warp')
375
+ );
376
+ }
377
+
378
+ function isCanonicalStandardCandidate(candidate) {
379
+ return isStandardCandidateSource(candidate) &&
380
+ candidate?.provenance?.localShift !== true &&
381
+ candidate?.provenance?.sizeJitter !== true;
382
+ }
383
+
384
+ function hasStrongCanonicalAnchorSignal(candidate) {
385
+ const baseSpatial = Number(candidate?.originalSpatialScore);
386
+ const baseGradient = Number(candidate?.originalGradientScore);
387
+ if (!Number.isFinite(baseSpatial) || !Number.isFinite(baseGradient)) {
388
+ return false;
389
+ }
390
+ return baseGradient >= STANDARD_LOCAL_SHIFT_STRONG_BASE_GRADIENT_SCORE ||
391
+ baseSpatial >= STANDARD_LOCAL_SHIFT_STRONG_BASE_SPATIAL_SCORE;
392
+ }
393
+
394
+ function hasWeakDriftEvidence(candidate) {
395
+ const candidateSpatial = Number(candidate?.originalSpatialScore);
396
+ const candidateGradient = Number(candidate?.originalGradientScore);
397
+ if (!Number.isFinite(candidateSpatial) || !Number.isFinite(candidateGradient)) {
398
+ return false;
399
+ }
400
+ return candidateGradient < STANDARD_LOCAL_SHIFT_WEAK_CANDIDATE_GRADIENT_SCORE ||
401
+ candidateSpatial < STANDARD_LOCAL_SHIFT_WEAK_CANDIDATE_SPATIAL_SCORE;
402
+ }
403
+
404
+ function leavesWorseResidualGradientThanCanonical(canonicalCandidate, driftCandidate) {
405
+ const canonicalProcessedGradientRaw = Number(canonicalCandidate?.processedGradientScore);
406
+ const driftProcessedGradientRaw = Number(driftCandidate?.processedGradientScore);
407
+ if (
408
+ !Number.isFinite(canonicalProcessedGradientRaw) ||
409
+ !Number.isFinite(driftProcessedGradientRaw)
410
+ ) {
411
+ return false;
412
+ }
413
+
414
+ return Math.max(0, canonicalProcessedGradientRaw) <= STANDARD_LOCAL_SHIFT_PRESERVE_CLEAN_BASE_GRADIENT_THRESHOLD &&
415
+ Math.max(0, driftProcessedGradientRaw) >= STANDARD_LOCAL_SHIFT_MAX_CANDIDATE_GRADIENT_FOR_CLEAN_BASE;
416
+ }
417
+
418
+ function shouldPreserveCanonicalAnchor(canonicalCandidate, driftCandidate) {
419
+ if (!isCanonicalStandardCandidate(canonicalCandidate)) return false;
420
+ if (!isDriftedStandardCandidate(driftCandidate)) return false;
421
+
422
+ const validationAdvantage = Number(canonicalCandidate.validationCost) - Number(driftCandidate.validationCost);
423
+ if (
424
+ !Number.isFinite(validationAdvantage)
425
+ ) {
426
+ return false;
427
+ }
428
+
429
+ return (
430
+ hasStrongCanonicalAnchorSignal(canonicalCandidate) &&
431
+ hasWeakDriftEvidence(driftCandidate) &&
432
+ validationAdvantage < STANDARD_LOCAL_SHIFT_MIN_VALIDATION_ADVANTAGE
433
+ ) || leavesWorseResidualGradientThanCanonical(canonicalCandidate, driftCandidate);
434
+ }
435
+
436
+ function shouldPreserveStrongStandardAnchor(currentBest, candidate) {
437
+ if (currentBest?.provenance?.localShift === true) return false;
438
+ if (!isStandardCandidateSource(candidate)) return false;
439
+ return shouldPreserveCanonicalAnchor(currentBest, candidate);
440
+ }
441
+
442
+ function shouldRevertLocalShiftToStandardTrial(selectedCandidate, standardTrial) {
443
+ if (selectedCandidate?.provenance?.localShift !== true) return false;
444
+ if (!isStandardCandidateSource(selectedCandidate) || !isStandardCandidateSource(standardTrial)) return false;
445
+ if (!standardTrial?.accepted) return false;
446
+ return shouldPreserveCanonicalAnchor(standardTrial, selectedCandidate);
447
+ }
448
+
449
+ function shouldSkipStandardLocalSearch(seedCandidate) {
450
+ if (!seedCandidate) return false;
451
+
452
+ return Math.max(0, Number(seedCandidate.processedGradientScore)) <=
453
+ STANDARD_LOCAL_SHIFT_SKIP_PROCESSED_GRADIENT_THRESHOLD;
454
+ }
455
+
456
+ function isPreviewAnchorSearchEligible(originalImageData, config) {
457
+ if (!config || config.logoSize !== 48) return false;
458
+
459
+ const width = Number(originalImageData?.width);
460
+ const height = Number(originalImageData?.height);
461
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return false;
462
+ if (width < 384 || width > 1536) return false;
463
+ if (height < 384 || height > 1536) return false;
464
+ if (Math.max(width, height) < 512) return false;
465
+
466
+ return matchOfficialGeminiImageSize(width, height) === null;
467
+ }
468
+
469
+ function shouldPreferPreviewAnchorCandidate(currentBest, candidate) {
470
+ if (candidate?.provenance?.previewAnchor !== true) return false;
471
+ if (!currentBest || currentBest?.provenance?.previewAnchor === true) return false;
472
+
473
+ const currentSpatial = Number(currentBest.originalSpatialScore);
474
+ const currentGradient = Number(currentBest.originalGradientScore);
475
+ const candidateSpatial = Number(candidate.originalSpatialScore);
476
+ const candidateGradient = Number(candidate.originalGradientScore);
477
+
478
+ if (
479
+ !Number.isFinite(currentSpatial) ||
480
+ !Number.isFinite(currentGradient) ||
481
+ !Number.isFinite(candidateSpatial) ||
482
+ !Number.isFinite(candidateGradient)
483
+ ) {
484
+ return false;
485
+ }
486
+
487
+ const currentReliable = hasReliableStandardWatermarkSignal({
488
+ spatialScore: currentSpatial,
489
+ gradientScore: currentGradient
490
+ });
491
+ const candidateReliable = hasReliableStandardWatermarkSignal({
492
+ spatialScore: candidateSpatial,
493
+ gradientScore: candidateGradient
494
+ });
495
+
496
+ if (candidateReliable && !currentReliable) {
497
+ return true;
498
+ }
499
+
500
+ return candidateGradient >= currentGradient + 0.2 &&
501
+ candidateSpatial >= currentSpatial + 0.05;
502
+ }
503
+
504
+ function findBestTemplateWarp({
505
+ originalImageData,
506
+ alphaMap,
507
+ position,
508
+ baselineSpatialScore,
509
+ baselineGradientScore,
510
+ shiftCandidates = TEMPLATE_ALIGN_SHIFTS,
511
+ scaleCandidates = TEMPLATE_ALIGN_SCALES
512
+ }) {
513
+ const size = position.width;
514
+ if (!size || size <= 8) return null;
515
+
516
+ let best = {
517
+ spatialScore: baselineSpatialScore,
518
+ gradientScore: baselineGradientScore,
519
+ shift: { dx: 0, dy: 0, scale: 1 },
520
+ alphaMap
521
+ };
522
+
523
+ for (const scale of scaleCandidates) {
524
+ for (const dy of shiftCandidates) {
525
+ for (const dx of shiftCandidates) {
526
+ if (dx === 0 && dy === 0 && scale === 1) continue;
527
+ const warped = warpAlphaMap(alphaMap, size, { dx, dy, scale });
528
+ const spatialScore = computeRegionSpatialCorrelation({
529
+ imageData: originalImageData,
530
+ alphaMap: warped,
531
+ region: { x: position.x, y: position.y, size }
532
+ });
533
+ const gradientScore = computeRegionGradientCorrelation({
534
+ imageData: originalImageData,
535
+ alphaMap: warped,
536
+ region: { x: position.x, y: position.y, size }
537
+ });
538
+
539
+ const confidence =
540
+ Math.max(0, spatialScore) * 0.7 +
541
+ Math.max(0, gradientScore) * 0.3;
542
+ const bestConfidence =
543
+ Math.max(0, best.spatialScore) * 0.7 +
544
+ Math.max(0, best.gradientScore) * 0.3;
545
+
546
+ if (confidence > bestConfidence + 0.01) {
547
+ best = {
548
+ spatialScore,
549
+ gradientScore,
550
+ shift: { dx, dy, scale },
551
+ alphaMap: warped
552
+ };
553
+ }
554
+ }
555
+ }
556
+ }
557
+
558
+ const improvedSpatial = best.spatialScore >= baselineSpatialScore + 0.01;
559
+ const improvedGradient = best.gradientScore >= baselineGradientScore + 0.01;
560
+ return improvedSpatial || improvedGradient ? best : null;
561
+ }
562
+
563
+ function searchNearbyStandardCandidate({
564
+ originalImageData,
565
+ candidateSeeds,
566
+ adaptiveConfidence = null
567
+ }) {
568
+ if (!Array.isArray(candidateSeeds) || candidateSeeds.length === 0) return null;
569
+
570
+ let bestCandidate = null;
571
+ for (const seed of candidateSeeds) {
572
+ if (shouldSkipStandardLocalSearch(seed)) continue;
573
+ for (const dy of STANDARD_NEARBY_SHIFTS) {
574
+ for (const dx of STANDARD_NEARBY_SHIFTS) {
575
+ if (dx === 0 && dy === 0) continue;
576
+
577
+ const candidatePosition = {
578
+ x: seed.position.x + dx,
579
+ y: seed.position.y + dy,
580
+ width: seed.position.width,
581
+ height: seed.position.height
582
+ };
583
+ if (candidatePosition.x < 0 || candidatePosition.y < 0) continue;
584
+ if (candidatePosition.x + candidatePosition.width > originalImageData.width) continue;
585
+ if (candidatePosition.y + candidatePosition.height > originalImageData.height) continue;
586
+
587
+ const candidate = evaluateRestorationCandidate({
588
+ originalImageData,
589
+ alphaMap: seed.alphaMap,
590
+ position: candidatePosition,
591
+ source: `${seed.source}+local`,
592
+ config: seed.config,
593
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, candidatePosition),
594
+ adaptiveConfidence,
595
+ provenance: mergeCandidateProvenance(seed.provenance, { localShift: true }),
596
+ includeImageData: false
597
+ });
598
+
599
+ if (!candidate?.accepted) continue;
600
+ bestCandidate = pickBetterCandidate(bestCandidate, candidate, 0.002);
601
+ }
602
+ }
603
+ }
604
+
605
+ return bestCandidate;
606
+ }
607
+
608
+ function searchStandardSizeJitterCandidate({
609
+ originalImageData,
610
+ candidateSeeds,
611
+ alpha48,
612
+ alpha96,
613
+ getAlphaMap,
614
+ resolveAlphaMap = null,
615
+ adaptiveConfidence = null
616
+ }) {
617
+ if (!Array.isArray(candidateSeeds) || candidateSeeds.length === 0) return null;
618
+
619
+ let bestCandidate = null;
620
+ for (const seed of candidateSeeds) {
621
+ for (const delta of STANDARD_SIZE_JITTERS) {
622
+ const size = seed.position.width + delta;
623
+ if (size <= 24) continue;
624
+ if (size === seed.position.width) continue;
625
+
626
+ const candidatePosition = {
627
+ x: originalImageData.width - seed.config.marginRight - size,
628
+ y: originalImageData.height - seed.config.marginBottom - size,
629
+ width: size,
630
+ height: size
631
+ };
632
+ if (candidatePosition.x < 0 || candidatePosition.y < 0) continue;
633
+ if (candidatePosition.x + candidatePosition.width > originalImageData.width) continue;
634
+ if (candidatePosition.y + candidatePosition.height > originalImageData.height) continue;
635
+
636
+ const candidateAlphaMap = typeof resolveAlphaMap === 'function'
637
+ ? resolveAlphaMap(size)
638
+ : resolveAlphaMapForSize(size, {
639
+ alpha48,
640
+ alpha96,
641
+ getAlphaMap
642
+ });
643
+ if (!candidateAlphaMap) continue;
644
+
645
+ const candidate = evaluateRestorationCandidate({
646
+ originalImageData,
647
+ alphaMap: candidateAlphaMap,
648
+ position: candidatePosition,
649
+ source: `${seed.source}+size`,
650
+ config: {
651
+ logoSize: size,
652
+ marginRight: seed.config.marginRight,
653
+ marginBottom: seed.config.marginBottom
654
+ },
655
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, candidatePosition),
656
+ adaptiveConfidence,
657
+ provenance: mergeCandidateProvenance(seed.provenance, { sizeJitter: true }),
658
+ includeImageData: false
659
+ });
660
+
661
+ if (!candidate?.accepted) continue;
662
+ bestCandidate = pickBetterCandidate(bestCandidate, candidate, 0.002);
663
+ }
664
+ }
665
+
666
+ return bestCandidate;
667
+ }
668
+
669
+ function searchFineStandardLocalCandidate({
670
+ originalImageData,
671
+ seedCandidate,
672
+ adaptiveConfidence = null,
673
+ shiftCandidates = STANDARD_FINE_LOCAL_SHIFTS
674
+ }) {
675
+ if (!seedCandidate?.alphaMap || !seedCandidate?.position) return null;
676
+ if (shouldSkipStandardLocalSearch(seedCandidate)) return null;
677
+
678
+ let bestCandidate = null;
679
+ for (const dy of shiftCandidates) {
680
+ for (const dx of shiftCandidates) {
681
+ if (dx === 0 && dy === 0) continue;
682
+
683
+ const candidatePosition = {
684
+ x: seedCandidate.position.x + dx,
685
+ y: seedCandidate.position.y + dy,
686
+ width: seedCandidate.position.width,
687
+ height: seedCandidate.position.height
688
+ };
689
+ if (candidatePosition.x < 0 || candidatePosition.y < 0) continue;
690
+ if (candidatePosition.x + candidatePosition.width > originalImageData.width) continue;
691
+ if (candidatePosition.y + candidatePosition.height > originalImageData.height) continue;
692
+
693
+ const candidate = evaluateRestorationCandidate({
694
+ originalImageData,
695
+ alphaMap: seedCandidate.alphaMap,
696
+ position: candidatePosition,
697
+ source: `${seedCandidate.source}+local`,
698
+ config: seedCandidate.config,
699
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, candidatePosition),
700
+ adaptiveConfidence,
701
+ provenance: mergeCandidateProvenance(seedCandidate.provenance, { localShift: true }),
702
+ includeImageData: false
703
+ });
704
+
705
+ if (!candidate?.accepted) continue;
706
+ bestCandidate = pickBetterCandidate(bestCandidate, candidate, 0.002);
707
+ }
708
+ }
709
+
710
+ return bestCandidate;
711
+ }
712
+
713
+ function searchCandidateAlphaGain({
714
+ originalImageData,
715
+ seedCandidate,
716
+ adaptiveConfidence = null,
717
+ alphaGainCandidates = []
718
+ }) {
719
+ if (!seedCandidate?.alphaMap || !seedCandidate?.position) return null;
720
+
721
+ let bestCandidate = null;
722
+ for (const candidateGain of alphaGainCandidates) {
723
+ if (!Number.isFinite(candidateGain) || candidateGain <= 1) continue;
724
+
725
+ const candidate = evaluateRestorationCandidate({
726
+ originalImageData,
727
+ alphaMap: seedCandidate.alphaMap,
728
+ position: seedCandidate.position,
729
+ source: `${seedCandidate.source}+gain`,
730
+ config: seedCandidate.config,
731
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, seedCandidate.position),
732
+ adaptiveConfidence,
733
+ alphaGain: candidateGain,
734
+ provenance: seedCandidate.provenance,
735
+ includeImageData: false
736
+ });
737
+
738
+ if (!candidate?.accepted) continue;
739
+ bestCandidate = pickBetterCandidate(bestCandidate, candidate, 0.002);
740
+ }
741
+
742
+ return bestCandidate;
743
+ }
744
+
745
+ function insertTopPreviewCandidate(topCandidates, candidate) {
746
+ topCandidates.push(candidate);
747
+ topCandidates.sort((a, b) => b.coarseScore - a.coarseScore);
748
+ if (topCandidates.length > PREVIEW_ANCHOR_TOP_K) {
749
+ topCandidates.length = PREVIEW_ANCHOR_TOP_K;
750
+ }
751
+ }
752
+
753
+ function searchBottomRightPreviewCandidate({
754
+ originalImageData,
755
+ config,
756
+ alpha48,
757
+ alpha96,
758
+ getAlphaMap,
759
+ resolveAlphaMap = null,
760
+ adaptiveConfidence = null
761
+ }) {
762
+ if (!isPreviewAnchorSearchEligible(originalImageData, config)) return null;
763
+
764
+ const minSize = Math.max(
765
+ PREVIEW_ANCHOR_MIN_SIZE,
766
+ Math.round(config.logoSize * PREVIEW_ANCHOR_MIN_SIZE_RATIO)
767
+ );
768
+ const maxSize = Math.max(
769
+ minSize,
770
+ Math.round(config.logoSize * PREVIEW_ANCHOR_MAX_SIZE_RATIO)
771
+ );
772
+ const minMarginRight = Math.max(8, config.marginRight - PREVIEW_ANCHOR_MARGIN_WINDOW);
773
+ const maxMarginRight = config.marginRight + PREVIEW_ANCHOR_MARGIN_EXTENSION;
774
+ const minMarginBottom = Math.max(8, config.marginBottom - PREVIEW_ANCHOR_MARGIN_WINDOW);
775
+ const maxMarginBottom = config.marginBottom + PREVIEW_ANCHOR_MARGIN_EXTENSION;
776
+ const topCandidates = [];
777
+
778
+ for (let size = minSize; size <= maxSize; size += PREVIEW_ANCHOR_SIZE_STEP) {
779
+ const alphaMap = typeof resolveAlphaMap === 'function'
780
+ ? resolveAlphaMap(size)
781
+ : resolveAlphaMapForSize(size, {
782
+ alpha48,
783
+ alpha96,
784
+ getAlphaMap
785
+ });
786
+ if (!alphaMap) continue;
787
+
788
+ for (let marginRight = minMarginRight; marginRight <= maxMarginRight; marginRight += PREVIEW_ANCHOR_MARGIN_STEP) {
789
+ const x = originalImageData.width - marginRight - size;
790
+ if (x < 0 || x + size > originalImageData.width) continue;
791
+
792
+ for (let marginBottom = minMarginBottom; marginBottom <= maxMarginBottom; marginBottom += PREVIEW_ANCHOR_MARGIN_STEP) {
793
+ const y = originalImageData.height - marginBottom - size;
794
+ if (y < 0 || y + size > originalImageData.height) continue;
795
+
796
+ const coarseSpatialScore = computeRegionSpatialCorrelation({
797
+ imageData: originalImageData,
798
+ alphaMap,
799
+ region: { x, y, size }
800
+ });
801
+ const coarseGradientScore = computeRegionGradientCorrelation({
802
+ imageData: originalImageData,
803
+ alphaMap,
804
+ region: { x, y, size }
805
+ });
806
+ const coarseScore =
807
+ Math.max(0, coarseGradientScore) * 0.6 +
808
+ Math.max(0, coarseSpatialScore) * 0.4;
809
+
810
+ if (coarseScore < PREVIEW_ANCHOR_MIN_SCORE) continue;
811
+
812
+ insertTopPreviewCandidate(topCandidates, {
813
+ coarseScore,
814
+ alphaMap,
815
+ position: { x, y, width: size, height: size },
816
+ config: {
817
+ logoSize: size,
818
+ marginRight,
819
+ marginBottom
820
+ }
821
+ });
822
+ }
823
+ }
824
+ }
825
+
826
+ let bestCandidate = null;
827
+ for (const coarseCandidate of topCandidates) {
828
+ for (const sizeDelta of PREVIEW_ANCHOR_LOCAL_DELTAS) {
829
+ const size = coarseCandidate.position.width + sizeDelta;
830
+ if (size < PREVIEW_ANCHOR_MIN_SIZE) continue;
831
+
832
+ const alphaMap = typeof resolveAlphaMap === 'function'
833
+ ? resolveAlphaMap(size)
834
+ : resolveAlphaMapForSize(size, {
835
+ alpha48,
836
+ alpha96,
837
+ getAlphaMap
838
+ });
839
+ if (!alphaMap) continue;
840
+
841
+ for (const dx of PREVIEW_ANCHOR_LOCAL_DELTAS) {
842
+ for (const dy of PREVIEW_ANCHOR_LOCAL_DELTAS) {
843
+ const position = {
844
+ x: coarseCandidate.position.x + dx,
845
+ y: coarseCandidate.position.y + dy,
846
+ width: size,
847
+ height: size
848
+ };
849
+ if (position.x < 0 || position.y < 0) continue;
850
+ if (position.x + position.width > originalImageData.width) continue;
851
+ if (position.y + position.height > originalImageData.height) continue;
852
+
853
+ const config = {
854
+ logoSize: size,
855
+ marginRight: originalImageData.width - position.x - size,
856
+ marginBottom: originalImageData.height - position.y - size
857
+ };
858
+ const candidate = evaluateRestorationCandidate({
859
+ originalImageData,
860
+ alphaMap,
861
+ position,
862
+ source: 'standard+preview-anchor',
863
+ config,
864
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, position),
865
+ adaptiveConfidence,
866
+ provenance: {
867
+ previewAnchor: true,
868
+ previewAnchorLocalRefine: sizeDelta !== 0 || dx !== 0 || dy !== 0
869
+ },
870
+ includeImageData: false
871
+ });
872
+
873
+ if (!candidate?.accepted) continue;
874
+ bestCandidate = pickBetterCandidate(bestCandidate, candidate, 0.002);
875
+ }
876
+ }
877
+ }
878
+ }
879
+
880
+ return bestCandidate;
881
+ }
882
+
883
+ function evaluateStandardTrialsForSeeds({
884
+ originalImageData,
885
+ candidateSeeds
886
+ }) {
887
+ const standardTrials = candidateSeeds
888
+ .map((seed) => evaluateRestorationCandidate({
889
+ originalImageData,
890
+ alphaMap: seed.alphaMap,
891
+ position: seed.position,
892
+ source: seed.source,
893
+ config: seed.config,
894
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, seed.position),
895
+ provenance: seed.provenance,
896
+ includeImageData: false
897
+ }))
898
+ .filter(Boolean);
899
+ const standardTrial = standardTrials.find((candidate) => candidate.source === 'standard') ?? standardTrials[0] ?? null;
900
+ const standardSpatialScore = standardTrial?.originalSpatialScore ?? null;
901
+ const standardGradientScore = standardTrial?.originalGradientScore ?? null;
902
+ const hasReliableStandardMatch = hasReliableStandardWatermarkSignal({
903
+ spatialScore: standardSpatialScore,
904
+ gradientScore: standardGradientScore
905
+ });
906
+
907
+ return {
908
+ standardTrials,
909
+ standardTrial,
910
+ standardSpatialScore,
911
+ standardGradientScore,
912
+ hasReliableStandardMatch
913
+ };
914
+ }
915
+
916
+ function resolveStandardAnchorSelection({
917
+ originalImageData,
918
+ config,
919
+ position,
920
+ alpha48,
921
+ alpha96,
922
+ getAlphaMap,
923
+ resolveAlphaMap
924
+ }) {
925
+ let standardCandidateSeeds = buildStandardCandidateSeeds({
926
+ originalImageData,
927
+ config,
928
+ position,
929
+ alpha48,
930
+ alpha96,
931
+ getAlphaMap,
932
+ resolveAlphaMap,
933
+ includeCatalogVariants: false
934
+ });
935
+ let standardSelection = evaluateStandardTrialsForSeeds({
936
+ originalImageData,
937
+ candidateSeeds: standardCandidateSeeds
938
+ });
939
+
940
+ const shouldExpandStandardCatalog =
941
+ !standardSelection.hasReliableStandardMatch &&
942
+ (!standardSelection.standardTrial || shouldEscalateSearch(standardSelection.standardTrial));
943
+
944
+ if (shouldExpandStandardCatalog) {
945
+ standardCandidateSeeds = buildStandardCandidateSeeds({
946
+ originalImageData,
947
+ config,
948
+ position,
949
+ alpha48,
950
+ alpha96,
951
+ getAlphaMap,
952
+ resolveAlphaMap,
953
+ includeCatalogVariants: true
954
+ });
955
+ standardSelection = evaluateStandardTrialsForSeeds({
956
+ originalImageData,
957
+ candidateSeeds: standardCandidateSeeds
958
+ });
959
+ }
960
+
961
+ return {
962
+ standardCandidateSeeds,
963
+ ...standardSelection
964
+ };
965
+ }
966
+
967
+ function resolveCandidatePromotion(candidate, {
968
+ reliableMatch = false
969
+ } = {}) {
970
+ if (!candidate?.accepted) {
971
+ return null;
972
+ }
973
+
974
+ if (reliableMatch) {
975
+ return {
976
+ candidate,
977
+ decisionTier: 'direct-match'
978
+ };
979
+ }
980
+
981
+ return {
982
+ candidate: {
983
+ ...candidate,
984
+ source: `${candidate.source}+validated`
985
+ },
986
+ decisionTier: 'validated-match'
987
+ };
988
+ }
989
+
990
+ function promoteBaseCandidate(baseCandidate, baseDecisionTier, candidate, {
991
+ reliableMatch = false,
992
+ minCostDelta = 0.002
993
+ } = {}) {
994
+ const promotion = resolveCandidatePromotion(candidate, {
995
+ reliableMatch
996
+ });
997
+ if (!promotion) {
998
+ return {
999
+ baseCandidate,
1000
+ baseDecisionTier
1001
+ };
1002
+ }
1003
+
1004
+ if (
1005
+ shouldPreserveCanonicalAnchor(baseCandidate, promotion.candidate)
1006
+ ) {
1007
+ return {
1008
+ baseCandidate,
1009
+ baseDecisionTier
1010
+ };
1011
+ }
1012
+
1013
+ const previousCandidate = baseCandidate;
1014
+ const nextCandidate = pickBetterCandidate(baseCandidate, promotion.candidate, minCostDelta);
1015
+ return {
1016
+ baseCandidate: nextCandidate,
1017
+ baseDecisionTier: nextCandidate !== previousCandidate
1018
+ ? promotion.decisionTier
1019
+ : baseDecisionTier
1020
+ };
1021
+ }
1022
+
1023
+ function evaluateAdaptiveTrial({
1024
+ originalImageData,
1025
+ config,
1026
+ alpha96,
1027
+ resolveAlphaMap,
1028
+ allowAdaptiveSearch
1029
+ }) {
1030
+ if (!allowAdaptiveSearch || !alpha96) {
1031
+ return {
1032
+ adaptive: null,
1033
+ adaptiveConfidence: null,
1034
+ adaptiveTrial: null
1035
+ };
1036
+ }
1037
+
1038
+ const adaptive = detectAdaptiveWatermarkRegion({
1039
+ imageData: originalImageData,
1040
+ alpha96,
1041
+ defaultConfig: config
1042
+ });
1043
+ const adaptiveConfidence = adaptive?.confidence ?? null;
1044
+
1045
+ if (!adaptive?.region || !(
1046
+ hasReliableAdaptiveWatermarkSignal(adaptive) ||
1047
+ adaptive.confidence >= VALIDATION_MIN_CONFIDENCE_FOR_ADAPTIVE_TRIAL
1048
+ )) {
1049
+ return {
1050
+ adaptive,
1051
+ adaptiveConfidence,
1052
+ adaptiveTrial: null
1053
+ };
1054
+ }
1055
+
1056
+ const size = adaptive.region.size;
1057
+ const adaptivePosition = {
1058
+ x: adaptive.region.x,
1059
+ y: adaptive.region.y,
1060
+ width: size,
1061
+ height: size
1062
+ };
1063
+ const adaptiveAlphaMap = resolveAlphaMap(size);
1064
+ if (!adaptiveAlphaMap) {
1065
+ throw new Error(`Missing alpha map for adaptive size ${size}`);
1066
+ }
1067
+ const adaptiveConfig = {
1068
+ logoSize: size,
1069
+ marginRight: originalImageData.width - adaptivePosition.x - size,
1070
+ marginBottom: originalImageData.height - adaptivePosition.y - size
1071
+ };
1072
+
1073
+ return {
1074
+ adaptive,
1075
+ adaptiveConfidence,
1076
+ adaptiveTrial: evaluateRestorationCandidate({
1077
+ originalImageData,
1078
+ alphaMap: adaptiveAlphaMap,
1079
+ position: adaptivePosition,
1080
+ source: 'adaptive',
1081
+ config: adaptiveConfig,
1082
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, adaptivePosition),
1083
+ adaptiveConfidence: adaptive.confidence,
1084
+ provenance: { adaptive: true },
1085
+ includeImageData: false
1086
+ })
1087
+ };
1088
+ }
1089
+
1090
+ function refineSelectedAnchorCandidate({
1091
+ originalImageData,
1092
+ baseCandidate,
1093
+ baseDecisionTier,
1094
+ adaptiveConfidence,
1095
+ alphaGainCandidates
1096
+ }) {
1097
+ let selectedTrial = ensureCandidateImageData(baseCandidate, originalImageData);
1098
+ let alphaMap = baseCandidate.alphaMap;
1099
+ let position = baseCandidate.position;
1100
+ let config = baseCandidate.config;
1101
+ let source = baseCandidate.source;
1102
+ let decisionTier = baseDecisionTier || inferDecisionTier(baseCandidate);
1103
+ let templateWarp = null;
1104
+ let selectedAlphaGain = baseCandidate.alphaGain ?? 1;
1105
+
1106
+ const warpCandidate = findBestTemplateWarp({
1107
+ originalImageData,
1108
+ alphaMap,
1109
+ position,
1110
+ baselineSpatialScore: selectedTrial.originalSpatialScore,
1111
+ baselineGradientScore: selectedTrial.originalGradientScore,
1112
+ shiftCandidates: selectedTrial.provenance?.previewAnchor === true
1113
+ ? PREVIEW_TEMPLATE_ALIGN_SHIFTS
1114
+ : TEMPLATE_ALIGN_SHIFTS,
1115
+ scaleCandidates: selectedTrial.provenance?.previewAnchor === true
1116
+ ? PREVIEW_TEMPLATE_ALIGN_SCALES
1117
+ : TEMPLATE_ALIGN_SCALES
1118
+ });
1119
+ if (warpCandidate) {
1120
+ const warpedTrial = evaluateRestorationCandidate({
1121
+ originalImageData,
1122
+ alphaMap: warpCandidate.alphaMap,
1123
+ position,
1124
+ source: `${source}+warp`,
1125
+ config,
1126
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, position),
1127
+ adaptiveConfidence,
1128
+ provenance: selectedTrial.provenance,
1129
+ includeImageData: false
1130
+ });
1131
+ const betterWarpTrial = pickBetterCandidate(selectedTrial, warpedTrial);
1132
+ if (betterWarpTrial !== selectedTrial) {
1133
+ alphaMap = warpedTrial.alphaMap;
1134
+ source = betterWarpTrial.source;
1135
+ selectedTrial = ensureCandidateImageData(betterWarpTrial, originalImageData);
1136
+ templateWarp = warpCandidate.shift;
1137
+ decisionTier = inferDecisionTier(betterWarpTrial, {
1138
+ directMatch: decisionTier === 'direct-match'
1139
+ });
1140
+ }
1141
+ }
1142
+
1143
+ const shouldRunGainSearch = selectedTrial.provenance?.previewAnchor === true
1144
+ ? isPreviewAnchorGainSearchRequired(selectedTrial)
1145
+ : shouldEscalateSearch(selectedTrial);
1146
+ let bestGainTrial = selectedTrial;
1147
+ if (shouldRunGainSearch) {
1148
+ for (const candidateGain of alphaGainCandidates) {
1149
+ const gainTrial = evaluateRestorationCandidate({
1150
+ originalImageData,
1151
+ alphaMap,
1152
+ position,
1153
+ source: `${source}+gain`,
1154
+ config,
1155
+ baselineNearBlackRatio: calculateNearBlackRatio(originalImageData, position),
1156
+ adaptiveConfidence,
1157
+ alphaGain: candidateGain,
1158
+ provenance: selectedTrial.provenance,
1159
+ includeImageData: false
1160
+ });
1161
+ bestGainTrial = pickBetterCandidate(bestGainTrial, gainTrial);
1162
+ }
1163
+ }
1164
+ if (bestGainTrial !== selectedTrial) {
1165
+ selectedTrial = ensureCandidateImageData(bestGainTrial, originalImageData);
1166
+ source = bestGainTrial.source;
1167
+ selectedAlphaGain = bestGainTrial.alphaGain;
1168
+ decisionTier = inferDecisionTier(bestGainTrial, {
1169
+ directMatch: decisionTier === 'direct-match'
1170
+ });
1171
+ }
1172
+
1173
+ return {
1174
+ selectedTrial: ensureCandidateImageData(selectedTrial, originalImageData),
1175
+ source,
1176
+ alphaMap,
1177
+ position,
1178
+ config,
1179
+ templateWarp,
1180
+ alphaGain: selectedAlphaGain,
1181
+ decisionTier
1182
+ };
1183
+ }
1184
+
1185
+ export function selectInitialCandidate({
1186
+ originalImageData,
1187
+ config,
1188
+ position,
1189
+ alpha48,
1190
+ alpha96,
1191
+ getAlphaMap,
1192
+ allowAdaptiveSearch,
1193
+ alphaGainCandidates
1194
+ }) {
1195
+ const resolveAlphaMap = createAlphaMapResolver({ alpha48, alpha96, getAlphaMap });
1196
+ const fallbackAlphaMap = config.logoSize === 96 ? alpha96 : alpha48;
1197
+ const {
1198
+ standardCandidateSeeds,
1199
+ standardTrials,
1200
+ standardTrial,
1201
+ standardSpatialScore,
1202
+ standardGradientScore,
1203
+ hasReliableStandardMatch
1204
+ } = resolveStandardAnchorSelection({
1205
+ originalImageData,
1206
+ config,
1207
+ position,
1208
+ alpha48,
1209
+ alpha96,
1210
+ getAlphaMap,
1211
+ resolveAlphaMap
1212
+ });
1213
+ let baseCandidate = null;
1214
+ let baseDecisionTier = 'insufficient';
1215
+ if (hasReliableStandardMatch && standardTrial?.accepted) {
1216
+ baseCandidate = standardTrial;
1217
+ baseDecisionTier = 'direct-match';
1218
+ } else if (standardTrial?.accepted) {
1219
+ baseCandidate = {
1220
+ ...standardTrial,
1221
+ source: `${standardTrial.source}+validated`
1222
+ };
1223
+ baseDecisionTier = 'validated-match';
1224
+ }
1225
+
1226
+ if (
1227
+ !baseCandidate &&
1228
+ standardTrial &&
1229
+ hasReliableStandardMatch
1230
+ ) {
1231
+ const adaptiveConfidence = null;
1232
+ const gainedStandardCandidate = searchCandidateAlphaGain({
1233
+ originalImageData,
1234
+ seedCandidate: {
1235
+ ...standardTrial,
1236
+ source: 'standard+validated'
1237
+ },
1238
+ adaptiveConfidence,
1239
+ alphaGainCandidates
1240
+ });
1241
+ if (gainedStandardCandidate) {
1242
+ baseCandidate = gainedStandardCandidate;
1243
+ baseDecisionTier = 'validated-match';
1244
+ }
1245
+ }
1246
+
1247
+ let adaptive = null;
1248
+ let adaptiveConfidence = null;
1249
+ let adaptiveTrial = null;
1250
+ for (const candidate of standardTrials) {
1251
+ if (!candidate || candidate === standardTrial) continue;
1252
+ ({
1253
+ baseCandidate,
1254
+ baseDecisionTier
1255
+ } = promoteBaseCandidate(baseCandidate, baseDecisionTier, candidate, {
1256
+ reliableMatch: hasReliableStandardWatermarkSignal({
1257
+ spatialScore: candidate.originalSpatialScore,
1258
+ gradientScore: candidate.originalGradientScore
1259
+ })
1260
+ }));
1261
+ }
1262
+
1263
+ const previewAnchorCandidate = searchBottomRightPreviewCandidate({
1264
+ originalImageData,
1265
+ config,
1266
+ alpha48,
1267
+ alpha96,
1268
+ getAlphaMap,
1269
+ resolveAlphaMap,
1270
+ adaptiveConfidence
1271
+ });
1272
+ if (previewAnchorCandidate) {
1273
+ ({
1274
+ baseCandidate,
1275
+ baseDecisionTier
1276
+ } = promoteBaseCandidate(baseCandidate, baseDecisionTier, previewAnchorCandidate));
1277
+ }
1278
+
1279
+ if (
1280
+ baseDecisionTier !== 'direct-match' &&
1281
+ !baseCandidate?.provenance?.previewAnchor &&
1282
+ shouldEscalateSearch(baseCandidate)
1283
+ ) {
1284
+ const sizeJitterCandidate = searchStandardSizeJitterCandidate({
1285
+ originalImageData,
1286
+ candidateSeeds: standardCandidateSeeds,
1287
+ alpha48,
1288
+ alpha96,
1289
+ getAlphaMap,
1290
+ resolveAlphaMap
1291
+ });
1292
+ if (sizeJitterCandidate) {
1293
+ ({
1294
+ baseCandidate,
1295
+ baseDecisionTier
1296
+ } = promoteBaseCandidate(baseCandidate, baseDecisionTier, sizeJitterCandidate));
1297
+ }
1298
+ }
1299
+
1300
+ if (
1301
+ baseDecisionTier !== 'direct-match' &&
1302
+ baseCandidate?.provenance?.sizeJitter === true &&
1303
+ !baseCandidate?.provenance?.previewAnchor &&
1304
+ isStandardCandidateSource(baseCandidate) &&
1305
+ shouldEscalateSearch(baseCandidate)
1306
+ ) {
1307
+ const fineLocalCandidate = searchFineStandardLocalCandidate({
1308
+ originalImageData,
1309
+ seedCandidate: baseCandidate,
1310
+ adaptiveConfidence
1311
+ });
1312
+ if (fineLocalCandidate) {
1313
+ ({
1314
+ baseCandidate,
1315
+ baseDecisionTier
1316
+ } = promoteBaseCandidate(baseCandidate, baseDecisionTier, fineLocalCandidate));
1317
+ }
1318
+ }
1319
+
1320
+ const shouldEvaluateAdaptive = () => {
1321
+ if (!allowAdaptiveSearch || !alpha96) return false;
1322
+ if (!baseCandidate) return true;
1323
+ if (!shouldEscalateSearch(baseCandidate)) return false;
1324
+
1325
+ baseCandidate = ensureCandidateImageData(baseCandidate, originalImageData);
1326
+
1327
+ return shouldAttemptAdaptiveFallback({
1328
+ processedImageData: baseCandidate.imageData,
1329
+ alphaMap: baseCandidate.alphaMap,
1330
+ position: baseCandidate.position,
1331
+ originalImageData,
1332
+ originalSpatialMismatchThreshold: 0
1333
+ });
1334
+ };
1335
+
1336
+ if (shouldEvaluateAdaptive()) {
1337
+ ({
1338
+ adaptive,
1339
+ adaptiveConfidence,
1340
+ adaptiveTrial
1341
+ } = evaluateAdaptiveTrial({
1342
+ originalImageData,
1343
+ config,
1344
+ alpha96,
1345
+ resolveAlphaMap,
1346
+ allowAdaptiveSearch
1347
+ }));
1348
+ }
1349
+
1350
+ if (adaptiveTrial) {
1351
+ ({
1352
+ baseCandidate,
1353
+ baseDecisionTier
1354
+ } = promoteBaseCandidate(baseCandidate, baseDecisionTier, adaptiveTrial, {
1355
+ reliableMatch: hasReliableAdaptiveWatermarkSignal(adaptive)
1356
+ }));
1357
+ }
1358
+
1359
+ if (
1360
+ !baseCandidate?.provenance?.previewAnchor &&
1361
+ !hasReliableAdaptiveWatermarkSignal(adaptive) &&
1362
+ shouldSearchNearbyStandardCandidate(baseCandidate, originalImageData)
1363
+ ) {
1364
+ const nearbyStandardCandidate = searchNearbyStandardCandidate({
1365
+ originalImageData,
1366
+ candidateSeeds: standardCandidateSeeds,
1367
+ adaptiveConfidence
1368
+ });
1369
+ if (nearbyStandardCandidate) {
1370
+ ({
1371
+ baseCandidate,
1372
+ baseDecisionTier
1373
+ } = promoteBaseCandidate(baseCandidate, baseDecisionTier, nearbyStandardCandidate));
1374
+ }
1375
+ }
1376
+
1377
+ if (!baseCandidate) {
1378
+ if (hasReliableStandardMatch && standardTrial) {
1379
+ baseCandidate = standardTrial;
1380
+ baseDecisionTier = 'direct-match';
1381
+ } else if (hasReliableAdaptiveWatermarkSignal(adaptive) && adaptiveTrial) {
1382
+ baseCandidate = adaptiveTrial;
1383
+ baseDecisionTier = 'direct-match';
1384
+ }
1385
+ }
1386
+
1387
+ if (!baseCandidate) {
1388
+ const validatedCandidate = pickBestValidatedCandidate([standardTrial, adaptiveTrial]);
1389
+ if (!validatedCandidate) {
1390
+ return {
1391
+ selectedTrial: null,
1392
+ source: 'skipped',
1393
+ alphaMap: fallbackAlphaMap,
1394
+ position,
1395
+ config,
1396
+ adaptiveConfidence,
1397
+ standardSpatialScore,
1398
+ standardGradientScore,
1399
+ templateWarp: null,
1400
+ alphaGain: 1,
1401
+ decisionTier: 'insufficient'
1402
+ };
1403
+ }
1404
+ baseCandidate = {
1405
+ ...validatedCandidate,
1406
+ source: `${validatedCandidate.source}+validated`
1407
+ };
1408
+ baseDecisionTier = 'validated-match';
1409
+ }
1410
+
1411
+ if (shouldRevertLocalShiftToStandardTrial(baseCandidate, standardTrial)) {
1412
+ baseCandidate = standardTrial;
1413
+ baseDecisionTier = hasReliableStandardMatch ? 'direct-match' : 'validated-match';
1414
+ }
1415
+
1416
+ const {
1417
+ selectedTrial,
1418
+ source,
1419
+ alphaMap,
1420
+ position: refinedPosition,
1421
+ config: refinedConfig,
1422
+ templateWarp,
1423
+ alphaGain,
1424
+ decisionTier
1425
+ } = refineSelectedAnchorCandidate({
1426
+ originalImageData,
1427
+ baseCandidate,
1428
+ baseDecisionTier,
1429
+ adaptiveConfidence,
1430
+ alphaGainCandidates
1431
+ });
1432
+
1433
+ return {
1434
+ selectedTrial: ensureCandidateImageData(selectedTrial, originalImageData),
1435
+ source,
1436
+ alphaMap,
1437
+ position: refinedPosition,
1438
+ config: refinedConfig,
1439
+ adaptiveConfidence,
1440
+ standardSpatialScore,
1441
+ standardGradientScore,
1442
+ templateWarp,
1443
+ alphaGain,
1444
+ decisionTier
1445
+ };
1446
+ }