File size: 9,484 Bytes
3790c48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/env python3
"""
utils.background_factory
─────────────────────────────────────────────────────────────────────────────
Generates professional backgrounds from presets **or** a user-supplied image.

Public API
----------
create_professional_background(cfg_or_key, width, height) β†’ np.ndarray  (BGR)

All lower-case helpers are considered private to this module.
"""

from __future__ import annotations
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional
import logging, os, cv2, numpy as np

from utils.background_presets import PROFESSIONAL_BACKGROUNDS

log = logging.getLogger(__name__)

__all__ = ["create_professional_background"]

# ────────────────────────────────────────────────────────────────────────────
# Main entry
# ────────────────────────────────────────────────────────────────────────────
def create_professional_background(
    bg_config: Dict[str, Any] | str,
    width:  int,
    height: int,
) -> np.ndarray:
    """
    Accepts either …
      β€’ a **key** into PROFESSIONAL_BACKGROUNDS  (e.g. "office_modern"), or
      β€’ a **dict**   (typically supplied by UI) that may include:
          ─ background_choice: "office_modern"
          ─ custom_path: "/path/to/image.png"
          ─ OR directly contain {type:"gradient", colors:[…]}
    Returns **BGR** uint8 image (OpenCV-ready).
    """
    try:
        # ── Resolve input ---------------------------------------------------
        choice       : str  = "minimalist"
        custom_path  : str | None = None
        direct_style : Dict[str, Any] | None = None

        if isinstance(bg_config, str):
            choice = bg_config.lower()

        elif isinstance(bg_config, dict):
            choice       = bg_config.get("background_choice", bg_config.get("name", "minimalist")).lower()
            custom_path  = bg_config.get("custom_path")
            if "type" in bg_config and "colors" in bg_config:
                direct_style = bg_config        # full inline style

        # ── 1) Custom image? ----------------------------------------------
        if custom_path and os.path.exists(custom_path):
            img = cv2.imread(custom_path, cv2.IMREAD_COLOR)
            if img is not None:
                img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                fitted  = _fit_image_letterbox(img_rgb, width, height, fill=(32,32,32))
                return cv2.cvtColor(fitted, cv2.COLOR_RGB2BGR)
            log.warning(f"Custom-background read failed: {custom_path}")

        # ── 2) Inline dict style? -----------------------------------------
        if direct_style:
            if direct_style["type"] == "color":
                bg = _create_solid_background(direct_style, width, height)
            else:  # gradient / image
                bg = _create_gradient_background(direct_style, width, height)
            return _apply_bg_adjustments(bg, direct_style)

        # ── 3) Preset dict lookup -----------------------------------------
        preset = PROFESSIONAL_BACKGROUNDS.get(choice, PROFESSIONAL_BACKGROUNDS["minimalist"])

        if preset["type"] == "color":
            bg = _create_solid_background(preset, width, height)
        elif preset["type"] == "image":
            path = Path(preset["path"])
            if path.exists():
                img_bgr = cv2.imread(str(path), cv2.IMREAD_COLOR)
                if img_bgr is not None:
                    return cv2.resize(img_bgr, (width, height), interpolation=cv2.INTER_LANCZOS4)
            log.warning(f"Preset image not found: {path}; falling back to gradient")
            bg = _create_gradient_background(
                {**preset, "type": "gradient", "colors": ["#3a3a3a", "#2e2e2e"]}, width, height
            )
        else:  # gradient
            bg = _create_gradient_background(preset, width, height)

        return _apply_bg_adjustments(bg, preset)

    except Exception as e:
        log.error(f"create_professional_background: {e}")
        return np.full((height, width, 3), (128,128,128), np.uint8)


# ────────────────────────────────────────────────────────────────────────────
# Letter-boxed fit for custom images
# ────────────────────────────────────────────────────────────────────────────
def _fit_image_letterbox(img_rgb: np.ndarray, dst_w: int, dst_h: int,
                         fill=(32,32,32)) -> np.ndarray:
    h, w = img_rgb.shape[:2]
    if h == 0 or w == 0:
        return np.full((dst_h, dst_w, 3), fill, np.uint8)

    src_a = w / h
    dst_a = dst_w / dst_h
    if src_a > dst_a:
        new_w, new_h = dst_w, int(dst_w / src_a)
    else:
        new_h, new_w = dst_h, int(dst_h * src_a)

    resized = cv2.resize(img_rgb, (new_w, new_h), interpolation=cv2.INTER_AREA)
    canvas  = np.full((dst_h, dst_w, 3), fill, np.uint8)
    y0 = (dst_h-new_h)//2; x0 = (dst_w-new_w)//2
    canvas[y0:y0+new_h, x0:x0+new_w] = resized
    return canvas


# ────────────────────────────────────────────────────────────────────────────
# Background builders
# ────────────────────────────────────────────────────────────────────────────
def _create_solid_background(style: Dict[str,Any], w: int, h: int) -> np.ndarray:
    clr_hex = style["colors"][0].lstrip("#")
    rgb     = tuple(int(clr_hex[i:i+2],16) for i in (0,2,4))
    return np.full((h,w,3), rgb[::-1], np.uint8)      # BGR

def _create_gradient_background(style: Dict[str,Any], w:int, h:int) -> np.ndarray:
    cols   = [hex.lstrip("#") for hex in style["colors"]]
    rgbs   = [tuple(int(c[i:i+2],16) for i in (0,2,4)) for c in cols]
    dirn   = style.get("direction","vertical")

    if dirn=="vertical":   grad = _grad_vertical(rgbs, w, h)
    elif dirn=="horizontal": grad = _grad_horizontal(rgbs, w, h)
    elif dirn=="diagonal":   grad = _grad_diagonal(rgbs, w, h)
    else:                   grad = _grad_radial(rgbs, w, h,
                                               soft=(dirn=="soft_radial"))
    return cv2.cvtColor(grad, cv2.COLOR_RGB2BGR)

# --- gradient helpers -------------------------------------------------------

def _grad_vertical(colors, w, h):
    g = np.zeros((h, w, 3), np.uint8)
    for y in range(h):
        g[y, :] = _interp_multi(colors, y/h)
    return g
def _grad_horizontal(colors, w, h):
    g = np.zeros((h, w, 3), np.uint8)
    for x in range(w):
        g[:, x] = _interp_multi(colors, x/w)
    return g
def _grad_diagonal(colors, w, h):
    y,x = np.mgrid[0:h, 0:w]
    prog = np.clip((x+y)/(h+w), 0, 1)
    g = np.zeros((h,w,3), np.uint8)
    for c in range(3):
        g[:,:,c] = _vector_interp(colors, prog, c)
    return g
def _grad_radial(colors, w, h, soft=False):
    cx, cy = w/2, h/2
    maxd = np.hypot(cx, cy)
    y,x  = np.mgrid[0:h, 0:w]
    prog = np.clip(np.hypot(x-cx, y-cy)/maxd, 0, 1)
    if soft: prog = prog**0.7
    g = np.zeros((h,w,3), np.uint8)
    for c in range(3):
        g[:,:,c] = _vector_interp(colors, prog, c)
    return g

def _vector_interp(cols, prog, chan):
    if len(cols)==1:
        return np.full_like(prog, cols[0][chan], np.uint8)
    segs = len(cols)-1
    seg_prog = prog*segs
    idx   = np.clip(np.floor(seg_prog).astype(int), 0, segs-1)
    local = seg_prog - idx
    start = np.take([c[chan] for c in cols], idx)
    end   = np.take([c[chan] for c in cols[1:]+[cols[-1]]], idx)
    return (start + (end-start)*local).astype(np.uint8)

def _interp_multi(cols, p):
    # cols length 1..n   p ∈[0,1]
    if len(cols)==1: return cols[0]
    seg = p*(len(cols)-1)
    i   = int(seg)
    l   = seg - i
    c1, c2 = cols[i], cols[min(i+1, len(cols)-1)]
    return tuple(int(c1[c]+(c2[c]-c1[c])*l) for c in range(3))

# ────────────────────────────────────────────────────────────────────────────
# Post-adjust
# ────────────────────────────────────────────────────────────────────────────
def _apply_bg_adjustments(bg: np.ndarray, cfg: Dict[str,Any]) -> np.ndarray:
    bright = cfg.get("brightness",1.0)
    contrast = cfg.get("contrast",1.0)
    if bright==1.0 and contrast==1.0:
        return bg
    out = bg.astype(np.float32)*contrast*bright
    return np.clip(out,0,255).astype(np.uint8)