Initial commit

This commit is contained in:
2026-09-04 19:50:29 +02:00
parent 742e5bc6b2
commit 3ae0ab0a88
37 changed files with 3459 additions and 0 deletions

282
make_icons.py Normal file
View File

@@ -0,0 +1,282 @@
#!/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 für die Symbolleisten, wird von Thunderbird skaliert.
PNG in 16, 32 und 64 Pixeln - das Manifest verlangt feste Größen, und
für die Erweiterungsverwaltung wird 64 gebraucht.
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, 32, 64)
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
# Thunderbird braucht weniger Symbole als die LibreOffice-Fassung: Es gibt
# nur zwei Schaltflächen (Ablegen im Nachrichtenfenster, Einfügen im
# Verfassenfenster) und das Symbol der Erweiterung selbst.
ICONS = {
"paperless-logo": icon_logo,
"paperless-store": icon_version,
"paperless-insert": icon_open,
"paperless-share": icon_share,
}
# ----------------------------------------------------------- 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()