Files
StaticGalleryBuilder/modules/svg_handling.py
2025-01-01 14:44:35 +01:00

271 lines
9.3 KiB
Python

import os
import shutil
from subprocess import Popen, PIPE
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.logger import logger
from modules.argumentparser import Args
from modules.css_color import extract_theme_color, extract_colorscheme
# Define constants for static files directory and icon sizes
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
STATIC_FILES_DIR = os.path.join(SCRIPTDIR, "files")
ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"]
# Initialize Jinja2 environment for template rendering
env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates")))
class Icon:
src: str
type: str
sizes: str
purpose: str
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:
logger.info("writing svg icon", extra={"iconspath": iconspath})
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:
logger.info("saving png icon", extra={"iconspath": iconspath})
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.
"""
favicon = os.path.join(root_directory, ".static", "favicon.ico")
logger.info("generating favicon with imagemagick", extra={"iconspath": iconspath, "favicon": favicon})
_env = dict(os.environ)
lp_key = "LD_LIBRARY_PATH"
lp_orig = _env.get(lp_key + "_ORIG")
if lp_orig is not None:
_env[lp_key] = lp_orig
else:
_env.pop(lp_key, None)
if not shutil.which("magick"):
magick = shutil.which("convert")
else:
magick = shutil.which("magick")
command = [magick, os.path.join(iconspath, "icon.png"), "-define", "icon:auto-resize=16,32,48,64,72,96,144,192", favicon]
with Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=_env, errors="ignore") as p:
out, err = p.communicate()
if p.returncode != 0:
logger.error("error generating favicon: %s", err, extra={"command": command, "out": out, "err": err})
logger.info("favicon generated successfully", extra={"command": command, "out": out, "err": err})
def icons(_args: Args) -> None:
"""
Generate icons and save them in the static directory.
Parameters:
-----------
_args : Args
Parsed command-line arguments.
"""
iconspath = os.path.join(_args.root_directory, ".static", "icons")
logger.info("generating icons", extra={"iconspath": iconspath})
print("Generating 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.")
logger.error("svg support not available")
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:
logger.info("rendering manifest.json", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.json")})
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]
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
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")
logger.info("converting svg to png", extra={"svg": svg, "size": size})
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:
logger.info("saving png file", extra={"iconpath": iconpath})
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]}"
logger.info("using icon", extra={"iconspath": iconspath, "icon": icon, "size": iconsize})
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.
"""
logger.info("generating webmanifest")
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!")
logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath})
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)