commit 616c746b100620cd96f2456f694e156fced617fe
parent 11aedc15e2dede14df11d4a1a370cdd3fc9bfe53
Author: lash <dev@holbrook.no>
Date: Sun, 6 Sep 2026 14:41:45 -0600
Implement title and description import from exiftool xmp export
Diffstat:
2 files changed, 186 insertions(+), 0 deletions(-)
diff --git a/adesse/util/xmp.py b/adesse/util/xmp.py
@@ -0,0 +1,157 @@
+import os
+import logging
+import hashlib
+
+import lxml.etree
+from rdflib import Graph, Namespace, RDF, URIRef, DC, Literal
+from adesse import Attachment
+
+logg = logging.getLogger('util.xmp')
+
+EXIFTOOL_XMP = Namespace('http://ns.exiftool.org/XMP/XMP-xmp/1.0/')
+EXIFTOOL_XMPPDF = Namespace('http://ns.exiftool.org/XMP/XMP-pdf/1.0/')
+EXIFTOOL_XMPDC = Namespace('http://ns.exiftool.org/XMP/XMP-dc/1.0/')
+EXIFTOOL_SYSTEM = Namespace('http://ns.exiftool.org/File/System/1.0/')
+
+
+
+def _from_xml(item, o, exiftool=False):
+ parsens = DC
+ if exiftool:
+ parsens = EXIFTOOL_XMPDC
+ title = o.value(item, parsens.term('title'))
+ if title == None:
+ title = o.value(item, parsens.term('Title'))
+ logg.debug('have title {} {} {}'.format(item, parsens, title))
+ desc = o.value(item, parsens.term('subject'))
+ if desc == None:
+ desc = o.value(item, parsens.term('Subject'))
+ logg.debug('have desc {} {} {}'.format(item, parsens, desc))
+# o = Attachment(title)
+
+
+def check_file(fp, strict=False):
+ logg.debug('checi file {}'.format(fp))
+ os.stat(fp)
+ if not strict:
+ return fp
+ return fp
+
+
+
+def _parse_xmp(xmp_file, resource_file, search_dir=None):
+ o = lxml.etree.parse(xmp_file)
+ r = o.getroot()
+ r = r.find('rdf:RDF', namespaces={'rdf': str(RDF)})
+
+ f = None
+ for d in search_dir:
+ fp = os.path.join(d, resource_file)
+ try:
+ f = open(fp, 'rb')
+ break
+ except FileNotFoundError:
+ pass
+
+ if f == None:
+ raise FileNotFoundError()
+
+ h = hashlib.sha256()
+ v = f.read()
+ h.update(v)
+ f.close()
+ z = h.digest()
+ uu = 'URN:sha256:' + z.hex()
+
+ for v in r.findall('rdf:Description', namespaces={'rdf': str(RDF)}):
+ v.set('{{{}}}about'.format(str(RDF)), uu)
+
+ s = lxml.etree.tostring(r)
+ return (uu, s,)
+
+
+
+def rdfn(v):
+ s = str(RDF) + '_' + str(int(v))
+ return URIRef(s)
+
+
+def _get_alt_lang(sbj, prd, o):
+ v = o.value(sbj, prd)
+ typ = o.value(v, RDF.type)
+ r = None
+ if typ == RDF.Alt:
+ i = 1
+ while i:
+ v = o.value(v, rdfn(i))
+ if isinstance(v, Literal):
+ if v.language == 'x-default':
+ r = v.value
+ i = 0
+ else:
+ i += 1
+ else:
+ i += 1
+ return r
+
+
+def parse_title(sbj, o):
+ return _get_alt_lang(sbj, DC.title, o)
+
+
+def parse_description(sbj, o):
+ return _get_alt_lang(sbj, DC.description, o)
+
+
+def attachment_from_xmp(xml_file, resource_file=None, search_dir=None):
+ if search_dir == None:
+ search_dir = ['.']
+ r = _parse_xmp(xml_file, resource_file=resource_file, search_dir=search_dir)
+ o = Graph()
+ sbj = URIRef(r[0])
+ o.parse(r[1], format='xml')
+ title = parse_title(sbj, o)
+ desc = parse_description(sbj, o)
+ logg.debug('have title {} desc {}'.format(title, desc))
+
+
+#def attachment_from_xmp(filepath, search_dir=None):
+# if search_dir == None:
+# search_dir = ['.']
+# state = {
+# 'exif': False,
+# 'xmp': False,
+# }
+# items = []
+# o = Graph()
+# r = o.parse(filepath, format='xml')
+# #o.bind('XMP-dc', DC, replace=True)
+# for v in o.namespace_manager.namespaces():
+# logg.debug('have namespace {}'.format(v))
+# k = v[0]
+# if 'ExifTool' in k:
+# state['exif'] = True
+# if 'xmp' in k:
+# state['xmp'] = True
+# if not state['xmp']:
+# raise AttributeError('not an xmp doument')
+# for v in o.subjects():
+# fp = None
+# for d in search_dir:
+# fp = os.path.join(d, v)
+# try:
+# fp = check_file(fp)
+# break
+# except FileNotFoundError:
+# fp = None
+# if fp == None:
+# continue
+#
+# if v not in items:
+# items.append(v)
+# logg.debug('added subject {} {}'.format(v, fp))
+#
+# r = []
+# for v in items:
+# r.append(_from_xml(v, o, exiftool=state['exif']))
+# return r
diff --git a/test/test_xmp.py b/test/test_xmp.py
@@ -0,0 +1,29 @@
+import unittest
+import datetime
+import hashlib
+import os
+import logging
+
+from rdflib.term import URIRef
+
+from adesse.ns import XMP
+from adesse.util import xmp
+
+logging.basicConfig(level=logging.DEBUG)
+
+thisdir = os.path.realpath(os.path.dirname(__file__))
+
+
+class TestXmpLoad(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+
+ def test_parse(self):
+ fp = os.path.join(thisdir, 'import.xmp')
+ xmp.attachment_from_xmp(fp, 'import.pdf', search_dir=[thisdir])
+
+
+if __name__ == '__main__':
+ unittest.main()