blob: 029816b66b967455d0fee1c98db4b6433e9a1858 [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.
"""Reproduce assets/logo.png from the product mark's vector geometry.
Run without arguments to write the image, or with --check to compare pixels.
"""
import math
import os
import sys
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"
VARIANT = V("logo", "", "", "#47A3E2", W, W, W)
# --- 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))
handle.write(b"\x89PNG\r\n\x1a\n")
handle.write(chunk(b"IHDR",
struct.pack(">IIBBBBB", size, h, 8, color_type, 0, 0, 0)))
handle.write(chunk(b"IDAT", zlib.compress(raw, 9)))
handle.write(chunk(b"IEND", b""))
def committed_scanlines(png):
"""Scanlines of a PNG this file wrote, or None if its header differs.
The check compares these to the fresh render rather than file bytes: IDAT
is whatever the running zlib produced, so a runner-image upgrade would
otherwise report drift that no one drew.
"""
if png[:8] != b"\x89PNG\r\n\x1a\n":
return None
pos, chunks = 8, []
while pos + 12 <= len(png):
length, tag = struct.unpack(">I4s", png[pos:pos + 8])
data = png[pos + 8:pos + 8 + length]
crc = png[pos + 8 + length:pos + 12 + length]
if len(data) != length or crc != struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF):
return None
chunks.append((tag, data))
pos += 12 + length
expected = struct.pack(">IIBBBBB", ART_SIZE, ART_SIZE, 8, 6, 0, 0, 0)
if (pos != len(png)
or [tag for tag, _ in chunks] != [b"IHDR", b"IDAT", b"IEND"]
or chunks[0][1] != expected
or chunks[2][1] != b""):
return None
try:
decoder = zlib.decompressobj()
scanlines = decoder.decompress(chunks[1][1]) + decoder.flush()
if not decoder.eof or decoder.unused_data or decoder.unconsumed_tail:
return None
return scanlines
except zlib.error:
return None
MACOS_MARGIN = 100 / 1024.0
ART_SIZE = 1024
def main(argv):
if argv not in ([], ["--check"]):
print("usage: generate-logo.py [--check]")
return 2
raw = render(VARIANT, ART_SIZE, inset=MACOS_MARGIN)
path = os.path.join(os.path.dirname(__file__), "..", "assets", "logo.png")
if argv:
with open(path, "rb") as handle:
if committed_scanlines(handle.read()) != raw:
print("logo is out of date; run python3 scripts/generate-logo.py")
return 1
print("logo matches the committed artwork")
else:
write_png(path, ART_SIZE, raw)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))