Added direct color remapping functionality

This commit is contained in:
NicklasVraa
2023-08-22 19:23:09 +02:00
parent 93f9349844
commit 512d08d470
27 changed files with 141 additions and 62 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
resources/demo.mp4 resources/demo.mp4
test_pack/theme/ test/theme/
*.egg-info/ *.egg-info/
__pycache__/ __pycache__/

View File

@@ -177,18 +177,23 @@ def generate_palette_dict(colors:List[str]) -> Dict[str,LabColor]:
return palette_dict return palette_dict
def get_input_colors(resource) -> Tuple[List[str],bool,bool]: def get_input_colors(resource):
""" 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. """ """ 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 resource is an hsl color.
if isinstance(resource, tuple) and len(resource) == 3: if isinstance(resource, tuple) and len(resource) == 3:
return resource, False, True return resource, False, "color"
else: else:
# If resource is a path to a palette. # If resource is a path to a resource, first unpack.
if type(resource) is str: resource = load_json_file(resource) 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]: def get_file_colors(text:str) -> Set[str]:
""" Return a set of all unique colors within a given string representing an svg-file. """ """ 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 ------------------------------------------------------ # 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. """ """ 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 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: if s == 0:
for color in colors: for color in colors:
graytone = hex_to_gray(color) graytone = hex_to_gray(color)
svg = re.sub(color, graytone, svg) text = re.sub(color, graytone, text)
else: else:
l_offset = (l_offset - 0.5) * 2 # Remapping. 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 = (0.21*r + 0.72*g + 0.07*b)/255
l = max(-1, min(l+l_offset, 1)) l = max(-1, min(l+l_offset, 1))
monochrome = rgb_to_hex(hsl_to_rgb((h, s, l))) 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: def apply_palette_to_vec(text: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. """ """ Replace hexadecimal color codes in a given svg/xml/css string with their closest matches within the given color palette. """
for color in colors: for color in colors:
new_color = closest_match(color, new_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 ------------------------------------------------------- # 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)] 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] hex_palette = ["#%02x%02x%02x" % rgb for rgb in rgb_palette]
new_palette = []
new_palette = []
for color in hex_palette: for color in hex_palette:
new_color = hex_to_rgb(closest_match(color, new_colors)) new_color = hex_to_rgb(closest_match(color, new_colors))
new_palette.extend(new_color) 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) img.putpalette(new_palette)
return img 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 ----------------------------------------------------- # User interface functions -----------------------------------------------------
def recolor(src_path:str, dest_path:str, name:str, replacement) -> None: 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(src_path)
check_path(dest_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) dest_path = copy_pack(src_path, dest_path, name)
# Recolor vector graphics. # Recolor vector graphics.
@@ -410,8 +448,12 @@ def recolor(src_path:str, dest_path:str, name:str, replacement) -> None:
x = expand_all_hex(x) x = expand_all_hex(x)
colors = get_file_colors(x) colors = get_file_colors(x)
if is_mono: x = apply_monotones_to_vec(x, colors, new_colors) if op == "color":
else: x = apply_palette_to_vec(x, colors, new_colors) 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) 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) x = expand_all_hex(x)
colors = get_file_colors(x) colors = get_file_colors(x)
if is_mono: x = apply_monotones_to_vec(x, colors, new_colors) if op == "color":
else: x = apply_palette_to_vec(x, colors, new_colors) 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) 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") x = x.convert("RGBA")
a = x.split()[3] # Save original alpha channel. a = x.split()[3] # Save original alpha channel.
if is_mono: x = apply_monotones_to_img(x, new_colors) if op == "color":
else: x = apply_palette_to_img(x, new_colors, smooth) 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") x = x.convert("RGBA")
r,g,b,_ = x.split() 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 = Image.open(path)
x = x.convert("RGB") x = x.convert("RGB")
if is_mono: x = apply_monotones_to_img(x, new_colors) if op == "color":
else: x = apply_palette_to_img(x, new_colors, smooth) 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 = x.convert("RGB")
x.save(path) x.save(path)

14
mappings/renord.json Normal file
View File

@@ -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"
}
}

View File

@@ -1,4 +1,5 @@
{ {
"type": "palette",
"name": "Catppuccin Latte", "name": "Catppuccin Latte",
"desc": "Pastel theme that aims to be the middle ground between low and high contrast themes.", "desc": "Pastel theme that aims to be the middle ground between low and high contrast themes.",
"smooth": false, "smooth": false,

View File

@@ -1,6 +1,7 @@
{ {
"type": "palette",
"name": "Dracula", "name": "Dracula",
"desc": "Dark mode color scheme with vibrant and contrasting colors.", "desc": "Dark mode scheme with vibrant and contrasting colors.",
"smooth": false, "smooth": false,
"colors": [ "colors": [
"#44475a", "#44475a",

View File

@@ -1,4 +1,5 @@
{ {
"type": "palette",
"name": "Nord", "name": "Nord",
"desc": "An arctic, north-bluish color palette.", "desc": "An arctic, north-bluish color palette.",
"smooth": true, "smooth": true,

View File

@@ -1,4 +1,5 @@
{ {
"type": "palette",
"name": "One Dark", "name": "One Dark",
"desc": "Dark grey background with vibrant colors.", "desc": "Dark grey background with vibrant colors.",
"smooth": true, "smooth": true,

View File

@@ -20,8 +20,11 @@ Color Manager is a program for recoloring and manipulating existing icon packs,
| **Original** | ![1](resources/wallpaper/original.png) | | **Original** | ![1](resources/wallpaper/original.png) |
| **Monochrome**:<br>`(0.6,0.54,0.5)` | ![2](resources/wallpaper/mono.png) | | **Monochrome**:<br>`(0.6,0.54,0.5)` | ![2](resources/wallpaper/mono.png) |
| **Multichrome**:<br>`nord.json`<br>`smooth=false` | ![3](resources/wallpaper/multi_accurate.png) | | **Multichrome**:<br>`nord.json`<br>`smooth=false` | ![3](resources/wallpaper/multi_accurate.png) |
| **Multichrome**:<br>`nord.json`<br>`smooth=true` | ![3](resources/wallpaper/multi_smooth.png) | | **Multichrome**:<br>`nord.json`<br>`smooth=true` | ![4](resources/wallpaper/multi_smooth.png) |
| **Extraction**:<br>Original `num=10` | ![4](resources/wallpaper/palette.png) | | **Remapping**:<br>`renord.json`<br>Remapped<br>previous to<br>alterate nord <br>colors. | ![5](resources/wallpaper/remapped.png) |
| **Extraction**:<br>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**: **GUI Demo**:
![demo](resources/demo.gif) ![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] Remove metadata from svgs.
- [x] Adding basic geometry to the backgrounds of svg icons. - [x] Adding basic geometry to the backgrounds of svg icons.
- [x] Preserve transparency in pngs after multichrome recoloring. - [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. - [ ] Optional automatic palette extending.
- [ ] Basic framework for manipulating GTK, Cinnamon and Metacity themes. - [ ] Basic framework for manipulating GTK, Cinnamon and Metacity themes.
- [ ] Intelligent color inversion. - [ ] Intelligent color inversion.
@@ -73,25 +76,26 @@ from color_manager import utils
``` ```
Recoloring collections: Recoloring collections:
```python ```python
src = "test_pack" src = "test/graphics" # Also try "test/theme"
name = "my_pack" name = "my_pack"
dest = "~/Downloads" dest = "~/Downloads"
hsl = (0.5, 0.5, 0.5) # = rc.norm_hsl(180, 50, 50) color = (0.6, 0.54, 0.5) # = rc.normalize_hsl(180, 50, 50)
palette = "palettes/dracula.json" 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: Extracting color palette:
```python ```python
image = "test_pack/imgs/lake_cabin.png" # Also try an svg. image = "test/graphics/imgs/lake_cabin.png" # Also try an svg.
num_colors = 10 num_colors = 10 # May be any number.
output = "resources/palette.png" # Optional - saves colors as image. output = "resources/palette.png" # Optional - saves colors as image.
utils.extract_colors(image, num_colors, output) utils.extract_colors(image, num_colors, output)
``` ```
Adding backdrops to svg icons: Adding backdrops to svg icons:
```python ```python
src = "test_pack" src = "test/graphics"
name = "my_pack" name = "my_pack"
dest = "~/Downloads" dest = "~/Downloads"
color = "#000000" # Optional - Defaults to black. color = "#000000" # Optional - Defaults to black.

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

View File

@@ -2,18 +2,9 @@
"cells": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 8, "execution_count": 1,
"metadata": {}, "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": [ "source": [
"%load_ext autoreload\n", "%load_ext autoreload\n",
"%autoreload 2\n", "%autoreload 2\n",
@@ -23,31 +14,43 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 10, "execution_count": 26,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"ename": "Exception", "name": "stdout",
"evalue": "Invalid path: ~/Downloads", "output_type": "stream",
"output_type": "error", "text": [
"traceback": [ "/home/nv/Downloads/my_pack/index.theme\n"
"\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", "name": "stderr",
"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", "output_type": "stream",
"\u001b[0;31mException\u001b[0m: Invalid path: ~/Downloads" "text": [
"svg: 100%|██████████| 6/6 [00:00<00:00, 1582.36it/s]\n",
"png: 0%| | 0/8 [00:00<?, ?it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"png: 100%|██████████| 8/8 [00:11<00:00, 1.46s/it]\n",
"jpg: 100%|██████████| 1/1 [00:04<00:00, 4.32s/it]\n"
] ]
} }
], ],
"source": [ "source": [
"src = \"test_pack\" # = \"test_theme\"\n", "src = \"test/graphics\" # Also try \"test/theme\"\n",
"name = \"my_pack3\" # = \"my_theme\"\n", "name = \"my_pack\"\n",
"dest = \"~/Downloads\"\n", "dest = \"~/Downloads\"\n",
"hsl = (0.6, 0.54, 0.5) # = rc.normalize_hsl(180, 50, 50)\n", "color = (0.6, 0.54, 0.5) # = rc.normalize_hsl(180, 50, 50)\n",
"palette = \"palettes/nord.json\"\n", "palette = \"palettes/nord.json\"\n",
"mapping = \"mappings/renord.json\"\n",
"\n", "\n",
"utils.recolor(src, dest, name, hsl) # Either hsl or palette" "# Either input a single color, palette, or mapping.\n",
"utils.recolor(src, dest, name, color)"
] ]
} }
], ],

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 1023 B

After

Width:  |  Height:  |  Size: 1023 B

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 577 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB