mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-02 16:44:16 +02:00
Added scripts that import KDE Breeze icons and build the icon themes with a drawing for each size
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2006-2026 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
#
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# QElectroTech is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""Copy the Breeze icons listed in misc/make_icon_themes.py into ico/breeze/.
|
||||
|
||||
Run from the repository root with a checkout of
|
||||
https://github.com/KDE/breeze-icons:
|
||||
|
||||
python3 misc/import_breeze_icons.py /path/to/breeze-icons
|
||||
python3 misc/make_icon_themes.py
|
||||
|
||||
For each Breeze name in BREEZE it writes the art Breeze draws at 16, 22
|
||||
and 32 pixels, unchanged:
|
||||
|
||||
ico/breeze/16/<name>.svg
|
||||
ico/breeze/22/<name>.svg
|
||||
ico/breeze/32/<name>.svg
|
||||
|
||||
Many Breeze names are symbolic links to another file. The copy holds the
|
||||
linked file's content under the name BREEZE gives, so list the link's
|
||||
target in BREEZE and several QET icons share one copy. Every name needs
|
||||
22 pixel art; 16 and 32 are copied where Breeze has them.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from make_icon_themes import BREEZE, BREEZE_SIZES, ICO # noqa: E402
|
||||
|
||||
CATEGORIES = ["actions", "places", "mimetypes", "status", "devices", "apps", "preferences"]
|
||||
OUT = ICO / "breeze"
|
||||
|
||||
|
||||
def find(checkout, name, size):
|
||||
for category in CATEGORIES:
|
||||
path = checkout / "icons" / category / str(size) / f"{name}.svg"
|
||||
if path.exists():
|
||||
return path.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit(__doc__)
|
||||
checkout = Path(sys.argv[1])
|
||||
names = sorted(set(BREEZE.values()))
|
||||
wanted = set()
|
||||
for name in names:
|
||||
if not find(checkout, name, 22):
|
||||
sys.exit(f"{name}: no 22 pixel icon in {checkout}")
|
||||
for size in BREEZE_SIZES:
|
||||
source = find(checkout, name, size)
|
||||
if source:
|
||||
target = OUT / str(size) / f"{name}.svg"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(source.read_bytes())
|
||||
wanted.add(target)
|
||||
# Drop icons no longer listed, so ico/breeze/ holds only what the theme uses.
|
||||
for old in sorted(OUT.glob("*/*.svg")):
|
||||
if old not in wanted:
|
||||
old.unlink()
|
||||
print(f"{len(names)} Breeze icons, {len(wanted)} files written to {OUT.relative_to(ICO.parent)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+410
-23
@@ -26,8 +26,81 @@ It writes:
|
||||
ico/themes/qet-dark/index.theme the dark theme, inherits "qet"
|
||||
ico/themes/qet-dark/<size>/*.png light-ink copies of the line-art icons
|
||||
ico/themes/qet-dark/scalable/*.svg the same for the SVG icons
|
||||
ico/themes/qet-dark/breeze*/ the same for the Breeze SVGs
|
||||
ico/themes/qet-dark/traced*/ the same for the traced SVGs
|
||||
ico/generated/scalable-22/*.svg QET's 24 pixel canvas cut down to its 22 pixel art
|
||||
ico/generated/breeze-24/*.svg Breeze 22 pixel art on a 24 pixel canvas
|
||||
ico/generated/traced-24/*.svg traced 22 pixel art on a 24 pixel canvas
|
||||
ico/generated/traced-large-24/*.svg the smooth 22 pixel design on that canvas
|
||||
ico/icon-themes.qrc resource file listing both themes
|
||||
|
||||
An icon listed in BREEZE comes from the KDE Breeze theme instead of its
|
||||
PNG files. misc/import_breeze_icons.py copies Breeze's 16, 22 and 32
|
||||
pixel art into ico/breeze/. The icon's PNG files are left out of the
|
||||
theme, so Qt never picks them over the SVG.
|
||||
|
||||
Qt draws an SVG at the size asked for. Art drawn on a 16 pixel grid is
|
||||
sharp at 16, 32, 64 and 128 pixels, where each of its pixels covers
|
||||
whole screen pixels; at 24 its 1 pixel lines fall between pixels and
|
||||
blur. So each size folder of the theme aliases the art whose grid
|
||||
divides that size:
|
||||
|
||||
16x16 16 pixel art
|
||||
22x22 22 pixel art
|
||||
scalable 22 pixel art on a 24 pixel canvas, for Fusion's toolbar
|
||||
32x32 32 pixel art, else 16 pixel art doubled
|
||||
48x48 the 24 pixel canvas doubled
|
||||
64x64 32 pixel art doubled, else 16 pixel art
|
||||
128x128 32 pixel art, else 16 pixel art
|
||||
|
||||
On a 2x screen Qt asks for twice the pixels and takes the scalable file
|
||||
unless a folder is declared for that scale. A 16 pixel menu icon would
|
||||
then be the 24 pixel canvas drawn at 32 pixels, blurred. So each size
|
||||
folder has a twin with Scale=2 (16x16@2 and so on) that aliases the art
|
||||
for twice the pixels, by the same rule.
|
||||
|
||||
Where Breeze has no 16 pixel drawing, QET adds one in
|
||||
ico/breeze-added/16/, on Breeze's grid and in its colors, so every icon
|
||||
has art drawn for 16 pixels. Without one, the 32 pixel art is halved,
|
||||
which blurs.
|
||||
|
||||
32 pixel art drawn in another style than the 22 pixel art (a blue
|
||||
folder for a line-art one) is not used; the 16 pixel art keeps the
|
||||
icon's look.
|
||||
|
||||
Qt takes a Fixed folder only at its exact size and the scalable file for
|
||||
every other size, so a size not listed, such as the 50 pixels of the
|
||||
elements panel, is still scaled.
|
||||
|
||||
An SVG of ico/scalable/ is 22 pixel art on a 24 pixel canvas: sharp in
|
||||
the toolbar and blurred at 16 pixels, in a menu. One with a 16 pixel
|
||||
drawing of the same name in ico/scalable/16/ (the Align icons) is
|
||||
served as a Breeze icon is: the 16 pixel drawing, the 22 pixel art cut
|
||||
out of the canvas, and the file itself at 24 and 48 pixels.
|
||||
|
||||
An icon with files in ico/traced/ is QET's own PNG art redrawn as SVG.
|
||||
ico/traced/16 and ico/traced/22 hold the PNG copied pixel by pixel, so
|
||||
the icon looks as it did at those sizes. ico/traced/large16 and
|
||||
ico/traced/large22 hold the same two designs as smooth vectors, for the
|
||||
larger sizes and for 2x screens. A smooth vector still has its straight
|
||||
edges on the grid it was drawn on, so it follows the same rule as the
|
||||
Breeze art:
|
||||
|
||||
16x16 the 16 pixel copy
|
||||
22x22 the 22 pixel copy
|
||||
24x24 the 22 pixel copy on a 24 pixel canvas
|
||||
a multiple of 24 pixels (48, 96) the smooth 22 pixel design
|
||||
on a 24 pixel canvas
|
||||
another multiple of 16 (32, 64, 128...) the smooth 16 pixel design
|
||||
44 pixels and scalable the smooth 22 pixel design
|
||||
|
||||
The Scale=2 folders follow the table at twice their size. Four icons
|
||||
have a 32 pixel design, used in place of both smooth vectors.
|
||||
|
||||
An SVG is recolored for the dark theme through its text color (see
|
||||
dark_svg). A colored SVG, one without a text color, gets no dark copy
|
||||
and is inherited from the light theme, as a colored PNG is.
|
||||
|
||||
The light theme does not copy any file. The .qrc aliases the existing
|
||||
ico/<size>/<name>.png files into the theme layout Qt's icon loader
|
||||
expects, so QIcon::fromTheme("name") finds them. The dark theme only holds
|
||||
@@ -77,15 +150,18 @@ QRC = ICO / "icon-themes.qrc"
|
||||
# Fixed-size folders that hold toolbar, menu and dialog icons. The 24x16
|
||||
# flags, the 22x22/color swatches and the application icons are not theme
|
||||
# icons and stay on their resource paths.
|
||||
SIZES = ["16x16", "22x22", "32x32", "48x48", "128x128"]
|
||||
SIZES = ["16x16", "22x22", "32x32", "48x48", "64x64", "128x128"]
|
||||
|
||||
# The folder that holds the traced icons' 22 pixel art on a 24 pixel
|
||||
# canvas. It has no PNGs. Breeze icons use the scalable folder for this;
|
||||
# a traced icon's scalable file is its smooth vector.
|
||||
CANVAS_SIZE = "24x24"
|
||||
|
||||
# Table entries in sources/qeticons.cpp that pair a 16 pixel file with a
|
||||
# 22 pixel file of another name. The theme needs one name per icon, so
|
||||
# the 22 pixel file is exposed under the 16 pixel name as well. The folio
|
||||
# icons that used to be listed here are SVGs now (ico/scalable/).
|
||||
ALIASES = {
|
||||
"22x22/conductor2.png": "conductor-reset",
|
||||
}
|
||||
# and conductor icons that used to be listed here are SVGs now.
|
||||
ALIASES = {}
|
||||
|
||||
# SVG icons referenced from sources/qeticons.cpp. ico/scalable/ holds the
|
||||
# ones drawn for QET as vectors; one file serves every size.
|
||||
@@ -113,6 +189,132 @@ SVGS = [
|
||||
"generated/rect-to-polyline.svg",
|
||||
]
|
||||
|
||||
# Icons served by Breeze SVGs in ico/breeze/, QET name to Breeze name.
|
||||
# The Breeze name is the file a symbolic link resolves to, so QET icons
|
||||
# that share art share one copy (misc/import_breeze_icons.py).
|
||||
BREEZE = {
|
||||
"application-exit": "application-exit",
|
||||
"applications-development-translation": "amarok_change_language",
|
||||
"arrow-left": "go-previous",
|
||||
"arrow-right": "go-next",
|
||||
"circle": "draw-circle",
|
||||
"configure": "configure",
|
||||
"dialog-cancel": "dialog-cancel",
|
||||
"dialog-ok": "dialog-ok-apply",
|
||||
"document-close": "document-close",
|
||||
"document-export": "document-export",
|
||||
"document-import": "document-import",
|
||||
"document-new": "document-new",
|
||||
"document-open": "document-open",
|
||||
"document-open-recent": "document-open-recent",
|
||||
"document-print": "document-print",
|
||||
"document-print-frame": "document-print",
|
||||
"document-save": "document-save",
|
||||
"document-save-all": "document-save-all",
|
||||
"document-save-as": "document-save-as",
|
||||
"edit-clear": "edit-clear",
|
||||
"edit-clear-locationbar-ltr": "edit-clear-locationbar-ltr",
|
||||
"edit-copy": "edit-copy",
|
||||
"edit-cut": "edit-cut",
|
||||
"edit-delete": "edit-delete",
|
||||
"edit-download": "edit-download",
|
||||
"edit-paste": "edit-paste",
|
||||
"edit-redo": "edit-redo",
|
||||
"edit-rename": "document-edit",
|
||||
"edit-select-all": "edit-select-all",
|
||||
"edit-select-invert": "edit-select-invert",
|
||||
"edit-select-none": "edit-select-none",
|
||||
"edit-table-cell-merge": "edit-table-cell-merge",
|
||||
"edit-table-cell-split": "edit-table-cell-split",
|
||||
"edit-table-delete-column": "edit-table-delete-column",
|
||||
"edit-table-delete-row": "edit-table-delete-row",
|
||||
"edit-table-insert-column-left": "edit-table-insert-column-left",
|
||||
"edit-table-insert-column-right": "edit-table-insert-column-right",
|
||||
"edit-table-insert-row-above": "edit-table-insert-row-above",
|
||||
"edit-table-insert-row-under": "edit-table-insert-row-under",
|
||||
"edit-undo": "edit-undo",
|
||||
"ellipse": "draw-ellipse",
|
||||
"flip": "object-flip-vertical",
|
||||
"folder": "folder",
|
||||
"folder-new": "folder-new",
|
||||
"folder-open": "folder-open",
|
||||
"folder-properties": "document-properties",
|
||||
"format-text-bold": "format-text-bold",
|
||||
"format-text-italic": "format-text-italic",
|
||||
"format-text-subscript": "format-text-subscript",
|
||||
"format-text-superscript": "format-text-superscript",
|
||||
"format-text-underline": "format-text-underline",
|
||||
"go-bottom": "go-bottom",
|
||||
"go-down": "go-down",
|
||||
"go-down-double": "go-down-skip",
|
||||
"go-home": "go-home",
|
||||
"go-top": "go-top",
|
||||
"go-up": "go-up",
|
||||
"go-up-double": "go-up-skip",
|
||||
"grid": "view-grid",
|
||||
"help-contents": "help-contents",
|
||||
"help-donate": "love-amarok",
|
||||
"image-x-eps": "application-postscript",
|
||||
"insert-image": "insert-image",
|
||||
"item-cancel": "dialog-cancel",
|
||||
"item-copy": "edit-copy",
|
||||
"item-move": "transform-move",
|
||||
"kdenlive-show-video": "video-symbolic",
|
||||
"line": "draw-line",
|
||||
"list-add": "list-add",
|
||||
"list-remove": "paint-none",
|
||||
"masquer": "view-hidden",
|
||||
"mirror": "object-flip-horizontal",
|
||||
"object-group": "object-group",
|
||||
"object-locked": "document-encrypted",
|
||||
"object-rotate-right": "object-rotate-right",
|
||||
"object-unlocked": "document-decrypt",
|
||||
"polygon": "draw-polygon",
|
||||
"preferences-desktop-user": "preferences-desktop-user",
|
||||
"rectangle": "draw-rectangle",
|
||||
"restaurer": "view-visible",
|
||||
"select": "edit-select",
|
||||
"single_page": "snap-page",
|
||||
"start": "media-playback-start",
|
||||
"table-of-content": "gtk-index",
|
||||
"text": "insert-text",
|
||||
"transform-rotate": "transform-rotate",
|
||||
"transform-scale": "transform-scale",
|
||||
"two_pages": "view-pages-facing",
|
||||
"user-busy": "im-user-busy",
|
||||
"user-online": "im-user-online",
|
||||
"view-fit-window": "zoom-fit-best",
|
||||
"view-fullscreen": "view-fullscreen",
|
||||
"view-pim-journal": "view-calendar-journal",
|
||||
"view-refresh": "view-refresh",
|
||||
"view-restore": "view-restore",
|
||||
"view_fit_width": "zoom-fit-width",
|
||||
"window-new": "window-new",
|
||||
"zoom-draw": "zoom-fit-best",
|
||||
"zoom-in": "zoom-in",
|
||||
"zoom-original": "zoom-original",
|
||||
"zoom-out": "zoom-out",
|
||||
}
|
||||
|
||||
# Sizes Breeze draws its action icons at, the folders of ico/breeze/.
|
||||
BREEZE_SIZES = [16, 22, 32]
|
||||
|
||||
# Where the generator puts the 22 pixel Breeze art moved onto a 24 pixel
|
||||
# canvas (see on_24_canvas).
|
||||
BREEZE_24 = ICO / "generated" / "breeze-24"
|
||||
|
||||
# Icons traced from QET's PNG art: every name with a file in ico/traced/16.
|
||||
TRACED = sorted(svg.stem for svg in (ICO / "traced" / "16").glob("*.svg"))
|
||||
|
||||
# Where the generator puts the 22 pixel art cut out of the ico/scalable/
|
||||
# files that have a 16 pixel drawing (see grid_entries).
|
||||
SCALABLE_22 = ICO / "generated" / "scalable-22"
|
||||
|
||||
# Where the generator puts the 22 pixel traced art on a 24 pixel canvas:
|
||||
# the pixel copy and the smooth vector.
|
||||
TRACED_24 = ICO / "generated" / "traced-24"
|
||||
TRACED_LARGE_24 = ICO / "generated" / "traced-large-24"
|
||||
|
||||
SATURATED_FRACTION = 0.20 # at or above this an icon is "colored"
|
||||
WHITE_LIGHTNESS = 0.85 # a visible pixel this light counts as white
|
||||
WHITE_FRACTION = 0.30 # at or above this an icon is "light art"
|
||||
@@ -201,6 +403,152 @@ def invert_lightness(image):
|
||||
return out
|
||||
|
||||
|
||||
TEXT_COLOR = re.compile(r"(\.ColorScheme-(?:Text|ButtonText)\s*\{[^}]*?color:\s*)#[0-9a-fA-F]{6}")
|
||||
TEXT_CLASS = re.compile(r'class="[^"]*\bColorScheme-(?:Text|ButtonText)\b')
|
||||
|
||||
|
||||
def dark_svg(text):
|
||||
"""The dark copy of an SVG, or None when the art is colored.
|
||||
|
||||
A Breeze SVG colors its parts through a stylesheet: ColorScheme-Text
|
||||
for the ink, ColorScheme-NegativeText for red and so on. Only the text
|
||||
colors change; a red delete icon stays red. A file whose stylesheet
|
||||
declares the text color but draws nothing in it is colored. An SVG
|
||||
without such a stylesheet has its currentColor replaced.
|
||||
"""
|
||||
if "ColorScheme-" in text:
|
||||
if not TEXT_CLASS.search(text):
|
||||
return None
|
||||
dark, count = TEXT_COLOR.subn(rf"\g<1>{SVG_INK}", text)
|
||||
return dark if count else None
|
||||
if "currentColor" in text:
|
||||
return text.replace("currentColor", SVG_INK)
|
||||
return None
|
||||
|
||||
|
||||
def svg_size(tag):
|
||||
"""The canvas width of an <svg> tag: its viewBox, else its width."""
|
||||
box = re.search(r'\sviewBox="0 0 (\d+) \d+"', tag)
|
||||
width = re.search(r'\swidth="(\d+)(?:px)?"', tag)
|
||||
return int(box.group(1)) if box else int(width.group(1)) if width else None
|
||||
|
||||
|
||||
def on_24_canvas(text, path):
|
||||
"""The 22 pixel art moved 1 pixel right and down on a 24 pixel canvas,
|
||||
as Breeze's own 24 pixel icons are, so Fusion's 24 pixel toolbar slot
|
||||
draws it unscaled."""
|
||||
root = re.search(r"<svg\b[^>]*>", text)
|
||||
size = svg_size(root.group(0)) if root else None
|
||||
if size != 22:
|
||||
sys.exit(f"{path}: expected a 22x22 canvas")
|
||||
tag = re.sub(r'\s(viewBox|width|height)="[^"]*"', "", root.group(0))
|
||||
tag = tag[:-1] + ' viewBox="0 0 24 24">'
|
||||
body = text[root.end():]
|
||||
defs = re.match(r"\s*<defs\b.*?</defs>", body, re.S)
|
||||
head = defs.group(0) if defs else ""
|
||||
body = body[len(head):]
|
||||
end = body.rindex("</svg>")
|
||||
return (text[:root.start()] + tag + head + '\n <g transform="translate(1,1)">'
|
||||
+ body[:end] + " </g>\n" + body[end:])
|
||||
|
||||
|
||||
def color_classes(path):
|
||||
"""The Breeze color classes (Text, Accent, NegativeText...) an SVG draws with."""
|
||||
text = (ICO / path).read_text(encoding="utf-8")
|
||||
return set(re.findall(r'class="[^"]*?\bColorScheme-(\w+)', text))
|
||||
|
||||
|
||||
def breeze_entries(breeze_name):
|
||||
"""(theme folder, source path relative to ico/) for one Breeze icon,
|
||||
following the table in the module docstring."""
|
||||
art = {size: f"breeze/{size}/{breeze_name}.svg" for size in BREEZE_SIZES
|
||||
if (ICO / "breeze" / str(size) / f"{breeze_name}.svg").exists()}
|
||||
added = ICO / "breeze-added" / "16" / f"{breeze_name}.svg"
|
||||
if 16 not in art and added.exists():
|
||||
art[16] = str(added.relative_to(ICO))
|
||||
# Breeze sometimes draws the 32 pixel icon in another style, a blue
|
||||
# folder where the smaller ones are line art. Its color classes then
|
||||
# differ from the 22 pixel art's; the 16 pixel art keeps the look.
|
||||
if 32 in art and 16 in art and color_classes(art[32]) != color_classes(art[22]):
|
||||
del art[32]
|
||||
canvas = str((BREEZE_24 / f"{breeze_name}.svg").relative_to(ICO))
|
||||
small = art.get(16, art.get(32))
|
||||
large = art.get(32, art.get(16))
|
||||
return [("16x16", small), ("22x22", art[22]), ("scalable", canvas),
|
||||
("32x32", large), ("48x48", canvas), ("64x64", large), ("128x128", large),
|
||||
("16x16@2", large), ("22x22@2", art[22]), ("32x32@2", large),
|
||||
("48x48@2", canvas), ("64x64@2", large), ("128x128@2", large)]
|
||||
|
||||
|
||||
def without_canvas(text, path):
|
||||
"""The 22 pixel art of a 24 pixel canvas file: the same drawing seen
|
||||
through a 22 pixel window, the reverse of on_24_canvas."""
|
||||
root = re.search(r"<svg\b[^>]*>", text)
|
||||
if not root or svg_size(root.group(0)) != 24:
|
||||
sys.exit(f"{path}: expected a 24x24 canvas")
|
||||
tag = re.sub(r'\s(viewBox|width|height)="[^"]*"', "", root.group(0))
|
||||
tag = tag[:-1] + ' viewBox="1 1 22 22" width="22" height="22">'
|
||||
return text[:root.start()] + tag + text[root.end():]
|
||||
|
||||
|
||||
def grid_entries(name):
|
||||
"""(theme folder, source path relative to ico/) for an ico/scalable/
|
||||
icon with a 16 pixel drawing. The scalable folder is not listed; it
|
||||
holds the file already."""
|
||||
small = f"scalable/16/{name}.svg"
|
||||
art = str((SCALABLE_22 / f"{name}.svg").relative_to(ICO))
|
||||
canvas = f"scalable/{name}.svg"
|
||||
return [("16x16", small), ("22x22", art), ("32x32", small), ("48x48", canvas),
|
||||
("64x64", small), ("128x128", small),
|
||||
("16x16@2", small), ("22x22@2", art), ("32x32@2", small),
|
||||
("48x48@2", canvas), ("64x64@2", small), ("128x128@2", small)]
|
||||
|
||||
|
||||
def traced_entries(name):
|
||||
"""(theme folder, source path relative to ico/) for one traced icon,
|
||||
following the table in the module docstring."""
|
||||
pixels = {16: f"traced/16/{name}.svg", 22: f"traced/22/{name}.svg",
|
||||
24: str((TRACED_24 / f"{name}.svg").relative_to(ICO))}
|
||||
small, large = f"traced/large16/{name}.svg", f"traced/large22/{name}.svg"
|
||||
canvas = TRACED_LARGE_24 / f"{name}.svg"
|
||||
# An icon with a 32 pixel design has no canvas copy (see main).
|
||||
canvas = str(canvas.relative_to(ICO)) if canvas.exists() else large
|
||||
entries = []
|
||||
for folder in SIZES + [CANVAS_SIZE]:
|
||||
size = int(folder.split("x")[0])
|
||||
for scale, suffix in ((1, ""), (2, "@2")):
|
||||
px = size * scale
|
||||
entries.append((folder + suffix,
|
||||
pixels.get(px) or (canvas if px % 24 == 0 else
|
||||
small if px % 16 == 0 else large)))
|
||||
return entries + [("scalable", large)]
|
||||
|
||||
|
||||
def dark_path(source):
|
||||
"""Where the dark copy of a Breeze or traced SVG goes: one copy per
|
||||
source file, however many theme entries alias it."""
|
||||
if source.startswith("scalable/16/"):
|
||||
return "themes/qet-dark/scalable-16/" + Path(source).name
|
||||
return "themes/qet-dark/" + source.removeprefix("generated/")
|
||||
|
||||
|
||||
def write_canvases(folder, sources, convert=on_24_canvas):
|
||||
"""Write the 24 pixel canvas copy of each 22 pixel file in sources
|
||||
(name to path) into folder and drop the copies no longer wanted.
|
||||
Returns the number of files written or removed."""
|
||||
changed = 0
|
||||
wanted = set()
|
||||
for name, source in sorted(sources.items()):
|
||||
target = folder / f"{name}.svg"
|
||||
changed += write_if_changed(target, convert(source.read_text(encoding="utf-8"), source))
|
||||
wanted.add(target)
|
||||
for stale in sorted(folder.glob("*.svg")):
|
||||
if stale not in wanted:
|
||||
stale.unlink()
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def write_if_changed(path, data):
|
||||
"""Write text or bytes only when the content differs, so git stays quiet."""
|
||||
mode = "rb" if isinstance(data, bytes) else "r"
|
||||
@@ -234,6 +582,8 @@ def index_theme(name, comment, dirs, inherits=None):
|
||||
lines += ["Size=22", "Type=Scalable", "MinSize=8", "MaxSize=256"]
|
||||
else:
|
||||
lines += [f"Size={d.split('x')[0]}", "Type=Fixed"]
|
||||
if d.endswith("@2"):
|
||||
lines.append("Scale=2")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -241,12 +591,16 @@ def index_theme(name, comment, dirs, inherits=None):
|
||||
def main():
|
||||
light = [] # (alias, source) pairs, paths relative to ico/
|
||||
dark = [] # paths relative to ico/, files exist on disk
|
||||
dark_aliases = [] # (alias, source) pairs in the dark theme
|
||||
changed = 0
|
||||
line_art = colored = light_art = 0
|
||||
|
||||
grid = sorted(svg.stem for svg in (ICO / "scalable" / "16").glob("*.svg"))
|
||||
for size in SIZES:
|
||||
folder = ICO / size
|
||||
for png in sorted(folder.glob("*.png")):
|
||||
if png.stem in BREEZE or png.stem in TRACED or png.stem in grid:
|
||||
continue
|
||||
rel = f"{size}/{png.name}"
|
||||
names = [png.stem]
|
||||
if rel in ALIASES:
|
||||
@@ -271,45 +625,78 @@ def main():
|
||||
dark.append(f"themes/qet-dark/{size}/{name}.png")
|
||||
|
||||
for rel in SVGS:
|
||||
src = ICO / rel
|
||||
name = src.name.replace("-symbolic", "")
|
||||
name = (ICO / rel).name.replace("-symbolic", "")
|
||||
light.append((f"themes/qet/scalable/{name}", rel))
|
||||
text = src.read_text(encoding="utf-8")
|
||||
text = re.sub(r"color:#[0-9a-fA-F]{6}", f"color:{SVG_INK}", text)
|
||||
text = text.replace("currentColor", SVG_INK)
|
||||
target = THEMES / "qet-dark" / "scalable" / name
|
||||
changed += write_if_changed(target, text)
|
||||
text = dark_svg((ICO / rel).read_text(encoding="utf-8"))
|
||||
if text is None:
|
||||
# Colored art: like a colored PNG, the dark theme inherits it.
|
||||
colored += 1
|
||||
continue
|
||||
changed += write_if_changed(THEMES / "qet-dark" / "scalable" / name, text)
|
||||
dark.append(f"themes/qet-dark/scalable/{name}")
|
||||
|
||||
# Breeze and traced icons: the 24 pixel canvas copies, then one light
|
||||
# and at most one dark alias per theme folder, each to a shared file.
|
||||
overlap = sorted(set(BREEZE) & set(TRACED))
|
||||
if overlap:
|
||||
sys.exit("both a Breeze and a traced icon: " + ", ".join(overlap))
|
||||
changed += write_canvases(BREEZE_24, {name: ICO / "breeze" / "22" / f"{name}.svg"
|
||||
for name in set(BREEZE.values())})
|
||||
changed += write_canvases(TRACED_24, {name: ICO / "traced" / "22" / f"{name}.svg"
|
||||
for name in TRACED})
|
||||
smooth = {name: ICO / "traced" / "large22" / f"{name}.svg" for name in TRACED}
|
||||
changed += write_canvases(TRACED_LARGE_24, {
|
||||
name: path for name, path in smooth.items()
|
||||
if 'viewBox="0 0 22 22"' in path.read_text(encoding="utf-8")})
|
||||
changed += write_canvases(SCALABLE_22, {name: ICO / "scalable" / f"{name}.svg" for name in grid},
|
||||
without_canvas)
|
||||
entries = [(qet_name, breeze_entries(breeze_name)) for qet_name, breeze_name in BREEZE.items()]
|
||||
entries += [(name, grid_entries(name)) for name in grid]
|
||||
entries += [(name, traced_entries(name)) for name in TRACED]
|
||||
dark_files = {} # dark copy path -> written, one per source file
|
||||
for qet_name, folders in sorted(entries):
|
||||
for folder, source in folders:
|
||||
light.append((f"themes/qet/{folder}/{qet_name}.svg", source))
|
||||
copy = dark_path(source)
|
||||
if copy not in dark_files:
|
||||
text = dark_svg((ICO / source).read_text(encoding="utf-8"))
|
||||
dark_files[copy] = text is not None
|
||||
if text is not None:
|
||||
changed += write_if_changed(ICO / copy, text)
|
||||
if dark_files[copy]:
|
||||
dark_aliases.append((f"themes/qet-dark/{folder}/{qet_name}.svg", copy))
|
||||
dark += [copy for copy, written in dark_files.items() if written and copy not in dark]
|
||||
|
||||
# Complete each dark name with the light files of its other sizes
|
||||
# that read on a dark window (see the module docstring): aliases, no
|
||||
# copies.
|
||||
# copies. A colored SVG counts as reading, as it does in the test.
|
||||
folders = SIZES + [CANVAS_SIZE] + [f"{d}@2" for d in SIZES + [CANVAS_SIZE]] + ["scalable"]
|
||||
dark_sizes = {}
|
||||
for path in dark:
|
||||
size, name = path.split("/")[2:]
|
||||
dark_sizes.setdefault(name, set()).add(size)
|
||||
dark_aliases = []
|
||||
for path in dark + [alias for alias, _ in dark_aliases]:
|
||||
size, name = path.split("/")[2:4]
|
||||
if size in folders:
|
||||
dark_sizes.setdefault(name, set()).add(size)
|
||||
for alias, source in light:
|
||||
size, name = alias.split("/")[2:]
|
||||
if name in dark_sizes and size not in dark_sizes[name] \
|
||||
and reads_on_dark(Image.open(ICO / source).convert("RGBA")):
|
||||
and (source.endswith(".svg")
|
||||
or reads_on_dark(Image.open(ICO / source).convert("RGBA"))):
|
||||
dark_aliases.append((f"themes/qet-dark/{size}/{name}", source))
|
||||
|
||||
# Drop dark files from an earlier run that are no longer generated, so
|
||||
# a reclassified icon falls back to the light theme instead of keeping
|
||||
# a stale copy.
|
||||
for stale in sorted((THEMES / "qet-dark").glob("*/*")):
|
||||
for stale in sorted((THEMES / "qet-dark").rglob("*")):
|
||||
if stale.is_file() and stale.name != "index.theme" \
|
||||
and str(stale.relative_to(ICO)) not in dark:
|
||||
stale.unlink()
|
||||
changed += 1
|
||||
|
||||
dirs = SIZES + ["scalable"]
|
||||
changed += write_if_changed(THEMES / "qet" / "index.theme",
|
||||
index_theme("qet", "QElectroTech icons", dirs))
|
||||
index_theme("qet", "QElectroTech icons", folders))
|
||||
changed += write_if_changed(THEMES / "qet-dark" / "index.theme",
|
||||
index_theme("qet-dark", "QElectroTech icons for dark palettes",
|
||||
dirs, inherits="qet"))
|
||||
folders, inherits="qet"))
|
||||
|
||||
qrc = ["<!DOCTYPE RCC>", "<!-- Generated by misc/make_icon_themes.py. Do not edit. -->",
|
||||
'<RCC version="1.0">', ' <qresource prefix="/ico">',
|
||||
@@ -325,7 +712,7 @@ def main():
|
||||
changed += write_if_changed(QRC, "\n".join(qrc))
|
||||
|
||||
print(f"{line_art} line-art icons, {light_art} light icons, {colored} colored icons, "
|
||||
f"{len(SVGS)} SVGs; {changed} files written or removed")
|
||||
f"{len(SVGS)} SVGs, {len(BREEZE)} Breeze icons, {len(TRACED)} traced icons; {changed} files written or removed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user