Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
67a9058b7d
|
|||
|
79e34d7e43
|
|||
|
0cda1706fa
|
|||
|
85467f4f1e
|
|||
|
00ccb96581
|
|||
|
5227beb02a
|
|||
|
22f9cb4e00
|
|||
|
dbf3e5c27a
|
|||
|
d9ca40bf77
|
|||
|
b8cbc49647
|
|||
|
b80e98a4ec
|
|||
|
350f41f439
|
|||
|
00b5020642
|
|||
|
492ea8755f
|
|||
|
39da474db6
|
|||
|
44bcd5607f
|
|||
|
5ff44a1912
|
|||
|
4241f3965a
|
|||
|
002e9c62db
|
|||
| cf494401c8 | |||
|
2a0323e579
|
|||
|
7e23b3625a
|
|||
|
bce51dc3d6
|
|||
| 57250b3adc | |||
| 08895902ec | |||
| 0107c4d38c | |||
| 6595cd11d6 | |||
| 08b53eb2ac | |||
| d71b6afd7f | |||
| ec32c24af3 | |||
| 122117f125 | |||
| 3b93fb8719 | |||
| b601b67b6e | |||
| 03a9e46baa | |||
|
0a39ecd637
|
|||
|
bc4fefa3f1
|
|||
|
156a886ac1
|
|||
| f6cf89a37f | |||
|
cc783ee30e
|
|||
|
c429cd06c8
|
|||
|
35f2ea7536
|
|||
| 0ec663ae68 | |||
| 0973868782 | |||
| cd06c526af | |||
| d155b05798 | |||
| 3352894e4d | |||
| d743ede95d | |||
| 9403e84d78 | |||
| 961d79754e | |||
| 383dd59851 | |||
| 549c15ca6c | |||
| 57d949677f | |||
| cc6ad14506 | |||
| cace0f8593 | |||
| 0484200fad | |||
| 74d97dac2a | |||
| 6c636905e2 | |||
| c79473d646 | |||
|
3cb5269985
|
|||
|
3bde09aebc
|
|||
|
effc05826a
|
|||
|
8ec9701aa9
|
|||
| a95a6e1722 |
4
.github/workflows/build-release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pip install -r requirements.txt
|
||||
- name: Build Package
|
||||
run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder -F --add-data files:files --add-data templates:templates --add-data .version:.
|
||||
run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
make_latest: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
dist/StaticGalleryBuilder
|
||||
dist/StaticGalleryBuilder*
|
||||
|
||||
5
.gitignore
vendored
@@ -164,6 +164,9 @@ cython_debug/
|
||||
test/.static
|
||||
test/.thumbnails
|
||||
test/**/index.html
|
||||
test/**/.sizelist.json
|
||||
test/**/license.html
|
||||
test/**/.metadata.json
|
||||
test/manifest.json
|
||||
themes/previews
|
||||
logs
|
||||
.lock
|
||||
@@ -1 +1 @@
|
||||
3.12
|
||||
StaticGalleryBuilder
|
||||
|
||||
27
README.md
@@ -20,19 +20,19 @@ Python script to generate static HTML files for website galleries.
|
||||
## Requirements
|
||||
|
||||
- Python 3.x
|
||||
- `numpy` library
|
||||
- `tqdm` library
|
||||
- `Jinja2` library
|
||||
- `Pillow` library
|
||||
- `rich_argparse` library
|
||||
- `cairosvg` library (for SVG to PNG icon conversion)
|
||||
- `python-json-logger` library (for logging)
|
||||
|
||||
## Installation
|
||||
|
||||
Install the required libraries using pip:
|
||||
|
||||
```sh
|
||||
pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg
|
||||
pip install tqdm Jinja2 Pillow rich-argparse cairosvg python-json-logger
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -43,20 +43,21 @@ The script supports several command-line options to customize its behavior. Belo
|
||||
|
||||
### Options
|
||||
|
||||
- `-h, --help`: Show the help message and exit.
|
||||
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. This option is required.
|
||||
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. This option is required.
|
||||
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. This option is required.
|
||||
- `-r, --regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
|
||||
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
|
||||
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
|
||||
- `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author".
|
||||
- `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times.
|
||||
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
|
||||
- `-m, --web-manifest`: Generate a web manifest file.
|
||||
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
|
||||
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. **(This option is required)**.
|
||||
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. **(This option is required)**.
|
||||
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. **(This option is required)**.
|
||||
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
|
||||
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
|
||||
- `--regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
|
||||
- `--reread-metadata`: Reread image metadata if it already exists.
|
||||
- `--reverse-sort`: Sort images by reverse name order.
|
||||
- `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme.
|
||||
- `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing.
|
||||
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
|
||||
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
|
||||
- `-m, --web-manifest`: Generate a web manifest file.
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -95,6 +96,8 @@ To generate a web manifest file:
|
||||
- The root and web root paths must point to the same folder, one on the filesystem and one on the web server. Use absolute paths.
|
||||
- The script generates the preview thumbnails in a `.thumbnails` subdirectory within the root folder.
|
||||
- The `.lock` file prevents multiple instances of the script from running simultaneously. Make sure to remove it if the script terminates unexpectedly.
|
||||
- Add a `info` file into any directory containing pictures and it will be read and displayed as a tooltip on the website.
|
||||
- Add tags to the Image xmp `subject` or to `.metadata.json` to tag images for filtering.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
{
|
||||
"extensions": {
|
||||
"recommendations": [
|
||||
"charliermarsh.ruff",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-edgedevtools.vscode-edge-devtools",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.debugpy",
|
||||
"ms-python.pylint",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"samuelcolvin.jinjahtml",
|
||||
"vscode.css-language-features",
|
||||
"vscode.html-language-features",
|
||||
"waderyan.gitblame",
|
||||
],
|
||||
"vscode.html-language-features"
|
||||
]
|
||||
},
|
||||
"folders": [
|
||||
{
|
||||
"name": "StaticGalleryBuilder",
|
||||
"path": "./",
|
||||
},
|
||||
"path": "./"
|
||||
}
|
||||
],
|
||||
"launch": {
|
||||
"version": "0.2.0",
|
||||
@@ -39,14 +37,17 @@
|
||||
"cc-by-nc-sa",
|
||||
"-n",
|
||||
"-m",
|
||||
"-r"
|
||||
"--reverse-sort",
|
||||
"--regenerate-thumbnails",
|
||||
"--reread-metadata",
|
||||
"--folderthumbnails"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"name": "Testfolder",
|
||||
"postDebugTask": "Delete Lockfile",
|
||||
"program": "${workspaceFolder}/builder.py",
|
||||
"request": "launch",
|
||||
"type": "debugpy",
|
||||
"type": "debugpy"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
@@ -57,66 +58,89 @@
|
||||
"-t",
|
||||
"Pictures",
|
||||
"--theme",
|
||||
"themes/catpuccin.css",
|
||||
"themes/default.css",
|
||||
"--use-fancy-folders",
|
||||
"--web-manifest",
|
||||
"-n",
|
||||
"-m",
|
||||
"-r",
|
||||
// "--regenerate-thumbnails",
|
||||
// "--reread-metadata",
|
||||
"--folderthumbnails"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"name": "woek",
|
||||
"postDebugTask": "Delete Lockfile 2",
|
||||
"program": "${workspaceFolder}/builder.py",
|
||||
"request": "launch",
|
||||
"type": "debugpy",
|
||||
"type": "debugpy"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"--use-fancy-folders",
|
||||
"-p",
|
||||
"/mnt/nfs/pictures/",
|
||||
"-w",
|
||||
"https://pictures.sorogon.eu/",
|
||||
"-t",
|
||||
"Sorogon's Pictures",
|
||||
"--theme",
|
||||
"/home/user/git/github.com/greflm13/simple-picture-server/themes/alpenglow.css",
|
||||
"-m",
|
||||
"--exclude-folder",
|
||||
"Scans",
|
||||
"--exclude-folder",
|
||||
"*/Galleries/*",
|
||||
"--folderthumbnails",
|
||||
"--reread-metadata"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"name": "production",
|
||||
"program": "${workspaceFolder}/builder.py",
|
||||
"request": "launch",
|
||||
"type": "debugpy"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"${workspaceFolder}/themes",
|
||||
"https://pictures.sorogon.eu/Analog/Example/"
|
||||
"https://pictures.sorogon.eu/public/Example/"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"name": "Generate Themes previews",
|
||||
"program": "${workspaceFolder}/generate_previews.py",
|
||||
"request": "launch",
|
||||
"type": "debugpy",
|
||||
"type": "debugpy"
|
||||
}
|
||||
],
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features",
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[jinja-css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features",
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[jinja-html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features",
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"[jinja-js]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"black-formatter.args": [
|
||||
"-l 140",
|
||||
],
|
||||
"black-formatter.interpreter": [
|
||||
"/usr/bin/python3",
|
||||
],
|
||||
"black-formatter.args": ["-l 260"],
|
||||
"black-formatter.interpreter": ["/usr/bin/python3"],
|
||||
"editor.formatOnSave": false,
|
||||
"emmet.includeLanguages": {
|
||||
"jinja-css": "css",
|
||||
"jinja-html": "html",
|
||||
"jinja-js": "javascript",
|
||||
"jinja-json": "json",
|
||||
"jinja-json": "json"
|
||||
},
|
||||
"files.associations": {
|
||||
"**/*.css.j2": "jinja-css",
|
||||
"**/*.css": "css",
|
||||
"**/*.html.j2": "jinja-html",
|
||||
"**/*.html.j2": "jinja-html"
|
||||
},
|
||||
"gitblame.inlineMessageEnabled": true,
|
||||
"gitblame.inlineMessageFormat": "${author.name}, ${time.ago} • ${commit.summary}",
|
||||
@@ -132,21 +156,23 @@
|
||||
"json.schemaDownload.enable": true,
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"manifest.json.j2",
|
||||
],
|
||||
"url": "https://json.schemastore.org/web-manifest-combined.json",
|
||||
},
|
||||
"fileMatch": ["manifest.json.j2"],
|
||||
"url": "https://json.schemastore.org/web-manifest-combined.json"
|
||||
}
|
||||
],
|
||||
"prettier.htmlWhitespaceSensitivity": "css",
|
||||
"pylint.args": [
|
||||
"--disable=C0111",
|
||||
"--disable=C0301",
|
||||
"--good-names-rgxs=^[_a-z][_a-z0-9]?$",
|
||||
"--good-names-rgxs=^[_a-z][_a-z0-9]?$"
|
||||
],
|
||||
"python.analysis.inlayHints.callArgumentNames": "off",
|
||||
"python.analysis.inlayHints.functionReturnTypes": false,
|
||||
"python.analysis.inlayHints.variableTypes": false,
|
||||
"yaml.schemas": {
|
||||
"https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json": "file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/hl_config.yaml"
|
||||
},
|
||||
"ruff.lineLength": 180
|
||||
},
|
||||
"tasks": {
|
||||
"version": "2.0.0",
|
||||
@@ -157,6 +183,14 @@
|
||||
"label": "Delete Lockfile",
|
||||
"problemMatcher": [],
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": false,
|
||||
"reveal": "never",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "rm -f /home/user/woek/Pictures/.lock",
|
||||
@@ -164,7 +198,66 @@
|
||||
"label": "Delete Lockfile 2",
|
||||
"problemMatcher": [],
|
||||
"type": "shell",
|
||||
},
|
||||
],
|
||||
},
|
||||
"presentation": {
|
||||
"echo": false,
|
||||
"reveal": "never",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.",
|
||||
"isBackground": false,
|
||||
"label": "Build",
|
||||
"problemMatcher": [],
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": false
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": ["Clean"]
|
||||
},
|
||||
{
|
||||
"command": "rm -rf build dist",
|
||||
"isBackground": true,
|
||||
"label": "Clean",
|
||||
"problemMatcher": [],
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "never",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"command": "LESS=-SR hl logs/latest.jsonl --config hl_config.yaml",
|
||||
"isBackground": false,
|
||||
"label": "View Latest Log",
|
||||
"problemMatcher": [],
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": false,
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
134
builder.py
@@ -1,19 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
import fnmatch
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from multiprocessing import Pool, freeze_support
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from tqdm.auto import tqdm
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
from modules.argumentparser import parse_arguments, Args
|
||||
from modules.svg_handling import icons, webmanifest, extract_colorscheme
|
||||
from modules.generate_html import list_folder, EXCLUDES
|
||||
|
||||
|
||||
# fmt: off
|
||||
# Constants
|
||||
@@ -34,10 +34,19 @@ IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"]
|
||||
NOT_LIST = ["*/Galleries/*", "Archives"]
|
||||
# fmt: on
|
||||
|
||||
pbardict: Dict[str, tqdm] = {}
|
||||
args = parse_arguments(VERSION)
|
||||
|
||||
lock_file = os.path.join(args.root_directory, ".lock")
|
||||
if os.path.exists(lock_file):
|
||||
print("Another instance of this program is running.")
|
||||
sys.exit()
|
||||
else:
|
||||
from modules.logger import logger
|
||||
from modules.svg_handling import icons, webmanifest, extract_colorscheme
|
||||
from modules.generate_html import list_folder
|
||||
|
||||
|
||||
def init_globals(_args: Args, raw: List[str]) -> Tuple[Args, List[str]]:
|
||||
def init_globals(_args: Args, raw: list[str]) -> tuple[Args, list[str]]:
|
||||
"""
|
||||
Initialize global variables and set default values for arguments.
|
||||
|
||||
@@ -45,12 +54,12 @@ def init_globals(_args: Args, raw: List[str]) -> Tuple[Args, List[str]]:
|
||||
-----------
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
raw : List[str]
|
||||
List of raw file extensions.
|
||||
raw : list[str]
|
||||
list of raw file extensions.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
Tuple[Args, List[str]]
|
||||
tuple[Args, list[str]]
|
||||
Updated arguments and raw file extensions.
|
||||
"""
|
||||
if not _args.file_extensions:
|
||||
@@ -77,10 +86,13 @@ def copy_static_files(_args: Args) -> None:
|
||||
static_dir = os.path.join(_args.root_directory, ".static")
|
||||
if os.path.exists(static_dir):
|
||||
print("Removing existing .static folder...")
|
||||
logger.info("removing existing .static folder")
|
||||
shutil.rmtree(static_dir)
|
||||
|
||||
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 {")
|
||||
@@ -92,33 +104,38 @@ def copy_static_files(_args: Args) -> None:
|
||||
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)
|
||||
svg = svg.replace("{{ color1 }}", colorscheme["color1"])
|
||||
svg = svg.replace("{{ color2 }}", colorscheme["color2"])
|
||||
svg = svg.replace("{{ color3 }}", colorscheme["color3"])
|
||||
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
|
||||
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)
|
||||
|
||||
|
||||
def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
|
||||
def generate_thumbnail(arguments: tuple[str, str, str]) -> None:
|
||||
"""
|
||||
Generate a thumbnail for a given image.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
arguments : Tuple[str, str, str, bool]
|
||||
arguments : tuple[str, str, str, bool]
|
||||
A tuple containing the folder, item, root directory, and regenerate thumbnails flag.
|
||||
"""
|
||||
folder, item, root_directory = arguments
|
||||
image = os.path.join(folder, item)
|
||||
path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), item) + ".jpg"
|
||||
oldpath = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".jpg"
|
||||
if os.path.exists(oldpath):
|
||||
@@ -127,64 +144,54 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if not os.path.exists(path):
|
||||
logger.info("generating thumbnail for %s", item, extra={"path": image})
|
||||
try:
|
||||
with Image.open(os.path.join(folder, item)) as imgfile:
|
||||
with Image.open(image) as imgfile:
|
||||
imgrgb = imgfile.convert("RGB")
|
||||
img = ImageOps.exif_transpose(imgrgb)
|
||||
img.thumbnail((512, 512))
|
||||
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB")
|
||||
except OSError:
|
||||
print(f"Failed to generate thumbnail for {os.path.join(folder, item)}")
|
||||
logger.error("Failed to generate thumbnail for %s", item, extra={"path": image})
|
||||
print(f"Failed to generate thumbnail for {image}")
|
||||
return
|
||||
else:
|
||||
logger.debug("thumbnail already exists for %s", item, extra={"path": image})
|
||||
|
||||
|
||||
def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
|
||||
"""
|
||||
Recursively count the total number of folders to be processed.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
folder : str
|
||||
The current folder being processed.
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
_total : int, optional
|
||||
The running total of folders, default is 0.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
int
|
||||
The total number of folders.
|
||||
"""
|
||||
_total += 1
|
||||
pbardict["traversingbar"].desc = f"Traversing filesystem - {folder}"
|
||||
pbardict["traversingbar"].update(1)
|
||||
|
||||
items = sorted(os.listdir(folder))
|
||||
for item in items:
|
||||
if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)) and not item.startswith("."):
|
||||
if item not in _args.exclude_folders and not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
|
||||
_total = get_total_folders(os.path.join(folder, item), _args, _total)
|
||||
return _total
|
||||
|
||||
|
||||
def main() -> None:
|
||||
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, bool]] = []
|
||||
|
||||
args = parse_arguments(VERSION)
|
||||
args, raw = init_globals(args, RAW_EXTENSIONS)
|
||||
|
||||
lock_file = os.path.join(args.root_directory, ".lock")
|
||||
if os.path.exists(lock_file):
|
||||
print("Another instance of this program is running.")
|
||||
exit()
|
||||
|
||||
try:
|
||||
Path(lock_file).touch()
|
||||
logger.info("starting builder", extra={"version": VERSION})
|
||||
|
||||
logger.info("getting logo from sorogon.eu")
|
||||
req = urllib.request.Request("https://files.sorogon.eu/logo.svg")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as res:
|
||||
logo = res.read().decode()
|
||||
|
||||
if logo.startswith("<?xml"):
|
||||
logo = re.sub(r"<\?xml.+\?>", "", logo).strip()
|
||||
if logo.startswith("<!--"):
|
||||
logo = re.sub(r"<!--.+-->", "", logo).strip()
|
||||
logo = logo.replace("\n", " ")
|
||||
logo = " ".join(logo.split())
|
||||
except urllib.error.URLError:
|
||||
logo = "</srgn>"
|
||||
|
||||
if args.reread_metadata:
|
||||
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")):
|
||||
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)
|
||||
|
||||
@@ -196,21 +203,18 @@ def main() -> None:
|
||||
webmanifest(args)
|
||||
|
||||
if args.non_interactive_mode:
|
||||
logger.info("generating HTML files")
|
||||
print("Generating HTML files...")
|
||||
thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION)
|
||||
thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo)
|
||||
with Pool(os.cpu_count()) as pool:
|
||||
logger.info("generating thumbnails")
|
||||
print("Generating thumbnails...")
|
||||
pool.map(generate_thumbnail, thumbnails)
|
||||
else:
|
||||
pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True)
|
||||
total = get_total_folders(args.root_directory, args)
|
||||
pbardict["traversingbar"].desc = "Traversing filesystem"
|
||||
pbardict["traversingbar"].update(0)
|
||||
pbardict["traversingbar"].close()
|
||||
|
||||
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION)
|
||||
thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo)
|
||||
|
||||
with Pool(os.cpu_count()) as pool:
|
||||
logger.info("generating thumbnails")
|
||||
for _ in tqdm(
|
||||
pool.imap_unordered(generate_thumbnail, thumbnails),
|
||||
total=len(thumbnails),
|
||||
@@ -222,9 +226,9 @@ def main() -> None:
|
||||
pass
|
||||
finally:
|
||||
os.remove(lock_file)
|
||||
return
|
||||
logger.info("finished builder", extra={"version": VERSION})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
freeze_support()
|
||||
main()
|
||||
main(args)
|
||||
|
||||
118
files/global.css
@@ -28,6 +28,7 @@ body {
|
||||
.folders figure {
|
||||
margin-bottom: 32px;
|
||||
margin-top: 50px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
@@ -42,11 +43,21 @@ body {
|
||||
}
|
||||
|
||||
.folders figcaption {
|
||||
width: 120px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: smaller;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.folderthumb {
|
||||
height: 40px;
|
||||
width: 70px !important;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
margin: 20px 20px 0px -90px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
@@ -59,6 +70,10 @@ figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.licensefile {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
padding-top: 4px;
|
||||
text-align: center;
|
||||
@@ -90,7 +105,7 @@ figure {
|
||||
.footer a img {
|
||||
height: 22px !important;
|
||||
margin-left: 3px;
|
||||
vertical-align: text-bottom
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
@@ -116,14 +131,18 @@ figure {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar .title {
|
||||
.navbar .navleft {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.navbar .navcenter {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.navbar .license {
|
||||
float: right;
|
||||
.navbar .navright {
|
||||
float: right
|
||||
}
|
||||
|
||||
.navbar li .header {
|
||||
@@ -135,18 +154,31 @@ figure {
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
display: none;
|
||||
cursor: default;
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
float: left;
|
||||
max-height: 286px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.tooltip .infotext {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.tooltip .tagdropdown {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
@@ -157,12 +189,33 @@ figure {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Create eight equal columns that sits next to each other */
|
||||
.tooltip .tooltiptext .tagentry {
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext ol {
|
||||
margin-left: 0;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext .tagentry label {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.column {
|
||||
-ms-flex: 12.5%;
|
||||
flex: 12.5%;
|
||||
max-width: 12.5%;
|
||||
padding: 0 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.column img {
|
||||
@@ -175,6 +228,14 @@ figure {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attribution svg {
|
||||
height: calc(6.75pt + 12px);
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
bottom: -8px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
#totop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -187,7 +248,6 @@ figure {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Responsive layout - makes a four column-layout instead of eight columns */
|
||||
@media screen and (max-width: 1000px) {
|
||||
.column {
|
||||
-ms-flex: 25%;
|
||||
@@ -195,21 +255,32 @@ figure {
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.folders figure {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.folders img {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.folders figcaption {
|
||||
width: 100px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.folderthumb {
|
||||
height: 30px;
|
||||
width: 50px !important;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
margin: 15px 20px 0px -70px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive layout - makes a two column-layout instead of four columns */
|
||||
@media screen and (max-width: 800px) {
|
||||
.column {
|
||||
-ms-flex: 50%;
|
||||
@@ -217,15 +288,27 @@ figure {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.folders figure {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.folders img {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.folders figcaption {
|
||||
width: 80px;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.folderthumb {
|
||||
height: 25px;
|
||||
width: 30px !important;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
margin: 10px 20px 0px -50px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
font-size: small;
|
||||
}
|
||||
@@ -239,7 +322,6 @@ figure {
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
|
||||
@media screen and (max-width: 600px) {
|
||||
.column {
|
||||
-ms-flex: 100%;
|
||||
@@ -247,15 +329,27 @@ figure {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.folders figure {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.folders img {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.folders figcaption {
|
||||
width: 60px;
|
||||
font-size: xx-small;
|
||||
}
|
||||
|
||||
.folderthumb {
|
||||
height: 15px;
|
||||
width: 20px !important;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
margin: 5px 10px 0px -35px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
font-size: xx-small;
|
||||
}
|
||||
|
||||
@@ -272,13 +272,14 @@ a.pswp__share--download:hover {
|
||||
color: #BBB; }
|
||||
|
||||
.pswp__caption__center {
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
line-height: 20px;
|
||||
color: #CCC; }
|
||||
color: #CCC;
|
||||
font-weight: bold; }
|
||||
|
||||
.pswp__caption--empty {
|
||||
display: none; }
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import base64
|
||||
import logging
|
||||
import fileinput
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import List
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from modules.logger import consolelogger as logger
|
||||
from modules.css_color import extract_colorscheme
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||
|
||||
|
||||
def replace_all(file, search_exp, replace_exp):
|
||||
for line in fileinput.input(file, inplace=1):
|
||||
@@ -36,11 +32,14 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
||||
output_file (str): Path where the screenshot will be saved.
|
||||
driver (webdriver.Chrome): The Chrome WebDriver instance.
|
||||
"""
|
||||
logger.info("taking screenshot for %s", css_file)
|
||||
try:
|
||||
# Open the HTML file or URL
|
||||
if html_file_path.startswith(("http://", "https://")):
|
||||
logger.info("opening URL: %s", html_file_path)
|
||||
driver.get(html_file_path)
|
||||
else:
|
||||
logger.info("opening file: %s", html_file_path)
|
||||
driver.get(f"file://{os.path.abspath(html_file_path)}")
|
||||
|
||||
# Remove current theme.css
|
||||
@@ -52,9 +51,11 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
||||
}
|
||||
});
|
||||
"""
|
||||
logger.info("removing current theme.css")
|
||||
driver.execute_script(remove_css_script)
|
||||
|
||||
with open(css_file, "r", encoding="utf-8") as f:
|
||||
logger.info("reading CSS file: %s", css_file)
|
||||
css_content = f.read()
|
||||
|
||||
# Extract folder icon content
|
||||
@@ -65,20 +66,26 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
||||
folder_icon_content = re.sub(r"/\*.*\*/", "", folder_icon_content)
|
||||
|
||||
for match in re.finditer(r"content: (.*);", folder_icon_content):
|
||||
logger.info("found foldericon", extra={"foldericon": folder_icon_content})
|
||||
folder_icon_content = match.group(1).replace('"', "")
|
||||
break
|
||||
|
||||
if "url" not in folder_icon_content:
|
||||
logger.info("Reading foldericon svg")
|
||||
with open(folder_icon_content, "r", encoding="utf-8") as f:
|
||||
svg = f.read()
|
||||
if "svg.j2" in folder_icon_content:
|
||||
logger.info("foldericon in theme file is a jinja2 template")
|
||||
colorscheme = extract_colorscheme(css_file)
|
||||
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)
|
||||
|
||||
css_content = f'{css_head}\n.foldericon {{\n content: url("data:image/svg+xml,{svg}");\n}}\n{css_tail}'
|
||||
|
||||
# Encode CSS content as Base64
|
||||
logger.info("encoding css content as base64")
|
||||
encoded_css = base64.b64encode(css_content.encode("utf-8")).decode("utf-8")
|
||||
|
||||
# Inject CSS into HTML using JavaScript
|
||||
@@ -87,24 +94,28 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
||||
style.innerHTML = atob('{encoded_css}');
|
||||
document.head.appendChild(style);
|
||||
"""
|
||||
logger.info("injecting CSS into HTML")
|
||||
driver.execute_script(apply_css_script)
|
||||
|
||||
# Wait for a while to ensure CSS is applied
|
||||
time.sleep(2)
|
||||
# time.sleep(1)
|
||||
|
||||
# Move mouse to info
|
||||
logger.info("moving mouse to info")
|
||||
hoverable = driver.find_element(By.CLASS_NAME, "tooltip")
|
||||
webdriver.ActionChains(driver).move_to_element(hoverable).perform()
|
||||
|
||||
# Capture screenshot
|
||||
logger.info("taking screenshot")
|
||||
driver.save_screenshot(output_file)
|
||||
logging.info("Screenshot saved to %s", output_file)
|
||||
logger.info("screenshot saved to %s", output_file)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("Failed to take screenshot for %s: %s", css_file, e)
|
||||
logger.error("failed to take screenshot for %s: %s", css_file, e)
|
||||
|
||||
|
||||
def create_preview(html_file_path: str, css_file: str, previews_folder: str):
|
||||
logger.info("creating preview for %s", css_file)
|
||||
out_file = os.path.basename(css_file).removesuffix(".css") + ".html"
|
||||
urllib.request.urlretrieve(html_file_path, os.path.join(previews_folder, out_file))
|
||||
basename = os.path.basename(css_file)
|
||||
@@ -127,21 +138,25 @@ def create_preview(html_file_path: str, css_file: str, previews_folder: str):
|
||||
foldericon = foldericon.replace('"', "")
|
||||
break
|
||||
if "url" in foldericon:
|
||||
logger.info("foldericon in theme file, using it")
|
||||
shutil.copyfile(css_file, os.path.join(path, "previews", basename))
|
||||
else:
|
||||
return
|
||||
with open(os.path.join(path, foldericon.removeprefix("themes/")), "r", encoding="utf-8") as f:
|
||||
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(css_file)
|
||||
svg = svg.replace("{{ color1 }}", colorscheme["color1"])
|
||||
svg = svg.replace("{{ color2 }}", colorscheme["color2"])
|
||||
svg = svg.replace("{{ color3 }}", colorscheme["color3"])
|
||||
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
|
||||
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)
|
||||
if os.path.exists(os.path.join(path, "previews", basename)):
|
||||
os.remove(os.path.join(path, "previews", basename))
|
||||
with open(os.path.join(path, "previews", basename), "x", encoding="utf-8") as f:
|
||||
logger.info("writing theme file")
|
||||
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
|
||||
logger.info("preview created for %s", css_file)
|
||||
|
||||
|
||||
def write_readme(directory_path: str, themes: List[str]) -> None:
|
||||
@@ -155,6 +170,7 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
|
||||
readme_path = os.path.join(directory_path, "README.md")
|
||||
try:
|
||||
with open(readme_path, "r", encoding="utf-8") as f:
|
||||
logger.info("reading README.md", extra={"file": readme_path})
|
||||
readme = f.read()
|
||||
|
||||
readme_head = readme.split("## Previews of included themes")[0]
|
||||
@@ -162,14 +178,15 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
|
||||
readme_head += "".join([f"\n### {theme}\n\n\n" for theme in themes])
|
||||
|
||||
with open(readme_path, "w", encoding="utf-8") as f:
|
||||
logger.info("writing README.md", extra={"file": readme_path})
|
||||
f.write(readme_head)
|
||||
|
||||
logging.info("README.md updated with previews of included themes.")
|
||||
logger.info("README.md updated with previews of included themes.")
|
||||
|
||||
except FileNotFoundError:
|
||||
logging.error("README.md not found in %s", directory_path)
|
||||
logger.error("README.md not found in %s", directory_path)
|
||||
except Exception as e:
|
||||
logging.error("Failed to write README.md: %s", e)
|
||||
logger.error("failed to write README.md: %s", e)
|
||||
|
||||
|
||||
def write_index(directory_path: str, themes: List[str]) -> None:
|
||||
@@ -198,7 +215,7 @@ def main(directory_path: str, html_file_path: str) -> None:
|
||||
html_file_path (str): Path to the HTML file or URL for rendering.
|
||||
"""
|
||||
if not os.path.exists(directory_path):
|
||||
logging.error('Error: Folder path "%s" does not exist.', directory_path)
|
||||
logger.error('Error: Folder path "%s" does not exist.', directory_path)
|
||||
return
|
||||
|
||||
# Setup Chrome options
|
||||
@@ -207,8 +224,9 @@ def main(directory_path: str, html_file_path: str) -> None:
|
||||
chrome_options.add_argument("--window-size=1920,1080") # Set window size to at least 1920x1080
|
||||
|
||||
# Initialize Chrome WebDriver
|
||||
chromedriver_path = "/usr/bin/chromedriver" # Replace with your actual path
|
||||
chromedriver_path = "/usr/bin/chromedriver"
|
||||
service = Service(chromedriver_path)
|
||||
logger.info("Using chromedriver at %s", chromedriver_path, extra={"chrome_options": chrome_options})
|
||||
driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||
|
||||
try:
|
||||
@@ -235,13 +253,16 @@ def main(directory_path: str, html_file_path: str) -> None:
|
||||
write_index(directory_path, themes)
|
||||
|
||||
finally:
|
||||
logger.info("closing chrome webdriver")
|
||||
driver.quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
logging.error("Usage: python script_name.py directory_path html_file_path")
|
||||
logger.error("Usage: python script_name.py directory_path html_file_path")
|
||||
else:
|
||||
dir_path = sys.argv[1]
|
||||
html_path = sys.argv[2]
|
||||
logger.info("Starting script", extra={"directory_path": dir_path, "html_file_path": html_path})
|
||||
main(dir_path, html_path)
|
||||
logger.info("Done!", extra={"directory_path": dir_path})
|
||||
|
||||
156
help.svg
@@ -1,4 +1,4 @@
|
||||
<svg class="rich-terminal" viewBox="0 0 1482 806.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="rich-terminal" viewBox="0 0 1482 855.1999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@@ -19,163 +19,171 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-3756623299-matrix {
|
||||
.terminal-1308707172-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-3756623299-title {
|
||||
.terminal-1308707172-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-3756623299-r1 { fill: #ff8700 }
|
||||
.terminal-3756623299-r2 { fill: #c5c8c6 }
|
||||
.terminal-3756623299-r3 { fill: #808080 }
|
||||
.terminal-3756623299-r4 { fill: #68a0b3 }
|
||||
.terminal-3756623299-r5 { fill: #00af87 }
|
||||
.terminal-1308707172-r1 { fill: #ff8700 }
|
||||
.terminal-1308707172-r2 { fill: #c5c8c6 }
|
||||
.terminal-1308707172-r3 { fill: #808080 }
|
||||
.terminal-1308707172-r4 { fill: #68a0b3 }
|
||||
.terminal-1308707172-r5 { fill: #00af87 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-3756623299-clip-terminal">
|
||||
<rect x="0" y="0" width="1463.0" height="755.4" />
|
||||
<clipPath id="terminal-1308707172-clip-terminal">
|
||||
<rect x="0" y="0" width="1463.0" height="804.1999999999999" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-0">
|
||||
<clipPath id="terminal-1308707172-line-0">
|
||||
<rect x="0" y="1.5" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-1">
|
||||
<clipPath id="terminal-1308707172-line-1">
|
||||
<rect x="0" y="25.9" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-2">
|
||||
<clipPath id="terminal-1308707172-line-2">
|
||||
<rect x="0" y="50.3" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-3">
|
||||
<clipPath id="terminal-1308707172-line-3">
|
||||
<rect x="0" y="74.7" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-4">
|
||||
<clipPath id="terminal-1308707172-line-4">
|
||||
<rect x="0" y="99.1" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-5">
|
||||
<clipPath id="terminal-1308707172-line-5">
|
||||
<rect x="0" y="123.5" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-6">
|
||||
<clipPath id="terminal-1308707172-line-6">
|
||||
<rect x="0" y="147.9" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-7">
|
||||
<clipPath id="terminal-1308707172-line-7">
|
||||
<rect x="0" y="172.3" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-8">
|
||||
<clipPath id="terminal-1308707172-line-8">
|
||||
<rect x="0" y="196.7" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-9">
|
||||
<clipPath id="terminal-1308707172-line-9">
|
||||
<rect x="0" y="221.1" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-10">
|
||||
<clipPath id="terminal-1308707172-line-10">
|
||||
<rect x="0" y="245.5" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-11">
|
||||
<clipPath id="terminal-1308707172-line-11">
|
||||
<rect x="0" y="269.9" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-12">
|
||||
<clipPath id="terminal-1308707172-line-12">
|
||||
<rect x="0" y="294.3" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-13">
|
||||
<clipPath id="terminal-1308707172-line-13">
|
||||
<rect x="0" y="318.7" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-14">
|
||||
<clipPath id="terminal-1308707172-line-14">
|
||||
<rect x="0" y="343.1" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-15">
|
||||
<clipPath id="terminal-1308707172-line-15">
|
||||
<rect x="0" y="367.5" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-16">
|
||||
<clipPath id="terminal-1308707172-line-16">
|
||||
<rect x="0" y="391.9" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-17">
|
||||
<clipPath id="terminal-1308707172-line-17">
|
||||
<rect x="0" y="416.3" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-18">
|
||||
<clipPath id="terminal-1308707172-line-18">
|
||||
<rect x="0" y="440.7" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-19">
|
||||
<clipPath id="terminal-1308707172-line-19">
|
||||
<rect x="0" y="465.1" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-20">
|
||||
<clipPath id="terminal-1308707172-line-20">
|
||||
<rect x="0" y="489.5" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-21">
|
||||
<clipPath id="terminal-1308707172-line-21">
|
||||
<rect x="0" y="513.9" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-22">
|
||||
<clipPath id="terminal-1308707172-line-22">
|
||||
<rect x="0" y="538.3" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-23">
|
||||
<clipPath id="terminal-1308707172-line-23">
|
||||
<rect x="0" y="562.7" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-24">
|
||||
<clipPath id="terminal-1308707172-line-24">
|
||||
<rect x="0" y="587.1" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-25">
|
||||
<clipPath id="terminal-1308707172-line-25">
|
||||
<rect x="0" y="611.5" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-26">
|
||||
<clipPath id="terminal-1308707172-line-26">
|
||||
<rect x="0" y="635.9" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-27">
|
||||
<clipPath id="terminal-1308707172-line-27">
|
||||
<rect x="0" y="660.3" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-28">
|
||||
<clipPath id="terminal-1308707172-line-28">
|
||||
<rect x="0" y="684.7" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-3756623299-line-29">
|
||||
<clipPath id="terminal-1308707172-line-29">
|
||||
<rect x="0" y="709.1" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1308707172-line-30">
|
||||
<rect x="0" y="733.5" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1308707172-line-31">
|
||||
<rect x="0" y="757.9" 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="804.4" rx="8"/>
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="853.2" 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-3756623299-clip-terminal)">
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-1308707172-clip-terminal)">
|
||||
|
||||
<g class="terminal-3756623299-matrix">
|
||||
<text class="terminal-3756623299-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">Usage:</text><text class="terminal-3756623299-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-3756623299-line-0)">builder.py</text><text class="terminal-3756623299-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)"> [</text><text class="terminal-3756623299-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-h</text><text class="terminal-3756623299-r2" x="256.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">] </text><text class="terminal-3756623299-r4" x="280.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-p</text><text class="terminal-3756623299-r5" x="317.2" y="20" textLength="48.8" clip-path="url(#terminal-3756623299-line-0)">ROOT</text><text class="terminal-3756623299-r4" x="378.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-w</text><text class="terminal-3756623299-r5" x="414.8" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">URL</text><text class="terminal-3756623299-r4" x="463.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-t</text><text class="terminal-3756623299-r5" x="500.2" y="20" textLength="61" clip-path="url(#terminal-3756623299-line-0)">TITLE</text><text class="terminal-3756623299-r2" x="561.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)"> [</text><text class="terminal-3756623299-r4" x="585.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-r</text><text class="terminal-3756623299-r2" x="610" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="646.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-n</text><text class="terminal-3756623299-r2" x="671" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="707.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-l</text><text class="terminal-3756623299-r5" x="744.2" y="20" textLength="85.4" clip-path="url(#terminal-3756623299-line-0)">LICENSE</text><text class="terminal-3756623299-r2" x="829.6" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-a</text><text class="terminal-3756623299-r5" x="902.8" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">AUTHOR</text><text class="terminal-3756623299-r2" x="976" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="1012.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-e</text><text class="terminal-3756623299-r5" x="1049.2" y="20" textLength="109.8" clip-path="url(#terminal-3756623299-line-0)">EXTENSION</text><text class="terminal-3756623299-r2" x="1159" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="1195.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-m</text><text class="terminal-3756623299-r2" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)">]</text><text class="terminal-3756623299-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)">
|
||||
</text><text class="terminal-3756623299-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">                  [</text><text class="terminal-3756623299-r4" x="231.8" y="44.4" textLength="146.4" clip-path="url(#terminal-3756623299-line-1)">--theme-path</text><text class="terminal-3756623299-r5" x="390.4" y="44.4" textLength="48.8" clip-path="url(#terminal-3756623299-line-1)">PATH</text><text class="terminal-3756623299-r2" x="439.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">] [</text><text class="terminal-3756623299-r4" x="475.8" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="707.6" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">] [</text><text class="terminal-3756623299-r4" x="744.2" y="44.4" textLength="244" clip-path="url(#terminal-3756623299-line-1)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="988.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">] [</text><text class="terminal-3756623299-r4" x="1024.8" y="44.4" textLength="195.2" clip-path="url(#terminal-3756623299-line-1)">--exclude-folder</text><text class="terminal-3756623299-r5" x="1232.2" y="44.4" textLength="73.2" clip-path="url(#terminal-3756623299-line-1)">FOLDER</text><text class="terminal-3756623299-r2" x="1305.4" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)">]</text><text class="terminal-3756623299-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)">
|
||||
</text><text class="terminal-3756623299-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-3756623299-line-2)">                  [</text><text class="terminal-3756623299-r4" x="231.8" y="68.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-2)">--version</text><text class="terminal-3756623299-r2" x="341.6" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)">]</text><text class="terminal-3756623299-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)">
|
||||
</text><text class="terminal-3756623299-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-3)">
|
||||
</text><text class="terminal-3756623299-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-3756623299-line-4)">Generate HTML files for a static image hosting website.</text><text class="terminal-3756623299-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-4)">
|
||||
</text><text class="terminal-3756623299-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-3756623299-line-5)">
|
||||
</text><text class="terminal-3756623299-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-3756623299-line-6)">Options:</text><text class="terminal-3756623299-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-6)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">-h</text><text class="terminal-3756623299-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">, </text><text class="terminal-3756623299-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-3756623299-line-7)">--help</text><text class="terminal-3756623299-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-3756623299-line-7)">show this help message and exit</text><text class="terminal-3756623299-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-7)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">-p</text><text class="terminal-3756623299-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">, </text><text class="terminal-3756623299-r4" x="73.2" y="215.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-8)">--root-directory</text><text class="terminal-3756623299-r5" x="280.6" y="215.2" textLength="48.8" clip-path="url(#terminal-3756623299-line-8)">ROOT</text><text class="terminal-3756623299-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-8)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="239.6" textLength="451.4" clip-path="url(#terminal-3756623299-line-9)">Root directory containing the images.</text><text class="terminal-3756623299-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-9)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">-w</text><text class="terminal-3756623299-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">, </text><text class="terminal-3756623299-r4" x="73.2" y="264" textLength="170.8" clip-path="url(#terminal-3756623299-line-10)">--web-root-url</text><text class="terminal-3756623299-r5" x="256.2" y="264" textLength="36.6" clip-path="url(#terminal-3756623299-line-10)">URL</text><text class="terminal-3756623299-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-3756623299-line-10)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="288.4" textLength="634.4" clip-path="url(#terminal-3756623299-line-11)">Base URL of the web root for the image hosting site.</text><text class="terminal-3756623299-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-11)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">-t</text><text class="terminal-3756623299-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">, </text><text class="terminal-3756623299-r4" x="73.2" y="312.8" textLength="146.4" clip-path="url(#terminal-3756623299-line-12)">--site-title</text><text class="terminal-3756623299-r5" x="231.8" y="312.8" textLength="61" clip-path="url(#terminal-3756623299-line-12)">TITLE</text><text class="terminal-3756623299-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-12)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="337.2" textLength="390.4" clip-path="url(#terminal-3756623299-line-13)">Title of the image hosting site.</text><text class="terminal-3756623299-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-13)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">-r</text><text class="terminal-3756623299-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">, </text><text class="terminal-3756623299-r4" x="73.2" y="361.6" textLength="280.6" clip-path="url(#terminal-3756623299-line-14)">--regenerate-thumbnails</text><text class="terminal-3756623299-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-14)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="386" textLength="597.8" clip-path="url(#terminal-3756623299-line-15)">Regenerate thumbnails even if they already exist.</text><text class="terminal-3756623299-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-3756623299-line-15)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">-n</text><text class="terminal-3756623299-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">, </text><text class="terminal-3756623299-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-3756623299-line-16)">--non-interactive-mode</text><text class="terminal-3756623299-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-16)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="434.8" textLength="646.6" clip-path="url(#terminal-3756623299-line-17)">Run in non-interactive mode, disabling progress bars.</text><text class="terminal-3756623299-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-17)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">-l</text><text class="terminal-3756623299-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">, </text><text class="terminal-3756623299-r4" x="73.2" y="459.2" textLength="170.8" clip-path="url(#terminal-3756623299-line-18)">--license-type</text><text class="terminal-3756623299-r5" x="256.2" y="459.2" textLength="85.4" clip-path="url(#terminal-3756623299-line-18)">LICENSE</text><text class="terminal-3756623299-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-18)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="483.6" textLength="488" clip-path="url(#terminal-3756623299-line-19)">Specify the license type for the images.</text><text class="terminal-3756623299-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-19)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">-a</text><text class="terminal-3756623299-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">, </text><text class="terminal-3756623299-r4" x="73.2" y="508" textLength="158.6" clip-path="url(#terminal-3756623299-line-20)">--author-name</text><text class="terminal-3756623299-r5" x="244" y="508" textLength="73.2" clip-path="url(#terminal-3756623299-line-20)">AUTHOR</text><text class="terminal-3756623299-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-3756623299-line-20)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="532.4" textLength="402.6" clip-path="url(#terminal-3756623299-line-21)">Name of the author of the images.</text><text class="terminal-3756623299-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-21)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">-e</text><text class="terminal-3756623299-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">, </text><text class="terminal-3756623299-r4" x="73.2" y="556.8" textLength="207.4" clip-path="url(#terminal-3756623299-line-22)">--file-extensions</text><text class="terminal-3756623299-r5" x="292.8" y="556.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-22)">EXTENSION</text><text class="terminal-3756623299-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-22)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="581.2" textLength="744.2" clip-path="url(#terminal-3756623299-line-23)">File extensions to include (can be specified multiple times).</text><text class="terminal-3756623299-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-23)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">-m</text><text class="terminal-3756623299-r2" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">, </text><text class="terminal-3756623299-r4" x="73.2" y="605.6" textLength="170.8" clip-path="url(#terminal-3756623299-line-24)">--web-manifest</text><text class="terminal-3756623299-r2" x="292.8" y="605.6" textLength="353.8" clip-path="url(#terminal-3756623299-line-24)">Generate a web manifest file.</text><text class="terminal-3756623299-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-24)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="630" textLength="146.4" clip-path="url(#terminal-3756623299-line-25)">--theme-path</text><text class="terminal-3756623299-r5" x="183" y="630" textLength="48.8" clip-path="url(#terminal-3756623299-line-25)">PATH</text><text class="terminal-3756623299-r2" x="292.8" y="630" textLength="329.4" clip-path="url(#terminal-3756623299-line-25)">Path to the CSS theme file.</text><text class="terminal-3756623299-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-3756623299-line-25)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="654.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-26)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="292.8" y="654.4" textLength="890.6" clip-path="url(#terminal-3756623299-line-26)">Enable fancy folder view instead of the default Apache directory listing.</text><text class="terminal-3756623299-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-26)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-3756623299-line-27)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="292.8" y="678.8" textLength="683.2" clip-path="url(#terminal-3756623299-line-27)">Ignore files that do not match the specified extensions.</text><text class="terminal-3756623299-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-27)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="703.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-28)">--exclude-folder</text><text class="terminal-3756623299-r5" x="231.8" y="703.2" textLength="73.2" clip-path="url(#terminal-3756623299-line-28)">FOLDER</text><text class="terminal-3756623299-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-28)">
|
||||
</text><text class="terminal-3756623299-r2" x="292.8" y="727.6" textLength="1049.2" clip-path="url(#terminal-3756623299-line-29)">Folders to exclude from processing, globs supported (can be specified multiple times).</text><text class="terminal-3756623299-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-29)">
|
||||
</text><text class="terminal-3756623299-r4" x="24.4" y="752" textLength="109.8" clip-path="url(#terminal-3756623299-line-30)">--version</text><text class="terminal-3756623299-r2" x="292.8" y="752" textLength="463.6" clip-path="url(#terminal-3756623299-line-30)">show program's version number and exit</text><text class="terminal-3756623299-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-3756623299-line-30)">
|
||||
<g class="terminal-1308707172-matrix">
|
||||
<text class="terminal-1308707172-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-1308707172-line-0)">Usage:</text><text class="terminal-1308707172-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-1308707172-line-0)">builder.py</text><text class="terminal-1308707172-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)"> [</text><text class="terminal-1308707172-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-h</text><text class="terminal-1308707172-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-1308707172-line-0)">] [</text><text class="terminal-1308707172-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-a</text><text class="terminal-1308707172-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-1308707172-line-0)">AUTHOR</text><text class="terminal-1308707172-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-1308707172-line-0)">] [</text><text class="terminal-1308707172-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-e</text><text class="terminal-1308707172-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-1308707172-line-0)">EXTENSION</text><text class="terminal-1308707172-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-1308707172-line-0)">] [</text><text class="terminal-1308707172-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-l</text><text class="terminal-1308707172-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-1308707172-line-0)">LICENSE</text><text class="terminal-1308707172-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-1308707172-line-0)">] [</text><text class="terminal-1308707172-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-m</text><text class="terminal-1308707172-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-1308707172-line-0)">] [</text><text class="terminal-1308707172-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-n</text><text class="terminal-1308707172-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">] </text><text class="terminal-1308707172-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-p</text><text class="terminal-1308707172-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-1308707172-line-0)">ROOT</text><text class="terminal-1308707172-r4" x="988.2" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-t</text><text class="terminal-1308707172-r5" x="1024.8" y="20" textLength="61" clip-path="url(#terminal-1308707172-line-0)">TITLE</text><text class="terminal-1308707172-r4" x="1098" y="20" textLength="24.4" clip-path="url(#terminal-1308707172-line-0)">-w</text><text class="terminal-1308707172-r5" x="1134.6" y="20" textLength="36.6" clip-path="url(#terminal-1308707172-line-0)">URL</text><text class="terminal-1308707172-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-1308707172-line-0)">
|
||||
</text><text class="terminal-1308707172-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-1308707172-line-1)">                  [</text><text class="terminal-1308707172-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-1308707172-line-1)">--exclude-folder</text><text class="terminal-1308707172-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-1308707172-line-1)">FOLDER</text><text class="terminal-1308707172-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-1308707172-line-1)">] [</text><text class="terminal-1308707172-r4" x="549" y="44.4" textLength="244" clip-path="url(#terminal-1308707172-line-1)">--ignore-other-files</text><text class="terminal-1308707172-r2" x="793" y="44.4" textLength="36.6" clip-path="url(#terminal-1308707172-line-1)">] [</text><text class="terminal-1308707172-r4" x="829.6" y="44.4" textLength="280.6" clip-path="url(#terminal-1308707172-line-1)">--regenerate-thumbnails</text><text class="terminal-1308707172-r2" x="1110.2" y="44.4" textLength="36.6" clip-path="url(#terminal-1308707172-line-1)">] [</text><text class="terminal-1308707172-r4" x="1146.8" y="44.4" textLength="207.4" clip-path="url(#terminal-1308707172-line-1)">--reread-metadata</text><text class="terminal-1308707172-r2" x="1354.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-1)">]</text><text class="terminal-1308707172-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-1)">
|
||||
</text><text class="terminal-1308707172-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-1308707172-line-2)">                  [</text><text class="terminal-1308707172-r4" x="231.8" y="68.8" textLength="170.8" clip-path="url(#terminal-1308707172-line-2)">--reverse-sort</text><text class="terminal-1308707172-r2" x="402.6" y="68.8" textLength="36.6" clip-path="url(#terminal-1308707172-line-2)">] [</text><text class="terminal-1308707172-r4" x="439.2" y="68.8" textLength="146.4" clip-path="url(#terminal-1308707172-line-2)">--theme-path</text><text class="terminal-1308707172-r5" x="597.8" y="68.8" textLength="48.8" clip-path="url(#terminal-1308707172-line-2)">PATH</text><text class="terminal-1308707172-r2" x="646.6" y="68.8" textLength="36.6" clip-path="url(#terminal-1308707172-line-2)">] [</text><text class="terminal-1308707172-r4" x="683.2" y="68.8" textLength="231.8" clip-path="url(#terminal-1308707172-line-2)">--use-fancy-folders</text><text class="terminal-1308707172-r2" x="915" y="68.8" textLength="36.6" clip-path="url(#terminal-1308707172-line-2)">] [</text><text class="terminal-1308707172-r4" x="951.6" y="68.8" textLength="109.8" clip-path="url(#terminal-1308707172-line-2)">--version</text><text class="terminal-1308707172-r2" x="1061.4" y="68.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-2)">]</text><text class="terminal-1308707172-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-2)">
|
||||
</text><text class="terminal-1308707172-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-1308707172-line-3)">
|
||||
</text><text class="terminal-1308707172-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-1308707172-line-4)">Generate HTML files for a static image hosting website.</text><text class="terminal-1308707172-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-1308707172-line-4)">
|
||||
</text><text class="terminal-1308707172-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-1308707172-line-5)">
|
||||
</text><text class="terminal-1308707172-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-1308707172-line-6)">Options:</text><text class="terminal-1308707172-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-6)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-1308707172-line-7)">-h</text><text class="terminal-1308707172-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-1308707172-line-7)">, </text><text class="terminal-1308707172-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-1308707172-line-7)">--help</text><text class="terminal-1308707172-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-1308707172-line-7)">show this help message and exit</text><text class="terminal-1308707172-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-7)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-1308707172-line-8)">-a</text><text class="terminal-1308707172-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-1308707172-line-8)">, </text><text class="terminal-1308707172-r4" x="73.2" y="215.2" textLength="158.6" clip-path="url(#terminal-1308707172-line-8)">--author-name</text><text class="terminal-1308707172-r5" x="244" y="215.2" textLength="73.2" clip-path="url(#terminal-1308707172-line-8)">AUTHOR</text><text class="terminal-1308707172-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-1308707172-line-8)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="239.6" textLength="402.6" clip-path="url(#terminal-1308707172-line-9)">Name of the author of the images.</text><text class="terminal-1308707172-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-1308707172-line-9)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-1308707172-line-10)">-e</text><text class="terminal-1308707172-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-1308707172-line-10)">, </text><text class="terminal-1308707172-r4" x="73.2" y="264" textLength="207.4" clip-path="url(#terminal-1308707172-line-10)">--file-extensions</text><text class="terminal-1308707172-r5" x="292.8" y="264" textLength="109.8" clip-path="url(#terminal-1308707172-line-10)">EXTENSION</text><text class="terminal-1308707172-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-1308707172-line-10)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="288.4" textLength="744.2" clip-path="url(#terminal-1308707172-line-11)">File extensions to include (can be specified multiple times).</text><text class="terminal-1308707172-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-11)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-1308707172-line-12)">-l</text><text class="terminal-1308707172-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-1308707172-line-12)">, </text><text class="terminal-1308707172-r4" x="73.2" y="312.8" textLength="170.8" clip-path="url(#terminal-1308707172-line-12)">--license-type</text><text class="terminal-1308707172-r5" x="256.2" y="312.8" textLength="85.4" clip-path="url(#terminal-1308707172-line-12)">LICENSE</text><text class="terminal-1308707172-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-12)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="337.2" textLength="488" clip-path="url(#terminal-1308707172-line-13)">Specify the license type for the images.</text><text class="terminal-1308707172-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-1308707172-line-13)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-1308707172-line-14)">-m</text><text class="terminal-1308707172-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-1308707172-line-14)">, </text><text class="terminal-1308707172-r4" x="73.2" y="361.6" textLength="170.8" clip-path="url(#terminal-1308707172-line-14)">--web-manifest</text><text class="terminal-1308707172-r2" x="292.8" y="361.6" textLength="353.8" clip-path="url(#terminal-1308707172-line-14)">Generate a web manifest file.</text><text class="terminal-1308707172-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-1308707172-line-14)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-1308707172-line-15)">-n</text><text class="terminal-1308707172-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-1308707172-line-15)">, </text><text class="terminal-1308707172-r4" x="73.2" y="386" textLength="268.4" clip-path="url(#terminal-1308707172-line-15)">--non-interactive-mode</text><text class="terminal-1308707172-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-1308707172-line-15)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="410.4" textLength="646.6" clip-path="url(#terminal-1308707172-line-16)">Run in non-interactive mode, disabling progress bars.</text><text class="terminal-1308707172-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-16)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="434.8" textLength="24.4" clip-path="url(#terminal-1308707172-line-17)">-p</text><text class="terminal-1308707172-r2" x="48.8" y="434.8" textLength="24.4" clip-path="url(#terminal-1308707172-line-17)">, </text><text class="terminal-1308707172-r4" x="73.2" y="434.8" textLength="195.2" clip-path="url(#terminal-1308707172-line-17)">--root-directory</text><text class="terminal-1308707172-r5" x="280.6" y="434.8" textLength="48.8" clip-path="url(#terminal-1308707172-line-17)">ROOT</text><text class="terminal-1308707172-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-17)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="459.2" textLength="451.4" clip-path="url(#terminal-1308707172-line-18)">Root directory containing the images.</text><text class="terminal-1308707172-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-1308707172-line-18)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="483.6" textLength="24.4" clip-path="url(#terminal-1308707172-line-19)">-t</text><text class="terminal-1308707172-r2" x="48.8" y="483.6" textLength="24.4" clip-path="url(#terminal-1308707172-line-19)">, </text><text class="terminal-1308707172-r4" x="73.2" y="483.6" textLength="146.4" clip-path="url(#terminal-1308707172-line-19)">--site-title</text><text class="terminal-1308707172-r5" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-1308707172-line-19)">TITLE</text><text class="terminal-1308707172-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-1308707172-line-19)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="508" textLength="390.4" clip-path="url(#terminal-1308707172-line-20)">Title of the image hosting site.</text><text class="terminal-1308707172-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-1308707172-line-20)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="532.4" textLength="24.4" clip-path="url(#terminal-1308707172-line-21)">-w</text><text class="terminal-1308707172-r2" x="48.8" y="532.4" textLength="24.4" clip-path="url(#terminal-1308707172-line-21)">, </text><text class="terminal-1308707172-r4" x="73.2" y="532.4" textLength="170.8" clip-path="url(#terminal-1308707172-line-21)">--web-root-url</text><text class="terminal-1308707172-r5" x="256.2" y="532.4" textLength="36.6" clip-path="url(#terminal-1308707172-line-21)">URL</text><text class="terminal-1308707172-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-21)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="556.8" textLength="634.4" clip-path="url(#terminal-1308707172-line-22)">Base URL of the web root for the image hosting site.</text><text class="terminal-1308707172-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-22)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="581.2" textLength="195.2" clip-path="url(#terminal-1308707172-line-23)">--exclude-folder</text><text class="terminal-1308707172-r5" x="231.8" y="581.2" textLength="73.2" clip-path="url(#terminal-1308707172-line-23)">FOLDER</text><text class="terminal-1308707172-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-1308707172-line-23)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="605.6" textLength="1049.2" clip-path="url(#terminal-1308707172-line-24)">Folders to exclude from processing, globs supported (can be specified multiple times).</text><text class="terminal-1308707172-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-1308707172-line-24)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="630" textLength="244" clip-path="url(#terminal-1308707172-line-25)">--ignore-other-files</text><text class="terminal-1308707172-r2" x="292.8" y="630" textLength="683.2" clip-path="url(#terminal-1308707172-line-25)">Ignore files that do not match the specified extensions.</text><text class="terminal-1308707172-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-1308707172-line-25)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="654.4" textLength="280.6" clip-path="url(#terminal-1308707172-line-26)">--regenerate-thumbnails</text><text class="terminal-1308707172-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-26)">
|
||||
</text><text class="terminal-1308707172-r2" x="292.8" y="678.8" textLength="597.8" clip-path="url(#terminal-1308707172-line-27)">Regenerate thumbnails even if they already exist.</text><text class="terminal-1308707172-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-27)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="703.2" textLength="207.4" clip-path="url(#terminal-1308707172-line-28)">--reread-metadata</text><text class="terminal-1308707172-r2" x="292.8" y="703.2" textLength="256.2" clip-path="url(#terminal-1308707172-line-28)">Reread image metadata</text><text class="terminal-1308707172-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-1308707172-line-28)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="727.6" textLength="170.8" clip-path="url(#terminal-1308707172-line-29)">--reverse-sort</text><text class="terminal-1308707172-r2" x="292.8" y="727.6" textLength="353.8" clip-path="url(#terminal-1308707172-line-29)">Sort images in reverse order.</text><text class="terminal-1308707172-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-1308707172-line-29)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="752" textLength="146.4" clip-path="url(#terminal-1308707172-line-30)">--theme-path</text><text class="terminal-1308707172-r5" x="183" y="752" textLength="48.8" clip-path="url(#terminal-1308707172-line-30)">PATH</text><text class="terminal-1308707172-r2" x="292.8" y="752" textLength="329.4" clip-path="url(#terminal-1308707172-line-30)">Path to the CSS theme file.</text><text class="terminal-1308707172-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-1308707172-line-30)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="776.4" textLength="231.8" clip-path="url(#terminal-1308707172-line-31)">--use-fancy-folders</text><text class="terminal-1308707172-r2" x="292.8" y="776.4" textLength="890.6" clip-path="url(#terminal-1308707172-line-31)">Enable fancy folder view instead of the default Apache directory listing.</text><text class="terminal-1308707172-r2" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-1308707172-line-31)">
|
||||
</text><text class="terminal-1308707172-r4" x="24.4" y="800.8" textLength="109.8" clip-path="url(#terminal-1308707172-line-32)">--version</text><text class="terminal-1308707172-r2" x="292.8" y="800.8" textLength="463.6" clip-path="url(#terminal-1308707172-line-32)">show program's version number and exit</text><text class="terminal-1308707172-r2" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-1308707172-line-32)">
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
73
hl_config.yaml
Normal file
@@ -0,0 +1,73 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
|
||||
$schema: https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
|
||||
|
||||
# Time format, see https://man7.org/linux/man-pages/man1/date.1.html for details.
|
||||
time-format: "%b %d %T.%3N"
|
||||
|
||||
# Time zone name, see column "TZ identifier" at
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones page.
|
||||
time-zone: "Europe/Vienna"
|
||||
|
||||
# Settings for fields processing.
|
||||
fields:
|
||||
# Configuration of the predefined set of fields.
|
||||
predefined:
|
||||
time:
|
||||
show: auto
|
||||
names: ["asctime"]
|
||||
logger:
|
||||
names: ["defaultlogger", "consolelogger"]
|
||||
level:
|
||||
show: auto
|
||||
variants:
|
||||
- names: ["levelname"]
|
||||
values:
|
||||
debug: ["DEBUG"]
|
||||
info: ["INFO"]
|
||||
warning: ["WARNING", "WARN"]
|
||||
error: ["ERROR", "FATAL", "CRITICAL"]
|
||||
- names: ["levelno"]
|
||||
values:
|
||||
debug: [10]
|
||||
info: [20]
|
||||
warning: [30]
|
||||
error: [40, 50]
|
||||
message:
|
||||
names: ["message"]
|
||||
caller:
|
||||
names: ["funcName"]
|
||||
caller-file:
|
||||
names: ["filename"]
|
||||
caller-line:
|
||||
names: ["lineno"]
|
||||
# List of wildcard field names to ignore.
|
||||
ignore: ["_*"]
|
||||
# List of exact field names to hide.
|
||||
hide: ["pathname", "created", "levelno", "taskname", "relativeCreated", "thread", "process", "msecs"]
|
||||
|
||||
# Formatting settings.
|
||||
formatting:
|
||||
flatten: always
|
||||
punctuation:
|
||||
logger-name-separator: ":"
|
||||
field-key-value-separator: "="
|
||||
string-opening-quote: "'"
|
||||
string-closing-quote: "'"
|
||||
source-location-separator: "→ "
|
||||
hidden-fields-indicator: " ..."
|
||||
level-left-separator: "│"
|
||||
level-right-separator: "│"
|
||||
input-number-prefix: "#"
|
||||
input-number-left-separator: ""
|
||||
input-number-right-separator: " │ "
|
||||
input-name-left-separator: ""
|
||||
input-name-right-separator: " │ "
|
||||
input-name-clipping: "··"
|
||||
input-name-common-part: "··"
|
||||
array-separator: " "
|
||||
|
||||
# Number of processing threads, configured automatically based on CPU count if not specified.
|
||||
concurrency: ~
|
||||
|
||||
# Currently selected theme.
|
||||
theme: "neutral"
|
||||
@@ -1,9 +1,16 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import os
|
||||
import argparse
|
||||
|
||||
try:
|
||||
from rich_argparse import RichHelpFormatter, HelpPreviewAction
|
||||
|
||||
RICH = True
|
||||
except ModuleNotFoundError:
|
||||
RICH = False
|
||||
|
||||
|
||||
if __package__ is None:
|
||||
PACKAGE = ""
|
||||
else:
|
||||
@@ -22,10 +29,12 @@ class Args:
|
||||
-----------
|
||||
author_name : str
|
||||
The name of the author of the images.
|
||||
exclude_folders : List[str]
|
||||
exclude_folders : list[str]
|
||||
A list of folders to exclude from processing.
|
||||
file_extensions : List[str]
|
||||
file_extensions : list[str]
|
||||
A list of file extensions to include.
|
||||
folder_thumbs : bool
|
||||
Wether to generate subfolder thumbnails.
|
||||
generate_webmanifest : bool
|
||||
Whether to generate a web manifest file.
|
||||
ignore_other_files : bool
|
||||
@@ -49,13 +58,16 @@ class Args:
|
||||
"""
|
||||
|
||||
author_name: str
|
||||
exclude_folders: List[str]
|
||||
file_extensions: List[str]
|
||||
exclude_folders: list[str]
|
||||
file_extensions: list[str]
|
||||
folder_thumbs: bool
|
||||
generate_webmanifest: bool
|
||||
ignore_other_files: bool
|
||||
license_type: Optional[str]
|
||||
non_interactive_mode: bool
|
||||
regenerate_thumbnails: bool
|
||||
reread_metadata: bool
|
||||
reverse_sort: bool
|
||||
root_directory: str
|
||||
site_title: str
|
||||
theme_path: str
|
||||
@@ -67,12 +79,15 @@ class Args:
|
||||
result["author_name"] = self.author_name
|
||||
result["exclude_folders"] = self.exclude_folders
|
||||
result["file_extensions"] = self.file_extensions
|
||||
result["folder_thumbs"] = self.folder_thumbs
|
||||
result["generate_webmanifest"] = self.generate_webmanifest
|
||||
result["ignore_other_files"] = self.ignore_other_files
|
||||
if self.license_type is not None:
|
||||
result["license_type"] = self.license_type
|
||||
result["non_interactive_mode"] = self.non_interactive_mode
|
||||
result["regenerate_thumbnails"] = self.regenerate_thumbnails
|
||||
result["reread_metadata"] = self.reread_metadata
|
||||
result["reverse_sort"] = self.reverse_sort
|
||||
result["root_directory"] = self.root_directory
|
||||
result["site_title"] = self.site_title
|
||||
result["theme_path"] = self.theme_path
|
||||
@@ -96,33 +111,43 @@ def parse_arguments(version: str) -> Args:
|
||||
An instance of the Args class containing the parsed arguments.
|
||||
"""
|
||||
# fmt: off
|
||||
if RICH:
|
||||
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter)
|
||||
parser.add_argument("--exclude-folder", help="Folders to exclude from processing, globs supported (can be specified multiple times).", action="append", dest="exclude_folders", metavar="FOLDER")
|
||||
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg")
|
||||
parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
|
||||
parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
|
||||
parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders")
|
||||
parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
|
||||
else:
|
||||
parser = argparse.ArgumentParser(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")
|
||||
parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest")
|
||||
parser.add_argument("-n", "--non-interactive-mode", help="Run in non-interactive mode, disabling progress bars.", action="store_true", default=False, dest="non_interactive_mode")
|
||||
parser.add_argument("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT")
|
||||
parser.add_argument("-r", "--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
|
||||
parser.add_argument("-t", "--site-title", help="Title of the image hosting site.", required=True, type=str, dest="site_title", metavar="TITLE")
|
||||
parser.add_argument("-w", "--web-root-url", help="Base URL of the web root for the image hosting site.", required=True, type=str, dest="web_root_url", metavar="URL")
|
||||
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("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
|
||||
parser.add_argument("--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
|
||||
parser.add_argument("--reread-metadata", help="Reread image metadata", action="store_true", default=False, dest="reread_metadata")
|
||||
parser.add_argument("--reverse-sort", help="Sort images in reverse order.", action="store_true", default=False, dest="reverse_sort")
|
||||
parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
|
||||
parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders")
|
||||
parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
|
||||
parsed_args = parser.parse_args()
|
||||
# fmt: on
|
||||
_args = Args(
|
||||
author_name=parsed_args.author_name,
|
||||
exclude_folders=parsed_args.exclude_folders,
|
||||
file_extensions=parsed_args.file_extensions,
|
||||
folder_thumbs=parsed_args.folder_thumbs,
|
||||
generate_webmanifest=parsed_args.generate_webmanifest,
|
||||
ignore_other_files=parsed_args.ignore_other_files,
|
||||
license_type=parsed_args.license_type,
|
||||
non_interactive_mode=parsed_args.non_interactive_mode,
|
||||
regenerate_thumbnails=parsed_args.regenerate_thumbnails,
|
||||
reread_metadata=parsed_args.reread_metadata,
|
||||
reverse_sort=parsed_args.reverse_sort,
|
||||
root_directory=parsed_args.root_directory,
|
||||
site_title=parsed_args.site_title,
|
||||
theme_path=parsed_args.theme_path,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import re
|
||||
import colorsys
|
||||
from typing import Dict
|
||||
|
||||
from modules.logger import logger
|
||||
|
||||
|
||||
def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
||||
def extract_colorscheme(theme_path: str) -> dict[str, str]:
|
||||
"""
|
||||
Extract color scheme from a CSS theme file.
|
||||
|
||||
@@ -14,9 +15,10 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
||||
|
||||
Returns:
|
||||
--------
|
||||
Dict[str, str]
|
||||
Dictionary containing color scheme variables and their hexadecimal values.
|
||||
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]+);"
|
||||
colorscheme = {}
|
||||
|
||||
@@ -30,6 +32,7 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
||||
color_value = match[1]
|
||||
hex_color_value = css_color_to_hex(color_value)
|
||||
colorscheme[variable_name] = hex_color_value
|
||||
logger.debug("extracted variable", extra={"variable": variable_name, "value": hex_color_value})
|
||||
|
||||
return colorscheme
|
||||
|
||||
@@ -86,10 +89,12 @@ def css_color_to_hex(css_color: str) -> str:
|
||||
|
||||
# Helper function to convert RGB tuple to hexadecimal string
|
||||
def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
||||
logger.debug("converting rgb tuple to hex string", extra={"rgb": rgb})
|
||||
return "#{:02x}{:02x}{:02x}".format(*rgb)
|
||||
|
||||
# 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))
|
||||
|
||||
# Regular expression pattern to match CSS colors
|
||||
@@ -103,6 +108,7 @@ def css_color_to_hex(css_color: str) -> str:
|
||||
match = color_pattern.match(css_color.strip())
|
||||
|
||||
if not match:
|
||||
logger.error("invalid CSS color format", extra={"css_color": css_color})
|
||||
raise ValueError("Invalid CSS color format")
|
||||
|
||||
groups = match.groupdict()
|
||||
@@ -119,8 +125,10 @@ def css_color_to_hex(css_color: str) -> str:
|
||||
b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"])
|
||||
a = float(groups["a"]) if groups["a"] else 1.0
|
||||
if a < 1.0:
|
||||
return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255))
|
||||
logger.debug("converting rgba color to hex", extra={"color": css_color, "r": r, "g": g, "b": b, "a": a})
|
||||
return rgb_to_hex((r, g, b)) + f"{round(a * 255):02x}"
|
||||
else:
|
||||
logger.debug("converting rgb color to hex", extra={"color": css_color, "r": r, "g": g, "b": b})
|
||||
return rgb_to_hex((r, g, b))
|
||||
|
||||
elif groups["hsl"]:
|
||||
@@ -130,8 +138,10 @@ def css_color_to_hex(css_color: str) -> str:
|
||||
a = float(groups["a"]) if groups["a"] else 1.0
|
||||
rgb_color = hsl_to_rgb((h, s, l))
|
||||
if a < 1.0:
|
||||
return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255))
|
||||
logger.debug("converting hsla color to hex", extra={"color": css_color, "hsl": (h, s, l), "a": a})
|
||||
return rgb_to_hex(rgb_color) + f"{round(a * 255):02x}"
|
||||
else:
|
||||
logger.debug("converting hsl color to hex", extra={"color": css_color, "hsl": (h, s, l)})
|
||||
return rgb_to_hex(rgb_color)
|
||||
|
||||
# fmt: off
|
||||
@@ -182,7 +192,9 @@ def css_color_to_hex(css_color: str) -> str:
|
||||
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
|
||||
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32'
|
||||
}
|
||||
logger.debug("parsing css color string", extra={"css_color": css_color})
|
||||
return named_colors[groups['name'].lower()]
|
||||
# fmt: on
|
||||
|
||||
logger.error("invalid CSS color format", extra={"css_color": css_color})
|
||||
raise ValueError("Invalid CSS color format")
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
import fnmatch
|
||||
import json
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
from tqdm.auto import tqdm
|
||||
from PIL import Image, ExifTags
|
||||
from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from defusedxml import ElementTree
|
||||
|
||||
import modules.cclicense as cclicense
|
||||
from modules.logger import logger
|
||||
from modules import cclicense
|
||||
from modules.argumentparser import Args
|
||||
|
||||
# Constants for file paths and exclusions
|
||||
@@ -22,57 +26,127 @@ FAVICON_PATH = ".static/favicon.ico"
|
||||
GLOBAL_CSS_PATH = ".static/global.css"
|
||||
EXCLUDES = ["index.html", "manifest.json", "robots.txt"]
|
||||
|
||||
# Set the maximum image pixels to prevent decompression bomb DOS attacks
|
||||
# Set the maximum image pixels
|
||||
Image.MAX_IMAGE_PIXELS = 933120000
|
||||
|
||||
# Initialize Jinja2 environment for template rendering
|
||||
env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates")))
|
||||
thumbnails: List[Tuple[str, str]] = []
|
||||
info: Dict[str, str] = {}
|
||||
pbardict: Dict[str, tqdm] = {}
|
||||
thumbnails: list[tuple[str, str, str]] = []
|
||||
info: dict[str, str] = {}
|
||||
licens: dict[str, str] = {}
|
||||
|
||||
|
||||
def initialize_sizelist(folder: str) -> Dict[str, Dict[str, int]]:
|
||||
def getxmp(strbuffer: str) -> dict[str, Any]:
|
||||
"""
|
||||
Initializes the size list JSON file if it doesn't exist.
|
||||
Returns a dictionary containing the XMP tags.
|
||||
Requires defusedxml to be installed.
|
||||
|
||||
:returns: XMP tags in a dictionary.
|
||||
"""
|
||||
|
||||
def get_name(tag: str) -> str:
|
||||
return re.sub("^{[^}]+}", "", tag)
|
||||
|
||||
def get_value(element) -> str | dict[str, Any] | None:
|
||||
value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()}
|
||||
children = list(element)
|
||||
if children:
|
||||
for child in children:
|
||||
name = get_name(child.tag)
|
||||
child_value = get_value(child)
|
||||
if name in value:
|
||||
if not isinstance(value[name], list):
|
||||
value[name] = [value[name]]
|
||||
value[name].append(child_value)
|
||||
else:
|
||||
value[name] = child_value
|
||||
elif value:
|
||||
if element.text:
|
||||
value["text"] = element.text
|
||||
else:
|
||||
return element.text
|
||||
return value
|
||||
|
||||
root = ElementTree.fromstring(strbuffer)
|
||||
return {get_name(root.tag): get_value(root)}
|
||||
|
||||
|
||||
def initialize_metadata(folder: str) -> dict[str, dict[str, int]]:
|
||||
"""
|
||||
Initializes the metadata JSON file if it doesn't exist.
|
||||
|
||||
Args:
|
||||
folder (str): The folder in which the size list file is located.
|
||||
folder (str): The folder in which the metadata file is located.
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict[str, int]]: The size list dictionary.
|
||||
dict[str, dict[str, int]]: The metadata dictionary.
|
||||
"""
|
||||
sizelist = {}
|
||||
sizelist_path = os.path.join(folder, ".sizelist.json")
|
||||
if not os.path.exists(sizelist_path):
|
||||
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile:
|
||||
sizelistfile.write("{}")
|
||||
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile:
|
||||
metadata = {}
|
||||
metadata_path = os.path.join(folder, ".metadata.json")
|
||||
if not os.path.exists(metadata_path):
|
||||
logger.info("creating new metadata file", extra={"file": metadata_path})
|
||||
with open(metadata_path, "x", encoding="utf-8") as metadatafile:
|
||||
metadatafile.write("{}")
|
||||
with open(metadata_path, "r+", encoding="utf-8") as metadatafile:
|
||||
logger.info("reading metadata file", extra={"file": metadata_path})
|
||||
try:
|
||||
sizelist = json.loads(sizelistfile.read())
|
||||
metadata = json.loads(metadatafile.read())
|
||||
except json.decoder.JSONDecodeError:
|
||||
sizelist = {}
|
||||
return sizelist
|
||||
logger.warning("invalid JSON in metadata file", extra={"file": metadata_path})
|
||||
metadata = {}
|
||||
|
||||
|
||||
def update_sizelist(sizelist: Dict[str, Dict[str, int]], folder: str) -> None:
|
||||
"""
|
||||
Updates the size list JSON file.
|
||||
|
||||
Args:
|
||||
sizelist (Dict[str, Dict[str, int]]): The size list dictionary to be written to the file.
|
||||
folder (str): The folder in which the size list file is located.
|
||||
"""
|
||||
# remove old sizelist if it exists
|
||||
sizelist_path = os.path.join(folder, ".sizelist.json")
|
||||
if sizelist != {}:
|
||||
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile:
|
||||
sizelistfile.write(json.dumps(sizelist, indent=4))
|
||||
else:
|
||||
if os.path.exists(sizelist_path):
|
||||
with open(sizelist_path, "r") as sizelist:
|
||||
metadata = json.loads(sizelist.read())
|
||||
logger.warning("found old .sizelist.json, removing it...", extra={"path": sizelist_path})
|
||||
os.remove(sizelist_path)
|
||||
|
||||
# convert from old metadata format
|
||||
if "images" not in metadata and "subfolders" not in metadata:
|
||||
images = metadata.copy()
|
||||
metadata = {}
|
||||
metadata["images"] = images
|
||||
elif "images" not in metadata:
|
||||
metadata["images"] = {}
|
||||
for k, v in metadata["images"].items():
|
||||
if "width" in v:
|
||||
metadata["images"][k]["w"] = v["width"]
|
||||
del metadata["images"][k]["width"]
|
||||
if "height" in v:
|
||||
metadata["images"][k]["h"] = v["height"]
|
||||
del metadata["images"][k]["height"]
|
||||
if "tags" not in v:
|
||||
metadata["images"][k]["tags"] = []
|
||||
if "exifdata" not in v:
|
||||
metadata["images"][k]["exifdata"] = None
|
||||
if "xmp" not in v:
|
||||
metadata["images"][k]["xmp"] = None
|
||||
|
||||
def get_image_info(item: str, folder: str) -> Dict[str, Any]:
|
||||
return metadata
|
||||
|
||||
|
||||
def update_metadata(metadata: dict[str, dict[str, Any]], folder: str) -> None:
|
||||
"""
|
||||
Updates the metadata JSON file.
|
||||
|
||||
Args:
|
||||
metadata (dict[str, dict[str, int]]): The metadata dictionary to be written to the file.
|
||||
folder (str): The folder in which the metadata file is located.
|
||||
"""
|
||||
metadata_path = os.path.join(folder, ".metadata.json")
|
||||
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))
|
||||
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]:
|
||||
"""
|
||||
Extracts image information and EXIF data.
|
||||
|
||||
@@ -81,18 +155,178 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
|
||||
folder (str): The folder containing the image.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary containing image width, height, and EXIF data.
|
||||
dict[str, Any]: A dictionary containing image width, height, and EXIF data.
|
||||
"""
|
||||
with Image.open(os.path.join(folder, item)) as img:
|
||||
exif = img.getexif()
|
||||
file = os.path.join(folder, item)
|
||||
try:
|
||||
with Image.open(file) as img:
|
||||
logger.info("extracting image information", extra={"file": file})
|
||||
width, height = img.size
|
||||
exifdata = {ExifTags.TAGS.get(key, key): val for key, val in exif.items()}
|
||||
exif = img.getexif()
|
||||
xmpdata = img.getxmp()
|
||||
|
||||
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}
|
||||
if exif:
|
||||
logger.info("extracting EXIF data", extra={"file": file})
|
||||
ifd = exif.get_ifd(ExifTags.IFD.Exif)
|
||||
exifdatas = dict(exif.items()) | ifd
|
||||
exifdata = {}
|
||||
for tag_id in exifdatas:
|
||||
tag = ExifTags.TAGS.get(tag_id, tag_id)
|
||||
content = exifdatas.get(tag_id)
|
||||
if isinstance(content, bytes):
|
||||
content = "0x" + content.hex()
|
||||
if isinstance(content, TiffImagePlugin.IFDRational):
|
||||
content = content.limit_rational(1000000)
|
||||
if isinstance(content, tuple):
|
||||
newtuple = ()
|
||||
for i in content:
|
||||
if isinstance(i, TiffImagePlugin.IFDRational):
|
||||
newtuple = (*newtuple, i.limit_rational(1000000))
|
||||
if newtuple:
|
||||
content = newtuple
|
||||
if tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]:
|
||||
epr = r"\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}"
|
||||
if re.match(epr, str(content)):
|
||||
try:
|
||||
content = datetime.strptime(str(content), "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
content = None
|
||||
else:
|
||||
content = None
|
||||
exifdata[tag] = content
|
||||
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
|
||||
logger.info("image is rotated", extra={"file": file})
|
||||
width, height = height, width
|
||||
return {"width": width, "height": height}
|
||||
for key in ["PrintImageMatching", "UserComment", "MakerNote"]:
|
||||
if key in exifdata:
|
||||
del exifdata[key]
|
||||
else:
|
||||
exifdata = None
|
||||
tags = []
|
||||
xmp = None
|
||||
if xmpdata:
|
||||
logger.info("extracting XMP data", extra={"file": file})
|
||||
try:
|
||||
tags = xmpdata["xmpmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
tags = xmpdata["xmpmeta"]["RDF"]["Description"]["hierarchicalSubject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
tags = xmpdata["xapmeta"]["RDF"]["Description"]["hierarchicalSubject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
if None in tags:
|
||||
tags.remove(None)
|
||||
return {"w": width, "h": height, "tags": tags, "exifdata": exifdata, "xmp": xmp}
|
||||
|
||||
|
||||
def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]:
|
||||
def nested_dict():
|
||||
return defaultdict(nested_dict)
|
||||
|
||||
|
||||
def insert_path(d, path):
|
||||
for part in path[:-1]:
|
||||
d = d[part]
|
||||
last = path[-1]
|
||||
if not isinstance(d[last], dict):
|
||||
d[last] = {}
|
||||
|
||||
|
||||
def finalize(d):
|
||||
if isinstance(d, defaultdict):
|
||||
# Sort keys before recursion
|
||||
return {k: finalize(d[k]) for k in sorted(d)}
|
||||
return d or []
|
||||
|
||||
|
||||
def parse_hierarchical_tags(tags, delimiter="|"):
|
||||
tree = nested_dict()
|
||||
for tag in tags:
|
||||
parts = tag.split(delimiter)
|
||||
insert_path(tree, parts)
|
||||
return finalize(tree)
|
||||
|
||||
|
||||
def get_tags(sidecarfile: str) -> list[str]:
|
||||
"""
|
||||
Extracts Tags from XMP sidecar file
|
||||
|
||||
Args:
|
||||
sidecarfile (str): The path to the XMP sidecar file.
|
||||
|
||||
Returns:
|
||||
list[str]: List containing image tags.
|
||||
"""
|
||||
logger.info("extracting XMP sidecar file data", extra={"file": sidecarfile})
|
||||
with open(sidecarfile) as sidecar:
|
||||
strbuffer = sidecar.read()
|
||||
xmpdata = getxmp(strbuffer)
|
||||
tags = []
|
||||
try:
|
||||
tags = xmpdata["xmpmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
tags = xmpdata["xmpmeta"]["RDF"]["Description"]["hierarchicalSubject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
tags = xmpdata["xapmeta"]["RDF"]["Description"]["hierarchicalSubject"]["Bag"]["li"]
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
if None in tags:
|
||||
tags.remove(None)
|
||||
return tags
|
||||
|
||||
|
||||
def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: dict[str, dict[str, int]], raw: list[str]) -> dict[str, Any]:
|
||||
"""
|
||||
Processes an image and prepares its data for the HTML template.
|
||||
|
||||
@@ -101,22 +335,30 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
|
||||
folder (str): The folder containing the image.
|
||||
_args (Args): Parsed command line arguments.
|
||||
baseurl (str): Base URL for the web root.
|
||||
sizelist (Dict[str, Dict[str, int]]): Dictionary containing size information for images.
|
||||
raw (List[str]): List of raw image file extensions.
|
||||
metadata (dict[str, dict[str, int]]): dictionary containing size information for images.
|
||||
raw (list[str]): list of raw image file extensions.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary containing image details for HTML rendering.
|
||||
dict[str, Any]: dictionary containing image details for HTML rendering.
|
||||
"""
|
||||
extsplit = os.path.splitext(item)
|
||||
if item not in sizelist or _args.regenerate_thumbnails:
|
||||
sizelist[item] = get_image_info(item, folder)
|
||||
if item not in metadata["images"] or _args.reread_metadata:
|
||||
metadata["images"][item] = get_image_info(item, folder)
|
||||
sidecarfile = os.path.join(folder, item + ".xmp")
|
||||
if os.path.exists(sidecarfile):
|
||||
logger.info("xmp sidecar file found", extra={"file": sidecarfile})
|
||||
try:
|
||||
metadata["images"][item]["tags"] = get_tags(sidecarfile)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
image = {
|
||||
"url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}",
|
||||
"thumbnail": f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg",
|
||||
"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,
|
||||
"width": sizelist[item]["width"],
|
||||
"height": sizelist[item]["height"],
|
||||
"w": metadata["images"][item]["w"],
|
||||
"h": metadata["images"][item]["h"],
|
||||
"tags": metadata["images"][item]["tags"],
|
||||
}
|
||||
path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg")
|
||||
if not os.path.exists(path) or _args.regenerate_thumbnails:
|
||||
@@ -125,16 +367,22 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
|
||||
thumbnails.append((folder, item, _args.root_directory))
|
||||
|
||||
for _raw in raw:
|
||||
if os.path.exists(os.path.join(folder, extsplit[0] + _raw)):
|
||||
url = urllib.parse.quote(extsplit[0]) + _raw
|
||||
file = os.path.join(folder, extsplit[0] + _raw)
|
||||
if os.path.exists(file):
|
||||
url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}"
|
||||
if _raw in (".tif", ".tiff"):
|
||||
image["tiff"] = f"{_args.web_root_url}{baseurl}{url}"
|
||||
logger.info("tiff file found", extra={"file": file})
|
||||
image["tiff"] = url
|
||||
else:
|
||||
image["raw"] = f"{_args.web_root_url}{baseurl}{url}"
|
||||
return image
|
||||
logger.info("raw file found", extra={"file": file, "extension": _raw})
|
||||
image["raw"] = url
|
||||
|
||||
metadata["images"][item].update(image)
|
||||
|
||||
return image, metadata
|
||||
|
||||
|
||||
def generate_html(folder: str, title: str, _args: Args, raw: List[str], version: str) -> None:
|
||||
def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo) -> list[str]:
|
||||
"""
|
||||
Generates HTML content for a folder of images.
|
||||
|
||||
@@ -142,50 +390,73 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
|
||||
folder (str): The folder to generate HTML for.
|
||||
title (str): The title of the HTML page.
|
||||
_args (Args): Parsed command line arguments.
|
||||
raw (List[str]): Raw image file names.
|
||||
raw (list[str]): Raw image file names.
|
||||
"""
|
||||
sizelist = initialize_sizelist(folder)
|
||||
logger.info("processing folder", extra={"folder": folder})
|
||||
if _args.regenerate_thumbnails:
|
||||
if os.path.exists(os.path.join(folder, ".metadata.json")):
|
||||
logger.info("removing .metadata.json", extra={"folder": folder})
|
||||
os.remove(os.path.join(folder, ".metadata.json"))
|
||||
metadata = initialize_metadata(folder)
|
||||
if _args.reverse_sort:
|
||||
items = sorted(os.listdir(folder), reverse=True)
|
||||
else:
|
||||
items = sorted(os.listdir(folder))
|
||||
|
||||
contains_files = False
|
||||
images = []
|
||||
subfolders = []
|
||||
subfoldertags = []
|
||||
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]
|
||||
for gon in gone:
|
||||
del metadata["images"][gon]
|
||||
|
||||
create_thumbnail_folder(foldername, _args.root_directory)
|
||||
|
||||
logger.info("processing contents", extra={"folder": folder})
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict[folder] = tqdm(total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True)
|
||||
|
||||
for item in items:
|
||||
for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True):
|
||||
if item not in EXCLUDES and not item.startswith("."):
|
||||
if os.path.isdir(os.path.join(folder, item)):
|
||||
process_subfolder(item, folder, baseurl, subfolders, _args, raw, version)
|
||||
subfoldertags = process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo)
|
||||
else:
|
||||
contains_files = True
|
||||
if os.path.splitext(item)[1].lower() in _args.file_extensions:
|
||||
images.append(process_image(item, folder, _args, baseurl, sizelist, raw))
|
||||
img, metadata = process_image(item, folder, _args, baseurl, metadata, raw)
|
||||
images.append(img)
|
||||
if item == "info":
|
||||
process_info_file(folder, item)
|
||||
if item == "LICENSE":
|
||||
process_license(folder, item)
|
||||
else:
|
||||
for item in items:
|
||||
if item not in EXCLUDES and not item.startswith("."):
|
||||
if os.path.isdir(os.path.join(folder, item)):
|
||||
subfoldertags = process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo)
|
||||
else:
|
||||
contains_files = True
|
||||
if os.path.splitext(item)[1].lower() in _args.file_extensions:
|
||||
img, metadata = process_image(item, folder, _args, baseurl, metadata, raw)
|
||||
images.append(img)
|
||||
if item == "info":
|
||||
process_info_file(folder, item)
|
||||
if item == "LICENSE":
|
||||
process_license(folder, item)
|
||||
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict[folder].update(1)
|
||||
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict[folder].close()
|
||||
|
||||
update_sizelist(sizelist, folder)
|
||||
metadata["subfolders"] = subfolders
|
||||
update_metadata(metadata, folder)
|
||||
|
||||
if should_generate_html(images, contains_files, _args):
|
||||
create_html_file(folder, title, foldername, images, subfolders, _args, version)
|
||||
subfoldertags = create_html_file(folder, title, foldername, images, subfolders, _args, version, logo, subfoldertags)
|
||||
else:
|
||||
if os.path.exists(os.path.join(folder, "index.html")):
|
||||
logger.info("removing existing index.html", extra={"folder": folder})
|
||||
os.remove(os.path.join(folder, "index.html"))
|
||||
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict["htmlbar"].update(1)
|
||||
return subfoldertags
|
||||
|
||||
|
||||
def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
|
||||
@@ -198,10 +469,11 @@ def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
|
||||
"""
|
||||
thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername)
|
||||
if not os.path.exists(thumbnails_path):
|
||||
logger.info("creating thumbnail folder", extra={"path": thumbnails_path})
|
||||
os.mkdir(thumbnails_path)
|
||||
|
||||
|
||||
def process_subfolder(item: str, folder: str, baseurl: str, subfolders: List[Dict[str, str]], _args: Args, raw: List[str], version: str) -> None:
|
||||
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]:
|
||||
"""
|
||||
Processes a subfolder.
|
||||
|
||||
@@ -209,15 +481,44 @@ def process_subfolder(item: str, folder: str, baseurl: str, subfolders: List[Dic
|
||||
item (str): The name of the subfolder.
|
||||
folder (str): The parent folder containing the subfolder.
|
||||
baseurl (str): Base URL for the web root.
|
||||
subfolders (List[Dict[str, str]]): List to store subfolder details.
|
||||
subfolders (list[dict[str, str]]): list to store subfolder details.
|
||||
_args (Args): Parsed command line arguments.
|
||||
raw (List[str]): Raw image file extensions.
|
||||
raw (list[str]): Raw image file extensions.
|
||||
"""
|
||||
subfolder_url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html" if _args.web_root_url.startswith("file://") else f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}"
|
||||
subfolders.append({"url": subfolder_url, "name": item})
|
||||
subfolder_url = (
|
||||
f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html"
|
||||
if _args.web_root_url.startswith("file://")
|
||||
else f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}"
|
||||
)
|
||||
thumb = None
|
||||
if _args.folder_thumbs:
|
||||
thumbitems = [i for i in sorted(os.listdir(os.path.join(folder, item))) if os.path.splitext(i)[1].lower() in _args.file_extensions]
|
||||
if len(thumbitems) > 0:
|
||||
if _args.reverse_sort:
|
||||
thumb = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}/{urllib.parse.quote(thumbitems[-1])}.jpg"
|
||||
else:
|
||||
thumb = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}/{urllib.parse.quote(thumbitems[0])}.jpg"
|
||||
|
||||
subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/.metadata.json"})
|
||||
if item not in _args.exclude_folders:
|
||||
if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
|
||||
generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version)
|
||||
return generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo)
|
||||
return []
|
||||
|
||||
|
||||
def process_license(folder: str, item: str) -> None:
|
||||
"""
|
||||
Processes a LICENSE file.
|
||||
|
||||
Args:
|
||||
folder (str): The folder containing the info file.
|
||||
item (str): The licenses file name.
|
||||
"""
|
||||
with open(os.path.join(folder, item), encoding="utf-8") as f:
|
||||
logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)})
|
||||
licens[urllib.parse.quote(folder)] = (
|
||||
f.read().replace("\n", "</br>\n").replace(" ", " ").replace(" ", " ").replace("sp; ", "sp; ").replace("  ", " ")
|
||||
)
|
||||
|
||||
|
||||
def process_info_file(folder: str, item: str) -> None:
|
||||
@@ -229,15 +530,16 @@ def process_info_file(folder: str, item: str) -> None:
|
||||
item (str): The info file name.
|
||||
"""
|
||||
with open(os.path.join(folder, item), encoding="utf-8") as f:
|
||||
logger.info("processing info file", extra={"path": os.path.join(folder, item)})
|
||||
info[urllib.parse.quote(folder)] = f.read()
|
||||
|
||||
|
||||
def should_generate_html(images: List[Dict[str, Any]], contains_files, _args: Args) -> bool:
|
||||
def should_generate_html(images: list[dict[str, Any]], contains_files, _args: Args) -> bool:
|
||||
"""
|
||||
Determines if HTML should be generated.
|
||||
|
||||
Args:
|
||||
images (List[Dict[str, Any]]): List of images.
|
||||
images (list[dict[str, Any]]): list of images.
|
||||
_args (Args): Parsed command line arguments.
|
||||
|
||||
Returns:
|
||||
@@ -246,7 +548,9 @@ def should_generate_html(images: List[Dict[str, Any]], contains_files, _args: Ar
|
||||
return images or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files)
|
||||
|
||||
|
||||
def create_html_file(folder: str, title: str, foldername: str, images: List[Dict[str, Any]], subfolders: List[Dict[str, str]], _args: Args, version: str) -> None:
|
||||
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]:
|
||||
"""
|
||||
Creates the HTML file using the template.
|
||||
|
||||
@@ -254,11 +558,12 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
|
||||
folder (str): The folder to create the HTML file in.
|
||||
title (str): The title of the HTML page.
|
||||
foldername (str): The name of the folder.
|
||||
images (List[Dict[str, Any]]): A list of images to include in the HTML.
|
||||
subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML.
|
||||
images (list[dict[str, Any]]): A list of images to include in the HTML.
|
||||
subfolders (list[dict[str, str]]): A list of subfolders to include in the HTML.
|
||||
_args (Args): Parsed command line arguments.
|
||||
"""
|
||||
image_chunks = np.array_split(images, 8) if images else []
|
||||
html_file = os.path.join(folder, "index.html")
|
||||
logger.info("generating html file with jinja2", extra={"path": html_file})
|
||||
header = os.path.basename(folder) or title
|
||||
parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}"
|
||||
if parent and _args.web_root_url.startswith("file://"):
|
||||
@@ -276,8 +581,42 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
|
||||
else None
|
||||
)
|
||||
|
||||
alltags = set()
|
||||
for img in images:
|
||||
alltags.update(img["tags"])
|
||||
|
||||
alltags.update(set(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)
|
||||
|
||||
license_url = ""
|
||||
|
||||
if folder_license:
|
||||
license_html = os.path.join(folder, "license.html")
|
||||
license_url = _args.web_root_url + urllib.parse.quote(foldername) + "license.html"
|
||||
with open(license_html, "w+", encoding="utf-8") as f:
|
||||
logger.info("writing license html file", extra={"path": license_html})
|
||||
gtml = env.get_template("license.html.j2")
|
||||
content = gtml.render(
|
||||
title=f"{title} - LICENSE",
|
||||
favicon=f"{_args.web_root_url}{FAVICON_PATH}",
|
||||
stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}",
|
||||
theme=f"{_args.web_root_url}.static/theme.css",
|
||||
root=_args.web_root_url,
|
||||
parent=f"{_args.web_root_url}{urllib.parse.quote(foldername)}",
|
||||
header=f"{header} - LICENSE",
|
||||
license=license_info,
|
||||
webmanifest=_args.generate_webmanifest,
|
||||
version=version,
|
||||
logo=logo,
|
||||
licensefile=folder_license,
|
||||
)
|
||||
f.write(content)
|
||||
|
||||
html = env.get_template("index.html.j2")
|
||||
content = html.render(
|
||||
@@ -290,32 +629,42 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
|
||||
header=header,
|
||||
license=license_info,
|
||||
subdirectories=subfolders,
|
||||
images=image_chunks,
|
||||
info=_info,
|
||||
allimages=images,
|
||||
webmanifest=_args.generate_webmanifest,
|
||||
version=version,
|
||||
logo=logo,
|
||||
licensefile=license_url,
|
||||
tags=parse_hierarchical_tags(alltags),
|
||||
)
|
||||
|
||||
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f:
|
||||
with open(html_file, "w", encoding="utf-8") as f:
|
||||
logger.info("writing html file", extra={"path": html_file})
|
||||
f.write(content)
|
||||
|
||||
if len(subfoldertags) > 1 and len(alltags) > 1:
|
||||
alltags.update(set(subfoldertags))
|
||||
return sorted(alltags)
|
||||
elif len(subfoldertags) > 1:
|
||||
return sorted(subfoldertags)
|
||||
elif len(alltags) > 1:
|
||||
return sorted(alltags)
|
||||
else:
|
||||
return []
|
||||
|
||||
def list_folder(total: int, folder: str, title: str, _args: Args, raw: List[str], version: str) -> List[Tuple[str, str]]:
|
||||
|
||||
def list_folder(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str, str]]:
|
||||
"""
|
||||
Lists and processes a folder, generating HTML files.
|
||||
lists and processes a folder, generating HTML files.
|
||||
|
||||
Args:
|
||||
total (int): Total number of folders to process.
|
||||
folder (str): The folder to process.
|
||||
title (str): The title of the HTML page.
|
||||
_args (Args): Parsed command line arguments.
|
||||
raw (List[str]): Raw image file names.
|
||||
raw (list[str]): Raw image file names.
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str]]: List of thumbnails generated.
|
||||
list[tuple[str, str]]: list of thumbnails generated.
|
||||
"""
|
||||
if not _args.non_interactive_mode:
|
||||
pbardict["htmlbar"] = tqdm(total=total, desc="Generating HTML files", unit="folders", ascii=True, dynamic_ncols=True)
|
||||
generate_html(folder, title, _args, raw, version)
|
||||
generate_html(folder, title, _args, raw, version, logo)
|
||||
return thumbnails
|
||||
|
||||
140
modules/logger.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
logger.py
|
||||
|
||||
This module provides functionality for setting up a centralized logging system using the
|
||||
`logging` library and the `python-json-logger` to output logs in JSON format. It handles
|
||||
log rotation by renaming old log files and saving them based on the first timestamp entry.
|
||||
|
||||
Functions:
|
||||
- log_format(keys): Generates the logging format string based on the list of keys.
|
||||
- rotate_log_file(): Handles renaming the existing log file to a timestamp-based name.
|
||||
- setup_logger(): Configures the logging system, applies a JSON format, and returns a logger instance.
|
||||
- setup_consolelogger(): Configures the logging system to output logs in console format.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import gzip
|
||||
import shutil
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pythonjsonlogger import jsonlogger
|
||||
|
||||
# Constants for file paths and exclusions
|
||||
if __package__ is None:
|
||||
PACKAGE = ""
|
||||
else:
|
||||
PACKAGE = __package__
|
||||
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
|
||||
LOG_DIR = os.path.join(SCRIPTDIR, "logs")
|
||||
LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl")
|
||||
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
|
||||
|
||||
def log_format(keys):
|
||||
"""
|
||||
Generates a list of format strings based on the given keys.
|
||||
|
||||
Args:
|
||||
keys (list): A list of string keys that represent the log attributes (e.g., 'asctime', 'levelname').
|
||||
|
||||
Returns:
|
||||
list: A list of formatted strings for each key, in the format "%(key)s".
|
||||
"""
|
||||
return [f"%({i})s" for i in keys]
|
||||
|
||||
|
||||
def rotate_log_file(compress=False):
|
||||
"""
|
||||
Truncates the 'latest.jsonl' file after optionally compressing its contents to a timestamped file.
|
||||
The 'latest.jsonl' file is not deleted or moved, just emptied.
|
||||
|
||||
Args:
|
||||
compress (bool): If True, compress the old log file using gzip.
|
||||
"""
|
||||
if os.path.exists(LATEST_LOG_FILE):
|
||||
with open(LATEST_LOG_FILE, "r+", encoding="utf-8") as f:
|
||||
first_line = f.readline()
|
||||
try:
|
||||
first_log = json.loads(first_line)
|
||||
first_timestamp = first_log.get("asctime")
|
||||
first_timestamp = first_timestamp.split(",")[0]
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
first_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_")
|
||||
old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl")
|
||||
|
||||
# Write contents to the new file
|
||||
with open(old_log_filename, "w", encoding="utf-8") as old_log_file:
|
||||
f.seek(0) # Go back to the beginning of the file
|
||||
shutil.copyfileobj(f, old_log_file)
|
||||
|
||||
if compress:
|
||||
with open(old_log_filename, "rb") as f_in:
|
||||
with gzip.open(f"{old_log_filename}.gz", "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
os.remove(old_log_filename)
|
||||
|
||||
# Truncate the original file
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
|
||||
|
||||
def setup_logger(level=logging.INFO):
|
||||
"""
|
||||
Configures the logging system with a custom format and outputs logs in JSON format.
|
||||
|
||||
The logger will write to the 'logs/latest.jsonl' file, and it will include
|
||||
multiple attributes such as the time of logging, the filename, function name, log level, etc.
|
||||
|
||||
Returns:
|
||||
logging.Logger: A configured logger instance that can be used to log messages.
|
||||
"""
|
||||
_logger = logging.getLogger(name="defaultlogger")
|
||||
|
||||
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
|
||||
|
||||
custom_format = " ".join(log_format(supported_keys))
|
||||
formatter = jsonlogger.JsonFormatter(custom_format)
|
||||
|
||||
log_handler = logging.FileHandler(LATEST_LOG_FILE)
|
||||
log_handler.setFormatter(formatter)
|
||||
|
||||
_logger.addHandler(log_handler)
|
||||
_logger.setLevel(level=level)
|
||||
|
||||
return _logger
|
||||
|
||||
|
||||
def setup_consolelogger(level=logging.INFO):
|
||||
"""
|
||||
Configures the logging system to output logs in console and JSON format.
|
||||
|
||||
The logger will write to the 'logs/latest.jsonl' file, and it will include
|
||||
multiple attributes such as the time of logging, the filename, function name, log level, etc.
|
||||
|
||||
Returns:
|
||||
logging.Logger: A configured logger instance that can be used to log messages.
|
||||
"""
|
||||
_logger = logging.getLogger(name="consolelogger")
|
||||
|
||||
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
|
||||
|
||||
custom_format = " ".join(log_format(supported_keys))
|
||||
formatter = jsonlogger.JsonFormatter(custom_format)
|
||||
|
||||
log_handler = logging.FileHandler(LATEST_LOG_FILE)
|
||||
log_handler.setFormatter(formatter)
|
||||
|
||||
_logger.addHandler(log_handler)
|
||||
_logger.addHandler(logging.StreamHandler())
|
||||
_logger.setLevel(level=level)
|
||||
return _logger
|
||||
|
||||
|
||||
rotate_log_file(compress=True)
|
||||
logger = setup_logger()
|
||||
consolelogger = setup_consolelogger()
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
from typing import List, Dict
|
||||
from subprocess import Popen, PIPE
|
||||
from PIL import Image
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
@@ -13,6 +13,7 @@ try:
|
||||
except ImportError:
|
||||
SVGSUPPORT = False
|
||||
|
||||
from modules.logger import logger
|
||||
from modules.argumentparser import Args
|
||||
from modules.css_color import extract_theme_color, extract_colorscheme
|
||||
|
||||
@@ -36,14 +37,14 @@ class Icon:
|
||||
purpose: str
|
||||
|
||||
|
||||
def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str:
|
||||
def render_svg_icon(colorscheme: dict[str, str], iconspath: str) -> str:
|
||||
"""
|
||||
Render an SVG icon using the provided color scheme.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
colorscheme : Dict[str, str]
|
||||
Dictionary containing color scheme variables and their values.
|
||||
colorscheme : dict[str, str]
|
||||
dictionary containing color scheme variables and their values.
|
||||
iconspath : str
|
||||
Path to the directory where the icon will be saved.
|
||||
|
||||
@@ -55,6 +56,7 @@ def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str:
|
||||
svg = env.get_template("icon.svg.j2")
|
||||
content = svg.render(colorscheme=colorscheme)
|
||||
with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f:
|
||||
logger.info("writing svg icon", extra={"iconspath": iconspath})
|
||||
f.write(content)
|
||||
return content
|
||||
|
||||
@@ -73,6 +75,7 @@ def save_png_icon(content: str, iconspath: str) -> None:
|
||||
tmpimg = BytesIO()
|
||||
cairosvg.svg2png(bytestring=content, write_to=tmpimg)
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
logger.info("saving png icon", extra={"iconspath": iconspath})
|
||||
iconfile.save(os.path.join(iconspath, "icon.png"))
|
||||
|
||||
|
||||
@@ -87,10 +90,25 @@ def generate_favicon(iconspath: str, root_directory: str) -> None:
|
||||
root_directory : str
|
||||
Root directory of the project where the favicon will be saved.
|
||||
"""
|
||||
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
|
||||
favicon = os.path.join(root_directory, ".static", "favicon.ico")
|
||||
logger.info("generating favicon with imagemagick", extra={"iconspath": iconspath, "favicon": favicon})
|
||||
_env = dict(os.environ)
|
||||
lp_key = "LD_LIBRARY_PATH"
|
||||
lp_orig = _env.get(lp_key + "_ORIG")
|
||||
if lp_orig is not None:
|
||||
_env[lp_key] = lp_orig
|
||||
else:
|
||||
_env.pop(lp_key, None)
|
||||
if not shutil.which("magick"):
|
||||
command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
|
||||
os.system(command)
|
||||
magick = shutil.which("convert")
|
||||
else:
|
||||
magick = shutil.which("magick")
|
||||
command = [magick, os.path.join(iconspath, "icon.png"), "-define", "icon:auto-resize=16,32,48,64,72,96,144,192", favicon]
|
||||
with Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=_env, errors="ignore") as p:
|
||||
out, err = p.communicate()
|
||||
if p.returncode != 0:
|
||||
logger.error("error generating favicon: %s", err, extra={"command": command, "out": out, "err": err})
|
||||
logger.info("favicon generated successfully", extra={"command": command, "out": out, "err": err})
|
||||
|
||||
|
||||
def icons(_args: Args) -> None:
|
||||
@@ -102,18 +120,20 @@ def icons(_args: Args) -> None:
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
"""
|
||||
print("Generating icons...")
|
||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||
logger.info("generating icons", extra={"iconspath": iconspath})
|
||||
print("Generating icons...")
|
||||
colorscheme = extract_colorscheme(_args.theme_path)
|
||||
content = render_svg_icon(colorscheme, iconspath)
|
||||
if not SVGSUPPORT:
|
||||
print("Please install cairosvg to generate favicon from svg icon.")
|
||||
logger.error("svg support not available")
|
||||
return
|
||||
save_png_icon(content, iconspath)
|
||||
generate_favicon(iconspath, _args.root_directory)
|
||||
|
||||
|
||||
def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, str]) -> None:
|
||||
def render_manifest_json(_args: Args, icon_list: list[Icon], colors: dict[str, str]) -> None:
|
||||
"""
|
||||
Render the manifest.json file for the web application.
|
||||
|
||||
@@ -121,10 +141,10 @@ def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, s
|
||||
-----------
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
icon_list : List[Icon]
|
||||
List of icons to be included in the manifest.
|
||||
colors : Dict[str, str]
|
||||
Dictionary containing color scheme and theme color.
|
||||
icon_list : list[Icon]
|
||||
list of icons to be included in the manifest.
|
||||
colors : dict[str, str]
|
||||
dictionary containing color scheme and theme color.
|
||||
"""
|
||||
manifest = env.get_template("manifest.json.j2")
|
||||
content = manifest.render(
|
||||
@@ -135,17 +155,18 @@ def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, s
|
||||
theme_color=colors["theme_color"],
|
||||
)
|
||||
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f:
|
||||
logger.info("rendering manifest.json", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.json")})
|
||||
f.write(content)
|
||||
|
||||
|
||||
def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List[Icon]:
|
||||
def create_icons_from_svg(files: list[str], iconspath: str, _args: Args) -> list[Icon]:
|
||||
"""
|
||||
Create icons from an SVG file.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
files : List[str]
|
||||
List of files in the icons directory.
|
||||
files : list[str]
|
||||
list of files in the icons directory.
|
||||
iconspath : str
|
||||
Path to the directory where the icons will be saved.
|
||||
_args : Args
|
||||
@@ -153,10 +174,11 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
|
||||
|
||||
Returns:
|
||||
--------
|
||||
List[Icon]
|
||||
List of icons created from the SVG file.
|
||||
list[Icon]
|
||||
list of icons created from the SVG file.
|
||||
"""
|
||||
svg = [file for file in files if file.endswith(".svg")][0]
|
||||
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
|
||||
icon_list = [
|
||||
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"},
|
||||
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"},
|
||||
@@ -165,6 +187,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
|
||||
tmpimg = BytesIO()
|
||||
sizes = size.split("x")
|
||||
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
|
||||
logger.info("converting svg to png", extra={"svg": svg, "size": size})
|
||||
cairosvg.svg2png(
|
||||
url=os.path.join(iconspath, svg),
|
||||
write_to=tmpimg,
|
||||
@@ -173,6 +196,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
|
||||
scale=1,
|
||||
)
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
logger.info("saving png file", extra={"iconpath": iconpath})
|
||||
iconfile.save(iconpath, format="PNG")
|
||||
icon_list.append(
|
||||
{
|
||||
@@ -193,7 +217,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
|
||||
return icon_list
|
||||
|
||||
|
||||
def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
|
||||
def create_icons_from_png(iconspath: str, web_root_url: str) -> list[Icon]:
|
||||
"""
|
||||
Create icons from PNG files.
|
||||
|
||||
@@ -206,8 +230,8 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
|
||||
|
||||
Returns:
|
||||
--------
|
||||
List[Icon]
|
||||
List of icons created from PNG files.
|
||||
list[Icon]
|
||||
list of icons created from PNG files.
|
||||
"""
|
||||
icon_list = []
|
||||
for icon in os.listdir(iconspath):
|
||||
@@ -215,6 +239,7 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
|
||||
continue
|
||||
with Image.open(os.path.join(iconspath, icon)) as iconfile:
|
||||
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
|
||||
logger.info("using icon", extra={"iconspath": iconspath, "icon": icon, "size": iconsize})
|
||||
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"})
|
||||
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
|
||||
return icon_list
|
||||
@@ -229,16 +254,15 @@ def webmanifest(_args: Args) -> None:
|
||||
_args : Args
|
||||
Parsed command-line arguments.
|
||||
"""
|
||||
logger.info("generating webmanifest")
|
||||
|
||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||
files = os.listdir(iconspath)
|
||||
icon_list = (
|
||||
create_icons_from_svg(files, iconspath, _args)
|
||||
if SVGSUPPORT and any(file.endswith(".svg") for file in files)
|
||||
else create_icons_from_png(iconspath, _args.web_root_url)
|
||||
)
|
||||
icon_list = create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url)
|
||||
|
||||
if not icon_list:
|
||||
print("No icons found in the static/icons folder!")
|
||||
logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath})
|
||||
return
|
||||
|
||||
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
CairoSVG==2.7.1
|
||||
Jinja2==3.1.4
|
||||
numpy==2.0.0
|
||||
pillow==10.4.0
|
||||
pyinstaller==6.9.0
|
||||
rich-argparse==1.5.2
|
||||
setuptools==70.3.0
|
||||
defusedxml==0.7.1
|
||||
Jinja2==3.1.5
|
||||
Pillow==11.1.0
|
||||
pyinstaller==6.11.1
|
||||
python_json_logger==2.0.7
|
||||
rich_argparse==1.7.0
|
||||
selenium==4.28.1
|
||||
tqdm==4.66.4
|
||||
@@ -52,6 +52,10 @@
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
{%- macro render_tags(tag_tree, parent) -%}
|
||||
<ol>
|
||||
{%- for key, value in tag_tree.items() %}
|
||||
<li class="tagentry">
|
||||
<label onclick="filter()" title="{{ key }}" id="{{ parent }}|{{ key }}">
|
||||
<input class="tag" type="checkbox" />{{ key }}
|
||||
</label>
|
||||
{%- if value %}
|
||||
{{ render_tags(value, parent + '|' + key) }}
|
||||
{%- endif %}
|
||||
</li>
|
||||
{%- endfor %}
|
||||
</ol>
|
||||
{%- endmacro -%}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
@@ -8,44 +22,75 @@
|
||||
{%- if webmanifest %}
|
||||
<link rel="manifest" href="/.static/manifest.json">
|
||||
{%- endif %}
|
||||
<link rel="preload" href="{{ stylesheet }}" as="style">
|
||||
{%- if theme %}
|
||||
<link rel="preload" href="{{ theme }}" as="style">
|
||||
{%- endif %}
|
||||
<link rel="icon" type="image/x-icon" href="{{ favicon }}">
|
||||
<link rel="stylesheet" href="{{ stylesheet }}">
|
||||
{%- if theme %}
|
||||
<link rel="stylesheet" href="{{ theme }}">
|
||||
{%- endif %}
|
||||
{%- if images %}
|
||||
<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">
|
||||
<link rel="modulepreload" href="{{ root }}.static/pswp/photoswipe.min.js">
|
||||
<link rel="modulepreload" href="{{ root }}.static/pswp/photoswipe-ui-default.min.js">
|
||||
<link rel="stylesheet" href="{{ root }}.static/pswp/photoswipe.css">
|
||||
<link rel="stylesheet" href="{{ root }}.static/pswp/default-skin/default-skin.css">
|
||||
<script src="{{ root }}.static/pswp/photoswipe.min.js"></script>
|
||||
<script src="{{ root }}.static/pswp/photoswipe-ui-default.min.js"></script>
|
||||
{%- endif %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<ul class="navbar">
|
||||
<ol class="navbar">
|
||||
<div class="navleft">
|
||||
<li><a href="{{ root }}">Home</a></li>
|
||||
{%- if parent %}
|
||||
<li><a href="{{ parent }}">Parent Directory</a></li>
|
||||
{%- endif %}
|
||||
{%- if info %}
|
||||
<li class="tooltip"><a>Info</a><span class="tooltiptext">
|
||||
<li class="tooltip"><a>Info</a><span class="tooltiptext infotext">
|
||||
{%- for infoline in info -%}
|
||||
{{ infoline }}<br />
|
||||
{%- endfor -%}
|
||||
</span></li>
|
||||
{%- endif %}
|
||||
</div>
|
||||
<div class="navcenter">
|
||||
<li class="title"><span class="header">{{ header }}</span></li>
|
||||
{%- if license %}
|
||||
<li class="license"><a href="{{ license.url }}" rel="license noopener noreferrer" target="_blank">License</a></li>
|
||||
</div>
|
||||
<div class="navright">
|
||||
{% if tags %}
|
||||
<li class="tooltip">
|
||||
<a>Filter by Tags</a>
|
||||
<ol class="tooltiptext tagdropdown" id="tagdropdown">
|
||||
<span class="tagentry">
|
||||
<label onclick="recursive()">
|
||||
<input type="checkbox" id="recursive" />recursive filter
|
||||
</label>
|
||||
</span>
|
||||
{{ render_tags(tags, '') }}
|
||||
</ol>
|
||||
</li>
|
||||
{% endif %}
|
||||
{%- if licensefile %}
|
||||
<li class="license"><a href="{{ licensefile }}">License</a></li>
|
||||
{%- endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</ol>
|
||||
{% if subdirectories %}
|
||||
{%- for subdirectory in subdirectories %}
|
||||
<link rel="preload" href="{{ subdirectory.url }}/index.html" type="text/html">
|
||||
{%- endfor %}
|
||||
<div class="folders">
|
||||
{%- for subdirectory in subdirectories %}
|
||||
<a href="{{ subdirectory.url }}">
|
||||
<figure>
|
||||
<img class="foldericon" />
|
||||
{%- if subdirectory.thumb %}
|
||||
<img class="folderthumb" src="{{ subdirectory.thumb }}" />
|
||||
{%- endif %}
|
||||
<figcaption>{{ subdirectory.name }}</figcaption>
|
||||
</figure>
|
||||
</a>
|
||||
@@ -53,29 +98,8 @@
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% if images %}
|
||||
{%- set ns = namespace(count = 0) -%}
|
||||
<div class="row">
|
||||
{%- for imageblock in images %}
|
||||
<div class="column">
|
||||
{%- for image in imageblock %}
|
||||
<figure>
|
||||
<img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" />
|
||||
{%- set ns.count = ns.count + 1 %}
|
||||
<figcaption class="caption">{{ image.name }}
|
||||
{%- if image.tiff %}
|
||||
<a href="{{ image.tiff }}">TIFF</a>
|
||||
{%- endif %}
|
||||
{%- if image.raw %}
|
||||
<a href="{{ image.raw }}">RAW</a>
|
||||
{%- endif %}
|
||||
</figcaption>
|
||||
</figure>
|
||||
{%- endfor %}
|
||||
<div class="row" id="imagelist">
|
||||
</div>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
{% if license %}
|
||||
{%- if 'CC' in license.type %}
|
||||
<div class="footer" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
|
||||
@@ -95,18 +119,17 @@
|
||||
</a>
|
||||
{%- endif %}
|
||||
<span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a
|
||||
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">Flo Greistorfer</a>.</span>
|
||||
<button onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button>
|
||||
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span>
|
||||
<button type="button" onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- else %}
|
||||
<div class="footer">
|
||||
<span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a
|
||||
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">Flo Greistorfer</a>.</span>
|
||||
<button onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button>
|
||||
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span>
|
||||
<button type="button" onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{% if images %}
|
||||
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="pswp__bg"></div>
|
||||
<div class="pswp__scroll-wrap">
|
||||
@@ -144,28 +167,86 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var pswpElement = document.querySelectorAll('.pswp')[0];
|
||||
var items = [
|
||||
{%- for image in allimages %}
|
||||
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" },
|
||||
{%- endfor %}
|
||||
];
|
||||
var re = /pid=(\d+)/;
|
||||
const pswpElement = document.querySelectorAll('.pswp')[0];
|
||||
const re = /pid=(\d+)/;
|
||||
const filterre = /#(.*)/;
|
||||
let items = [];
|
||||
let shown = [];
|
||||
let subfolders = [];
|
||||
let controllers = {};
|
||||
let currentFolder = "";
|
||||
|
||||
function openSwipe(img) {
|
||||
var options = {
|
||||
index: img
|
||||
};
|
||||
var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);
|
||||
gallery.init();
|
||||
function requestMetadata() {
|
||||
fetch(".metadata.json").then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
items = Object.values(data.images);
|
||||
subfolders = data.subfolders;
|
||||
if (filterre.test(window.location.href)) {
|
||||
var selected = window.location.href.match(filterre)[1].split(",");
|
||||
setFilter(selected);
|
||||
}
|
||||
filter();
|
||||
|
||||
if (re.test(window.location.href)) {
|
||||
var pid = window.location.href.match(re)[1];
|
||||
openSwipe(parseInt(pid));
|
||||
}
|
||||
|
||||
let totopbutton = document.getElementById("totop");
|
||||
if (items == []) {
|
||||
document.getElementById("imagelist").style.display = "none"
|
||||
} else {
|
||||
document.getElementById("imagelist").style.display = ""
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Failed to fetch data:', error));
|
||||
}
|
||||
|
||||
function openSwipe(img) {
|
||||
const options = {
|
||||
index: img
|
||||
};
|
||||
const gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, shown, options);
|
||||
gallery.init();
|
||||
}
|
||||
|
||||
async function recursive(sub = undefined) {
|
||||
const ischecked = document.getElementById("recursive").checked;
|
||||
const folders = document.getElementsByClassName("folders")[0];
|
||||
if (sub == undefined) {
|
||||
sub = subfolders;
|
||||
}
|
||||
if (ischecked) {
|
||||
if (folders != undefined) {
|
||||
folders.style.display = "none";
|
||||
}
|
||||
for (const folder of sub) {
|
||||
currentFolder = folder.name;
|
||||
try {
|
||||
const response = await fetch(folder.metadata);
|
||||
const data = await response.json();
|
||||
if (data.subfolders.length > 0) {
|
||||
await recursive(data.subfolders);
|
||||
}
|
||||
items = items.concat(Object.values(data.images));
|
||||
filter();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch folder metadata:', error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (folders != undefined) {
|
||||
folders.style.display = "";
|
||||
}
|
||||
requestMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
const totopbutton = document.getElementById("totop");
|
||||
|
||||
window.onscroll = function () { scrollFunction() };
|
||||
|
||||
@@ -178,11 +259,96 @@
|
||||
}
|
||||
|
||||
function topFunction() {
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function updateImageList() {
|
||||
let str = ""
|
||||
let imagelist = document.getElementById("imagelist");
|
||||
shown.forEach((item, index) => {
|
||||
str += '<div class="column"><figure><img src="' + item.msrc + '" onclick="openSwipe(' + index + ')" onmouseover="prefetch(' + index + ')" onmouseleave="cancel(' + index + ')" /><figcaption class="caption">' + item.name;
|
||||
if (item.tiff != "" & item.tiff != undefined) {
|
||||
str += ' <a href="' + item.tiff + '">TIFF</a>';
|
||||
}
|
||||
if (item.raw != "" & item.raw != undefined) {
|
||||
str += ' <a href="' + item.raw + '">RAW</a>';
|
||||
}
|
||||
str += '</figcaption></figure></div>';
|
||||
});
|
||||
|
||||
imagelist.innerHTML = str;
|
||||
}
|
||||
|
||||
function prefetch(img) {
|
||||
const controller = new AbortController()
|
||||
const signal = controller.signal
|
||||
controllers[img] = controller;
|
||||
let urlToFetch = items[img].src;
|
||||
|
||||
fetch(urlToFetch, {
|
||||
method: 'get',
|
||||
signal: signal,
|
||||
}).catch(function (err) { });
|
||||
}
|
||||
|
||||
function cancel(img) {
|
||||
controllers[img].abort();
|
||||
delete controllers[img];
|
||||
}
|
||||
|
||||
function filter() {
|
||||
shown = [];
|
||||
|
||||
window.location.href = window.location.href.split("#")[0] + "#";
|
||||
|
||||
const selected_tags = [];
|
||||
const tagcheckboxes = document.querySelectorAll("#tagdropdown input[class='tag']:checked");
|
||||
|
||||
tagcheckboxes.forEach((checkbox) => {
|
||||
const tag = checkbox.parentElement.id.trim().substring(1);
|
||||
selected_tags.push(tag);
|
||||
});
|
||||
|
||||
const urltags = selected_tags.join(",");
|
||||
|
||||
const isRecursiveChecked = document.getElementById("recursive").checked;
|
||||
|
||||
for (const item of items) {
|
||||
const tags = item.tags || [];
|
||||
const include = selected_tags.every(tag => tags.some(t => t.startsWith(tag)));
|
||||
|
||||
if (include || selected_tags.length === 0) {
|
||||
if (isRecursiveChecked || item.folder === currentFolder) {
|
||||
shown.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateImageList();
|
||||
window.location.href += urltags;
|
||||
}
|
||||
|
||||
|
||||
function setFilter(selected) {
|
||||
tagdropdown = document.getElementById("tagdropdown").getElementsByTagName("li");
|
||||
selected.forEach((tag) => {
|
||||
for (var i = 0; i < tagdropdown.length; i++) {
|
||||
if (tagdropdown[i].innerText == tag) {
|
||||
tagdropdown[i].firstChild.firstChild.checked = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
requestMetadata();
|
||||
}
|
||||
|
||||
window.addEventListener ?
|
||||
window.addEventListener("load", onLoad, false) :
|
||||
window.attachEvent && window.attachEvent("onload", onLoad);
|
||||
|
||||
</script>
|
||||
{%- endif %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
67
templates/license.html.j2
Normal file
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
{%- if webmanifest %}
|
||||
<link rel="manifest" href="/.static/manifest.json">
|
||||
{%- endif %}
|
||||
<link rel="preload" href="{{ stylesheet }}" as="style">
|
||||
{%- if theme %}
|
||||
<link rel="preload" href="{{ theme }}" as="style">
|
||||
{%- endif %}
|
||||
<link rel="icon" type="image/x-icon" href="{{ favicon }}">
|
||||
<link rel="stylesheet" href="{{ stylesheet }}">
|
||||
{%- if theme %}
|
||||
<link rel="stylesheet" href="{{ theme }}">
|
||||
{%- endif %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<ul class="navbar">
|
||||
<li><a href="{{ root }}">Home</a></li>
|
||||
{%- if parent %}
|
||||
<li><a href="{{ parent }}">Parent Directory</a></li>
|
||||
{%- endif %}
|
||||
<li class="title"><span class="header">{{ header }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
{%- if licensefile %}
|
||||
<div class="licensefile">
|
||||
{{ licensefile }}
|
||||
</div>
|
||||
{%- endif %}
|
||||
{% if license %}
|
||||
{%- if 'CC' in license.type %}
|
||||
<div class="footer" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
|
||||
{%- if license.type == 'CC0 1.0' %}
|
||||
<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="" />
|
||||
{%- endfor %}
|
||||
</a>
|
||||
{%- else %}
|
||||
<a property="dct:title" rel="cc:attributionURL" href="{{ root }}">{{ license.project }}</a> by <span property="cc:attributionName">{{ license.author }}</span> is licensed under
|
||||
<a href="{{ license.url }}" target="_blank" rel="license noopener noreferrer">{{ license.type }}
|
||||
{%- for pic in license.pics %}
|
||||
<img src="{{ pic }}" alt="" />
|
||||
{%- endfor %}
|
||||
</a>
|
||||
{%- endif %}
|
||||
<span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a
|
||||
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span>
|
||||
<button type="button" onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- else %}
|
||||
<div class="footer">
|
||||
<span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a
|
||||
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span>
|
||||
<button type="button" onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</body>
|
||||
BIN
test/example/DSC00009.jpg
Normal file
|
After Width: | Height: | Size: 28 MiB |
7
test/example/DSC00009.jpg.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="DSC00009.jpg">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>aqueduct</rdf:li><rdf:li>arch</rdf:li><rdf:li>arch bridge</rdf:li><rdf:li>bridge</rdf:li><rdf:li>hillside</rdf:li><rdf:li>passenger train</rdf:li><rdf:li>railroad</rdf:li><rdf:li>railroad bridge</rdf:li><rdf:li>span</rdf:li><rdf:li>train track</rdf:li><rdf:li>tree</rdf:li><rdf:li>viaduct</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|aqueduct</rdf:li><rdf:li>st|arch</rdf:li><rdf:li>st|arch bridge</rdf:li><rdf:li>st|bridge</rdf:li><rdf:li>st|hillside</rdf:li><rdf:li>st|passenger train</rdf:li><rdf:li>st|railroad</rdf:li><rdf:li>st|railroad bridge</rdf:li><rdf:li>st|span</rdf:li><rdf:li>st|train track</rdf:li><rdf:li>st|tree</rdf:li><rdf:li>st|viaduct</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
BIN
test/example/DSC03470.JPG
Executable file
|
After Width: | Height: | Size: 20 MiB |
7
test/example/DSC03470.JPG.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="DSC03470.JPG">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>bus</rdf:li><rdf:li>illuminate</rdf:li><rdf:li>neon</rdf:li><rdf:li>neon light</rdf:li><rdf:li>night</rdf:li><rdf:li>sign</rdf:li><rdf:li>train car</rdf:li><rdf:li>trolley</rdf:li><rdf:li>window</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|bus</rdf:li><rdf:li>st|illuminate</rdf:li><rdf:li>st|neon</rdf:li><rdf:li>st|neon light</rdf:li><rdf:li>st|night</rdf:li><rdf:li>st|sign</rdf:li><rdf:li>st|train car</rdf:li><rdf:li>st|trolley</rdf:li><rdf:li>st|window</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
BIN
test/example/DSC03508.ARW
Executable file
7
test/example/DSC03508.ARW.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="DSC03508.ARW">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>building</rdf:li><rdf:li>ceiling</rdf:li><rdf:li>pillar</rdf:li><rdf:li>display</rdf:li><rdf:li>rail</rdf:li><rdf:li>steam engine</rdf:li><rdf:li>steam locomotive</rdf:li><rdf:li>train</rdf:li><rdf:li>train car</rdf:li><rdf:li>train track</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|building</rdf:li><rdf:li>st|ceiling</rdf:li><rdf:li>st|pillar</rdf:li><rdf:li>st|display</rdf:li><rdf:li>st|rail</rdf:li><rdf:li>st|steam engine</rdf:li><rdf:li>st|steam locomotive</rdf:li><rdf:li>st|train</rdf:li><rdf:li>st|train car</rdf:li><rdf:li>st|train track</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
BIN
test/example/DSC03508.JPG
Executable file
|
After Width: | Height: | Size: 20 MiB |
7
test/example/DSC03508.JPG.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="DSC03508.JPG">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>attach</rdf:li><rdf:li>basement</rdf:li><rdf:li>beam</rdf:li><rdf:li>building</rdf:li><rdf:li>ceiling</rdf:li><rdf:li>equipment</rdf:li><rdf:li>floor</rdf:li><rdf:li>pipe</rdf:li><rdf:li>red</rdf:li><rdf:li>room</rdf:li><rdf:li>scaffold</rdf:li><rdf:li>tube</rdf:li><rdf:li>warehouse</rdf:li><rdf:li>water pipe</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|attach</rdf:li><rdf:li>st|basement</rdf:li><rdf:li>st|beam</rdf:li><rdf:li>st|building</rdf:li><rdf:li>st|ceiling</rdf:li><rdf:li>st|equipment</rdf:li><rdf:li>st|floor</rdf:li><rdf:li>st|pipe</rdf:li><rdf:li>st|red</rdf:li><rdf:li>st|room</rdf:li><rdf:li>st|scaffold</rdf:li><rdf:li>st|tube</rdf:li><rdf:li>st|warehouse</rdf:li><rdf:li>st|water pipe</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
427
test/example/LICENSE
Normal file
@@ -0,0 +1,427 @@
|
||||
Attribution-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
7
test/example/example.jpg.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="example.jpg">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>cloud</rdf:li><rdf:li>cloudy</rdf:li><rdf:li>evening sky</rdf:li><rdf:li>sea</rdf:li><rdf:li>sky</rdf:li><rdf:li>storm cloud</rdf:li><rdf:li>stormy</rdf:li><rdf:li>sun</rdf:li><rdf:li>sunset</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|cloud</rdf:li><rdf:li>st|cloudy</rdf:li><rdf:li>st|evening sky</rdf:li><rdf:li>st|sea</rdf:li><rdf:li>st|sky</rdf:li><rdf:li>st|storm cloud</rdf:li><rdf:li>st|stormy</rdf:li><rdf:li>st|sun</rdf:li><rdf:li>st|sunset</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
7
test/example/example.tif.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="example.tif">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>cloud</rdf:li><rdf:li>cloudy</rdf:li><rdf:li>evening sky</rdf:li><rdf:li>sea</rdf:li><rdf:li>sky</rdf:li><rdf:li>storm cloud</rdf:li><rdf:li>stormy</rdf:li><rdf:li>sun</rdf:li><rdf:li>sunset</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|cloud</rdf:li><rdf:li>st|cloudy</rdf:li><rdf:li>st|evening sky</rdf:li><rdf:li>st|sea</rdf:li><rdf:li>st|sky</rdf:li><rdf:li>st|storm cloud</rdf:li><rdf:li>st|stormy</rdf:li><rdf:li>st|sun</rdf:li><rdf:li>st|sunset</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
BIN
test/example/subfolder/DSC01106.jpg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
7
test/example/subfolder/DSC01106.jpg.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="DSC01106.jpg">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>clear</rdf:li><rdf:li>dark</rdf:li><rdf:li>moon</rdf:li><rdf:li>night</rdf:li><rdf:li>night sky</rdf:li><rdf:li>sky</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|clear</rdf:li><rdf:li>st|dark</rdf:li><rdf:li>st|moon</rdf:li><rdf:li>st|night</rdf:li><rdf:li>st|night sky</rdf:li><rdf:li>st|sky</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
BIN
test/example/subfolder/subsubfolder/000041900001.jpg
Normal file
|
After Width: | Height: | Size: 7.3 MiB |
27
test/example/subfolder/subsubfolder/000041900001.jpg.xmp
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/"
|
||||
xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
|
||||
xmpMM:DerivedFrom="000041900001.jpg">
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>st</rdf:li>
|
||||
<rdf:li>cloud</rdf:li>
|
||||
<rdf:li>cloudy</rdf:li>
|
||||
<rdf:li>fly</rdf:li>
|
||||
<rdf:li>sky</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
<lr:hierarchicalSubject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>st|cloud</rdf:li>
|
||||
<rdf:li>st|cloudy</rdf:li>
|
||||
<rdf:li>st|fly</rdf:li>
|
||||
<rdf:li>st|sky</rdf:li>
|
||||
</rdf:Bag>
|
||||
</lr:hierarchicalSubject>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
BIN
test/example/subfolder/subsubfolder/000041900001.tif
Normal file
7
test/example/subfolder/subsubfolder/000041900001.tif.xmp
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<x:xmpmeta x:xmptk="XMP Core 4.4.0-Exiv2" xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:lr="http://ns.adobe.com/lightroom/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmpMM:DerivedFrom="000041900001.tif">
|
||||
<dc:subject><rdf:Bag><rdf:li>st</rdf:li><rdf:li>cloud</rdf:li><rdf:li>cloudy</rdf:li><rdf:li>fly</rdf:li><rdf:li>sky</rdf:li></rdf:Bag></dc:subject><lr:hierarchicalSubject><rdf:Bag><rdf:li>st|cloud</rdf:li><rdf:li>st|cloudy</rdf:li><rdf:li>st|fly</rdf:li><rdf:li>st|sky</rdf:li></rdf:Bag></lr:hierarchicalSubject></rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
@@ -97,6 +97,10 @@ body {
|
||||
background-color: var(--color6);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ body {
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color7);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor1);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@
|
||||
background-color: var(--bcolor1);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor3);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor1);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@
|
||||
background-color: var(--bcolor1);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor3);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor1);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
font-family: "Playfair Display", serif;
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
--color2: #b22222;
|
||||
--color3: #ff4500;
|
||||
--color4: #6e0000;
|
||||
--bcolor1: #ebebeb;
|
||||
--bcolor1: #171717;
|
||||
--bcolor2: #191919;
|
||||
--bcolor3: #171717;
|
||||
--bcolor3: #ebebeb;
|
||||
--bcolor4: #0a0a0a;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
font-weight: bold;
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
background-color: var(--color1);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.navbar li a {
|
||||
font-weight: 800;
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
}
|
||||
|
||||
/* Change the link color on hover */
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
background-color: var(--color3);
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -73,6 +73,10 @@
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
font-family: "Nunito", sans-serif;
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
--color2: #1346a4;
|
||||
--color3: #0e3377;
|
||||
--color4: #3674e7;
|
||||
--bcolor1: #ebebeb;
|
||||
--bcolor1: #171717;
|
||||
--bcolor2: #191919;
|
||||
--bcolor3: #171717;
|
||||
--bcolor3: #ebebeb;
|
||||
--bcolor4: #0a0a0a;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
font-weight: bold;
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
background-color: var(--color1);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.navbar li a {
|
||||
font-weight: 800;
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
}
|
||||
|
||||
/* Change the link color on hover */
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
background-color: var(--color3);
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -73,6 +73,10 @@
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg width='64' height='64' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M20 6H10L8 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6Z' fill='{{ color1 }}' />
|
||||
<path d='M4 4H8L10 6H20C21.1046 6 22 6.89543 22 8H2C2 6.89543 2.89543 6 4 6V4Z' fill='{{ color4 }}' />
|
||||
<path d='M10 6H14L12 4H8L10 6Z' fill='{{ color2 }}' />
|
||||
<path d='M14 6H18L16 4H12L14 6Z' fill='{{ color3 }}' />
|
||||
<path d='M10 6H14L12 4H8L10 6Z' fill='{{ color3 }}' />
|
||||
<path d='M14 6H18L16 4H12L14 6Z' fill='{{ color2 }}' />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 493 B |
@@ -5,22 +5,22 @@
|
||||
--color2: #008000;
|
||||
--color3: #32cd32;
|
||||
--color4: #004300;
|
||||
--bcolor1: #ebebeb;
|
||||
--bcolor1: #171717;
|
||||
--bcolor2: #191919;
|
||||
--bcolor3: #171717;
|
||||
--bcolor3: #ebebeb;
|
||||
--bcolor4: #0a0a0a;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
font-weight: bold;
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
background-color: var(--color1);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.navbar li a {
|
||||
font-weight: 800;
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
}
|
||||
|
||||
/* Change the link color on hover */
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
background-color: var(--color3);
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -73,6 +73,10 @@
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,10 @@
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor3);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor3);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor5);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor5);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -79,6 +79,10 @@
|
||||
font-family: "Lora", serif;
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor3);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
background-color: var(--color4);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--color4);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,11 @@
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor3);
|
||||
color: var(--bcolor2);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
--color2: #ffd700;
|
||||
--color3: #ffe135;
|
||||
--color4: #ce8c00;
|
||||
--bcolor1: #ebebeb;
|
||||
--bcolor1: #171717;
|
||||
--bcolor2: #191919;
|
||||
--bcolor3: #171717;
|
||||
--bcolor3: #ebebeb;
|
||||
--bcolor4: #0a0a0a;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
.row a {
|
||||
font-weight: 800;
|
||||
color: var(--color2);
|
||||
color: var(--color1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -70,10 +70,14 @@
|
||||
|
||||
.tooltiptext {
|
||||
font-weight: 600;
|
||||
color: var(--bcolor1);
|
||||
color: var(--bcolor3);
|
||||
background-color: var(--bcolor2);
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,10 @@
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
.tagentry > label:hover {
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
.column img {
|
||||
background-color: var(--bcolor4);
|
||||
}
|
||||
|
||||
4
view-latest-log.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
LESS=-SR hl logs/latest.jsonl --config hl_config.yaml
|
||||
|
||||
3
view-logs.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
LESS=-SR hl $(ls -tr logs/*.{jsonl,jsonl.gz}) --config hl_config.yaml
|
||||