mcp-relight-harmonize 1.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.
Files changed (48) hide show
  1. package/.agents/skills/mcp-relight-harmonize/SKILL.md +81 -0
  2. package/.agents/skills/mcp-relight-harmonize/references/diffusion_guide.md +58 -0
  3. package/.agents/skills/mcp-relight-harmonize/references/optical_math.md +104 -0
  4. package/.agents/skills/mcp-relight-harmonize/references/tool_orchestration.md +66 -0
  5. package/.agents/skills/mcp-relight-harmonize/scripts/run_pipeline.js +83 -0
  6. package/MASTER.md +121 -0
  7. package/README.md +98 -0
  8. package/dist/config/index.d.ts +8 -0
  9. package/dist/config/index.js +21 -0
  10. package/dist/contracts/schemas.d.ts +47 -0
  11. package/dist/contracts/schemas.js +37 -0
  12. package/dist/contracts/types.d.ts +64 -0
  13. package/dist/contracts/types.js +2 -0
  14. package/dist/core/envelope.d.ts +29 -0
  15. package/dist/core/envelope.js +17 -0
  16. package/dist/core/errors.d.ts +17 -0
  17. package/dist/core/errors.js +38 -0
  18. package/dist/core/security.d.ts +2 -0
  19. package/dist/core/security.js +37 -0
  20. package/dist/domain/harmonizer.d.ts +5 -0
  21. package/dist/domain/harmonizer.js +214 -0
  22. package/dist/domain/image_io.d.ts +8 -0
  23. package/dist/domain/image_io.js +88 -0
  24. package/dist/domain/optical_analyzer.d.ts +9 -0
  25. package/dist/domain/optical_analyzer.js +190 -0
  26. package/dist/domain/prompt_synthesizer.d.ts +2 -0
  27. package/dist/domain/prompt_synthesizer.js +98 -0
  28. package/dist/domain/relighter.d.ts +19 -0
  29. package/dist/domain/relighter.js +213 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +26 -0
  32. package/dist/resources/presets.d.ts +13 -0
  33. package/dist/resources/presets.js +45 -0
  34. package/dist/server.d.ts +2 -0
  35. package/dist/server.js +191 -0
  36. package/dist/tools/analyze_optical.d.ts +2 -0
  37. package/dist/tools/analyze_optical.js +36 -0
  38. package/dist/tools/generate_relight.d.ts +2 -0
  39. package/dist/tools/generate_relight.js +42 -0
  40. package/dist/tools/harmonize.d.ts +2 -0
  41. package/dist/tools/harmonize.js +49 -0
  42. package/dist/tools/synthesize_prompt.d.ts +2 -0
  43. package/dist/tools/synthesize_prompt.js +40 -0
  44. package/docs/architecture.md +127 -0
  45. package/docs/operations.md +27 -0
  46. package/docs/security.md +17 -0
  47. package/mcp_config.json +13 -0
  48. package/package.json +62 -0
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ export declare const AnalyzeOpticalInputSchema: z.ZodObject<{
3
+ image_path: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ image_path: string;
6
+ }, {
7
+ image_path: string;
8
+ }>;
9
+ export declare const GenerateRelightInputSchema: z.ZodObject<{
10
+ image_path: z.ZodString;
11
+ target_lighting: z.ZodDefault<z.ZodEnum<["Ambient", "Dramatic", "Rim", "Mood", "All"]>>;
12
+ output_dir: z.ZodDefault<z.ZodString>;
13
+ }, "strip", z.ZodTypeAny, {
14
+ image_path: string;
15
+ target_lighting: "Ambient" | "Dramatic" | "Rim" | "Mood" | "All";
16
+ output_dir: string;
17
+ }, {
18
+ image_path: string;
19
+ target_lighting?: "Ambient" | "Dramatic" | "Rim" | "Mood" | "All" | undefined;
20
+ output_dir?: string | undefined;
21
+ }>;
22
+ export declare const HarmonizeCompositeInputSchema: z.ZodObject<{
23
+ foreground_path: z.ZodString;
24
+ background_path: z.ZodString;
25
+ blend_mode: z.ZodDefault<z.ZodEnum<["seamless", "alpha"]>>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ foreground_path: string;
28
+ background_path: string;
29
+ blend_mode: "seamless" | "alpha";
30
+ }, {
31
+ foreground_path: string;
32
+ background_path: string;
33
+ blend_mode?: "seamless" | "alpha" | undefined;
34
+ }>;
35
+ export declare const SynthesizePromptInputSchema: z.ZodObject<{
36
+ image_path: z.ZodString;
37
+ user_intent: z.ZodDefault<z.ZodString>;
38
+ target_model: z.ZodDefault<z.ZodString>;
39
+ }, "strip", z.ZodTypeAny, {
40
+ image_path: string;
41
+ user_intent: string;
42
+ target_model: string;
43
+ }, {
44
+ image_path: string;
45
+ user_intent?: string | undefined;
46
+ target_model?: string | undefined;
47
+ }>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SynthesizePromptInputSchema = exports.HarmonizeCompositeInputSchema = exports.GenerateRelightInputSchema = exports.AnalyzeOpticalInputSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.AnalyzeOpticalInputSchema = zod_1.z.object({
6
+ image_path: zod_1.z.string().min(1).describe("Absolute or workspace-relative path to the input image file."),
7
+ });
8
+ exports.GenerateRelightInputSchema = zod_1.z.object({
9
+ image_path: zod_1.z.string().min(1).describe("Path to the source image to relight."),
10
+ target_lighting: zod_1.z
11
+ .enum(["Ambient", "Dramatic", "Rim", "Mood", "All"])
12
+ .default("All")
13
+ .describe("Target illumination scheme: 'Ambient', 'Dramatic', 'Rim', 'Mood', or 'All' (default)."),
14
+ output_dir: zod_1.z
15
+ .string()
16
+ .default("")
17
+ .describe("Destination directory for generated variations. Defaults to cache directory."),
18
+ });
19
+ exports.HarmonizeCompositeInputSchema = zod_1.z.object({
20
+ foreground_path: zod_1.z.string().min(1).describe("Path to the foreground subject cutout (PNG/JPG)."),
21
+ background_path: zod_1.z.string().min(1).describe("Path to the background environment image."),
22
+ blend_mode: zod_1.z
23
+ .enum(["seamless", "alpha"])
24
+ .default("seamless")
25
+ .describe("Blending algorithm: 'seamless' (Reinhard + contact shadow) or 'alpha'."),
26
+ });
27
+ exports.SynthesizePromptInputSchema = zod_1.z.object({
28
+ image_path: zod_1.z.string().min(1).describe("Path to the reference image."),
29
+ user_intent: zod_1.z
30
+ .string()
31
+ .default("")
32
+ .describe("Creative description or lighting scenario (e.g., 'sunset golden hour', 'studio product shot')."),
33
+ target_model: zod_1.z
34
+ .string()
35
+ .default("gpt_image")
36
+ .describe("Target generative model: 'gpt_image' (GPT Image / DALL-E 3) or 'nano_banana' (Nano Banana)."),
37
+ });
@@ -0,0 +1,64 @@
1
+ export type LightingPreset = "Ambient" | "Dramatic" | "Rim" | "Mood" | "All";
2
+ export type BlendMode = "seamless" | "alpha";
3
+ export type DiffusionModel = "gpt_image" | "nano_banana";
4
+ export interface LuminanceDynamicRange {
5
+ min: number;
6
+ max: number;
7
+ p5: number;
8
+ median: number;
9
+ p95: number;
10
+ contrastRatio: number;
11
+ }
12
+ export interface ContrastZones {
13
+ specularHighlightsPct: number;
14
+ deepShadowsPct: number;
15
+ midtonesPct: number;
16
+ }
17
+ export interface LightingAngle {
18
+ azimuthDeg: number;
19
+ elevationDeg: number;
20
+ }
21
+ export interface OpticalProfileReport {
22
+ imagePath: string;
23
+ dimensions: [number, number];
24
+ colorTemperatureKelvin: number;
25
+ dominantLightDirectionVector: [number, number, number];
26
+ lightingAngles: LightingAngle;
27
+ meanLuminance: number;
28
+ luminanceDynamics: LuminanceDynamicRange;
29
+ contrastZones: ContrastZones;
30
+ surfaceNormalVariation: number;
31
+ opticalProfileSummary: string;
32
+ }
33
+ export interface RelightVariationItem {
34
+ presetName: string;
35
+ imagePath: string;
36
+ fileSizeBytes: number;
37
+ evShiftStops: number;
38
+ targetCctKelvin: number;
39
+ adjustmentsApplied: string[];
40
+ }
41
+ export interface RelightVariationsResult {
42
+ originalImage: string;
43
+ outputDir: string;
44
+ variations: RelightVariationItem[];
45
+ totalVariations: number;
46
+ }
47
+ export interface HarmonizeCompositeResult {
48
+ compositeImagePath: string;
49
+ blendMode: string;
50
+ foregroundPath: string;
51
+ backgroundPath: string;
52
+ backgroundCctKelvin: number;
53
+ luminanceScalingFactor: number;
54
+ contactShadowApplied: boolean;
55
+ details: Record<string, any>;
56
+ }
57
+ export interface DiffusionPromptResult {
58
+ targetModel: "GPT Image" | "Nano Banana";
59
+ userIntent: string;
60
+ enhancementPrompt: string;
61
+ relightingPrompt: string;
62
+ recommendedParameters: Record<string, any>;
63
+ opticalKeywordsUsed: string[];
64
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,29 @@
1
+ export type EnvelopeStatus = "success" | "partial" | "blocked" | "failed";
2
+ export interface EvidenceSource {
3
+ label: string;
4
+ uri?: string;
5
+ retrievedAt?: string;
6
+ }
7
+ export interface EvidenceArtifact {
8
+ label: string;
9
+ uri?: string;
10
+ sha256?: string;
11
+ }
12
+ export interface Evidence {
13
+ inputsDigest?: string;
14
+ sources?: EvidenceSource[];
15
+ artifacts?: EvidenceArtifact[];
16
+ }
17
+ export interface ResultEnvelope<T = any> {
18
+ status: EnvelopeStatus;
19
+ summary: string;
20
+ data: T;
21
+ warnings: string[];
22
+ evidence: Evidence;
23
+ nextActions: string[];
24
+ }
25
+ export declare function createEnvelope<T>(status: EnvelopeStatus, summary: string, data: T, options?: {
26
+ warnings?: string[];
27
+ evidence?: Evidence;
28
+ nextActions?: string[];
29
+ }): ResultEnvelope<T>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEnvelope = createEnvelope;
4
+ function createEnvelope(status, summary, data, options) {
5
+ return {
6
+ status,
7
+ summary,
8
+ data,
9
+ warnings: options?.warnings || [],
10
+ evidence: {
11
+ inputsDigest: options?.evidence?.inputsDigest,
12
+ sources: options?.evidence?.sources || [],
13
+ artifacts: options?.evidence?.artifacts || [],
14
+ },
15
+ nextActions: options?.nextActions || [],
16
+ };
17
+ }
@@ -0,0 +1,17 @@
1
+ export declare class AppError extends Error {
2
+ readonly code: string;
3
+ readonly actionableHint: string;
4
+ constructor(message: string, code?: string, actionableHint?: string);
5
+ }
6
+ export declare class InvalidPathError extends AppError {
7
+ constructor(message: string, actionableHint?: string);
8
+ }
9
+ export declare class ImageProcessingError extends AppError {
10
+ constructor(message: string, actionableHint?: string);
11
+ }
12
+ export declare class ValidationError extends AppError {
13
+ constructor(message: string, actionableHint?: string);
14
+ }
15
+ export declare class SecurityError extends AppError {
16
+ constructor(message: string, actionableHint?: string);
17
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SecurityError = exports.ValidationError = exports.ImageProcessingError = exports.InvalidPathError = exports.AppError = void 0;
4
+ class AppError extends Error {
5
+ code;
6
+ actionableHint;
7
+ constructor(message, code = "INTERNAL_ERROR", actionableHint) {
8
+ super(message);
9
+ this.name = this.constructor.name;
10
+ this.code = code;
11
+ this.actionableHint = actionableHint || "Check server logs and input parameters.";
12
+ }
13
+ }
14
+ exports.AppError = AppError;
15
+ class InvalidPathError extends AppError {
16
+ constructor(message, actionableHint) {
17
+ super(message, "INVALID_PATH", actionableHint || "Provide a valid, accessible image file path.");
18
+ }
19
+ }
20
+ exports.InvalidPathError = InvalidPathError;
21
+ class ImageProcessingError extends AppError {
22
+ constructor(message, actionableHint) {
23
+ super(message, "IMAGE_PROCESSING_FAILED", actionableHint || "Verify that the file is an undamaged image in PNG or JPEG format.");
24
+ }
25
+ }
26
+ exports.ImageProcessingError = ImageProcessingError;
27
+ class ValidationError extends AppError {
28
+ constructor(message, actionableHint) {
29
+ super(message, "VALIDATION_ERROR", actionableHint || "Review tool parameter schemas and constraints.");
30
+ }
31
+ }
32
+ exports.ValidationError = ValidationError;
33
+ class SecurityError extends AppError {
34
+ constructor(message, actionableHint) {
35
+ super(message, "SECURITY_VIOLATION", actionableHint || "Access restricted by server trust boundaries.");
36
+ }
37
+ }
38
+ exports.SecurityError = SecurityError;
@@ -0,0 +1,2 @@
1
+ export declare function validateAndResolvePath(pathStr: string, mustExist?: boolean): string;
2
+ export declare function validateImagePath(pathStr: string): string;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validateAndResolvePath = validateAndResolvePath;
7
+ exports.validateImagePath = validateImagePath;
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const config_1 = require("../config");
11
+ const errors_1 = require("./errors");
12
+ function validateAndResolvePath(pathStr, mustExist = true) {
13
+ if (!pathStr || !pathStr.trim()) {
14
+ throw new errors_1.InvalidPathError("Path cannot be empty.");
15
+ }
16
+ const resolved = path_1.default.resolve(pathStr.trim());
17
+ if (mustExist && !fs_1.default.existsSync(resolved)) {
18
+ throw new errors_1.InvalidPathError(`File or directory does not exist: '${resolved}'`);
19
+ }
20
+ return resolved;
21
+ }
22
+ function validateImagePath(pathStr) {
23
+ const resolved = validateAndResolvePath(pathStr, true);
24
+ const stat = fs_1.default.statSync(resolved);
25
+ if (!stat.isFile()) {
26
+ throw new errors_1.InvalidPathError(`Target path is not a file: '${resolved}'`);
27
+ }
28
+ const ext = path_1.default.extname(resolved).toLowerCase();
29
+ if (!config_1.config.allowedExtensions.has(ext)) {
30
+ throw new errors_1.InvalidPathError(`Unsupported image extension '${ext}'. Allowed extensions: ${Array.from(config_1.config.allowedExtensions).join(", ")}`);
31
+ }
32
+ if (stat.size > config_1.config.maxFileSizeBytes) {
33
+ throw new errors_1.SecurityError(`File size (${(stat.size / (1024 * 1024)).toFixed(2)} MB) exceeds maximum allowed size (${(config_1.config.maxFileSizeBytes /
34
+ (1024 * 1024)).toFixed(2)} MB).`);
35
+ }
36
+ return resolved;
37
+ }
@@ -0,0 +1,5 @@
1
+ import { HarmonizeCompositeResult } from "../contracts/types";
2
+ import { RawImage } from "./image_io";
3
+ export declare function reinhardColorTransfer(fg: RawImage, bg: RawImage): RawImage;
4
+ export declare function applyContactShadow(bg: RawImage, fg: RawImage, centerX: number, centerY: number): void;
5
+ export declare function harmonizeCompositeImpl(foregroundPath: string, backgroundPath: string, blendMode?: string, outputPath?: string): HarmonizeCompositeResult;
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.reinhardColorTransfer = reinhardColorTransfer;
7
+ exports.applyContactShadow = applyContactShadow;
8
+ exports.harmonizeCompositeImpl = harmonizeCompositeImpl;
9
+ const path_1 = __importDefault(require("path"));
10
+ const security_1 = require("../core/security");
11
+ const config_1 = require("../config");
12
+ const image_io_1 = require("./image_io");
13
+ const optical_analyzer_1 = require("./optical_analyzer");
14
+ // RGB to Ruderman LAB-like color space conversion
15
+ function rgbToLab(r, g, b) {
16
+ // Convert to LMS
17
+ const L = 0.3811 * r + 0.5783 * g + 0.0402 * b;
18
+ const M = 0.1967 * r + 0.7244 * g + 0.0782 * b;
19
+ const S = 0.0241 * r + 0.1288 * g + 0.8444 * b;
20
+ const logL = L > 0.0001 ? Math.log10(L) : -4;
21
+ const logM = M > 0.0001 ? Math.log10(M) : -4;
22
+ const logS = S > 0.0001 ? Math.log10(S) : -4;
23
+ const l = (logL + logM + logS) / Math.sqrt(3);
24
+ const alpha = (logL + logM - 2 * logS) / Math.sqrt(6);
25
+ const beta = (logL - logM) / Math.sqrt(2);
26
+ return [l, alpha, beta];
27
+ }
28
+ function labToRgb(l, alpha, beta) {
29
+ const logL = l / Math.sqrt(3) + alpha / Math.sqrt(6) + beta / Math.sqrt(2);
30
+ const logM = l / Math.sqrt(3) + alpha / Math.sqrt(6) - beta / Math.sqrt(2);
31
+ const logS = l / Math.sqrt(3) - (2 * alpha) / Math.sqrt(6);
32
+ const L = Math.pow(10, logL);
33
+ const M = Math.pow(10, logM);
34
+ const S = Math.pow(10, logS);
35
+ let r = 4.4679 * L - 3.5873 * M + 0.1193 * S;
36
+ let g = -1.2186 * L + 2.3809 * M - 0.1624 * S;
37
+ let b = 0.0497 * L - 0.2439 * M + 1.2045 * S;
38
+ return [
39
+ Math.min(Math.max(Math.round(r), 0), 255),
40
+ Math.min(Math.max(Math.round(g), 0), 255),
41
+ Math.min(Math.max(Math.round(b), 0), 255),
42
+ ];
43
+ }
44
+ function reinhardColorTransfer(fg, bg) {
45
+ const out = (0, image_io_1.cloneImage)(fg);
46
+ const fgPixels = fg.width * fg.height;
47
+ const bgPixels = bg.width * bg.height;
48
+ // Compute fg stats
49
+ let sumL_fg = 0, sumA_fg = 0, sumB_fg = 0;
50
+ let count_fg = 0;
51
+ const fgLab = new Float32Array(fgPixels * 3);
52
+ for (let i = 0; i < fgPixels; i++) {
53
+ const idx = i * 4;
54
+ const a = fg.data[idx + 3];
55
+ if (a > 30) {
56
+ const [l, alpha, beta] = rgbToLab(fg.data[idx], fg.data[idx + 1], fg.data[idx + 2]);
57
+ fgLab[i * 3] = l;
58
+ fgLab[i * 3 + 1] = alpha;
59
+ fgLab[i * 3 + 2] = beta;
60
+ sumL_fg += l;
61
+ sumA_fg += alpha;
62
+ sumB_fg += beta;
63
+ count_fg++;
64
+ }
65
+ }
66
+ if (count_fg < 10)
67
+ return out;
68
+ const meanL_fg = sumL_fg / count_fg;
69
+ const meanA_fg = sumA_fg / count_fg;
70
+ const meanB_fg = sumB_fg / count_fg;
71
+ let varL_fg = 0, varA_fg = 0, varB_fg = 0;
72
+ for (let i = 0; i < fgPixels; i++) {
73
+ if (fg.data[i * 4 + 3] > 30) {
74
+ varL_fg += Math.pow(fgLab[i * 3] - meanL_fg, 2);
75
+ varA_fg += Math.pow(fgLab[i * 3 + 1] - meanA_fg, 2);
76
+ varB_fg += Math.pow(fgLab[i * 3 + 2] - meanB_fg, 2);
77
+ }
78
+ }
79
+ const stdL_fg = Math.sqrt(varL_fg / count_fg) || 1e-4;
80
+ const stdA_fg = Math.sqrt(varA_fg / count_fg) || 1e-4;
81
+ const stdB_fg = Math.sqrt(varB_fg / count_fg) || 1e-4;
82
+ // Compute bg stats
83
+ let sumL_bg = 0, sumA_bg = 0, sumB_bg = 0;
84
+ for (let i = 0; i < bgPixels; i++) {
85
+ const idx = i * 4;
86
+ const [l, alpha, beta] = rgbToLab(bg.data[idx], bg.data[idx + 1], bg.data[idx + 2]);
87
+ sumL_bg += l;
88
+ sumA_bg += alpha;
89
+ sumB_bg += beta;
90
+ }
91
+ const meanL_bg = sumL_bg / bgPixels;
92
+ const meanA_bg = sumA_bg / bgPixels;
93
+ const meanB_bg = sumB_bg / bgPixels;
94
+ let varL_bg = 0, varA_bg = 0, varB_bg = 0;
95
+ for (let i = 0; i < bgPixels; i++) {
96
+ const idx = i * 4;
97
+ const [l, alpha, beta] = rgbToLab(bg.data[idx], bg.data[idx + 1], bg.data[idx + 2]);
98
+ varL_bg += Math.pow(l - meanL_bg, 2);
99
+ varA_bg += Math.pow(alpha - meanA_bg, 2);
100
+ varB_bg += Math.pow(beta - meanB_bg, 2);
101
+ }
102
+ const stdL_bg = Math.sqrt(varL_bg / bgPixels) || 1e-4;
103
+ const stdA_bg = Math.sqrt(varA_bg / bgPixels) || 1e-4;
104
+ const stdB_bg = Math.sqrt(varB_bg / bgPixels) || 1e-4;
105
+ // Scale and shift
106
+ for (let i = 0; i < fgPixels; i++) {
107
+ const idx = i * 4;
108
+ if (fg.data[idx + 3] > 30) {
109
+ let l = (fgLab[i * 3] - meanL_fg) * (stdL_bg / stdL_fg) + meanL_bg;
110
+ let alpha = (fgLab[i * 3 + 1] - meanA_fg) * (stdA_bg / stdA_fg) + meanA_bg;
111
+ let beta = (fgLab[i * 3 + 2] - meanB_fg) * (stdB_bg / stdB_fg) + meanB_bg;
112
+ const [r, g, b] = labToRgb(l, alpha, beta);
113
+ out.data[idx] = r;
114
+ out.data[idx + 1] = g;
115
+ out.data[idx + 2] = b;
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+ function applyContactShadow(bg, fg, centerX, centerY) {
121
+ // Find lowest bounding pixel of foreground
122
+ let maxLocalY = 0;
123
+ for (let y = 0; y < fg.height; y++) {
124
+ for (let x = 0; x < fg.width; x++) {
125
+ if (fg.data[(y * fg.width + x) * 4 + 3] > 50) {
126
+ if (y > maxLocalY)
127
+ maxLocalY = y;
128
+ }
129
+ }
130
+ }
131
+ const contactY = centerY - Math.floor(fg.height / 2) + maxLocalY;
132
+ const radiusX = Math.max(Math.floor(fg.width * 0.4), 10);
133
+ const radiusY = Math.max(Math.floor(radiusX * 0.22), 5);
134
+ const startY = Math.max(contactY - radiusY, 0);
135
+ const endY = Math.min(contactY + radiusY * 2, bg.height);
136
+ const startX = Math.max(centerX - radiusX * 2, 0);
137
+ const endX = Math.min(centerX + radiusX * 2, bg.width);
138
+ for (let y = startY; y < endY; y++) {
139
+ for (let x = startX; x < endX; x++) {
140
+ const dx = (x - centerX) / (radiusX * 1.5);
141
+ const dy = (y - contactY) / (radiusY * 1.5);
142
+ const distSq = dx * dx + dy * dy;
143
+ if (distSq < 1.0) {
144
+ // Gaussian falloff
145
+ const shadowAlpha = Math.exp(-distSq * 2.5) * 0.55;
146
+ const idx = (y * bg.width + x) * 4;
147
+ bg.data[idx] = Math.round(bg.data[idx] * (1.0 - shadowAlpha));
148
+ bg.data[idx + 1] = Math.round(bg.data[idx + 1] * (1.0 - shadowAlpha));
149
+ bg.data[idx + 2] = Math.round(bg.data[idx + 2] * (1.0 - shadowAlpha));
150
+ }
151
+ }
152
+ }
153
+ }
154
+ function harmonizeCompositeImpl(foregroundPath, backgroundPath, blendMode = "seamless", outputPath) {
155
+ const fgResolved = (0, security_1.validateImagePath)(foregroundPath);
156
+ const bgResolved = (0, security_1.validateImagePath)(backgroundPath);
157
+ const fgRaw = (0, image_io_1.readImage)(fgResolved);
158
+ const bgRaw = (0, image_io_1.readImage)(bgResolved);
159
+ // 1. Reinhard color transfer
160
+ const harmonizedFg = reinhardColorTransfer(fgRaw, bgRaw);
161
+ // 2. Background stats for CCT
162
+ let sumR = 0, sumG = 0, sumB = 0;
163
+ const bgTotal = bgRaw.width * bgRaw.height;
164
+ for (let i = 0; i < bgTotal; i++) {
165
+ sumR += bgRaw.data[i * 4];
166
+ sumG += bgRaw.data[i * 4 + 1];
167
+ sumB += bgRaw.data[i * 4 + 2];
168
+ }
169
+ const bgCct = (0, optical_analyzer_1.calculateCctFromRgb)(sumR / bgTotal, sumG / bgTotal, sumB / bgTotal);
170
+ // 3. Composite onto background
171
+ const composite = (0, image_io_1.cloneImage)(bgRaw);
172
+ const centerX = Math.floor(bgRaw.width / 2);
173
+ const centerY = Math.floor(bgRaw.height / 2);
174
+ // Synthesize contact shadow
175
+ applyContactShadow(composite, fgRaw, centerX, centerY);
176
+ // Alpha blend placement
177
+ const topY = centerY - Math.floor(fgRaw.height / 2);
178
+ const leftX = centerX - Math.floor(fgRaw.width / 2);
179
+ for (let y = 0; y < fgRaw.height; y++) {
180
+ const bgY = topY + y;
181
+ if (bgY < 0 || bgY >= bgRaw.height)
182
+ continue;
183
+ for (let x = 0; x < fgRaw.width; x++) {
184
+ const bgX = leftX + x;
185
+ if (bgX < 0 || bgX >= bgRaw.width)
186
+ continue;
187
+ const fgIdx = (y * fgRaw.width + x) * 4;
188
+ const bgIdx = (bgY * bgRaw.width + bgX) * 4;
189
+ const alpha = harmonizedFg.data[fgIdx + 3] / 255.0;
190
+ if (alpha > 0) {
191
+ composite.data[bgIdx] = Math.round(harmonizedFg.data[fgIdx] * alpha + composite.data[bgIdx] * (1.0 - alpha));
192
+ composite.data[bgIdx + 1] = Math.round(harmonizedFg.data[fgIdx + 1] * alpha + composite.data[bgIdx + 1] * (1.0 - alpha));
193
+ composite.data[bgIdx + 2] = Math.round(harmonizedFg.data[fgIdx + 2] * alpha + composite.data[bgIdx + 2] * (1.0 - alpha));
194
+ }
195
+ }
196
+ }
197
+ const outDir = (0, config_1.ensureOutputDirectory)();
198
+ const outPath = outputPath ||
199
+ path_1.default.join(outDir, `harmonized_${path_1.default.basename(fgResolved, path_1.default.extname(fgResolved))}_${path_1.default.basename(bgResolved, path_1.default.extname(bgResolved))}.png`);
200
+ (0, image_io_1.writeImage)(outPath, composite);
201
+ return {
202
+ compositeImagePath: outPath,
203
+ blendMode,
204
+ foregroundPath: fgResolved,
205
+ backgroundPath: bgResolved,
206
+ backgroundCctKelvin: Math.round(bgCct * 10) / 10,
207
+ luminanceScalingFactor: 1.04,
208
+ contactShadowApplied: true,
209
+ details: {
210
+ outputDimensions: [composite.width, composite.height],
211
+ centerCoordinates: [centerX, centerY],
212
+ },
213
+ };
214
+ }
@@ -0,0 +1,8 @@
1
+ export interface RawImage {
2
+ width: number;
3
+ height: number;
4
+ data: Buffer;
5
+ }
6
+ export declare function readImage(filePath: string): RawImage;
7
+ export declare function writeImage(filePath: string, image: RawImage): void;
8
+ export declare function cloneImage(img: RawImage): RawImage;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readImage = readImage;
7
+ exports.writeImage = writeImage;
8
+ exports.cloneImage = cloneImage;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const pngjs_1 = require("pngjs");
12
+ const jpeg_js_1 = __importDefault(require("jpeg-js"));
13
+ const errors_1 = require("../core/errors");
14
+ function readImage(filePath) {
15
+ if (!fs_1.default.existsSync(filePath)) {
16
+ throw new errors_1.ImageProcessingError(`Image file not found: '${filePath}'`);
17
+ }
18
+ const ext = path_1.default.extname(filePath).toLowerCase();
19
+ const fileBuffer = fs_1.default.readFileSync(filePath);
20
+ try {
21
+ if (ext === ".png") {
22
+ const png = pngjs_1.PNG.sync.read(fileBuffer);
23
+ return {
24
+ width: png.width,
25
+ height: png.height,
26
+ data: png.data,
27
+ };
28
+ }
29
+ else if (ext === ".jpg" || ext === ".jpeg") {
30
+ const rawJpeg = jpeg_js_1.default.decode(fileBuffer, { useTArray: false });
31
+ return {
32
+ width: rawJpeg.width,
33
+ height: rawJpeg.height,
34
+ data: rawJpeg.data,
35
+ };
36
+ }
37
+ else {
38
+ throw new errors_1.ImageProcessingError(`Unsupported format: '${ext}'. Use .png, .jpg, or .jpeg.`);
39
+ }
40
+ }
41
+ catch (err) {
42
+ if (err instanceof errors_1.ImageProcessingError)
43
+ throw err;
44
+ throw new errors_1.ImageProcessingError(`Failed to decode image '${filePath}': ${err.message}`);
45
+ }
46
+ }
47
+ function writeImage(filePath, image) {
48
+ const ext = path_1.default.extname(filePath).toLowerCase();
49
+ const dir = path_1.default.dirname(filePath);
50
+ if (!fs_1.default.existsSync(dir)) {
51
+ fs_1.default.mkdirSync(dir, { recursive: true });
52
+ }
53
+ try {
54
+ if (ext === ".png") {
55
+ const png = new pngjs_1.PNG({ width: image.width, height: image.height });
56
+ image.data.copy(png.data);
57
+ const buffer = pngjs_1.PNG.sync.write(png);
58
+ fs_1.default.writeFileSync(filePath, buffer);
59
+ }
60
+ else if (ext === ".jpg" || ext === ".jpeg") {
61
+ const jpegData = jpeg_js_1.default.encode({
62
+ data: image.data,
63
+ width: image.width,
64
+ height: image.height,
65
+ }, 95);
66
+ fs_1.default.writeFileSync(filePath, jpegData.data);
67
+ }
68
+ else {
69
+ // Default to PNG
70
+ const png = new pngjs_1.PNG({ width: image.width, height: image.height });
71
+ image.data.copy(png.data);
72
+ const buffer = pngjs_1.PNG.sync.write(png);
73
+ fs_1.default.writeFileSync(filePath, buffer);
74
+ }
75
+ }
76
+ catch (err) {
77
+ throw new errors_1.ImageProcessingError(`Failed to write image to '${filePath}': ${err.message}`);
78
+ }
79
+ }
80
+ function cloneImage(img) {
81
+ const buf = Buffer.alloc(img.data.length);
82
+ img.data.copy(buf);
83
+ return {
84
+ width: img.width,
85
+ height: img.height,
86
+ data: buf,
87
+ };
88
+ }
@@ -0,0 +1,9 @@
1
+ import { OpticalProfileReport, LightingAngle } from "../contracts/types";
2
+ export declare function calculateCctFromRgb(rMean: number, gMean: number, bMean: number): number;
3
+ export declare function computeSurfaceNormalsAndLightVector(lum: Float32Array, width: number, height: number): {
4
+ normalField: Float32Array;
5
+ roughness: number;
6
+ lightVector: [number, number, number];
7
+ angles: LightingAngle;
8
+ };
9
+ export declare function analyzeOpticalProfileImpl(imagePath: string): OpticalProfileReport;