18 Commits

Author SHA1 Message Date
6593e63ea9 updated themes version 2026-02-04 08:41:54 +01:00
027a5688cb updated dependencies 2026-02-04 07:53:28 +01:00
1aa9dd716b recursive darktheme applied 2026-02-04 07:17:29 +01:00
d1f7f62229 added automatic dark theme detection and dark mode switch 2026-02-03 15:35:45 +01:00
2f37f78039 updated subproject 2026-01-26 15:49:28 +01:00
b109627bf9 fixed so symlink gets resolved 2026-01-26 15:47:10 +01:00
4ef1b429f2 version bump 2026-01-07 14:36:21 +01:00
b8314c0e3d don't leave progressbar 2026-01-07 08:15:56 +01:00
60f3ca1f20 reduced thumbnail quality 2025-12-11 08:59:40 +01:00
bc519ac5d0 Remove duplicate 'Star History' section
Removed duplicate 'Star History' section from README.
2025-12-10 13:30:26 +01:00
b0c1b2ce48 Add Star History section to README 2025-12-10 13:29:56 +01:00
faa283efa3 tags optional 2025-11-25 07:07:47 +01:00
ce70aa7bd9 changed python version 2025-11-07 09:22:47 +01:00
59c7cfd406 breaking change: new requirement ConfigArgParse 2025-11-07 09:04:17 +01:00
cfe1585c39 added exception to log 2025-10-27 15:05:10 +01:00
57b6b56388 fixed TypeError 2025-10-27 12:06:14 +01:00
2c6f8fbcb7 move thumbdir declare 2025-10-27 11:12:03 +01:00
30eb1ed8df added metadata dataclass 2025-10-27 09:51:00 +01:00
18 changed files with 727 additions and 298 deletions

View File

@@ -8,7 +8,9 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
python-version: '3.12'
- name: Install PyInstaller
run: pip install pyinstaller
- name: Install Dependencies
run: pip install -r requirements.txt
- name: Build Package

View File

@@ -1 +1 @@
2.8.6
2.9.0

View File

@@ -103,3 +103,7 @@ To generate a web manifest file:
## License
This project is licensed under the AGPL-3.0 License. See the [LICENSE](LICENSE) file for details.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=greflm13/StaticGalleryBuilder&type=date&legend=top-left)](https://www.star-history.com/#greflm13/StaticGalleryBuilder&type=date&legend=top-left)

View File

@@ -18,11 +18,7 @@ from modules.argumentparser import parse_arguments, Args
# fmt: off
# Constants
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
STATIC_FILES_DIR = os.path.join(os.path.abspath(SCRIPTDIR), "files")
VERSION = open(os.path.join(SCRIPTDIR, ".version"), "r", encoding="utf-8").read()
RAW_EXTENSIONS = [
@@ -37,8 +33,8 @@ NOT_LIST = ["*/Galleries/*", "Archives"]
args = parse_arguments(VERSION)
lock_file = os.path.join(args.root_directory, ".lock")
if os.path.exists(lock_file):
LOCKFILE = os.path.join(args.root_directory, ".lock")
if os.path.exists(LOCKFILE):
print("Another instance of this program is running.")
sys.exit()
else:
@@ -75,7 +71,48 @@ def init_globals(_args: Args, raw: list[str]) -> tuple[Args, list[str]]:
return _args, raw
def copy_static_files(_args: Args) -> None:
def handle_theme_icon(themepath: str, dest: str) -> None:
"""
Handle the icon specified in the theme file.
"""
logger.info("reading theme file", extra={"theme": themepath})
with open(themepath, "r", encoding="utf-8") as f:
theme = f.read()
split = theme.split(".foldericon {")
split2 = split[1].split("}", maxsplit=1)
themehead = split[0]
themetail = split2[1]
foldericon = split2[0].strip()
foldericon = re.sub(r"/\*.*\*/", "", foldericon)
for match in re.finditer(r"content: (.*);", foldericon):
foldericon = match[1]
foldericon = foldericon.replace('"', "")
logger.info("found foldericon", extra={"foldericon": foldericon})
break
if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(themepath, dest)
else:
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
logger.info("Reading foldericon svg")
svg = f.read()
if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(themepath)
for color_key, color_value in colorscheme.items():
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
logger.info("replaced colors in svg")
svg = urllib.parse.quote(svg)
with open(dest, "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)
def copy_static_files(_args: Args) -> bool:
"""
Copy static files to the root directory.
@@ -85,6 +122,7 @@ def copy_static_files(_args: Args) -> None:
Parsed command-line arguments.
"""
static_dir = os.path.join(_args.root_directory, ".static")
darktheme = False
if os.path.exists(static_dir):
print("Removing existing .static folder...")
logger.info("removing existing .static folder")
@@ -93,42 +131,21 @@ def copy_static_files(_args: Args) -> None:
print("Copying static files...")
logger.info("copying static files")
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:
theme = f.read()
split = theme.split(".foldericon {")
split2 = split[1].split("}", maxsplit=1)
themehead = split[0]
themetail = split2[1]
foldericon = split2[0].strip()
foldericon = re.sub(r"/\*.*\*/", "", foldericon)
for match in re.finditer(r"content: (.*);", foldericon):
foldericon = match[1]
foldericon = foldericon.replace('"', "")
logger.info("found foldericon", extra={"foldericon": foldericon})
break
if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
return
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
logger.info("Reading foldericon svg")
svg = f.read()
if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(_args.theme_path)
for color_key, color_value in colorscheme.items():
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
logger.info("replaced colors in svg")
svg = urllib.parse.quote(svg)
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)
theme = os.path.splitext(os.path.abspath(_args.theme_path))[0]
darktheme_path = f"{theme}-dark.css"
if os.path.exists(darktheme_path):
handle_theme_icon(darktheme_path, os.path.join(static_dir, "theme-dark.css"))
darktheme = True
handle_theme_icon(_args.theme_path, os.path.join(static_dir, "theme.css"))
logger.info("minifying javascript")
with open(os.path.join(SCRIPTDIR, "templates", "functionality.js"), "r", encoding="utf-8") as js_file:
with open(os.path.join(static_dir, "functionality.min.js"), "w+", encoding="utf-8") as min_file:
min_file.write(jsmin(js_file.read()))
return darktheme
def generate_thumbnail(arguments: tuple[str, str, str]) -> None:
"""
@@ -155,7 +172,7 @@ def generate_thumbnail(arguments: tuple[str, str, str]) -> None:
imgrgb = imgfile.convert("RGB")
img = ImageOps.exif_transpose(imgrgb)
img.thumbnail((512, 512))
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB")
img.save(path, "JPEG", quality=50, optimize=True, mode="RGB", subsampling=2)
except OSError:
logger.error("Failed to generate thumbnail for %s", item, extra={"path": image})
print(f"Failed to generate thumbnail for {image}")
@@ -168,12 +185,13 @@ def main(args) -> None:
"""
Main function to process images and generate a static image hosting website.
"""
thumbnails: list[tuple[str, str, str, bool]] = []
thumbnails: list[tuple[str, str, str]] = []
args, raw = init_globals(args, RAW_EXTENSIONS)
thumbdir = os.path.join(args.root_directory, ".thumbnails")
try:
Path(lock_file).touch()
Path(LOCKFILE).touch()
logger.info("starting builder", extra={"version": VERSION, "arguments": args})
logger.info("getting logo from sorogon.eu")
@@ -195,12 +213,12 @@ def main(args) -> None:
logger.warning("reread metadata flag is set to true, all image metadata will be reread")
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(thumbdir):
logger.info("removing old thumbnails folder")
shutil.rmtree(os.path.join(args.root_directory, ".thumbnails"))
os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True)
shutil.rmtree(thumbdir)
os.makedirs(thumbdir, exist_ok=True)
copy_static_files(args)
args.darktheme = copy_static_files(args)
icons(args)
if args.generate_webmanifest:
@@ -229,8 +247,11 @@ def main(args) -> None:
dynamic_ncols=True,
):
pass
except Exception as e:
logger.critical("an unhandled exception occurred: %s", str(e), exc_info=True)
print(f"An unhandled exception occurred: {str(e)}")
finally:
os.remove(lock_file)
os.remove(LOCKFILE)
logger.info("finished builder", extra={"version": VERSION})

3
example.config Normal file
View File

@@ -0,0 +1,3 @@
root-directory = /path/to/webroot
site-title = Website Title
web-root-url = https://www.example.com

View File

@@ -77,10 +77,6 @@ figure {
margin: 0;
}
.licensefile {
padding: 30px;
}
.caption {
padding-top: 4px;
text-align: center;
@@ -93,7 +89,6 @@ figure {
position: absolute;
bottom: 0;
right: 0;
font-size: xx-small;
padding: 6px;
}
@@ -102,7 +97,8 @@ figure {
bottom: 0;
width: 100%;
padding: 6px;
min-height: calc(6.75pt + 12px);
height: calc(9.75pt + 12px);
font-size: small;
}
.footer a {
@@ -110,7 +106,7 @@ figure {
}
.footer a img {
height: 22px !important;
height: 9.75pt !important;
margin-left: 3px;
vertical-align: text-bottom;
}
@@ -140,16 +136,19 @@ figure {
.navbar .navleft {
float: left;
height: 100%;
}
.navbar .navcenter {
position: absolute;
left: 50%;
transform: translateX(-50%);
height: 100%;
}
.navbar .navright {
float: right
float: right;
height: 100%;
}
.navbar li .header {
@@ -303,6 +302,59 @@ input {
border-style: none;
}
.darkmodeswitch {
font-size: smaller;
height: 100%;
}
#dark-mode-switch {
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
position: relative;
}
#dark-mode-switch .checkbox {
position: absolute;
width: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
z-index: 2;
}
#dark-mode-switch .knobs {
display: flex;
align-items: center;
justify-content: center;
column-gap: 1em;
position: relative;
width: 100%;
}
#dark-mode-switch .light,
#dark-mode-switch .dark {
text-align: center;
}
#dark-mode-switch .slider {
position: absolute;
width: calc(2em - 2px);
height: calc(2em - 2px);
border: 1px solid currentColor;
border-radius: 3px;
top: 50%;
transform: translate(-50%, -50%);
transition: left 0.25s ease, transform 0.25s ease;
left: calc(50% - 1em + 1px);
}
#dark-mode-switch .checkbox:checked+.knobs .slider {
left: calc(50% + 1em - 1px);
}
@media screen and (max-width: 1000px) {
.column {
-ms-flex: 25%;
@@ -310,6 +362,14 @@ input {
max-width: 25%;
}
.footer {
font-size: small;
}
.footer a img {
height: 9.75pt !important;
}
.folders figure {
width: 160px;
}
@@ -343,6 +403,14 @@ input {
max-width: 50%;
}
.footer {
font-size: x-small;
}
.footer a img {
height: 7.5pt !important;
}
.folders figure {
width: 140px;
}
@@ -384,6 +452,14 @@ input {
max-width: 100%;
}
.footer {
font-size: xx-small;
}
.footer a img {
height: 6.75pt !important;
}
.folders figure {
width: 120px;
}

192
help.svg
View File

@@ -1,4 +1,4 @@
<svg class="rich-terminal" viewBox="0 0 1482 977.1999999999999" xmlns="http://www.w3.org/2000/svg">
<svg class="rich-terminal" viewBox="0 0 1482 1074.8" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,191 +19,207 @@
font-weight: 700;
}
.terminal-1232074479-matrix {
.terminal-1915255058-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-1232074479-title {
.terminal-1915255058-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-1232074479-r1 { fill: #ff8700 }
.terminal-1232074479-r2 { fill: #c5c8c6 }
.terminal-1232074479-r3 { fill: #808080 }
.terminal-1232074479-r4 { fill: #68a0b3 }
.terminal-1232074479-r5 { fill: #00af87 }
.terminal-1915255058-r1 { fill: #ff8700 }
.terminal-1915255058-r2 { fill: #c5c8c6 }
.terminal-1915255058-r3 { fill: #808080 }
.terminal-1915255058-r4 { fill: #68a0b3 }
.terminal-1915255058-r5 { fill: #00af87 }
</style>
<defs>
<clipPath id="terminal-1232074479-clip-terminal">
<rect x="0" y="0" width="1463.0" height="926.1999999999999" />
<clipPath id="terminal-1915255058-clip-terminal">
<rect x="0" y="0" width="1463.0" height="1023.8" />
</clipPath>
<clipPath id="terminal-1232074479-line-0">
<clipPath id="terminal-1915255058-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-1">
<clipPath id="terminal-1915255058-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-2">
<clipPath id="terminal-1915255058-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-3">
<clipPath id="terminal-1915255058-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-4">
<clipPath id="terminal-1915255058-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-5">
<clipPath id="terminal-1915255058-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-6">
<clipPath id="terminal-1915255058-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-7">
<clipPath id="terminal-1915255058-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-8">
<clipPath id="terminal-1915255058-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-9">
<clipPath id="terminal-1915255058-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-10">
<clipPath id="terminal-1915255058-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-11">
<clipPath id="terminal-1915255058-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-12">
<clipPath id="terminal-1915255058-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-13">
<clipPath id="terminal-1915255058-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-14">
<clipPath id="terminal-1915255058-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-15">
<clipPath id="terminal-1915255058-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-16">
<clipPath id="terminal-1915255058-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-17">
<clipPath id="terminal-1915255058-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-18">
<clipPath id="terminal-1915255058-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-19">
<clipPath id="terminal-1915255058-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-20">
<clipPath id="terminal-1915255058-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-21">
<clipPath id="terminal-1915255058-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-22">
<clipPath id="terminal-1915255058-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-23">
<clipPath id="terminal-1915255058-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-24">
<clipPath id="terminal-1915255058-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-25">
<clipPath id="terminal-1915255058-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-26">
<clipPath id="terminal-1915255058-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-27">
<clipPath id="terminal-1915255058-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-28">
<clipPath id="terminal-1915255058-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-29">
<clipPath id="terminal-1915255058-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-30">
<clipPath id="terminal-1915255058-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-31">
<clipPath id="terminal-1915255058-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-32">
<clipPath id="terminal-1915255058-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-33">
<clipPath id="terminal-1915255058-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-34">
<clipPath id="terminal-1915255058-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-35">
<clipPath id="terminal-1915255058-line-35">
<rect x="0" y="855.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1232074479-line-36">
<clipPath id="terminal-1915255058-line-36">
<rect x="0" y="879.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1915255058-line-37">
<rect x="0" y="904.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1915255058-line-38">
<rect x="0" y="928.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1915255058-line-39">
<rect x="0" y="953.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1915255058-line-40">
<rect x="0" y="977.5" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="975.2" rx="8"/>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="1072.8" rx="8"/>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-1232074479-clip-terminal)">
<g transform="translate(9, 41)" clip-path="url(#terminal-1915255058-clip-terminal)">
<g class="terminal-1232074479-matrix">
<text class="terminal-1232074479-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-1232074479-line-0)">Usage:</text><text class="terminal-1232074479-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-1232074479-line-0)">builder.py</text><text class="terminal-1232074479-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-h</text><text class="terminal-1232074479-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-a</text><text class="terminal-1232074479-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-1232074479-line-0)">AUTHOR</text><text class="terminal-1232074479-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-e</text><text class="terminal-1232074479-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-1232074479-line-0)">EXTENSION</text><text class="terminal-1232074479-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-l</text><text class="terminal-1232074479-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-1232074479-line-0)">LICENSE</text><text class="terminal-1232074479-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-m</text><text class="terminal-1232074479-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-n</text><text class="terminal-1232074479-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">]&#160;</text><text class="terminal-1232074479-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-p</text><text class="terminal-1232074479-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-1232074479-line-0)">ROOT</text><text class="terminal-1232074479-r4" x="988.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-t</text><text class="terminal-1232074479-r5" x="1024.8" y="20" textLength="61" clip-path="url(#terminal-1232074479-line-0)">TITLE</text><text class="terminal-1232074479-r4" x="1098" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-w</text><text class="terminal-1232074479-r5" x="1134.6" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">URL</text><text class="terminal-1232074479-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-1232074479-line-0)">
</text><text class="terminal-1232074479-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-1232074479-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-1232074479-line-1)">--exclude-folder</text><text class="terminal-1232074479-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-1232074479-line-1)">FOLDER</text><text class="terminal-1232074479-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-1232074479-line-1)">]&#160;[</text><text class="terminal-1232074479-r4" x="549" y="44.4" textLength="219.6" clip-path="url(#terminal-1232074479-line-1)">--folderthumbnails</text><text class="terminal-1232074479-r2" x="768.6" y="44.4" textLength="36.6" clip-path="url(#terminal-1232074479-line-1)">]&#160;[</text><text class="terminal-1232074479-r4" x="805.2" y="44.4" textLength="244" clip-path="url(#terminal-1232074479-line-1)">--ignore-other-files</text><text class="terminal-1232074479-r2" x="1049.2" y="44.4" textLength="36.6" clip-path="url(#terminal-1232074479-line-1)">]&#160;[</text><text class="terminal-1232074479-r4" x="1085.8" y="44.4" textLength="219.6" clip-path="url(#terminal-1232074479-line-1)">--ignore-extension</text><text class="terminal-1232074479-r5" x="1317.6" y="44.4" textLength="109.8" clip-path="url(#terminal-1232074479-line-1)">EXTENSION</text><text class="terminal-1232074479-r2" x="1427.4" y="44.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-1)">]</text><text class="terminal-1232074479-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-1)">
</text><text class="terminal-1232074479-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-1232074479-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="68.8" textLength="280.6" clip-path="url(#terminal-1232074479-line-2)">--regenerate-thumbnails</text><text class="terminal-1232074479-r2" x="512.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-2)">]&#160;[</text><text class="terminal-1232074479-r4" x="549" y="68.8" textLength="207.4" clip-path="url(#terminal-1232074479-line-2)">--reread-metadata</text><text class="terminal-1232074479-r2" x="756.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-2)">]&#160;[</text><text class="terminal-1232074479-r4" x="793" y="68.8" textLength="195.2" clip-path="url(#terminal-1232074479-line-2)">--reread-sidecar</text><text class="terminal-1232074479-r2" x="988.2" y="68.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-2)">]&#160;[</text><text class="terminal-1232074479-r4" x="1024.8" y="68.8" textLength="170.8" clip-path="url(#terminal-1232074479-line-2)">--reverse-sort</text><text class="terminal-1232074479-r2" x="1195.6" y="68.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-2)">]</text><text class="terminal-1232074479-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-2)">
</text><text class="terminal-1232074479-r2" x="0" y="93.2" textLength="231.8" clip-path="url(#terminal-1232074479-line-3)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="93.2" textLength="146.4" clip-path="url(#terminal-1232074479-line-3)">--theme-path</text><text class="terminal-1232074479-r5" x="390.4" y="93.2" textLength="48.8" clip-path="url(#terminal-1232074479-line-3)">PATH</text><text class="terminal-1232074479-r2" x="439.2" y="93.2" textLength="36.6" clip-path="url(#terminal-1232074479-line-3)">]&#160;[</text><text class="terminal-1232074479-r4" x="475.8" y="93.2" textLength="231.8" clip-path="url(#terminal-1232074479-line-3)">--use-fancy-folders</text><text class="terminal-1232074479-r2" x="707.6" y="93.2" textLength="36.6" clip-path="url(#terminal-1232074479-line-3)">]&#160;[</text><text class="terminal-1232074479-r4" x="744.2" y="93.2" textLength="109.8" clip-path="url(#terminal-1232074479-line-3)">--version</text><text class="terminal-1232074479-r2" x="854" y="93.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-3)">]</text><text class="terminal-1232074479-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-3)">
</text><text class="terminal-1232074479-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-4)">
</text><text class="terminal-1232074479-r2" x="0" y="142" textLength="658.8" clip-path="url(#terminal-1232074479-line-5)">generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website</text><text class="terminal-1232074479-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-1232074479-line-5)">
</text><text class="terminal-1232074479-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-6)">
</text><text class="terminal-1232074479-r1" x="0" y="190.8" textLength="97.6" clip-path="url(#terminal-1232074479-line-7)">Options:</text><text class="terminal-1232074479-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-7)">
</text><text class="terminal-1232074479-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-8)">-h</text><text class="terminal-1232074479-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-8)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="215.2" textLength="73.2" clip-path="url(#terminal-1232074479-line-8)">--help</text><text class="terminal-1232074479-r2" x="292.8" y="215.2" textLength="378.2" clip-path="url(#terminal-1232074479-line-8)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-1232074479-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-8)">
</text><text class="terminal-1232074479-r4" x="24.4" y="239.6" textLength="24.4" clip-path="url(#terminal-1232074479-line-9)">-a</text><text class="terminal-1232074479-r2" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-1232074479-line-9)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="239.6" textLength="158.6" clip-path="url(#terminal-1232074479-line-9)">--author-name</text><text class="terminal-1232074479-r5" x="244" y="239.6" textLength="73.2" clip-path="url(#terminal-1232074479-line-9)">AUTHOR</text><text class="terminal-1232074479-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-9)">
</text><text class="terminal-1232074479-r2" x="292.8" y="264" textLength="390.4" clip-path="url(#terminal-1232074479-line-10)">name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images</text><text class="terminal-1232074479-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-1232074479-line-10)">
</text><text class="terminal-1232074479-r4" x="24.4" y="288.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-11)">-e</text><text class="terminal-1232074479-r2" x="48.8" y="288.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-11)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="288.4" textLength="207.4" clip-path="url(#terminal-1232074479-line-11)">--file-extensions</text><text class="terminal-1232074479-r5" x="292.8" y="288.4" textLength="109.8" clip-path="url(#terminal-1232074479-line-11)">EXTENSION</text><text class="terminal-1232074479-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-11)">
</text><text class="terminal-1232074479-r2" x="292.8" y="312.8" textLength="732" clip-path="url(#terminal-1232074479-line-12)">file&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1232074479-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-12)">
</text><text class="terminal-1232074479-r4" x="24.4" y="337.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-13)">-l</text><text class="terminal-1232074479-r2" x="48.8" y="337.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-13)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="337.2" textLength="170.8" clip-path="url(#terminal-1232074479-line-13)">--license-type</text><text class="terminal-1232074479-r5" x="256.2" y="337.2" textLength="85.4" clip-path="url(#terminal-1232074479-line-13)">LICENSE</text><text class="terminal-1232074479-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-13)">
</text><text class="terminal-1232074479-r2" x="292.8" y="361.6" textLength="475.8" clip-path="url(#terminal-1232074479-line-14)">specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images</text><text class="terminal-1232074479-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-14)">
</text><text class="terminal-1232074479-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-1232074479-line-15)">-m</text><text class="terminal-1232074479-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-1232074479-line-15)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="386" textLength="170.8" clip-path="url(#terminal-1232074479-line-15)">--web-manifest</text><text class="terminal-1232074479-r2" x="292.8" y="386" textLength="341.6" clip-path="url(#terminal-1232074479-line-15)">generate&#160;a&#160;web&#160;manifest&#160;file</text><text class="terminal-1232074479-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-1232074479-line-15)">
</text><text class="terminal-1232074479-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-16)">-n</text><text class="terminal-1232074479-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-16)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-1232074479-line-16)">--non-interactive-mode</text><text class="terminal-1232074479-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-16)">
</text><text class="terminal-1232074479-r2" x="292.8" y="434.8" textLength="634.4" clip-path="url(#terminal-1232074479-line-17)">run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars</text><text class="terminal-1232074479-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-17)">
</text><text class="terminal-1232074479-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-18)">-p</text><text class="terminal-1232074479-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-18)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="459.2" textLength="195.2" clip-path="url(#terminal-1232074479-line-18)">--root-directory</text><text class="terminal-1232074479-r5" x="280.6" y="459.2" textLength="48.8" clip-path="url(#terminal-1232074479-line-18)">ROOT</text><text class="terminal-1232074479-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-18)">
</text><text class="terminal-1232074479-r2" x="292.8" y="483.6" textLength="439.2" clip-path="url(#terminal-1232074479-line-19)">root&#160;directory&#160;containing&#160;the&#160;images</text><text class="terminal-1232074479-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-19)">
</text><text class="terminal-1232074479-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-1232074479-line-20)">-t</text><text class="terminal-1232074479-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-1232074479-line-20)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="508" textLength="146.4" clip-path="url(#terminal-1232074479-line-20)">--site-title</text><text class="terminal-1232074479-r5" x="231.8" y="508" textLength="61" clip-path="url(#terminal-1232074479-line-20)">TITLE</text><text class="terminal-1232074479-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-1232074479-line-20)">
</text><text class="terminal-1232074479-r2" x="292.8" y="532.4" textLength="378.2" clip-path="url(#terminal-1232074479-line-21)">title&#160;of&#160;the&#160;image&#160;hosting&#160;site</text><text class="terminal-1232074479-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-21)">
</text><text class="terminal-1232074479-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-1232074479-line-22)">-w</text><text class="terminal-1232074479-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-1232074479-line-22)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="556.8" textLength="170.8" clip-path="url(#terminal-1232074479-line-22)">--web-root-url</text><text class="terminal-1232074479-r5" x="256.2" y="556.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-22)">URL</text><text class="terminal-1232074479-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-22)">
</text><text class="terminal-1232074479-r2" x="292.8" y="581.2" textLength="622.2" clip-path="url(#terminal-1232074479-line-23)">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-1232074479-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-23)">
</text><text class="terminal-1232074479-r4" x="24.4" y="605.6" textLength="195.2" clip-path="url(#terminal-1232074479-line-24)">--exclude-folder</text><text class="terminal-1232074479-r5" x="231.8" y="605.6" textLength="73.2" clip-path="url(#terminal-1232074479-line-24)">FOLDER</text><text class="terminal-1232074479-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-24)">
</text><text class="terminal-1232074479-r2" x="292.8" y="630" textLength="1037" clip-path="url(#terminal-1232074479-line-25)">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-1232074479-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-1232074479-line-25)">
</text><text class="terminal-1232074479-r4" x="24.4" y="654.4" textLength="219.6" clip-path="url(#terminal-1232074479-line-26)">--folderthumbnails</text><text class="terminal-1232074479-r2" x="292.8" y="654.4" textLength="817.4" clip-path="url(#terminal-1232074479-line-26)">generate&#160;subfolder&#160;thumbnails&#160;(first&#160;image&#160;in&#160;folder&#160;will&#160;be&#160;shown)</text><text class="terminal-1232074479-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-26)">
</text><text class="terminal-1232074479-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-1232074479-line-27)">--ignore-other-files</text><text class="terminal-1232074479-r2" x="292.8" y="678.8" textLength="671" clip-path="url(#terminal-1232074479-line-27)">ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions</text><text class="terminal-1232074479-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-27)">
</text><text class="terminal-1232074479-r4" x="24.4" y="703.2" textLength="219.6" clip-path="url(#terminal-1232074479-line-28)">--ignore-extension</text><text class="terminal-1232074479-r5" x="256.2" y="703.2" textLength="109.8" clip-path="url(#terminal-1232074479-line-28)">EXTENSION</text><text class="terminal-1232074479-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-28)">
</text><text class="terminal-1232074479-r2" x="292.8" y="727.6" textLength="719.8" clip-path="url(#terminal-1232074479-line-29)">file&#160;extensions&#160;to&#160;ignore&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1232074479-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-29)">
</text><text class="terminal-1232074479-r4" x="24.4" y="752" textLength="280.6" clip-path="url(#terminal-1232074479-line-30)">--regenerate-thumbnails</text><text class="terminal-1232074479-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-1232074479-line-30)">
</text><text class="terminal-1232074479-r2" x="292.8" y="776.4" textLength="585.6" clip-path="url(#terminal-1232074479-line-31)">regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist</text><text class="terminal-1232074479-r2" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-31)">
</text><text class="terminal-1232074479-r4" x="24.4" y="800.8" textLength="207.4" clip-path="url(#terminal-1232074479-line-32)">--reread-metadata</text><text class="terminal-1232074479-r2" x="292.8" y="800.8" textLength="256.2" clip-path="url(#terminal-1232074479-line-32)">reread&#160;image&#160;metadata</text><text class="terminal-1232074479-r2" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-32)">
</text><text class="terminal-1232074479-r4" x="24.4" y="825.2" textLength="195.2" clip-path="url(#terminal-1232074479-line-33)">--reread-sidecar</text><text class="terminal-1232074479-r2" x="292.8" y="825.2" textLength="244" clip-path="url(#terminal-1232074479-line-33)">reread&#160;sidecar&#160;files</text><text class="terminal-1232074479-r2" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-33)">
</text><text class="terminal-1232074479-r4" x="24.4" y="849.6" textLength="170.8" clip-path="url(#terminal-1232074479-line-34)">--reverse-sort</text><text class="terminal-1232074479-r2" x="292.8" y="849.6" textLength="341.6" clip-path="url(#terminal-1232074479-line-34)">sort&#160;images&#160;in&#160;reverse&#160;order</text><text class="terminal-1232074479-r2" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-34)">
</text><text class="terminal-1232074479-r4" x="24.4" y="874" textLength="146.4" clip-path="url(#terminal-1232074479-line-35)">--theme-path</text><text class="terminal-1232074479-r5" x="183" y="874" textLength="48.8" clip-path="url(#terminal-1232074479-line-35)">PATH</text><text class="terminal-1232074479-r2" x="292.8" y="874" textLength="317.2" clip-path="url(#terminal-1232074479-line-35)">path&#160;to&#160;the&#160;CSS&#160;theme&#160;file</text><text class="terminal-1232074479-r2" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-1232074479-line-35)">
</text><text class="terminal-1232074479-r4" x="24.4" y="898.4" textLength="231.8" clip-path="url(#terminal-1232074479-line-36)">--use-fancy-folders</text><text class="terminal-1232074479-r2" x="292.8" y="898.4" textLength="878.4" clip-path="url(#terminal-1232074479-line-36)">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-1232074479-r2" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-36)">
</text><text class="terminal-1232074479-r4" x="24.4" y="922.8" textLength="109.8" clip-path="url(#terminal-1232074479-line-37)">--version</text><text class="terminal-1232074479-r2" x="292.8" y="922.8" textLength="463.6" clip-path="url(#terminal-1232074479-line-37)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-1232074479-r2" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-37)">
<g class="terminal-1915255058-matrix">
<text class="terminal-1915255058-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-1915255058-line-0)">Usage:</text><text class="terminal-1915255058-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-1915255058-line-0)">builder.py</text><text class="terminal-1915255058-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-h</text><text class="terminal-1915255058-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-a</text><text class="terminal-1915255058-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-1915255058-line-0)">AUTHOR</text><text class="terminal-1915255058-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-e</text><text class="terminal-1915255058-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-1915255058-line-0)">EXTENSION</text><text class="terminal-1915255058-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-l</text><text class="terminal-1915255058-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-1915255058-line-0)">LICENSE</text><text class="terminal-1915255058-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-m</text><text class="terminal-1915255058-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-n</text><text class="terminal-1915255058-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">]&#160;</text><text class="terminal-1915255058-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-p</text><text class="terminal-1915255058-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-1915255058-line-0)">ROOT</text><text class="terminal-1915255058-r4" x="988.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-t</text><text class="terminal-1915255058-r5" x="1024.8" y="20" textLength="61" clip-path="url(#terminal-1915255058-line-0)">TITLE</text><text class="terminal-1915255058-r4" x="1098" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-w</text><text class="terminal-1915255058-r5" x="1134.6" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">URL</text><text class="terminal-1915255058-r2" x="1171.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">&#160;[</text><text class="terminal-1915255058-r4" x="1195.6" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-c</text><text class="terminal-1915255058-r5" x="1232.2" y="20" textLength="134.2" clip-path="url(#terminal-1915255058-line-0)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1366.4" y="20" textLength="12.2" clip-path="url(#terminal-1915255058-line-0)">]</text><text class="terminal-1915255058-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-1915255058-line-0)">
</text><text class="terminal-1915255058-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-1915255058-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-1915255058-line-1)">--exclude-folder</text><text class="terminal-1915255058-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-1915255058-line-1)">FOLDER</text><text class="terminal-1915255058-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-1915255058-line-1)">]&#160;[</text><text class="terminal-1915255058-r4" x="549" y="44.4" textLength="219.6" clip-path="url(#terminal-1915255058-line-1)">--folderthumbnails</text><text class="terminal-1915255058-r2" x="768.6" y="44.4" textLength="36.6" clip-path="url(#terminal-1915255058-line-1)">]&#160;[</text><text class="terminal-1915255058-r4" x="805.2" y="44.4" textLength="244" clip-path="url(#terminal-1915255058-line-1)">--ignore-other-files</text><text class="terminal-1915255058-r2" x="1049.2" y="44.4" textLength="36.6" clip-path="url(#terminal-1915255058-line-1)">]&#160;[</text><text class="terminal-1915255058-r4" x="1085.8" y="44.4" textLength="219.6" clip-path="url(#terminal-1915255058-line-1)">--ignore-extension</text><text class="terminal-1915255058-r5" x="1317.6" y="44.4" textLength="109.8" clip-path="url(#terminal-1915255058-line-1)">EXTENSION</text><text class="terminal-1915255058-r2" x="1427.4" y="44.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-1)">]</text><text class="terminal-1915255058-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-1)">
</text><text class="terminal-1915255058-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-1915255058-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="68.8" textLength="280.6" clip-path="url(#terminal-1915255058-line-2)">--regenerate-thumbnails</text><text class="terminal-1915255058-r2" x="512.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-2)">]&#160;[</text><text class="terminal-1915255058-r4" x="549" y="68.8" textLength="207.4" clip-path="url(#terminal-1915255058-line-2)">--reread-metadata</text><text class="terminal-1915255058-r2" x="756.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-2)">]&#160;[</text><text class="terminal-1915255058-r4" x="793" y="68.8" textLength="195.2" clip-path="url(#terminal-1915255058-line-2)">--reread-sidecar</text><text class="terminal-1915255058-r2" x="988.2" y="68.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-2)">]&#160;[</text><text class="terminal-1915255058-r4" x="1024.8" y="68.8" textLength="170.8" clip-path="url(#terminal-1915255058-line-2)">--reverse-sort</text><text class="terminal-1915255058-r2" x="1195.6" y="68.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-2)">]</text><text class="terminal-1915255058-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-2)">
</text><text class="terminal-1915255058-r2" x="0" y="93.2" textLength="231.8" clip-path="url(#terminal-1915255058-line-3)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="93.2" textLength="146.4" clip-path="url(#terminal-1915255058-line-3)">--theme-path</text><text class="terminal-1915255058-r5" x="390.4" y="93.2" textLength="48.8" clip-path="url(#terminal-1915255058-line-3)">PATH</text><text class="terminal-1915255058-r2" x="439.2" y="93.2" textLength="36.6" clip-path="url(#terminal-1915255058-line-3)">]&#160;[</text><text class="terminal-1915255058-r4" x="475.8" y="93.2" textLength="231.8" clip-path="url(#terminal-1915255058-line-3)">--use-fancy-folders</text><text class="terminal-1915255058-r2" x="707.6" y="93.2" textLength="36.6" clip-path="url(#terminal-1915255058-line-3)">]&#160;[</text><text class="terminal-1915255058-r4" x="744.2" y="93.2" textLength="109.8" clip-path="url(#terminal-1915255058-line-3)">--version</text><text class="terminal-1915255058-r2" x="854" y="93.2" textLength="36.6" clip-path="url(#terminal-1915255058-line-3)">]&#160;[</text><text class="terminal-1915255058-r4" x="890.6" y="93.2" textLength="170.8" clip-path="url(#terminal-1915255058-line-3)">--write-config</text><text class="terminal-1915255058-r5" x="1073.6" y="93.2" textLength="134.2" clip-path="url(#terminal-1915255058-line-3)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1207.8" y="93.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-3)">]</text><text class="terminal-1915255058-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-3)">
</text><text class="terminal-1915255058-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-4)">
</text><text class="terminal-1915255058-r2" x="0" y="142" textLength="658.8" clip-path="url(#terminal-1915255058-line-5)">generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website</text><text class="terminal-1915255058-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-1915255058-line-5)">
</text><text class="terminal-1915255058-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-6)">
</text><text class="terminal-1915255058-r1" x="0" y="190.8" textLength="97.6" clip-path="url(#terminal-1915255058-line-7)">Options:</text><text class="terminal-1915255058-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-7)">
</text><text class="terminal-1915255058-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-8)">-h</text><text class="terminal-1915255058-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-8)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="215.2" textLength="73.2" clip-path="url(#terminal-1915255058-line-8)">--help</text><text class="terminal-1915255058-r2" x="292.8" y="215.2" textLength="378.2" clip-path="url(#terminal-1915255058-line-8)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-1915255058-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-8)">
</text><text class="terminal-1915255058-r4" x="24.4" y="239.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-9)">-a</text><text class="terminal-1915255058-r2" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-9)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="239.6" textLength="158.6" clip-path="url(#terminal-1915255058-line-9)">--author-name</text><text class="terminal-1915255058-r5" x="244" y="239.6" textLength="73.2" clip-path="url(#terminal-1915255058-line-9)">AUTHOR</text><text class="terminal-1915255058-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-9)">
</text><text class="terminal-1915255058-r2" x="292.8" y="264" textLength="390.4" clip-path="url(#terminal-1915255058-line-10)">name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images</text><text class="terminal-1915255058-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-1915255058-line-10)">
</text><text class="terminal-1915255058-r4" x="24.4" y="288.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-11)">-e</text><text class="terminal-1915255058-r2" x="48.8" y="288.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-11)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="288.4" textLength="207.4" clip-path="url(#terminal-1915255058-line-11)">--file-extensions</text><text class="terminal-1915255058-r5" x="292.8" y="288.4" textLength="109.8" clip-path="url(#terminal-1915255058-line-11)">EXTENSION</text><text class="terminal-1915255058-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-11)">
</text><text class="terminal-1915255058-r2" x="292.8" y="312.8" textLength="732" clip-path="url(#terminal-1915255058-line-12)">file&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1915255058-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-12)">
</text><text class="terminal-1915255058-r4" x="24.4" y="337.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-13)">-l</text><text class="terminal-1915255058-r2" x="48.8" y="337.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-13)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="337.2" textLength="170.8" clip-path="url(#terminal-1915255058-line-13)">--license-type</text><text class="terminal-1915255058-r5" x="256.2" y="337.2" textLength="85.4" clip-path="url(#terminal-1915255058-line-13)">LICENSE</text><text class="terminal-1915255058-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-13)">
</text><text class="terminal-1915255058-r2" x="292.8" y="361.6" textLength="475.8" clip-path="url(#terminal-1915255058-line-14)">specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images</text><text class="terminal-1915255058-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-14)">
</text><text class="terminal-1915255058-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-1915255058-line-15)">-m</text><text class="terminal-1915255058-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-1915255058-line-15)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="386" textLength="170.8" clip-path="url(#terminal-1915255058-line-15)">--web-manifest</text><text class="terminal-1915255058-r2" x="292.8" y="386" textLength="341.6" clip-path="url(#terminal-1915255058-line-15)">generate&#160;a&#160;web&#160;manifest&#160;file</text><text class="terminal-1915255058-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-1915255058-line-15)">
</text><text class="terminal-1915255058-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-16)">-n</text><text class="terminal-1915255058-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-16)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-1915255058-line-16)">--non-interactive-mode</text><text class="terminal-1915255058-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-16)">
</text><text class="terminal-1915255058-r2" x="292.8" y="434.8" textLength="634.4" clip-path="url(#terminal-1915255058-line-17)">run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars</text><text class="terminal-1915255058-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-17)">
</text><text class="terminal-1915255058-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-18)">-p</text><text class="terminal-1915255058-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-18)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="459.2" textLength="195.2" clip-path="url(#terminal-1915255058-line-18)">--root-directory</text><text class="terminal-1915255058-r5" x="280.6" y="459.2" textLength="48.8" clip-path="url(#terminal-1915255058-line-18)">ROOT</text><text class="terminal-1915255058-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-18)">
</text><text class="terminal-1915255058-r2" x="292.8" y="483.6" textLength="439.2" clip-path="url(#terminal-1915255058-line-19)">root&#160;directory&#160;containing&#160;the&#160;images</text><text class="terminal-1915255058-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-19)">
</text><text class="terminal-1915255058-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-1915255058-line-20)">-t</text><text class="terminal-1915255058-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-1915255058-line-20)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="508" textLength="146.4" clip-path="url(#terminal-1915255058-line-20)">--site-title</text><text class="terminal-1915255058-r5" x="231.8" y="508" textLength="61" clip-path="url(#terminal-1915255058-line-20)">TITLE</text><text class="terminal-1915255058-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-1915255058-line-20)">
</text><text class="terminal-1915255058-r2" x="292.8" y="532.4" textLength="378.2" clip-path="url(#terminal-1915255058-line-21)">title&#160;of&#160;the&#160;image&#160;hosting&#160;site</text><text class="terminal-1915255058-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-21)">
</text><text class="terminal-1915255058-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-1915255058-line-22)">-w</text><text class="terminal-1915255058-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-1915255058-line-22)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="556.8" textLength="170.8" clip-path="url(#terminal-1915255058-line-22)">--web-root-url</text><text class="terminal-1915255058-r5" x="256.2" y="556.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-22)">URL</text><text class="terminal-1915255058-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-22)">
</text><text class="terminal-1915255058-r2" x="292.8" y="581.2" textLength="622.2" clip-path="url(#terminal-1915255058-line-23)">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-1915255058-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-23)">
</text><text class="terminal-1915255058-r4" x="24.4" y="605.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-24)">-c</text><text class="terminal-1915255058-r2" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-24)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="605.6" textLength="158.6" clip-path="url(#terminal-1915255058-line-24)">--config-file</text><text class="terminal-1915255058-r5" x="244" y="605.6" textLength="134.2" clip-path="url(#terminal-1915255058-line-24)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-24)">
</text><text class="terminal-1915255058-r2" x="292.8" y="630" textLength="195.2" clip-path="url(#terminal-1915255058-line-25)">config&#160;file&#160;path</text><text class="terminal-1915255058-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-1915255058-line-25)">
</text><text class="terminal-1915255058-r4" x="24.4" y="654.4" textLength="195.2" clip-path="url(#terminal-1915255058-line-26)">--exclude-folder</text><text class="terminal-1915255058-r5" x="231.8" y="654.4" textLength="73.2" clip-path="url(#terminal-1915255058-line-26)">FOLDER</text><text class="terminal-1915255058-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-26)">
</text><text class="terminal-1915255058-r2" x="292.8" y="678.8" textLength="1037" clip-path="url(#terminal-1915255058-line-27)">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-1915255058-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-27)">
</text><text class="terminal-1915255058-r4" x="24.4" y="703.2" textLength="219.6" clip-path="url(#terminal-1915255058-line-28)">--folderthumbnails</text><text class="terminal-1915255058-r2" x="292.8" y="703.2" textLength="817.4" clip-path="url(#terminal-1915255058-line-28)">generate&#160;subfolder&#160;thumbnails&#160;(first&#160;image&#160;in&#160;folder&#160;will&#160;be&#160;shown)</text><text class="terminal-1915255058-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-28)">
</text><text class="terminal-1915255058-r4" x="24.4" y="727.6" textLength="244" clip-path="url(#terminal-1915255058-line-29)">--ignore-other-files</text><text class="terminal-1915255058-r2" x="292.8" y="727.6" textLength="671" clip-path="url(#terminal-1915255058-line-29)">ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions</text><text class="terminal-1915255058-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-29)">
</text><text class="terminal-1915255058-r4" x="24.4" y="752" textLength="219.6" clip-path="url(#terminal-1915255058-line-30)">--ignore-extension</text><text class="terminal-1915255058-r5" x="256.2" y="752" textLength="109.8" clip-path="url(#terminal-1915255058-line-30)">EXTENSION</text><text class="terminal-1915255058-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-1915255058-line-30)">
</text><text class="terminal-1915255058-r2" x="292.8" y="776.4" textLength="719.8" clip-path="url(#terminal-1915255058-line-31)">file&#160;extensions&#160;to&#160;ignore&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1915255058-r2" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-31)">
</text><text class="terminal-1915255058-r4" x="24.4" y="800.8" textLength="280.6" clip-path="url(#terminal-1915255058-line-32)">--regenerate-thumbnails</text><text class="terminal-1915255058-r2" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-32)">
</text><text class="terminal-1915255058-r2" x="292.8" y="825.2" textLength="585.6" clip-path="url(#terminal-1915255058-line-33)">regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist</text><text class="terminal-1915255058-r2" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-33)">
</text><text class="terminal-1915255058-r4" x="24.4" y="849.6" textLength="207.4" clip-path="url(#terminal-1915255058-line-34)">--reread-metadata</text><text class="terminal-1915255058-r2" x="292.8" y="849.6" textLength="256.2" clip-path="url(#terminal-1915255058-line-34)">reread&#160;image&#160;metadata</text><text class="terminal-1915255058-r2" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-34)">
</text><text class="terminal-1915255058-r4" x="24.4" y="874" textLength="195.2" clip-path="url(#terminal-1915255058-line-35)">--reread-sidecar</text><text class="terminal-1915255058-r2" x="292.8" y="874" textLength="244" clip-path="url(#terminal-1915255058-line-35)">reread&#160;sidecar&#160;files</text><text class="terminal-1915255058-r2" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-1915255058-line-35)">
</text><text class="terminal-1915255058-r4" x="24.4" y="898.4" textLength="170.8" clip-path="url(#terminal-1915255058-line-36)">--reverse-sort</text><text class="terminal-1915255058-r2" x="292.8" y="898.4" textLength="341.6" clip-path="url(#terminal-1915255058-line-36)">sort&#160;images&#160;in&#160;reverse&#160;order</text><text class="terminal-1915255058-r2" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-36)">
</text><text class="terminal-1915255058-r4" x="24.4" y="922.8" textLength="146.4" clip-path="url(#terminal-1915255058-line-37)">--theme-path</text><text class="terminal-1915255058-r5" x="183" y="922.8" textLength="48.8" clip-path="url(#terminal-1915255058-line-37)">PATH</text><text class="terminal-1915255058-r2" x="292.8" y="922.8" textLength="317.2" clip-path="url(#terminal-1915255058-line-37)">path&#160;to&#160;the&#160;CSS&#160;theme&#160;file</text><text class="terminal-1915255058-r2" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-37)">
</text><text class="terminal-1915255058-r4" x="24.4" y="947.2" textLength="231.8" clip-path="url(#terminal-1915255058-line-38)">--use-fancy-folders</text><text class="terminal-1915255058-r2" x="292.8" y="947.2" textLength="878.4" clip-path="url(#terminal-1915255058-line-38)">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-1915255058-r2" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-38)">
</text><text class="terminal-1915255058-r4" x="24.4" y="971.6" textLength="109.8" clip-path="url(#terminal-1915255058-line-39)">--version</text><text class="terminal-1915255058-r2" x="292.8" y="971.6" textLength="463.6" clip-path="url(#terminal-1915255058-line-39)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-1915255058-r2" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-39)">
</text><text class="terminal-1915255058-r4" x="24.4" y="996" textLength="170.8" clip-path="url(#terminal-1915255058-line-40)">--write-config</text><text class="terminal-1915255058-r5" x="207.4" y="996" textLength="134.2" clip-path="url(#terminal-1915255058-line-40)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1464" y="996" textLength="12.2" clip-path="url(#terminal-1915255058-line-40)">
</text><text class="terminal-1915255058-r2" x="292.8" y="1020.4" textLength="561.2" clip-path="url(#terminal-1915255058-line-41)">write&#160;current&#160;command&#160;line&#160;args&#160;to&#160;config&#160;file</text><text class="terminal-1915255058-r2" x="1464" y="1020.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-41)">
</text>
</g>
</g>

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,7 +1,8 @@
from dataclasses import dataclass
from typing import Optional
import os
import argparse
import configargparse
try:
from rich_argparse import RichHelpFormatter, HelpPreviewAction
@@ -11,14 +12,18 @@ except ModuleNotFoundError:
RICH = False
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__)
DEFAULT_THEME_PATH = os.path.join(SCRIPTDIR, "templates", "default.css")
DEFAULT_AUTHOR = "Author"
if "APPDATA" in os.environ:
CONFIGHOME = os.environ["APPDATA"]
elif "XDG_CONFIG_HOME" in os.environ:
CONFIGHOME = os.environ["XDG_CONFIG_HOME"]
else:
CONFIGHOME = os.path.join(os.environ["HOME"], ".config")
CONFIGPATH = os.path.join(CONFIGHOME, "StaticGalleryBuilder")
@dataclass(init=True)
class Args:
@@ -55,6 +60,8 @@ class Args:
Whether to enable fancy folder view.
web_root_url : str
The base URL of the web root for the image hosting site.
darktheme : bool
Whether a dark theme is present.
"""
author_name: str
@@ -75,6 +82,7 @@ class Args:
theme_path: str
use_fancy_folders: bool
web_root_url: str
darktheme: bool = False
def to_dict(self) -> dict:
result: dict = {}
@@ -97,6 +105,7 @@ class Args:
result["theme_path"] = self.theme_path
result["use_fancy_folders"] = self.use_fancy_folders
result["web_root_url"] = self.web_root_url
result["darktheme"] = self.darktheme
return result
@@ -116,9 +125,9 @@ def parse_arguments(version: str) -> Args:
"""
# fmt: off
if RICH:
parser = argparse.ArgumentParser(description="generate HTML files for a static image hosting website", formatter_class=RichHelpFormatter)
parser = configargparse.ArgumentParser(default_config_files=[CONFIGPATH], add_config_file_help=False, description="generate HTML files for a static image hosting website", formatter_class=RichHelpFormatter)
else:
parser = argparse.ArgumentParser(description="generate HTML files for a static image hosting website")
parser = configargparse.ArgumentParser(default_config_files=[CONFIGPATH], add_config_file_help=False, description="generate HTML files for a static image hosting website")
parser.add_argument("-a", "--author-name", help="name of the author of the images", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
parser.add_argument("-e", "--file-extensions", help="file extensions to include (can be specified multiple times)", action="append", dest="file_extensions", metavar="EXTENSION")
parser.add_argument("-l", "--license-type", help="specify the license type for the images", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE")
@@ -127,10 +136,11 @@ def parse_arguments(version: str) -> Args:
parser.add_argument("-p", "--root-directory", help="root directory containing the images", required=True, type=str, dest="root_directory", metavar="ROOT")
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('-c', '--config-file', is_config_file=True, help='config file path', metavar="CONFIG_FILE")
parser.add_argument("--exclude-folder", help="folders to exclude from processing, globs supported (can be specified multiple times)", action="append", dest="exclude_folders", metavar="FOLDER")
parser.add_argument("--folderthumbnails", help="generate subfolder thumbnails (first image in folder will be shown)", action="store_true", default=False, dest="folder_thumbs")
if RICH:
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg", )
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("--ignore-extension", help="file extensions to ignore (can be specified multiple times)", action="append", default=[], dest="ignore_extensions", metavar="EXTENSION")
parser.add_argument("--regenerate-thumbnails", help="regenerate thumbnails even if they already exist", action="store_true", default=False, dest="regenerate_thumbnails")
@@ -140,7 +150,12 @@ def parse_arguments(version: str) -> Args:
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("--write-config", type=str, required=False, help="write current command line args to config file", metavar="CONFIG_FILE")
parsed_args = parser.parse_args()
if parsed_args.write_config:
config_path = parsed_args.write_config
del parsed_args.write_config
parser.write_config_file(parsed_args, [config_path], exit_after=False)
# fmt: on
_args = Args(
author_name=parsed_args.author_name,
@@ -161,5 +176,6 @@ def parse_arguments(version: str) -> Args:
theme_path=parsed_args.theme_path,
use_fancy_folders=parsed_args.use_fancy_folders,
web_root_url=parsed_args.web_root_url,
darktheme=False,
)
return _args

View File

@@ -19,7 +19,7 @@ def extract_colorscheme(theme_path: str) -> dict[str, str]:
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\d+|bcolor\d+):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);"
colorscheme = {}
with open(theme_path, "r", encoding="utf-8") as f:
@@ -95,7 +95,8 @@ def css_color_to_hex(css_color: str) -> str:
# Helper function to convert HSL tuple to RGB tuple
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))
r, g, b = colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100)
return (round(r * 255), round(g * 255), round(b * 255))
# Regular expression pattern to match CSS colors
color_pattern = re.compile(

View File

@@ -0,0 +1,158 @@
from dataclasses import dataclass
from typing import List, Optional, Any, Dict, TypeVar, Callable, Type, cast
T = TypeVar("T")
def from_int(x: Any) -> int:
assert isinstance(x, int) and not isinstance(x, bool)
return x
def from_str(x: Any) -> str:
assert isinstance(x, str)
return x
def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
assert isinstance(x, list)
return [f(y) for y in x]
def from_none(x: Any) -> Any:
assert x is None
return x
def from_union(fs, x):
for f in fs:
try:
return f(x)
except AssertionError:
pass
assert False
def to_class(c: Type[T], x: Any) -> dict:
assert isinstance(x, c)
return cast(Any, x).to_dict()
def from_dict(f: Callable[[Any], T], x: Any) -> Dict[str, T]:
assert isinstance(x, dict)
return {k: f(v) for (k, v) in x.items()}
def from_native_dict(f: Callable[[Any], T], x: Any) -> Dict[Any, T]:
assert isinstance(x, dict)
return x
@dataclass
class ImageMetadata:
w: Optional[int]
h: Optional[int]
tags: Optional[List[str]]
exifdata: Optional[Dict[str, Any]]
xmp: Optional[Dict[str, Any]]
src: str
msrc: str
name: str
title: str
tiff: Optional[str] = None
raw: Optional[str] = None
@staticmethod
def from_dict(obj: Any) -> "ImageMetadata":
assert isinstance(obj, dict)
w = from_union([from_int, from_none], obj.get("w"))
h = from_union([from_int, from_none], obj.get("h"))
tags = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tags"))
exifdata = from_union([lambda x: from_native_dict(dict, x), from_none], obj.get("exifdata"))
xmp = from_union([lambda x: from_native_dict(dict, x), from_none], obj.get("xmp"))
src = from_str(obj.get("src"))
msrc = from_str(obj.get("msrc"))
name = from_str(obj.get("name"))
title = from_str(obj.get("title"))
tiff = from_union([from_str, from_none], obj.get("tiff"))
raw = from_union([from_str, from_none], obj.get("raw"))
return ImageMetadata(w, h, tags, exifdata, xmp, src, msrc, name, title, tiff, raw)
def to_dict(self) -> dict:
result: dict = {}
if self.w is not None:
result["w"] = from_union([from_int, from_none], self.w)
if self.h is not None:
result["h"] = from_union([from_int, from_none], self.h)
if self.tags is not None:
result["tags"] = from_union([lambda x: from_list(from_str, x), from_none], self.tags)
result["src"] = from_str(self.src)
result["msrc"] = from_str(self.msrc)
result["name"] = from_str(self.name)
result["title"] = from_str(self.title)
if self.tiff is not None:
result["tiff"] = from_union([from_str, from_none], self.tiff)
if self.raw is not None:
result["raw"] = from_union([from_str, from_none], self.raw)
if self.exifdata is not None:
result["exifdata"] = from_union([lambda x: from_native_dict(dict, x), from_none], self.exifdata)
if self.xmp is not None:
result["xmp"] = from_union([lambda x: from_native_dict(dict, x), from_none], self.xmp)
return result
@dataclass
class SubfolderMetadata:
url: str
name: str
metadata: Optional[str] = None
thumb: Optional[str] = None
@staticmethod
def from_dict(obj: Any) -> "SubfolderMetadata":
assert isinstance(obj, dict)
url = from_str(obj.get("url"))
name = from_str(obj.get("name"))
metadata = from_union([from_none, from_str], obj.get("metadata"))
thumb = from_union([from_none, from_str], obj.get("thumb"))
return SubfolderMetadata(url, name, metadata, thumb)
def to_dict(self) -> dict:
result: dict = {}
result["url"] = from_str(self.url)
result["name"] = from_str(self.name)
result["metadata"] = from_union([from_none, from_str], self.metadata)
result["thumb"] = from_union([from_none, from_str], self.thumb)
return result
@dataclass
class Metadata:
images: Dict[str, ImageMetadata]
subfolders: Optional[List[SubfolderMetadata]] = None
@staticmethod
def from_dict(obj: Any) -> "Metadata":
assert isinstance(obj, dict)
images = from_dict(ImageMetadata.from_dict, obj.get("images"))
subfolders = from_union([lambda x: from_list(SubfolderMetadata.from_dict, x), from_none], obj.get("subfolders"))
return Metadata(images, subfolders)
def to_dict(self) -> dict:
result: dict = {}
result["images"] = from_dict(lambda x: to_class(ImageMetadata, x), self.images)
if self.subfolders is not None:
result["subfolders"] = from_union([lambda x: from_list(lambda x: to_class(SubfolderMetadata, x), x), from_none], self.subfolders)
return result
def sort(self, reverse=False) -> "Metadata":
self.images = {key: self.images[key] for key in sorted(self.images, reverse=reverse)}
return self
def top_level_from_dict(s: Any) -> Metadata:
return Metadata.from_dict(s)
def top_level_to_dict(x: Metadata) -> Any:
return to_class(Metadata, x)

View File

@@ -17,13 +17,10 @@ from bs4 import BeautifulSoup
from modules.logger import logger
from modules import cclicense
from modules.argumentparser import Args
from modules.datatypes.metadata import Metadata, ImageMetadata, SubfolderMetadata
# Constants for file paths and exclusions
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__)
FAVICON_PATH = ".static/favicon.ico"
GLOBAL_CSS_PATH = ".static/global.css"
EXCLUDES = ["index.html", "manifest.json", "robots.txt"]
@@ -73,7 +70,7 @@ def getxmp(strbuffer: str) -> dict[str, Any]:
return {get_name(root.tag): get_value(root)}
def initialize_metadata(folder: str) -> dict[str, dict[str, int]]:
def initialize_metadata(folder: str) -> Metadata:
"""
Initializes the metadata JSON file if it doesn't exist.
@@ -128,10 +125,10 @@ def initialize_metadata(folder: str) -> dict[str, dict[str, int]]:
if "title" not in v:
metadata["images"][k]["title"] = v["name"]
return metadata
return Metadata.from_dict(metadata)
def update_metadata(metadata: dict[str, dict[str, Any]], folder: str) -> None:
def update_metadata(metadata: Metadata, folder: str) -> None:
"""
Updates the metadata JSON file.
@@ -143,14 +140,14 @@ def update_metadata(metadata: dict[str, dict[str, Any]], folder: str) -> None:
if metadata:
with open(metadata_path, "w", encoding="utf-8") as metadatafile:
logger.info("writing metadata file", extra={"file": metadata_path})
metadatafile.write(json.dumps(metadata, indent=4))
metadatafile.write(json.dumps(metadata.to_dict(), indent=4))
else:
if os.path.exists(metadata_path):
logger.info("deleting empty metadata file", extra={"file": metadata_path})
os.remove(metadata_path)
def get_image_info(item: str, folder: str) -> dict[str, Any]:
def get_image_info(item: str, folder: str) -> ImageMetadata:
"""
Extracts image information and EXIF data.
@@ -172,7 +169,7 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
except UnidentifiedImageError:
logger.error("cannot identify image file", extra={"file": file})
print(f"cannot identify image file: {file}")
return {"w": None, "h": None, "tags": None, "exifdata": None, "xmp": None}
return ImageMetadata(w=None, h=None, tags=[], exifdata=None, xmp=None, src="", msrc="", name="", title="")
if exif:
logger.info("extracting EXIF data", extra={"file": file})
ifd = exif.get_ifd(ExifTags.IFD.Exif)
@@ -253,9 +250,11 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
tags = get_tags(sidecarfile)
except Exception as e:
logger.error(e)
if None in tags:
tags.remove(None)
return {"w": width, "h": height, "tags": tags, "exifdata": exifdata, "xmp": xmp}
if None in tags: # type: ignore
tags.remove(None) # type: ignore
if not isinstance(tags, list):
tags = []
return ImageMetadata(w=width, h=height, tags=tags, exifdata=exifdata, xmp=xmp, src="", msrc="", name="", title="")
def nested_dict():
@@ -331,12 +330,12 @@ def get_tags(sidecarfile: str) -> list[str]:
pass
except KeyError:
pass
if None in tags:
tags.remove(None)
return tags
if None in tags: # type: ignore
tags.remove(None) # type: ignore
return tags # type: ignore
def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: dict[str, dict[str, int]], raw: list[str]) -> dict[str, Any]:
def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: Metadata, raw: list[str]) -> tuple[ImageMetadata, Metadata]:
"""
Processes an image and prepares its data for the HTML template.
@@ -353,24 +352,21 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: d
"""
extsplit = os.path.splitext(item)
sidecarfile = os.path.join(folder, item + ".xmp")
if item not in metadata["images"] or _args.reread_metadata:
metadata["images"][item] = get_image_info(item, folder)
if item not in metadata.images or _args.reread_metadata:
metadata.images[item] = get_image_info(item, folder)
if _args.reread_sidecar and os.path.exists(sidecarfile):
logger.info("xmp sidecar file found", extra={"file": sidecarfile})
try:
metadata["images"][item]["tags"] = get_tags(sidecarfile)
metadata.images[item].tags = get_tags(sidecarfile)
except Exception as e:
logger.error(e)
image = {
"src": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}",
"msrc": f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg",
"name": item,
"w": metadata["images"][item]["w"],
"h": metadata["images"][item]["h"],
"tags": metadata["images"][item]["tags"],
"title": item,
}
image = metadata.images[item]
image.src = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}"
image.msrc = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg"
image.name = item
image.title = item
path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg")
if not os.path.exists(path) or _args.regenerate_thumbnails:
if os.path.exists(path):
@@ -383,17 +379,17 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: d
url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}"
if _raw in (".tif", ".tiff"):
logger.info("tiff file found", extra={"file": file})
image["tiff"] = url
image.tiff = url
else:
logger.info("raw file found", extra={"file": file, "extension": _raw})
image["raw"] = url
image.raw = url
metadata["images"][item].update(image)
metadata.images[item] = image
return image, metadata
def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[str]:
def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> set[str]:
"""
Generates HTML content for a folder of images.
@@ -412,22 +408,22 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
items = sorted(os.listdir(folder))
contains_files = False
images = []
subfolders = []
subfoldertags = set()
images: list[ImageMetadata] = []
subfolders: list[SubfolderMetadata] = []
subfoldertags: set[str] = set()
foldername = folder.removeprefix(_args.root_directory)
foldername = f"{foldername}/" if foldername else ""
baseurl = urllib.parse.quote(foldername)
gone = [item for item in metadata["images"] if item not in items]
gone = [item for item in metadata.images if item not in items]
for gon in gone:
del metadata["images"][gon]
del metadata.images[gon]
create_thumbnail_folder(foldername, _args.root_directory)
logger.info("processing contents", extra={"folder": folder})
if not _args.non_interactive_mode:
for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True):
for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True, leave=False):
if item not in EXCLUDES and not item.startswith(".") and os.path.splitext(item)[1][1:].lower() not in _args.ignore_extensions:
if os.path.isdir(os.path.join(folder, item)):
subfoldertags.update(process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo))
@@ -455,11 +451,11 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
if item == "LICENSE":
process_license(folder, item)
metadata["subfolders"] = subfolders
metadata.subfolders = subfolders
if _args.reverse_sort:
metadata["images"] = {key: metadata["images"][key] for key in sorted(metadata["images"], reverse=True)}
metadata.sort(reverse=True)
else:
metadata["images"] = {key: metadata["images"][key] for key in sorted(metadata["images"])}
metadata.sort()
update_metadata(metadata, folder)
if should_generate_html(images, contains_files, _args):
@@ -485,7 +481,7 @@ def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
os.mkdir(thumbnails_path)
def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dict[str, str | None]], _args: Args, raw: list[str], version: str, logo: str) -> list[str]:
def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[SubfolderMetadata], _args: Args, raw: list[str], version: str, logo: str) -> set[str]:
"""
Processes a subfolder.
@@ -513,10 +509,10 @@ def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dic
if item not in _args.exclude_folders:
if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/.metadata.json"})
subfolders.append(SubfolderMetadata(url=subfolder_url, name=item, thumb=thumb, metadata=f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/.metadata.json"))
return generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo)
subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": None})
return []
subfolders.append(SubfolderMetadata(url=subfolder_url, name=item, thumb=thumb))
return set()
def process_license(folder: str, item: str) -> None:
@@ -548,7 +544,7 @@ def process_info_file(folder: str, item: str) -> None:
info[urllib.parse.quote(folder)] = f.read()
def should_generate_html(images: list[dict[str, Any]], contains_files, _args: Args) -> bool:
def should_generate_html(images: list[ImageMetadata], contains_files, _args: Args) -> bool:
"""
Determines if HTML should be generated.
@@ -559,7 +555,7 @@ def should_generate_html(images: list[dict[str, Any]], contains_files, _args: Ar
Returns:
bool: True if HTML should be generated, False otherwise.
"""
return images or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files)
return bool(images) or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files)
def format_html(html: str) -> str:
@@ -568,8 +564,8 @@ def format_html(html: str) -> str:
def create_html_file(
folder: str, title: str, foldername: str, images: list[dict[str, Any]], subfolders: list[dict[str, str]], _args: Args, version: str, logo: str, subfoldertags: list[str]
) -> list[str]:
folder: str, title: str, foldername: str, images: list[ImageMetadata], subfolders: list[SubfolderMetadata], _args: Args, version: str, logo: str, subfoldertags: set[str]
) -> set[str]:
"""
Creates the HTML file using the template.
@@ -602,15 +598,13 @@ def create_html_file(
alltags = set()
for img in images:
if img["tags"]:
alltags.update(img["tags"])
if img.tags:
alltags.update(img.tags)
alltags.update(set(subfoldertags))
alltags.update(subfoldertags)
folder_info = info.get(urllib.parse.quote(folder), "").split("\n")
_info = [i for i in folder_info if len(i) > 1] if folder_info else None
if _args.reverse_sort:
images.sort(key=lambda i: i["name"], reverse=True)
folder_license = licens.get(urllib.parse.quote(folder), False)
@@ -627,6 +621,7 @@ def create_html_file(
favicon=f"{_args.web_root_url}{FAVICON_PATH}",
stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}",
theme=f"{_args.web_root_url}.static/theme.css",
darktheme=f"{_args.web_root_url}.static/theme-dark.css" if _args.darktheme else None,
root=_args.web_root_url,
parent=f"{_args.web_root_url}{urllib.parse.quote(foldername)}",
header=f"{header} - LICENSE",
@@ -644,6 +639,7 @@ def create_html_file(
favicon=f"{_args.web_root_url}{FAVICON_PATH}",
stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}",
theme=f"{_args.web_root_url}.static/theme.css",
darktheme=f"{_args.web_root_url}.static/theme-dark.css" if _args.darktheme else None,
root=_args.web_root_url,
parent=parent,
header=header,
@@ -661,7 +657,7 @@ def create_html_file(
logger.info("writing formatted html file", extra={"path": html_file})
f.write(format_html(content))
return sorted(alltags)
return set(sorted(alltags))
def list_folder(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str, str]]:

View File

@@ -21,11 +21,7 @@ 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))
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__)
LOG_DIR = os.path.join(SCRIPTDIR, "logs")
LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl")

View File

@@ -18,11 +18,7 @@ from modules.argumentparser import Args
from modules.css_color import extract_theme_color, extract_colorscheme
# Define constants for static files directory and icon sizes
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__)
STATIC_FILES_DIR = os.path.join(SCRIPTDIR, "files")
ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"]

View File

@@ -1,12 +1,12 @@
beautifulsoup4~=4.13.4
beautifulsoup4~=4.14.3
CairoSVG~=2.7.1
ConfigArgParse~=1.7.1
defusedxml~=0.7.1
html5lib~=1.1
Jinja2~=3.1.6
jsmin~=3.0.1
Pillow~=11.3.0
pyinstaller~=6.11.1
Pillow~=12.1.0
python_json_logger~=2.0.7
rich_argparse~=1.7.1
selenium~=4.34.2
tqdm~=4.66.4
rich_argparse~=1.7.2
selenium~=4.40.0
tqdm~=4.66.6

111
templates/default-dark.css Normal file
View File

@@ -0,0 +1,111 @@
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
* {
--color1: #262a2b;
--color2: #0d0e0e;
--color3: #313537;
--color4: #181a1b;
--color5: #5483ef;
--bcolor1: #e8e6e3;
--bcolor2: #0c0d0e;
}
.navbar {
font-weight: bold;
color: var(--bcolor1);
background-color: var(--color1);
}
.navbar li a {
font-weight: bold;
color: var(--bcolor1);
}
/* Change the link color on hover */
.navbar li a:hover {
background-color: var(--color2);
}
.footer {
color: var(--bcolor1);
background-color: var(--color3);
font-weight: 500;
}
.footer a {
color: var(--color5);
text-decoration: none;
}
.foldericon {
content: "themes/icons/folder-2.svg.j2";
}
.folders a {
font-weight: 700;
color: var(--color5);
text-decoration: none;
}
.tooltiptext {
font-weight: 400;
background-color: var(--color3);
}
.tagentry label:hover {
background-color: var(--color4);
}
.tagentry .tagtoggle:hover {
background-color: var(--color4);
}
.column img {
background-color: var(--bcolor2);
}
#totop:hover {
background-color: var(--color2);
}
#totop {
background-color: var(--color1);
color: var(--bcolor1);
font-weight: 800;
}
.loader {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
border-top: 3px solid var(--bcolor1);
border-right: 3px solid transparent;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
body {
color: var(--bcolor1);
background-color: var(--color4);
font-family: "Ubuntu", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
}
body a {
font-weight: 400;
color: var(--color5);
text-decoration: none;
}

View File

@@ -24,6 +24,10 @@ class PhotoGallery {
this.scrollFunction = this.scrollFunction.bind(this);
this.topFunction = this.topFunction.bind(this);
this.onLoad = this.onLoad.bind(this);
this.darkMode = this.darkMode.bind(this);
this.lightMode = this.lightMode.bind(this);
this.darkModeToggle = this.darkModeToggle.bind(this);
this.detectDarkMode = this.detectDarkMode.bind(this);
this.init();
}
@@ -38,12 +42,7 @@ class PhotoGallery {
openSwipe(imgIndex) {
const options = { index: imgIndex };
const gallery = new PhotoSwipe(
this.pswpElement,
PhotoSwipeUI_Default,
this.shown,
options
);
const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options);
gallery.init();
}
@@ -78,9 +77,7 @@ class PhotoGallery {
if (folders) folders.style.display = "";
document.getElementById("recursive").checked = false;
document
.querySelectorAll("#tagdropdown input.tagcheckbox:checked")
.forEach((checkbox) => (checkbox.checked = false));
document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false));
window.history.replaceState({ html: content, pageTitle: title }, "", path);
this.requestMetadata();
}
@@ -150,10 +147,9 @@ class PhotoGallery {
existingItems.add(image.src);
}
}
if (Array.isArray(data.subfolders))
nextLevel.push(...data.subfolders);
if (Array.isArray(data.subfolders)) nextLevel.push(...data.subfolders);
} catch {}
})
}),
);
if (nextLevel.length > 0) await fetchFoldersRecursively(nextLevel);
};
@@ -194,39 +190,30 @@ class PhotoGallery {
filter() {
const searchParams = new URLSearchParams(window.location.search);
this.shown = [];
let path = decodeURIComponent(
window.location.origin +
window.location.pathname.replace("index.html", "")
);
let path = decodeURIComponent(window.location.origin + window.location.pathname.replace("index.html", ""));
if (path.startsWith("null")) {
path = window.location.protocol + "//" + path.substring(4);
}
const selectedTags = [];
document
.querySelectorAll("#tagdropdown input.tagcheckbox:checked")
.forEach((checkbox) => {
let tag = checkbox.parentElement.id.trim().substring(1);
if (checkbox.parentElement.parentElement.children.length > 1)
tag += "|";
selectedTags.push(tag);
});
document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => {
let tag = checkbox.parentElement.id.trim().substring(1);
if (checkbox.parentElement.parentElement.children.length > 1) tag += "|";
selectedTags.push(tag);
});
const urltags = selectedTags.join(",");
let isRecursiveChecked = false;
try {
isRecursiveChecked =
document.getElementById("recursive")?.checked || false;
isRecursiveChecked = document.getElementById("recursive")?.checked || false;
} catch {}
for (const item of this.items) {
const tags = item.tags || [];
const include = selectedTags.every((selected) => {
const isParent = selected.endsWith("|");
return isParent
? tags.some((t) => t.startsWith(selected))
: tags.includes(selected);
return isParent ? tags.some((t) => t.startsWith(selected)) : tags.includes(selected);
});
if (include || selectedTags.length === 0) {
@@ -313,9 +300,7 @@ class PhotoGallery {
let str = "";
this.shown.forEach((item, index) => {
let tags = this.parseHierarchicalTags(item.tags || []);
str += `<div class="column"><figure title="${this.renderTree(
tags
)}"><img src="${
str += `<div class="column"><figure title="${this.renderTree(tags)}"><img src="${
item.msrc
}" data-index="${index}" /><figcaption class="caption">${item.name}`;
if (item.tiff) str += `&nbsp;<a href="${item.tiff}">TIFF</a>`;
@@ -328,20 +313,13 @@ class PhotoGallery {
}
setFilter(selected) {
document
.querySelectorAll("#tagdropdown input.tagcheckbox")
.forEach((checkbox) => {
selected.forEach((tag) => {
if (
checkbox.parentElement.id
.trim()
.substring(1)
.replace(" ", "%20") === tag
) {
checkbox.checked = true;
}
});
document.querySelectorAll("#tagdropdown input.tagcheckbox").forEach((checkbox) => {
selected.forEach((tag) => {
if (checkbox.parentElement.id.trim().substring(1).replace(" ", "%20") === tag) {
checkbox.checked = true;
}
});
});
}
toggleTag(tagid) {
@@ -350,9 +328,7 @@ class PhotoGallery {
const svg = tag?.parentElement.querySelector(".tagtoggle svg");
if (!ol || !svg) return;
ol.classList.toggle("show");
svg.style.transform = ol.classList.contains("show")
? "rotate(180deg)"
: "rotate(0deg)";
svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
}
setupDropdownToggle() {
@@ -364,18 +340,12 @@ class PhotoGallery {
event.stopPropagation();
const svg = toggleLink.querySelector("svg");
dropdown.classList.toggle("show");
if (svg)
svg.style.transform = dropdown.classList.contains("show")
? "rotate(180deg)"
: "rotate(0deg)";
if (svg) svg.style.transform = dropdown.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
this.tagDropdownShown = dropdown.classList.contains("show");
});
document.addEventListener("click", (event) => {
if (
!dropdown.contains(event.target) &&
!toggleLink.contains(event.target)
) {
if (!dropdown.contains(event.target) && !toggleLink.contains(event.target)) {
dropdown.classList.remove("show");
this.tagDropdownShown = false;
const svg = toggleLink.querySelector("svg");
@@ -402,18 +372,18 @@ class PhotoGallery {
}
setupClickHandlers() {
const resetEl = document
.getElementById("reset-filter")
?.querySelector("label");
const resetEl = document.getElementById("reset-filter")?.querySelector("label");
if (resetEl) resetEl.addEventListener("click", this.reset);
const recurseEl = document.getElementById("recursive");
if (recurseEl)
recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
if (recurseEl) recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
const totop = document.getElementById("totop");
if (totop) totop.addEventListener("click", this.topFunction);
const darkModeSwitch = document.getElementById("dark-mode-switch");
if (darkModeSwitch) darkModeSwitch.addEventListener("click", this.darkModeToggle);
const imagelist = document.getElementById("imagelist");
if (imagelist) {
imagelist.addEventListener("click", (event) => {
@@ -442,10 +412,7 @@ class PhotoGallery {
scrollFunction() {
const totopbutton = document.getElementById("totop");
if (!totopbutton) return;
if (
document.body.scrollTop > 20 ||
document.documentElement.scrollTop > 20
) {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
totopbutton.style.display = "block";
} else {
totopbutton.style.display = "none";
@@ -456,6 +423,53 @@ class PhotoGallery {
window.scrollTo({ top: 0, behavior: "smooth" });
}
darkMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
if (themeLink) themeLink.disabled = true;
if (darkThemeLink) darkThemeLink.disabled = false;
}
lightMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
if (themeLink) themeLink.disabled = false;
if (darkThemeLink) darkThemeLink.disabled = true;
}
darkModeToggle(mode) {
const switchState = document.getElementById("dark-mode-switch-check");
if (mode == "dark") {
this.darkMode();
if (switchState) {
switchState.checked = true;
}
} else if (mode == "light") {
this.lightMode();
if (switchState) {
switchState.checked = false;
}
} else {
if (switchState.checked) {
switchState.checked = false;
this.lightMode();
} else {
switchState.checked = true;
this.darkMode();
}
}
}
detectDarkMode() {
if (document.getElementById("darktheme")) {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
this.darkModeToggle("dark");
} else {
this.darkModeToggle("light");
}
}
}
onLoad() {
document.querySelectorAll(".tagtoggle").forEach((toggle) => {
toggle.addEventListener("mouseup", (event) => {
@@ -469,6 +483,7 @@ class PhotoGallery {
this.setupDropdownToggle();
this.setupTagHandlers();
this.setupClickHandlers();
this.detectDarkMode();
window.addEventListener("scroll", this.scrollFunction);
}

View File

@@ -34,10 +34,16 @@
{%- if theme %}
<link rel="preload" href="{{ theme }}" as="style">
{%- endif %}
{%- if darktheme %}
<link rel="preload" href="{{ darktheme }}" as="style">
{%- endif %}
<link rel="icon" type="image/x-icon" href="{{ favicon }}">
<link rel="stylesheet" href="{{ stylesheet }}">
{%- if theme %}
<link rel="stylesheet" href="{{ theme }}">
<link rel="stylesheet" href="{{ theme }}" id="theme">
{%- endif %}
{%- if darktheme %}
<link rel="stylesheet" href="{{ darktheme }}" id="darktheme" disabled>
{%- endif %}
<link rel="preload" href="{{ root }}.static/pswp/photoswipe.css" as="style">
<link rel="preload" href="{{ root }}.static/pswp/default-skin/default-skin.css" as="style">
@@ -89,10 +95,22 @@
{{ render_tags(tags, '') }}
</ol>
</li>
{% endif %}
{%- endif %}
{%- if licensefile %}
<li class="license"><a href="{{ licensefile }}">License</a></li>
{%- endif %}
{%- if darktheme %}
<li class="darkmodeswitch">
<a class="button" id="dark-mode-switch">
<input type="checkbox" class="checkbox" id="dark-mode-switch-check" />
<div class="knobs">
<span class="light">☀︎</span>
<span class="slider"></span>
<span class="dark">☽</span>
</div>
</a>
</li>
{%- endif %}
</div>
</ol>
{% if subdirectories %}
@@ -120,7 +138,7 @@
<a property="dct:title" rel="cc:attributionURL" href="{{ root }}">{{ license.project }}</a> by <span property="cc:attributionName">{{ license.author }}</span> is marked with
<a href="{{ license.url }}" target="_blank" rel="license noopener noreferrer" style="display: inline-block">CC0 1.0
{%- for pic in license.pics %}
<img style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom" src="{{ pic }}" alt="" />
<img src="{{ pic }}" alt="" />
{%- endfor %}
</a>
{%- else %}

2
themes

Submodule themes updated: 2d2f96abb3...3bb36480e7