Add configurable title, matte dimensions, and interactive installer
- Configurable collage title via localStorage (bird:title) with admin settings UI - Configurable matte opening dimensions in display.py (mat_opening_w/mat_opening_h) - Interactive frame installer (install-interactive.sh) with guided setup prompts Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+37
-17
@@ -51,7 +51,9 @@ DEFAULTS = {
|
||||
"shoot_title": None, "shoot_subtitle": None,
|
||||
"shoot_headline_px": 42, "shoot_eyebrow_px": 18, "shoot_lowercase": False,
|
||||
"shoot_mat": 0.04, "shoot_small_floor": 0.04, "shoot_count_exp": 0.65,
|
||||
"mat": 0.0, # extra global shrink of the content inside the A5 opening
|
||||
"mat": 0.0, # extra global shrink of the content inside the opening
|
||||
"mat_opening_w": 0, # matte opening width in inches (0 = use A5 default)
|
||||
"mat_opening_h": 0, # matte opening height in inches (0 = use A5 default)
|
||||
"rotate": 90, # 90 or 270 if the frame hangs the other way up
|
||||
"saturation": 0.6,
|
||||
"panel": "", # "el133uf1" forces the 13.3" driver if auto() fails
|
||||
@@ -131,14 +133,27 @@ def _paper(img):
|
||||
return tuple(int(statistics.median(c)) for c in zip(*px))
|
||||
|
||||
|
||||
# The mat opening is an A5 rectangle (1 : sqrt(2)) centred in the panel; the
|
||||
# content floats inside it with `mat` of inner whitespace.
|
||||
A5_H = PANEL_H * 0.7071 # A5 is 1/sqrt(2) of the panel height
|
||||
A5_W = A5_H / 1.41421 # A5 aspect 1 : sqrt(2)
|
||||
# The mat opening defaults to A5 (1 : sqrt(2)) for the standard kit frame.
|
||||
# Override with mat_opening_w / mat_opening_h (in inches) for custom frames.
|
||||
_A5_H = PANEL_H * 0.7071
|
||||
_A5_W = _A5_H / 1.41421
|
||||
|
||||
# Panel physical dimensions in inches (13.3" diagonal, 4:3 aspect in portrait)
|
||||
_PANEL_W_IN = 8.4
|
||||
_PANEL_H_IN = 11.2
|
||||
|
||||
|
||||
def _place(content, paper, mat):
|
||||
s = min(A5_W * (1 - mat) / content.width, A5_H * (1 - mat) / content.height)
|
||||
def _opening_dims(cfg):
|
||||
w = float(cfg.get("mat_opening_w") or 0)
|
||||
h = float(cfg.get("mat_opening_h") or 0)
|
||||
if w > 0 and h > 0:
|
||||
return PANEL_W * (w / _PANEL_W_IN), PANEL_H * (h / _PANEL_H_IN)
|
||||
return _A5_W, _A5_H
|
||||
|
||||
|
||||
def _place(content, paper, mat, opening):
|
||||
op_w, op_h = opening
|
||||
s = min(op_w * (1 - mat) / content.width, op_h * (1 - mat) / content.height)
|
||||
nw, nh = max(1, round(content.width * s)), max(1, round(content.height * s))
|
||||
content = content.resize((nw, nh), Image.LANCZOS)
|
||||
canvas = Image.new("RGB", (PANEL_W, PANEL_H), paper)
|
||||
@@ -177,9 +192,12 @@ def _centroid_x(img, paper):
|
||||
TITLE_H_FRAC, COLLAGE_FRAC, GAP_FRAC = 0.065, 0.66, 0.1
|
||||
|
||||
|
||||
def mat_and_center(img, mat):
|
||||
"""Crop the title and collage, size each to a fraction of the A5 opening,
|
||||
def mat_and_center(img, mat, opening=None):
|
||||
"""Crop the title and collage, size each to a fraction of the opening,
|
||||
stack with a gap, and centre on the panel."""
|
||||
if opening is None:
|
||||
opening = (_A5_W, _A5_H)
|
||||
op_w, op_h = opening
|
||||
img = img.convert("RGB")
|
||||
paper = _paper(img)
|
||||
mask = ImageChops.difference(img, Image.new("RGB", img.size, paper))
|
||||
@@ -193,7 +211,7 @@ def mat_and_center(img, mat):
|
||||
for y in range(top, bot):
|
||||
if levels[y] <= 2:
|
||||
run += 1
|
||||
if run >= 60: # split below the headline; a 60px band clears the ~30px eyebrow/headline gap so the title stays whole
|
||||
if run >= 60:
|
||||
cy = y
|
||||
while cy < bot and levels[cy] <= 2:
|
||||
cy += 1
|
||||
@@ -203,9 +221,9 @@ def mat_and_center(img, mat):
|
||||
run = 0
|
||||
tb = _region_bbox(img, paper, top, split[0]) if split else None
|
||||
cb = _region_bbox(img, paper, split[1], bot + 1) if split else None
|
||||
box_w, box_h = A5_W * (1 - mat), A5_H * (1 - mat)
|
||||
box_w, box_h = op_w * (1 - mat), op_h * (1 - mat)
|
||||
if not (tb and cb):
|
||||
return _place(img.crop(full), paper, mat)
|
||||
return _place(img.crop(full), paper, mat, opening)
|
||||
title = _scale_h(img.crop(tb), box_h * TITLE_H_FRAC)
|
||||
gap = round(box_h * GAP_FRAC)
|
||||
# Size the collage to fill the room left under the fixed-size title,
|
||||
@@ -242,9 +260,10 @@ def quantize_spectra6(img):
|
||||
return img.convert("RGB").quantize(palette=pal, dither=Image.Dither.FLOYDSTEINBERG).convert("RGB")
|
||||
|
||||
|
||||
def _draw_mat_box(img):
|
||||
"""Dev aid: outline the A5 mat opening so the matte and centring show."""
|
||||
x0, y0 = round((PANEL_W - A5_W) / 2), round((PANEL_H - A5_H) / 2)
|
||||
def _draw_mat_box(img, opening=None):
|
||||
"""Dev aid: outline the mat opening so the matte and centring show."""
|
||||
op_w, op_h = opening if opening else (_A5_W, _A5_H)
|
||||
x0, y0 = round((PANEL_W - op_w) / 2), round((PANEL_H - op_h) / 2)
|
||||
ImageDraw.Draw(img).rectangle((x0, y0, PANEL_W - x0 - 1, PANEL_H - y0 - 1),
|
||||
outline=(170, 60, 56), width=2)
|
||||
|
||||
@@ -351,11 +370,12 @@ def run(cfg, preview=None, force=False, use_signature=True, mat_box=False):
|
||||
except Exception as e:
|
||||
print(f"could not get image: {e}", file=sys.stderr) # keep last panel image
|
||||
return
|
||||
img = mat_and_center(img, cfg["mat"])
|
||||
opening = _opening_dims(cfg)
|
||||
img = mat_and_center(img, cfg["mat"], opening)
|
||||
if preview:
|
||||
out = quantize_spectra6(img)
|
||||
if mat_box:
|
||||
_draw_mat_box(out)
|
||||
_draw_mat_box(out, opening)
|
||||
out.save(preview)
|
||||
print(f"wrote preview {preview}")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user