-
Normalize every logo to identical content height. Default raster file
target is 240px (3× density for retina poster export at scale: 2;
80px gives only 1.7× and looks soft after PNG export). Use --invert off
if the source is already light-on-transparent (don't double-invert):
for f in lujiazui juanyi citic-bookstore citic-thinker-lab; do
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src "<configured-partners-source>/<brand>/<file>.png" \
--dst <event-assets>/partners/$f.png \
--height 240 --invert auto
done
Always normalize from the original source, never from a previously
normalized 80px file (upscaling = blurry — burned by this on juanyi).
-
For SVG sources, rasterize first. normalize_logo.py operates on
raster pixels and cannot crop SVG viewBox padding. Without this step
an SVG always renders smaller than rasterized PNG siblings:
rsvg-convert -h 720 brand.svg -o /tmp/brand-raw.png
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src /tmp/brand-raw.png --dst <event-assets>/partners/brand.png \
--height 240 --invert off
rsvg-convert ships with librsvg (brew install librsvg).
-
For SVG with embedded background rect (icon wrapped in a black/colored
rounded square — common in app-icon-style SVGs from find-logo), strip
the background before rasterizing, otherwise filter brightness(0) invert(1) flattens it into a solid white block that hides the icon:
sed -E 's|<rect[^/]*fill="#0+"[^/]*/>||' brand.svg > /tmp/brand-clean.svg
rsvg-convert -h 720 /tmp/brand-clean.svg -o /tmp/brand-raw.png
-
Wrap each logo in a fixed-size box (recommended over auto-width flex):
<span class="ps-logo-box"><img src="..." class="ps-logo"></span>
.ps-logo-box {
width: 96px; height: 30px;
display: inline-flex;
align-items: center; justify-content: center;
border: 1px solid rgba(255,255,255,0.10);
border-radius: 4px;
padding: 3px 6px;
box-sizing: border-box;
}
.ps-logo { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }
Fixed boxes give a stable matrix look — narrow logos (icon-only) and wide
logos (icon + wordmark) all occupy the same footprint, with the asset
scaled to fit. Auto-width flex (the older recipe) makes per-row total
widths unpredictable as logos get added/removed.
-
Dark-background unification — when the row sits on a dark canvas
(e.g. event poster), most brand logos are designed for white BG and look
inconsistent (some have black text, some have brand-colored marks). The
stable recipe:
.ps-logo { filter: brightness(0) invert(1) opacity(0.88); }
.ps-logo.ps-logo-original { filter: opacity(0.88); }
brightness(0) flattens all colors to black, then invert(1) produces
uniform white at the configured opacity. The .ps-logo-original escape
hatch is for source files that are already white-on-transparent (white
SVG variants from a brand kit) so you don't double-process them into
invisible black-on-dark.
-
Icon-only SVG → composite icon + wordmark — if the brand SVG only
has an icon (no "BrandName" wordmark beside it), don't ship just the icon
in a 96×30 box (it'll look like an unidentified mark). Compose the
wordmark with PIL using the brand's own font when possible:
from PIL import Image, ImageDraw, ImageFont, ImageOps
icon = Image.open('/tmp/brand-icon.png').convert('RGBA')
r, g, b, a = icon.split()
inv = Image.merge('RGB', (ImageOps.invert(r), ImageOps.invert(g), ImageOps.invert(b)))
icon = Image.merge('RGBA', (*inv.split(), a))
icon = icon.crop(icon.getbbox())
target_h = 240
icon = icon.resize((int(icon.width * target_h / icon.height), target_h), Image.LANCZOS)
font = ImageFont.truetype('partners/<brand>/fonts/<Family>.ttf', 150)
The PNG goes through the same brightness(0) invert(1) filter as raster
logos — match colors with all other entries automatically. Use the brand's
own font (often shipped under <brand>/fonts/ by the find-logo skill);
fall back to system SF / Helvetica only if no brand font is available.
-
Anti-pattern — do not try to fix alignment by setting per-logo
heights like .ps-logo-juanyi { height: 26px }. It's brittle (every new
logo needs another magic number), unstable across browsers, and breaks
the moment a designer reships the source asset with different padding.