Added direct color remapping functionality
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
resources/demo.mp4
|
||||
test_pack/theme/
|
||||
test/theme/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"type": "palette",
|
||||
"name": "Nord",
|
||||
"desc": "An arctic, north-bluish color palette.",
|
||||
"smooth": true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"type": "palette",
|
||||
"name": "One Dark",
|
||||
"desc": "Dark grey background with vibrant colors.",
|
||||
"smooth": true,
|
||||
|
||||
24
readme.md
@@ -20,8 +20,11 @@ Color Manager is a program for recoloring and manipulating existing icon packs,
|
||||
| **Original** |  |
|
||||
| **Monochrome**:<br>`(0.6,0.54,0.5)` |  |
|
||||
| **Multichrome**:<br>`nord.json`<br>`smooth=false` |  |
|
||||
| **Multichrome**:<br>`nord.json`<br>`smooth=true` |  |
|
||||
| **Extraction**:<br>Original `num=10` |  |
|
||||
| **Multichrome**:<br>`nord.json`<br>`smooth=true` |  |
|
||||
| **Remapping**:<br>`renord.json`<br>Remapped<br>previous to<br>alterate nord <br>colors. |  |
|
||||
| **Extraction**:<br>Original `num=10` |  |
|
||||
|
||||
With these tools, one can build a pipeline to customize most assets to fit into a larger design.
|
||||
|
||||
**GUI Demo**:
|
||||

|
||||
@@ -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.
|
||||
|
||||
BIN
resources/wallpaper/remapped.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
55
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<?, ?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)"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1023 B After Width: | Height: | Size: 1023 B |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 577 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
BIN
test/graphics/wallpapers/lake_cabin_nord.png
Normal file
|
After Width: | Height: | Size: 232 KiB |