Skip to content

Commit d05f44a

Browse files
authored
Merge branch 'main' into issue-531-temporal-margin
2 parents 7615a65 + 11381c5 commit d05f44a

18 files changed

+944
-7
lines changed

dist/generate_assets.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python
2+
"""Generate pyscenedetect.ico and logo PNGs from SVG sources.
3+
4+
Requires Inkscape (for SVG rasterization) and Pillow (for ICO generation).
5+
"""
6+
7+
import contextlib
8+
import shutil
9+
import subprocess
10+
import sys
11+
import tempfile
12+
from pathlib import Path
13+
from typing import NamedTuple
14+
15+
from PIL import Image, ImageFilter
16+
17+
18+
class LogoOutput(NamedTuple):
19+
path: Path
20+
width: int
21+
height: int
22+
source: Path
23+
24+
# Colors matching the SVG design
25+
BG = (224, 232, 240, 255) # #e0e8f0
26+
FG = (42, 53, 69, 255) # #2a3545
27+
28+
RASTER_SIZES = [16, 24, 32, 48, 64, 128, 256]
29+
30+
SHARPEN_AMOUNT = {
31+
24: 75,
32+
32: 75,
33+
48: 75,
34+
64: 100,
35+
128: 150,
36+
256: 150,
37+
}
38+
39+
SHARPEN_RADIUS = 0.5
40+
41+
DIST_DIR = Path(__file__).resolve().parent
42+
REPO_DIR = DIST_DIR.parent
43+
LOGO_DIR = DIST_DIR / "logo"
44+
ICO_PATH = DIST_DIR / "pyscenedetect.ico"
45+
46+
LOGO_SVG = LOGO_DIR / "pyscenedetect-logo.svg"
47+
LOGO_BG_SVG = LOGO_DIR / "pyscenedetect-logo-bg.svg"
48+
49+
# Heights match the natural SVG aspect ratio (1024x480).
50+
# _small outputs use the -bg variant (background included).
51+
FAVICON_OUTPUTS: list[Path] = [
52+
REPO_DIR / "docs" / "_static" / "favicon.ico",
53+
REPO_DIR / "website" / "pages" / "img" / "favicon.ico",
54+
]
55+
56+
LOGO_OUTPUTS: list[LogoOutput] = [
57+
LogoOutput(REPO_DIR / "docs" / "_static" / "pyscenedetect_logo.png", 900, 422, LOGO_SVG),
58+
LogoOutput(REPO_DIR / "docs" / "_static" / "pyscenedetect_logo_small.png", 300, 141, LOGO_BG_SVG),
59+
LogoOutput(REPO_DIR / "website" / "pages" / "img" / "pyscenedetect_logo.png", 640, 300, LOGO_BG_SVG),
60+
LogoOutput(REPO_DIR / "website" / "pages" / "img" / "pyscenedetect_logo_small.png", 462, 217, LOGO_SVG),
61+
]
62+
63+
SVG_FOR_SIZE: dict[int, Path] = {
64+
24: LOGO_DIR / "pyscenedetect-24.svg",
65+
32: LOGO_DIR / "pyscenedetect-32.svg",
66+
48: LOGO_DIR / "pyscenedetect.svg",
67+
64: LOGO_DIR / "pyscenedetect.svg",
68+
128: LOGO_DIR / "pyscenedetect.svg",
69+
256: LOGO_DIR / "pyscenedetect.svg",
70+
}
71+
72+
73+
def make_icon_16() -> Image.Image:
74+
"""Create a hand-crafted 16x16 clapperboard icon."""
75+
img = Image.new("RGBA", (16, 16), FG)
76+
px = img.load()
77+
78+
# Clear 1px padding on all sides
79+
for i in range(16):
80+
px[0, i] = BG
81+
px[15, i] = BG
82+
px[i, 0] = BG
83+
px[i, 15] = BG
84+
85+
# Arm stripe gaps (rows 2–4): clear pixels not part of a complete stripe.
86+
# A stripe x+y=s spans all 3 arm rows only when 5 <= s <= 16.
87+
for y in range(2, 5):
88+
for x in range(1, 15):
89+
if y < 4 and x < 3:
90+
continue
91+
if y > 2 and x > 12:
92+
continue
93+
if not ((x + y) % 4 < 2 and 5 <= (x + y) <= 16):
94+
px[x, y] = BG
95+
96+
# Slate interior (rows 8–12, cols 3–12)
97+
for y in range(8, 13):
98+
for x in range(3, 13):
99+
px[x, y] = BG
100+
101+
return img
102+
103+
104+
def find_inkscape() -> str:
105+
"""Find the Inkscape executable."""
106+
inkscape = shutil.which("inkscape")
107+
if inkscape:
108+
return inkscape
109+
# Common Windows install path
110+
candidate = Path(r"C:\Program Files\Inkscape\bin\inkscape.exe")
111+
if candidate.exists():
112+
return str(candidate)
113+
print("Error: Inkscape not found. Please install it or add it to PATH.", file=sys.stderr)
114+
sys.exit(1)
115+
116+
117+
def render_svg(inkscape: str, svg: Path, output: Path, width: int, height: int):
118+
"""Render an SVG to a PNG at the given dimensions using Inkscape."""
119+
subprocess.run(
120+
[inkscape, str(svg), "--export-type=png", f"--export-filename={output}", "-w", str(width), "-h", str(height)],
121+
check=True,
122+
capture_output=True,
123+
)
124+
125+
126+
def render_logos(inkscape: str):
127+
"""Render the logo SVG to all required PNG outputs."""
128+
print("Rendering logo PNGs...")
129+
for entry in LOGO_OUTPUTS:
130+
print(f" {entry.path.relative_to(REPO_DIR)} ({entry.width}x{entry.height}) [source: {entry.source.name}]...")
131+
render_svg(inkscape, entry.source, entry.path, entry.width, entry.height)
132+
print(f" Done ({len(LOGO_OUTPUTS)} files).")
133+
134+
135+
def render_all_sizes(inkscape: str, work_dir: Path) -> list[Image.Image]:
136+
"""Render the SVG at all icon sizes, applying sharpening where configured."""
137+
images = []
138+
for size in RASTER_SIZES:
139+
png_path = work_dir / f"icon_{size}.png"
140+
if size == 16:
141+
print(f" Using hand-crafted {size}x{size} icon...")
142+
img = make_icon_16()
143+
img.save(png_path)
144+
else:
145+
svg_path = SVG_FOR_SIZE[size]
146+
print(f" Rendering {size}x{size} using {svg_path.name}...")
147+
render_svg(inkscape, svg_path, png_path, size, size)
148+
img = Image.open(png_path).copy()
149+
if size in SHARPEN_AMOUNT:
150+
img = img.filter(ImageFilter.UnsharpMask(radius=SHARPEN_RADIUS, percent=SHARPEN_AMOUNT[size], threshold=0))
151+
print(f" Sharpened {size}x{size} (USM {SHARPEN_AMOUNT[size]}%)")
152+
img.save(png_path)
153+
images.append(img)
154+
return images
155+
156+
157+
def main():
158+
persist_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else None
159+
if persist_dir:
160+
persist_dir.mkdir(parents=True, exist_ok=True)
161+
print(f"Persisting PNGs to: {persist_dir}")
162+
163+
inkscape = find_inkscape()
164+
print(f"Using Inkscape: {inkscape}")
165+
print(f"Logo directory: {LOGO_DIR}")
166+
167+
ctx = contextlib.nullcontext(str(persist_dir)) if persist_dir else tempfile.TemporaryDirectory()
168+
with ctx as work:
169+
images = render_all_sizes(inkscape, Path(work))
170+
images[-1].save(ICO_PATH, format="ICO", append_images=images[:-1])
171+
172+
print(f"Output ICO: {ICO_PATH}")
173+
print("Copying favicons...")
174+
for dest in FAVICON_OUTPUTS:
175+
shutil.copy2(ICO_PATH, dest)
176+
print(f" {dest.relative_to(REPO_DIR)}")
177+
render_logos(inkscape)
178+
179+
180+
if __name__ == "__main__":
181+
main()

dist/logo/pyscenedetect-24.svg

Lines changed: 81 additions & 0 deletions
Loading

dist/logo/pyscenedetect-32.svg

Lines changed: 81 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)