mirror of
https://github.com/greflm13/StaticGalleryBuilder.git
synced 2026-02-05 02:59:27 +00:00
separated into modules and documented everything
This commit is contained in:
@@ -35,6 +35,8 @@
|
||||
"themes/default.css",
|
||||
"--use-fancy-folders",
|
||||
"--web-manifest",
|
||||
"-l",
|
||||
"cc-by-nc-sa",
|
||||
"-n",
|
||||
"-m",
|
||||
"-r"
|
||||
|
||||
463
builder.py
463
builder.py
@@ -1,113 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import argparse
|
||||
import urllib.parse
|
||||
import shutil
|
||||
import fnmatch
|
||||
import json
|
||||
import re
|
||||
from multiprocessing import Pool
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
import numpy as np
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from tqdm.auto import tqdm
|
||||
from PIL import Image, ImageOps, ExifTags
|
||||
from rich_argparse import RichHelpFormatter, HelpPreviewAction
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
try:
|
||||
import cairosvg
|
||||
from io import BytesIO
|
||||
|
||||
SVGSUPPORT = True
|
||||
except ImportError:
|
||||
SVGSUPPORT = False
|
||||
|
||||
import cclicense
|
||||
from modules.argumentparser import parse_arguments, Args
|
||||
from modules.svg_handling import icons, webmanifest
|
||||
from modules.generate_html import list_folder, EXCLUDES
|
||||
|
||||
# fmt: off
|
||||
# Constants
|
||||
STATIC_FILES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "files")
|
||||
FAVICON_PATH = ".static/favicon.ico"
|
||||
GLOBAL_CSS_PATH = ".static/global.css"
|
||||
DEFAULT_THEME_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "themes", "default.css")
|
||||
DEFAULT_AUTHOR = "Author"
|
||||
VERSION = "1.10.0"
|
||||
RAW_EXTENSIONS = [".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw", ".obm", ".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl", ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".tif", ".tiff", ".x3f"]
|
||||
IMG_EXTENSIONS = [".jpg", ".jpeg"]
|
||||
EXCLUDES = [".lock", "index.html", "manifest.json", ".sizelist.json", ".thumbnails", ".static"]
|
||||
VERSION = "2.0.0"
|
||||
RAW_EXTENSIONS = [
|
||||
".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr",
|
||||
".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos",
|
||||
".mrw", ".nef", ".nrw", ".obm", ".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl",
|
||||
".rw2", ".rwz", ".sr2", ".srf", ".srw", ".tif", ".tiff", ".x3f"
|
||||
]
|
||||
IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"]
|
||||
NOT_LIST = ["*/Galleries/*", "Archives"]
|
||||
ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"]
|
||||
# fmt: on
|
||||
|
||||
# Initialize Jinja2 environment
|
||||
env = Environment(loader=FileSystemLoader(os.path.join(os.path.abspath(os.path.dirname(__file__)), "templates")))
|
||||
thumbnails: List[Tuple[str, str]] = []
|
||||
info: Dict[str, str] = {}
|
||||
pbardict: Dict[str, tqdm] = {}
|
||||
|
||||
|
||||
class Icon:
|
||||
src: str
|
||||
type: str
|
||||
sizes: str
|
||||
purpose: str
|
||||
def init_globals(_args: Args, raw: List[str]) -> Tuple[Args, List[str]]:
|
||||
"""
|
||||
Initialize global variables and set default values for arguments.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
raw : List[str]
|
||||
List of raw file extensions.
|
||||
|
||||
class Args:
|
||||
author_name: str
|
||||
exclude_folders: List[str]
|
||||
file_extensions: List[str]
|
||||
generate_webmanifest: bool
|
||||
ignore_other_files: bool
|
||||
license_type: Optional[str]
|
||||
non_interactive_mode: bool
|
||||
regenerate_thumbnails: bool
|
||||
root_directory: str
|
||||
site_title: str
|
||||
theme_path: str
|
||||
use_fancy_folders: bool
|
||||
web_root_url: str
|
||||
|
||||
|
||||
# fmt: off
|
||||
def parse_arguments() -> Args:
|
||||
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter)
|
||||
parser.add_argument("--exclude-folder", help="Folders to exclude from processing, globs supported (can be specified multiple times).", action="append", dest="exclude_folders", metavar="FOLDER")
|
||||
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg")
|
||||
parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
|
||||
parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
|
||||
parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders")
|
||||
parser.add_argument("--version", action="version", version=f"%(prog)s {VERSION}")
|
||||
parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
|
||||
parser.add_argument("-e", "--file-extensions", help="File extensions to include (can be specified multiple times).", action="append", dest="file_extensions", metavar="EXTENSION")
|
||||
parser.add_argument("-l", "--license-type", help="Specify the license type for the images.", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE")
|
||||
parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest")
|
||||
parser.add_argument("-n", "--non-interactive-mode", help="Run in non-interactive mode, disabling progress bars.", action="store_true", default=False, dest="non_interactive_mode")
|
||||
parser.add_argument("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT")
|
||||
parser.add_argument("-r", "--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
|
||||
parser.add_argument("-t", "--site-title", help="Title of the image hosting site.", required=True, type=str, dest="site_title", metavar="TITLE")
|
||||
parser.add_argument("-w", "--web-root-url", help="Base URL of the web root for the image hosting site.", required=True, type=str, dest="web_root_url", metavar="URL")
|
||||
parsed_args = parser.parse_args()
|
||||
_args = Args()
|
||||
_args.author_name = parsed_args.author_name
|
||||
_args.exclude_folders = parsed_args.exclude_folders
|
||||
_args.file_extensions = parsed_args.file_extensions
|
||||
_args.generate_webmanifest = parsed_args.generate_webmanifest
|
||||
_args.ignore_other_files = parsed_args.ignore_other_files
|
||||
_args.license_type = parsed_args.license_type
|
||||
_args.non_interactive_mode = parsed_args.non_interactive_mode
|
||||
_args.regenerate_thumbnails = parsed_args.regenerate_thumbnails
|
||||
_args.root_directory = parsed_args.root_directory
|
||||
_args.site_title = parsed_args.site_title
|
||||
_args.theme_path = parsed_args.theme_path
|
||||
_args.use_fancy_folders = parsed_args.use_fancy_folders
|
||||
_args.web_root_url = parsed_args.web_root_url
|
||||
return _args
|
||||
# fmt: on
|
||||
|
||||
|
||||
def init_globals(_args: Args, raw: list[str]) -> Args:
|
||||
Returns:
|
||||
--------
|
||||
Tuple[Args, List[str]]
|
||||
Updated arguments and raw file extensions.
|
||||
"""
|
||||
if not _args.file_extensions:
|
||||
_args.file_extensions = IMG_EXTENSIONS
|
||||
if not _args.exclude_folders:
|
||||
@@ -121,315 +59,105 @@ def init_globals(_args: Args, raw: list[str]) -> Args:
|
||||
|
||||
|
||||
def copy_static_files(_args: Args) -> None:
|
||||
if os.path.exists(os.path.join(_args.root_directory, ".static")):
|
||||
"""
|
||||
Copy static files to the root directory.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
"""
|
||||
static_dir = os.path.join(_args.root_directory, ".static")
|
||||
if os.path.exists(static_dir):
|
||||
print("Removing existing .static folder...")
|
||||
shutil.rmtree(os.path.join(_args.root_directory, ".static"))
|
||||
if not os.path.exists(os.path.join(_args.root_directory, ".static")):
|
||||
shutil.rmtree(static_dir)
|
||||
|
||||
print("Copying static files...")
|
||||
shutil.copytree(STATIC_FILES_DIR, os.path.join(_args.root_directory, ".static"), dirs_exist_ok=True)
|
||||
shutil.copyfile(_args.theme_path, os.path.join(_args.root_directory, ".static", "theme.css"))
|
||||
|
||||
|
||||
def icons(_args: Args) -> None:
|
||||
print("Generating icons...")
|
||||
pattern = r"--color([1-4]):\s*(#[0-9a-f]+);"
|
||||
colorscheme = {}
|
||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||
with open(_args.theme_path, "r") as f:
|
||||
filecontent = f.read()
|
||||
matches = re.findall(pattern, filecontent)
|
||||
for match in matches:
|
||||
colorscheme["color" + match[0]] = match[1]
|
||||
svg = env.get_template("icon.svg.j2")
|
||||
content = svg.render(colorscheme=colorscheme)
|
||||
with open(os.path.join(iconspath, "icon.svg"), "w+") as f:
|
||||
f.write(content)
|
||||
if not SVGSUPPORT:
|
||||
print("Please install cairosvg to generate favicon from svg icon.")
|
||||
return
|
||||
tmpimg = BytesIO()
|
||||
cairosvg.svg2png(bytestring=content, write_to=tmpimg)
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
iconfile.save(os.path.join(iconspath, "icon.png"))
|
||||
if shutil.which("magick"):
|
||||
os.system(
|
||||
f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(_args.root_directory, ".static", "favicon.ico")}'
|
||||
)
|
||||
else:
|
||||
os.system(
|
||||
f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(_args.root_directory, ".static", "favicon.ico")}'
|
||||
)
|
||||
|
||||
|
||||
def webmanifest(_args: Args) -> None:
|
||||
icons: List[Icon] = []
|
||||
files = os.listdir(os.path.join(_args.root_directory, ".static", "icons"))
|
||||
if SVGSUPPORT and any(file.endswith(".svg") for file in files):
|
||||
svg = [file for file in files if file.endswith(".svg")][0]
|
||||
icons.append(
|
||||
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"}
|
||||
)
|
||||
icons.append({"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"})
|
||||
for size in ICON_SIZES:
|
||||
tmpimg = BytesIO()
|
||||
sizes = size.split("x")
|
||||
iconpath = os.path.join(_args.root_directory, ".static", "icons", os.path.splitext(svg)[0] + "-" + size + ".png")
|
||||
cairosvg.svg2png(
|
||||
url=os.path.join(_args.root_directory, ".static", "icons", svg),
|
||||
write_to=tmpimg,
|
||||
output_width=int(sizes[0]),
|
||||
output_height=int(sizes[1]),
|
||||
scale=1,
|
||||
)
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
iconfile.save(iconpath, format="PNG")
|
||||
icons.append(
|
||||
{
|
||||
"src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png",
|
||||
"sizes": size,
|
||||
"type": "image/png",
|
||||
"purpose": "maskable",
|
||||
}
|
||||
)
|
||||
icons.append(
|
||||
{
|
||||
"src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png",
|
||||
"sizes": size,
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
}
|
||||
)
|
||||
else:
|
||||
for icon in os.listdir(os.path.join(STATIC_FILES_DIR, "icons")):
|
||||
if not icon.endswith(".png"):
|
||||
continue
|
||||
with Image.open(os.path.join(STATIC_FILES_DIR, "icons", icon)) as iconfile:
|
||||
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
|
||||
icons.append(
|
||||
{"src": f"{_args.web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"}
|
||||
)
|
||||
icons.append({"src": f"{_args.web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
|
||||
if len(icons) == 0:
|
||||
print("No icons found in the static/icons folder!")
|
||||
return
|
||||
|
||||
with open(os.path.join(_args.root_directory, ".static", "theme.css"), "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
background_color = (
|
||||
content.replace("body{", "body {").split("body {")[1].split("}")[0].split("background-color:")[1].split(";")[0].strip()
|
||||
)
|
||||
theme_color = (
|
||||
content.replace(".navbar{", "navbar {").split(".navbar {")[1].split("}")[0].split("background-color:")[1].split(";")[0].strip()
|
||||
)
|
||||
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f:
|
||||
manifest = env.get_template("manifest.json.j2")
|
||||
content = manifest.render(
|
||||
name=_args.web_root_url.replace("https://", "").replace("http://", "").replace("/", ""),
|
||||
short_name=_args.site_title,
|
||||
icons=icons,
|
||||
background_color=background_color,
|
||||
theme_color=theme_color,
|
||||
)
|
||||
f.write(content)
|
||||
shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True)
|
||||
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
|
||||
|
||||
|
||||
def generate_thumbnail(arguments: Tuple[str, str, str, bool]) -> None:
|
||||
"""
|
||||
Generate a thumbnail for a given image.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
arguments : Tuple[str, str, str, bool]
|
||||
A tuple containing the folder, item, root directory, and regenerate thumbnails flag.
|
||||
"""
|
||||
folder, item, root_directory, regenerate_thumbnails = arguments
|
||||
path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".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"
|
||||
if os.path.exists(oldpath):
|
||||
try:
|
||||
os.rename(oldpath, path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if not os.path.exists(path) or regenerate_thumbnails:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
try:
|
||||
with Image.open(os.path.join(folder, item)) as imgfile:
|
||||
img = ImageOps.exif_transpose(imgfile)
|
||||
imgrgb = imgfile.convert("RGB")
|
||||
img = ImageOps.exif_transpose(imgrgb)
|
||||
img.thumbnail((512, 512))
|
||||
img.save(path, "JPEG", quality=75, optimize=True)
|
||||
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB")
|
||||
except OSError:
|
||||
print(f"Failed to generate thumbnail for {os.path.join(folder, item)}")
|
||||
|
||||
|
||||
def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
|
||||
"""
|
||||
Recursively count the total number of folders to be processed.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
folder : str
|
||||
The current folder being processed.
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
_total : int, optional
|
||||
The running total of folders, default is 0.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
int
|
||||
The total number of folders.
|
||||
"""
|
||||
_total += 1
|
||||
|
||||
pbardict["traversingbar"].desc = f"Traversing filesystem - {folder}"
|
||||
pbardict["traversingbar"].update(1)
|
||||
|
||||
items = os.listdir(folder)
|
||||
items.sort()
|
||||
items = sorted(os.listdir(folder))
|
||||
for item in items:
|
||||
if item not in EXCLUDES:
|
||||
if os.path.isdir(os.path.join(folder, item)):
|
||||
if item not in _args.exclude_folders:
|
||||
skip = False
|
||||
for exclude in _args.exclude_folders:
|
||||
if fnmatch.fnmatchcase(os.path.join(folder, item), exclude):
|
||||
skip = True
|
||||
if not skip:
|
||||
if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)):
|
||||
if item not in _args.exclude_folders and not any(
|
||||
fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders
|
||||
):
|
||||
_total = get_total_folders(os.path.join(folder, item), _args, _total)
|
||||
return _total
|
||||
|
||||
|
||||
def list_folder(folder: str, title: str, _args: Args, raw: list[str]) -> None:
|
||||
sizelist: Dict[Dict[str, int], Dict[str, int]] = {}
|
||||
if not os.path.exists(os.path.join(folder, ".sizelist.json")):
|
||||
sizelistfile = open(os.path.join(folder, ".sizelist.json"), "x", encoding="utf-8")
|
||||
sizelistfile.write("{}")
|
||||
sizelistfile.close()
|
||||
with open(os.path.join(folder, ".sizelist.json"), "r+", encoding="utf-8") as sizelistfile:
|
||||
sizelist = json.loads(sizelistfile.read())
|
||||
items = os.listdir(folder)
|
||||
items.sort()
|
||||
images: List[Dict[str, Any]] = []
|
||||
subfolders: List[Dict[str, str]] = []
|
||||
foldername = folder.removeprefix(_args.root_directory)
|
||||
foldername = f"{foldername}/" if foldername else ""
|
||||
baseurl = urllib.parse.quote(foldername)
|
||||
if not os.path.exists(os.path.join(_args.root_directory, ".thumbnails", foldername)):
|
||||
os.mkdir(os.path.join(_args.root_directory, ".thumbnails", foldername))
|
||||
contains_files = False
|
||||
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)
|
||||
for item in items:
|
||||
if item not in EXCLUDES:
|
||||
if os.path.isdir(os.path.join(folder, item)):
|
||||
if _args.web_root_url.startswith("file://"):
|
||||
subfolder = {"url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html", "name": item}
|
||||
else:
|
||||
subfolder = {"url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}", "name": item}
|
||||
subfolders.append(subfolder)
|
||||
if item not in _args.exclude_folders:
|
||||
skip = False
|
||||
for exclude in _args.exclude_folders:
|
||||
if fnmatch.fnmatchcase(os.path.join(folder, item), exclude):
|
||||
skip = True
|
||||
if not skip:
|
||||
list_folder(
|
||||
os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw
|
||||
)
|
||||
else:
|
||||
extsplit = os.path.splitext(item)
|
||||
contains_files = True
|
||||
if extsplit[1].lower() in _args.file_extensions:
|
||||
if not sizelist.get(item) or _args.regenerate_thumbnails:
|
||||
exifdata = {}
|
||||
with Image.open(os.path.join(folder, item)) as img:
|
||||
exif = img.getexif()
|
||||
width, height = img.size
|
||||
|
||||
for key, val in exif.items():
|
||||
if key in ExifTags.TAGS:
|
||||
exifdata[ExifTags.TAGS[key]] = val
|
||||
else:
|
||||
exifdata[key] = val
|
||||
if "Orientation" in exifdata and (exifdata["Orientation"] == 6 or exifdata["Orientation"] == 8):
|
||||
sizelist[item] = {"width": height, "height": width}
|
||||
else:
|
||||
sizelist[item] = {"width": width, "height": height}
|
||||
|
||||
image = {
|
||||
"url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}",
|
||||
"thumbnail": f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(extsplit[0])}.jpg",
|
||||
"name": item,
|
||||
"width": sizelist[item]["width"],
|
||||
"height": sizelist[item]["height"],
|
||||
}
|
||||
if not os.path.exists(os.path.join(_args.root_directory, ".thumbnails", foldername, item)):
|
||||
thumbnails.append((folder, item, _args.root_directory, _args.regenerate_thumbnails))
|
||||
for _raw in raw:
|
||||
if os.path.exists(os.path.join(folder, extsplit[0] + _raw)):
|
||||
url = urllib.parse.quote(extsplit[0]) + _raw
|
||||
if _raw in (".tif", ".tiff"):
|
||||
image["tiff"] = f"{_args.web_root_url}{baseurl}{url}"
|
||||
else:
|
||||
image["raw"] = f"{_args.web_root_url}{baseurl}{url}"
|
||||
images.append(image)
|
||||
if item == "info":
|
||||
with open(os.path.join(folder, item), encoding="utf-8") as f:
|
||||
_info = f.read()
|
||||
info[urllib.parse.quote(folder)] = _info
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict[folder].update(1)
|
||||
pbardict["htmlbar"].update(0)
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict[folder].close()
|
||||
sizelistfile.seek(0)
|
||||
sizelistfile.write(json.dumps(sizelist, indent=4))
|
||||
sizelistfile.truncate()
|
||||
if os.path.exists(os.path.join(folder, ".sizelist.json")) and sizelist == {}:
|
||||
os.remove(os.path.join(folder, ".sizelist.json"))
|
||||
if not contains_files and not _args.use_fancy_folders:
|
||||
return
|
||||
if images or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files):
|
||||
image_chunks = np.array_split(images, 8) if images else []
|
||||
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f:
|
||||
_info: List[str] = None
|
||||
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] + '/'))}"
|
||||
)
|
||||
if parent and _args.web_root_url.startswith("file://"):
|
||||
parent += "index.html"
|
||||
license_info: cclicense.License = (
|
||||
{
|
||||
"project": _args.site_title,
|
||||
"author": _args.author_name,
|
||||
"type": cclicense.licensenameswitch(_args.license_type),
|
||||
"url": cclicense.licenseurlswitch(_args.license_type),
|
||||
"pics": cclicense.licensepicswitch(_args.license_type),
|
||||
}
|
||||
if _args.license_type
|
||||
else None
|
||||
)
|
||||
if urllib.parse.quote(folder) in info:
|
||||
_info = []
|
||||
_infolst = info[urllib.parse.quote(folder)].split("\n")
|
||||
for i in _infolst:
|
||||
if len(i) > 1:
|
||||
_info.append(i)
|
||||
html = env.get_template("index.html.j2")
|
||||
content = html.render(
|
||||
title=title,
|
||||
favicon=f"{_args.web_root_url}{FAVICON_PATH}",
|
||||
stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}",
|
||||
theme=f"{_args.web_root_url}.static/theme.css",
|
||||
root=_args.web_root_url,
|
||||
parent=parent,
|
||||
header=header,
|
||||
license=license_info,
|
||||
subdirectories=subfolders,
|
||||
images=image_chunks,
|
||||
info=_info,
|
||||
allimages=images,
|
||||
webmanifest=_args.generate_webmanifest,
|
||||
)
|
||||
f.write(content)
|
||||
else:
|
||||
if os.path.exists(os.path.join(folder, "index.html")):
|
||||
os.remove(os.path.join(folder, "index.html"))
|
||||
if os.path.exists(os.path.join(folder, ".sizelist.json")):
|
||||
os.remove(os.path.join(folder, ".sizelist.json"))
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict["htmlbar"].update(1)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_arguments()
|
||||
"""
|
||||
Main function to process images and generate a static image hosting website.
|
||||
"""
|
||||
thumbnails: List[Tuple[str, str, str, bool]] = []
|
||||
|
||||
args = parse_arguments(VERSION)
|
||||
args, raw = init_globals(args, RAW_EXTENSIONS)
|
||||
|
||||
if os.path.exists(os.path.join(args.root_directory, ".lock")):
|
||||
lock_file = os.path.join(args.root_directory, ".lock")
|
||||
if os.path.exists(lock_file):
|
||||
print("Another instance of this program is running.")
|
||||
exit()
|
||||
|
||||
try:
|
||||
Path(os.path.join(args.root_directory, ".lock")).touch()
|
||||
if not os.path.exists(os.path.join(args.root_directory, ".thumbnails")):
|
||||
os.mkdir(os.path.join(args.root_directory, ".thumbnails"))
|
||||
Path(lock_file).touch()
|
||||
os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True)
|
||||
|
||||
copy_static_files(args)
|
||||
|
||||
icons(args)
|
||||
|
||||
if args.generate_webmanifest:
|
||||
@@ -438,7 +166,7 @@ def main() -> None:
|
||||
|
||||
if args.non_interactive_mode:
|
||||
print("Generating HTML files...")
|
||||
list_folder(args.root_directory, args.site_title, args, raw)
|
||||
thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw)
|
||||
with Pool(os.cpu_count()) as pool:
|
||||
print("Generating thumbnails...")
|
||||
pool.map(generate_thumbnail, thumbnails)
|
||||
@@ -449,9 +177,7 @@ def main() -> None:
|
||||
pbardict["traversingbar"].update(0)
|
||||
pbardict["traversingbar"].close()
|
||||
|
||||
pbardict["htmlbar"] = tqdm(total=total, desc="Generating HTML files", unit="folders", ascii=True, dynamic_ncols=True)
|
||||
list_folder(args.root_directory, args.site_title, args, raw)
|
||||
pbardict["htmlbar"].close()
|
||||
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw)
|
||||
|
||||
with Pool(os.cpu_count()) as pool:
|
||||
for _ in tqdm(
|
||||
@@ -464,7 +190,8 @@ def main() -> None:
|
||||
):
|
||||
pass
|
||||
finally:
|
||||
os.remove(os.path.join(args.root_directory, ".lock"))
|
||||
os.remove(lock_file)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
104
modules/argumentparser.py
Normal file
104
modules/argumentparser.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import List, Optional
|
||||
import os
|
||||
import argparse
|
||||
from rich_argparse import RichHelpFormatter, HelpPreviewAction
|
||||
|
||||
|
||||
DEFAULT_THEME_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "themes", "default.css")
|
||||
DEFAULT_AUTHOR = "Author"
|
||||
|
||||
|
||||
class Args:
|
||||
"""
|
||||
A class to store command-line arguments for the script.
|
||||
|
||||
Attributes:
|
||||
-----------
|
||||
author_name : str
|
||||
The name of the author of the images.
|
||||
exclude_folders : List[str]
|
||||
A list of folders to exclude from processing.
|
||||
file_extensions : List[str]
|
||||
A list of file extensions to include.
|
||||
generate_webmanifest : bool
|
||||
Whether to generate a web manifest file.
|
||||
ignore_other_files : bool
|
||||
Whether to ignore files that do not match the specified extensions.
|
||||
license_type : Optional[str]
|
||||
The type of license for the images.
|
||||
non_interactive_mode : bool
|
||||
Whether to run in non-interactive mode.
|
||||
regenerate_thumbnails : bool
|
||||
Whether to regenerate thumbnails even if they already exist.
|
||||
root_directory : str
|
||||
The root directory containing the images.
|
||||
site_title : str
|
||||
The title of the image hosting site.
|
||||
theme_path : str
|
||||
The path to the CSS theme file.
|
||||
use_fancy_folders : bool
|
||||
Whether to enable fancy folder view.
|
||||
web_root_url : str
|
||||
The base URL of the web root for the image hosting site.
|
||||
"""
|
||||
author_name: str
|
||||
exclude_folders: List[str]
|
||||
file_extensions: List[str]
|
||||
generate_webmanifest: bool
|
||||
ignore_other_files: bool
|
||||
license_type: Optional[str]
|
||||
non_interactive_mode: bool
|
||||
regenerate_thumbnails: bool
|
||||
root_directory: str
|
||||
site_title: str
|
||||
theme_path: str
|
||||
use_fancy_folders: bool
|
||||
web_root_url: str
|
||||
|
||||
|
||||
def parse_arguments(version: str) -> Args:
|
||||
"""
|
||||
Parse command-line arguments.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
version : str
|
||||
The version of the program.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
Args
|
||||
An instance of the Args class containing the parsed arguments.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter)
|
||||
parser.add_argument("--exclude-folder", help="Folders to exclude from processing, globs supported (can be specified multiple times).", action="append", dest="exclude_folders", metavar="FOLDER")
|
||||
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg")
|
||||
parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
|
||||
parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
|
||||
parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders")
|
||||
parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
|
||||
parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
|
||||
parser.add_argument("-e", "--file-extensions", help="File extensions to include (can be specified multiple times).", action="append", dest="file_extensions", metavar="EXTENSION")
|
||||
parser.add_argument("-l", "--license-type", help="Specify the license type for the images.", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE")
|
||||
parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest")
|
||||
parser.add_argument("-n", "--non-interactive-mode", help="Run in non-interactive mode, disabling progress bars.", action="store_true", default=False, dest="non_interactive_mode")
|
||||
parser.add_argument("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT")
|
||||
parser.add_argument("-r", "--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
|
||||
parser.add_argument("-t", "--site-title", help="Title of the image hosting site.", required=True, type=str, dest="site_title", metavar="TITLE")
|
||||
parser.add_argument("-w", "--web-root-url", help="Base URL of the web root for the image hosting site.", required=True, type=str, dest="web_root_url", metavar="URL")
|
||||
parsed_args = parser.parse_args()
|
||||
_args = Args()
|
||||
_args.author_name = parsed_args.author_name
|
||||
_args.exclude_folders = parsed_args.exclude_folders
|
||||
_args.file_extensions = parsed_args.file_extensions
|
||||
_args.generate_webmanifest = parsed_args.generate_webmanifest
|
||||
_args.ignore_other_files = parsed_args.ignore_other_files
|
||||
_args.license_type = parsed_args.license_type
|
||||
_args.non_interactive_mode = parsed_args.non_interactive_mode
|
||||
_args.regenerate_thumbnails = parsed_args.regenerate_thumbnails
|
||||
_args.root_directory = parsed_args.root_directory
|
||||
_args.site_title = parsed_args.site_title
|
||||
_args.theme_path = parsed_args.theme_path
|
||||
_args.use_fancy_folders = parsed_args.use_fancy_folders
|
||||
_args.web_root_url = parsed_args.web_root_url
|
||||
return _args
|
||||
@@ -1,4 +1,21 @@
|
||||
class License:
|
||||
"""
|
||||
A class to represent a Creative Commons license.
|
||||
|
||||
Attributes:
|
||||
-----------
|
||||
project : str
|
||||
The name of the project.
|
||||
author : str
|
||||
The author of the work.
|
||||
type : str
|
||||
The type of the license.
|
||||
url : str
|
||||
The URL of the license.
|
||||
pics : list of str
|
||||
A list of URLs to the license images.
|
||||
"""
|
||||
|
||||
project: str
|
||||
author: str
|
||||
type: str
|
||||
@@ -7,6 +24,19 @@ class License:
|
||||
|
||||
|
||||
def licenseurlswitch(cclicense: str) -> str:
|
||||
"""
|
||||
Get the URL for a given Creative Commons license type.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
cclincense : str
|
||||
The license type identifier.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
str
|
||||
The URL associated with the specified license type.
|
||||
"""
|
||||
switch = {
|
||||
"cc-zero": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||
"cc-by": "https://creativecommons.org/licenses/by/4.0/",
|
||||
@@ -21,6 +51,19 @@ def licenseurlswitch(cclicense: str) -> str:
|
||||
|
||||
|
||||
def licensenameswitch(cclicense: str) -> str:
|
||||
"""
|
||||
Get the name for a given Creative Commons license type.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
cclincense : str
|
||||
The license type identifier.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
str
|
||||
The name associated with the specified license type.
|
||||
"""
|
||||
switch = {
|
||||
"cc-zero": "CC0 1.0",
|
||||
"cc-by": "CC BY 4.0",
|
||||
@@ -35,6 +78,19 @@ def licensenameswitch(cclicense: str) -> str:
|
||||
|
||||
|
||||
def licensepicswitch(cclicense: str) -> list[str]:
|
||||
"""
|
||||
Get the list of image URLs for a given Creative Commons license type.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
cclincense : str
|
||||
The license type identifier.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
list of str
|
||||
A list of URLs to the license images.
|
||||
"""
|
||||
switch = {
|
||||
"cc-zero": [
|
||||
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
|
||||
313
modules/generate_html.py
Normal file
313
modules/generate_html.py
Normal file
@@ -0,0 +1,313 @@
|
||||
import os
|
||||
import urllib.parse
|
||||
import fnmatch
|
||||
import json
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
from tqdm.auto import tqdm
|
||||
from PIL import Image, ExifTags
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import modules.cclicense as cclicense
|
||||
from modules.argumentparser import Args
|
||||
|
||||
# Constants for file paths and exclusions
|
||||
FAVICON_PATH = ".static/favicon.ico"
|
||||
GLOBAL_CSS_PATH = ".static/global.css"
|
||||
EXCLUDES = [".lock", "index.html", "manifest.json", ".sizelist.json", ".thumbnails", ".static"]
|
||||
|
||||
# Set the maximum image pixels to prevent decompression bomb DOS attacks
|
||||
Image.MAX_IMAGE_PIXELS = 933120000
|
||||
|
||||
# Initialize Jinja2 environment for template rendering
|
||||
env = Environment(loader=FileSystemLoader(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "templates")))
|
||||
thumbnails: List[Tuple[str, str]] = []
|
||||
info: Dict[str, str] = {}
|
||||
pbardict: Dict[str, tqdm] = {}
|
||||
|
||||
|
||||
def initialize_sizelist(folder: str) -> Dict[str, Dict[str, int]]:
|
||||
"""
|
||||
Initializes the size list JSON file if it doesn't exist.
|
||||
|
||||
Args:
|
||||
folder (str): The folder in which the size list file is located.
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict[str, int]]: The size list dictionary.
|
||||
"""
|
||||
sizelist = {}
|
||||
sizelist_path = os.path.join(folder, ".sizelist.json")
|
||||
if not os.path.exists(sizelist_path):
|
||||
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile:
|
||||
sizelistfile.write("{}")
|
||||
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile:
|
||||
try:
|
||||
sizelist = json.loads(sizelistfile.read())
|
||||
except json.decoder.JSONDecodeError:
|
||||
sizelist = {}
|
||||
return sizelist
|
||||
|
||||
|
||||
def update_sizelist(sizelist: Dict[str, Dict[str, int]], folder: str) -> None:
|
||||
"""
|
||||
Updates the size list JSON file.
|
||||
|
||||
Args:
|
||||
sizelist (Dict[str, Dict[str, int]]): The size list dictionary to be written to the file.
|
||||
folder (str): The folder in which the size list file is located.
|
||||
"""
|
||||
sizelist_path = os.path.join(folder, ".sizelist.json")
|
||||
if sizelist != {}:
|
||||
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile:
|
||||
sizelistfile.write(json.dumps(sizelist, indent=4))
|
||||
else:
|
||||
if os.path.exists(sizelist_path):
|
||||
os.remove(sizelist_path)
|
||||
|
||||
|
||||
def get_image_info(item: str, folder: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extracts image information and EXIF data.
|
||||
|
||||
Args:
|
||||
item (str): The image file name.
|
||||
folder (str): The folder containing the image.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary containing image width, height, and EXIF data.
|
||||
"""
|
||||
with Image.open(os.path.join(folder, item)) as img:
|
||||
exif = img.getexif()
|
||||
width, height = img.size
|
||||
exifdata = {ExifTags.TAGS.get(key, key): val for key, val in exif.items()}
|
||||
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
|
||||
width, height = height, width
|
||||
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]:
|
||||
"""
|
||||
Processes an image and prepares its data for the HTML template.
|
||||
|
||||
Args:
|
||||
item (str): The image file name.
|
||||
folder (str): The folder containing the image.
|
||||
_args (Args): Parsed command line arguments.
|
||||
baseurl (str): Base URL for the web root.
|
||||
sizelist (Dict[str, Dict[str, int]]): Dictionary containing size information for images.
|
||||
raw (List[str]): List of raw image file extensions.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary containing image details for HTML rendering.
|
||||
"""
|
||||
extsplit = os.path.splitext(item)
|
||||
if item not in sizelist or _args.regenerate_thumbnails:
|
||||
sizelist[item] = get_image_info(item, folder)
|
||||
|
||||
image = {
|
||||
"url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}",
|
||||
"thumbnail": f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg",
|
||||
"name": item,
|
||||
"width": sizelist[item]["width"],
|
||||
"height": sizelist[item]["height"],
|
||||
}
|
||||
if not os.path.exists(os.path.join(_args.root_directory, ".thumbnails", baseurl, item)):
|
||||
thumbnails.append((folder, item, _args.root_directory, _args.regenerate_thumbnails))
|
||||
|
||||
for _raw in raw:
|
||||
if os.path.exists(os.path.join(folder, extsplit[0] + _raw)):
|
||||
url = urllib.parse.quote(extsplit[0]) + _raw
|
||||
if _raw in (".tif", ".tiff"):
|
||||
image["tiff"] = f"{_args.web_root_url}{baseurl}{url}"
|
||||
else:
|
||||
image["raw"] = f"{_args.web_root_url}{baseurl}{url}"
|
||||
return image
|
||||
|
||||
|
||||
def generate_html(folder: str, title: str, _args: Args, raw: List[str]) -> None:
|
||||
"""
|
||||
Generates HTML content for a folder of images.
|
||||
|
||||
Args:
|
||||
folder (str): The folder to generate HTML for.
|
||||
title (str): The title of the HTML page.
|
||||
_args (Args): Parsed command line arguments.
|
||||
raw (List[str]): Raw image file names.
|
||||
"""
|
||||
sizelist = initialize_sizelist(folder)
|
||||
items = sorted(os.listdir(folder))
|
||||
|
||||
images = []
|
||||
subfolders = []
|
||||
foldername = folder.removeprefix(_args.root_directory)
|
||||
foldername = f"{foldername}/" if foldername else ""
|
||||
baseurl = urllib.parse.quote(foldername)
|
||||
|
||||
create_thumbnail_folder(foldername, _args.root_directory)
|
||||
|
||||
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)
|
||||
|
||||
for item in items:
|
||||
if item not in EXCLUDES:
|
||||
if os.path.isdir(os.path.join(folder, item)):
|
||||
process_subfolder(item, folder, baseurl, subfolders, _args, raw)
|
||||
else:
|
||||
if os.path.splitext(item)[1].lower() in _args.file_extensions:
|
||||
images.append(process_image(item, folder, _args, baseurl, sizelist, raw))
|
||||
if item == "info":
|
||||
process_info_file(folder, item)
|
||||
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict[folder].update(1)
|
||||
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict[folder].close()
|
||||
|
||||
update_sizelist(sizelist, folder)
|
||||
|
||||
if should_generate_html(images, _args):
|
||||
create_html_file(folder, title, foldername, images, subfolders, _args)
|
||||
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict["htmlbar"].update(1)
|
||||
|
||||
|
||||
def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
|
||||
"""
|
||||
Creates a folder for thumbnails if it doesn't exist.
|
||||
|
||||
Args:
|
||||
foldername (str): The name of the folder.
|
||||
root_directory (str): The root directory path.
|
||||
"""
|
||||
thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername)
|
||||
if not os.path.exists(thumbnails_path):
|
||||
os.mkdir(thumbnails_path)
|
||||
|
||||
|
||||
def process_subfolder(item: str, folder: str, baseurl: str, subfolders: List[Dict[str, str]], _args: Args, raw: List[str]) -> None:
|
||||
"""
|
||||
Processes a subfolder.
|
||||
|
||||
Args:
|
||||
item (str): The name of the subfolder.
|
||||
folder (str): The parent folder containing the subfolder.
|
||||
baseurl (str): Base URL for the web root.
|
||||
subfolders (List[Dict[str, str]]): List to store subfolder details.
|
||||
_args (Args): Parsed command line arguments.
|
||||
raw (List[str]): Raw image file extensions.
|
||||
"""
|
||||
subfolder_url = (
|
||||
f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html"
|
||||
if _args.web_root_url.startswith("file://")
|
||||
else f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}"
|
||||
)
|
||||
subfolders.append({"url": subfolder_url, "name": item})
|
||||
if item not in _args.exclude_folders:
|
||||
if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
|
||||
generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw)
|
||||
|
||||
|
||||
def process_info_file(folder: str, item: str) -> None:
|
||||
"""
|
||||
Processes an info file.
|
||||
|
||||
Args:
|
||||
folder (str): The folder containing the info file.
|
||||
item (str): The info file name.
|
||||
"""
|
||||
with open(os.path.join(folder, item), encoding="utf-8") as f:
|
||||
info[urllib.parse.quote(folder)] = f.read()
|
||||
|
||||
|
||||
def should_generate_html(images: List[Dict[str, Any]], _args: Args) -> bool:
|
||||
"""
|
||||
Determines if HTML should be generated.
|
||||
|
||||
Args:
|
||||
images (List[Dict[str, Any]]): List of images.
|
||||
_args (Args): Parsed command line arguments.
|
||||
|
||||
Returns:
|
||||
bool: True if HTML should be generated, False otherwise.
|
||||
"""
|
||||
return images or (_args.use_fancy_folders and (not images or _args.ignore_other_files))
|
||||
|
||||
|
||||
def create_html_file(
|
||||
folder: str, title: str, foldername: str, images: List[Dict[str, Any]], subfolders: List[Dict[str, str]], _args: Args
|
||||
) -> None:
|
||||
"""
|
||||
Creates the HTML file using the template.
|
||||
|
||||
Args:
|
||||
folder (str): The folder to create the HTML file in.
|
||||
title (str): The title of the HTML page.
|
||||
foldername (str): The name of the folder.
|
||||
images (List[Dict[str, Any]]): A list of images 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.
|
||||
"""
|
||||
image_chunks = np.array_split(images, 8) if images else []
|
||||
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] + '/'))}"
|
||||
if parent and _args.web_root_url.startswith("file://"):
|
||||
parent += "index.html"
|
||||
|
||||
license_info = (
|
||||
{
|
||||
"project": _args.site_title,
|
||||
"author": _args.author_name,
|
||||
"type": cclicense.licensenameswitch(_args.license_type),
|
||||
"url": cclicense.licenseurlswitch(_args.license_type),
|
||||
"pics": cclicense.licensepicswitch(_args.license_type),
|
||||
}
|
||||
if _args.license_type
|
||||
else None
|
||||
)
|
||||
|
||||
folder_info = info.get(urllib.parse.quote(folder), "").split("\n")
|
||||
_info = [i for i in folder_info if len(i) > 1] if folder_info else None
|
||||
|
||||
html = env.get_template("index.html.j2")
|
||||
content = html.render(
|
||||
title=title,
|
||||
favicon=f"{_args.web_root_url}{FAVICON_PATH}",
|
||||
stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}",
|
||||
theme=f"{_args.web_root_url}.static/theme.css",
|
||||
root=_args.web_root_url,
|
||||
parent=parent,
|
||||
header=header,
|
||||
license=license_info,
|
||||
subdirectories=subfolders,
|
||||
images=image_chunks,
|
||||
info=_info,
|
||||
allimages=images,
|
||||
webmanifest=_args.generate_webmanifest,
|
||||
)
|
||||
|
||||
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def list_folder(total: int, folder: str, title: str, _args: Args, raw: List[str]) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Lists and processes a folder, generating HTML files.
|
||||
|
||||
Args:
|
||||
total (int): Total number of folders to process.
|
||||
folder (str): The folder to process.
|
||||
title (str): The title of the HTML page.
|
||||
_args (Args): Parsed command line arguments.
|
||||
raw (List[str]): Raw image file names.
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str]]: List of thumbnails generated.
|
||||
"""
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict["htmlbar"] = tqdm(total=total, desc="Generating HTML files", unit="folders", ascii=True, dynamic_ncols=True)
|
||||
generate_html(folder, title, _args, raw)
|
||||
return thumbnails
|
||||
286
modules/svg_handling.py
Normal file
286
modules/svg_handling.py
Normal file
@@ -0,0 +1,286 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from typing import List, Dict
|
||||
from PIL import Image
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
# Attempt to import cairosvg for SVG support, set flag based on success
|
||||
try:
|
||||
import cairosvg
|
||||
from io import BytesIO
|
||||
|
||||
SVGSUPPORT = True
|
||||
except ImportError:
|
||||
SVGSUPPORT = False
|
||||
|
||||
from modules.argumentparser import Args
|
||||
|
||||
# Define constants for static files directory and icon sizes
|
||||
STATIC_FILES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "files")
|
||||
ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"]
|
||||
|
||||
# Initialize Jinja2 environment for template rendering
|
||||
env = Environment(loader=FileSystemLoader(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "templates")))
|
||||
|
||||
|
||||
class Icon:
|
||||
src: str
|
||||
type: str
|
||||
sizes: str
|
||||
purpose: str
|
||||
|
||||
|
||||
def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
||||
"""
|
||||
Extract color scheme from a CSS theme file.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
theme_path : str
|
||||
Path to the CSS theme file.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
Dict[str, str]
|
||||
Dictionary containing color scheme variables and their values.
|
||||
"""
|
||||
pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+);"
|
||||
colorscheme = {}
|
||||
with open(theme_path, "r", encoding="utf-8") as f:
|
||||
filecontent = f.read()
|
||||
matches = re.findall(pattern, filecontent)
|
||||
for match in matches:
|
||||
colorscheme[match[0]] = match[1]
|
||||
return colorscheme
|
||||
|
||||
|
||||
def extract_theme_color(theme_path: str) -> str:
|
||||
"""
|
||||
Extract the theme color from a CSS theme file.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
theme_path : str
|
||||
Path to the CSS theme file.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
str
|
||||
The theme color value.
|
||||
"""
|
||||
pattern = r"--bcolor1:\s*(#[0-9a-fA-F]+);"
|
||||
with open(theme_path, "r", encoding="utf-8") as f:
|
||||
filecontent = f.read()
|
||||
match = re.search(pattern, filecontent)
|
||||
return match.group(1) if match else ""
|
||||
|
||||
|
||||
def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str:
|
||||
"""
|
||||
Render an SVG icon using the provided color scheme.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
colorscheme : Dict[str, str]
|
||||
Dictionary containing color scheme variables and their values.
|
||||
iconspath : str
|
||||
Path to the directory where the icon will be saved.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
str
|
||||
The rendered SVG content.
|
||||
"""
|
||||
svg = env.get_template("icon.svg.j2")
|
||||
content = svg.render(colorscheme=colorscheme)
|
||||
with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return content
|
||||
|
||||
|
||||
def save_png_icon(content: str, iconspath: str) -> None:
|
||||
"""
|
||||
Save the rendered SVG content as a PNG icon.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
content : str
|
||||
The rendered SVG content.
|
||||
iconspath : str
|
||||
Path to the directory where the PNG icon will be saved.
|
||||
"""
|
||||
tmpimg = BytesIO()
|
||||
cairosvg.svg2png(bytestring=content, write_to=tmpimg)
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
iconfile.save(os.path.join(iconspath, "icon.png"))
|
||||
|
||||
|
||||
def generate_favicon(iconspath: str, root_directory: str) -> None:
|
||||
"""
|
||||
Generate a favicon from a PNG icon using ImageMagick.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
iconspath : str
|
||||
Path to the directory containing the PNG icon.
|
||||
root_directory : str
|
||||
Root directory of the project where the favicon will be saved.
|
||||
"""
|
||||
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
|
||||
if not shutil.which("magick"):
|
||||
command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
|
||||
os.system(command)
|
||||
|
||||
|
||||
def icons(_args: Args) -> None:
|
||||
"""
|
||||
Generate icons and save them in the static directory.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
"""
|
||||
print("Generating icons...")
|
||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||
colorscheme = extract_colorscheme(_args.theme_path)
|
||||
content = render_svg_icon(colorscheme, iconspath)
|
||||
if not SVGSUPPORT:
|
||||
print("Please install cairosvg to generate favicon from svg icon.")
|
||||
return
|
||||
save_png_icon(content, iconspath)
|
||||
generate_favicon(iconspath, _args.root_directory)
|
||||
|
||||
|
||||
def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, str]) -> None:
|
||||
"""
|
||||
Render the manifest.json file for the web application.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
icon_list : List[Icon]
|
||||
List of icons to be included in the manifest.
|
||||
colors : Dict[str, str]
|
||||
Dictionary containing color scheme and theme color.
|
||||
"""
|
||||
manifest = env.get_template("manifest.json.j2")
|
||||
content = manifest.render(
|
||||
name=_args.web_root_url.replace("https://", "").replace("http://", "").replace("/", ""),
|
||||
short_name=_args.site_title,
|
||||
icons=icon_list,
|
||||
background_color=colors["bcolor1"],
|
||||
theme_color=colors["theme_color"],
|
||||
)
|
||||
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List[Icon]:
|
||||
"""
|
||||
Create icons from an SVG file.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
files : List[str]
|
||||
List of files in the icons directory.
|
||||
iconspath : str
|
||||
Path to the directory where the icons will be saved.
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
List[Icon]
|
||||
List of icons created from the SVG file.
|
||||
"""
|
||||
svg = [file for file in files if file.endswith(".svg")][0]
|
||||
icon_list = [
|
||||
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"},
|
||||
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"},
|
||||
]
|
||||
for size in ICON_SIZES:
|
||||
tmpimg = BytesIO()
|
||||
sizes = size.split("x")
|
||||
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
|
||||
cairosvg.svg2png(
|
||||
url=os.path.join(iconspath, svg),
|
||||
write_to=tmpimg,
|
||||
output_width=int(sizes[0]),
|
||||
output_height=int(sizes[1]),
|
||||
scale=1,
|
||||
)
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
iconfile.save(iconpath, format="PNG")
|
||||
icon_list.append(
|
||||
{
|
||||
"src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png",
|
||||
"sizes": size,
|
||||
"type": "image/png",
|
||||
"purpose": "maskable",
|
||||
}
|
||||
)
|
||||
icon_list.append(
|
||||
{
|
||||
"src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png",
|
||||
"sizes": size,
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
}
|
||||
)
|
||||
return icon_list
|
||||
|
||||
|
||||
def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
|
||||
"""
|
||||
Create icons from PNG files.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
iconspath : str
|
||||
Path to the directory containing the PNG icons.
|
||||
web_root_url : str
|
||||
Base URL of the web root for the image hosting site.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
List[Icon]
|
||||
List of icons created from PNG files.
|
||||
"""
|
||||
icon_list = []
|
||||
for icon in os.listdir(iconspath):
|
||||
if not icon.endswith(".png"):
|
||||
continue
|
||||
with Image.open(os.path.join(iconspath, icon)) as iconfile:
|
||||
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
|
||||
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"})
|
||||
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
|
||||
return icon_list
|
||||
|
||||
|
||||
def webmanifest(_args: Args) -> None:
|
||||
"""
|
||||
Generate the web manifest file for the application.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
"""
|
||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||
files = os.listdir(iconspath)
|
||||
icon_list = (
|
||||
create_icons_from_svg(files, iconspath, _args)
|
||||
if SVGSUPPORT and any(file.endswith(".svg") for file in files)
|
||||
else create_icons_from_png(iconspath, _args.web_root_url)
|
||||
)
|
||||
|
||||
if not icon_list:
|
||||
print("No icons found in the static/icons folder!")
|
||||
return
|
||||
|
||||
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))
|
||||
colorscheme["theme_color"] = extract_theme_color(os.path.join(_args.root_directory, ".static", "theme.css"))
|
||||
render_manifest_json(_args, icon_list, colorscheme)
|
||||
Reference in New Issue
Block a user