src/ImageMetadata.vala (11476B) - raw
1 /* 2 * xmpedit 3 * Copyright 2010 Dan Callaghan <djc@djc.id.au> 4 * Released under GPLv2 5 */ 6 7 namespace Xmpedit { 8 9 public abstract class ImageProperty : Object { 10 11 public static Type[] all_types() { 12 return { typeof(Description), typeof(Location) }; 13 } 14 15 public abstract string name { get; } 16 public RDF.Graph graph { get; construct; } 17 public RDF.URIRef subject { get; construct; } 18 19 public signal void changed(); 20 21 /** 22 * Examine the graph and return a one-line summary of the value found 23 * (or "<i>not set</i>" if no value was found). 24 */ 25 protected abstract string value_summary(); 26 27 public string display_name() { 28 return name.substring(0, 1).up() + name.substring(1); 29 } 30 31 public string list_markup() { 32 return @"<b>$(display_name())</b>\n$(value_summary())"; 33 } 34 35 protected RDF.PlainLiteral? find_literal(RDF.URIRef predicate) { 36 var obj = graph.find_object(subject, predicate); 37 if (obj == null) 38 return null; 39 if (obj is RDF.SubjectNode) { 40 var node = (RDF.SubjectNode) obj; 41 if (graph.has_matching_statement(node, 42 new RDF.URIRef(RDF.RDF_NS + "type"), 43 new RDF.URIRef(RDF.RDF_NS + "Alt"))) { 44 obj = select_alternative(node); 45 if (obj == null) { 46 warning("found rdf:Alt with no alternatives for %s", name); 47 return null; 48 } 49 } 50 } 51 if (!(obj is RDF.PlainLiteral)) { 52 warning("found non-literal node for %s", name); 53 return null; 54 } 55 return (RDF.PlainLiteral) obj; 56 } 57 58 /** Given an rdf:Alt node, returns the "best" alternative */ 59 protected RDF.Node? select_alternative(RDF.SubjectNode alt_node) { 60 var preferred_lang = get_preferred_lang(); 61 var alternatives = graph.find_objects(alt_node, new RDF.URIRef(RDF.RDF_NS + "li")); 62 if (alternatives.size == 0) 63 return null; 64 foreach (var alternative in alternatives) { 65 if (alternative is RDF.PlainLiteral) { 66 var literal = (RDF.PlainLiteral) alternative; 67 if (literal.lang == preferred_lang) 68 return literal; 69 } 70 } 71 return alternatives[0]; 72 } 73 74 } 75 76 public class Description : ImageProperty { 77 78 private static RDF.URIRef DC_DESCRIPTION = new RDF.URIRef("http://purl.org/dc/elements/1.1/description"); 79 80 public override string name { get { return "description"; } } 81 82 private string _value; 83 private string _lang; 84 85 public string value { 86 get { return _value; } 87 set { _value = value; update(); } 88 } 89 public string lang { 90 get { return _lang; } 91 set { _lang = value; update(); } 92 } 93 94 construct { 95 var literal = find_literal(DC_DESCRIPTION); 96 if (literal == null) { 97 _value = ""; 98 _lang = ""; 99 } else { 100 _value = literal.lexical_value; 101 _lang = (literal.lang != null ? literal.lang : ""); 102 } 103 } 104 105 protected override string value_summary() { 106 return _value; 107 } 108 109 private void update() { 110 graph.remove_matching_statements(subject, DC_DESCRIPTION, null); 111 if (_value.length > 0) { 112 var alt = new RDF.Blank(); 113 graph.insert(new RDF.Statement(subject, DC_DESCRIPTION, alt)); 114 graph.insert(new RDF.Statement(alt, 115 new RDF.URIRef(RDF.RDF_NS + "type"), 116 new RDF.URIRef(RDF.RDF_NS + "Alt"))); 117 RDF.PlainLiteral object; 118 if (_lang.length > 0) 119 object = new RDF.PlainLiteral.with_lang(_value, _lang); 120 else 121 object = new RDF.PlainLiteral(_value); 122 graph.insert(new RDF.Statement(alt, 123 new RDF.URIRef(RDF.RDF_NS + "li"), object)); 124 } 125 changed(); 126 } 127 128 } 129 130 public class Location : ImageProperty { 131 132 private static RDF.URIRef IPTCCORE_LOCATION = new RDF.URIRef("http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/Location"); 133 134 public override string name { get { return "location"; } } 135 136 private string _location; 137 // XXX double bad 138 private double? _latitude; 139 private double? _longitude; 140 141 public string location { 142 get { return _location; } 143 set { _location = value; update(); } 144 } 145 public double? latitude { 146 get { return _latitude; } 147 set { _latitude = value; update(); } 148 } 149 public double? longitude { 150 get { return _longitude; } 151 set { _longitude = value; update(); } 152 } 153 154 construct { 155 var location_literal = find_literal(IPTCCORE_LOCATION); 156 if (location_literal != null) { 157 _location = location_literal.lexical_value; 158 } else { 159 _location = ""; 160 } 161 } 162 163 protected override string value_summary() { 164 if (_location.length > 0) 165 return _location; 166 else if (_latitude != null && _longitude != null) 167 return @"$(_latitude), $(_longitude)"; 168 else 169 return ""; 170 } 171 172 private void update() { 173 // XXX 174 changed(); 175 } 176 177 } 178 179 // XXX make this a user pref somehow? 180 private string get_preferred_lang() { 181 var lang = Environment.get_variable("LANG"); 182 return (lang != null ? lang.substring(0, 2) : "en"); 183 } 184 185 public class ImageMetadata : Object, Gtk.TreeModel { 186 187 public string path { get; construct; } 188 public Gee.List<ImageProperty> properties { get; construct; } 189 public bool dirty { get; set; } 190 private Exiv2.Image image; 191 private size_t xmp_packet_size; 192 private RDF.Graph graph; 193 private RDF.URIRef subject; 194 195 // TreeModel stuff 196 private static int last_stamp = 1; 197 private int stamp; 198 199 public ImageMetadata(string path) { 200 Object(path: path, dirty: false); 201 } 202 203 construct { 204 properties = new Gee.LinkedList<ImageProperty>(); 205 lock (last_stamp) { 206 stamp = last_stamp ++; 207 } 208 } 209 210 public void load() { 211 return_if_fail(image == null); // only call this once 212 image = new Exiv2.Image.from_path(path); 213 image.read_metadata(); 214 read_xmp(); 215 } 216 217 public void revert() { 218 return_if_fail(image != null); 219 read_xmp(); 220 } 221 222 private void read_xmp() { 223 unowned string xmp = image.xmp_packet; 224 xmp_packet_size = xmp.length; 225 #if DEBUG 226 stderr.puts("=== Extracted XMP packet:\n"); 227 stderr.puts(xmp); 228 stderr.putc('\n'); 229 #endif 230 var base_uri = File.new_for_path(path).get_uri(); 231 graph = new RDF.Graph.from_xml(xmp, base_uri); 232 #if DEBUG 233 stderr.puts("=== Parsed RDF graph:\n"); 234 foreach (var s in graph.get_statements()) 235 stderr.puts(@"$s\n"); 236 #endif 237 subject = new RDF.URIRef(base_uri); 238 clear_properties(); 239 foreach (var type in ImageProperty.all_types()) { 240 add_property(type); 241 } 242 dirty = false; 243 } 244 245 public void save() { 246 return_if_fail(image != null); // only call after successful loading 247 return_if_fail(dirty); // only call if dirty 248 #if DEBUG 249 stderr.puts("=== Final RDF graph:\n"); 250 foreach (var s in graph.get_statements()) 251 stderr.puts(@"$s\n"); 252 #endif 253 var xml = new StringBuilder.sized(xmp_packet_size); 254 xml.append("<?xpacket begin=\"\xef\xbb\xbf\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>" + 255 """<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="xmpedit 0.0-dev">"""); 256 xml.append(graph.to_xml(subject)); 257 xml.append("</x:xmpmeta>"); 258 var new_size = xml.str.length + 19; // plus trailing PI 259 size_t padding; 260 if (new_size <= xmp_packet_size) 261 padding = xmp_packet_size - new_size; 262 else 263 padding = new_size + 1024; 264 for (size_t i = 0; i < padding; i ++) 265 xml.append_c(' '); 266 xml.append("""<?xpacket end="w"?>"""); 267 #if DEBUG 268 stderr.puts("=== Serialized XMP packet:\n"); 269 stderr.puts(xml.str); 270 #endif 271 image.xmp_packet = xml.str; 272 image.write_metadata(); 273 dirty = false; 274 } 275 276 private void clear_properties() { 277 for (var i = properties.size - 1; i >= 0; i --) { 278 properties.remove_at(i); 279 row_deleted(path_for_index(i)); 280 } 281 } 282 283 private void add_property(Type type) { 284 var index = properties.size; 285 var p = (ImageProperty) Object.new(type, graph: graph, subject: subject); 286 properties.add(p); 287 row_inserted(path_for_index(index), iter_for_index(index)); 288 p.changed.connect(() => { 289 dirty = true; 290 row_changed(path_for_index(index), iter_for_index(index)); 291 }); 292 } 293 294 /****** TREEMODEL IMPLEMENTATION STUFF **********/ 295 296 private Gtk.TreePath path_for_index(int index) { 297 return new Gtk.TreePath.from_indices(index); 298 } 299 300 private Gtk.TreeIter iter_for_index(int index) { 301 return { stamp, (void*) properties[index], null, null }; 302 } 303 304 public Type get_column_type(int column) { 305 return_val_if_fail(column == 0, 0); 306 return typeof(ImageProperty); 307 } 308 309 public int get_n_columns() { 310 return 1; 311 } 312 313 public Gtk.TreeModelFlags get_flags() { 314 return Gtk.TreeModelFlags.LIST_ONLY; 315 } 316 317 public bool get_iter(out Gtk.TreeIter iter, Gtk.TreePath path) { 318 if (path.get_depth() > 1) return false; 319 var index = path.get_indices()[0]; 320 if (index > properties.size - 1) return false; 321 iter = iter_for_index(index); 322 return true; 323 } 324 325 public Gtk.TreePath get_path(Gtk.TreeIter iter) { 326 return_val_if_fail(iter.stamp == stamp, null); 327 var p = (ImageProperty) iter.user_data; 328 return path_for_index(properties.index_of(p)); 329 } 330 331 public void get_value(Gtk.TreeIter iter, int column, out Value value) { 332 return_if_fail(iter.stamp == stamp); 333 return_if_fail(column == 0); 334 value = Value(typeof(ImageProperty)); 335 value.set_object((ImageProperty) iter.user_data); 336 } 337 338 public bool iter_children(out Gtk.TreeIter iter, Gtk.TreeIter? parent) { 339 if (parent == null && !properties.is_empty) { 340 iter = iter_for_index(0); 341 return true; 342 } 343 return false; 344 } 345 346 public bool iter_has_child(Gtk.TreeIter iter) { 347 return false; 348 } 349 350 public int iter_n_children(Gtk.TreeIter? iter) { 351 if (iter == null) 352 return properties.size; 353 return 0; 354 } 355 356 public bool iter_next(ref Gtk.TreeIter iter) { 357 return_val_if_fail(iter.stamp == stamp, false); 358 var index = properties.index_of((ImageProperty) iter.user_data); 359 if (index < properties.size - 1) { 360 iter.user_data = (void*) properties[index + 1]; 361 return true; 362 } 363 return false; 364 } 365 366 public bool iter_nth_child(out Gtk.TreeIter iter, Gtk.TreeIter? parent, int n) { 367 if (parent == null && n <= properties.size - 1) { 368 iter = { stamp, (void*) properties[n], null, null }; 369 return true; 370 } 371 return false; 372 } 373 374 public bool iter_parent(out Gtk.TreeIter iter, Gtk.TreeIter child) { 375 return false; 376 } 377 378 public void ref_node(Gtk.TreeIter iter) { 379 } 380 381 public void unref_node(Gtk.TreeIter iter) { 382 } 383 384 } 385 386 }