Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified brand/apparel/back-lockup-2400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/apparel/chest-mark-1050.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 29 additions & 6 deletions brand/build/raster.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
import hashlib
import shutil
import subprocess
from pathlib import Path

try:
from PIL import Image, PngImagePlugin
except ModuleNotFoundError:
Image = PngImagePlugin = None

_PNG_COLORS = 32 # palette size; flat art needs few (8 already looked clean; 32 = headroom)
_SOURCE_KEY = "source-sha256"


def _source_hash(svg_path: Path, width: int | None, height: int | None) -> str:
h = hashlib.sha256(svg_path.read_bytes())
h.update(f"|{width}|{height}|{_PNG_COLORS}".encode())
return h.hexdigest()


def _stored_hash(png_path: Path) -> str | None:
if Image is None or not png_path.is_file():
return None
with Image.open(png_path) as im:
return im.info.get(_SOURCE_KEY)


def _quantize_png(path: Path, colors: int = _PNG_COLORS) -> None:
def _quantize_png(path: Path, source_hash: str, colors: int = _PNG_COLORS) -> None:
"""Re-save a PNG as an indexed-palette image — visually lossless for flat
art. FASTOCTREE preserves alpha, so it is correct for both the opaque
social cards and the transparent marks. No-op if Pillow is unavailable."""
try:
from PIL import Image
except ModuleNotFoundError:
if Image is None:
return
im = Image.open(path).convert("RGBA")
q = im.quantize(colors=colors, method=Image.Quantize.FASTOCTREE)
q.save(path, format="PNG", optimize=True)
info = PngImagePlugin.PngInfo()
info.add_text(_SOURCE_KEY, source_hash)
q.save(path, format="PNG", optimize=True, pnginfo=info)


def export_png(
Expand All @@ -28,12 +48,15 @@ def export_png(
exe = shutil.which("rsvg-convert")
if exe is None:
return False
source_hash = _source_hash(svg_path, width, height)
if _stored_hash(png_path) == source_hash:
return True
args = [exe]
if width is not None:
args += ["-w", str(width)]
if height is not None:
args += ["-h", str(height)]
args += [str(svg_path), "-o", str(png_path)]
subprocess.run(args, check=True)
_quantize_png(png_path)
_quantize_png(png_path, source_hash)
return True
Binary file modified brand/org/apple-touch-icon-180.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/avatar-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/avatar-circle-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/boosty-cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/favicon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/favicon-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/social-card-green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/social-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/social-square-green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/org/social-square.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/chat-app/lockup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/chat-app/mark-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/chat-app/mark-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/compose2pod/lockup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/compose2pod/mark-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/compose2pod/mark-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/db-retry/lockup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/db-retry/mark-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/db-retry/mark-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/eof-fixer/lockup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/eof-fixer/mark-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/eof-fixer/mark-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brand/projects/fastapi-sqlalchemy-template/lockup.png
Binary file modified brand/projects/fastapi-sqlalchemy-template/mark-1024.png
Binary file modified brand/projects/fastapi-sqlalchemy-template/mark-512.png
Binary file modified brand/projects/faststream-concurrent-aiokafka/lockup.png
Binary file modified brand/projects/faststream-concurrent-aiokafka/mark-1024.png
Binary file modified brand/projects/faststream-concurrent-aiokafka/mark-512.png
Binary file modified brand/projects/faststream-outbox/lockup.png
Binary file modified brand/projects/faststream-outbox/mark-1024.png
Binary file modified brand/projects/faststream-outbox/mark-512.png
Binary file modified brand/projects/faststream-outbox/social-card.png
Binary file modified brand/projects/faststream-redis-timers/lockup.png
Binary file modified brand/projects/faststream-redis-timers/mark-1024.png
Binary file modified brand/projects/faststream-redis-timers/mark-512.png
Binary file modified brand/projects/faststream-redis-timers/social-card.png
Binary file modified brand/projects/httpware/lockup.png
Binary file modified brand/projects/httpware/mark-1024.png
Binary file modified brand/projects/httpware/mark-512.png
Binary file modified brand/projects/httpware/social-card.png
Binary file modified brand/projects/jwks-client/lockup.png
Binary file modified brand/projects/jwks-client/mark-1024.png
Binary file modified brand/projects/jwks-client/mark-512.png
Binary file modified brand/projects/lite-bootstrap/lockup.png
Binary file modified brand/projects/lite-bootstrap/mark-1024.png
Binary file modified brand/projects/lite-bootstrap/mark-512.png
Binary file modified brand/projects/lite-bootstrap/social-card.png
Binary file modified brand/projects/litestar-sqlalchemy-template/lockup.png
Binary file modified brand/projects/litestar-sqlalchemy-template/mark-1024.png
Binary file modified brand/projects/litestar-sqlalchemy-template/mark-512.png
Binary file modified brand/projects/modern-di-aiogram/lockup.png
Binary file modified brand/projects/modern-di-aiogram/mark-1024.png
Binary file modified brand/projects/modern-di-aiogram/mark-512.png
Binary file modified brand/projects/modern-di-aiohttp/lockup.png
Binary file modified brand/projects/modern-di-aiohttp/mark-1024.png
Binary file modified brand/projects/modern-di-aiohttp/mark-512.png
Binary file modified brand/projects/modern-di-arq/lockup.png
Binary file modified brand/projects/modern-di-arq/mark-1024.png
Binary file modified brand/projects/modern-di-arq/mark-512.png
Binary file modified brand/projects/modern-di-celery/lockup.png
Binary file modified brand/projects/modern-di-celery/mark-1024.png
Binary file modified brand/projects/modern-di-celery/mark-512.png
Binary file modified brand/projects/modern-di-fastapi/lockup.png
Binary file modified brand/projects/modern-di-fastapi/mark-1024.png
Binary file modified brand/projects/modern-di-fastapi/mark-512.png
Binary file modified brand/projects/modern-di-fastmcp/lockup.png
Binary file modified brand/projects/modern-di-fastmcp/mark-1024.png
Binary file modified brand/projects/modern-di-fastmcp/mark-512.png
Binary file modified brand/projects/modern-di-faststream/lockup.png
Binary file modified brand/projects/modern-di-faststream/mark-1024.png
Binary file modified brand/projects/modern-di-faststream/mark-512.png
Binary file modified brand/projects/modern-di-flask/lockup.png
Binary file modified brand/projects/modern-di-flask/mark-1024.png
Binary file modified brand/projects/modern-di-flask/mark-512.png
Binary file modified brand/projects/modern-di-grpc/lockup.png
Binary file modified brand/projects/modern-di-grpc/mark-1024.png
Binary file modified brand/projects/modern-di-grpc/mark-512.png
Binary file modified brand/projects/modern-di-litestar/lockup.png
Binary file modified brand/projects/modern-di-litestar/mark-1024.png
Binary file modified brand/projects/modern-di-litestar/mark-512.png
Binary file modified brand/projects/modern-di-pytest/lockup.png
Binary file modified brand/projects/modern-di-pytest/mark-1024.png
Binary file modified brand/projects/modern-di-pytest/mark-512.png
Binary file modified brand/projects/modern-di-starlette/lockup.png
Binary file modified brand/projects/modern-di-starlette/mark-1024.png
Binary file modified brand/projects/modern-di-starlette/mark-512.png
Binary file modified brand/projects/modern-di-taskiq/lockup.png
Binary file modified brand/projects/modern-di-taskiq/mark-1024.png
Binary file modified brand/projects/modern-di-taskiq/mark-512.png
Binary file modified brand/projects/modern-di-typer/lockup.png
Binary file modified brand/projects/modern-di-typer/mark-1024.png
Binary file modified brand/projects/modern-di-typer/mark-512.png
Binary file modified brand/projects/modern-di/lockup.png
Binary file modified brand/projects/modern-di/mark-1024.png
Binary file modified brand/projects/modern-di/mark-512.png
Binary file modified brand/projects/modern-di/social-card.png
Binary file modified brand/projects/release-scope/lockup.png
Binary file modified brand/projects/release-scope/mark-1024.png
Binary file modified brand/projects/release-scope/mark-512.png
Binary file modified brand/projects/semvertag/lockup.png
Binary file modified brand/projects/semvertag/mark-1024.png
Binary file modified brand/projects/semvertag/mark-512.png
Binary file modified brand/projects/semvertag/social-card.png
Binary file modified brand/projects/sqlalchemy-inbox/lockup.png
Binary file modified brand/projects/sqlalchemy-inbox/mark-1024.png
Binary file modified brand/projects/sqlalchemy-inbox/mark-512.png
Binary file modified brand/projects/that-depends/lockup.png
Binary file modified brand/projects/that-depends/mark-1024.png
Binary file modified brand/projects/that-depends/mark-512.png
Binary file modified brand/projects/that-depends/social-card.png
Binary file modified docs/assets/social-card-green.png
55 changes: 55 additions & 0 deletions tests/test_raster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import shutil
from pathlib import Path

import pytest

from brand.build.raster import export_png

pytestmark = pytest.mark.skipif(
shutil.which("rsvg-convert") is None, reason="rsvg-convert not installed"
)

_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><rect width="10" height="10" fill="{fill}"/></svg>'


def _write_svg(path: Path, fill: str = "#c98a00") -> Path:
path.write_text(_SVG.format(fill=fill), encoding="utf-8")
return path


def test_unchanged_source_leaves_png_untouched(tmp_path: Path) -> None:
"""INVARIANT: re-rendering an unchanged SVG does not rewrite its PNG.

rsvg-convert builds differ byte-wise from machine to machine, so a render that
always rewrites turns every brand PR into a diff of every committed PNG.
"""
svg = _write_svg(tmp_path / "a.svg")
png = tmp_path / "a.png"
assert export_png(svg, png, width=32, height=32)
before = png.read_bytes()
stamp = png.stat().st_mtime_ns

assert export_png(svg, png, width=32, height=32)
assert png.stat().st_mtime_ns == stamp
assert png.read_bytes() == before


def test_changed_svg_rerenders(tmp_path: Path) -> None:
svg = _write_svg(tmp_path / "a.svg")
png = tmp_path / "a.png"
assert export_png(svg, png, width=32, height=32)
before = png.read_bytes()

_write_svg(svg, fill="#1f4d3a")
assert export_png(svg, png, width=32, height=32)
assert png.read_bytes() != before


def test_changed_size_rerenders(tmp_path: Path) -> None:
svg = _write_svg(tmp_path / "a.svg")
png = tmp_path / "a.png"
assert export_png(svg, png, width=32, height=32)
before = png.read_bytes()

assert export_png(svg, png, width=64, height=64)
assert png.read_bytes() != before
Loading