diff --git a/.gitignore b/.gitignore index b63eb088..f62f15eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ resources/demo.mp4 -test_pack/theme/ +test/theme/ *.egg-info/ __pycache__/ diff --git a/color_manager/utils.py b/color_manager/utils.py index 88bbcfd2..e1cf7092 100644 --- a/color_manager/utils.py +++ b/color_manager/utils.py @@ -177,18 +177,23 @@ def generate_palette_dict(colors:List[str]) -> Dict[str,LabColor]: return palette_dict -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. """ +def get_input_colors(resource): + """ Returns an HSL tuple, or a palette of colors, or a color mapping, depending on the input, as well as a string indicating which one, and if smoothing should be applied to pngs/jpgs. """ # If resource is an hsl color. if isinstance(resource, tuple) and len(resource) == 3: - return resource, False, True + return resource, False, "color" else: - # If resource is a path to a palette. - if type(resource) is str: resource = load_json_file(resource) + # If resource is a path to a resource, first unpack. + if type(resource) is str: + resource = load_json_file(resource) - return generate_palette_dict(resource["colors"]), resource["smooth"], False + if resource["type"] == "palette": + return generate_palette_dict(resource["colors"]), resource["smooth"], "palette" + + elif resource["type"] == "mapping": + return resource["map"], resource["smooth"], "mapping" def get_file_colors(text:str) -> Set[str]: """ Return a set of all unique colors within a given string representing an svg-file. """ @@ -307,7 +312,7 @@ def copy_pack(src_path:str, dest_path:str, name:str) -> str: # Vector-based recoloring ------------------------------------------------------ -def apply_monotones_to_vec(svg:str, colors:Set[str], hsl:Tuple[float,float,float]) -> str: +def apply_monotones_to_vec(text: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 @@ -315,7 +320,7 @@ def apply_monotones_to_vec(svg:str, colors:Set[str], hsl:Tuple[float,float,float if s == 0: for color in colors: graytone = hex_to_gray(color) - svg = re.sub(color, graytone, svg) + text = re.sub(color, graytone, text) else: l_offset = (l_offset - 0.5) * 2 # Remapping. @@ -325,18 +330,27 @@ def apply_monotones_to_vec(svg:str, colors:Set[str], hsl:Tuple[float,float,float l = (0.21*r + 0.72*g + 0.07*b)/255 l = max(-1, min(l+l_offset, 1)) monochrome = rgb_to_hex(hsl_to_rgb((h, s, l))) - svg = re.sub(color, monochrome, svg) + text = re.sub(color, monochrome, text) - return svg + return text -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. """ +def apply_palette_to_vec(text:str, colors:Set[str], new_colors:Dict[str,LabColor]) -> str: + """ Replace hexadecimal color codes in a given svg/xml/css string with their closest matches within the given color palette. """ for color in colors: new_color = closest_match(color, new_colors) - svg = re.sub(color, new_color, svg) + text = re.sub(color, new_color, text) - return svg + return text + +def apply_mapping_to_vec(text:str, colors:Set[str], map:Dict[str,str]) -> str: + """ Replace hexadecimal color codes in a given svg/xml/css string according to a given color mapping. """ + + for color in colors: + if color in map: + text = re.sub(color, map[color], text) + + return text # Pixel-based recoloring ------------------------------------------------------- @@ -382,8 +396,8 @@ def apply_palette_to_img(img:Image, new_colors:Dict[str,LabColor], smooth:bool) rgb_palette = [(palette[i], palette[i+1], palette[i+2]) for i in range(0, len(palette), 3)] hex_palette = ["#%02x%02x%02x" % rgb for rgb in rgb_palette] - new_palette = [] + new_palette = [] for color in hex_palette: new_color = hex_to_rgb(closest_match(color, new_colors)) new_palette.extend(new_color) @@ -391,6 +405,30 @@ def apply_palette_to_img(img:Image, new_colors:Dict[str,LabColor], smooth:bool) img.putpalette(new_palette) return img +def apply_mapping_to_img(img:Image, map:Dict[str,str], smooth:bool) -> Image: + """ Replace colors in a given image according to a given mapping. """ + + #raise Exception("Function not yet implemented.") + + if smooth: img = img.convert("P", palette=Image.ADAPTIVE, colors=256) + else: img = img.convert("P") + + palette = img.getpalette() + + rgb_palette = [(palette[i], palette[i+1], palette[i+2]) for i in range(0, len(palette), 3)] + + hex_palette = ["#%02x%02x%02x" % rgb for rgb in rgb_palette] + + new_palette = [] + for color in hex_palette: + if color in map: + new_palette.extend(hex_to_rgb(map[color])) + else: + new_palette.extend(hex_to_rgb(color)) + + img.putpalette(new_palette) + return img + # User interface functions ----------------------------------------------------- def recolor(src_path:str, dest_path:str, name:str, replacement) -> None: @@ -399,7 +437,7 @@ def recolor(src_path:str, dest_path:str, name:str, replacement) -> None: check_path(src_path) check_path(dest_path) - new_colors, smooth, is_mono = get_input_colors(replacement) + new_colors, smooth, op = get_input_colors(replacement) dest_path = copy_pack(src_path, dest_path, name) # Recolor vector graphics. @@ -410,8 +448,12 @@ def recolor(src_path:str, dest_path:str, name:str, replacement) -> None: 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) + if op == "color": + x = apply_monotones_to_vec(x, colors, new_colors) + elif op == "palette": + x = apply_palette_to_vec(x, colors, new_colors) + elif op == "mapping": + x = apply_mapping_to_vec(x, colors, new_colors) with open(path, 'w') as file: file.write(x) @@ -424,8 +466,12 @@ def recolor(src_path:str, dest_path:str, name:str, replacement) -> None: 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) + if op == "color": + x = apply_monotones_to_vec(x, colors, new_colors) + elif op == "palette": + x = apply_palette_to_vec(x, colors, new_colors) + elif op == "mapping": + x = apply_mapping_to_vec(x, colors, new_colors) with open(path, 'w') as file: file.write(x) @@ -436,8 +482,12 @@ def recolor(src_path:str, dest_path:str, name:str, replacement) -> None: x = x.convert("RGBA") a = x.split()[3] # Save original alpha channel. - if is_mono: x = apply_monotones_to_img(x, new_colors) - else: x = apply_palette_to_img(x, new_colors, smooth) + if op == "color": + x = apply_monotones_to_img(x, new_colors) + elif op == "palette": + x = apply_palette_to_img(x, new_colors, smooth) + elif op == "mapping": + x = apply_mapping_to_img(x, new_colors, smooth) x = x.convert("RGBA") r,g,b,_ = x.split() @@ -450,8 +500,12 @@ def recolor(src_path:str, dest_path:str, name:str, replacement) -> None: x = Image.open(path) x = x.convert("RGB") - if is_mono: x = apply_monotones_to_img(x, new_colors) - else: x = apply_palette_to_img(x, new_colors, smooth) + if op == "color": + x = apply_monotones_to_img(x, new_colors) + elif op == "palette": + x = apply_palette_to_img(x, new_colors, smooth) + elif op == "mapping": + x = apply_mapping_to_img(x, new_colors, smooth) x = x.convert("RGB") x.save(path) diff --git a/mappings/renord.json b/mappings/renord.json new file mode 100644 index 00000000..9abe9269 --- /dev/null +++ b/mappings/renord.json @@ -0,0 +1,14 @@ +{ + "type": "mapping", + "name": "Re-Nord", + "desc": "Maps nord colors to different nord colors. Only meant for testing the color remapping function.", + "smooth": true, + "map": { + "#434c5e": "#2e3440", + "#4c566a": "#3b4252", + "#b48ead": "#8fbcbb", + "#ebcb8b": "#eceff4", + "#d08770": "#e5e9f0", + "#bf616a": "#d8dee9" + } +} diff --git a/palettes/catppuccin.json b/palettes/catppuccin.json index f718162f..9e7bf058 100644 --- a/palettes/catppuccin.json +++ b/palettes/catppuccin.json @@ -1,4 +1,5 @@ { + "type": "palette", "name": "Catppuccin Latte", "desc": "Pastel theme that aims to be the middle ground between low and high contrast themes.", "smooth": false, diff --git a/palettes/dracula.json b/palettes/dracula.json index f40ef99d..889a5040 100644 --- a/palettes/dracula.json +++ b/palettes/dracula.json @@ -1,6 +1,7 @@ { + "type": "palette", "name": "Dracula", - "desc": "Dark mode color scheme with vibrant and contrasting colors.", + "desc": "Dark mode scheme with vibrant and contrasting colors.", "smooth": false, "colors": [ "#44475a", diff --git a/palettes/nord.json b/palettes/nord.json index 27b8b1cd..c7f24942 100644 --- a/palettes/nord.json +++ b/palettes/nord.json @@ -1,4 +1,5 @@ { + "type": "palette", "name": "Nord", "desc": "An arctic, north-bluish color palette.", "smooth": true, diff --git a/palettes/one_dark.json b/palettes/one_dark.json index d59df951..7a15bf52 100644 --- a/palettes/one_dark.json +++ b/palettes/one_dark.json @@ -1,4 +1,5 @@ { + "type": "palette", "name": "One Dark", "desc": "Dark grey background with vibrant colors.", "smooth": true, diff --git a/readme.md b/readme.md index 00b74f1e..f70b460d 100644 --- a/readme.md +++ b/readme.md @@ -20,8 +20,11 @@ Color Manager is a program for recoloring and manipulating existing icon packs, | **Original** | ![1](resources/wallpaper/original.png) | | **Monochrome**:
`(0.6,0.54,0.5)` | ![2](resources/wallpaper/mono.png) | | **Multichrome**:
`nord.json`
`smooth=false` | ![3](resources/wallpaper/multi_accurate.png) | -| **Multichrome**:
`nord.json`
`smooth=true` | ![3](resources/wallpaper/multi_smooth.png) | -| **Extraction**:
Original `num=10` | ![4](resources/wallpaper/palette.png) | +| **Multichrome**:
`nord.json`
`smooth=true` | ![4](resources/wallpaper/multi_smooth.png) | +| **Remapping**:
`renord.json`
Remapped
previous to
alterate nord
colors. | ![5](resources/wallpaper/remapped.png) | +| **Extraction**:
Original `num=10` | ![6](resources/wallpaper/palette.png) | + +With these tools, one can build a pipeline to customize most assets to fit into a larger design. **GUI Demo**: ![demo](resources/demo.gif) @@ -41,7 +44,7 @@ Color Manager is a program for recoloring and manipulating existing icon packs, - [x] Remove metadata from svgs. - [x] Adding basic geometry to the backgrounds of svg icons. - [x] Preserve transparency in pngs after multichrome recoloring. -- [ ] Add an option to map colors directly using a json dictionary. +- [x] Add function to remap colors directly using a json dictionary. - [ ] Optional automatic palette extending. - [ ] Basic framework for manipulating GTK, Cinnamon and Metacity themes. - [ ] Intelligent color inversion. @@ -73,25 +76,26 @@ from color_manager import utils ``` Recoloring collections: ```python -src = "test_pack" +src = "test/graphics" # Also try "test/theme" name = "my_pack" dest = "~/Downloads" -hsl = (0.5, 0.5, 0.5) # = rc.norm_hsl(180, 50, 50) -palette = "palettes/dracula.json" +color = (0.6, 0.54, 0.5) # = rc.normalize_hsl(180, 50, 50) +palette = "palettes/nord.json" +mapping = "mappings/renord.json" -utils.recolor(src, dest, name, hsl) # Either hsl or palette. +utils.recolor(src, dest, name, color) # Either color, palette, or mapping. ``` Extracting color palette: ```python -image = "test_pack/imgs/lake_cabin.png" # Also try an svg. -num_colors = 10 +image = "test/graphics/imgs/lake_cabin.png" # Also try an svg. +num_colors = 10 # May be any number. output = "resources/palette.png" # Optional - saves colors as image. utils.extract_colors(image, num_colors, output) ``` Adding backdrops to svg icons: ```python -src = "test_pack" +src = "test/graphics" name = "my_pack" dest = "~/Downloads" color = "#000000" # Optional - Defaults to black. diff --git a/resources/wallpaper/remapped.png b/resources/wallpaper/remapped.png new file mode 100644 index 00000000..f9c1fd21 Binary files /dev/null and b/resources/wallpaper/remapped.png differ diff --git a/test.ipynb b/test.ipynb index d99db35f..10acc233 100644 --- a/test.ipynb +++ b/test.ipynb @@ -2,18 +2,9 @@ "cells": [ { "cell_type": "code", - "execution_count": 8, + "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" - ] - } - ], + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -23,31 +14,43 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 26, "metadata": {}, "outputs": [ { - "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" + "name": "stdout", + "output_type": "stream", + "text": [ + "/home/nv/Downloads/my_pack/index.theme\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "svg: 100%|██████████| 6/6 [00:00<00:00, 1582.36it/s]\n", + "png: 0%| | 0/8 [00:00