Files
EricRollei-AAA_Metadata_System/eric_metadata/utils/namespace.py
T
Eric Rollei 1f9fa9b670 Add backward compatibility, XMP regions, per-node hooks toggle, and enhancements
Major Features:
- Backward compatibility: VALIDATE_INPUTS handles old boolean True/False values for enable_runtime_hooks parameter
- Phase 3: XMP region metadata with MWG-RS compliance (faces, areas, coordinates, extensions)
- Phase 4.4: Per-node UI toggle for runtime hooks (dropdown: Auto/Enabled/Disabled)
- Runtime hooks enhancements: performance tracking, disagreement logging, configurable limits

New Files:
- config.json: Configuration file for runtime hooks and metadata settings
- eric_metadata/hooks/runtime_capture.py: Runtime hook infrastructure
- eric_metadata/utils/config.py: Configuration loader with priority system
- eric_metadata/utils/version.py: Version helper for generated_by labels
- eric_metadata/utils/a1111.py: Automatic1111 parameter string generation
- eric_metadata/utils/jpeg_fallback.py: 4-stage JPEG metadata fallback
- eric_metadata/workflow/: Workflow parsing and generation helpers
- tests/test_backward_compatibility.py: 7 tests for parameter migration
- tests/test_xmp_regions.py: 6 tests for XMP region round-trip
- tests/test_per_node_runtime_hooks.py: 9 tests for per-node toggle

Documentation:
- CONFIG_GUIDE.md: Configuration options and usage
- RUNTIME_HOOKS_QUICK_REFERENCE.md: Quick reference for runtime hooks
- RUNTIME_HOOKS_ENHANCEMENTS_SUMMARY.md: Detailed enhancement documentation
- SAFE_RUNTIME_HOOKS_RESEARCH.md: Safety analysis and validation
- edit_log.md: Complete change history

Test Results: All 143 tests passing
- 7 backward compatibility tests
- 6 XMP region tests
- 9 per-node toggle tests
- 17 runtime hooks enhancement tests
- 104 existing tests

Breaking Changes: None - fully backward compatible
2025-11-20 01:26:04 -08:00

238 lines
8.8 KiB
Python

"""
namespace.py
Description: Handles XML namespaces for metadata operations
This module is responsible for managing the registration of XML namespaces
Author: Eric Hiss (GitHub: EricRollei)
Contact: [eric@historic.camera, eric@rollei.us]
Version: 1.0.0
Date: [March 2025]
License: Dual License (Non-Commercial and Commercial Use)
Copyright (c) 2025 Eric Hiss. All rights reserved.
Dual License:
1. Non-Commercial Use: This software is licensed under the terms of the
Creative Commons Attribution-NonCommercial 4.0 International License.
To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/
2. Commercial Use: For commercial use, a separate license is required.
Please contact Eric Hiss at [eric@historic.camera, eric@rollei.us] for licensing options.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT.
Dependencies:
This code depends on several third-party libraries, each with its own license:
"""
# Metadata_system/src/eric_metadata/utils/namespace.py
import os
import sys
import importlib
from typing import Dict, Optional, Tuple, List
class NamespaceManager:
"""Manages XML namespaces for metadata handling"""
# Core namespaces required for metadata operations
NAMESPACES = {
'x': 'adobe:ns:meta/',
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
'dc': 'http://purl.org/dc/elements/1.1/',
'xmp': 'http://ns.adobe.com/xap/1.0/',
'photoshop': 'http://ns.adobe.com/photoshop/1.0/',
'xmpRights': 'http://ns.adobe.com/xap/1.0/rights/',
'xmpMM': 'http://ns.adobe.com/xap/1.0/mm/',
'mwg-rs': 'http://www.metadataworkinggroup.com/schemas/regions/',
'stArea': 'http://ns.adobe.com/xmp/sType/Area#',
'Iptc4xmpCore': 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/',
'eiqa': 'http://ericproject.org/schemas/eiqa/1.0/',
'ai': 'http://ericproject.org/schemas/ai/1.0/',
'xml': 'http://www.w3.org/XML/1998/namespace',
'mwg-kw': 'http://www.metadataworkinggroup.com/schemas/keywords/'
}
@classmethod
def register_with_pyexiv2(cls, debug: bool = False) -> bool:
"""
Register all namespaces with PyExiv2
Args:
debug: Whether to enable debug output
Returns:
bool: True if at least one namespace was registered
"""
success_count = 0
# Get PyExiv2 module
pyexiv2 = cls.get_pyexiv2_module()
if not pyexiv2:
if debug:
print("PyExiv2 module not found")
return False
# Method 0: Top-level registerNs function (for 2.15.x)
if hasattr(pyexiv2, 'registerNs'):
try:
for prefix, uri in cls.NAMESPACES.items():
try:
pyexiv2.registerNs(uri, prefix)
success_count += 1
if debug:
print(f"Registered namespace {prefix} with registerNs")
except Exception as e:
if debug:
print(f"registerNs failed for {prefix}: {e}")
except Exception as e:
if debug:
print(f"registerNs global error: {e}")
# Continue with other methods if top-level method failed
if success_count == 0:
# Try the other methods as fallbacks...
# Method 1: Direct module function (old API)
if hasattr(pyexiv2, 'registerNamespace'):
try:
for prefix, uri in cls.NAMESPACES.items():
try:
pyexiv2.registerNamespace(uri, prefix)
success_count += 1
if debug:
print(f"Registered namespace {prefix} with method 1")
except Exception as e:
if debug:
print(f"Method 1 failed for {prefix}: {e}")
except Exception as e:
if debug:
print(f"Method 1 global error: {e}")
# Method 2: xmp module function (newer API)
if hasattr(pyexiv2, 'xmp'):
try:
for prefix, uri in cls.NAMESPACES.items():
try:
if hasattr(pyexiv2.xmp, 'register_namespace'):
pyexiv2.xmp.register_namespace(uri, prefix)
success_count += 1
if debug:
print(f"Registered namespace {prefix} with method 2")
except Exception as e:
if debug:
print(f"Method 2 failed for {prefix}: {e}")
except Exception as e:
if debug:
print(f"Method 2 global error: {e}")
# Method 3: exiv2api module function
if hasattr(pyexiv2, 'exiv2api'):
try:
for prefix, uri in cls.NAMESPACES.items():
try:
if hasattr(pyexiv2.exiv2api, 'registerNamespace'):
pyexiv2.exiv2api.registerNamespace(uri, prefix)
success_count += 1
if debug:
print(f"Registered namespace {prefix} with method 3")
except Exception as e:
if debug:
print(f"Method 3 failed for {prefix}: {e}")
except Exception as e:
if debug:
print(f"Method 3 global error: {e}")
return success_count > 0
@classmethod
def get_pyexiv2_module(cls) -> Optional[object]:
"""
Get the PyExiv2 module using various methods
Returns:
object or None: The PyExiv2 module if found
"""
# First try direct import
try:
import pyexiv2
return pyexiv2
except ImportError:
pass
# Try py3exiv2 which is sometimes imported as pyexiv2
try:
import py3exiv2
return py3exiv2
except ImportError:
pass
# Try dynamic import
try:
pyexiv2 = importlib.import_module('pyexiv2')
return pyexiv2
except ImportError:
try:
py3exiv2 = importlib.import_module('py3exiv2')
return py3exiv2
except ImportError:
pass
# Look for pyexiv2 in any loaded modules
for name, module in sys.modules.items():
if 'pyexiv2' in name or 'exiv2' in name:
return module
# Not found
return None
@classmethod
def create_exiftool_config(cls, output_path: Optional[str] = None) -> str:
"""
Create ExifTool configuration file with custom namespaces
Args:
output_path: Optional path to write the config file
If None, writes to the current directory
Returns:
str: Path to the created config file
"""
# Generate namespace configuration
ns_config = []
for prefix, uri in cls.NAMESPACES.items():
ns_config.append(f' NAMESPACE => {{ "{prefix}" => "{uri}" }}')
# Create config file content
config = f'''
%Image::ExifTool::UserDefined = (
'Image::ExifTool::XMP::eiqa' => {{
GROUPS => {{ 0 => 'XMP', 1 => 'XMP-eiqa', 2 => 'Image' }},
NAMESPACE => {{ 'eiqa' => '{cls.NAMESPACES["eiqa"]}' }},
WRITABLE => 'string',
noise_analysis => {{ }},
blur_analysis => {{ }},
quality_sort => {{ }},
technical => {{ }}
}},
'Image::ExifTool::XMP::ai' => {{
GROUPS => {{ 0 => 'XMP', 1 => 'XMP-ai', 2 => 'Image' }},
NAMESPACE => {{ 'ai' => '{cls.NAMESPACES["ai"]}' }},
WRITABLE => 'string',
aesthetic => {{ }},
semantic => {{ }},
clip_iqa => {{ }},
nima => {{ }}
}}
);
1;
'''
# Determine output path
if output_path is None:
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'exiftool_config.conf')
# Write config file
with open(output_path, 'w', encoding='utf-8') as f:
f.write(config)
return output_path