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
test_pack/theme/
test/theme/
*.egg-info/
__pycache__/

View File

@@ -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)

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",
"desc": "Pastel theme that aims to be the middle ground between low and high contrast themes.",
"smooth": false,

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
{
"type": "palette",
"name": "One Dark",
"desc": "Dark grey background with vibrant colors.",
"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) |
| **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=true` | ![3](resources/wallpaper/multi_smooth.png) |
| **Extraction**:<br>Original `num=10` | ![4](resources/wallpaper/palette.png) |
| **Multichrome**:<br>`nord.json`<br>`smooth=true` | ![4](resources/wallpaper/multi_smooth.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**:
![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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

View File

@@ -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<?, ?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": [
"src = \"test_pack\" # = \"test_theme\"\n",
"name = \"my_pack3\" # = \"my_theme\"\n",
"src = \"test/graphics\" # Also try \"test/theme\"\n",
"name = \"my_pack\"\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",
"mapping = \"mappings/renord.json\"\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