xmpedit

GTK+ editor for XMP metadata embedded in images
git clone https://code.djc.id.au/git/xmpedit/

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 }