Fixed pack renaming and added css support

This commit is contained in:
NicklasVraa
2023-08-20 23:00:52 +02:00
parent 1905bfe2df
commit 5c2cbd0eab
9 changed files with 6602 additions and 66 deletions

View File

@@ -0,0 +1,150 @@
{
"aliceblue": "#f0f8ff",
"antiquewhite": "#faebd7",
"aqua": "#00ffff",
"aquamarine": "#7fffd4",
"azure": "#f0ffff",
"beige": "#f5f5dc",
"bisque": "#ffe4c4",
"black": "#000000",
"blanchedalmond": "#ffebcd",
"blue": "#0000ff",
"blueviolet": "#8a2be2",
"brown": "#a52a2a",
"burlywood": "#deb887",
"cadetblue": "#5f9ea0",
"chartreuse": "#7fff00",
"chocolate": "#d2691e",
"coral": "#ff7f50",
"cornflowerblue": "#6495ed",
"cornsilk": "#fff8dc",
"crimson": "#dc143c",
"cyan": "#00ffff",
"darkblue": "#00008b",
"darkcyan": "#008b8b",
"darkgoldenrod": "#b8860b",
"darkgray": "#a9a9a9",
"darkgreen": "#006400",
"darkgrey": "#a9a9a9",
"darkkhaki": "#bdb76b",
"darkmagenta": "#8b008b",
"darkolivegreen": "#556b2f",
"darkorange": "#ff8c00",
"darkorchid": "#9932cc",
"darkred": "#8b0000",
"darksalmon": "#e9967a",
"darkseagreen": "#8fbc8f",
"darkslateblue": "#483d8b",
"darkslategray": "#2f4f4f",
"darkslategrey": "#2f4f4f",
"darkturquoise": "#00ced1",
"darkviolet": "#9400d3",
"deeppink": "#ff1493",
"deepskyblue": "#00bfff",
"dimgray": "#696969",
"dimgrey": "#696969",
"dodgerblue": "#1e90ff",
"firebrick": "#b22222",
"floralwhite": "#fffaf0",
"forestgreen": "#228b22",
"fuchsia": "#ff00ff",
"gainsboro": "#dcdcdc",
"ghostwhite": "#f8f8ff",
"goldenrod": "#daa520",
"gold": "#ffd700",
"gray": "#808080",
"green": "#008000",
"greenyellow": "#adff2f",
"grey": "#808080",
"honeydew": "#f0fff0",
"hotpink": "#ff69b4",
"indianred": "#cd5c5c",
"indigo": "#4b0082",
"ivory": "#fffff0",
"khaki": "#f0e68c",
"lavenderblush": "#fff0f5",
"lavender": "#e6e6fa",
"lawngreen": "#7cfc00",
"lemonchiffon": "#fffacd",
"lightblue": "#add8e6",
"lightcoral": "#f08080",
"lightcyan": "#e0ffff",
"lightgoldenrodyellow": "#fafad2",
"lightgray": "#d3d3d3",
"lightgreen": "#90ee90",
"lightgrey": "#d3d3d3",
"lightpink": "#ffb6c1",
"lightsalmon": "#ffa07a",
"lightseagreen": "#20b2aa",
"lightskyblue": "#87cefa",
"lightslategray": "#778899",
"lightslategrey": "#778899",
"lightsteelblue": "#b0c4de",
"lightyellow": "#ffffe0",
"lime": "#00ff00",
"limegreen": "#32cd32",
"linen": "#faf0e6",
"magenta": "#ff00ff",
"maroon": "#800000",
"mediumaquamarine": "#66cdaa",
"mediumblue": "#0000cd",
"mediumorchid": "#ba55d3",
"mediumpurple": "#9370db",
"mediumseagreen": "#3cb371",
"mediumslateblue": "#7b68ee",
"mediumspringgreen": "#00fa9a",
"mediumturquoise": "#48d1cc",
"mediumvioletred": "#c71585",
"midnightblue": "#191970",
"mintcream": "#f5fffa",
"mistyrose": "#ffe4e1",
"moccasin": "#ffe4b5",
"navajowhite": "#ffdead",
"navy": "#000080",
"oldlace": "#fdf5e6",
"olive": "#808000",
"olivedrab": "#6b8e23",
"orange": "#ffa500",
"orangered": "#ff4500",
"orchid": "#da70d6",
"palegoldenrod": "#eee8aa",
"palegreen": "#98fb98",
"paleturquoise": "#afeeee",
"palevioletred": "#db7093",
"papayawhip": "#ffefd5",
"peachpuff": "#ffdab9",
"peru": "#cd853f",
"pink": "#ffc0cb",
"plum": "#dda0dd",
"powderblue": "#b0e0e6",
"purple": "#800080",
"rebeccapurple": "#663399",
"red": "#ff0000",
"rosybrown": "#bc8f8f",
"royalblue": "#4169e1",
"saddlebrown": "#8b4513",
"salmon": "#fa8072",
"sandybrown": "#f4a460",
"seagreen": "#2e8b57",
"seashell": "#fff5ee",
"sienna": "#a0522d",
"silver": "#c0c0c0",
"skyblue": "#87ceeb",
"slateblue": "#6a5acd",
"slategray": "#708090",
"slategrey": "#708090",
"snow": "#fffafa",
"springgreen": "#00ff7f",
"steelblue": "#4682b4",
"tan": "#d2b48c",
"teal": "#008080",
"thistle": "#d8bfd8",
"tomato": "#ff6347",
"turquoise": "#40e0d0",
"violet": "#ee82ee",
"wheat": "#f5deb3",
"white": "#ffffff",
"whitesmoke": "#f5f5f5",
"yellow": "#ffff00",
"yellowgreen": "#9acd32"
}

View File

@@ -1,7 +1,7 @@
# Desc: A program for recoloring icon packs, themes and wallpapers. For NovaOS.
# Auth: Nicklas Vraa
from typing import List, Set, Tuple, Dict
from typing import List, Set, Tuple, Dict, Optional
from tqdm import tqdm
from colormath.color_objects import sRGBColor, LabColor
from colormath.color_conversions import convert_color
@@ -9,14 +9,6 @@ from colormath.color_diff import delta_e_cie2000
from PIL import Image, ImageDraw
import os, re, shutil, json
# Global constants -------------------------------------------------------------
# A dynamic dictionary to avoid multiple color conversions.
hex_to_lab_dict = {
"#ffffff": LabColor(9341.568974319263, -0.037058350415009045, -0.6906417562959177), # White.
"#000000": LabColor(0,0,0) # Black.
}
# Color conversion -------------------------------------------------------------
def hex_to_rgb(hex:str) -> Tuple[int,int,int]:
@@ -33,6 +25,16 @@ def rgb_to_hex(rgb:Tuple[int,int,int]) -> str:
r, g, b = rgb
return "#{:02x}{:02x}{:02x}".format(r, g, b)
def rgba_to_hex(rgba:Tuple[int,int,int,Optional[float]]) -> str:
r, g, b = rgba[:3]
hex = "#{:02X}{:02X}{:02X}".format(r, g, b)
if len(rgba) > 3:
if rgba[3] != 1.0:
hex += format(int(rgba[3] * 255), '02X')
return hex
def rgb_to_hsl(rgb:Tuple[int,int,int]) -> Tuple[float,float,float]:
r, g, b = rgb
r /= 255.0; g /= 255.0; b /= 255.0
@@ -83,6 +85,32 @@ def hue_to_rgb(p:float, q:float, t:float) -> float:
def norm_hsl(h:int, s:int, l:int) -> Tuple[float,float,float]:
return h/360, s/100, l/100
# CSS handling -----------------------------------------------------------------
def expand_css_rgba(match) -> None:
return rgba_to_hex((
int(match.group(1)), int(match.group(2)),
int(match.group(3)), float(match.group(4))
))
def css_to_hex(text:str):
text = re.sub(r"rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\)",
lambda match: expand_css_rgba(match), text)
for key in name_to_hex_dict:
text = re.sub(key + "([,;])", name_to_hex_dict[key] + "\\1", text)
return text
def expand_hex(hex:str) -> str:
"""Returns 6-digit version of any hexadecimal color code."""
hex = hex.lstrip('#')
if len(hex) == 3:
hex = ''.join([c * 2 for c in hex])
return '#' + hex.ljust(6, '0')
# Color comparision ------------------------------------------------------------
def generate_palette_dict(colors:List[str]) -> Dict[str,LabColor]:
@@ -94,7 +122,7 @@ def generate_palette_dict(colors:List[str]) -> Dict[str,LabColor]:
return palette_dict
def load_palette_file(path:str) -> Dict[str,LabColor]:
def load_json_file(path:str) -> Dict:
with open(path, 'r') as file:
palette = json.load(file)
return palette
@@ -106,18 +134,18 @@ def get_input_colors(resource) -> Tuple[List[str],bool,bool]:
return resource, False, True
elif type(resource) is str:
palette_file = load_palette_file(resource)
palette_file = load_json_file(resource)
return generate_palette_dict(palette_file["colors"]), palette_file["smooth"], False
else:
return generate_palette_dict(resource["colors"]), palette_file["smooth"], False
def get_svg_colors(svg:str) -> Set[str]:
"""Return a list of all unique colors within a given string
def get_file_colors(file:str) -> Set[str]:
"""Return a set of all unique colors within a given string
representing an svg-file."""
colors = set()
matches = re.findall(r"#[A-Fa-f0-9]{6}", svg)
matches = re.findall(r"#[A-Fa-f0-9]{6}", file)
for match in matches:
colors.add(match)
@@ -205,20 +233,28 @@ def copy_file_structure(src_path:str, dest_path:str) -> None:
relative_target = relative_target.replace(src_path, dest_path, 1)
os.symlink(relative_target, file_path)
def rename_pack(path:str, name:str) -> None:
def rename_pack(src_path, dest_path:str, name:str) -> None:
"""If an index.theme file exists within the given folder, apply
appropiate naming."""
path = os.path.join(path, name)
index_path = os.path.join(path, "index.theme")
print("rename called")
index_path = os.path.join(dest_path, "index.theme")
print(index_path)
if os.path.exists(index_path):
print("index.theme found")
with open(index_path, 'r') as file:
content = file.read()
contents = re.sub(r"Name=.*", "Name=" + name, content, count=1)
text = file.read()
text = re.sub(r"(Name=).*", "\\1" + name, text, count=1)
text = re.sub(r"(GtkTheme=).*", "\\1" + name, text, count=1)
text = re.sub(r"(MetacityTheme=).*", "\\1" + name, text, count=1)
text = re.sub(r"(IconTheme=).*", "\\1" + name, text, count=1)
text = re.sub(r"(Comment=).*", "\\1" + "A variant of " + os.path.basename(src_path) + " created by nicklasvraa/color-manager", text, count=1)
with open(index_path, 'w') as file:
file.write(contents)
file.write(text)
def copy_pack(src_path:str, dest_path:str, name:str) -> str:
"""Copy pack and return the resulting copy's directory path."""
@@ -227,13 +263,13 @@ def copy_pack(src_path:str, dest_path:str, name:str) -> str:
dest_path = os.path.join(expand_path(dest_path), name)
copy_file_structure(src_path, dest_path)
rename_pack(dest_path, name)
rename_pack(src_path, dest_path, name)
return dest_path
# Recoloring -------------------------------------------------------------------
# SVG/CSS recoloring -----------------------------------------------------------
def monochrome_svg(svg:str, colors:Set[str], hsl:Tuple[float,float,float]) -> str:
def monochrome_vec(svg:str, colors:Set[str], hsl:Tuple[float,float,float]) -> str:
"""Replace every instance of color within the given list with their
monochrome equivalent in the given string representing an svg-file,
determined by the given hue, saturation and lightness offset."""
@@ -257,8 +293,8 @@ def monochrome_svg(svg:str, colors:Set[str], hsl:Tuple[float,float,float]) -> st
return svg
def multichrome_svg(svg:str, colors:Set[str], new_colors:Dict[str,LabColor]) -> str:
"""Replace colors in a given svg with the closest match within a given
def multichrome_vec(svg:str, colors:Set[str], new_colors:Dict[str,LabColor]) -> str:
"""Replace colors in a given svg/css with the closest match within a given
color palette."""
for color in colors:
@@ -267,24 +303,25 @@ def multichrome_svg(svg:str, colors:Set[str], new_colors:Dict[str,LabColor]) ->
return svg
# PNG/JPG recoloring -----------------------------------------------------------
def monochrome_img(img:Image, hsl:Tuple[float,float,float]) -> Image:
"""Replace every instance of color within the given list with their
monochrome equivalent in the given image, determined by the given hue, saturation and lightness offset."""
mode = img.mode
h, s, l_offset = hsl
if s == 0:
if img.mode == "RGBA":
img = img.convert("LA")
else:
img = img.convert("L")
if mode == "RGBA": img = img.convert("LA")
else: img = img.convert("L")
else:
width, height = img.size
l_offset = (l_offset - 0.5) * 2 # Remapping.
for x in range(width):
for y in range(height):
if img.mode == "RGBA":
if mode == "RGBA":
r, g, b, a = img.getpixel((x, y))
else:
r, g, b = img.getpixel((x, y))
@@ -293,7 +330,7 @@ def monochrome_img(img:Image, hsl:Tuple[float,float,float]) -> Image:
l = max(-1, min(l+l_offset, 1))
new_color = hsl_to_rgb((h, s, l))
if img.mode == "RGBA":
if mode == "RGBA":
img.putpixel((x,y), new_color + (a,))
else:
img.putpixel((x,y), new_color)
@@ -330,27 +367,48 @@ def recolor(src_path:str, dest_path:str, name:str, replacement) -> None:
new_colors, smooth, is_mono = get_input_colors(replacement)
dest_path = copy_pack(src_path, dest_path, name)
svg_paths = get_paths(dest_path, [".svg"])
img_paths = get_paths(dest_path, [".png", ".jpg", ".jpeg"])
png_paths = get_paths(dest_path, [".png"])
jpg_paths = get_paths(dest_path, [".jpg", ".jpeg"])
css_paths = get_paths(dest_path, [".css"])
for path in tqdm(svg_paths, desc="Recoloring svgs", unit="file"):
with open(path, 'r') as file:
svg = file.read()
for path in tqdm(svg_paths, desc="svg", unit="file"):
with open(path, 'r') as file: x = file.read()
colors = get_svg_colors(svg)
colors = get_file_colors(x)
if is_mono: svg = monochrome_svg(svg, colors, new_colors)
else: svg = multichrome_svg(svg, colors, new_colors)
if is_mono: x = monochrome_vec(x, colors, new_colors)
else: x = multichrome_vec(x, colors, new_colors)
with open(path, 'w') as file:
file.write(svg)
with open(path, 'w') as file: file.write(x)
for path in tqdm(img_paths, desc="Recoloring imgs", unit="file"):
img = Image.open(path)
for path in tqdm(png_paths, desc="png", unit="file"):
x = Image.open(path)
x = x.convert("RGBA")
if is_mono: img = monochrome_img(img, new_colors)
else: img = multichrome_img(img, new_colors, smooth)
if is_mono: x = monochrome_img(x, new_colors)
else: x = multichrome_img(x, new_colors, smooth)
img.save(path)
x.save(path)
for path in tqdm(jpg_paths, desc="jpg", unit="file"):
x = Image.open(path)
x = x.convert("RGB")
if is_mono: x = monochrome_img(x, new_colors)
else: x = multichrome_img(x, new_colors, smooth)
x.save(path)
for path in tqdm(css_paths, desc="css", unit="file"):
with open(path, 'r') as file: x = file.read()
x = css_to_hex(x)
colors = get_file_colors(x)
if is_mono: x = monochrome_vec(x, colors, new_colors)
else: x = multichrome_vec(x, colors, new_colors)
with open(path, 'w') as file: file.write(x)
def extract_colors(img_path:str, num_colors:int=8, save_path:str=None, pixels:int=50, cols:int=10) -> List[str]:
"""Returns and optionally saves the color palette of the given image, finding the specified number of colors."""
@@ -361,7 +419,7 @@ def extract_colors(img_path:str, num_colors:int=8, save_path:str=None, pixels:in
with open(img_path, 'r') as file:
svg = file.read()
colors = list(get_svg_colors(svg))
colors = list(get_file_colors(svg))
num_colors = len(colors)
else:
@@ -432,3 +490,16 @@ def add_backdrop(src_path:str, dest_path:str, name:str, color:str="#000000", pad
with open(path, 'w') as file:
file.write(svg)
# Global constants -------------------------------------------------------------
this_path = os.path.dirname(os.path.abspath(__file__))
# A dynamic dictionary to avoid multiple color conversions.
hex_to_lab_dict = {
"#ffffff": LabColor(9341.568974319263, -0.037058350415009045, -0.6906417562959177), # White.
"#000000": LabColor(0,0,0) # Black.
}
# A static dictionary of named colors from the css standard.
name_to_hex_dict = load_json_file(os.path.join(this_path, "named_colors.json"))

View File

@@ -50,9 +50,9 @@ Color Manager is a program for recoloring and manipulating existing icon packs,
Currently, three operations are supported:
| Type | Result | Speed | Supports |
| ---- | ------ | ----- | -------- |
| Monochrome recoloring | A monochromatic variant, colored by appropriate shades of the provided base color. | ~5000 svgs/sec | svg, png, jpg |
| Multichrome recoloring | A multichromatic variant, where all colors are replaced by their nearest perceived equivalent that adheres to the provided color palette. | ~100 svgs/sec | svg, png, jpg |
| Extract colors | Returns and optionally saves the color palette of an image, in specified detail. | ~100 colors/sec | svg, png, jpg |
| Monochrome recoloring | A monochromatic variant, colored by appropriate shades of the provided base color. | ~5000 svgs/sec | svg, png, jpg, css |
| Multichrome recoloring | A multichromatic variant, where all colors are replaced by their nearest perceived equivalent that adheres to the provided color palette. | ~100 svgs/sec | svg, png, jpg, css |
| Extract colors | Returns and optionally saves the color palette of an image, in specified detail. | ~100 colors/sec | svg, png, jpg, css |
| Add backdrop | Add a rectangular or elliptical background (and anything in between) to all svg icons. | ~5000 svgs/sec | svg |
Speeds were recorded with an Intel i7-4770K CPU. Any asset can serve as the base for any color palette or base color. Svg recolorings will always be perfect, but png/jpgs may require experimentation.

BIN
resources/palette.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
@@ -12,9 +12,20 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 14,
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"svgs: 100%|██████████| 6/6 [00:00<00:00, 2730.07file/s]\n",
"pngs: 100%|██████████| 1/1 [00:05<00:00, 5.13s/file]\n",
"jpgs: 100%|██████████| 1/1 [00:04<00:00, 4.37s/file]\n",
"css': 0file [00:00, ?file/s]\n"
]
}
],
"source": [
"from color_manager import utils\n",
"\n",
@@ -24,7 +35,7 @@
"hsl = (0.6, 0.54, 0.5) # = rc.normalize_hsl(180, 50, 50)\n",
"palette = \"palettes/nord.json\"\n",
"\n",
"utils.recolor(src, dest, name, palette) # Either hsl or palette"
"utils.recolor(src, dest, name, hsl) # Either hsl or palette"
]
},
{
@@ -58,29 +69,64 @@
},
{
"cell_type": "code",
"execution_count": 93,
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Changing svgs : 100%|██████████| 6/6 [00:00<00:00, 4617.58file/s]\n"
]
}
],
"outputs": [],
"source": [
"from color_manager import utils\n",
"\n",
"src = \"test_pack\"\n",
"name = \"my_pack\"\n",
"dest = \"~/Downloads\"\n",
"color = \"#551144\" # Optional\n",
"padding = 4 # Optional - Between backdrop and edge.\n",
"rounding = 1 # Optional - Between 0 and 1, i.e. rectangle and ellipse.\n",
"color = \"#000000\" # Optional - Defaults to black.\n",
"padding = 0 # Optional - Between backdrop and edge.\n",
"rounding = 0.5 # Optional - Between 0 and 1, i.e. rectangle and ellipse.\n",
"\n",
"utils.add_backdrop(src, dest, name, color, padding, rounding)"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"rename called\n",
"/home/nv/Downloads/my_theme/index.theme\n",
"index.theme found\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"svg: 0file [00:00, ?file/s]\n",
"png: 0%| | 0/1 [00:00<?, ?file/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"png: 100%|██████████| 1/1 [00:00<00:00, 33.59file/s]\n",
"jpg: 0file [00:00, ?file/s]\n",
"css: 100%|██████████| 1/1 [00:00<00:00, 18.40file/s]\n"
]
}
],
"source": [
"from color_manager import utils\n",
"src = \"test_theme\"\n",
"name = \"my_theme\"\n",
"dest = \"~/Downloads\"\n",
"hsl = (0.6, 0.54, 0.5) # = rc.normalize_hsl(180, 50, 50)\n",
"palette = \"palettes/nord.json\"\n",
"\n",
"colors = utils.recolor(src, dest, name, hsl) # Either hsl or palette"
]
}
],
"metadata": {

View File

@@ -0,0 +1 @@
gtk.css

6257
test_theme/gtk-3.0/gtk.css Executable file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

11
test_theme/index.theme Executable file
View File

@@ -0,0 +1,11 @@
[Desktop Entry]
Type=X-GNOME-Metatheme
Name=NovaOS-nord
Comment=Dark nord theme for Nova OS
Encoding=UTF-8
[X-GNOME-Metatheme]
GtkTheme=NovaOS-nord
MetacityTheme=NovaOS-nord
IconTheme=NovaOS-nord
ButtonLayout=:minimize,maximize,close