Initial commit
[fillmore] / src / marina / util.vala
1 /* Copyright 2009 Yorba Foundation
2  *
3  * This software is licensed under the GNU Lesser General Public License
4  * (version 2.1 or later).  See the COPYING file in this distribution. 
5  */
6
7 public errordomain MediaError {
8     MISSING_PLUGIN
9 }
10
11 // I can't find a floating point absolute value function in Vala...
12 public float float_abs(float f) {
13     if (f < 0.0f)
14         return -f;
15     return f;
16 }
17
18 public bool float_within(double f, double epsilon) {
19     return float_abs((float) f) < epsilon;
20 }
21
22 public int sign(int x) {
23     if (x == 0)
24         return 0;
25     return x < 0 ? -1 : 1;
26 }
27
28 int stricmp(string str1, string str2) {
29     string temp_str1 = str1.casefold(-1);
30     string temp_str2 = str2.casefold(-1);
31     
32     return temp_str1.collate(temp_str2);
33 }
34
35 // TODO: write this using generics.  
36 public string[] copy_array(string[] source) {
37     string[] destination = new string[source.length];
38     int i = 0;
39     foreach (string item in source) {
40         destination[i] = item;
41         ++i;
42     }
43     return destination;
44 }
45
46 // Debug utilities
47
48 public bool debug_enabled;
49
50 public void print_debug(string text) {
51     if (!debug_enabled)
52         return;
53         
54     debug("%s", text);
55 }
56
57 public struct Fraction {
58     public int numerator;
59     public int denominator;
60     
61     public Fraction(int numerator, int denominator) {
62         this.numerator = numerator;
63         this.denominator = denominator;
64     }
65     
66     public Fraction.from_string(string s) {
67         string[] elements = s.split("/");
68         if (elements.length != 2) {
69             numerator = 0;
70             denominator = 0;
71         } else {
72             numerator = elements[0].to_int();
73             denominator = elements[1].to_int();
74         }
75     }
76     
77     public bool equal(Fraction f) {
78         if (float_abs(((numerator / (float)denominator) - (f.numerator / (float)f.denominator))) <=
79             (1000.0f / 1001.0f))
80             return true;
81         return false;
82     }
83     
84     public int nearest_int() {
85         return (int) (((double) numerator / denominator) + 0.5);    
86     }
87     
88     public string to_string() {
89         return "%d/%d".printf(numerator, denominator);
90     }
91     
92 }
93
94 public struct TimeCode {
95     public int hour;
96     public int minute;
97     public int second;
98     public int frame;
99     public bool drop_code;
100     
101     public void get_from_length(int64 length) {
102         length /= Gst.SECOND;
103         
104         hour = (int) (length / 3600);
105         minute = (int) ((length % 3600) / 60);
106         second = (int) ((length % 3600) % 60);
107         frame = 0;
108     }
109
110     public string to_string() {
111         string ret = "";
112         if (hour != 0)
113             ret += "%.2d:".printf(hour);
114
115         ret += "%.2d:".printf(minute);
116         ret += "%.2d".printf(second);
117         
118         if (drop_code)
119             ret += ";";
120         else
121             ret += ":";
122         ret += "%.2d".printf(frame);
123         
124         return ret;        
125     }
126 }
127
128 public bool time_in_range(int64 time, int64 center, int64 delta) {
129     int64 diff = time - center;
130     return diff.abs() <= delta;
131 }
132
133 public string isolate_filename(string path) {
134     string str = Path.get_basename(path);
135     return str.split(".")[0];
136 }
137
138 public string get_file_extension(string path) {
139     unowned string dot = path.rchr(-1, '.');
140     return dot == null ? "" : dot.next_char();
141 }
142
143 public string append_extension(string path, string extension) {
144     if (get_file_extension(path) == extension)
145         return path;
146         
147     return path + "." + extension;
148 }
149
150 // Given two version number strings such as "0.10.2.4", return true if the first is
151 // greater than or equal to the second.
152 public bool version_at_least(string v, string w) {
153     string[] va = v.split(".");
154     string[] wa = w.split(".");
155     for (int i = 0 ; i < wa.length ; ++i) {
156         if (i >= va.length)
157             return false;
158         int vi = va[i].to_int();
159         int wi = wa[i].to_int();
160         if (vi > wi)
161             return true;
162         if (wi > vi)
163             return false;
164     }
165     return true;
166 }
167
168 public bool get_file_md5_checksum(string filename, out string checksum) {
169     string new_filename = append_extension(filename, "md5");
170     
171     size_t buffer_length;
172     try {
173         GLib.FileUtils.get_contents(new_filename, out checksum, out buffer_length);
174     } catch (GLib.FileError e) {
175         return false;
176     }
177     
178     return buffer_length == 32;
179 }
180
181 public void save_file_md5_checksum(string filename, string checksum) {
182     string new_filename = append_extension(filename, "md5");
183     
184     try {
185         GLib.FileUtils.set_contents(new_filename, checksum);
186     } catch (GLib.FileError e) {
187         error("Cannot save md5 file %s!\n", new_filename);
188     }
189 }
190
191 public bool md5_checksum_on_file(string filename, out string checksum) {
192     string file_buffer;
193     size_t len;
194     
195     try {
196         GLib.FileUtils.get_contents(filename, out file_buffer, out len);
197     } catch (GLib.FileError e) {
198         return false;
199     }
200
201     GLib.Checksum c = new GLib.Checksum(GLib.ChecksumType.MD5);
202     c.update((uchar[]) file_buffer, len);
203     checksum = c.get_string();
204     return true;
205 }
206
207 // GDK/GTK utility functions
208
209 // constants from gdkkeysyms.h https://bugzilla.gnome.org/show_bug.cgi?id=551184
210 [CCode (cprefix = "GDK_", has_type_id = "0", cheader_filename = "gdk/gdkkeysyms.h")]
211 public enum KeySyms {
212     Control_L,
213     Control_R,
214     Down,
215     equal,
216     Escape,
217     KP_Add,
218     KP_Enter,
219     KP_Subtract,
220     Left,
221     minus,
222     plus,
223     Return,
224     Right,
225     Shift_L,
226     Shift_R,
227     underscore,
228     Up
229 }
230
231 public Gdk.ModifierType GDK_SHIFT_ALT_CONTROL_MASK = Gdk.ModifierType.SHIFT_MASK |
232                                                      Gdk.ModifierType.MOD1_MASK |
233                                                      Gdk.ModifierType.CONTROL_MASK;
234
235 public const Gtk.TargetEntry[] drag_target_entries = {
236     { "text/uri-list", 0, 0 } 
237 };
238
239 public Gdk.Color parse_color(string color) {
240     Gdk.Color c;
241     if (!Gdk.Color.parse(color, out c))
242         error("can't parse color");
243     return c;
244 }
245
246 public Gtk.Widget get_widget(Gtk.UIManager manager, string name) {
247     Gtk.Widget widget = manager.get_widget(name);
248     if (widget == null)
249         error("can't find widget");
250     return widget;
251 }
252
253 /////////////////////////////////////////////////////////////////////////////
254 //                    Rectangle drawing stuff                              //
255 // Original rounded rectangle code from: http://cairographics.org/samples/ //
256 /////////////////////////////////////////////////////////////////////////////
257
258 const double LINE_WIDTH = 1.0;
259 const double RADIUS = 15.0;
260 const Cairo.Antialias ANTIALIAS = Cairo.Antialias.DEFAULT; // NONE/DEFAULT
261
262 public void draw_rounded_rectangle(Gdk.Window window, Gdk.Color color, bool filled, 
263                             int x0, int y0, int width, int height) {
264     if (width == 0 || height == 0)
265         return;
266
267     double x1 = x0 + width;
268     double y1 = y0 + height;
269     
270     Cairo.Context cairo_window = Gdk.cairo_create(window);
271     Gdk.cairo_set_source_color(cairo_window, color);
272     cairo_window.set_antialias(ANTIALIAS);
273         
274     if ((width / 2) < RADIUS) {
275         if ((height / 2) < RADIUS) {
276             cairo_window.move_to(x0, ((y0 + y1) / 2));
277             cairo_window.curve_to(x0, y0, x0, y0, (x0 + x1) / 2, y0);
278             cairo_window.curve_to(x1, y0, x1, y0, x1, (y0 + y1) / 2);
279             cairo_window.curve_to(x1, y1, x1, y1, (x1 + x0) / 2, y1);
280             cairo_window.curve_to(x0, y1, x0, y1, x0, (y0 + y1) / 2);
281         } else {
282             cairo_window.move_to(x0, y0 + RADIUS);
283             cairo_window.curve_to(x0,y0, x0, y0, (x0 + x1) / 2, y0);
284             cairo_window.curve_to(x1, y0, x1, y0, x1, y0 + RADIUS);
285             cairo_window.line_to(x1, y1 - RADIUS);
286             cairo_window.curve_to(x1, y1, x1, y1, (x1 + x0) / 2, y1);
287             cairo_window.curve_to(x0, y1, x0, y1, x0, y1 - RADIUS);
288         }
289     } else {
290         if ((height / 2) < RADIUS) {
291             cairo_window.move_to(x0, (y0 + y1) / 2);
292             cairo_window.curve_to(x0, y0, x0, y0, x0 + RADIUS, y0);
293             cairo_window.line_to(x1 - RADIUS, y0);
294             cairo_window.curve_to(x1, y0, x1, y0, x1, (y0 + y1) / 2);
295             cairo_window.curve_to(x1, y1, x1, y1, x1 - RADIUS, y1);
296             cairo_window.line_to(x0 + RADIUS, y1);
297             cairo_window.curve_to(x0, y1, x0, y1, x0, (y0 + y1) / 2);
298         } else {
299             cairo_window.move_to(x0, y0 + RADIUS);
300             cairo_window.curve_to(x0, y0, x0, y0, x0 + RADIUS, y0);
301             cairo_window.line_to(x1 - RADIUS, y0);
302             cairo_window.curve_to(x1, y0, x1, y0, x1, y0 + RADIUS);
303             cairo_window.line_to(x1, y1 - RADIUS);
304             cairo_window.curve_to(x1, y1, x1, y1, x1 - RADIUS, y1);
305             cairo_window.line_to(x0 + RADIUS, y1);
306             cairo_window.curve_to(x0, y1, x0, y1, x0, y1 - RADIUS);
307         }
308     }
309     cairo_window.close_path();
310
311     if (filled) {
312         cairo_window.fill();
313     } else {
314         cairo_window.set_line_width(LINE_WIDTH);
315         cairo_window.stroke();
316     }
317 }
318
319 public void draw_right_rounded_rectangle(Gdk.Window window, Gdk.Color color, bool filled, 
320                                   int x0, int y0, int width, int height) {
321     if (width == 0 || height == 0)
322         return;
323
324     double x1 = x0 + width;
325     double y1 = y0 + height;
326     
327     Cairo.Context cairo_window = Gdk.cairo_create(window);
328     Gdk.cairo_set_source_color(cairo_window, color);
329     cairo_window.set_antialias(ANTIALIAS);
330
331     if ((width / 2) < RADIUS) {
332         if ((height / 2) < RADIUS) {
333             cairo_window.move_to(x0, y0);
334             cairo_window.line_to((x0 + x1) / 2, y0);
335             cairo_window.curve_to(x1, y0, x1, y0, x1, (y0 + y1) / 2);
336             cairo_window.curve_to(x1, y1, x1, y1, (x1 + x0) / 2, y1);
337             cairo_window.line_to(x0, y1);
338             cairo_window.line_to(x0, y0);
339         } else {
340             cairo_window.move_to(x0, y0);
341             cairo_window.line_to((x0 + x1) / 2, y0);
342             cairo_window.curve_to(x1, y0, x1, y0, x1, y0 + RADIUS);
343             cairo_window.line_to(x1, y1 - RADIUS);
344             cairo_window.curve_to(x1, y1, x1, y1, (x1 + x0) / 2, y1);
345             cairo_window.line_to(x0, y1);
346             cairo_window.line_to(x0, y0);
347         }
348     } else {
349         if ((height / 2) < RADIUS) {
350             cairo_window.move_to(x0, y0);
351             cairo_window.line_to(x1 - RADIUS, y0);
352             cairo_window.curve_to(x1, y0, x1, y0, x1, (y0 + y1) / 2);
353             cairo_window.curve_to(x1, y1, x1, y1, x1 - RADIUS, y1);
354             cairo_window.line_to(x0, y1);
355             cairo_window.line_to(x0, y0);
356         } else {
357             cairo_window.move_to(x0, y0);
358             cairo_window.line_to(x1 - RADIUS, y0);
359             cairo_window.curve_to(x1, y0, x1, y0, x1, y0 + RADIUS);
360             cairo_window.line_to(x1, y1 - RADIUS);
361             cairo_window.curve_to(x1, y1, x1, y1, x1 - RADIUS, y1);
362             cairo_window.line_to(x0, y1);
363             cairo_window.line_to(x0, y0);
364         }
365     }
366     cairo_window.close_path();
367
368     if (filled) {
369         cairo_window.fill();
370     } else {
371         cairo_window.set_line_width(LINE_WIDTH);
372         cairo_window.stroke();
373     }
374 }
375
376 public void draw_left_rounded_rectangle(Gdk.Window window, Gdk.Color color, bool filled, 
377                                  int x0, int y0, int width, int height) {
378     if (width == 0 || height == 0)
379         return;
380
381     double x1 = x0 + width;
382     double y1 = y0 + height;
383
384     Cairo.Context cairo_window = Gdk.cairo_create(window);
385     Gdk.cairo_set_source_color(cairo_window, color);
386     cairo_window.set_antialias(ANTIALIAS);
387
388     if ((width / 2) < RADIUS) {
389         if ((height / 2) < RADIUS) {
390             cairo_window.move_to(x0, ((y0 + y1) / 2));
391             cairo_window.curve_to(x0, y0, x0, y0, (x0 + x1) / 2, y0);
392             cairo_window.line_to(x1, y0);
393             cairo_window.line_to(x1, y1);
394             cairo_window.line_to((x1 + x0) / 2, y1);
395             cairo_window.curve_to(x0, y1, x0, y1, x0, (y0 + y1) / 2);
396         } else {
397             cairo_window.move_to(x0, y0 + RADIUS);
398             cairo_window.curve_to(x0,y0, x0, y0, (x0 + x1) / 2, y0);
399             cairo_window.line_to(x1, y0);
400             cairo_window.line_to(x1, y1);
401             cairo_window.line_to((x1 + x0) / 2, y1);
402             cairo_window.curve_to(x0, y1, x0, y1, x0, y1 - RADIUS);
403         }
404     } else {
405         if ((height / 2) < RADIUS) {
406             cairo_window.move_to(x0, (y0 + y1) / 2);
407             cairo_window.curve_to(x0, y0, x0, y0, x0 + RADIUS, y0);
408             cairo_window.line_to(x1, y0);
409             cairo_window.line_to(x1, y1);
410             cairo_window.line_to(x0 + RADIUS, y1);
411             cairo_window.curve_to(x0, y1, x0, y1, x0, (y0 + y1) / 2);
412         } else {
413             cairo_window.move_to(x0, y0 + RADIUS);
414             cairo_window.curve_to(x0, y0, x0, y0, x0 + RADIUS, y0);
415             cairo_window.line_to(x1, y0);
416             cairo_window.line_to(x1, y1);
417             cairo_window.line_to(x0 + RADIUS, y1);
418             cairo_window.curve_to(x0, y1, x0, y1, x0, y1 - RADIUS);
419         }
420     }
421     cairo_window.close_path();
422
423     if (filled) {
424         cairo_window.fill();
425     } else {
426         cairo_window.set_line_width(LINE_WIDTH);
427         cairo_window.stroke();
428     }
429 }
430
431 public void draw_square_rectangle(Gdk.Window window, Gdk.Color color, bool filled, 
432                            int x, int y, int width, int height) {
433     if (width == 0 || height == 0)
434         return;
435
436     Cairo.Context cairo_window = Gdk.cairo_create(window);
437     Gdk.cairo_set_source_color(cairo_window, color);
438     cairo_window.set_antialias(ANTIALIAS);
439
440     cairo_window.rectangle(x, y, width, height);
441
442     if (filled) {
443         cairo_window.fill();
444     } else {
445         cairo_window.set_line_width(LINE_WIDTH);
446         cairo_window.stroke();
447     }
448 }
449
450 // GStreamer utility functions
451
452 public bool is_drop_frame_rate(Fraction r) {
453     return r.numerator == 2997 && r.denominator == 100 ||
454            r.numerator == 30000 && r.denominator == 1001;    
455 }
456
457 public int64 frame_to_time_with_rate(int frame, Fraction rate) {
458     int64 time = (int64) Gst.util_uint64_scale(frame, Gst.SECOND * rate.denominator, rate.numerator);
459     return time;
460 }
461
462 public int time_to_frame_with_rate(int64 time, Fraction rate) {
463     int frame = (int) Gst.util_uint64_scale(time, rate.numerator, Gst.SECOND * rate.denominator);
464         
465     /* We need frame_to_time_with_rate and time_to_frame_with_rate to be inverse functions, so that
466      * time_to_frame(frame_to_time_with_rate(f)) = f for all f.  With the simple calculation
467      * above the functions might not be inverses due to rounding error, so we
468      * need the following check. */
469     return time >= frame_to_time_with_rate(frame + 1, rate) ? frame + 1 : frame;
470 }
471
472 public TimeCode frame_to_time(int frame, Fraction rate) {
473     int frame_rate = 0;
474    
475     TimeCode t = {};
476     
477     t.drop_code = false;   
478     if (rate.denominator == 1)
479         frame_rate = rate.numerator;
480     else if (is_drop_frame_rate(rate)) {
481         t.drop_code = true;
482         frame_rate = 30;
483
484         // We can't declare these as const int due to a Vala compiler bug.
485         int FRAMES_PER_MINUTE = 30 * 60 - 2;
486         int FRAMES_PER_10_MINUTES = 10 * FRAMES_PER_MINUTE + 2;
487        
488         int block = frame / FRAMES_PER_10_MINUTES;  // number of 10-minute blocks elapsed
489         int minute_in_block = (frame % FRAMES_PER_10_MINUTES - 2) / FRAMES_PER_MINUTE;
490         int minutes = 10 * block + minute_in_block;
491         frame += 2 * minutes - 2 * block;   // skip 2 frames per minute, except every 10 minutes
492     } else {
493         // TODO: We're getting odd framerate fractions from imported videos, so
494         // I've removed the error call until we decide what to do
495         frame_rate = rate.numerator / rate.denominator;
496     }
497     
498     t.frame = frame % frame_rate;
499     
500     int64 secs = frame / frame_rate;
501     t.hour = (int) secs / 3600;
502     t.minute = ((int) secs % 3600) / 60;   
503     t.second = ((int) secs % 3600) % 60;
504     
505     return t;
506 }
507
508 public string frame_to_string(int frame, Fraction rate) {    
509     return frame_to_time(frame, rate).to_string();
510 }
511
512 void breakup_time(int64 time, out int hours, out int minutes, out double seconds) {
513     int64 the_time = time;
514     int64 minute = Gst.SECOND * 60;
515     int64 hour = minute * 60;
516     hours = (int) (the_time / hour);
517     the_time = the_time % hour;
518     minutes = (int) (the_time / minute);
519     the_time = the_time % minute;
520     seconds = (double) the_time / Gst.SECOND;
521 }
522
523 public string time_to_HHMMSS(int64 time) {
524     int hours;
525     int minutes;
526     double seconds;
527
528     breakup_time(time, out hours, out minutes, out seconds);
529     return "%02d:%02d:%05.2lf".printf(hours, minutes, seconds);
530 }
531
532 public string time_to_string(int64 time) {
533     int hours;
534     int minutes;
535     double seconds;
536
537     breakup_time(time, out hours, out minutes, out seconds);
538     string return_value = "%1.2lfs".printf(seconds);
539     if (hours > 0 || minutes > 0) {
540         return_value = "%dm ".printf(minutes) + return_value;
541     }
542     
543     if (hours > 0) {
544         return_value = "%dh ".printf(hours) + return_value;
545     }
546     
547     return return_value;
548 }
549
550 public static Gst.Element make_element_with_name(string element_name, string? display_name) 
551         throws GLib.Error {
552     Gst.Element e = Gst.ElementFactory.make(element_name, display_name);
553     if (e == null) {
554         throw new
555             MediaError.MISSING_PLUGIN("Could not create element %s(%s)".printf(element_name, display_name));
556     }
557     return e;
558 }
559
560 public static Gst.Element make_element(string name) throws Error {
561     return make_element_with_name(name, null);
562 }
563