From 350f41f4398e3902fe1ed157d15aef278839d677 Mon Sep 17 00:00:00 2001 From: Flo Greistorfer Date: Mon, 23 Jun 2025 23:52:17 +0200 Subject: [PATCH] added support for xmp sidecar file tags --- .version | 2 +- StaticGalleryBuilder.code-workspace | 493 ++++++++++++++-------------- modules/generate_html.py | 63 ++++ requirements.txt | 1 + templates/index.html.j2 | 2 + test/example/DSC00009.jpg.xmp | 7 + test/example/DSC01106.jpg.xmp | 7 + test/example/DSC03470.JPG.xmp | 7 + test/example/DSC03508.ARW.xmp | 7 + test/example/DSC03508.JPG.xmp | 7 + test/example/example.jpg.xmp | 7 + test/example/example.tif.xmp | 7 + 12 files changed, 371 insertions(+), 239 deletions(-) create mode 100644 test/example/DSC00009.jpg.xmp create mode 100644 test/example/DSC01106.jpg.xmp create mode 100644 test/example/DSC03470.JPG.xmp create mode 100644 test/example/DSC03508.ARW.xmp create mode 100644 test/example/DSC03508.JPG.xmp create mode 100644 test/example/example.jpg.xmp create mode 100644 test/example/example.tif.xmp 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