7 Commits

37 changed files with 302 additions and 74 deletions

3
.gitignore vendored
View File

@@ -166,4 +166,5 @@ test/.thumbnails
test/**/index.html test/**/index.html
test/**/.sizelist.json test/**/.sizelist.json
test/manifest.json test/manifest.json
themes/previews themes/previews
logs

View File

@@ -1 +1 @@
2.2.7 2.3.2

View File

@@ -101,7 +101,7 @@
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "ms-python.black-formatter",
}, },
"black-formatter.args": [ "black-formatter.args": [
"-l 140", "-l 260",
], ],
"black-formatter.interpreter": [ "black-formatter.interpreter": [
"/usr/bin/python3", "/usr/bin/python3",

View File

@@ -11,10 +11,12 @@ from typing import Dict, List, Tuple
from tqdm.auto import tqdm from tqdm.auto import tqdm
from PIL import Image, ImageOps from PIL import Image, ImageOps
from modules.logger import logger
from modules.argumentparser import parse_arguments, Args from modules.argumentparser import parse_arguments, Args
from modules.svg_handling import icons, webmanifest, extract_colorscheme from modules.svg_handling import icons, webmanifest, extract_colorscheme
from modules.generate_html import list_folder, EXCLUDES from modules.generate_html import list_folder, EXCLUDES
# fmt: off # fmt: off
# Constants # Constants
if __package__ is None: if __package__ is None:
@@ -32,6 +34,7 @@ RAW_EXTENSIONS = [
] ]
IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"] IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"]
NOT_LIST = ["*/Galleries/*", "Archives"] NOT_LIST = ["*/Galleries/*", "Archives"]
LOG_FILE = os.path.join(SCRIPTDIR, "log.json")
# fmt: on # fmt: on
pbardict: Dict[str, tqdm] = {} pbardict: Dict[str, tqdm] = {}
@@ -77,10 +80,13 @@ def copy_static_files(_args: Args) -> None:
static_dir = os.path.join(_args.root_directory, ".static") static_dir = os.path.join(_args.root_directory, ".static")
if os.path.exists(static_dir): if os.path.exists(static_dir):
print("Removing existing .static folder...") print("Removing existing .static folder...")
logger.info("removing existing .static folder")
shutil.rmtree(static_dir) shutil.rmtree(static_dir)
print("Copying static files...") print("Copying static files...")
logger.info("copying static files")
shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True) shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True)
logger.info("reading theme file", extra={"theme": _args.theme_path})
with open(_args.theme_path, "r", encoding="utf-8") as f: with open(_args.theme_path, "r", encoding="utf-8") as f:
theme = f.read() theme = f.read()
split = theme.split(".foldericon {") split = theme.split(".foldericon {")
@@ -92,20 +98,24 @@ def copy_static_files(_args: Args) -> None:
for match in re.finditer(r"content: (.*);", foldericon): for match in re.finditer(r"content: (.*);", foldericon):
foldericon = match[1] foldericon = match[1]
foldericon = foldericon.replace('"', "") foldericon = foldericon.replace('"', "")
logger.info("found foldericon", extra={"foldericon": foldericon})
break break
if "url" in foldericon: if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css")) shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
return return
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f: with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
logger.info("Reading foldericon svg")
svg = f.read() svg = f.read()
if "svg.j2" in foldericon: if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(_args.theme_path) colorscheme = extract_colorscheme(_args.theme_path)
svg = svg.replace("{{ color1 }}", colorscheme["color1"]) for color_key, color_value in colorscheme.items():
svg = svg.replace("{{ color2 }}", colorscheme["color2"]) svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
svg = svg.replace("{{ color3 }}", colorscheme["color3"]) logger.info("replaced colors in svg")
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
svg = urllib.parse.quote(svg) svg = urllib.parse.quote(svg)
with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f: with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f:
logger.info("writing theme file")
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail) f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
@@ -119,6 +129,7 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
A tuple containing the folder, item, root directory, and regenerate thumbnails flag. A tuple containing the folder, item, root directory, and regenerate thumbnails flag.
""" """
folder, item, root_directory = arguments folder, item, root_directory = arguments
image = os.path.join(folder, item)
path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), item) + ".jpg" path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), item) + ".jpg"
oldpath = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".jpg" oldpath = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".jpg"
if os.path.exists(oldpath): if os.path.exists(oldpath):
@@ -127,14 +138,19 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
except FileNotFoundError: except FileNotFoundError:
pass pass
if not os.path.exists(path): if not os.path.exists(path):
logger.info("generating thumbnail for %s", item, extra={"path": image})
try: try:
with Image.open(os.path.join(folder, item)) as imgfile: with Image.open(image) as imgfile:
imgrgb = imgfile.convert("RGB") imgrgb = imgfile.convert("RGB")
img = ImageOps.exif_transpose(imgrgb) img = ImageOps.exif_transpose(imgrgb)
img.thumbnail((512, 512)) img.thumbnail((512, 512))
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB") img.save(path, "JPEG", quality=75, optimize=True, mode="RGB")
except OSError: except OSError:
print(f"Failed to generate thumbnail for {os.path.join(folder, item)}") logger.error("Failed to generate thumbnail for %s", item, extra={"path": image})
print(f"Failed to generate thumbnail for {image}")
return
else:
logger.debug("thumbnail already exists for %s", item, extra={"path": image})
def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int: def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
@@ -163,6 +179,7 @@ def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
for item in items: for item in items:
if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)) and not item.startswith("."): if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)) and not item.startswith("."):
if item not in _args.exclude_folders and not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders): if item not in _args.exclude_folders and not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
logger.debug("Found folder %s in %s", item, folder)
_total = get_total_folders(os.path.join(folder, item), _args, _total) _total = get_total_folders(os.path.join(folder, item), _args, _total)
return _total return _total
@@ -171,6 +188,7 @@ def main() -> None:
""" """
Main function to process images and generate a static image hosting website. Main function to process images and generate a static image hosting website.
""" """
logger.info("starting builder", extra={"version": VERSION})
thumbnails: List[Tuple[str, str, str, bool]] = [] thumbnails: List[Tuple[str, str, str, bool]] = []
args = parse_arguments(VERSION) args = parse_arguments(VERSION)
@@ -179,12 +197,15 @@ def main() -> None:
lock_file = os.path.join(args.root_directory, ".lock") lock_file = os.path.join(args.root_directory, ".lock")
if os.path.exists(lock_file): if os.path.exists(lock_file):
print("Another instance of this program is running.") print("Another instance of this program is running.")
logger.info("nother instance of this program is running")
exit() exit()
try: try:
Path(lock_file).touch() Path(lock_file).touch()
if args.regenerate_thumbnails: if args.regenerate_thumbnails:
logger.warning("regenerate thumbnails flag is set to true, all thumbnails will be regenerated")
if os.path.exists(os.path.join(args.root_directory, ".thumbnails")): if os.path.exists(os.path.join(args.root_directory, ".thumbnails")):
logger.info("removing old thumbnails folder")
shutil.rmtree(os.path.join(args.root_directory, ".thumbnails")) shutil.rmtree(os.path.join(args.root_directory, ".thumbnails"))
os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True) os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True)
@@ -192,17 +213,21 @@ def main() -> None:
icons(args) icons(args)
if args.generate_webmanifest: if args.generate_webmanifest:
logger.info("generating webmanifest")
print("Generating webmanifest...") print("Generating webmanifest...")
webmanifest(args) webmanifest(args)
if args.non_interactive_mode: if args.non_interactive_mode:
logger.info("generating HTML files")
print("Generating HTML files...") print("Generating HTML files...")
thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION) thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails")
print("Generating thumbnails...") print("Generating thumbnails...")
pool.map(generate_thumbnail, thumbnails) pool.map(generate_thumbnail, thumbnails)
else: else:
pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True) pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True)
logger.info("getting total number of folders to process")
total = get_total_folders(args.root_directory, args) total = get_total_folders(args.root_directory, args)
pbardict["traversingbar"].desc = "Traversing filesystem" pbardict["traversingbar"].desc = "Traversing filesystem"
pbardict["traversingbar"].update(0) pbardict["traversingbar"].update(0)
@@ -211,6 +236,7 @@ def main() -> None:
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION) thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails")
for _ in tqdm( for _ in tqdm(
pool.imap_unordered(generate_thumbnail, thumbnails), pool.imap_unordered(generate_thumbnail, thumbnails),
total=len(thumbnails), total=len(thumbnails),
@@ -222,6 +248,7 @@ def main() -> None:
pass pass
finally: finally:
os.remove(lock_file) os.remove(lock_file)
logger.info("finished builder", extra={"version": VERSION})
return return

View File

@@ -4,21 +4,18 @@ import sys
import time import time
import shutil import shutil
import base64 import base64
import logging
import fileinput import fileinput
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from typing import List from typing import List
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from modules.logger import consolelogger as logger
from modules.css_color import extract_colorscheme from modules.css_color import extract_colorscheme
# Set up logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
def replace_all(file, search_exp, replace_exp): def replace_all(file, search_exp, replace_exp):
for line in fileinput.input(file, inplace=1): for line in fileinput.input(file, inplace=1):
@@ -36,11 +33,14 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
output_file (str): Path where the screenshot will be saved. output_file (str): Path where the screenshot will be saved.
driver (webdriver.Chrome): The Chrome WebDriver instance. driver (webdriver.Chrome): The Chrome WebDriver instance.
""" """
logger.info("taking screenshot for %s", css_file)
try: try:
# Open the HTML file or URL # Open the HTML file or URL
if html_file_path.startswith(("http://", "https://")): if html_file_path.startswith(("http://", "https://")):
logger.info("opening URL: %s", html_file_path)
driver.get(html_file_path) driver.get(html_file_path)
else: else:
logger.info("opening file: %s", html_file_path)
driver.get(f"file://{os.path.abspath(html_file_path)}") driver.get(f"file://{os.path.abspath(html_file_path)}")
# Remove current theme.css # Remove current theme.css
@@ -52,9 +52,11 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
} }
}); });
""" """
logger.info("removing current theme.css")
driver.execute_script(remove_css_script) driver.execute_script(remove_css_script)
with open(css_file, "r", encoding="utf-8") as f: with open(css_file, "r", encoding="utf-8") as f:
logger.info("reading CSS file: %s", css_file)
css_content = f.read() css_content = f.read()
# Extract folder icon content # Extract folder icon content
@@ -65,20 +67,26 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
folder_icon_content = re.sub(r"/\*.*\*/", "", folder_icon_content) folder_icon_content = re.sub(r"/\*.*\*/", "", folder_icon_content)
for match in re.finditer(r"content: (.*);", folder_icon_content): for match in re.finditer(r"content: (.*);", folder_icon_content):
logger.info("found foldericon", extra={"foldericon": folder_icon_content})
folder_icon_content = match.group(1).replace('"', "") folder_icon_content = match.group(1).replace('"', "")
break break
if "url" not in folder_icon_content: if "url" not in folder_icon_content:
logger.info("Reading foldericon svg")
with open(folder_icon_content, "r", encoding="utf-8") as f: with open(folder_icon_content, "r", encoding="utf-8") as f:
svg = f.read() svg = f.read()
if "svg.j2" in folder_icon_content: if "svg.j2" in folder_icon_content:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(css_file) colorscheme = extract_colorscheme(css_file)
for color_key, color_value in colorscheme.items(): for color_key, color_value in colorscheme.items():
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value) svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
logger.info("replaced colors in svg")
svg = urllib.parse.quote(svg) svg = urllib.parse.quote(svg)
css_content = f'{css_head}\n.foldericon {{\n content: url("data:image/svg+xml,{svg}");\n}}\n{css_tail}' css_content = f'{css_head}\n.foldericon {{\n content: url("data:image/svg+xml,{svg}");\n}}\n{css_tail}'
# Encode CSS content as Base64 # Encode CSS content as Base64
logger.info("encoding css content as base64")
encoded_css = base64.b64encode(css_content.encode("utf-8")).decode("utf-8") encoded_css = base64.b64encode(css_content.encode("utf-8")).decode("utf-8")
# Inject CSS into HTML using JavaScript # Inject CSS into HTML using JavaScript
@@ -87,24 +95,28 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
style.innerHTML = atob('{encoded_css}'); style.innerHTML = atob('{encoded_css}');
document.head.appendChild(style); document.head.appendChild(style);
""" """
logger.info("injecting CSS into HTML")
driver.execute_script(apply_css_script) driver.execute_script(apply_css_script)
# Wait for a while to ensure CSS is applied # Wait for a while to ensure CSS is applied
time.sleep(2) # time.sleep(1)
# Move mouse to info # Move mouse to info
logger.info("moving mouse to info")
hoverable = driver.find_element(By.CLASS_NAME, "tooltip") hoverable = driver.find_element(By.CLASS_NAME, "tooltip")
webdriver.ActionChains(driver).move_to_element(hoverable).perform() webdriver.ActionChains(driver).move_to_element(hoverable).perform()
# Capture screenshot # Capture screenshot
logger.info("taking screenshot")
driver.save_screenshot(output_file) driver.save_screenshot(output_file)
logging.info("Screenshot saved to %s", output_file) logger.info("screenshot saved to %s", output_file)
except Exception as e: except Exception as e:
logging.error("Failed to take screenshot for %s: %s", css_file, e) logger.error("failed to take screenshot for %s: %s", css_file, e)
def create_preview(html_file_path: str, css_file: str, previews_folder: str): def create_preview(html_file_path: str, css_file: str, previews_folder: str):
logger.info("creating preview for %s", css_file)
out_file = os.path.basename(css_file).removesuffix(".css") + ".html" out_file = os.path.basename(css_file).removesuffix(".css") + ".html"
urllib.request.urlretrieve(html_file_path, os.path.join(previews_folder, out_file)) urllib.request.urlretrieve(html_file_path, os.path.join(previews_folder, out_file))
basename = os.path.basename(css_file) basename = os.path.basename(css_file)
@@ -127,21 +139,25 @@ def create_preview(html_file_path: str, css_file: str, previews_folder: str):
foldericon = foldericon.replace('"', "") foldericon = foldericon.replace('"', "")
break break
if "url" in foldericon: if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(css_file, os.path.join(path, "previews", basename)) shutil.copyfile(css_file, os.path.join(path, "previews", basename))
else: return
with open(os.path.join(path, foldericon.removeprefix("themes/")), "r", encoding="utf-8") as f: with open(os.path.join(path, foldericon.removeprefix("themes/")), "r", encoding="utf-8") as f:
svg = f.read() logger.info("Reading foldericon svg")
if "svg.j2" in foldericon: svg = f.read()
colorscheme = extract_colorscheme(css_file) if "svg.j2" in foldericon:
svg = svg.replace("{{ color1 }}", colorscheme["color1"]) logger.info("foldericon in theme file is a jinja2 template")
svg = svg.replace("{{ color2 }}", colorscheme["color2"]) colorscheme = extract_colorscheme(css_file)
svg = svg.replace("{{ color3 }}", colorscheme["color3"]) for color_key, color_value in colorscheme.items():
svg = svg.replace("{{ color4 }}", colorscheme["color4"]) svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
svg = urllib.parse.quote(svg) logger.info("replaced colors in svg")
if os.path.exists(os.path.join(path, "previews", basename)): svg = urllib.parse.quote(svg)
os.remove(os.path.join(path, "previews", basename)) if os.path.exists(os.path.join(path, "previews", basename)):
with open(os.path.join(path, "previews", basename), "x", encoding="utf-8") as f: os.remove(os.path.join(path, "previews", basename))
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail) with open(os.path.join(path, "previews", basename), "x", encoding="utf-8") as f:
logger.info("writing theme file")
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
logger.info("preview created for %s", css_file)
def write_readme(directory_path: str, themes: List[str]) -> None: def write_readme(directory_path: str, themes: List[str]) -> None:
@@ -155,6 +171,7 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
readme_path = os.path.join(directory_path, "README.md") readme_path = os.path.join(directory_path, "README.md")
try: try:
with open(readme_path, "r", encoding="utf-8") as f: with open(readme_path, "r", encoding="utf-8") as f:
logger.info("reading README.md", extra={"file": readme_path})
readme = f.read() readme = f.read()
readme_head = readme.split("## Previews of included themes")[0] readme_head = readme.split("## Previews of included themes")[0]
@@ -162,14 +179,15 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
readme_head += "".join([f"\n### {theme}\n\n![{theme}](screenshots/{theme}.png)\n" for theme in themes]) readme_head += "".join([f"\n### {theme}\n\n![{theme}](screenshots/{theme}.png)\n" for theme in themes])
with open(readme_path, "w", encoding="utf-8") as f: with open(readme_path, "w", encoding="utf-8") as f:
logger.info("writing README.md", extra={"file": readme_path})
f.write(readme_head) f.write(readme_head)
logging.info("README.md updated with previews of included themes.") logger.info("README.md updated with previews of included themes.")
except FileNotFoundError: except FileNotFoundError:
logging.error("README.md not found in %s", directory_path) logger.error("README.md not found in %s", directory_path)
except Exception as e: except Exception as e:
logging.error("Failed to write README.md: %s", e) logger.error("failed to write README.md: %s", e)
def write_index(directory_path: str, themes: List[str]) -> None: def write_index(directory_path: str, themes: List[str]) -> None:
@@ -198,7 +216,7 @@ def main(directory_path: str, html_file_path: str) -> None:
html_file_path (str): Path to the HTML file or URL for rendering. html_file_path (str): Path to the HTML file or URL for rendering.
""" """
if not os.path.exists(directory_path): if not os.path.exists(directory_path):
logging.error('Error: Folder path "%s" does not exist.', directory_path) logger.error('Error: Folder path "%s" does not exist.', directory_path)
return return
# Setup Chrome options # Setup Chrome options
@@ -207,8 +225,9 @@ def main(directory_path: str, html_file_path: str) -> None:
chrome_options.add_argument("--window-size=1920,1080") # Set window size to at least 1920x1080 chrome_options.add_argument("--window-size=1920,1080") # Set window size to at least 1920x1080
# Initialize Chrome WebDriver # Initialize Chrome WebDriver
chromedriver_path = "/usr/bin/chromedriver" # Replace with your actual path chromedriver_path = "/usr/bin/chromedriver"
service = Service(chromedriver_path) service = Service(chromedriver_path)
logger.info("Using chromedriver at %s", chromedriver_path, extra={"chrome_options": chrome_options})
driver = webdriver.Chrome(service=service, options=chrome_options) driver = webdriver.Chrome(service=service, options=chrome_options)
try: try:
@@ -235,13 +254,16 @@ def main(directory_path: str, html_file_path: str) -> None:
write_index(directory_path, themes) write_index(directory_path, themes)
finally: finally:
logger.info("closing chrome webdriver")
driver.quit() driver.quit()
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 3: if len(sys.argv) != 3:
logging.error("Usage: python script_name.py directory_path html_file_path") logger.error("Usage: python script_name.py directory_path html_file_path")
else: else:
dir_path = sys.argv[1] dir_path = sys.argv[1]
html_path = sys.argv[2] html_path = sys.argv[2]
logger.info("Starting script", extra={"directory_path": dir_path, "html_file_path": html_path})
main(dir_path, html_path) main(dir_path, html_path)
logger.info("Done!", extra={"directory_path": dir_path})

View File

@@ -4,6 +4,8 @@ import os
import argparse import argparse
from rich_argparse import RichHelpFormatter, HelpPreviewAction from rich_argparse import RichHelpFormatter, HelpPreviewAction
from modules.logger import logger
if __package__ is None: if __package__ is None:
PACKAGE = "" PACKAGE = ""
else: else:
@@ -129,4 +131,5 @@ def parse_arguments(version: str) -> Args:
use_fancy_folders=parsed_args.use_fancy_folders, use_fancy_folders=parsed_args.use_fancy_folders,
web_root_url=parsed_args.web_root_url, web_root_url=parsed_args.web_root_url,
) )
logger.debug("parsed arguments", extra={"args": _args.to_dict()})
return _args return _args

View File

@@ -2,6 +2,8 @@ import re
import colorsys import colorsys
from typing import Dict from typing import Dict
from modules.logger import logger
def extract_colorscheme(theme_path: str) -> Dict[str, str]: def extract_colorscheme(theme_path: str) -> Dict[str, str]:
""" """
@@ -17,6 +19,7 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
Dict[str, str] Dict[str, str]
Dictionary containing color scheme variables and their hexadecimal values. Dictionary containing color scheme variables and their hexadecimal values.
""" """
logger.info("extracting color scheme from theme file", extra={"theme_path": theme_path})
pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);" pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);"
colorscheme = {} colorscheme = {}
@@ -30,6 +33,7 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
color_value = match[1] color_value = match[1]
hex_color_value = css_color_to_hex(color_value) hex_color_value = css_color_to_hex(color_value)
colorscheme[variable_name] = hex_color_value colorscheme[variable_name] = hex_color_value
logger.debug("extracted variable", extra={"variable": variable_name, "value": hex_color_value})
return colorscheme return colorscheme
@@ -86,10 +90,12 @@ def css_color_to_hex(css_color: str) -> str:
# Helper function to convert RGB tuple to hexadecimal string # Helper function to convert RGB tuple to hexadecimal string
def rgb_to_hex(rgb: tuple[int, int, int]) -> str: def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
logger.debug("converting rgb tuple to hex string", extra={"rgb": rgb})
return "#{:02x}{:02x}{:02x}".format(*rgb) return "#{:02x}{:02x}{:02x}".format(*rgb)
# Helper function to convert HSL tuple to RGB tuple # Helper function to convert HSL tuple to RGB tuple
def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]: def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]:
logger.debug("converting hsl tuple to rgb tuple", extra={"hsl": hsl})
return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100)) return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100))
# Regular expression pattern to match CSS colors # Regular expression pattern to match CSS colors
@@ -103,6 +109,7 @@ def css_color_to_hex(css_color: str) -> str:
match = color_pattern.match(css_color.strip()) match = color_pattern.match(css_color.strip())
if not match: if not match:
logger.error("invalid CSS color format", extra={"css_color": css_color})
raise ValueError("Invalid CSS color format") raise ValueError("Invalid CSS color format")
groups = match.groupdict() groups = match.groupdict()
@@ -119,8 +126,10 @@ def css_color_to_hex(css_color: str) -> str:
b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"]) b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"])
a = float(groups["a"]) if groups["a"] else 1.0 a = float(groups["a"]) if groups["a"] else 1.0
if a < 1.0: if a < 1.0:
logger.debug("converting rgba color to hex", extra={"color": css_color, "r": r, "g": g, "b": b, "a": a})
return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255)) return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255))
else: else:
logger.debug("converting rgb color to hex", extra={"color": css_color, "r": r, "g": g, "b": b})
return rgb_to_hex((r, g, b)) return rgb_to_hex((r, g, b))
elif groups["hsl"]: elif groups["hsl"]:
@@ -130,8 +139,10 @@ def css_color_to_hex(css_color: str) -> str:
a = float(groups["a"]) if groups["a"] else 1.0 a = float(groups["a"]) if groups["a"] else 1.0
rgb_color = hsl_to_rgb((h, s, l)) rgb_color = hsl_to_rgb((h, s, l))
if a < 1.0: if a < 1.0:
logger.debug("converting hsla color to hex", extra={"color": css_color, "hsl": (h, s, l), "a": a})
return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255)) return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255))
else: else:
logger.debug("converting hsl color to hex", extra={"color": css_color, "hsl": (h, s, l)})
return rgb_to_hex(rgb_color) return rgb_to_hex(rgb_color)
# fmt: off # fmt: off
@@ -182,7 +193,9 @@ def css_color_to_hex(css_color: str) -> str:
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff', 'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32' 'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32'
} }
logger.debug("parsing css color string", extra={"css_color": css_color})
return named_colors[groups['name'].lower()] return named_colors[groups['name'].lower()]
# fmt: on # fmt: on
logger.error("invalid CSS color format", extra={"css_color": css_color})
raise ValueError("Invalid CSS color format") raise ValueError("Invalid CSS color format")

View File

@@ -6,9 +6,10 @@ from typing import Any, Dict, List, Tuple
import numpy as np import numpy as np
from tqdm.auto import tqdm from tqdm.auto import tqdm
from PIL import Image, ExifTags from PIL import Image, ExifTags, TiffImagePlugin
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from modules.logger import logger
import modules.cclicense as cclicense import modules.cclicense as cclicense
from modules.argumentparser import Args from modules.argumentparser import Args
@@ -45,17 +46,20 @@ def initialize_sizelist(folder: str) -> Dict[str, Dict[str, int]]:
sizelist = {} sizelist = {}
sizelist_path = os.path.join(folder, ".sizelist.json") sizelist_path = os.path.join(folder, ".sizelist.json")
if not os.path.exists(sizelist_path): if not os.path.exists(sizelist_path):
logger.info("creating new size list file", extra={"file": sizelist_path})
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile: with open(sizelist_path, "x", encoding="utf-8") as sizelistfile:
sizelistfile.write("{}") sizelistfile.write("{}")
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile: with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile:
logger.info("reading size list file", extra={"file": sizelist_path})
try: try:
sizelist = json.loads(sizelistfile.read()) sizelist = json.loads(sizelistfile.read())
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
logger.warning("invalid JSON in size list file", extra={"file": sizelist_path})
sizelist = {} sizelist = {}
return sizelist return sizelist
def update_sizelist(sizelist: Dict[str, Dict[str, int]], folder: str) -> None: def update_sizelist(sizelist: Dict[str, Dict[str, Any]], folder: str) -> None:
""" """
Updates the size list JSON file. Updates the size list JSON file.
@@ -64,11 +68,13 @@ def update_sizelist(sizelist: Dict[str, Dict[str, int]], folder: str) -> None:
folder (str): The folder in which the size list file is located. folder (str): The folder in which the size list file is located.
""" """
sizelist_path = os.path.join(folder, ".sizelist.json") sizelist_path = os.path.join(folder, ".sizelist.json")
if sizelist != {}: if sizelist:
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile: with open(sizelist_path, "w", encoding="utf-8") as sizelistfile:
logger.info("writing size list file", extra={"file": sizelist_path})
sizelistfile.write(json.dumps(sizelist, indent=4)) sizelistfile.write(json.dumps(sizelist, indent=4))
else: else:
if os.path.exists(sizelist_path): if os.path.exists(sizelist_path):
logger.info("deleting empty size list file", extra={"file": sizelist_path})
os.remove(sizelist_path) os.remove(sizelist_path)
@@ -84,12 +90,36 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
Dict[str, Any]: A dictionary containing image width, height, and EXIF data. Dict[str, Any]: A dictionary containing image width, height, and EXIF data.
""" """
with Image.open(os.path.join(folder, item)) as img: with Image.open(os.path.join(folder, item)) as img:
logger.info("extracting image information", extra={"file": item})
exif = img.getexif() exif = img.getexif()
width, height = img.size width, height = img.size
exifdata = {ExifTags.TAGS.get(key, key): val for key, val in exif.items()} if exif:
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]: ifd = exif.get_ifd(ExifTags.IFD.Exif)
width, height = height, width exifdatas = dict(exif.items()) | ifd
return {"width": width, "height": height} exifdata = {}
for tag_id in exifdatas:
tag = ExifTags.TAGS.get(tag_id, tag_id)
content = exifdatas.get(tag_id)
if isinstance(content, bytes):
content = content.hex(" ")
if isinstance(content, TiffImagePlugin.IFDRational):
content = content.limit_rational(1000000)
if isinstance(content, tuple):
newtuple = ()
for i in content:
if isinstance(i, TiffImagePlugin.IFDRational):
newtuple = newtuple + (i.limit_rational(1000000),)
if newtuple:
content = newtuple
exifdata[tag] = content
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
width, height = height, width
for key in ["PrintImageMatching", "UserComment", "MakerNote"]:
if key in exifdata:
del exifdata[key]
return {"width": width, "height": height, "exifdata": exifdata}
else:
return {"width": width, "height": height}
def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]: def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]:
@@ -144,6 +174,11 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
_args (Args): Parsed command line arguments. _args (Args): Parsed command line arguments.
raw (List[str]): Raw image file names. raw (List[str]): Raw image file names.
""" """
logger.info("processing folder", extra={"folder": folder})
if _args.regenerate_thumbnails:
if os.path.exists(os.path.join(folder, ".sizelist.json")):
logger.info("removing .sizelist.json", extra={"folder": folder})
os.remove(os.path.join(folder, ".sizelist.json"))
sizelist = initialize_sizelist(folder) sizelist = initialize_sizelist(folder)
items = sorted(os.listdir(folder)) items = sorted(os.listdir(folder))
@@ -159,6 +194,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
if not _args.non_interactive_mode: if not _args.non_interactive_mode:
pbardict[folder] = tqdm(total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True) pbardict[folder] = tqdm(total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True)
logger.info("processing contents", extra={"folder": folder})
for item in items: for item in items:
if item not in EXCLUDES and not item.startswith("."): if item not in EXCLUDES and not item.startswith("."):
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
@@ -182,6 +218,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
create_html_file(folder, title, foldername, images, subfolders, _args, version) create_html_file(folder, title, foldername, images, subfolders, _args, version)
else: else:
if os.path.exists(os.path.join(folder, "index.html")): if os.path.exists(os.path.join(folder, "index.html")):
logger.info("removing existing index.html", extra={"folder": folder})
os.remove(os.path.join(folder, "index.html")) os.remove(os.path.join(folder, "index.html"))
if not _args.non_interactive_mode: if not _args.non_interactive_mode:
@@ -198,6 +235,7 @@ def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
""" """
thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername) thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername)
if not os.path.exists(thumbnails_path): if not os.path.exists(thumbnails_path):
logger.info("creating thumbnail folder", extra={"path": thumbnails_path})
os.mkdir(thumbnails_path) os.mkdir(thumbnails_path)
@@ -229,6 +267,7 @@ def process_info_file(folder: str, item: str) -> None:
item (str): The info file name. item (str): The info file name.
""" """
with open(os.path.join(folder, item), encoding="utf-8") as f: with open(os.path.join(folder, item), encoding="utf-8") as f:
logger.info("processing info file", extra={"path": os.path.join(folder, item)})
info[urllib.parse.quote(folder)] = f.read() info[urllib.parse.quote(folder)] = f.read()
@@ -258,6 +297,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML. subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML.
_args (Args): Parsed command line arguments. _args (Args): Parsed command line arguments.
""" """
html_file = os.path.join(folder, "index.html")
logger.info("generating html file with jinja2", extra={"path": html_file})
image_chunks = np.array_split(images, 8) if images else [] image_chunks = np.array_split(images, 8) if images else []
header = os.path.basename(folder) or title header = os.path.basename(folder) or title
parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}" parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}"
@@ -297,7 +338,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
version=version, version=version,
) )
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f: with open(html_file, "w", encoding="utf-8") as f:
logger.info("writing html file", extra={"path": html_file})
f.write(content) f.write(content)

110
modules/logger.py Normal file
View File

@@ -0,0 +1,110 @@
"""
loggerdabn.py
This module provides functionality for setting up a centralized logging system using the
`logging` library and the `python-json-logger` to output logs in JSON format. It handles
log rotation by renaming old log files and saving them based on the first timestamp entry.
Functions:
- log_format(keys): Generates the logging format string based on the list of keys.
- rotate_log_file(): Handles renaming the existing log file to a timestamp-based name.
- setup_logger(): Configures the logging system, applies a JSON format, and returns a logger instance.
- setup_consolelogger(): Configures the logging system to output logs in console format.
"""
import logging
import os
import json
from datetime import datetime
from pythonjsonlogger import jsonlogger
# Constants for file paths and exclusions
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
LOG_DIR = os.path.join(SCRIPTDIR, "logs")
LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl")
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
def log_format(keys):
"""
Generates a list of format strings based on the given keys.
Args:
keys (list): A list of string keys that represent the log attributes (e.g., 'asctime', 'levelname').
Returns:
list: A list of formatted strings for each key, in the format "%(key)s".
"""
return [f"%({i})s" for i in keys]
def rotate_log_file():
"""
Renames the existing 'latest.jsonl' file to a timestamped file based on the first log entry's asctime.
If 'latest.jsonl' exists, it's renamed to the first timestamp found in the log entry.
"""
if os.path.exists(LATEST_LOG_FILE):
with open(LATEST_LOG_FILE, "r", encoding="utf-8") as f:
first_line = f.readline()
try:
first_log = json.loads(first_line)
first_timestamp = first_log.get("asctime")
first_timestamp = first_timestamp.split(",")[0]
except (json.JSONDecodeError, KeyError):
first_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_")
old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl")
os.rename(LATEST_LOG_FILE, old_log_filename)
def setup_logger():
"""
Configures the logging system with a custom format and outputs logs in JSON format.
The logger will write to the 'logs/latest.jsonl' file, and it will include
multiple attributes such as the time of logging, the filename, function name, log level, etc.
If 'latest.jsonl' already exists, it will be renamed to a timestamped file before creating a new one.
Returns:
logging.Logger: A configured logger instance that can be used to log messages.
"""
rotate_log_file()
_logger = logging.getLogger()
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
custom_format = " ".join(log_format(supported_keys))
formatter = jsonlogger.JsonFormatter(custom_format)
log_handler = logging.FileHandler(LATEST_LOG_FILE)
log_handler.setFormatter(formatter)
_logger.addHandler(log_handler)
_logger.setLevel(logging.INFO)
return _logger
def setup_consolelogger():
"""
Configures the logging system to output logs in console format.
Returns:
logging.Logger: A configured logger instance that can be used to log messages.
"""
_logger = setup_logger()
_logger.addHandler(logging.StreamHandler())
return _logger
logger = setup_logger()
consolelogger = setup_consolelogger()

View File

@@ -13,6 +13,7 @@ try:
except ImportError: except ImportError:
SVGSUPPORT = False SVGSUPPORT = False
from modules.logger import logger
from modules.argumentparser import Args from modules.argumentparser import Args
from modules.css_color import extract_theme_color, extract_colorscheme from modules.css_color import extract_theme_color, extract_colorscheme
@@ -55,6 +56,7 @@ def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str:
svg = env.get_template("icon.svg.j2") svg = env.get_template("icon.svg.j2")
content = svg.render(colorscheme=colorscheme) content = svg.render(colorscheme=colorscheme)
with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f: 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) f.write(content)
return content return content
@@ -73,6 +75,7 @@ def save_png_icon(content: str, iconspath: str) -> None:
tmpimg = BytesIO() tmpimg = BytesIO()
cairosvg.svg2png(bytestring=content, write_to=tmpimg) cairosvg.svg2png(bytestring=content, write_to=tmpimg)
with Image.open(tmpimg) as iconfile: with Image.open(tmpimg) as iconfile:
logger.info("saving png icon", extra={"iconspath": iconspath})
iconfile.save(os.path.join(iconspath, "icon.png")) iconfile.save(os.path.join(iconspath, "icon.png"))
@@ -87,9 +90,11 @@ def generate_favicon(iconspath: str, root_directory: str) -> None:
root_directory : str root_directory : str
Root directory of the project where the favicon will be saved. 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")}' favicon = os.path.join(root_directory, ".static", "favicon.ico")
logger.info("generating favicon with imagemagick", extra={"iconspath": iconspath, "favicon": favicon})
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {favicon}'
if not shutil.which("magick"): 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")}' command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {favicon}'
os.system(command) os.system(command)
@@ -102,12 +107,14 @@ def icons(_args: Args) -> None:
_args : Args _args : Args
Parsed command-line arguments. Parsed command-line arguments.
""" """
print("Generating icons...")
iconspath = os.path.join(_args.root_directory, ".static", "icons") 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) colorscheme = extract_colorscheme(_args.theme_path)
content = render_svg_icon(colorscheme, iconspath) content = render_svg_icon(colorscheme, iconspath)
if not SVGSUPPORT: if not SVGSUPPORT:
print("Please install cairosvg to generate favicon from svg icon.") print("Please install cairosvg to generate favicon from svg icon.")
logger.error("svg support not available")
return return
save_png_icon(content, iconspath) save_png_icon(content, iconspath)
generate_favicon(iconspath, _args.root_directory) generate_favicon(iconspath, _args.root_directory)
@@ -135,6 +142,7 @@ def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, s
theme_color=colors["theme_color"], theme_color=colors["theme_color"],
) )
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f: 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) f.write(content)
@@ -156,6 +164,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
List[Icon] List[Icon]
List of icons created from the SVG file. List of icons created from the SVG file.
""" """
logger.info("creating icons for web application", extra={"iconspath": iconspath})
svg = [file for file in files if file.endswith(".svg")][0] svg = [file for file in files if file.endswith(".svg")][0]
icon_list = [ 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": "maskable"},
@@ -165,6 +174,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
tmpimg = BytesIO() tmpimg = BytesIO()
sizes = size.split("x") sizes = size.split("x")
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png") iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
logger.info("converting svg to png", extra={"iconpath": iconpath, "size": size})
cairosvg.svg2png( cairosvg.svg2png(
url=os.path.join(iconspath, svg), url=os.path.join(iconspath, svg),
write_to=tmpimg, write_to=tmpimg,
@@ -173,6 +183,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
scale=1, scale=1,
) )
with Image.open(tmpimg) as iconfile: with Image.open(tmpimg) as iconfile:
logger.info("saving png file", extra={"iconpath": iconpath})
iconfile.save(iconpath, format="PNG") iconfile.save(iconpath, format="PNG")
icon_list.append( icon_list.append(
{ {
@@ -215,6 +226,7 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
continue continue
with Image.open(os.path.join(iconspath, icon)) as iconfile: with Image.open(os.path.join(iconspath, icon)) as iconfile:
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}" iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
logger.info("using icon", extra={"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": "maskable"})
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"}) icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
return icon_list return icon_list
@@ -231,14 +243,11 @@ def webmanifest(_args: Args) -> None:
""" """
iconspath = os.path.join(_args.root_directory, ".static", "icons") iconspath = os.path.join(_args.root_directory, ".static", "icons")
files = os.listdir(iconspath) files = os.listdir(iconspath)
icon_list = ( 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)
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: if not icon_list:
print("No icons found in the static/icons folder!") print("No icons found in the static/icons folder!")
logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath})
return return
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css")) colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))

View File

@@ -3,6 +3,7 @@ Jinja2==3.1.4
numpy==2.0.0 numpy==2.0.0
pillow==10.4.0 pillow==10.4.0
pyinstaller==6.9.0 pyinstaller==6.9.0
python-json-logger==2.0.7
rich-argparse==1.5.2 rich-argparse==1.5.2
setuptools==70.3.0 setuptools==70.3.0
tqdm==4.66.4 tqdm==4.66.4

BIN
test/example/DSC03470.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

BIN
test/example/DSC03508.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

View File

@@ -5,22 +5,22 @@
--color2: #b22222; --color2: #b22222;
--color3: #ff4500; --color3: #ff4500;
--color4: #6e0000; --color4: #6e0000;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
.navbar { .navbar {
font-weight: bold; font-weight: bold;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color1); background-color: var(--color1);
font-weight: 900; font-weight: 900;
} }
.navbar li a { .navbar li a {
font-weight: 800; font-weight: 800;
color: var(--bcolor1); color: var(--bcolor3);
} }
/* Change the link color on hover */ /* Change the link color on hover */
@@ -30,7 +30,7 @@
} }
.footer { .footer {
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color3); background-color: var(--color3);
font-weight: 700; font-weight: 700;
} }

View File

@@ -5,22 +5,22 @@
--color2: #1346a4; --color2: #1346a4;
--color3: #0e3377; --color3: #0e3377;
--color4: #3674e7; --color4: #3674e7;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
.navbar { .navbar {
font-weight: bold; font-weight: bold;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color1); background-color: var(--color1);
font-weight: 900; font-weight: 900;
} }
.navbar li a { .navbar li a {
font-weight: 800; font-weight: 800;
color: var(--bcolor1); color: var(--bcolor3);
} }
/* Change the link color on hover */ /* Change the link color on hover */
@@ -30,7 +30,7 @@
} }
.footer { .footer {
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color3); background-color: var(--color3);
font-weight: 700; font-weight: 700;
} }

View File

@@ -1,6 +1,6 @@
<svg width='64' height='64' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'> <svg width='64' height='64' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M20 6H10L8 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6Z' fill='{{ color1 }}' /> <path d='M20 6H10L8 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6Z' fill='{{ color1 }}' />
<path d='M4 4H8L10 6H20C21.1046 6 22 6.89543 22 8H2C2 6.89543 2.89543 6 4 6V4Z' fill='{{ color4 }}' /> <path d='M4 4H8L10 6H20C21.1046 6 22 6.89543 22 8H2C2 6.89543 2.89543 6 4 6V4Z' fill='{{ color4 }}' />
<path d='M10 6H14L12 4H8L10 6Z' fill='{{ color2 }}' /> <path d='M10 6H14L12 4H8L10 6Z' fill='{{ color3 }}' />
<path d='M14 6H18L16 4H12L14 6Z' fill='{{ color3 }}' /> <path d='M14 6H18L16 4H12L14 6Z' fill='{{ color2 }}' />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 493 B

View File

@@ -5,22 +5,22 @@
--color2: #008000; --color2: #008000;
--color3: #32cd32; --color3: #32cd32;
--color4: #004300; --color4: #004300;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
.navbar { .navbar {
font-weight: bold; font-weight: bold;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color1); background-color: var(--color1);
font-weight: 900; font-weight: 900;
} }
.navbar li a { .navbar li a {
font-weight: 800; font-weight: 800;
color: var(--bcolor1); color: var(--bcolor3);
} }
/* Change the link color on hover */ /* Change the link color on hover */
@@ -30,7 +30,7 @@
} }
.footer { .footer {
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color3); background-color: var(--color3);
font-weight: 700; font-weight: 700;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -5,9 +5,9 @@
--color2: #ffd700; --color2: #ffd700;
--color3: #ffe135; --color3: #ffe135;
--color4: #ce8c00; --color4: #ce8c00;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
@@ -60,7 +60,7 @@
.row a { .row a {
font-weight: 800; font-weight: 800;
color: var(--color2); color: var(--color1);
text-decoration: none; text-decoration: none;
} }
@@ -70,7 +70,7 @@
.tooltiptext { .tooltiptext {
font-weight: 600; font-weight: 600;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }