mirror of
https://github.com/NicklasVraa/Color-manager.git
synced 2026-03-18 21:50:34 +01:00
Bug fixes
This commit is contained in:
@@ -9,7 +9,8 @@ from colormath.color_diff import delta_e_cie2000
|
||||
from PIL import Image, ImageDraw
|
||||
import os, re, shutil, json, subprocess
|
||||
|
||||
# Utility ----------------------------------------------------------------------
|
||||
# Basic utility ----------------------------------------------------------------
|
||||
|
||||
def expand_path(path:str) -> str:
|
||||
""" Returns the absolute version of the given path, and expands unix notation like tilde for home folder. """
|
||||
return os.path.abspath(os.path.expanduser(path))
|
||||
@@ -24,6 +25,31 @@ def load_json_file(path:str) -> Dict:
|
||||
obj = json.load(file)
|
||||
return obj
|
||||
|
||||
def check_path(path:str) -> None:
|
||||
if not os.path.exists(expand_path(path)):
|
||||
raise Exception("Invalid path: " + path)
|
||||
|
||||
def svg_to_png(src_path:str, dest_path:str, width:int = 300) -> None:
|
||||
""" Generate pngs at given destination path from a given source folder with a given width. """
|
||||
|
||||
src_path = expand_path(src_path)
|
||||
dest_path = expand_path(dest_path)
|
||||
|
||||
try:
|
||||
subprocess.run(['inkscape', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError("Inkscape is not installed.")
|
||||
|
||||
os.makedirs(dest_path, exist_ok=True)
|
||||
svgs = [file for file in os.listdir(src_path) if file.endswith('.svg')]
|
||||
|
||||
for svg in svgs:
|
||||
svg_path = os.path.join(src_path, svg)
|
||||
png = os.path.splitext(svg)[0] + '.png'
|
||||
png_path = os.path.join(dest_path, png)
|
||||
command = ['inkscape', svg_path, '-o', png_path, '-w', str(width)]
|
||||
subprocess.run(command)
|
||||
|
||||
# Color conversion -------------------------------------------------------------
|
||||
|
||||
def hex_to_rgb(hex:str) -> Tuple[int,int,int]:
|
||||
@@ -110,29 +136,6 @@ def norm_hsl(h:int, s:int, l:int) -> Tuple[float,float,float]:
|
||||
""" Normalize hsl color values. """
|
||||
return h/360, s/100, l/100
|
||||
|
||||
# File conversion --------------------------------------------------------------
|
||||
|
||||
def svg_to_png(src_path:str, dest_path:str, width:int = 300) -> None:
|
||||
""" Generate pngs at given destination path from a given source folder with a given width. """
|
||||
|
||||
src_path = expand_path(src_path)
|
||||
dest_path = expand_path(dest_path)
|
||||
|
||||
try:
|
||||
subprocess.run(['inkscape', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError("Inkscape is not installed.")
|
||||
|
||||
os.makedirs(dest_path, exist_ok=True)
|
||||
svgs = [file for file in os.listdir(src_path) if file.endswith('.svg')]
|
||||
|
||||
for svg in svgs:
|
||||
svg_path = os.path.join(src_path, svg)
|
||||
png = os.path.splitext(svg)[0] + '.png'
|
||||
png_path = os.path.join(dest_path, png)
|
||||
command = ['inkscape', svg_path, '-o', png_path, '-w', str(width)]
|
||||
subprocess.run(command)
|
||||
|
||||
# Preprocessing ----------------------------------------------------------------
|
||||
|
||||
def expand_css_rgba(match) -> str:
|
||||
@@ -177,15 +180,15 @@ def generate_palette_dict(colors:List[str]) -> Dict[str,LabColor]:
|
||||
def get_input_colors(resource) -> Tuple[List[str],bool,bool]:
|
||||
""" Returns an HSL tuple, or a list of colors, depending on the input, as well as a boolean indicating which one, as well as if the palettes specifies smoothing pngs/jpgs. """
|
||||
|
||||
# If resource is an hsl color.
|
||||
if isinstance(resource, tuple) and len(resource) == 3:
|
||||
return resource, False, True
|
||||
|
||||
elif type(resource) is str:
|
||||
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
|
||||
# If resource is a path to a palette.
|
||||
if type(resource) is str: resource = load_json_file(resource)
|
||||
|
||||
return generate_palette_dict(resource["colors"]), resource["smooth"], False
|
||||
|
||||
def get_file_colors(text:str) -> Set[str]:
|
||||
""" Return a set of all unique colors within a given string representing an svg-file. """
|
||||
@@ -302,9 +305,9 @@ def copy_pack(src_path:str, dest_path:str, name:str) -> str:
|
||||
|
||||
return dest_path
|
||||
|
||||
# SVG/CSS recoloring -----------------------------------------------------------
|
||||
# Vector-based recoloring ------------------------------------------------------
|
||||
|
||||
def monochrome_vec(svg:str, colors:Set[str], hsl:Tuple[float,float,float]) -> str:
|
||||
def apply_monotones_to_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. """
|
||||
|
||||
h, s, l_offset = hsl
|
||||
@@ -326,7 +329,7 @@ def monochrome_vec(svg:str, colors:Set[str], hsl:Tuple[float,float,float]) -> st
|
||||
|
||||
return svg
|
||||
|
||||
def multichrome_vec(svg:str, colors:Set[str], new_colors:Dict[str,LabColor]) -> str:
|
||||
def apply_palette_to_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:
|
||||
@@ -335,9 +338,9 @@ def multichrome_vec(svg:str, colors:Set[str], new_colors:Dict[str,LabColor]) ->
|
||||
|
||||
return svg
|
||||
|
||||
# PNG/JPG recoloring -----------------------------------------------------------
|
||||
# Pixel-based recoloring -------------------------------------------------------
|
||||
|
||||
def monochrome_img(img:Image, hsl:Tuple[float,float,float]) -> Image:
|
||||
def apply_monotones_to_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
|
||||
@@ -368,7 +371,7 @@ def monochrome_img(img:Image, hsl:Tuple[float,float,float]) -> Image:
|
||||
|
||||
return img
|
||||
|
||||
def multichrome_img(img:Image, new_colors:Dict[str,LabColor], smooth:bool) -> Image:
|
||||
def apply_palette_to_img(img:Image, new_colors:Dict[str,LabColor], smooth:bool) -> Image:
|
||||
""" Replace colors in a given image with the closest match within a given color palette. """
|
||||
|
||||
if smooth: img = img.convert("P", palette=Image.ADAPTIVE, colors=256)
|
||||
@@ -391,76 +394,83 @@ def multichrome_img(img:Image, new_colors:Dict[str,LabColor], smooth:bool) -> Im
|
||||
# User interface functions -----------------------------------------------------
|
||||
|
||||
def recolor(src_path:str, dest_path:str, name:str, replacement) -> None:
|
||||
""" Recursively copies and converts a source folder into a destination, given a either a color or a palette. """
|
||||
""" Recursively copies and converts a source folder into a destination, given either an hsl color, a palette, or a color mapping. """
|
||||
|
||||
check_path(src_path)
|
||||
check_path(dest_path)
|
||||
|
||||
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", ".xml"])
|
||||
png_paths = get_paths(dest_path, [".png"])
|
||||
jpg_paths = get_paths(dest_path, [".jpg", ".jpeg"])
|
||||
css_paths = get_paths(dest_path, [".css", "rc"])
|
||||
|
||||
for path in tqdm(svg_paths, desc="svg", disable=is_empty(svg_paths)):
|
||||
# Recolor vector graphics.
|
||||
paths = get_paths(dest_path, [".svg", ".xml"])
|
||||
for path in tqdm(paths, desc="svg", disable=is_empty(paths)):
|
||||
with open(path, 'r') as file: x = file.read()
|
||||
|
||||
x = expand_all_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)
|
||||
if is_mono: x = apply_monotones_to_vec(x, colors, new_colors)
|
||||
else: x = apply_palette_to_vec(x, colors, new_colors)
|
||||
|
||||
with open(path, 'w') as file: file.write(x)
|
||||
|
||||
for path in tqdm(png_paths, desc="png", disable=is_empty(png_paths)):
|
||||
# Recolor stylesheets.
|
||||
paths = get_paths(dest_path, [".css", "rc"])
|
||||
for path in tqdm(paths, desc="css", disable=is_empty(paths)):
|
||||
with open(path, 'r') as file: x = file.read()
|
||||
|
||||
x = css_to_hex(x)
|
||||
x = expand_all_hex(x)
|
||||
colors = get_file_colors(x)
|
||||
|
||||
if is_mono: x = apply_monotones_to_vec(x, colors, new_colors)
|
||||
else: x = apply_palette_to_vec(x, colors, new_colors)
|
||||
|
||||
with open(path, 'w') as file: file.write(x)
|
||||
|
||||
# Recolor pngs.
|
||||
paths = get_paths(dest_path, [".png"])
|
||||
for path in tqdm(paths, desc="png", disable=is_empty(paths)):
|
||||
x = Image.open(path)
|
||||
x = x.convert("RGBA")
|
||||
a = x.split()[3] # Save original alpha channel.
|
||||
|
||||
if is_mono: x = monochrome_img(x, new_colors)
|
||||
else: x = multichrome_img(x, new_colors, smooth)
|
||||
if is_mono: x = apply_monotones_to_img(x, new_colors)
|
||||
else: x = apply_palette_to_img(x, new_colors, smooth)
|
||||
|
||||
x = x.convert("RGBA")
|
||||
r,g,b,_ = x.split()
|
||||
x = Image.merge("RGBA",(r,g,b,a)) # Restore original alpha channel.
|
||||
x.save(path)
|
||||
|
||||
for path in tqdm(jpg_paths, desc="jpg", disable=is_empty(jpg_paths)):
|
||||
# Recolor jpgs.
|
||||
paths = get_paths(dest_path, [".jpg", ".jpeg"])
|
||||
for path in tqdm(paths, desc="jpg", disable=is_empty(paths)):
|
||||
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)
|
||||
if is_mono: x = apply_monotones_to_img(x, new_colors)
|
||||
else: x = apply_palette_to_img(x, new_colors, smooth)
|
||||
|
||||
x = x.convert("RGB")
|
||||
x.save(path)
|
||||
|
||||
for path in tqdm(css_paths, desc="css", disable=is_empty(css_paths)):
|
||||
with open(path, 'r') as file: x = file.read()
|
||||
def extract_colors(src_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, as its own image. Optionally specify the number of unique colors you want to be found. """
|
||||
|
||||
x = css_to_hex(x)
|
||||
x = expand_all_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. """
|
||||
|
||||
_, ext = os.path.splitext(img_path)
|
||||
check_path(src_path)
|
||||
_, ext = os.path.splitext(src_path)
|
||||
|
||||
if ext == ".svg":
|
||||
with open(img_path, 'r') as file:
|
||||
with open(src_path, 'r') as file:
|
||||
svg = file.read()
|
||||
|
||||
colors = list(get_file_colors(svg))
|
||||
num_colors = len(colors)
|
||||
|
||||
else:
|
||||
img = Image.open(img_path)
|
||||
img = Image.open(src_path)
|
||||
|
||||
colors = img.convert('P', palette=Image.ADAPTIVE, colors=num_colors)
|
||||
colors = colors.getpalette()[0:num_colors*3]
|
||||
@@ -468,6 +478,8 @@ def extract_colors(img_path:str, num_colors:int=8, save_path:str=None, pixels:in
|
||||
colors = ['#{:02X}{:02X}{:02X}'.format(colors[i], colors[i+1], colors[i+2]) for i in range(0, len(colors), 3)]
|
||||
|
||||
if save_path != None:
|
||||
check_path(save_path)
|
||||
|
||||
if num_colors < cols: cols = num_colors
|
||||
|
||||
rows = -(-len(colors) // cols)
|
||||
@@ -486,10 +498,11 @@ def extract_colors(img_path:str, num_colors:int=8, save_path:str=None, pixels:in
|
||||
|
||||
return colors
|
||||
|
||||
def clean_svg(input_path:str, output_path:str=None) -> str:
|
||||
def clean_svg(src_path:str, dest_path:str=None) -> str:
|
||||
""" Removes needless metadata from svgs and optionally saves as copy, if output path is specified. """
|
||||
|
||||
with open(input_path, 'r') as f:
|
||||
check_path(src_path)
|
||||
with open(src_path, 'r') as f:
|
||||
svg = f.read()
|
||||
|
||||
patterns = [
|
||||
@@ -501,15 +514,17 @@ def clean_svg(input_path:str, output_path:str=None) -> str:
|
||||
for pattern in patterns:
|
||||
svg = re.sub(pattern, '', svg)
|
||||
|
||||
if output_path is None:
|
||||
output_path = input_path
|
||||
if dest_path is None: dest_path = src_path
|
||||
else: check_path(dest_path)
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
with open(dest_path, 'w') as f:
|
||||
f.write(svg)
|
||||
|
||||
def add_backdrop(src_path:str, dest_path:str, name:str, color:str="#000000", padding=0, rounding=0):
|
||||
""" Add a customizable backdrop to all svg-based icons. """
|
||||
""" Add a customizable backdrop to all svg-based icons. Optionally specify the backdrop color, the padding to the edge of the graphic, and the corner rounding factor. """
|
||||
|
||||
check_path(src_path)
|
||||
check_path(dest_path)
|
||||
dest_path = copy_pack(src_path, dest_path, name)
|
||||
svg_paths = get_paths(dest_path, [".svg"])
|
||||
|
||||
|
||||
15
readme.md
15
readme.md
@@ -45,19 +45,20 @@ Color Manager is a program for recoloring and manipulating existing icon packs,
|
||||
- [ ] Optional automatic palette extending.
|
||||
- [ ] Basic framework for manipulating GTK, Cinnamon and Metacity themes.
|
||||
- [ ] Intelligent color inversion.
|
||||
- [ ] Update GUI to support new features.
|
||||
- [ ] GNU/Linux binary (deb, flatpak, appimage).
|
||||
|
||||
|
||||
## Features <a name="features"></a>
|
||||
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, 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 |
|
||||
| Type | Result | Speed | Support |
|
||||
| :--: | ------ | ----- | ------- |
|
||||
| **Monochrome recoloring** | A monochromatic variant, colored by appropriate shades of the provided base color. | ~5000svg/s<br> ~500png/s | svg, xml, css, png, jpg |
|
||||
| **Palette recoloring** | A multichromatic variant, where all colors are replaced by their nearest perceived equivalent that adheres to the provided color palette. | ~100svg/s<br> ~3png/s | svg, xml, css, png, jpg |
|
||||
| **Extract colors** | Returns and optionally saves the color palette of an image, in specified detail. | ~10colors/s | svg, xml, css, png, jpg |
|
||||
| **Add backdrop** | Add a rectangular or elliptical background (and anything in between) to all svg icons. | ~5000svg/s | 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.
|
||||
Speeds were recorded with an Intel i7-4770K CPU and is strongly dependent on file sizes. Any asset can serve as the base for any color palette or base color. Svg/css recolorings will always be perfect, but png/jpgs may require experimentation.
|
||||
|
||||
**Tip**: To increase the quality, i.e. decrease the perceived noise of multichromatic recolorings of pngs/jpgs, either...
|
||||
- Increase the number of colors in the palette you provide to the program, e.g. populate it with slight variations of the existing colors
|
||||
|
||||
49
test.ipynb
49
test.ipynb
@@ -2,9 +2,18 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"The autoreload extension is already loaded. To reload it, use:\n",
|
||||
" %reload_ext autoreload\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%load_ext autoreload\n",
|
||||
"%autoreload 2\n",
|
||||
@@ -14,42 +23,32 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/home/nv/Downloads/my_pack/index.theme\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"svg: 100%|██████████| 43/43 [00:00<00:00, 250.05it/s]\n",
|
||||
"png: 100%|██████████| 439/439 [00:01<00:00, 360.71it/s]\n",
|
||||
"css: 100%|██████████| 12/12 [00:00<00:00, 84.68it/s]\n"
|
||||
"ename": "Exception",
|
||||
"evalue": "Invalid path: ~/Downloads",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
||||
"\u001b[0;31mException\u001b[0m Traceback (most recent call last)",
|
||||
"Cell \u001b[0;32mIn[10], line 7\u001b[0m\n\u001b[1;32m 4\u001b[0m hsl \u001b[39m=\u001b[39m (\u001b[39m0.6\u001b[39m, \u001b[39m0.54\u001b[39m, \u001b[39m0.5\u001b[39m) \u001b[39m# = rc.normalize_hsl(180, 50, 50)\u001b[39;00m\n\u001b[1;32m 5\u001b[0m palette \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mpalettes/nord.json\u001b[39m\u001b[39m\"\u001b[39m\n\u001b[0;32m----> 7\u001b[0m utils\u001b[39m.\u001b[39;49mrecolor(src, dest, name, hsl) \u001b[39m# Either hsl or palette\u001b[39;00m\n",
|
||||
"File \u001b[0;32m~/Projects/Color_manager/color_manager/utils.py:402\u001b[0m, in \u001b[0;36mrecolor\u001b[0;34m(src_path, dest_path, name, replacement)\u001b[0m\n\u001b[1;32m 399\u001b[0m \u001b[39m\u001b[39m\u001b[39m\"\"\" Recursively copies and converts a source folder into a destination, given either an hsl color, a palette, or a color mapping. \"\"\"\u001b[39;00m\n\u001b[1;32m 401\u001b[0m check_path(src_path)\n\u001b[0;32m--> 402\u001b[0m check_path(dest_path)\n\u001b[1;32m 404\u001b[0m new_colors, smooth, is_mono \u001b[39m=\u001b[39m get_input_colors(replacement)\n\u001b[1;32m 405\u001b[0m dest_path \u001b[39m=\u001b[39m copy_pack(src_path, dest_path, name)\n",
|
||||
"File \u001b[0;32m~/Projects/Color_manager/color_manager/utils.py:30\u001b[0m, in \u001b[0;36mcheck_path\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m 28\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mcheck_path\u001b[39m(path:\u001b[39mstr\u001b[39m) \u001b[39m-\u001b[39m\u001b[39m>\u001b[39m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m 29\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mnot\u001b[39;00m os\u001b[39m.\u001b[39mpath\u001b[39m.\u001b[39mexists(path):\n\u001b[0;32m---> 30\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mException\u001b[39;00m(\u001b[39m\"\u001b[39m\u001b[39mInvalid path: \u001b[39m\u001b[39m\"\u001b[39m \u001b[39m+\u001b[39m path)\n",
|
||||
"\u001b[0;31mException\u001b[0m: Invalid path: ~/Downloads"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"src = \"test_theme\" # = \"test_theme\"\n",
|
||||
"name = \"my_pack\" # = \"my_theme\"\n",
|
||||
"src = \"test_pack\" # = \"test_theme\"\n",
|
||||
"name = \"my_pack3\" # = \"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",
|
||||
"utils.recolor(src, dest, name, hsl) # Either hsl or palette"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Priority: Preserve transparency in PNGs after multichrome recoloring."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
Reference in New Issue
Block a user