Files
2026-09-04 19:49:43 +02:00

284 lines
9.4 KiB
Python

#!/usr/bin/env python3
"""
make_icons.py - Erzeugt die Symbole der Erweiterung als SVG und PNG.
Die Geometrie ist EINMAL beschrieben und wird von zwei Ausgaben gelesen.
So können SVG und PNG nicht auseinanderlaufen.
SVG wird von LibreOffice skaliert und passt sich damit der eingestellten
Symbolgröße an (Extras → Optionen → Ansicht → Symbolgröße).
PNG in 16, 26 und 32 Pixeln als Rückfallebene, falls die
SVG-Unterstützung der eingesetzten Version nicht greift.
Alle Symbole teilen dieselbe Grundform - ein Blatt mit umgeknickter Ecke in
Paperless-Grün - damit die Leiste als zusammengehörige Gruppe erkennbar
bleibt. Unterschieden wird über eine farbige Scheibe unten rechts.
Aufruf: python3 make_icons.py (benötigt Pillow für die PNG-Ausgabe)
"""
import math
import os
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icons")
# Zeichenfläche in Einheiten. Die Formen reichen bewusst bis dicht an den
# Rand, damit bei kleiner Darstellung möglichst viel Fläche trägt.
BOX = 24.0
PNG_SIZES = (16, 26, 32)
SCALE = 12 # Überabtastung für die PNG-Ausgabe
GREEN = "#175420"
GREEN_LIGHT = "#3a8a44"
PAPER = "#fcfcfa"
WHITE = "#ffffff"
BLUE = "#1e60a8" # herunterladen
ORANGE = "#c86a14" # hinauf
VIOLET = "#703e98" # klassifizieren
TEAL = "#117a7a" # teilen
GREY = "#5c6064" # einstellungen
# ----------------------------------------------------------- Grundformen
def poly(points, fill=None, stroke=None, width=0.0):
return ("poly", points, fill, stroke, width)
def circle(cx, cy, r, fill=None, stroke=None, width=0.0):
return ("circle", (cx, cy, r), fill, stroke, width)
def line(p1, p2, stroke, width):
return ("line", (p1, p2), None, stroke, width)
def rrect(x0, y0, x1, y1, r, fill=None, stroke=None, width=0.0):
return ("rrect", (x0, y0, x1, y1, r), fill, stroke, width)
def sheet(colour=GREEN):
"""Blatt mit umgeknickter Ecke, füllt die Fläche links oben."""
x0, y0, x1, y1 = 0.6, 0.5, 18.4, 23.4
fold = 5.4
out = [
poly([(x0, y0), (x1 - fold, y0), (x1, y0 + fold), (x1, y1), (x0, y1)],
fill=PAPER, stroke=colour, width=1.8),
poly([(x1 - fold, y0), (x1, y0 + fold), (x1 - fold, y0 + fold)],
fill=colour),
]
for i, w in enumerate((10.4, 12.6, 8.4)):
yy = y0 + 7.4 + i * 3.8
out.append(line((x0 + 2.2, yy), (x0 + 2.2 + w, yy), colour, 1.4))
return out
def badge(colour):
"""Scheibe unten rechts, mit hellem Ring zur Abgrenzung vom Blatt."""
cx, cy, r = 16.9, 17.1, 6.7
return cx, cy, r, [
circle(cx, cy, r + 1.1, fill=WHITE),
circle(cx, cy, r, fill=colour),
]
def arrow(cx, cy, up=True, colour=PAPER):
w, h = 2.9, 3.5
if up:
return [poly([(cx, cy - h), (cx - w, cy - 0.3), (cx + w, cy - 0.3)],
fill=colour),
line((cx, cy - 0.6), (cx, cy + h * 0.85), colour, 2.0)]
return [poly([(cx, cy + h), (cx - w, cy + 0.3), (cx + w, cy + 0.3)],
fill=colour),
line((cx, cy - h * 0.85), (cx, cy + 0.6), colour, 2.0)]
# --------------------------------------------------------------- Symbole
def icon_logo():
"""Markensymbol für den Anfang der Leiste: gefüllte Fläche, weißes Blatt."""
out = [rrect(0.4, 0.4, 23.6, 23.6, 4.0, fill=GREEN)]
x0, y0, x1, y1 = 5.0, 3.9, 19.0, 20.1
fold = 4.4
out += [
poly([(x0, y0), (x1 - fold, y0), (x1, y0 + fold), (x1, y1), (x0, y1)],
fill=PAPER),
poly([(x1 - fold, y0), (x1, y0 + fold), (x1 - fold, y0 + fold)],
fill=GREEN_LIGHT),
]
for i, w in enumerate((7.4, 9.2, 6.0)):
yy = y0 + 6.0 + i * 3.4
out.append(line((x0 + 2.0, yy), (x0 + 2.0 + w, yy), GREEN, 1.5))
return out
def icon_open():
out = sheet()
cx, cy, _r, b = badge(BLUE)
return out + b + arrow(cx, cy, up=False)
def icon_version():
out = sheet()
cx, cy, _r, b = badge(ORANGE)
out += b + arrow(cx - 1.2, cy, up=True)
px, py, a = cx + 3.4, cy - 2.6, 1.7
out += [line((px - a, py), (px + a, py), PAPER, 1.4),
line((px, py - a), (px, py + a), PAPER, 1.4)]
return out
def icon_save_as():
# Zweites Blatt dahinter: es kann ein eigenständiges Dokument entstehen.
out = [poly([(4.4, 0.5), (18.4, 0.5), (18.4, 4.6), (4.4, 4.6)],
fill=PAPER, stroke=GREEN_LIGHT, width=1.5)]
out += sheet()
cx, cy, _r, b = badge(ORANGE)
out += b + arrow(cx - 1.2, cy, up=True)
sx, sy, a = cx + 3.4, cy - 2.6, 1.9
for dx, dy in ((a, 0), (0, a), (a * .7, a * .7), (-a * .7, a * .7)):
out.append(line((sx - dx, sy - dy), (sx + dx, sy + dy), PAPER, 1.15))
return out
def icon_classify():
out = sheet()
cx, cy, _r, b = badge(VIOLET)
out += b + [
poly([(cx - 3.6, cy - 0.5), (cx + 0.4, cy - 4.0),
(cx + 3.7, cy - 0.6), (cx - 0.2, cy + 3.6)], fill=PAPER),
circle(cx + 1.4, cy - 1.5, 0.9, fill=VIOLET),
]
return out
def icon_share():
out = sheet()
cx, cy, _r, b = badge(TEAL)
a, bb, c = (cx + 2.6, cy - 3.2), (cx + 2.6, cy + 3.2), (cx - 3.2, cy)
out += b + [line(c, a, PAPER, 1.3), line(c, bb, PAPER, 1.3)]
for p in (a, bb, c):
out.append(circle(p[0], p[1], 1.65, fill=PAPER))
return out
def icon_settings():
out = sheet()
cx, cy, _r, b = badge(GREY)
out += b
outer, inner, hole = 5.0, 3.4, 1.6
for i in range(8):
ang = math.pi * i / 4
dx, dy = math.cos(ang), math.sin(ang)
out.append(line((cx + dx * inner * .7, cy + dy * inner * .7),
(cx + dx * outer, cy + dy * outer), PAPER, 1.7))
out += [circle(cx, cy, inner, stroke=PAPER, width=1.5),
circle(cx, cy, hole, fill=GREY)]
return out
ICONS = {
"paperless-logo": icon_logo,
"paperless-open": icon_open,
"paperless-version": icon_version,
"paperless-saveas": icon_save_as,
"paperless-classify": icon_classify,
"paperless-share": icon_share,
"paperless-settings": icon_settings,
}
# ----------------------------------------------------------- Ausgabe SVG
def to_svg(shapes):
out = ['<?xml version="1.0" encoding="UTF-8"?>',
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '
'viewBox="0 0 %g %g">' % (BOX, BOX)]
def attrs(fill, stroke, width):
a = ' fill="%s"' % (fill or "none")
if stroke:
a += (' stroke="%s" stroke-width="%g" stroke-linecap="round"'
' stroke-linejoin="round"' % (stroke, width))
return a
for kind, data, fill, stroke, width in shapes:
if kind == "poly":
pts = " ".join("%g,%g" % p for p in data)
out.append(' <polygon points="%s"%s/>'
% (pts, attrs(fill, stroke, width)))
elif kind == "circle":
cx, cy, r = data
out.append(' <circle cx="%g" cy="%g" r="%g"%s/>'
% (cx, cy, r, attrs(fill, stroke, width)))
elif kind == "line":
(x1, y1), (x2, y2) = data
out.append(' <line x1="%g" y1="%g" x2="%g" y2="%g"%s/>'
% (x1, y1, x2, y2, attrs(None, stroke, width)))
elif kind == "rrect":
x0, y0, x1, y1, r = data
out.append(' <rect x="%g" y="%g" width="%g" height="%g" '
'rx="%g"%s/>'
% (x0, y0, x1 - x0, y1 - y0, r,
attrs(fill, stroke, width)))
out.append("</svg>")
return "\n".join(out) + "\n"
# ----------------------------------------------------------- Ausgabe PNG
def to_png(shapes, size):
from PIL import Image, ImageDraw
def rgb(c):
c = c.lstrip("#")
return tuple(int(c[i:i + 2], 16) for i in (0, 2, 4)) + (255,)
s = SCALE
img = Image.new("RGBA", (int(BOX * s), int(BOX * s)), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
for kind, data, fill, stroke, width in shapes:
f = rgb(fill) if fill else None
st = rgb(stroke) if stroke else None
w = max(1, int(width * s))
if kind == "poly":
pts = [(x * s, y * s) for x, y in data]
d.polygon(pts, fill=f, outline=st, width=w if st else 0)
elif kind == "circle":
cx, cy, r = data
box = [(cx - r) * s, (cy - r) * s, (cx + r) * s, (cy + r) * s]
if f:
d.ellipse(box, fill=f)
if st:
d.ellipse(box, outline=st, width=w)
elif kind == "line":
(x1, y1), (x2, y2) = data
d.line([(x1 * s, y1 * s), (x2 * s, y2 * s)], fill=st, width=w)
elif kind == "rrect":
x0, y0, x1, y1, r = data
d.rounded_rectangle([x0 * s, y0 * s, x1 * s, y1 * s],
radius=r * s, fill=f,
outline=st, width=w if st else 0)
return img.resize((size, size), Image.LANCZOS)
def main():
os.makedirs(OUT, exist_ok=True)
for name, fn in ICONS.items():
shapes = fn()
with open(os.path.join(OUT, name + ".svg"), "w",
encoding="utf-8") as fh:
fh.write(to_svg(shapes))
for size in PNG_SIZES:
to_png(shapes, size).save(
os.path.join(OUT, "%s_%d.png" % (name, size)),
"PNG", optimize=True)
print("erzeugt: %s.svg und PNG in %s"
% (name, ", ".join("%d" % z for z in PNG_SIZES)))
if __name__ == "__main__":
main()