www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git

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())