diff --git a/.version b/.version
index 5588ae8..fbafd6b 100644
--- a/.version
+++ b/.version
@@ -1 +1 @@
-2.7.1
\ No newline at end of file
+2.7.2
\ No newline at end of file
diff --git a/StaticGalleryBuilder.code-workspace b/StaticGalleryBuilder.code-workspace
index 6c9d414..6dd3605 100644
--- a/StaticGalleryBuilder.code-workspace
+++ b/StaticGalleryBuilder.code-workspace
@@ -1,246 +1,263 @@
{
- "extensions": {
- "recommendations": [
- "charliermarsh.ruff",
- "esbenp.prettier-vscode",
- "ms-edgedevtools.vscode-edge-devtools",
- "ms-python.debugpy",
- "ms-python.python",
- "ms-python.vscode-pylance",
- "samuelcolvin.jinjahtml",
- "vscode.css-language-features",
- "vscode.html-language-features",
+ "extensions": {
+ "recommendations": [
+ "charliermarsh.ruff",
+ "esbenp.prettier-vscode",
+ "ms-edgedevtools.vscode-edge-devtools",
+ "ms-python.debugpy",
+ "ms-python.python",
+ "ms-python.vscode-pylance",
+ "samuelcolvin.jinjahtml",
+ "vscode.css-language-features",
+ "vscode.html-language-features"
+ ]
+ },
+ "folders": [
+ {
+ "name": "StaticGalleryBuilder",
+ "path": "./"
+ }
+ ],
+ "launch": {
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "args": [
+ "-p",
+ "${workspaceFolder}/test",
+ "-w",
+ "file://${workspaceFolder}/test",
+ "-t",
+ "Pictures",
+ "--theme",
+ "themes/alpenglow.css",
+ "--use-fancy-folders",
+ "--web-manifest",
+ "-l",
+ "cc-by-nc-sa",
+ "-n",
+ "-m",
+ "--reverse-sort",
+ // "--regenerate-thumbnails",
+ // "--reread-metadata",
+ "--folderthumbnails"
],
+ "console": "integratedTerminal",
+ "name": "Testfolder",
+ "postDebugTask": "Delete Lockfile",
+ "program": "${workspaceFolder}/builder.py",
+ "request": "launch",
+ "type": "debugpy"
+ },
+ {
+ "args": [
+ "-p",
+ "/home/user/woek/Pictures",
+ "-w",
+ "file:///home/user/woek/Pictures",
+ "-t",
+ "Pictures",
+ "--theme",
+ "themes/default.css",
+ "--use-fancy-folders",
+ "--web-manifest",
+ "-n",
+ "-m",
+ // "--regenerate-thumbnails",
+ // "--reread-metadata",
+ "--folderthumbnails"
+ ],
+ "console": "integratedTerminal",
+ "name": "woek",
+ "postDebugTask": "Delete Lockfile 2",
+ "program": "${workspaceFolder}/builder.py",
+ "request": "launch",
+ "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/public/Example/"
+ ],
+ "console": "integratedTerminal",
+ "name": "Generate Themes previews",
+ "program": "${workspaceFolder}/generate_previews.py",
+ "request": "launch",
+ "type": "debugpy"
+ }
+ ]
+ },
+ "settings": {
+ "[css]": {
+ "editor.defaultFormatter": "vscode.css-language-features"
},
- "folders": [
- {
- "name": "StaticGalleryBuilder",
- "path": "./",
- },
+ "[jinja-css]": {
+ "editor.defaultFormatter": "vscode.css-language-features"
+ },
+ "[jinja-html]": {
+ "editor.defaultFormatter": "vscode.html-language-features"
+ },
+ "[jinja-js]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[python]": {
+ "editor.defaultFormatter": "charliermarsh.ruff"
+ },
+ "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"
+ },
+ "files.associations": {
+ "**/*.css.j2": "jinja-css",
+ "**/*.css": "css",
+ "**/*.html.j2": "jinja-html"
+ },
+ "gitblame.inlineMessageEnabled": true,
+ "gitblame.inlineMessageFormat": "${author.name}, ${time.ago} • ${commit.summary}",
+ "gitblame.statusBarMessageFormat": "${author.name} (${time.ago})",
+ "html.format.indentHandlebars": true,
+ "html.format.templating": true,
+ "html.format.wrapAttributes": "preserve",
+ "html.format.wrapLineLength": 200,
+ "html.hover.documentation": true,
+ "html.suggest.html5": true,
+ "html.validate.scripts": true,
+ "html.validate.styles": true,
+ "json.schemaDownload.enable": true,
+ "json.schemas": [
+ {
+ "fileMatch": ["manifest.json.j2"],
+ "url": "https://json.schemastore.org/web-manifest-combined.json"
+ }
],
- "launch": {
- "version": "0.2.0",
- "configurations": [
- {
- "args": [
- "-p",
- "${workspaceFolder}/test",
- "-w",
- "file://${workspaceFolder}/test",
- "-t",
- "Pictures",
- "--theme",
- "themes/alpenglow.css",
- "--use-fancy-folders",
- "--web-manifest",
- "-l",
- "cc-by-nc-sa",
- "-n",
- "-m",
- "--reverse-sort",
- "--regenerate-thumbnails",
- "--reread-metadata",
- "--folderthumbnails",
- ],
- "console": "integratedTerminal",
- "name": "Testfolder",
- "postDebugTask": "Delete Lockfile",
- "program": "${workspaceFolder}/builder.py",
- "request": "launch",
- "type": "debugpy",
- },
- {
- "args": [
- "-p",
- "/home/user/woek/Pictures",
- "-w",
- "file:///home/user/woek/Pictures",
- "-t",
- "Pictures",
- "--theme",
- "themes/default.css",
- "--use-fancy-folders",
- "--web-manifest",
- "-n",
- "-m",
- // "--regenerate-thumbnails",
- // "--reread-metadata",
- "--folderthumbnails",
- ],
- "console": "integratedTerminal",
- "name": "woek",
- "postDebugTask": "Delete Lockfile 2",
- "program": "${workspaceFolder}/builder.py",
- "request": "launch",
- "type": "debugpy",
- },
- {
- "args": [
- "${workspaceFolder}/themes",
- "https://pictures.sorogon.eu/public/Example/"
- ],
- "console": "integratedTerminal",
- "name": "Generate Themes previews",
- "program": "${workspaceFolder}/generate_previews.py",
- "request": "launch",
- "type": "debugpy",
- }
- ],
+ "prettier.htmlWhitespaceSensitivity": "css",
+ "pylint.args": [
+ "--disable=C0111",
+ "--disable=C0301",
+ "--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"
},
- "settings": {
- "[css]": {
- "editor.defaultFormatter": "vscode.css-language-features",
+ "ruff.lineLength": 180
+ },
+ "tasks": {
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "command": "rm -f ${workspaceFolder}/test/.lock",
+ "isBackground": true,
+ "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",
+ "isBackground": true,
+ "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
},
- "[jinja-css]": {
- "editor.defaultFormatter": "vscode.css-language-features",
+ "group": {
+ "kind": "build",
+ "isDefault": true
},
- "[jinja-html]": {
- "editor.defaultFormatter": "vscode.html-language-features",
+ "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
},
- "[jinja-js]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode",
- },
- "[python]": {
- "editor.defaultFormatter": "charliermarsh.ruff",
- },
- "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",
- },
- "files.associations": {
- "**/*.css.j2": "jinja-css",
- "**/*.css": "css",
- "**/*.html.j2": "jinja-html",
- },
- "gitblame.inlineMessageEnabled": true,
- "gitblame.inlineMessageFormat": "${author.name}, ${time.ago} • ${commit.summary}",
- "gitblame.statusBarMessageFormat": "${author.name} (${time.ago})",
- "html.format.indentHandlebars": true,
- "html.format.templating": true,
- "html.format.wrapAttributes": "preserve",
- "html.format.wrapLineLength": 200,
- "html.hover.documentation": true,
- "html.suggest.html5": true,
- "html.validate.scripts": true,
- "html.validate.styles": true,
- "json.schemaDownload.enable": true,
- "json.schemas": [
- {
- "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]?$",
- ],
- "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",
- "tasks": [
- {
- "command": "rm -f ${workspaceFolder}/test/.lock",
- "isBackground": true,
- "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",
- "isBackground": true,
- "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
- }
- }
- ],
- },
-}
\ No newline at end of file
+ "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
+ }
+ }
+ ]
+ }
+}
diff --git a/modules/generate_html.py b/modules/generate_html.py
index 7d97d7a..9f0e19d 100644
--- a/modules/generate_html.py
+++ b/modules/generate_html.py
@@ -9,6 +9,7 @@ from datetime import datetime
from tqdm.auto import tqdm
from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError
from jinja2 import Environment, FileSystemLoader
+from defusedxml import ElementTree
from modules.logger import logger
from modules import cclicense
@@ -34,6 +35,41 @@ info: dict[str, str] = {}
licens: dict[str, str] = {}
+def getxmp(strbuffer: str) -> dict[str, Any]:
+ """
+ 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.
@@ -162,9 +198,33 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
if isinstance(tags, str):
tags = [tags]
xmp = xmpdata
+ if None in tags:
+ tags.remove(None)
return {"width": width, "height": height, "tags": tags, "exifdata": exifdata, "xmp": xmp}
+def get_tags(sidecarfile: str) -> list[str]:
+ with open(sidecarfile) as sidecar:
+ strbuffer = sidecar.read()
+ xmpdata = getxmp(strbuffer)
+ tags = []
+ if xmpdata.get("xmpmeta", False):
+ if isinstance(xmpdata["xmpmeta"]["RDF"]["Description"], dict):
+ if xmpdata["xmpmeta"]["RDF"]["Description"].get("subject", False):
+ tags = xmpdata["xmpmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
+ if isinstance(tags, str):
+ tags = [tags]
+ if xmpdata.get("xapmeta", False):
+ if isinstance(xmpdata["xapmeta"]["RDF"]["Description"], dict):
+ if xmpdata["xapmeta"]["RDF"]["Description"].get("subject", False):
+ tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
+ if isinstance(tags, str):
+ tags = [tags]
+ 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.
@@ -183,6 +243,9 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: d
extsplit = os.path.splitext(item)
if item not in metadata or _args.reread_metadata:
metadata[item] = get_image_info(item, folder)
+ sidecarfile = os.path.join(folder, item + ".xmp")
+ if os.path.exists(sidecarfile):
+ metadata[item]["tags"] = get_tags(sidecarfile)
image = {
"url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}",
diff --git a/requirements.txt b/requirements.txt
index cfcd697..01e43a1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
CairoSVG==2.7.1
+defusedxml==0.7.1
Jinja2==3.1.5
Pillow==11.1.0
pyinstaller==6.11.1
diff --git a/templates/index.html.j2 b/templates/index.html.j2
index fb43591..b45bfea 100644
--- a/templates/index.html.j2
+++ b/templates/index.html.j2
@@ -257,6 +257,8 @@
}
}
}
+
+ filter()
{%- endif %}
{%- endif %}
diff --git a/test/example/DSC00009.jpg.xmp b/test/example/DSC00009.jpg.xmp
new file mode 100644
index 0000000..3f6ea50
--- /dev/null
+++ b/test/example/DSC00009.jpg.xmp
@@ -0,0 +1,7 @@
+
+
+
+
+aqueductarcharch bridgebridgehillsidepassenger trainrailroadrailroad bridgespantrain tracktreeviaduct|aqueduct|arch|arch bridge|bridge|hillside|passenger train|railroad|railroad bridge|span|train track|tree|viaduct
+
+
\ No newline at end of file
diff --git a/test/example/DSC01106.jpg.xmp b/test/example/DSC01106.jpg.xmp
new file mode 100644
index 0000000..757bb28
--- /dev/null
+++ b/test/example/DSC01106.jpg.xmp
@@ -0,0 +1,7 @@
+
+
+
+
+cleardarkmoonnightnight skysky|clear|dark|moon|night|night sky|sky
+
+
\ No newline at end of file
diff --git a/test/example/DSC03470.JPG.xmp b/test/example/DSC03470.JPG.xmp
new file mode 100644
index 0000000..e184ae5
--- /dev/null
+++ b/test/example/DSC03470.JPG.xmp
@@ -0,0 +1,7 @@
+
+
+
+
+busilluminateneonneon lightnightsigntrain cartrolleywindow|bus|illuminate|neon|neon light|night|sign|train car|trolley|window
+
+
\ No newline at end of file
diff --git a/test/example/DSC03508.ARW.xmp b/test/example/DSC03508.ARW.xmp
new file mode 100644
index 0000000..77c6ca2
--- /dev/null
+++ b/test/example/DSC03508.ARW.xmp
@@ -0,0 +1,7 @@
+
+
+
+
+buildingceilingpillardisplayrailsteam enginesteam locomotivetraintrain cartrain track|building|ceiling|pillar|display|rail|steam engine|steam locomotive|train|train car|train track
+
+
\ No newline at end of file
diff --git a/test/example/DSC03508.JPG.xmp b/test/example/DSC03508.JPG.xmp
new file mode 100644
index 0000000..7ec1303
--- /dev/null
+++ b/test/example/DSC03508.JPG.xmp
@@ -0,0 +1,7 @@
+
+
+
+
+attachbasementbeambuildingceilingequipmentfloorpiperedroomscaffoldtubewarehousewater pipe|attach|basement|beam|building|ceiling|equipment|floor|pipe|red|room|scaffold|tube|warehouse|water pipe
+
+
\ No newline at end of file
diff --git a/test/example/example.jpg.xmp b/test/example/example.jpg.xmp
new file mode 100644
index 0000000..afe5b68
--- /dev/null
+++ b/test/example/example.jpg.xmp
@@ -0,0 +1,7 @@
+
+
+
+
+cloudcloudyevening skyseaskystorm cloudstormysunsunset|cloud|cloudy|evening sky|sea|sky|storm cloud|stormy|sun|sunset
+
+
\ No newline at end of file
diff --git a/test/example/example.tif.xmp b/test/example/example.tif.xmp
new file mode 100644
index 0000000..a605eb8
--- /dev/null
+++ b/test/example/example.tif.xmp
@@ -0,0 +1,7 @@
+
+
+
+
+cloudcloudyevening skyseaskystorm cloudstormysunsunset|cloud|cloudy|evening sky|sea|sky|storm cloud|stormy|sun|sunset
+
+
\ No newline at end of file