Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3cb5269985
|
|||
|
3bde09aebc
|
|||
|
effc05826a
|
|||
|
8ec9701aa9
|
|||
| a95a6e1722 |
3
.gitignore
vendored
@@ -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
|
||||||
@@ -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",
|
||||||
|
|||||||
33
builder.py
@@ -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,26 @@ 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:
|
||||||
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
|
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
|
||||||
|
logger.info("foldericon in theme file, using it")
|
||||||
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"])
|
svg = svg.replace("{{ color1 }}", colorscheme["color1"])
|
||||||
svg = svg.replace("{{ color2 }}", colorscheme["color2"])
|
svg = svg.replace("{{ color2 }}", colorscheme["color2"])
|
||||||
svg = svg.replace("{{ color3 }}", colorscheme["color3"])
|
svg = svg.replace("{{ color3 }}", colorscheme["color3"])
|
||||||
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
|
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
|
||||||
|
logger.info("replaced colors in svg")
|
||||||
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 +131,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 +140,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 +181,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 +190,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 +199,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 +215,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 +238,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 +250,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,8 @@ 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})
|
||||||
|
logger.info("extracted color scheme", extra={"colorscheme": colorscheme})
|
||||||
|
|
||||||
return colorscheme
|
return colorscheme
|
||||||
|
|
||||||
@@ -86,10 +91,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 +110,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 +127,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 +140,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 +194,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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
96
modules/logger.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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()
|
||||||
|
app_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)
|
||||||
|
|
||||||
|
app_logger.addHandler(log_handler)
|
||||||
|
app_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
return app_logger
|
||||||
|
|
||||||
|
|
||||||
|
logger = setup_logger()
|
||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
After Width: | Height: | Size: 20 MiB |
BIN
test/example/DSC03508.JPG
Executable file
|
After Width: | Height: | Size: 20 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |