import os import re import shutil from typing import List, Dict from PIL import Image from jinja2 import Environment, FileSystemLoader # Attempt to import cairosvg for SVG support, set flag based on success try: import cairosvg from io import BytesIO SVGSUPPORT = True except ImportError: SVGSUPPORT = False from modules.argumentparser import Args # Define constants for static files directory and icon sizes STATIC_FILES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "files") ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"] # Initialize Jinja2 environment for template rendering env = Environment(loader=FileSystemLoader(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "templates"))) class Icon: src: str type: str sizes: str purpose: str def extract_colorscheme(theme_path: str) -> Dict[str, str]: """ Extract color scheme from a CSS theme file. Parameters: ----------- theme_path : str Path to the CSS theme file. Returns: -------- Dict[str, str] Dictionary containing color scheme variables and their values. """ pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+);" colorscheme = {} with open(theme_path, "r", encoding="utf-8") as f: filecontent = f.read() matches = re.findall(pattern, filecontent) for match in matches: colorscheme[match[0]] = match[1] return colorscheme def extract_theme_color(theme_path: str) -> str: """ Extract the theme color from a CSS theme file. Parameters: ----------- theme_path : str Path to the CSS theme file. Returns: -------- str The theme color value. """ pattern = r"--bcolor1:\s*(#[0-9a-fA-F]+);" with open(theme_path, "r", encoding="utf-8") as f: filecontent = f.read() match = re.search(pattern, filecontent) return match.group(1) if match else "" def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str: """ Render an SVG icon using the provided color scheme. Parameters: ----------- colorscheme : Dict[str, str] Dictionary containing color scheme variables and their values. iconspath : str Path to the directory where the icon will be saved. Returns: -------- str The rendered SVG content. """ svg = env.get_template("icon.svg.j2") content = svg.render(colorscheme=colorscheme) with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f: f.write(content) return content def save_png_icon(content: str, iconspath: str) -> None: """ Save the rendered SVG content as a PNG icon. Parameters: ----------- content : str The rendered SVG content. iconspath : str Path to the directory where the PNG icon will be saved. """ tmpimg = BytesIO() cairosvg.svg2png(bytestring=content, write_to=tmpimg) with Image.open(tmpimg) as iconfile: iconfile.save(os.path.join(iconspath, "icon.png")) def generate_favicon(iconspath: str, root_directory: str) -> None: """ Generate a favicon from a PNG icon using ImageMagick. Parameters: ----------- iconspath : str Path to the directory containing the PNG icon. root_directory : str Root directory of the project where the favicon will be saved. """ command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}' if not shutil.which("magick"): command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}' os.system(command) def icons(_args: Args) -> None: """ Generate icons and save them in the static directory. Parameters: ----------- _args : Args Parsed command-line arguments. """ print("Generating icons...") iconspath = os.path.join(_args.root_directory, ".static", "icons") colorscheme = extract_colorscheme(_args.theme_path) content = render_svg_icon(colorscheme, iconspath) if not SVGSUPPORT: print("Please install cairosvg to generate favicon from svg icon.") return save_png_icon(content, iconspath) generate_favicon(iconspath, _args.root_directory) def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, str]) -> None: """ Render the manifest.json file for the web application. Parameters: ----------- _args : Args Parsed command-line arguments. icon_list : List[Icon] List of icons to be included in the manifest. colors : Dict[str, str] Dictionary containing color scheme and theme color. """ manifest = env.get_template("manifest.json.j2") content = manifest.render( name=_args.web_root_url.replace("https://", "").replace("http://", "").replace("/", ""), short_name=_args.site_title, icons=icon_list, background_color=colors["bcolor1"], theme_color=colors["theme_color"], ) with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f: f.write(content) def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List[Icon]: """ Create icons from an SVG file. Parameters: ----------- files : List[str] List of files in the icons directory. iconspath : str Path to the directory where the icons will be saved. _args : Args Parsed command-line arguments. Returns: -------- List[Icon] List of icons created from the SVG file. """ svg = [file for file in files if file.endswith(".svg")][0] icon_list = [ {"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"}, {"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"}, ] for size in ICON_SIZES: tmpimg = BytesIO() sizes = size.split("x") iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png") cairosvg.svg2png( url=os.path.join(iconspath, svg), write_to=tmpimg, output_width=int(sizes[0]), output_height=int(sizes[1]), scale=1, ) with Image.open(tmpimg) as iconfile: iconfile.save(iconpath, format="PNG") icon_list.append( { "src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png", "sizes": size, "type": "image/png", "purpose": "maskable", } ) icon_list.append( { "src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png", "sizes": size, "type": "image/png", "purpose": "any", } ) return icon_list def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]: """ Create icons from PNG files. Parameters: ----------- iconspath : str Path to the directory containing the PNG icons. web_root_url : str Base URL of the web root for the image hosting site. Returns: -------- List[Icon] List of icons created from PNG files. """ icon_list = [] for icon in os.listdir(iconspath): if not icon.endswith(".png"): continue with Image.open(os.path.join(iconspath, icon)) as iconfile: iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}" icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"}) icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"}) return icon_list def webmanifest(_args: Args) -> None: """ Generate the web manifest file for the application. Parameters: ----------- _args : Args Parsed command-line arguments. """ iconspath = os.path.join(_args.root_directory, ".static", "icons") files = os.listdir(iconspath) icon_list = ( create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url) ) if not icon_list: print("No icons found in the static/icons folder!") return colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css")) colorscheme["theme_color"] = extract_theme_color(os.path.join(_args.root_directory, ".static", "theme.css")) render_manifest_json(_args, icon_list, colorscheme)