hierarchical tagging selection

This commit is contained in:
2025-06-26 13:36:02 +02:00
parent 0cda1706fa
commit 79e34d7e43
23 changed files with 143 additions and 72 deletions

View File

@@ -176,6 +176,7 @@ figure {
.tooltip .tagdropdown { .tooltip .tagdropdown {
padding: 0; padding: 0;
margin: 0;
} }
.tooltip:hover .tooltiptext { .tooltip:hover .tooltiptext {
@@ -196,6 +197,11 @@ figure {
padding: 0; padding: 0;
} }
.tooltip .tooltiptext ol {
margin-left: 0;
padding-left: 0.5em;
}
.tooltip .tooltiptext .tagentry label { .tooltip .tooltiptext .tagentry label {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;

View File

@@ -5,6 +5,7 @@ import fnmatch
import json import json
from typing import Any from typing import Any
from datetime import datetime from datetime import datetime
from collections import defaultdict
from tqdm.auto import tqdm from tqdm.auto import tqdm
from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError
@@ -189,27 +190,66 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
tags = xmpdata["xmpmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"] tags = xmpdata["xmpmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
if isinstance(tags, str): if isinstance(tags, str):
tags = [tags] tags = [tags]
xmp = xmpdata
except TypeError: except TypeError:
... pass
except KeyError: except KeyError:
... pass
try: try:
tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"] tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
if isinstance(tags, str): if isinstance(tags, str):
tags = [tags] tags = [tags]
xmp = xmpdata
except TypeError: except TypeError:
... pass
except KeyError: 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: if None in tags:
tags.remove(None) tags.remove(None)
if "st" in tags:
tags.remove("st")
return {"width": width, "height": height, "tags": tags, "exifdata": exifdata, "xmp": xmp} return {"width": width, "height": height, "tags": tags, "exifdata": exifdata, "xmp": xmp}
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]: def get_tags(sidecarfile: str) -> list[str]:
""" """
Extracts Tags from XMP sidecar file Extracts Tags from XMP sidecar file
@@ -230,21 +270,35 @@ def get_tags(sidecarfile: str) -> list[str]:
if isinstance(tags, str): if isinstance(tags, str):
tags = [tags] tags = [tags]
except TypeError: except TypeError:
... pass
except KeyError: except KeyError:
... pass
try: try:
tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"] tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
if isinstance(tags, str): if isinstance(tags, str):
tags = [tags] tags = [tags]
except TypeError: except TypeError:
... pass
except KeyError: 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: if None in tags:
tags.remove(None) tags.remove(None)
if "st" in tags:
tags.remove("st")
return tags return tags
@@ -489,9 +543,9 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict
alltags = set() alltags = set()
for img in images: for img in images:
for tag in img["tags"]: alltags.update(img["tags"])
alltags.add(tag)
alltags = sorted(alltags) nested_tags = parse_hierarchical_tags(alltags)
folder_info = info.get(urllib.parse.quote(folder), "").split("\n") 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 _info = [i for i in folder_info if len(i) > 1] if folder_info else None
@@ -541,7 +595,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict
version=version, version=version,
logo=logo, logo=logo,
licensefile=license_url, licensefile=license_url,
tags=alltags, tags=nested_tags,
) )
with open(html_file, "w", encoding="utf-8") as f: with open(html_file, "w", encoding="utf-8") as f:

View File

@@ -52,7 +52,7 @@
background-color: var(--color2); background-color: var(--color2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color4); background-color: var(--color4);
} }

View File

@@ -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 type="checkbox" />{{ key }}
</label>
{%- if value %}
{{ render_tags(value, parent + '|' + key) }}
{%- endif %}
</li>
{%- endfor %}
</ol>
{%- endmacro -%}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@@ -49,15 +63,14 @@
<li class="title"><span class="header">{{ header }}</span></li> <li class="title"><span class="header">{{ header }}</span></li>
</div> </div>
<div class="navright"> <div class="navright">
{%- if tags|length > 0 %} {% if tags %}
<li class="tooltip"><a>Filter by Tags</a> <li class="tooltip">
<a>Filter by Tags</a>
<ol class="tooltiptext tagdropdown" id="tagdropdown"> <ol class="tooltiptext tagdropdown" id="tagdropdown">
{%- for tag in tags -%} {{ render_tags(tags, '') }}
<li class="tagentry"><label onclick="filter()"><input type="checkbox" />{{ tag }}</label></li><br />
{%- endfor -%}
</ol> </ol>
</li> </li>
{%- endif %} {% endif %}
{%- if licensefile %} {%- if licensefile %}
<li class="license"><a href="{{ licensefile }}">License</a></li> <li class="license"><a href="{{ licensefile }}">License</a></li>
{%- endif %} {%- endif %}
@@ -165,7 +178,6 @@
{%- endif %} {%- endif %}
{%- endfor %} {%- endfor %}
]; ];
var shown = [];
var re = /pid=(\d+)/; var re = /pid=(\d+)/;
var filterre = /#(.*)/; var filterre = /#(.*)/;
var controllers = {} var controllers = {}
@@ -194,10 +206,10 @@
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
} }
function updateImageList() { function updateImageList(images) {
var str = "" var str = ""
var imagelist = document.getElementById("imagelist"); var imagelist = document.getElementById("imagelist");
shown.forEach((item, index) => { images.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; 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 != "") { if (item.tiff != "") {
str += ' <a href="' + item.tiff + '">TIFF</a>'; str += ' <a href="' + item.tiff + '">TIFF</a>';
@@ -229,31 +241,31 @@
} }
function filter() { function filter() {
window.location.href = window.location.href.split("#")[0] + "#" window.location.href = window.location.href.split("#")[0] + "#";
var selected_tags = [];
var tagdropdown, tags, incl; const selected_tags = [];
shown = []; const shown = [];
tagdropdown = document.getElementById("tagdropdown").getElementsByTagName("li");
for (var i = 0; i < tagdropdown.length; i++) { const tagcheckboxes = document.querySelectorAll("#tagdropdown input[type='checkbox']:checked");
if (tagdropdown[i].firstChild.firstChild.checked) {
selected_tags.push([tagdropdown[i].innerText]) tagcheckboxes.forEach((checkbox) => {
} const tag = checkbox.parentElement.id.trim().substring(1);
} selected_tags.push(tag);
var urltags = selected_tags.join(","); });
items.forEach((item, index) => { console.log(selected_tags);
tags = item.tags;
incl = true; const urltags = selected_tags.join(",");
selected_tags.forEach((tag) => {
if (tags.indexOf(tag) == -1) { items.forEach((item) => {
incl = false; const tags = item.tags || [];
const include = selected_tags.every(tag => tags.includes(tag));
if (include || selected_tags.length === 0) {
shown.push(item);
} }
}); });
if (incl | selected_tags == []) {
shown.push(item) updateImageList(shown);
} window.location.href += urltags;
});
updateImageList();
window.location.href += urltags
} }
function setFilter(selected) { function setFilter(selected) {
@@ -275,8 +287,7 @@
} }
filter(); filter();
{%- else %} {%- else %}
shown = items; updateImageList(items);
updateImageList();
{%- endif %} {%- endif %}
if (re.test(window.location.href)) { if (re.test(window.location.href)) {

View File

@@ -97,7 +97,7 @@ body {
background-color: var(--color6); background-color: var(--color6);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color3); background-color: var(--color3);
} }

View File

@@ -96,7 +96,7 @@ body {
background-color: var(--color3); background-color: var(--color3);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color7); background-color: var(--color7);
} }

View File

@@ -74,7 +74,7 @@
background-color: var(--bcolor1); background-color: var(--bcolor1);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor3); background-color: var(--bcolor3);
} }

View File

@@ -74,7 +74,7 @@
background-color: var(--bcolor1); background-color: var(--bcolor1);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor3); background-color: var(--bcolor3);
} }

View File

@@ -79,7 +79,7 @@
font-family: "Playfair Display", serif; font-family: "Playfair Display", serif;
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color3); background-color: var(--color3);
} }

View File

@@ -73,7 +73,7 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -79,7 +79,7 @@
font-family: "Nunito", sans-serif; font-family: "Nunito", sans-serif;
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color4); background-color: var(--color4);
} }

View File

@@ -73,7 +73,7 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -52,7 +52,7 @@
background-color: var(--color3); background-color: var(--color3);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color4); background-color: var(--color4);
} }

View File

@@ -52,7 +52,7 @@
background-color: var(--color2); background-color: var(--color2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color4); background-color: var(--color4);
} }

View File

@@ -73,7 +73,7 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -72,7 +72,7 @@
background-color: var(--color3); background-color: var(--color3);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color2); background-color: var(--color2);
} }

View File

@@ -55,7 +55,7 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor3); background-color: var(--bcolor3);
} }

View File

@@ -76,7 +76,7 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -79,7 +79,7 @@
font-family: "Lora", serif; font-family: "Lora", serif;
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor3); background-color: var(--bcolor3);
} }

View File

@@ -80,7 +80,7 @@
background-color: var(--color4); background-color: var(--color4);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color3); background-color: var(--color3);
} }

View File

@@ -80,7 +80,7 @@
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor3); background-color: var(--bcolor3);
color: var(--bcolor2); color: var(--bcolor2);
} }

View File

@@ -74,7 +74,7 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -88,7 +88,7 @@
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
} }
.tagentry:hover { .tagentry > label:hover {
background-color: var(--color2); background-color: var(--color2);
} }