fmt: use treefmt-nix

This commit is contained in:
EdenQwQ 2025-03-02 12:49:19 +08:00
parent 6b551fb01d
commit 4667f69f52
5 changed files with 286 additions and 135 deletions

37
flake.lock generated
View file

@ -660,6 +660,22 @@
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1735554305,
"narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixvim": {
"inputs": {
"flake-parts": "flake-parts_3",
@ -760,7 +776,8 @@
"nixpkgs-unstable": "nixpkgs-unstable",
"nixvim": "nixvim",
"nur": "nur",
"stylix": "stylix"
"stylix": "stylix",
"treefmt-nix": "treefmt-nix_4"
}
},
"rust-overlay": {
@ -1012,6 +1029,24 @@
"type": "github"
}
},
"treefmt-nix_4": {
"inputs": {
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1739829690,
"narHash": "sha256-mL1szCeIsjh6Khn3nH2cYtwO5YXG6gBiTw1A30iGeDU=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "3d0579f5cc93436052d94b73925b48973a104204",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"xwayland-satellite-stable": {
"flake": false,
"locked": {

View file

@ -11,8 +11,31 @@
systems = [ "x86_64-linux" ];
imports = [
./hosts
inputs.treefmt-nix.flakeModule
{ _module.args = { inherit inputs self nixpkgs; }; }
];
perSystem =
{ pkgs, ... }:
{
treefmt = {
projectRootFile = "flake.nix";
programs.nixfmt.enable = true;
programs.nixfmt.package = pkgs.nixfmt-rfc-style;
programs.black.enable = true;
programs.prettier.enable = true;
programs.beautysh.enable = true;
settings.formatter = {
jsonc = {
command = "${pkgs.nodePackages.prettier}/bin/prettier";
includes = [ "*.jsonc" ];
};
scripts = {
command = "${pkgs.beautysh}/bin/beautysh";
includes = [ "*/scripts/*" ];
};
};
};
};
};
inputs = {
@ -42,5 +65,6 @@
inputs.nixpkgs.follows = "nixpkgs";
};
nh.url = "github:viperML/nh";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
}

View file

@ -3,7 +3,7 @@
wallpapers_dir="$HOME/Pictures/Wallpapers/generated"
wall=$(ls $wallpapers_dir | grep -v '\-blurred\.jpg$' | tofi)
if [ -z $wall ]; then
exit
exit
fi
output=$(niri msg -j focused-output | jq -r .name)
swww img -o $output $wallpapers_dir/$wall --transition-type random --transition-duration 1

View file

@ -2,6 +2,6 @@
wall=$(yad --file)
if [[ $wall == "" ]]; then
exit
exit
fi
swww img $wall --transition-type random --transition-duration 1

View file

@ -6,27 +6,35 @@
# Desc: A program for recoloring icon packs, themes and wallpapers. For NovaOS.
# Auth: Nicklas Vraa
from typing import Annotated,List, Set, Tuple, Dict, Optional
from typing import Annotated, List, Set, Tuple, Dict, Optional
from tqdm import tqdm
# from basic_colormath.type_hints import RGB, Lab
from basic_colormath.distance import rgb_to_lab, get_delta_e_lab
from PIL import Image, ImageDraw # noqa
import os, re, shutil, json, subprocess, argparse, random # noqa
from PIL import Image, ImageDraw # noqa
import os, re, shutil, json, subprocess, argparse, random # noqa
# Using custom type hints as the default ones in basic_colormath.type_hits arent compatible past python 3.8
RGB = Annotated[tuple[float, float, float], ([0, 255], [0, 255], [0, 255])]
Lab = Annotated[tuple[float, float, float], ([0, 100], [-128, 127], [-128, 127])]
def LabColor(L:float, a:float, b:float) -> Lab:
def LabColor(L: float, a: float, b: float) -> Lab:
return L, a, b
def sRGBColor(r:float, g:float, b:float) -> RGB:
return r,g,b
def sRGBColor(r: float, g: float, b: float) -> RGB:
return r, g, b
# Global constants -------------------------------------------------------------
# A dynamic dictionary to avoid multiple color conversions.
hex_to_lab_dict = {
"#ffffff": LabColor(9341.568974319263, -0.037058350415009045, -0.6906417562959177), # White.
"#000000": LabColor(0,0,0) # Black.
"#ffffff": LabColor(
9341.568974319263, -0.037058350415009045, -0.6906417562959177
), # White.
"#000000": LabColor(0, 0, 0), # Black.
}
name_to_hex_dict = {
@ -177,86 +185,105 @@ name_to_hex_dict = {
"white": "#ffffff",
"whitesmoke": "#f5f5f5",
"yellow": "#ffff00",
"yellowgreen": "#9acd32"
"yellowgreen": "#9acd32",
}
# 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. """
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))
def is_empty(list:List) -> bool:
""" Returns true if given list is empty, else false. """
def is_empty(list: List) -> bool:
"""Returns true if given list is empty, else false."""
return len(list) == 0
def load_json_file(path:str) -> Dict:
""" Return object defined in given json file. """
with open(path, 'r') as file:
def load_json_file(path: str) -> Dict:
"""Return object defined in given json file."""
with open(path, "r") as file:
obj = json.load(file)
return obj
def check_path(path:str) -> None:
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. """
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)
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')]
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 = os.path.splitext(svg)[0] + ".png"
png_path = os.path.join(dest_path, png)
command = ['inkscape', svg_path, '-o', png_path, '-w', str(width)]
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]:
""" Converts 6-digit hexadecimal color to rgb color. """
def hex_to_rgb(hex: str) -> Tuple[int, int, int]:
"""Converts 6-digit hexadecimal color to rgb color."""
return int(hex[1:3], 16), int(hex[3:5], 16), int(hex[5:7], 16)
def hex_to_hsl(hex:str) -> Tuple[float,float,float]:
""" Converts 6-digit hexadecimal color to hsl color. """
def hex_to_hsl(hex: str) -> Tuple[float, float, float]:
"""Converts 6-digit hexadecimal color to hsl color."""
return rgb_to_hsl(hex_to_rgb(hex))
def hex_to_gray(hex:str) -> str:
""" Grayscales a given 6-digit hexadecimal color. """
r, g, b = hex_to_rgb(hex)
return '#' + format(int(0.21*r + 0.72*g + 0.07*b), '02x')*3
def rgb_to_hex(rgb:Tuple[int,int,int]) -> str:
""" Converts rgb color to 6-digit hexadecimal color. """
def hex_to_gray(hex: str) -> str:
"""Grayscales a given 6-digit hexadecimal color."""
r, g, b = hex_to_rgb(hex)
return "#" + format(int(0.21 * r + 0.72 * g + 0.07 * b), "02x") * 3
def rgb_to_hex(rgb: Tuple[int, int, int]) -> str:
"""Converts rgb color to 6-digit hexadecimal color."""
r, g, b = rgb
return "#{:02x}{:02x}{:02x}".format(r, g, b)
def rgba_to_hex(rgba:Tuple[int,int,int,Optional[float]]) -> str:
""" Converts rgba color to 8-digit hexadecimal color. """
def rgba_to_hex(rgba: Tuple[int, int, int, Optional[float]]) -> str:
"""Converts rgba color to 8-digit hexadecimal color."""
r, g, b = rgba[:3]
hex = "#{:02X}{:02X}{:02X}".format(r, g, b)
if len(rgba) > 3:
if rgba[3] != 1.0:
hex += format(int(rgba[3] * 255), '02X')
hex += format(int(rgba[3] * 255), "02X")
return hex
def rgb_to_hsl(rgb:Tuple[int,int,int]) -> Tuple[float,float,float]:
""" Converts rgb color to hsl color. """
def rgb_to_hsl(rgb: Tuple[int, int, int]) -> Tuple[float, float, float]:
"""Converts rgb color to hsl color."""
r, g, b = rgb
r /= 255.0; g /= 255.0; b /= 255.0
max_val = max(r, g, b); min_val = min(r, g, b)
r /= 255.0
g /= 255.0
b /= 255.0
max_val = max(r, g, b)
min_val = min(r, g, b)
h = s = l = (max_val + min_val) / 2.0
if max_val == min_val:
@ -265,28 +292,35 @@ def rgb_to_hsl(rgb:Tuple[int,int,int]) -> Tuple[float,float,float]:
d = max_val - min_val
s = d / (2.0 - max_val - min_val)
if max_val == r: h = (g - b) / d + (6.0 if g < b else 0.0)
elif max_val == g: h = (b - r) / d + 2.0
else: h = (r - g) / d + 4.0
if max_val == r:
h = (g - b) / d + (6.0 if g < b else 0.0)
elif max_val == g:
h = (b - r) / d + 2.0
else:
h = (r - g) / d + 4.0
h /= 6.0
return h, s, l
def rgb_to_gray(rgb:Tuple[int,int,int]) -> Tuple[int,int,int]:
""" Grayscales a given rgb color. """
def rgb_to_gray(rgb: Tuple[int, int, int]) -> Tuple[int, int, int]:
"""Grayscales a given rgb color."""
r, g, b = rgb
weighed_avg = int(0.21*r + 0.72*g + 0.07*b)
weighed_avg = int(0.21 * r + 0.72 * g + 0.07 * b)
return weighed_avg, weighed_avg, weighed_avg
def hsl_to_rgb(hsl:Tuple[float,float,float]) -> Tuple[int,int,int]:
""" Converts hsl color to rgb color. """
def hsl_to_rgb(hsl: Tuple[float, float, float]) -> Tuple[int, int, int]:
"""Converts hsl color to rgb color."""
h, s, l = hsl
if s == 0:
r = g = b = l
else:
if l < 0.5: q = l * (1 + s)
else: q = l + s - l * s
if l < 0.5:
q = l * (1 + s)
else:
q = l + s - l * s
p = 2 * l - q
r = hue_to_rgb(p, q, h + 1 / 3)
g = hue_to_rgb(p, q, h)
@ -294,78 +328,92 @@ def hsl_to_rgb(hsl:Tuple[float,float,float]) -> Tuple[int,int,int]:
return int(round(r * 255)), int(round(g * 255)), int(round(b * 255))
def hue_to_rgb(p:float, q:float, t:float) -> float:
""" Converts hue to rgb values. Only used by the hsl_to_rgb function. """
if t < 0: t += 1
if t > 1: t -= 1
if t < 1 / 6: return p + (q - p) * 6 * t
if t < 1 / 2: return q
if t < 2 / 3: return p + (q - p) * (2 / 3 - t) * 6
def hue_to_rgb(p: float, q: float, t: float) -> float:
"""Converts hue to rgb values. Only used by the hsl_to_rgb function."""
if t < 0:
t += 1
if t > 1:
t -= 1
if t < 1 / 6:
return p + (q - p) * 6 * t
if t < 1 / 2:
return q
if t < 2 / 3:
return p + (q - p) * (2 / 3 - t) * 6
return p
def norm_hsl(h:int, s:int, l:int) -> Tuple[float,float,float]:
""" Normalize hsl color values. """
return h/360, s/100, l/100
def norm_hsl(h: int, s: int, l: int) -> Tuple[float, float, float]:
"""Normalize hsl color values."""
return h / 360, s / 100, l / 100
# Preprocessing ----------------------------------------------------------------
def expand_css_rgb(match) -> str:
""" Used by the css_to_hex function. """
return rgb_to_hex((
int(match.group(1)), int(match.group(2)),
int(match.group(3)))
"""Used by the css_to_hex function."""
return rgb_to_hex((int(match.group(1)), int(match.group(2)), int(match.group(3))))
def css_to_hex(text: str) -> str:
"""Returns the given string with css rgba functions and named colors substituted for their corresponding hexadecimal codes."""
text = re.sub(
r"rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\)", expand_css_rgb, text
)
def css_to_hex(text:str) -> str:
""" Returns the given string with css rgba functions and named colors substituted for their corresponding hexadecimal codes. """
text = re.sub(r"rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\)",
expand_css_rgb, text)
text = re.sub(r"rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)",
expand_css_rgb, text)
text = re.sub(r"rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)", expand_css_rgb, text)
for key in name_to_hex_dict:
text = re.sub(key + r"\b", name_to_hex_dict[key], text)
return text
# Post-processing --------------------------------------------------------------
def hex_to_rgba(match) -> str:
""" Converts 8-digit hexadecimal code to rgba function. """
"""Converts 8-digit hexadecimal code to rgba function."""
hex = match.group(1)
return f"rgba({int(hex[1:3], 16)}, {int(hex[3:5], 16)}, {int(hex[5:7], 16)}, {int(hex[7:9], 16) / 255.0:.2f})"
def hex_to_css(text:str) -> str:
""" Convert 8-digit hexadecimal color codes to css rgba color functions. Needed when a css interpreter does not recognize the alpha-channel when reading hexadecimal color codes. """
def hex_to_css(text: str) -> str:
"""Convert 8-digit hexadecimal color codes to css rgba color functions. Needed when a css interpreter does not recognize the alpha-channel when reading hexadecimal color codes."""
return re.sub(r"(#[0-9a-fA-F]{8})", hex_to_rgba, text)
def expand_all_hex(text:str) -> str:
def expand_all_hex(text: str) -> str:
"""Expand all 3-digit hexadecimal codes in the input string to 6 digits."""
return re.sub(
r"((?<!&)#[A-Fa-f0-9]{3})\b",
lambda match: ("#" + "".join([c * 2 for c in match.group(1)[1:]])),
text
text,
)
# Color comparision ------------------------------------------------------------
def generate_palette_dict(colors:List[str]) -> Dict[str,Lab]:
""" Returns a dictionary mapping hexadecimal colors to lab colors. """
def generate_palette_dict(colors: List[str]) -> Dict[str, Lab]:
"""Returns a dictionary mapping hexadecimal colors to lab colors."""
palette_dict = {}
for color in colors:
r, g, b = hex_to_rgb(color)
palette_dict[color] = rgb_to_lab(sRGBColor(r,g,b))
palette_dict[color] = rgb_to_lab(sRGBColor(r, g, b))
return palette_dict
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. """
"""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:
@ -377,13 +425,18 @@ def get_input_colors(resource):
resource = load_json_file(resource)
if resource["type"] == "palette":
return generate_palette_dict(resource["colors"]), resource["smooth"], "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. """
def get_file_colors(text: str) -> Set[str]:
"""Return a set of all unique colors within a given string representing an svg-file."""
colors = set()
matches = re.findall(r"#[A-Fa-f0-9]{6}", text)
@ -393,11 +446,12 @@ def get_file_colors(text:str) -> Set[str]:
return colors
def closest_match(color:str, palette:Dict[str,Lab]) -> str:
""" Compare the similarity of colors in the CIELAB colorspace. Return the closest match, i.e. the palette entry with the smallest euclidian distance to the given color. """
def closest_match(color: str, palette: Dict[str, Lab]) -> str:
"""Compare the similarity of colors in the CIELAB colorspace. Return the closest match, i.e. the palette entry with the smallest euclidian distance to the given color."""
closest_color = None
min_distance = float('inf')
min_distance = float("inf")
for entry in palette:
@ -406,7 +460,7 @@ def closest_match(color:str, palette:Dict[str,Lab]) -> str:
if lab_color is None:
r, g, b = hex_to_rgb(color)
lab_color = rgb_to_lab(sRGBColor(r,g,b))
lab_color = rgb_to_lab(sRGBColor(r, g, b))
hex_to_lab_dict[color] = lab_color
distance = get_delta_e_lab(lab_color, palette[entry])
@ -417,32 +471,35 @@ def closest_match(color:str, palette:Dict[str,Lab]) -> str:
return closest_color
# Pack management --------------------------------------------------------------
def get_paths(folder: str, exts: List[str]) -> List[str]:
""" Return paths of every file with the given extensions within a folder and its subfolders, excluding symbolic links. """
"""Return paths of every file with the given extensions within a folder and its subfolders, excluding symbolic links."""
paths = []
for item in os.listdir(folder):
item_path = os.path.join(folder, item)
if os.path.islink(item_path): # Link.
if os.path.islink(item_path): # Link.
continue
if os.path.isfile(item_path): # File.
if os.path.isfile(item_path): # File.
for ext in exts:
if item.lower().endswith(ext):
paths.append(item_path)
elif os.path.isdir(item_path): # Folder.
elif os.path.isdir(item_path): # Folder.
subfolder_paths = get_paths(item_path, exts)
paths.extend(subfolder_paths)
return paths
def copy_file_structure(src_path:str, dest_path:str) -> None:
""" Copies a directory tree, but changes symbolic links to point to files within the destination folder instead of the source. Assumes that no link points to files outside the source folder. """
def copy_file_structure(src_path: str, dest_path: str) -> None:
"""Copies a directory tree, but changes symbolic links to point to files within the destination folder instead of the source. Assumes that no link points to files outside the source folder."""
shutil.rmtree(dest_path, ignore_errors=True)
shutil.copytree(src_path, dest_path, symlinks=True)
@ -466,14 +523,15 @@ def copy_file_structure(src_path:str, dest_path:str) -> None:
relative_target = relative_target.replace(src_path, dest_path, 1)
os.symlink(relative_target, file_path)
def rename_pack(src_path, dest_path:str, name:str) -> None:
""" If an index.theme file exists within the given folder, apply appropiate naming. """
def rename_pack(src_path, dest_path: str, name: str) -> None:
"""If an index.theme file exists within the given folder, apply appropiate naming."""
index_path = os.path.join(dest_path, "index.theme")
print(index_path)
if os.path.exists(index_path):
with open(index_path, 'r') as file:
with open(index_path, "r") as file:
text = file.read()
text = re.sub(r"(Name=).*", "\\1" + name, text, count=1)
@ -481,13 +539,22 @@ def rename_pack(src_path, dest_path:str, name:str) -> None:
text = re.sub(r"(MetacityTheme=).*", "\\1" + name, text, count=1)
text = re.sub(r"(IconTheme=).*", "\\1" + name, text, count=1)
text = re.sub(r"(Comment=).*", "\\1" + "A variant of " + os.path.basename(src_path) + " created by nicklasvraa/color-manager", text, count=1)
text = re.sub(
r"(Comment=).*",
"\\1"
+ "A variant of "
+ os.path.basename(src_path)
+ " created by nicklasvraa/color-manager",
text,
count=1,
)
with open(index_path, 'w') as file:
with open(index_path, "w") as file:
file.write(text)
def copy_pack(src_path:str, dest_path:str, name:str) -> str:
""" Copy pack and return the resulting copy's directory path. """
def copy_pack(src_path: str, dest_path: str, name: str) -> str:
"""Copy pack and return the resulting copy's directory path."""
src_path = expand_path(src_path)
dest_path = os.path.join(expand_path(dest_path), name)
@ -497,10 +564,14 @@ def copy_pack(src_path:str, dest_path:str, name:str) -> str:
return dest_path
# Vector-based recoloring ------------------------------------------------------
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. """
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, _ = hsl
@ -516,8 +587,11 @@ def apply_monotones_to_vec(text:str, colors:Set[str], hsl:Tuple[float,float,floa
return text
def apply_palette_to_vec(text:str, colors:Set[str], new_colors:Dict[str,Lab]) -> str:
""" Replace hexadecimal color codes in a given svg/xml/css string with their closest matches within the given color palette. """
def apply_palette_to_vec(
text: str, colors: Set[str], new_colors: Dict[str, Lab]
) -> 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)
@ -528,17 +602,21 @@ def apply_palette_to_vec(text:str, colors:Set[str], new_colors:Dict[str,Lab]) ->
return text
# Pixel-based recoloring -------------------------------------------------------
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. """
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
h, s, _ = hsl
if s == 0:
if mode == "RGBA": img = img.convert("LA")
else: img = img.convert("L")
if mode == "RGBA":
img = img.convert("LA")
else:
img = img.convert("L")
else:
width, height = img.size
@ -553,21 +631,26 @@ def apply_monotones_to_img(img:Image, hsl:Tuple[float,float,float]) -> Image:
new_color = hsl_to_rgb((h, s, old_l))
if mode == "RGBA":
img.putpixel((x,y), new_color + (a,))
img.putpixel((x, y), new_color + (a,))
else:
img.putpixel((x,y), new_color)
img.putpixel((x, y), new_color)
return img
def apply_palette_to_img(img:Image, new_colors:Dict[str,Lab], args) -> Image:
""" Replace colors in a given image with the closest match within a given color palette. """
if args.smooth: img = img.convert("P", palette=Image.ADAPTIVE, colors=256)
else: img = img.convert("P")
def apply_palette_to_img(img: Image, new_colors: Dict[str, Lab], args) -> Image:
"""Replace colors in a given image with the closest match within a given color palette."""
if args.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)]
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]
@ -582,10 +665,12 @@ def apply_palette_to_img(img:Image, new_colors:Dict[str,Lab], args) -> Image:
img.putpalette(new_palette)
return img
# User interface functions -----------------------------------------------------
def recolor_modified(op:str, args) -> None:
""" Recursively copies and converts a source folder into a destination, given either an hsl color, a palette, or a color mapping. """
def recolor_modified(op: str, args) -> None:
"""Recursively copies and converts a source folder into a destination, given either an hsl color, a palette, or a color mapping."""
check_path(args.src)
@ -595,7 +680,8 @@ def recolor_modified(op:str, args) -> None:
# Recolor vector graphics.
paths = get_paths(args.src, [".svg", ".xml"])
for path in tqdm(paths, desc="svg", disable=is_empty(paths)):
with open(path, 'r') as file: x = file.read()
with open(path, "r") as file:
x = file.read()
x = css_to_hex(x)
x = expand_all_hex(x)
@ -606,12 +692,14 @@ def recolor_modified(op:str, args) -> None:
elif op == "palette":
x = apply_palette_to_vec(x, colors, args.palette)
with open(path, 'w') as file: file.write(x)
with open(path, "w") as file:
file.write(x)
# Recolor stylesheets.
paths = get_paths(args.src, [".css", "rc"])
for path in tqdm(paths, desc="css", disable=is_empty(paths)):
with open(path, 'r') as file: x = file.read()
with open(path, "r") as file:
x = file.read()
x = css_to_hex(x)
x = expand_all_hex(x)
@ -623,14 +711,15 @@ def recolor_modified(op:str, args) -> None:
x = apply_palette_to_vec(x, colors, args.palette)
x = hex_to_css(x)
with open(path, 'w') as file: file.write(x)
with open(path, "w") as file:
file.write(x)
# Recolor pngs.
paths = get_paths(args.src, [".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.
a = x.split()[3] # Save original alpha channel.
if op == "monochrome":
x = apply_monotones_to_img(x, random.choice(args.monochrome))
@ -638,8 +727,8 @@ def recolor_modified(op:str, args) -> None:
x = apply_palette_to_img(x, args.palette, args)
x = x.convert("RGBA")
r,g,b,_ = x.split()
x = Image.merge("RGBA",(r,g,b,a)) # Restore original alpha channel.
r, g, b, _ = x.split()
x = Image.merge("RGBA", (r, g, b, a)) # Restore original alpha channel.
x.save(path)
# Recolor jpgs.
@ -656,16 +745,18 @@ def recolor_modified(op:str, args) -> None:
x = x.convert("RGB")
x.save(path)
def list_of_strings(arg):
return arg.split(',')
return arg.split(",")
def main():
parser = argparse.ArgumentParser(description='Recolor an image.')
parser = argparse.ArgumentParser(description="Recolor an image.")
parser.add_argument('--src', type=str)
parser.add_argument('--monochrome', type=list_of_strings)
parser.add_argument('--palette', type=list_of_strings)
parser.add_argument('--smooth', type=bool, default=True)
parser.add_argument("--src", type=str)
parser.add_argument("--monochrome", type=list_of_strings)
parser.add_argument("--palette", type=list_of_strings)
parser.add_argument("--smooth", type=bool, default=True)
args = parser.parse_args()
@ -676,5 +767,6 @@ def main():
elif args.palette != None:
recolor_modified("palette", args)
if __name__ == '__main__':
if __name__ == "__main__":
main()