21 Commits

Author SHA1 Message Date
0973868782 changed from fixed columns to inline-block 2024-10-17 09:07:07 +02:00
cd06c526af smooth scroll to top 2024-10-10 08:52:50 +02:00
d155b05798 False 2024-10-08 07:25:57 +02:00
3352894e4d rich_argparse optional dependency 2024-10-08 07:23:39 +02:00
d743ede95d added regular expression to datetime conversion 2024-09-23 12:37:52 +02:00
9403e84d78 added metadata regeneration option and display of date captured if avialable 2024-09-20 10:17:13 +02:00
961d79754e log file truncation and modification of log messages 2024-09-18 11:49:34 +02:00
383dd59851 added log viewer "hl" 2024-09-18 11:04:53 +02:00
549c15ca6c now gzips rotated logs 2024-09-18 09:26:53 +02:00
57d949677f fixed spelling mistake 2024-09-17 13:35:22 +02:00
cc6ad14506 removed deprecated constant 2024-09-17 13:33:57 +02:00
cace0f8593 added glob 2024-09-17 13:26:58 +02:00
0484200fad update README 2024-09-17 13:19:24 +02:00
74d97dac2a better handling of imagemagick subprocess 2024-09-17 13:12:54 +02:00
6c636905e2 added missing documentation 2024-09-17 11:37:26 +02:00
c79473d646 modified themes and added logging to preview generator 2024-09-17 11:33:36 +02:00
3cb5269985 log start and end 2024-09-16 22:42:58 +02:00
3bde09aebc info → debug 2024-09-16 22:40:44 +02:00
effc05826a additional logging 2024-09-16 22:39:31 +02:00
8ec9701aa9 added json logger 2024-09-16 22:18:36 +02:00
a95a6e1722 added exifdata to sizelist 2024-09-16 13:10:57 +02:00
47 changed files with 648 additions and 191 deletions

View File

@@ -12,7 +12,7 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: pip install -r requirements.txt run: pip install -r requirements.txt
- name: Build Package - name: Build Package
run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder -F --add-data files:files --add-data templates:templates --add-data .version:. run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@@ -20,4 +20,4 @@ jobs:
make_latest: true make_latest: true
generate_release_notes: true generate_release_notes: true
files: | files: |
dist/StaticGalleryBuilder dist/StaticGalleryBuilder*

1
.gitignore vendored
View File

@@ -167,3 +167,4 @@ test/**/index.html
test/**/.sizelist.json test/**/.sizelist.json
test/manifest.json test/manifest.json
themes/previews themes/previews
logs

View File

@@ -1 +1 @@
2.2.7 2.4.0

View File

@@ -26,13 +26,14 @@ Python script to generate static HTML files for website galleries.
- `Pillow` library - `Pillow` library
- `rich_argparse` library - `rich_argparse` library
- `cairosvg` library (for SVG to PNG icon conversion) - `cairosvg` library (for SVG to PNG icon conversion)
- `python-json-logger` library (for logging)
## Installation ## Installation
Install the required libraries using pip: Install the required libraries using pip:
```sh ```sh
pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg python-json-logger
``` ```
## Usage ## Usage
@@ -43,20 +44,20 @@ The script supports several command-line options to customize its behavior. Belo
### Options ### Options
- `-h, --help`: Show the help message and exit.
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. This option is required.
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. This option is required.
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. This option is required.
- `-r, --regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
- `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author". - `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author".
- `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times. - `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times.
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
- `-m, --web-manifest`: Generate a web manifest file.
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. **(This option is required)**.
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. **(This option is required)**.
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. **(This option is required)**.
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
- `--regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
- `--reread-metadata`: Reread image metadata if it already exists.
- `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme. - `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme.
- `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing. - `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing.
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
- `-m, --web-manifest`: Generate a web manifest file.
### Examples ### Examples

View File

@@ -39,7 +39,8 @@
"cc-by-nc-sa", "cc-by-nc-sa",
"-n", "-n",
"-m", "-m",
"-r" "--regenerate-thumbnails",
"--reread-metadata",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "Testfolder", "name": "Testfolder",
@@ -57,12 +58,13 @@
"-t", "-t",
"Pictures", "Pictures",
"--theme", "--theme",
"themes/catpuccin.css", "themes/default.css",
"--use-fancy-folders", "--use-fancy-folders",
"--web-manifest", "--web-manifest",
"-n", "-n",
"-m", "-m",
"-r", // "--regenerate-thumbnails",
// "--reread-metadata",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "woek", "name": "woek",
@@ -101,7 +103,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",
@@ -147,6 +149,9 @@
"python.analysis.inlayHints.callArgumentNames": "off", "python.analysis.inlayHints.callArgumentNames": "off",
"python.analysis.inlayHints.functionReturnTypes": false, "python.analysis.inlayHints.functionReturnTypes": false,
"python.analysis.inlayHints.variableTypes": false, "python.analysis.inlayHints.variableTypes": false,
"yaml.schemas": {
"https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json": "file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/hl_config.yaml"
},
}, },
"tasks": { "tasks": {
"version": "2.0.0", "version": "2.0.0",
@@ -157,6 +162,14 @@
"label": "Delete Lockfile", "label": "Delete Lockfile",
"problemMatcher": [], "problemMatcher": [],
"type": "shell", "type": "shell",
"presentation": {
"echo": false,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
}
}, },
{ {
"command": "rm -f /home/user/woek/Pictures/.lock", "command": "rm -f /home/user/woek/Pictures/.lock",
@@ -164,7 +177,68 @@
"label": "Delete Lockfile 2", "label": "Delete Lockfile 2",
"problemMatcher": [], "problemMatcher": [],
"type": "shell", "type": "shell",
"presentation": {
"echo": false,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
}
}, },
{
"command": "pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.",
"isBackground": false,
"label": "Build",
"problemMatcher": [],
"type": "shell",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
},
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": [
"Clean"
]
},
{
"command": "rm -rf build dist",
"isBackground": true,
"label": "Clean",
"problemMatcher": [],
"type": "shell",
"presentation": {
"echo": true,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
},
"group": "build"
},
{
"command": "LESS=-SR hl logs/latest.jsonl --config hl_config.yaml",
"isBackground": false,
"label": "View Latest Log",
"problemMatcher": [],
"type": "shell",
"presentation": {
"echo": false,
"reveal": "always",
"focus": true,
"panel": "dedicated",
"showReuseMessage": false,
"clear": true
}
}
], ],
}, },
} }

View File

@@ -11,10 +11,12 @@ from typing import Dict, List, Tuple
from tqdm.auto import tqdm from tqdm.auto import tqdm
from PIL import Image, ImageOps from PIL import Image, ImageOps
from modules.logger import logger
from modules.argumentparser import parse_arguments, Args from modules.argumentparser import parse_arguments, Args
from modules.svg_handling import icons, webmanifest, extract_colorscheme from modules.svg_handling import icons, webmanifest, extract_colorscheme
from modules.generate_html import list_folder, EXCLUDES from modules.generate_html import list_folder, EXCLUDES
# fmt: off # fmt: off
# Constants # Constants
if __package__ is None: if __package__ is None:
@@ -77,10 +79,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 +97,24 @@ def copy_static_files(_args: Args) -> None:
for match in re.finditer(r"content: (.*);", foldericon): for match in re.finditer(r"content: (.*);", foldericon):
foldericon = match[1] foldericon = match[1]
foldericon = foldericon.replace('"', "") foldericon = foldericon.replace('"', "")
logger.info("found foldericon", extra={"foldericon": foldericon})
break break
if "url" in foldericon: if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css")) shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
return return
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f: with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
logger.info("Reading foldericon svg")
svg = f.read() svg = f.read()
if "svg.j2" in foldericon: if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(_args.theme_path) colorscheme = extract_colorscheme(_args.theme_path)
svg = svg.replace("{{ color1 }}", colorscheme["color1"]) for color_key, color_value in colorscheme.items():
svg = svg.replace("{{ color2 }}", colorscheme["color2"]) svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
svg = svg.replace("{{ color3 }}", colorscheme["color3"]) logger.info("replaced colors in svg")
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
svg = urllib.parse.quote(svg) svg = urllib.parse.quote(svg)
with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f: with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f:
logger.info("writing theme file")
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail) f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
@@ -119,6 +128,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 +137,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 +178,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 +187,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 +196,17 @@ 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.error("another instance of this program is running")
exit() exit()
try: try:
Path(lock_file).touch() Path(lock_file).touch()
if args.reread_metadata:
logger.warning("reread metadata flag is set to true, all image metadata will be reread")
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)
@@ -196,13 +218,16 @@ def main() -> None:
webmanifest(args) webmanifest(args)
if args.non_interactive_mode: if args.non_interactive_mode:
logger.info("generating HTML files")
print("Generating HTML files...") print("Generating HTML files...")
thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION) thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails")
print("Generating thumbnails...") print("Generating thumbnails...")
pool.map(generate_thumbnail, thumbnails) pool.map(generate_thumbnail, thumbnails)
else: else:
pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True) pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True)
logger.info("getting total number of folders to process")
total = get_total_folders(args.root_directory, args) total = get_total_folders(args.root_directory, args)
pbardict["traversingbar"].desc = "Traversing filesystem" pbardict["traversingbar"].desc = "Traversing filesystem"
pbardict["traversingbar"].update(0) pbardict["traversingbar"].update(0)
@@ -211,6 +236,7 @@ def main() -> None:
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION) thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails")
for _ in tqdm( for _ in tqdm(
pool.imap_unordered(generate_thumbnail, thumbnails), pool.imap_unordered(generate_thumbnail, thumbnails),
total=len(thumbnails), total=len(thumbnails),
@@ -222,6 +248,7 @@ def main() -> None:
pass pass
finally: finally:
os.remove(lock_file) os.remove(lock_file)
logger.info("finished builder", extra={"version": VERSION})
return return

View File

@@ -90,7 +90,7 @@ figure {
.footer a img { .footer a img {
height: 22px !important; height: 22px !important;
margin-left: 3px; margin-left: 3px;
vertical-align: text-bottom vertical-align: text-bottom;
} }
.navbar { .navbar {
@@ -157,12 +157,12 @@ figure {
opacity: 1; opacity: 1;
} }
/* Create eight equal columns that sits next to each other */
.column { .column {
-ms-flex: 12.5%; -ms-flex: 12.5%;
flex: 12.5%; flex: 12.5%;
max-width: 12.5%; max-width: 12.5%;
padding: 0 4px; padding: 0 4px;
display: inline-block;
} }
.column img { .column img {
@@ -187,7 +187,6 @@ figure {
border-style: none; border-style: none;
} }
/* Responsive layout - makes a four column-layout instead of eight columns */
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
.column { .column {
-ms-flex: 25%; -ms-flex: 25%;
@@ -209,7 +208,6 @@ figure {
} }
} }
/* Responsive layout - makes a two column-layout instead of four columns */
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.column { .column {
-ms-flex: 50%; -ms-flex: 50%;
@@ -239,7 +237,6 @@ figure {
} }
} }
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.column { .column {
-ms-flex: 100%; -ms-flex: 100%;

View File

@@ -272,13 +272,14 @@ a.pswp__share--download:hover {
color: #BBB; } color: #BBB; }
.pswp__caption__center { .pswp__caption__center {
text-align: left; text-align: center;
max-width: 420px; max-width: 420px;
margin: 0 auto; margin: 0 auto;
font-size: 13px; font-size: 13px;
padding: 10px; padding: 10px;
line-height: 20px; line-height: 20px;
color: #CCC; } color: #CCC;
font-weight: bold; }
.pswp__caption--empty { .pswp__caption--empty {
display: none; } display: none; }

View File

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

142
help.svg
View File

@@ -19,118 +19,118 @@
font-weight: 700; font-weight: 700;
} }
.terminal-3756623299-matrix { .terminal-2271905219-matrix {
font-family: Fira Code, monospace; font-family: Fira Code, monospace;
font-size: 20px; font-size: 20px;
line-height: 24.4px; line-height: 24.4px;
font-variant-east-asian: full-width; font-variant-east-asian: full-width;
} }
.terminal-3756623299-title { .terminal-2271905219-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
font-family: arial; font-family: arial;
} }
.terminal-3756623299-r1 { fill: #ff8700 } .terminal-2271905219-r1 { fill: #ff8700 }
.terminal-3756623299-r2 { fill: #c5c8c6 } .terminal-2271905219-r2 { fill: #c5c8c6 }
.terminal-3756623299-r3 { fill: #808080 } .terminal-2271905219-r3 { fill: #808080 }
.terminal-3756623299-r4 { fill: #68a0b3 } .terminal-2271905219-r4 { fill: #68a0b3 }
.terminal-3756623299-r5 { fill: #00af87 } .terminal-2271905219-r5 { fill: #00af87 }
</style> </style>
<defs> <defs>
<clipPath id="terminal-3756623299-clip-terminal"> <clipPath id="terminal-2271905219-clip-terminal">
<rect x="0" y="0" width="1463.0" height="755.4" /> <rect x="0" y="0" width="1463.0" height="755.4" />
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-0"> <clipPath id="terminal-2271905219-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/> <rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-1"> <clipPath id="terminal-2271905219-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/> <rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-2"> <clipPath id="terminal-2271905219-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/> <rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-3"> <clipPath id="terminal-2271905219-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/> <rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-4"> <clipPath id="terminal-2271905219-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/> <rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-5"> <clipPath id="terminal-2271905219-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/> <rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-6"> <clipPath id="terminal-2271905219-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/> <rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-7"> <clipPath id="terminal-2271905219-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/> <rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-8"> <clipPath id="terminal-2271905219-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/> <rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-9"> <clipPath id="terminal-2271905219-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/> <rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-10"> <clipPath id="terminal-2271905219-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/> <rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-11"> <clipPath id="terminal-2271905219-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/> <rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-12"> <clipPath id="terminal-2271905219-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/> <rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-13"> <clipPath id="terminal-2271905219-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/> <rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-14"> <clipPath id="terminal-2271905219-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/> <rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-15"> <clipPath id="terminal-2271905219-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/> <rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-16"> <clipPath id="terminal-2271905219-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/> <rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-17"> <clipPath id="terminal-2271905219-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/> <rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-18"> <clipPath id="terminal-2271905219-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/> <rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-19"> <clipPath id="terminal-2271905219-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/> <rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-20"> <clipPath id="terminal-2271905219-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/> <rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-21"> <clipPath id="terminal-2271905219-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/> <rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-22"> <clipPath id="terminal-2271905219-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/> <rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-23"> <clipPath id="terminal-2271905219-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/> <rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-24"> <clipPath id="terminal-2271905219-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/> <rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-25"> <clipPath id="terminal-2271905219-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/> <rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-26"> <clipPath id="terminal-2271905219-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/> <rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-27"> <clipPath id="terminal-2271905219-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/> <rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-28"> <clipPath id="terminal-2271905219-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/> <rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-29"> <clipPath id="terminal-2271905219-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/> <rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
</defs> </defs>
@@ -142,40 +142,40 @@
<circle cx="44" cy="0" r="7" fill="#28c840"/> <circle cx="44" cy="0" r="7" fill="#28c840"/>
</g> </g>
<g transform="translate(9, 41)" clip-path="url(#terminal-3756623299-clip-terminal)"> <g transform="translate(9, 41)" clip-path="url(#terminal-2271905219-clip-terminal)">
<g class="terminal-3756623299-matrix"> <g class="terminal-2271905219-matrix">
<text class="terminal-3756623299-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">Usage:</text><text class="terminal-3756623299-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-3756623299-line-0)">builder.py</text><text class="terminal-3756623299-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">&#160;[</text><text class="terminal-3756623299-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-h</text><text class="terminal-3756623299-r2" x="256.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">]&#160;</text><text class="terminal-3756623299-r4" x="280.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-p</text><text class="terminal-3756623299-r5" x="317.2" y="20" textLength="48.8" clip-path="url(#terminal-3756623299-line-0)">ROOT</text><text class="terminal-3756623299-r4" x="378.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-w</text><text class="terminal-3756623299-r5" x="414.8" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">URL</text><text class="terminal-3756623299-r4" x="463.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-t</text><text class="terminal-3756623299-r5" x="500.2" y="20" textLength="61" clip-path="url(#terminal-3756623299-line-0)">TITLE</text><text class="terminal-3756623299-r2" x="561.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">&#160;[</text><text class="terminal-3756623299-r4" x="585.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-r</text><text class="terminal-3756623299-r2" x="610" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="646.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-n</text><text class="terminal-3756623299-r2" x="671" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="707.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-l</text><text class="terminal-3756623299-r5" x="744.2" y="20" textLength="85.4" clip-path="url(#terminal-3756623299-line-0)">LICENSE</text><text class="terminal-3756623299-r2" x="829.6" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-a</text><text class="terminal-3756623299-r5" x="902.8" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">AUTHOR</text><text class="terminal-3756623299-r2" x="976" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="1012.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-e</text><text class="terminal-3756623299-r5" x="1049.2" y="20" textLength="109.8" clip-path="url(#terminal-3756623299-line-0)">EXTENSION</text><text class="terminal-3756623299-r2" x="1159" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="1195.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-m</text><text class="terminal-3756623299-r2" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)">]</text><text class="terminal-3756623299-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)"> <text class="terminal-2271905219-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-2271905219-line-0)">Usage:</text><text class="terminal-2271905219-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-2271905219-line-0)">builder.py</text><text class="terminal-2271905219-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">&#160;[</text><text class="terminal-2271905219-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-h</text><text class="terminal-2271905219-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-a</text><text class="terminal-2271905219-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-2271905219-line-0)">AUTHOR</text><text class="terminal-2271905219-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-e</text><text class="terminal-2271905219-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-2271905219-line-0)">EXTENSION</text><text class="terminal-2271905219-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-l</text><text class="terminal-2271905219-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-2271905219-line-0)">LICENSE</text><text class="terminal-2271905219-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-m</text><text class="terminal-2271905219-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-n</text><text class="terminal-2271905219-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">]&#160;</text><text class="terminal-2271905219-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-p</text><text class="terminal-2271905219-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-2271905219-line-0)">ROOT</text><text class="terminal-2271905219-r2" x="976" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">&#160;[</text><text class="terminal-2271905219-r4" x="1000.4" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-r</text><text class="terminal-2271905219-r2" x="1024.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">]&#160;</text><text class="terminal-2271905219-r4" x="1049.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-t</text><text class="terminal-2271905219-r5" x="1085.8" y="20" textLength="61" clip-path="url(#terminal-2271905219-line-0)">TITLE</text><text class="terminal-2271905219-r4" x="1159" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-w</text><text class="terminal-2271905219-r5" x="1195.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">URL</text><text class="terminal-2271905219-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-2271905219-line-0)">
</text><text class="terminal-3756623299-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-3756623299-r4" x="231.8" y="44.4" textLength="146.4" clip-path="url(#terminal-3756623299-line-1)">--theme-path</text><text class="terminal-3756623299-r5" x="390.4" y="44.4" textLength="48.8" clip-path="url(#terminal-3756623299-line-1)">PATH</text><text class="terminal-3756623299-r2" x="439.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">]&#160;[</text><text class="terminal-3756623299-r4" x="475.8" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="707.6" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">]&#160;[</text><text class="terminal-3756623299-r4" x="744.2" y="44.4" textLength="244" clip-path="url(#terminal-3756623299-line-1)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="988.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">]&#160;[</text><text class="terminal-3756623299-r4" x="1024.8" y="44.4" textLength="195.2" clip-path="url(#terminal-3756623299-line-1)">--exclude-folder</text><text class="terminal-3756623299-r5" x="1232.2" y="44.4" textLength="73.2" clip-path="url(#terminal-3756623299-line-1)">FOLDER</text><text class="terminal-3756623299-r2" x="1305.4" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)">]</text><text class="terminal-3756623299-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)"> </text><text class="terminal-2271905219-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-2271905219-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-2271905219-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-2271905219-line-1)">--exclude-folder</text><text class="terminal-2271905219-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-2271905219-line-1)">FOLDER</text><text class="terminal-2271905219-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">]&#160;[</text><text class="terminal-2271905219-r4" x="549" y="44.4" textLength="244" clip-path="url(#terminal-2271905219-line-1)">--ignore-other-files</text><text class="terminal-2271905219-r2" x="793" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">]&#160;[</text><text class="terminal-2271905219-r4" x="829.6" y="44.4" textLength="146.4" clip-path="url(#terminal-2271905219-line-1)">--theme-path</text><text class="terminal-2271905219-r5" x="988.2" y="44.4" textLength="48.8" clip-path="url(#terminal-2271905219-line-1)">PATH</text><text class="terminal-2271905219-r2" x="1037" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">]&#160;[</text><text class="terminal-2271905219-r4" x="1073.6" y="44.4" textLength="231.8" clip-path="url(#terminal-2271905219-line-1)">--use-fancy-folders</text><text class="terminal-2271905219-r2" x="1305.4" y="44.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-1)">]</text><text class="terminal-2271905219-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-1)">
</text><text class="terminal-3756623299-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-3756623299-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-3756623299-r4" x="231.8" y="68.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-2)">--version</text><text class="terminal-3756623299-r2" x="341.6" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)">]</text><text class="terminal-3756623299-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)"> </text><text class="terminal-2271905219-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-2271905219-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-2271905219-r4" x="231.8" y="68.8" textLength="109.8" clip-path="url(#terminal-2271905219-line-2)">--version</text><text class="terminal-2271905219-r2" x="341.6" y="68.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-2)">]</text><text class="terminal-2271905219-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-2)">
</text><text class="terminal-3756623299-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-3)"> </text><text class="terminal-2271905219-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-3)">
</text><text class="terminal-3756623299-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-3756623299-line-4)">Generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website.</text><text class="terminal-3756623299-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-4)"> </text><text class="terminal-2271905219-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-2271905219-line-4)">Generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website.</text><text class="terminal-2271905219-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-4)">
</text><text class="terminal-3756623299-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-3756623299-line-5)"> </text><text class="terminal-2271905219-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-2271905219-line-5)">
</text><text class="terminal-3756623299-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-3756623299-line-6)">Options:</text><text class="terminal-3756623299-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-6)"> </text><text class="terminal-2271905219-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-2271905219-line-6)">Options:</text><text class="terminal-2271905219-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-6)">
</text><text class="terminal-3756623299-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">-h</text><text class="terminal-3756623299-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-3756623299-line-7)">--help</text><text class="terminal-3756623299-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-3756623299-line-7)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-3756623299-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-7)"> </text><text class="terminal-2271905219-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-7)">-h</text><text class="terminal-2271905219-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-7)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-2271905219-line-7)">--help</text><text class="terminal-2271905219-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-2271905219-line-7)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-2271905219-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-7)">
</text><text class="terminal-3756623299-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">-p</text><text class="terminal-3756623299-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="215.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-8)">--root-directory</text><text class="terminal-3756623299-r5" x="280.6" y="215.2" textLength="48.8" clip-path="url(#terminal-3756623299-line-8)">ROOT</text><text class="terminal-3756623299-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-8)"> </text><text class="terminal-2271905219-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-8)">-a</text><text class="terminal-2271905219-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-8)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="215.2" textLength="158.6" clip-path="url(#terminal-2271905219-line-8)">--author-name</text><text class="terminal-2271905219-r5" x="244" y="215.2" textLength="73.2" clip-path="url(#terminal-2271905219-line-8)">AUTHOR</text><text class="terminal-2271905219-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-8)">
</text><text class="terminal-3756623299-r2" x="292.8" y="239.6" textLength="451.4" clip-path="url(#terminal-3756623299-line-9)">Root&#160;directory&#160;containing&#160;the&#160;images.</text><text class="terminal-3756623299-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-9)"> </text><text class="terminal-2271905219-r2" x="292.8" y="239.6" textLength="402.6" clip-path="url(#terminal-2271905219-line-9)">Name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images.</text><text class="terminal-2271905219-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-9)">
</text><text class="terminal-3756623299-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">-w</text><text class="terminal-3756623299-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="264" textLength="170.8" clip-path="url(#terminal-3756623299-line-10)">--web-root-url</text><text class="terminal-3756623299-r5" x="256.2" y="264" textLength="36.6" clip-path="url(#terminal-3756623299-line-10)">URL</text><text class="terminal-3756623299-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-3756623299-line-10)"> </text><text class="terminal-2271905219-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-2271905219-line-10)">-e</text><text class="terminal-2271905219-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-2271905219-line-10)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="264" textLength="207.4" clip-path="url(#terminal-2271905219-line-10)">--file-extensions</text><text class="terminal-2271905219-r5" x="292.8" y="264" textLength="109.8" clip-path="url(#terminal-2271905219-line-10)">EXTENSION</text><text class="terminal-2271905219-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-2271905219-line-10)">
</text><text class="terminal-3756623299-r2" x="292.8" y="288.4" textLength="634.4" clip-path="url(#terminal-3756623299-line-11)">Base&#160;URL&#160;of&#160;the&#160;web&#160;root&#160;for&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-3756623299-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-11)"> </text><text class="terminal-2271905219-r2" x="292.8" y="288.4" textLength="744.2" clip-path="url(#terminal-2271905219-line-11)">File&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-2271905219-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-11)">
</text><text class="terminal-3756623299-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">-t</text><text class="terminal-3756623299-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="312.8" textLength="146.4" clip-path="url(#terminal-3756623299-line-12)">--site-title</text><text class="terminal-3756623299-r5" x="231.8" y="312.8" textLength="61" clip-path="url(#terminal-3756623299-line-12)">TITLE</text><text class="terminal-3756623299-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-12)"> </text><text class="terminal-2271905219-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-12)">-l</text><text class="terminal-2271905219-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-12)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="312.8" textLength="170.8" clip-path="url(#terminal-2271905219-line-12)">--license-type</text><text class="terminal-2271905219-r5" x="256.2" y="312.8" textLength="85.4" clip-path="url(#terminal-2271905219-line-12)">LICENSE</text><text class="terminal-2271905219-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-12)">
</text><text class="terminal-3756623299-r2" x="292.8" y="337.2" textLength="390.4" clip-path="url(#terminal-3756623299-line-13)">Title&#160;of&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-3756623299-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-13)"> </text><text class="terminal-2271905219-r2" x="292.8" y="337.2" textLength="488" clip-path="url(#terminal-2271905219-line-13)">Specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images.</text><text class="terminal-2271905219-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-13)">
</text><text class="terminal-3756623299-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">-r</text><text class="terminal-3756623299-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="361.6" textLength="280.6" clip-path="url(#terminal-3756623299-line-14)">--regenerate-thumbnails</text><text class="terminal-3756623299-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-14)"> </text><text class="terminal-2271905219-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-14)">-m</text><text class="terminal-2271905219-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-14)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="361.6" textLength="170.8" clip-path="url(#terminal-2271905219-line-14)">--web-manifest</text><text class="terminal-2271905219-r2" x="292.8" y="361.6" textLength="353.8" clip-path="url(#terminal-2271905219-line-14)">Generate&#160;a&#160;web&#160;manifest&#160;file.</text><text class="terminal-2271905219-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-14)">
</text><text class="terminal-3756623299-r2" x="292.8" y="386" textLength="597.8" clip-path="url(#terminal-3756623299-line-15)">Regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist.</text><text class="terminal-3756623299-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-3756623299-line-15)"> </text><text class="terminal-2271905219-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-2271905219-line-15)">-n</text><text class="terminal-2271905219-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-2271905219-line-15)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="386" textLength="268.4" clip-path="url(#terminal-2271905219-line-15)">--non-interactive-mode</text><text class="terminal-2271905219-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-2271905219-line-15)">
</text><text class="terminal-3756623299-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">-n</text><text class="terminal-3756623299-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-3756623299-line-16)">--non-interactive-mode</text><text class="terminal-3756623299-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-16)"> </text><text class="terminal-2271905219-r2" x="292.8" y="410.4" textLength="646.6" clip-path="url(#terminal-2271905219-line-16)">Run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars.</text><text class="terminal-2271905219-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-16)">
</text><text class="terminal-3756623299-r2" x="292.8" y="434.8" textLength="646.6" clip-path="url(#terminal-3756623299-line-17)">Run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars.</text><text class="terminal-3756623299-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-17)"> </text><text class="terminal-2271905219-r4" x="24.4" y="434.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-17)">-p</text><text class="terminal-2271905219-r2" x="48.8" y="434.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-17)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="434.8" textLength="195.2" clip-path="url(#terminal-2271905219-line-17)">--root-directory</text><text class="terminal-2271905219-r5" x="280.6" y="434.8" textLength="48.8" clip-path="url(#terminal-2271905219-line-17)">ROOT</text><text class="terminal-2271905219-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-17)">
</text><text class="terminal-3756623299-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">-l</text><text class="terminal-3756623299-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="459.2" textLength="170.8" clip-path="url(#terminal-3756623299-line-18)">--license-type</text><text class="terminal-3756623299-r5" x="256.2" y="459.2" textLength="85.4" clip-path="url(#terminal-3756623299-line-18)">LICENSE</text><text class="terminal-3756623299-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-18)"> </text><text class="terminal-2271905219-r2" x="292.8" y="459.2" textLength="451.4" clip-path="url(#terminal-2271905219-line-18)">Root&#160;directory&#160;containing&#160;the&#160;images.</text><text class="terminal-2271905219-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-18)">
</text><text class="terminal-3756623299-r2" x="292.8" y="483.6" textLength="488" clip-path="url(#terminal-3756623299-line-19)">Specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images.</text><text class="terminal-3756623299-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-19)"> </text><text class="terminal-2271905219-r4" x="24.4" y="483.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-19)">-r</text><text class="terminal-2271905219-r2" x="48.8" y="483.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-19)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="483.6" textLength="280.6" clip-path="url(#terminal-2271905219-line-19)">--regenerate-thumbnails</text><text class="terminal-2271905219-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-19)">
</text><text class="terminal-3756623299-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">-a</text><text class="terminal-3756623299-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="508" textLength="158.6" clip-path="url(#terminal-3756623299-line-20)">--author-name</text><text class="terminal-3756623299-r5" x="244" y="508" textLength="73.2" clip-path="url(#terminal-3756623299-line-20)">AUTHOR</text><text class="terminal-3756623299-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-3756623299-line-20)"> </text><text class="terminal-2271905219-r2" x="292.8" y="508" textLength="597.8" clip-path="url(#terminal-2271905219-line-20)">Regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist.</text><text class="terminal-2271905219-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-2271905219-line-20)">
</text><text class="terminal-3756623299-r2" x="292.8" y="532.4" textLength="402.6" clip-path="url(#terminal-3756623299-line-21)">Name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images.</text><text class="terminal-3756623299-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-21)"> </text><text class="terminal-2271905219-r4" x="24.4" y="532.4" textLength="24.4" clip-path="url(#terminal-2271905219-line-21)">-t</text><text class="terminal-2271905219-r2" x="48.8" y="532.4" textLength="24.4" clip-path="url(#terminal-2271905219-line-21)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="532.4" textLength="146.4" clip-path="url(#terminal-2271905219-line-21)">--site-title</text><text class="terminal-2271905219-r5" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-2271905219-line-21)">TITLE</text><text class="terminal-2271905219-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-21)">
</text><text class="terminal-3756623299-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">-e</text><text class="terminal-3756623299-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="556.8" textLength="207.4" clip-path="url(#terminal-3756623299-line-22)">--file-extensions</text><text class="terminal-3756623299-r5" x="292.8" y="556.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-22)">EXTENSION</text><text class="terminal-3756623299-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-22)"> </text><text class="terminal-2271905219-r2" x="292.8" y="556.8" textLength="390.4" clip-path="url(#terminal-2271905219-line-22)">Title&#160;of&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-2271905219-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-22)">
</text><text class="terminal-3756623299-r2" x="292.8" y="581.2" textLength="744.2" clip-path="url(#terminal-3756623299-line-23)">File&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-3756623299-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-23)"> </text><text class="terminal-2271905219-r4" x="24.4" y="581.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-23)">-w</text><text class="terminal-2271905219-r2" x="48.8" y="581.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-23)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="581.2" textLength="170.8" clip-path="url(#terminal-2271905219-line-23)">--web-root-url</text><text class="terminal-2271905219-r5" x="256.2" y="581.2" textLength="36.6" clip-path="url(#terminal-2271905219-line-23)">URL</text><text class="terminal-2271905219-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-23)">
</text><text class="terminal-3756623299-r4" x="24.4" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">-m</text><text class="terminal-3756623299-r2" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="605.6" textLength="170.8" clip-path="url(#terminal-3756623299-line-24)">--web-manifest</text><text class="terminal-3756623299-r2" x="292.8" y="605.6" textLength="353.8" clip-path="url(#terminal-3756623299-line-24)">Generate&#160;a&#160;web&#160;manifest&#160;file.</text><text class="terminal-3756623299-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-24)"> </text><text class="terminal-2271905219-r2" x="292.8" y="605.6" textLength="634.4" clip-path="url(#terminal-2271905219-line-24)">Base&#160;URL&#160;of&#160;the&#160;web&#160;root&#160;for&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-2271905219-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-24)">
</text><text class="terminal-3756623299-r4" x="24.4" y="630" textLength="146.4" clip-path="url(#terminal-3756623299-line-25)">--theme-path</text><text class="terminal-3756623299-r5" x="183" y="630" textLength="48.8" clip-path="url(#terminal-3756623299-line-25)">PATH</text><text class="terminal-3756623299-r2" x="292.8" y="630" textLength="329.4" clip-path="url(#terminal-3756623299-line-25)">Path&#160;to&#160;the&#160;CSS&#160;theme&#160;file.</text><text class="terminal-3756623299-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-3756623299-line-25)"> </text><text class="terminal-2271905219-r4" x="24.4" y="630" textLength="195.2" clip-path="url(#terminal-2271905219-line-25)">--exclude-folder</text><text class="terminal-2271905219-r5" x="231.8" y="630" textLength="73.2" clip-path="url(#terminal-2271905219-line-25)">FOLDER</text><text class="terminal-2271905219-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-2271905219-line-25)">
</text><text class="terminal-3756623299-r4" x="24.4" y="654.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-26)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="292.8" y="654.4" textLength="890.6" clip-path="url(#terminal-3756623299-line-26)">Enable&#160;fancy&#160;folder&#160;view&#160;instead&#160;of&#160;the&#160;default&#160;Apache&#160;directory&#160;listing.</text><text class="terminal-3756623299-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-26)"> </text><text class="terminal-2271905219-r2" x="292.8" y="654.4" textLength="1049.2" clip-path="url(#terminal-2271905219-line-26)">Folders&#160;to&#160;exclude&#160;from&#160;processing,&#160;globs&#160;supported&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-2271905219-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-26)">
</text><text class="terminal-3756623299-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-3756623299-line-27)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="292.8" y="678.8" textLength="683.2" clip-path="url(#terminal-3756623299-line-27)">Ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions.</text><text class="terminal-3756623299-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-27)"> </text><text class="terminal-2271905219-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-2271905219-line-27)">--ignore-other-files</text><text class="terminal-2271905219-r2" x="292.8" y="678.8" textLength="683.2" clip-path="url(#terminal-2271905219-line-27)">Ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions.</text><text class="terminal-2271905219-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-27)">
</text><text class="terminal-3756623299-r4" x="24.4" y="703.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-28)">--exclude-folder</text><text class="terminal-3756623299-r5" x="231.8" y="703.2" textLength="73.2" clip-path="url(#terminal-3756623299-line-28)">FOLDER</text><text class="terminal-3756623299-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-28)"> </text><text class="terminal-2271905219-r4" x="24.4" y="703.2" textLength="146.4" clip-path="url(#terminal-2271905219-line-28)">--theme-path</text><text class="terminal-2271905219-r5" x="183" y="703.2" textLength="48.8" clip-path="url(#terminal-2271905219-line-28)">PATH</text><text class="terminal-2271905219-r2" x="292.8" y="703.2" textLength="329.4" clip-path="url(#terminal-2271905219-line-28)">Path&#160;to&#160;the&#160;CSS&#160;theme&#160;file.</text><text class="terminal-2271905219-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-28)">
</text><text class="terminal-3756623299-r2" x="292.8" y="727.6" textLength="1049.2" clip-path="url(#terminal-3756623299-line-29)">Folders&#160;to&#160;exclude&#160;from&#160;processing,&#160;globs&#160;supported&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-3756623299-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-29)"> </text><text class="terminal-2271905219-r4" x="24.4" y="727.6" textLength="231.8" clip-path="url(#terminal-2271905219-line-29)">--use-fancy-folders</text><text class="terminal-2271905219-r2" x="292.8" y="727.6" textLength="890.6" clip-path="url(#terminal-2271905219-line-29)">Enable&#160;fancy&#160;folder&#160;view&#160;instead&#160;of&#160;the&#160;default&#160;Apache&#160;directory&#160;listing.</text><text class="terminal-2271905219-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-29)">
</text><text class="terminal-3756623299-r4" x="24.4" y="752" textLength="109.8" clip-path="url(#terminal-3756623299-line-30)">--version</text><text class="terminal-3756623299-r2" x="292.8" y="752" textLength="463.6" clip-path="url(#terminal-3756623299-line-30)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-3756623299-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-3756623299-line-30)"> </text><text class="terminal-2271905219-r4" x="24.4" y="752" textLength="109.8" clip-path="url(#terminal-2271905219-line-30)">--version</text><text class="terminal-2271905219-r2" x="292.8" y="752" textLength="463.6" clip-path="url(#terminal-2271905219-line-30)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-2271905219-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-2271905219-line-30)">
</text> </text>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

73
hl_config.yaml Normal file
View File

@@ -0,0 +1,73 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
$schema: https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
# Time format, see https://man7.org/linux/man-pages/man1/date.1.html for details.
time-format: "%b %d %T.%3N"
# Time zone name, see column "TZ identifier" at
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones page.
time-zone: "Europe/Vienna"
# Settings for fields processing.
fields:
# Configuration of the predefined set of fields.
predefined:
time:
show: auto
names: ["asctime"]
logger:
names: ["defaultlogger", "consolelogger"]
level:
show: auto
variants:
- names: ["levelname"]
values:
debug: ["DEBUG"]
info: ["INFO"]
warning: ["WARNING", "WARN"]
error: ["ERROR", "FATAL", "CRITICAL"]
- names: ["levelno"]
values:
debug: [10]
info: [20]
warning: [30]
error: [40, 50]
message:
names: ["message"]
caller:
names: ["funcName"]
caller-file:
names: ["filename"]
caller-line:
names: ["lineno"]
# List of wildcard field names to ignore.
ignore: ["_*"]
# List of exact field names to hide.
hide: ["pathname", "created", "levelno", "taskname", "relativeCreated", "thread", "process", "msecs"]
# Formatting settings.
formatting:
flatten: always
punctuation:
logger-name-separator: ":"
field-key-value-separator: "="
string-opening-quote: "'"
string-closing-quote: "'"
source-location-separator: "→ "
hidden-fields-indicator: " ..."
level-left-separator: "│"
level-right-separator: "│"
input-number-prefix: "#"
input-number-left-separator: ""
input-number-right-separator: " │ "
input-name-left-separator: ""
input-name-right-separator: " │ "
input-name-clipping: "··"
input-name-common-part: "··"
array-separator: " "
# Number of processing threads, configured automatically based on CPU count if not specified.
concurrency: ~
# Currently selected theme.
theme: "neutral"

View File

@@ -2,7 +2,16 @@ from dataclasses import dataclass
from typing import List, Optional from typing import List, Optional
import os import os
import argparse import argparse
from rich_argparse import RichHelpFormatter, HelpPreviewAction
try:
from rich_argparse import RichHelpFormatter, HelpPreviewAction
RICH = True
except ModuleNotFoundError:
RICH = False
from modules.logger import logger
if __package__ is None: if __package__ is None:
PACKAGE = "" PACKAGE = ""
@@ -56,6 +65,7 @@ class Args:
license_type: Optional[str] license_type: Optional[str]
non_interactive_mode: bool non_interactive_mode: bool
regenerate_thumbnails: bool regenerate_thumbnails: bool
reread_metadata: bool
root_directory: str root_directory: str
site_title: str site_title: str
theme_path: str theme_path: str
@@ -73,6 +83,7 @@ class Args:
result["license_type"] = self.license_type result["license_type"] = self.license_type
result["non_interactive_mode"] = self.non_interactive_mode result["non_interactive_mode"] = self.non_interactive_mode
result["regenerate_thumbnails"] = self.regenerate_thumbnails result["regenerate_thumbnails"] = self.regenerate_thumbnails
result["reread_metadata"] = self.reread_metadata
result["root_directory"] = self.root_directory result["root_directory"] = self.root_directory
result["site_title"] = self.site_title result["site_title"] = self.site_title
result["theme_path"] = self.theme_path result["theme_path"] = self.theme_path
@@ -96,22 +107,27 @@ def parse_arguments(version: str) -> Args:
An instance of the Args class containing the parsed arguments. An instance of the Args class containing the parsed arguments.
""" """
# fmt: off # fmt: off
if RICH:
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter) 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") else:
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg") parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.")
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("-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("-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("-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("-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("-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("-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("-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") 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")
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")
if RICH:
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("--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
parser.add_argument("--reread-metadata", help="Reread image metadata", action="store_true", default=False, dest="reread_metadata")
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}")
parsed_args = parser.parse_args() parsed_args = parser.parse_args()
# fmt: on # fmt: on
_args = Args( _args = Args(
@@ -123,10 +139,12 @@ def parse_arguments(version: str) -> Args:
license_type=parsed_args.license_type, license_type=parsed_args.license_type,
non_interactive_mode=parsed_args.non_interactive_mode, non_interactive_mode=parsed_args.non_interactive_mode,
regenerate_thumbnails=parsed_args.regenerate_thumbnails, regenerate_thumbnails=parsed_args.regenerate_thumbnails,
reread_metadata=parsed_args.reread_metadata,
root_directory=parsed_args.root_directory, root_directory=parsed_args.root_directory,
site_title=parsed_args.site_title, site_title=parsed_args.site_title,
theme_path=parsed_args.theme_path, theme_path=parsed_args.theme_path,
use_fancy_folders=parsed_args.use_fancy_folders, use_fancy_folders=parsed_args.use_fancy_folders,
web_root_url=parsed_args.web_root_url, web_root_url=parsed_args.web_root_url,
) )
logger.debug("parsed arguments", extra={"args": _args.to_dict()})
return _args return _args

View File

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

View File

@@ -1,14 +1,17 @@
import os import os
import re
import urllib.parse import urllib.parse
import fnmatch import fnmatch
import json import json
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from datetime import datetime
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 +48,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 +70,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)
@@ -83,13 +91,46 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
Returns: Returns:
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: file = os.path.join(folder, item)
with Image.open(file) as img:
logger.info("extracting image information", extra={"file": file})
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:
logger.info("extracting EXIF data", extra={"file": file})
ifd = exif.get_ifd(ExifTags.IFD.Exif)
exifdatas = dict(exif.items()) | ifd
exifdata = {}
for tag_id in exifdatas:
tag = ExifTags.TAGS.get(tag_id, tag_id)
content = exifdatas.get(tag_id)
if isinstance(content, bytes):
content = "0x" + 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
if tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]:
epr = r'\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}'
if re.match(epr, content):
content = datetime.strptime(content, "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d %H:%M:%S")
else:
content = None
exifdata[tag] = content
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]: if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
logger.info("image is rotated", extra={"file": file})
width, height = height, width width, height = height, width
return {"width": width, "height": height} 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, "exifdata": None}
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]:
@@ -108,7 +149,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
Dict[str, Any]: Dictionary containing image details for HTML rendering. Dict[str, Any]: Dictionary containing image details for HTML rendering.
""" """
extsplit = os.path.splitext(item) extsplit = os.path.splitext(item)
if item not in sizelist or _args.regenerate_thumbnails: if item not in sizelist or _args.reread_metadata:
sizelist[item] = get_image_info(item, folder) sizelist[item] = get_image_info(item, folder)
image = { image = {
@@ -117,6 +158,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
"name": item, "name": item,
"width": sizelist[item]["width"], "width": sizelist[item]["width"],
"height": sizelist[item]["height"], "height": sizelist[item]["height"],
"exifdata": sizelist[item]["exifdata"],
} }
path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg") path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg")
if not os.path.exists(path) or _args.regenerate_thumbnails: if not os.path.exists(path) or _args.regenerate_thumbnails:
@@ -125,12 +167,15 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
thumbnails.append((folder, item, _args.root_directory)) thumbnails.append((folder, item, _args.root_directory))
for _raw in raw: for _raw in raw:
if os.path.exists(os.path.join(folder, extsplit[0] + _raw)): file = os.path.join(folder, extsplit[0] + _raw)
url = urllib.parse.quote(extsplit[0]) + _raw if os.path.exists(file):
url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}"
if _raw in (".tif", ".tiff"): if _raw in (".tif", ".tiff"):
image["tiff"] = f"{_args.web_root_url}{baseurl}{url}" logger.info("tiff file found", extra={"file": file})
image["tiff"] = url
else: else:
image["raw"] = f"{_args.web_root_url}{baseurl}{url}" logger.info("raw file found", extra={"file": file, "extension": _raw})
image["raw"] = url
return image return image
@@ -144,6 +189,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 +209,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 +233,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 +250,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 +282,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,7 +312,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.
""" """
image_chunks = np.array_split(images, 8) if images else [] html_file = os.path.join(folder, "index.html")
logger.info("generating html file with jinja2", extra={"path": html_file})
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] + '/'))}"
if parent and _args.web_root_url.startswith("file://"): if parent and _args.web_root_url.startswith("file://"):
@@ -290,14 +345,15 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
header=header, header=header,
license=license_info, license=license_info,
subdirectories=subfolders, subdirectories=subfolders,
images=image_chunks, images=images,
info=_info, info=_info,
allimages=images, allimages=images,
webmanifest=_args.generate_webmanifest, webmanifest=_args.generate_webmanifest,
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)

140
modules/logger.py Normal file
View File

@@ -0,0 +1,140 @@
"""
logger.py
This module provides functionality for setting up a centralized logging system using the
`logging` library and the `python-json-logger` to output logs in JSON format. It handles
log rotation by renaming old log files and saving them based on the first timestamp entry.
Functions:
- log_format(keys): Generates the logging format string based on the list of keys.
- rotate_log_file(): Handles renaming the existing log file to a timestamp-based name.
- setup_logger(): Configures the logging system, applies a JSON format, and returns a logger instance.
- setup_consolelogger(): Configures the logging system to output logs in console format.
"""
import os
import json
import gzip
import shutil
import logging
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(compress=False):
"""
Truncates the 'latest.jsonl' file after optionally compressing its contents to a timestamped file.
The 'latest.jsonl' file is not deleted or moved, just emptied.
Args:
compress (bool): If True, compress the old log file using gzip.
"""
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")
# Write contents to the new file
with open(old_log_filename, "w", encoding="utf-8") as old_log_file:
f.seek(0) # Go back to the beginning of the file
shutil.copyfileobj(f, old_log_file)
if compress:
with open(old_log_filename, "rb") as f_in:
with gzip.open(f"{old_log_filename}.gz", "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(old_log_filename)
# Truncate the original file
f.seek(0)
f.truncate()
def setup_logger(level=logging.INFO):
"""
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.
Returns:
logging.Logger: A configured logger instance that can be used to log messages.
"""
_logger = logging.getLogger(name="defaultlogger")
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
custom_format = " ".join(log_format(supported_keys))
formatter = jsonlogger.JsonFormatter(custom_format)
log_handler = logging.FileHandler(LATEST_LOG_FILE)
log_handler.setFormatter(formatter)
_logger.addHandler(log_handler)
_logger.setLevel(level=level)
return _logger
def setup_consolelogger(level=logging.INFO):
"""
Configures the logging system to output logs in console and 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.
Returns:
logging.Logger: A configured logger instance that can be used to log messages.
"""
_logger = logging.getLogger(name="consolelogger")
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
custom_format = " ".join(log_format(supported_keys))
formatter = jsonlogger.JsonFormatter(custom_format)
log_handler = logging.FileHandler(LATEST_LOG_FILE)
log_handler.setFormatter(formatter)
_logger.addHandler(log_handler)
_logger.addHandler(logging.StreamHandler())
_logger.setLevel(level=level)
return _logger
rotate_log_file(compress=True)
logger = setup_logger()
consolelogger = setup_consolelogger()

View File

@@ -1,6 +1,7 @@
import os import os
import shutil import shutil
from typing import List, Dict from typing import List, Dict
from subprocess import Popen, PIPE
from PIL import Image from PIL import Image
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@@ -13,6 +14,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 +57,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 +76,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,10 +91,25 @@ 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})
_env = dict(os.environ)
lp_key = "LD_LIBRARY_PATH"
lp_orig = _env.get(lp_key + "_ORIG")
if lp_orig is not None:
_env[lp_key] = lp_orig
else:
_env.pop(lp_key, None)
if not shutil.which("magick"): 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")}' magick = shutil.which("convert")
os.system(command) else:
magick = shutil.which("magick")
command = [magick, os.path.join(iconspath, "icon.png"), "-define", "icon:auto-resize=16,32,48,64,72,96,144,192", favicon]
with Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=_env, errors="ignore") as p:
out, err = p.communicate()
if p.returncode != 0:
logger.error("error generating favicon: %s", err, extra={"command": command, "out": out, "err": err})
logger.info("favicon generated successfully", extra={"command": command, "out": out, "err": err})
def icons(_args: Args) -> None: def icons(_args: Args) -> None:
@@ -102,12 +121,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 +156,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)
@@ -157,6 +179,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
List of icons created from the SVG file. List of icons created from the SVG file.
""" """
svg = [file for file in files if file.endswith(".svg")][0] svg = [file for file in files if file.endswith(".svg")][0]
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
icon_list = [ 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"},
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"}, {"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"},
@@ -165,6 +188,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={"svg": svg, "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 +197,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 +240,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={"iconspath": iconspath, "icon": icon, "size": iconsize})
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"}) icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "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
@@ -229,16 +255,15 @@ def webmanifest(_args: Args) -> None:
_args : Args _args : Args
Parsed command-line arguments. Parsed command-line arguments.
""" """
logger.info("generating webmanifest")
iconspath = os.path.join(_args.root_directory, ".static", "icons") iconspath = os.path.join(_args.root_directory, ".static", "icons")
files = os.listdir(iconspath) files = os.listdir(iconspath)
icon_list = ( icon_list = create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url)
create_icons_from_svg(files, iconspath, _args)
if SVGSUPPORT and any(file.endswith(".svg") for file in files)
else create_icons_from_png(iconspath, _args.web_root_url)
)
if not icon_list: if not icon_list:
print("No icons found in the static/icons folder!") print("No icons found in the static/icons folder!")
logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath})
return return
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css")) colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))

View File

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

View File

@@ -56,9 +56,8 @@
{% if images %} {% if images %}
{%- set ns = namespace(count = 0) -%} {%- set ns = namespace(count = 0) -%}
<div class="row"> <div class="row">
{%- for imageblock in images %} {%- for image in images %}
<div class="column"> <div class="column">
{%- for image in imageblock %}
<figure> <figure>
<img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" /> <img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" />
{%- set ns.count = ns.count + 1 %} {%- set ns.count = ns.count + 1 %}
@@ -71,7 +70,6 @@
{%- endif %} {%- endif %}
</figcaption> </figcaption>
</figure> </figure>
{%- endfor %}
</div> </div>
{%- endfor %} {%- endfor %}
</div> </div>
@@ -147,7 +145,11 @@
var pswpElement = document.querySelectorAll('.pswp')[0]; var pswpElement = document.querySelectorAll('.pswp')[0];
var items = [ var items = [
{%- for image in allimages %} {%- for image in allimages %}
{%- if image.exifdata.DateTime %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}", title: "Captured: {{ image.exifdata.DateTime }}" },
{%- else %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" }, { src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" },
{%- endif %}
{%- endfor %} {%- endfor %}
]; ];
var re = /pid=(\d+)/; var re = /pid=(\d+)/;
@@ -178,8 +180,7 @@
} }
function topFunction() { function topFunction() {
document.body.scrollTop = 0; window.scrollTo({ top: 0, behavior: 'smooth' })
document.documentElement.scrollTop = 0;
} }
</script> </script>
{%- endif %} {%- endif %}

BIN
test/example/DSC03470.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

BIN
test/example/DSC03508.ARW Executable file

Binary file not shown.

BIN
test/example/DSC03508.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 493 B

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

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

4
view-latest-log.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
LESS=-SR hl logs/latest.jsonl --config hl_config.yaml

3
view-logs.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
LESS=-SR hl $(ls -tr logs/*.{jsonl,jsonl.gz}) --config hl_config.yaml