blob: 2575fadf9f4f1a9ccf2b9f53e3d4b9c7954518df [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Regenerate the shipped app-icon artwork.
Every PNG under apps/desktop/assets/app-icons/ except `mono.png` comes out of
this file. The mark is not traced, not drawn over stock art, and not generated
by a model: it is a handful of stroked polylines defined by the constants
below, so the provenance of the artwork is the source you are reading.
python3 scripts/generate-app-icons.py # rewrite every icon
python3 scripts/generate-app-icons.py --check # verify, write nothing
`--check` is what the test harness uses: it re-renders and compares bytes, so
a change to the geometry that is not reflected in the committed PNGs fails.
The harness passes no names, so every shipped tile is compared, and rendering
is spread across all cores to keep that affordable.
There is no SVG rasteriser available in CI, so the PNGs are rendered here by
evaluating the same geometry as signed distance fields — including the miter
wedge a sharp join adds, which a plain capsule union would miss.
Artwork is written at 1024x1024 with a 100px transparent margin on all four
sides. That is the macOS icon grid: the solid block occupies 824/1024 (80.5%)
and the dock scales the canvas without adding padding of its own, so a
full-bleed tile would sit 24% wider than every neighbour.
"""
import io
import math
import os
import sys
from multiprocessing import Pool
import struct
import zlib
SIZE = 1024.0
CORNER_R = 229.0
STROKE = 70.0
HW = STROKE / 2.0
APEX = (512, 274)
VALLEY = (512, 800)
FOOT_L, FOOT_R = (180, 845), (844, 845)
BAR_Y = 162
BAR = [(405, BAR_Y), (619, BAR_Y)]
# clearance between the bar's underside and the top of the tip's round join,
# kept at ~60% of the stroke so the two never read as one shape
BAR_GAP = (APEX[1] - HW) - (BAR_Y + HW)
def shoulder(foot):
"""Where the inner arm crosses the leg. The inner V mirrors the outer legs,
so by symmetry the crossing sits halfway between apex and valley."""
sy = (APEX[1] + VALLEY[1]) / 2.0
sx = APEX[0] + (foot[0] - APEX[0]) * (sy - APEX[1]) / (foot[1] - APEX[1])
return (round(sx, 1), sy)
SHOULDER_L, SHOULDER_R = shoulder(FOOT_L), shoulder(FOOT_R)
def mark_box():
"""viewBox for the bare mark: the glyph's bounding box."""
x0, x1 = FOOT_L[0] - HW, FOOT_R[0] + HW
y0, y1 = BAR_Y - HW, FOOT_L[1] + HW
return ("%g %g %g %g" % (x0, y0, x1 - x0, y1 - y0), int(x1 - x0), int(y1 - y0))
# How the tip meets the M, i.e. what the boundary between the two colours looks
# like. Whoever is painted last owns the joint:
# flat tip on top, butt cap on the shoulder — cut across the leg, and it
# shaves off the M's corner
# round tip on top, round cap — arc bulging down into the M
# tangent as round, raised half a stroke so the arc just touches the shoulder
# round-up M on top, round join — its round shoulder bulges up into the tip
# flat-up M on top, legs run half a stroke past the shoulder, cut flat
# miter-up M on top, sharp (miter) join — the corner runs out to a point and
# the boundary lies along the inner arm's outer edge, i.e. parallel
# to the M's two middle strokes
JOINT = "miter-up"
JOINT_STYLES = {
"flat": dict(lift=0.0, cap="butt", m_on_top=False, join="round"),
"round": dict(lift=0.0, cap="round", m_on_top=False, join="round"),
"tangent": dict(lift=HW, cap="round", m_on_top=False, join="round"),
"round-up": dict(lift=0.0, cap="butt", m_on_top=True, join="round"),
"flat-up": dict(lift=HW, cap="butt", m_on_top=True, join="round"),
"miter-up": dict(lift=0.0, cap="butt", m_on_top=True, join="miter"),
}
def along_leg(shoulder, lift):
"""Walk `lift` units from a shoulder towards the apex."""
dx, dy = APEX[0] - shoulder[0], APEX[1] - shoulder[1]
n = math.hypot(dx, dy)
return (shoulder[0] + dx / n * lift, shoulder[1] + dy / n * lift)
def parts(joint=None):
"""(name, polyline, cap, join), painted in order — the last one drawn owns
the shoulder, so its outline is what the colour boundary follows."""
s = JOINT_STYLES[joint or JOINT]
lift, join = s["lift"], s["join"]
bar = [("bar", BAR, "round", "round")]
if not s["m_on_top"]:
body = [("body", [FOOT_L, SHOULDER_L, VALLEY, SHOULDER_R, FOOT_R],
"round", "round")]
tip = [("tip", [along_leg(SHOULDER_L, lift), APEX,
along_leg(SHOULDER_R, lift)], s["cap"], "round")]
return body + tip + bar
# M on top: the tip runs down to the shoulders and is painted over.
tip = [("tip", [SHOULDER_L, APEX, SHOULDER_R], "butt", "round")]
if join == "miter":
# each half is one stroke so the shoulder is a real join and can be
# mitred; they overlap at the valley, which stays round
body = [("body", [FOOT_L, SHOULDER_L, VALLEY], "round", "miter"),
("body", [FOOT_R, SHOULDER_R, VALLEY], "round", "miter")]
else:
body = [("body", [FOOT_L, SHOULDER_L, VALLEY, SHOULDER_R, FOOT_R],
"round", "round")]
# stubs carry the M past the shoulder and stop flat there
body += [("body", [along_leg(s2, -lift), along_leg(s2, lift)], "butt", "round")
for s2 in (SHOULDER_L, SHOULDER_R) if lift]
return tip + body + bar
# name, group, label, background, body, tip, bar
V = lambda name, group, label, bg, body, tip, bar: dict(
name=name, group=group, label=label, bg=bg, body=body, tip=tip, bar=bar)
def grad(c1, c2, angle=90):
"""A gradient paint spec, valid for the tile or for any stroked part.
angle: 0 = left-to-right, 90 = top-to-bottom. A bare (c1, c2) tuple is the
original top-to-bottom form and is still accepted everywhere.
"""
return ("grad", c1, c2, angle)
W = "#FFFFFF"
VARIANTS = [
# --- 浅蓝系 ---------------------------------------------------------
V("sky", "蓝", "原色 sky", "#47A3E2", W, W, W),
V("cyan", "蓝", "青蓝", "#4FC3D9", W, W, W),
V("ice", "蓝", "冰蓝渐变", ("#9BDCFB", "#4A9FE0"), W, W, W),
V("pale-inverted", "蓝", "淡底深标", "#DCEEFB", "#2E86D6", "#2E86D6", "#2E86D6"),
# --- 黑白 -----------------------------------------------------------
V("ink", "黑白", "墨黑底", "#111315", W, W, W),
V("paper", "黑白", "纸白底", "#FFFFFF", "#14161A", "#14161A", "#14161A"),
V("graphite", "黑白", "白底灰尖", "#FFFFFF", "#14161A", "#8A9199", "#14161A"),
# --- 铅笔 -----------------------------------------------------------
V("pencil-kraft", "铅笔", "牛皮纸底", "#D9C7A4", "#F5B301", "#14110F", W),
V("pencil-sky", "铅笔", "天蓝底", "#47A3E2", "#FFC531", "#17191B", W),
V("pencil-navy", "铅笔", "深蓝底", "#1F3A5F", "#F7C948", "#0B0D10", W),
# --- 高山 -----------------------------------------------------------
V("alpine", "高山", "晴空雪山", ("#C8E9FF", "#79C3F0"), "#3E5C76", W, W),
V("dusk", "高山", "黄昏", ("#FFC48C", "#F4886B"), "#35314C", "#FFF3E4", W),
V("night", "高山", "夜山", ("#16233A", "#31527A"), "#0C1422", "#E8F1FF", "#DCE8FF"),
V("forest", "高山", "苍绿", ("#D6ECDD", "#8FC7A8"), "#2C4A3B", W, W),
# --- 深色 -----------------------------------------------------------
# ink was the only true dark tile; a mid-tone one glows like a light leak
# in a dark dock or a dark UI shell.
V("midnight", "深色", "午夜蓝", "#0A1220", "#9EC7F0", "#E8F2FF", "#5B8DEF"),
V("carbon", "深色", "纯黑 OLED", "#000000", "#3FA9F5", "#7FD4FF", "#3FA9F5"),
V("slate", "深色", "石板", "#1E242C", "#8FA6BC", "#DDE7F0", "#6B8299"),
V("obsidian", "深色", "曜石", grad("#1A1030", "#0A0616", 135),
"#A78BFA", "#E9E2FF", "#7C5CE0"),
# --- 霓虹 -----------------------------------------------------------
# Amber CRT and phosphor green are the terminal's own history, not styling.
V("neon-cyan", "霓虹", "荧光青", "#0B0F14", "#00E5FF", "#B8FBFF", "#00A8CC"),
V("matrix", "霓虹", "磷绿", "#050A06", "#39FF14", "#C8FFB8", "#1F9E0A"),
V("magenta", "霓虹", "品红", "#14091F", "#FF3DCE", "#FFD6F5", "#B01E8E"),
V("amber-crt", "霓虹", "琥珀 CRT", "#0D0A05", "#FFB000", "#FFE7B0", "#C77A00"),
# --- 莫兰迪 ---------------------------------------------------------
V("clay", "莫兰迪", "陶土", "#C97B63", "#FFF6F0", W, "#8E4A36"),
V("sage", "莫兰迪", "鼠尾草", "#9CAF88", "#2F3D28", "#F4F8EF", "#2F3D28"),
V("dust", "莫兰迪", "灰粉", "#C4A69C", "#4A362F", "#FBF4F1", "#4A362F"),
V("fog", "莫兰迪", "雾蓝", "#A8B5BF", "#2B3742", "#F2F6F9", "#2B3742"),
# --- 暖色 -----------------------------------------------------------
V("sunset", "暖色", "日落", grad("#FF9A5A", "#FF5E7E", 135), W, "#FFF0E8", W),
V("amber", "暖色", "琥珀", "#F5A623", "#3D2A08", "#FFF6E2", "#3D2A08"),
V("terracotta", "暖色", "赤陶", grad("#E2825A", "#C4553C", 135),
"#FFF2EA", W, "#8E3A26"),
# --- 自然 -----------------------------------------------------------
V("ocean", "自然", "深海", grad("#0E4D64", "#1B7A8C", 160), "#DFF6F8", W, W),
V("moss", "自然", "苔原", grad("#2F4A34", "#1C2E20", 135), "#9FD4A8", W, "#6FA97A"),
V("desert", "自然", "沙漠", grad("#EBCF9C", "#D89A55", 135),
"#4A2E16", "#FFF8EC", "#4A2E16"),
V("glacier", "自然", "冰川", grad("#F0FAFF", "#BFE4F5", 135),
"#2C5F7C", "#6FB5D8", "#2C5F7C"),
# --- 金属(描边渐变)------------------------------------------------
V("gold", "金属", "鎏金", "#141210", grad("#F7E7A0", "#B8860B", 120),
grad("#FFF8D8", "#D4AF37", 120), grad("#F7E7A0", "#B8860B", 120)),
V("chrome", "金属", "铬", "#16181C", grad("#F4F7FA", "#8A97A6", 120),
grad("#FFFFFF", "#B8C4D0", 120), grad("#F4F7FA", "#8A97A6", 120)),
# --- 高对比 ---------------------------------------------------------
# Not a style but an obligation: one-colour printing, and 7:1 contrast.
V("mono-black", "高对比", "纯黑", W, "#000000", "#000000", "#000000"),
V("mono-white", "高对比", "纯白", "#000000", W, W, W),
V("hazard", "高对比", "黑黄", "#111111", "#FFD400", "#FFD400", "#FFD400"),
]
# --- svg --------------------------------------------------------------------
def svg(variant, transparent=False, joint=None, box=None, inset=0.0):
name = variant["name"]
bg = variant["bg"]
# the bare mark is cropped to the glyph's bounding box
box = box or (mark_box() if transparent else ("0 0 1024 1024", 1024, 1024))
out = ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="%s" '
'width="%d" height="%d">' % box,
' <title>%s</title>' % variant["label"]]
defs = []
def paint_of(spec, ident, user_space):
"""A fill/stroke value, registering a gradient def when needed.
Stroke gradients must be userSpaceOnUse: under the default
objectBoundingBox each path gets its own box, so the ramp would
restart at every seam between body, tip and bar.
"""
gr = as_grad(spec)
if gr is None:
return spec
c1, c2, angle = gr
if user_space:
_, mw, mh = mark_box()
p1, p2 = grad_axis(angle, (FOOT_L[0] - HW, BAR_Y - HW,
float(mw), float(mh)))
coords = ('x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" '
'gradientUnits="userSpaceOnUse"' % (p1 + p2))
else:
p1, p2 = grad_axis(angle, (0.0, 0.0, 1.0, 1.0))
coords = 'x1="%.4f" y1="%.4f" x2="%.4f" y2="%.4f"' % (p1 + p2)
defs.append(' <linearGradient id="%s" %s>'
'<stop offset="0" stop-color="%s"/>'
'<stop offset="1" stop-color="%s"/></linearGradient>'
% (ident, coords, c1, c2))
return "url(#%s)" % ident
paint = paint_of(bg, "bg-" + name, False)
strokes = {p: paint_of(variant[p], "%s-%s" % (p, name), True)
for p in ("body", "tip", "bar")}
if defs:
out += [' <defs>'] + defs + [' </defs>']
if not transparent:
if inset:
out.append(' <g transform="translate(%g %g) scale(%g)">'
% (inset * SIZE, inset * SIZE, 1 - 2 * inset))
out.append(' <rect width="1024" height="1024" rx="229" ry="229" fill="%s"/>' % paint)
if inset:
out.append(' </g>')
if inset:
out.append(' <g transform="translate(%g %g) scale(%g)">'
% (inset * SIZE, inset * SIZE, 1 - 2 * inset))
out.append(' <g fill="none" stroke-width="70" stroke-miterlimit="8">')
for part, points, cap, join in parts(joint):
d = "M " + " L ".join("%.1f %.1f" % p for p in points)
out.append(' <path d="%s" stroke="%s" stroke-linecap="%s" '
'stroke-linejoin="%s"/>' % (d, strokes[part], cap, join))
out.append(' </g>')
if inset:
out.append(' </g>')
out += ['</svg>', '']
return "\n".join(out)
# --- png --------------------------------------------------------------------
def rgb(h):
h = h.lstrip("#")
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def as_grad(spec):
"""Normalise a paint spec to (c1, c2, angle) or None if it is flat.
A bare 2-tuple is the original top-to-bottom form and stays valid.
"""
if isinstance(spec, str):
return None
if spec[0] == "grad":
return (spec[1], spec[2], spec[3] if len(spec) > 3 else 90)
return (spec[0], spec[1], 90)
def grad_axis(angle, box):
"""The two endpoints of the gradient axis, matching how SVG maps a
linearGradient onto a box: the unit-square endpoints scaled onto it."""
x0, y0, w, h = box
a = math.radians(angle)
cx, sy = math.cos(a), math.sin(a)
p1 = (x0 + (0.5 - cx / 2) * w, y0 + (0.5 - sy / 2) * h)
p2 = (x0 + (0.5 + cx / 2) * w, y0 + (0.5 + sy / 2) * h)
return p1, p2
def ramp(spec, box):
"""Compile a paint spec into f(x, y) -> (r, g, b)."""
gr = as_grad(spec)
if gr is None:
c = rgb(spec)
return lambda x, y: c
ca, cb, angle = rgb(gr[0]), rgb(gr[1]), gr[2]
(px, py), (qx, qy) = grad_axis(angle, box)
dx, dy = qx - px, qy - py
den = dx * dx + dy * dy or 1.0
def f(x, y):
t = ((x - px) * dx + (y - py) * dy) / den
t = 0.0 if t < 0.0 else (1.0 if t > 1.0 else t)
return (ca[0] + (cb[0] - ca[0]) * t,
ca[1] + (cb[1] - ca[1]) * t,
ca[2] + (cb[2] - ca[2]) * t)
return f
def miter_rhombus(p, v, n):
"""The wedge a miter join fills at vertex `v`, between segments p->v->n.
Returned as the full rhombus where the two stroke bands' infinite strips
cross, not just the wedge above the vertex: the extra half sits inside the
two bands, so this overlaps them instead of merely abutting them. Abutting
shapes each cover ~50% of the pixels along the shared edge and the union
would show a hairline of whatever is underneath. Returns None if straight.
"""
d1 = (v[0] - p[0], v[1] - p[1])
d2 = (n[0] - v[0], n[1] - v[1])
l1, l2 = math.hypot(*d1), math.hypot(*d2)
d1, d2 = (d1[0] / l1, d1[1] / l1), (d2[0] / l2, d2[1] / l2)
cross = d1[0] * d2[1] - d1[1] * d2[0]
if abs(cross) < 1e-9:
return None
k = HW / cross
pts = [(v[0] + k * (s1 * d2[0] - s2 * d1[0]),
v[1] + k * (s1 * d2[1] - s2 * d1[1]))
for s1 in (1, -1) for s2 in (1, -1)]
pts.sort(key=lambda q: math.atan2(q[1] - v[1], q[0] - v[0]))
return pts
def prims(joint=None):
"""Flatten the parts into drawing primitives with bounding boxes."""
out = []
for part, points, cap, join in parts(joint):
for i in range(len(points) - 1):
(ax, ay), (bx, by) = points[i], points[i + 1]
ra = cap == "round" if i == 0 else join == "round"
rb = cap == "round" if i == len(points) - 2 else join == "round"
out.append((part, "seg", (ax, ay, bx, by, ra, rb),
(min(ax, bx) - HW - 2, min(ay, by) - HW - 2,
max(ax, bx) + HW + 2, max(ay, by) + HW + 2)))
if join == "miter":
for i in range(1, len(points) - 1):
q = miter_rhombus(points[i - 1], points[i], points[i + 1])
if q:
xs, ys = [p[0] for p in q], [p[1] for p in q]
out.append((part, "poly", q,
(min(xs) - 2, min(ys) - 2, max(xs) + 2, max(ys) + 2)))
return out
_PRIMS = {}
def prims_for(joint):
joint = joint or JOINT
if joint not in _PRIMS:
_PRIMS[joint] = prims(joint)
return _PRIMS[joint]
def seg_sdf(px, py, ax, ay, bx, by, ra, rb):
vx, vy = bx - ax, by - ay
length = math.hypot(vx, vy)
ux, uy = vx / length, vy / length
wx, wy = px - ax, py - ay
s = wx * ux + wy * uy # axial position, 0..length
dp = abs(wx * uy - wy * ux) # perpendicular distance
d = max(dp - HW, -s, s - length) # band with butt ends
if ra:
d = min(d, math.hypot(wx, wy) - HW)
if rb:
d = min(d, math.hypot(px - bx, py - by) - HW)
return d
def poly_sdf(px, py, pts):
"""Exact distance to a convex polygon, negative inside."""
best = 1e18
pos = neg = False
for i in range(len(pts)):
ax, ay = pts[i]
bx, by = pts[(i + 1) % len(pts)]
vx, vy = bx - ax, by - ay
wx, wy = px - ax, py - ay
t = (wx * vx + wy * vy) / (vx * vx + vy * vy)
t = 0.0 if t < 0.0 else (1.0 if t > 1.0 else t)
dx, dy = wx - t * vx, wy - t * vy
best = min(best, math.sqrt(dx * dx + dy * dy))
if vx * wy - vy * wx > 0.0: # same side of every edge = inside,
pos = True # whichever way the quad is wound
else:
neg = True
return best if (pos and neg) else -best
def render(variant, size, transparent=False, joint=None, inset=0.0):
k = 1.0 - 2.0 * inset
scale = size / SIZE * k # pixels per design unit
aa = 1.0 / scale # antialiasing width, design units
origin = inset * SIZE * (size / SIZE) # margin, in pixels
half = SIZE / 2.0
inner = half - CORNER_R
prim = prims_for(joint)
order = list(dict.fromkeys(p for p, _, _, _ in prim))
# the tile gradient spans the canvas; a stroke gradient spans the glyph's
# bounding box, so it matches the SVG's userSpaceOnUse ramp exactly
_, mw, mh = mark_box()
mbox = (FOOT_L[0] - HW, BAR_Y - HW, float(mw), float(mh))
bg_paint = ramp(variant["bg"], (0.0, 0.0, SIZE, SIZE))
part_paint = {p: ramp(variant[p], mbox) for p in order}
rows = []
for iy in range(size):
y = (iy + 0.5 - origin) / scale
row = bytearray(b"\x00") # PNG filter: none
for ix in range(size):
x = (ix + 0.5 - origin) / scale
cover = {}
for part, kind, data, (x0, y0, x1, y1) in prim:
if x < x0 or x > x1 or y < y0 or y > y1:
continue
d = (seg_sdf(x, y, *data) if kind == "seg"
else poly_sdf(x, y, data))
if d < aa:
c = min(1.0, max(0.0, 0.5 - d / aa))
if c > cover.get(part, 0.0):
cover[part] = c
if transparent:
alpha = 0.0
r = g = b = 0.0
else:
qx = max(abs(x - half) - inner, 0.0)
qy = max(abs(y - half) - inner, 0.0)
d = math.hypot(qx, qy) - CORNER_R
alpha = min(1.0, max(0.0, 0.5 - d / aa))
r, g, b = bg_paint(x, y)
for part in order:
c = cover.get(part, 0.0)
if not c:
continue
if not transparent:
c = min(c, alpha) # never paint outside the tile
else:
alpha = max(alpha, c)
cr, cg, cb = part_paint[part](x, y)
r += (cr - r) * c
g += (cg - g) * c
b += (cb - b) * c
px = (r, g, b, alpha * 255)
row += bytes(0 if c < 0 else (255 if c > 255 else int(c + .5)) for c in px)
rows.append(bytes(row))
return b"".join(rows)
def write_png(path, size, raw, height=None, rgb=False):
with open(path, "wb") as handle:
write_png_stream(handle, size, raw, height=height, rgb=rgb)
def write_png_stream(handle, size, raw, height=None, rgb=False):
"""Write raw scanlines out as a PNG.
Defaults to the square RGBA image every icon is; `height` and `rgb` exist
for the contact sheet, which is neither.
"""
color_type = 2 if rgb else 6 # 2 = RGB, 6 = RGBA
h = height or size
def chunk(tag, data):
return (struct.pack(">I", len(data)) + tag + data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
fh = handle
if True:
fh.write(b"\x89PNG\r\n\x1a\n")
fh.write(chunk(b"IHDR",
struct.pack(">IIBBBBB", size, h, 8, color_type, 0, 0, 0)))
fh.write(chunk(b"IDAT", zlib.compress(raw, 9)))
fh.write(chunk(b"IEND", b""))
MACOS_MARGIN = 100 / 1024.0
ART_SIZE = 1024
def art_directory():
here = os.path.dirname(os.path.abspath(__file__))
return os.path.join(here, "..", "apps", "desktop", "assets", "app-icons")
def _render_one(variant):
"""Render one icon. Top-level so it survives the pickling a Pool does."""
return variant["name"], render(variant, ART_SIZE, inset=MACOS_MARGIN)
def main(argv):
check = "--check" in argv
# Positional names limit the run to a subset. This is a convenience for
# working on one colourway by hand — the test deliberately passes none, so
# CI always compares the whole catalogue.
wanted = {a for a in argv if not a.startswith("-")}
selected = [v for v in VARIANTS if not wanted or v["name"] in wanted]
unknown = wanted - {v["name"] for v in VARIANTS}
if unknown:
print("unknown icon(s): %s" % ", ".join(sorted(unknown)))
return 2
dest = art_directory()
stale = []
# Rasterising 38 tiles at 1024px is ~90s of single-threaded signed-distance
# evaluation, which is the difference between checking the whole catalogue
# in CI and checking a sample of it. The work is per-icon and pure, so it
# parallelises exactly.
workers = min(len(selected), os.cpu_count() or 1)
if workers > 1:
with Pool(workers) as pool:
rendered = pool.map(_render_one, selected)
else:
rendered = [_render_one(v) for v in selected]
for name, raw in rendered:
path = os.path.join(dest, name + ".png")
if check:
buffer = io.BytesIO()
write_png_stream(buffer, ART_SIZE, raw)
with open(path, "rb") as handle:
if handle.read() != buffer.getvalue():
stale.append(name)
else:
write_png(path, ART_SIZE, raw)
if not check:
print("wrote %d icons to %s" % (len(selected), dest))
return 0
if stale:
print("out of date: %s" % ", ".join(stale))
print("run: python3 scripts/generate-app-icons.py")
return 1
print("all %d icons match the committed artwork" % len(selected))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))