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 }