"""Extract business-management PNG colors into matching vector card templates. Reads the reference only. No bitmap is embedded in the generated SVGs. Usage: python create-business-card-assets.py """ import hashlib import json import re import shutil import sys from pathlib import Path import numpy as np from PIL import Image root = Path(__file__).resolve().parents[2] reference = Path(sys.argv[1]) image = Image.open(reference).convert('RGBA') pixels = np.asarray(image, dtype=float) width, height = image.size scale = np.array([width / 146, height / 68]) start = np.array([124.527, 13.2139]) * scale end = np.array([91.4091, 70.3397]) * scale vector = end - start # Probe only the flat outer bands, away from rounded corners and inner face. probes = ([(x, 2) for x in range(20, width - 20)] + [(3, y) for y in range(20, height - 15)] + [(width - 4, y) for y in range(20, height - 15)] + [(x, height - 3) for x in range(20, width - 20)]) coordinates = np.array(probes, dtype=float) + 0.5 times = np.clip((coordinates - start) @ vector / (vector @ vector), 0, 1) values = np.array([pixels[y, x] for x, y in probes]) basis = np.zeros((len(times), 3)) first = times <= 0.72 basis[first, 0] = 1 - times[first] / 0.72 basis[first, 1] = times[first] / 0.72 basis[~first, 1] = (1 - times[~first]) / 0.28 basis[~first, 2] = (times[~first] - 0.72) / 0.28 # Test both interpolation representations rather than assuming raster alpha. fits = [] for premultiplied in (False, True): samples = values.copy() if premultiplied: samples[:, :3] *= samples[:, 3:] / 255 stops = np.linalg.lstsq(basis, samples, rcond=None)[0] predicted = basis @ stops if premultiplied: predicted[:, :3] /= np.maximum(predicted[:, 3:], 1) / 255 stops[:, :3] /= np.maximum(stops[:, 3:], 1) / 255 error = float(np.abs(predicted - values).mean()) fits.append((error, premultiplied, np.clip(stops, 0, 255))) error, premultiplied, stops = min(fits, key=lambda item: item[0]) # Use plateau medians for the clamped endpoints, reducing raster edge noise. stops[0] = np.median(values[times == 0], axis=0) stops[2] = np.median(values[times == 1], axis=0) colors = ['#' + ''.join(f'{int(round(channel)):02X}' for channel in row[:3]) for row in stops] opacities = [round(float(row[3] / 255), 2) for row in stops] inner_top = pixels[9:35, 20:150].reshape(-1, 4) inner_bottom = pixels[height - 13:height - 6, 20:150].reshape(-1, 4) inner_colors = ['#' + ''.join(f'{int(round(channel)):02X}' for channel in row[:3]) for row in [np.median(inner_top, axis=0), np.median(inner_bottom, axis=0)]] # Recover the inner face's gradient direction from its unclamped RGB plane. # Colors alone are insufficient: retaining the template's gradient direction # would leave a visible tone difference despite matching all endpoint colors. inner_start_rgb = np.array([int(inner_colors[0][i:i + 2], 16) for i in (1, 3, 5)]) inner_end_rgb = np.array([int(inner_colors[1][i:i + 2], 16) for i in (1, 3, 5)]) delta = inner_start_rgb - inner_end_rgb inner_points, inner_times = [], [] for y in range(12, height - 12, 2): for x in range(20, min(220, width // 2 + 16), 2): rgb = pixels[y, x, :3] t = float((inner_start_rgb - rgb) @ delta / (delta @ delta)) if 0.06 < t < 0.94 and pixels[y, x, 3] == 255: inner_points.append([(x + 0.5) / scale[0], (y + 0.5) / scale[1], 1]) inner_times.append(t) plane = np.linalg.lstsq(np.array(inner_points), np.array(inner_times), rcond=None)[0] gradient_vector = plane[:2] / (plane[:2] @ plane[:2]) inner_start = np.array([78.0741, (-plane[2] - plane[0] * 78.0741) / plane[1]]) inner_end = inner_start + gradient_vector assets = root / 'packages/org-chart/src/assets/cards' for suffix in ('', '-sm'): template = (assets / f'경영기획팀{suffix}.svg').read_text(encoding='utf-8') # Retain the supplied department geometry and its size-specific gradients. for original, replacement in zip(['#DEE6F0', '#A8BBD1', '#577BA5'], colors): template = template.replace(f'stop-color="{original}"', f'stop-color="{replacement}"') template = template.replace(f'stop-color="{colors[2]}"', f'stop-color="{colors[2]}" stop-opacity="{opacities[2]}"') template = template.replace('stop-color="white"', f'stop-color="{inner_colors[0]}"') template = template.replace('stop-color="#E0E6F4"', f'stop-color="{inner_colors[1]}"') template = template.replace('paint0_linear_', 'business_management_outer_').replace('paint1_linear_', 'business_management_inner_') size_scale = np.array([1, 1]) if not suffix else np.array([108 / 146, 52 / 68]) p1, p2 = inner_start * size_scale, inner_end * size_scale template = re.sub(r'(]+>', lambda match: f'{match[1]} x1="{p1[0]:.4f}" y1="{p1[1]:.4f}" x2="{p2[0]:.4f}" y2="{p2[1]:.4f}" gradientUnits="userSpaceOnUse">', template) template = template.replace('offset="0.97"', 'offset="1"') (assets / f'사업관리팀{suffix}.svg').write_text(template, encoding='utf-8') output = root / 'test-results/org-chart-card-design' output.mkdir(parents=True, exist_ok=True) shutil.copyfile(reference, output / 'business-management-reference.png') evidence = { 'reference': str(reference), 'sha256': hashlib.sha256(reference.read_bytes()).hexdigest(), 'referenceSize': [width, height], 'outerStops': colors, 'outerOpacities': opacities, 'innerStops': inner_colors, 'outerStopOffsets': [0, 0.72, 1], 'innerGradientStart': inner_start.tolist(), 'innerGradientEnd': inner_end.tolist(), 'innerSampleCount': len(inner_points), 'sampleCount': len(probes), 'fitMeanAbsoluteRGBAError': error, 'premultipliedFit': premultiplied, 'method': 'Outer-band RGBA samples fitted to the supplied department gradient geometry; endpoint plateau medians and inner-face medians. Inner gradient direction recovered from unclamped RGB plane. Reference PNG is preserved; geometry reuses department SVG paths.', } (output / 'business-management-colors.json').write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding='utf-8') print(json.dumps(evidence, ensure_ascii=False, indent=2))