feat: Enhance XMP writing capabilities with face region and tag support

This commit is contained in:
2025-11-15 14:43:59 +01:00
parent 8d05bdfd63
commit f7f1547592
4 changed files with 166 additions and 37 deletions

View File

@@ -3,7 +3,9 @@ use std::{path::PathBuf, sync::Arc};
use futures_util::StreamExt;
use libertas_core::plugins::PluginContext;
use libertas_infra::factory::{
build_album_repository, build_database_pool, build_media_metadata_repository, build_media_repository, build_user_repository
build_album_repository, build_database_pool, build_face_region_repository,
build_media_metadata_repository, build_media_repository, build_person_repository,
build_tag_repository, build_user_repository,
};
use serde::Deserialize;
use tokio::fs;
@@ -37,13 +39,18 @@ async fn main() -> anyhow::Result<()> {
let media_repo = build_media_repository(&config, db_pool.clone()).await?;
let album_repo = build_album_repository(&config.database, db_pool.clone()).await?;
let user_repo = build_user_repository(&config.database, db_pool.clone()).await?;
let metadata_repo =
build_media_metadata_repository(&config.database, db_pool.clone()).await?;
let metadata_repo = build_media_metadata_repository(&config.database, db_pool.clone()).await?;
let tag_repo = build_tag_repository(&config.database, db_pool.clone()).await?;
let person_repo = build_person_repository(&config.database, db_pool.clone()).await?;
let face_region_repo = build_face_region_repository(&config.database, db_pool.clone()).await?;
let context = Arc::new(PluginContext {
media_repo,
album_repo,
user_repo,
tag_repo,
person_repo,
face_region_repo,
metadata_repo,
media_library_path: config.media_library_path.clone(),
config: Arc::new(config.clone()),

View File

@@ -3,11 +3,15 @@ use std::path::PathBuf;
use async_trait::async_trait;
use libertas_core::{
error::{CoreError, CoreResult},
models::Media,
media_utils::is_invalid_exif_tag,
models::{FaceRegion, Media, MediaMetadataSource, Tag},
plugins::{MediaProcessorPlugin, PluginContext, PluginData},
};
use tokio::fs;
use xmp_toolkit::{XmpMeta, XmpValue};
use xmp_toolkit::{
XmpMeta, XmpValue,
xmp_ns::{DC, EXIF, XMP},
};
pub struct XmpWriterPlugin;
@@ -19,31 +23,52 @@ impl MediaProcessorPlugin for XmpWriterPlugin {
async fn process(&self, media: &Media, context: &PluginContext) -> CoreResult<PluginData> {
let metadata = context.metadata_repo.find_by_media_id(media.id).await?;
let tags: Vec<Tag> = context.tag_repo.list_tags_for_media(media.id).await?;
let faces: Vec<FaceRegion> = context.face_region_repo.find_by_media_id(media.id).await?;
let file_path = PathBuf::from(&context.media_library_path).join(&media.storage_path);
let xmp_path = format!("{}.xmp", file_path.to_string_lossy());
let mut xmp = XmpMeta::new()
.map_err(|e| CoreError::Unknown(format!("Failed to create new XMP metadata: {}", e)))?;
xmp.set_property(
"http://purl.org/dc/elements/1.1/",
"description",
&XmpValue::from(media.original_filename.as_str()),
)
.map_err(|e| {
CoreError::Unknown(format!("Failed to set description property in XMP: {}", e))
})?;
XmpMeta::register_namespace(DC, "dc")
.map_err(|e| CoreError::Unknown(format!("Failed to register DC namespace: {}", e)))?;
XmpMeta::register_namespace(EXIF, "exif")
.map_err(|e| CoreError::Unknown(format!("Failed to register EXIF namespace: {}", e)))?;
XmpMeta::register_namespace(XMP, "xmp")
.map_err(|e| CoreError::Unknown(format!("Failed to register XMP namespace: {}", e)))?;
if let Some(date_tag) = metadata.iter().find(|m| m.tag_name == "DateTimeOriginal") {
let date_str = &date_tag.tag_value;
xmp.set_property(
"http://ns.adobe.com/exif/1.0/",
"DateTimeOriginal",
&XmpValue::from(date_str.as_str()),
)
.map_err(|e| {
CoreError::Unknown(format!("Failed to set DateTimeOriginal in XMP: {}", e))
})?;
set_xmp_prop(&mut xmp, DC, "title", &media.original_filename);
for meta in metadata {
match meta.source {
MediaMetadataSource::Exif => {
if is_invalid_exif_tag(&meta.tag_name, &meta.tag_value) {
continue;
}
set_xmp_prop(&mut xmp, EXIF, &meta.tag_name, &meta.tag_value);
}
MediaMetadataSource::TrackInfo => {
set_xmp_prop(&mut xmp, XMP, &meta.tag_name, &meta.tag_value);
}
}
}
if !tags.is_empty() {
xmp.set_property(DC, "subject", &XmpValue::from("[]"))
.map_err(|e| {
CoreError::Unknown(format!("Failed to create subject array in XMP: {}", e))
})?;
for tag in tags {
add_xmp_array_item(&mut xmp, DC, "subject", &tag.name);
}
}
if let Err(e) = write_face_regions(&mut xmp, &faces, context).await {
eprintln!("Failed to write face regions to XMP: {}", e);
}
let xmp_str = xmp.to_string();
@@ -55,3 +80,64 @@ impl MediaProcessorPlugin for XmpWriterPlugin {
})
}
}
fn set_xmp_prop(xmp: &mut XmpMeta, ns: &str, key: &str, value: &str) {
if let Err(e) = xmp.set_property(ns, key, &XmpValue::from(value)) {
eprintln!("Failed to set {}:{} in XMP: {}", ns, key, e);
}
}
fn add_xmp_array_item(xmp: &mut XmpMeta, ns: &str, key: &str, value: &str) {
if let Err(e) = xmp.append_array_item(ns, &XmpValue::from(key), &XmpValue::from(value)) {
eprintln!("Failed to add item to {}:{} in XMP: {}", ns, key, e);
}
}
async fn write_face_regions(
xmp: &mut XmpMeta,
faces: &[FaceRegion],
context: &PluginContext,
) -> CoreResult<()> {
if faces.is_empty() {
return Ok(());
}
XmpMeta::register_namespace("", "mwg-rs")
.map_err(|e| CoreError::Unknown(format!("Failed to register MWG namespace: {}", e)))?;
xmp.set_property("", "mwg-rs:Regions", &XmpValue::from("[]"))
.map_err(|e| CoreError::Unknown(format!("Failed to create Regions array in XMP: {}", e)))?;
for face in faces {
let mut person_name = "Unknown".to_string();
if let Some(person_id) = face.person_id {
if let Ok(Some(person)) = context.person_repo.find_by_id(person_id).await {
person_name = person.name;
}
}
let region_path = format!("Regions[last()]/mwg-rs:RegionInfo/{{ {} }}", face.id);
xmp.set_property("mwg-rs", &region_path, &XmpValue::from("[]"))
.map_err(|e| {
CoreError::Unknown(format!("Failed to create RegionInfo in XMP: {}", e))
})?;
let name_path = format!("{}/mwg-rs:Name", region_path);
set_xmp_prop(xmp, "mwg-rs", &name_path, &person_name);
let area_str = format!(
"{}, {}, {}, {}",
face.x_min,
face.y_min,
face.x_max - face.x_min, // Width
face.y_max - face.y_min // Height
);
let area_path = format!("{}/mwg-rs:Area", region_path);
set_xmp_prop(xmp, "mwg-rs", &area_path, &area_str);
let type_path = format!("{}/mwg-rs:Type", region_path);
set_xmp_prop(xmp, "mwg-rs", &type_path, "Face");
}
Ok(())
}