tools/icons.py (4586B)
1 #!/usr/bin/env python3
2 """로고 원본에서 마스크 자산과 파비콘을 만든다.
3
4 원본은 **흰 배경에 검은 선화**다. 알파 채널이 있지만 전부 불투명이라, 알파를
5 형태로 읽으면 마크가 단색 사각형으로 나오고 여백 트림도 안 먹는다. 형태는
6 명도에 있으므로 명도를 뒤집어 알파로 쓴다. (이걸 세 번 틀려서 스크립트가 됐다.)
7
8 마크는 마스크로만 나간다. 선 색은 CSS 가 정하고, 규칙은 하나다 — **다크면 흰
9 선, 라이트면 검은 선. 중간 회색은 없다.** 회색 선화는 비활성 상태로 읽힌다.
10
11 파비콘에는 CSS 가 닿지 않고 투명 배경은 브라우저 탭 색에 따라 사라진다. 그래서
12 불투명 판을 깔고 두 벌을 낸다 — 밝은 탭에는 검은 판에 흰 선, 어두운 탭에는 흰
13 판에 검은 선.
14
15 python3 tools/icons.py [원본디렉터리] # 기본 ~/Downloads
16 """
17
18 import sys
19 from pathlib import Path
20
21 from PIL import Image, ImageDraw, ImageFilter, ImageOps
22
23 ROOT = Path(__file__).resolve().parent.parent
24 IMG = ROOT / "static" / "images"
25 SRC_DIR = Path(sys.argv[1] if len(sys.argv) > 1 else "/home/himesama/Downloads")
26
27 # main.css 의 --ink / --bg 와 같은 값이어야 파비콘 판이 사이트와 같은 물건으로 보인다.
28 INK = (14, 18, 17)
29 PAPER = (234, 238, 236)
30
31 # 선 굵히기 커널(px, 홀수). 원본 해상도 기준이라 결과물에서는 훨씬 얇게 보인다.
32 THICKEN = 9
33
34
35 def shape(path: Path) -> Image.Image:
36 """선화를 알파 마스크로. 흰 배경 위에 합성한 뒤 명도를 뒤집는다."""
37 im = Image.open(path).convert("RGBA")
38 flat = Image.alpha_composite(Image.new("RGBA", im.size, (255,) * 4), im).convert("L")
39 a = ImageOps.invert(flat).point(lambda v: 0 if v < 24 else min(255, int(v * 1.35)))
40 # 선을 한 겹 불린다. 원본 획이 가늘어서 마스트헤드 크기(약 30px)나 파비콘에서는
41 # 안티에일리어싱에 먹혀 흐려진다. MaxFilter 는 알파의 최댓값을 퍼뜨리므로
42 # 획이 두꺼워지고, 홀수 커널이라 중심이 안 밀린다.
43 a = a.filter(ImageFilter.MaxFilter(THICKEN))
44 return a.crop(a.getbbox())
45
46
47 def write_mask(alpha: Image.Image, dst: Path, width: int) -> None:
48 h = max(1, round(alpha.height * width / alpha.width))
49 a = alpha.resize((width, h), Image.LANCZOS)
50 Image.merge("LA", (Image.new("L", a.size, 0), a)).save(dst, optimize=True)
51 print(f" {dst.relative_to(ROOT)} {width}x{h} {dst.stat().st_size // 1024}KB")
52
53
54 def plate(alpha: Image.Image, size: int, bg: tuple, fg: tuple, pad=0.05) -> Image.Image:
55 """둥근 사각 판 위에 마크. 판이 불투명해야 탭에서 사라지지 않는다.
56
57 여백은 거의 없다 — 파비콘은 16~32px 로 줄어들어서, 여백에 쓰는 1px 이
58 획에 쓰는 1px 보다 비싸다."""
59 card = Image.new("RGBA", (size, size), (0, 0, 0, 0))
60 mask = Image.new("L", (size, size), 0)
61 ImageDraw.Draw(mask).rounded_rectangle(
62 [0, 0, size - 1, size - 1], radius=round(size * 0.22), fill=255
63 )
64 card.paste(Image.new("RGBA", (size, size), bg + (255,)), (0, 0), mask)
65
66 inner = round(size * (1 - pad * 2))
67 w, h = alpha.size
68 w2, h2 = (inner, max(1, round(h * inner / w))) if w >= h else (max(1, round(w * inner / h)), inner)
69 a = alpha.resize((w2, h2), Image.LANCZOS)
70 layer = Image.new("RGBA", (w2, h2), fg + (255,))
71 layer.putalpha(a)
72 card.paste(layer, ((size - w2) // 2, (size - h2) // 2), layer)
73 return card
74
75
76 def main() -> int:
77 IMG.mkdir(parents=True, exist_ok=True)
78 (IMG / "projects").mkdir(exist_ok=True)
79
80 sqzer = shape(SRC_DIR / "sqzer_logo.png")
81 write_mask(sqzer, IMG / "mark.png", 128)
82 write_mask(shape(SRC_DIR / "sqzass_logo.png"), IMG / "projects" / "sqzass.png", 400)
83
84 # 흰 판에 검은 선, 두 벌 다. 판이 불투명한 흰색이면 밝은 탭에서도 어두운
85 # 탭에서도 그대로 보이므로 변형을 나눌 이유가 없다 — 그래도 두 파일을 남기는
86 # 것은 media 로 고르는 <link> 를 그대로 두기 위해서다.
87 plate(sqzer, 128, PAPER, INK).save(IMG / "favicon.png", optimize=True)
88 plate(sqzer, 128, PAPER, INK).save(IMG / "favicon-dark.png", optimize=True)
89 plate(sqzer, 180, PAPER, INK).convert("RGB").save(IMG / "apple-touch-icon.png", optimize=True)
90 for f in ("favicon.png", "favicon-dark.png", "apple-touch-icon.png"):
91 print(f" images/{f} {(IMG / f).stat().st_size}B")
92 return 0
93
94
95 if __name__ == "__main__":
96 sys.exit(main())