]> git.decadent.org.uk Git - videolink.git/blob - webdvd.cpp
8019007deb4fda978bc7533328dbbdcd28bfb169
[videolink.git] / webdvd.cpp
1 // Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
2 // See the file "COPYING" for licence details.
3
4 #include <cassert>
5 #include <cstring>
6 #include <exception>
7 #include <fstream>
8 #include <iomanip>
9 #include <iostream>
10 #include <memory>
11 #include <queue>
12 #include <set>
13 #include <sstream>
14 #include <string>
15
16 #include <stdlib.h>
17
18 #include <boost/shared_ptr.hpp>
19
20 #include <gdkmm/pixbuf.h>
21 #include <glibmm/convert.h>
22 #include <glibmm/spawn.h>
23 #include <gtkmm/main.h>
24 #include <gtkmm/window.h>
25
26 #include <imglib2/ImageErrors.h>
27 #include <nsGUIEvent.h>
28 #include <nsIBoxObject.h>
29 #include <nsIContent.h>
30 #include <nsIDocShell.h>
31 #include <nsIDOMAbstractView.h>
32 #include <nsIDOMBarProp.h>
33 #include <nsIDOMDocumentEvent.h>
34 #include <nsIDOMDocumentView.h>
35 #include <nsIDOMElement.h>
36 #include <nsIDOMEventTarget.h>
37 #include <nsIDOMHTMLDocument.h>
38 #include <nsIDOMMouseEvent.h>
39 #include <nsIDOMNSDocument.h>
40 #include <nsIDOMWindow.h>
41 #include <nsIEventStateManager.h>
42 #include <nsIInterfaceRequestorUtils.h>
43 #include <nsIURI.h> // required before nsILink.h
44 #include <nsILink.h>
45 #include <nsIPrefBranch.h>
46 #include <nsIPrefService.h>
47 #include <nsIPresContext.h>
48 #include <nsIPresShell.h>
49 #include <nsIServiceManagerUtils.h>
50 #include <nsIWebBrowser.h>
51 #include <nsString.h>
52
53 #include "browser_widget.hpp"
54 #include "child_iterator.hpp"
55 #include "dvd.hpp"
56 #include "generate_dvd.hpp"
57 #include "link_iterator.hpp"
58 #include "pixbufs.hpp"
59 #include "style_sheets.hpp"
60 #include "temp_file.hpp"
61 #include "video.hpp"
62 #include "x_frame_buffer.hpp"
63 #include "xpcom_support.hpp"
64
65 using xpcom_support::check;
66
67 namespace
68 {
69     struct rectangle
70     {
71         int left, top;     // inclusive
72         int right, bottom; // exclusive
73
74         rectangle operator|=(const rectangle & other)
75             {
76                 if (other.empty())
77                 {
78                     // use current extents unchanged
79                 }
80                 else if (empty())
81                 {
82                     // use other extents
83                     *this = other;
84                 }
85                 else
86                 {
87                     // find rectangle enclosing both extents
88                     left = std::min(left, other.left);
89                     top = std::min(top, other.top);
90                     right = std::max(right, other.right);
91                     bottom = std::max(bottom, other.bottom);
92                 }
93
94                 return *this;
95             }
96
97         rectangle operator&=(const rectangle & other)
98             {
99                 // find rectangle enclosed in both extents
100                 left = std::max(left, other.left);
101                 top = std::max(top, other.top);
102                 right = std::max(left, std::min(right, other.right));
103                 bottom = std::max(top, std::min(bottom, other.bottom));
104                 return *this;
105             }
106
107         bool empty() const
108             {
109                 return left == right || bottom == top;
110             }
111     };
112
113     rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
114                             nsIDOMElement * elem)
115     {
116         rectangle result;
117
118         // Start with this element's bounding box
119         nsCOMPtr<nsIBoxObject> box;
120         check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
121         int width, height;
122         check(box->GetScreenX(&result.left));
123         check(box->GetScreenY(&result.top));
124         check(box->GetWidth(&width));
125         check(box->GetHeight(&height));
126         result.right = result.left + width;
127         result.bottom = result.top + height;
128
129         // Merge bounding boxes of all child elements
130         for (child_iterator it = child_iterator(elem), end; it != end; ++it)
131         {
132             nsCOMPtr<nsIDOMNode> child_node(*it);
133             PRUint16 child_type;
134             if (check(child_node->GetNodeType(&child_type)),
135                 child_type == nsIDOMNode::ELEMENT_NODE)
136             {
137                 nsCOMPtr<nsIDOMElement> child_elem(
138                     do_QueryInterface(child_node));
139                 result |= get_elem_rect(ns_doc, child_elem);
140             }
141         }
142
143         return result;
144     }
145
146
147     std::string xml_escape(const std::string & str)
148     {
149         std::string result;
150         std::size_t begin = 0;
151
152         for (;;)
153         {
154             std::size_t end = str.find_first_of("\"&'<>", begin);
155             result.append(str, begin, end - begin);
156             if (end == std::string::npos)
157                 return result;
158
159             const char * entity = NULL;
160             switch (str[end])
161             {
162             case '"':  entity = "&quot;"; break;
163             case '&':  entity = "&amp;";  break;
164             case '\'': entity = "&apos;"; break;
165             case '<':  entity = "&lt;";   break;
166             case '>':  entity = "&gt;";   break;
167             }
168             assert(entity);
169             result.append(entity);
170
171             begin = end + 1;
172         }
173     }
174
175     
176     class webdvd_window : public Gtk::Window
177     {
178     public:
179         webdvd_window(
180             const video::frame_params & frame_params,
181             const std::string & main_page_uri,
182             const std::string & output_dir);
183
184     private:
185         dvd_contents::pgc_ref add_menu(const std::string & uri);
186         dvd_contents::pgc_ref add_title(const std::string & uri);
187         void load_next_page();
188         void on_net_state_change(const char * uri, gint flags, guint status);
189         bool browser_is_busy() const
190             {
191                 return pending_window_update_ || pending_req_count_;
192             }
193         bool process_page();
194         void save_screenshot();
195         void process_links(nsIPresShell * pres_shell,
196                            nsIPresContext * pres_context,
197                            nsIDOMWindow * dom_window);
198
199         video::frame_params frame_params_;
200         std::string output_dir_;
201         browser_widget browser_widget_;
202         nsCOMPtr<nsIStyleSheet> stylesheet_;
203
204         dvd_contents contents_;
205         typedef std::map<std::string, dvd_contents::pgc_ref> resource_map_type;
206         resource_map_type resource_map_;
207
208         std::queue<std::string> page_queue_;
209         bool pending_window_update_;
210         int pending_req_count_;
211         bool have_tweaked_page_;
212         std::auto_ptr<temp_file> background_temp_;
213         struct page_state;
214         std::auto_ptr<page_state> page_state_;
215     };
216
217     webdvd_window::webdvd_window(
218         const video::frame_params & frame_params,
219         const std::string & main_page_uri,
220         const std::string & output_dir)
221             : frame_params_(frame_params),
222               output_dir_(output_dir),
223               stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
224               pending_window_update_(false),
225               pending_req_count_(0),
226               have_tweaked_page_(false)
227     {
228         set_size_request(frame_params_.width, frame_params_.height);
229         set_resizable(false);
230
231         add(browser_widget_);
232         browser_widget_.show();
233         browser_widget_.signal_net_state().connect(
234             SigC::slot(*this, &webdvd_window::on_net_state_change));
235
236         add_menu(main_page_uri);
237         load_next_page();
238     }
239
240     dvd_contents::pgc_ref webdvd_window::add_menu(const std::string & uri)
241     {
242         dvd_contents::pgc_ref next_menu(dvd_contents::menu_pgc,
243                                         contents_.menus.size());
244         std::pair<resource_map_type::iterator, bool> insert_result(
245             resource_map_.insert(std::make_pair(uri, next_menu)));
246
247         if (!insert_result.second)
248         {
249             return insert_result.first->second;
250         }
251         else
252         {
253             page_queue_.push(uri);
254             contents_.menus.resize(contents_.menus.size() + 1);
255             return next_menu;
256         }
257     }
258
259     dvd_contents::pgc_ref webdvd_window::add_title(const std::string & uri)
260     {
261         dvd_contents::pgc_ref next_title(dvd_contents::title_pgc,
262                                          contents_.titles.size());
263         std::pair<resource_map_type::iterator, bool> insert_result(
264             resource_map_.insert(std::make_pair(uri, next_title)));
265
266         if (!insert_result.second)
267         {
268             return insert_result.first->second;
269         }
270         else
271         {
272             Glib::ustring hostname;
273             std::string filename(Glib::filename_from_uri(uri, hostname));
274             // FIXME: Should check the hostname
275
276             std::string vob_list;
277
278             // Store a reference to a linked VOB file, or the contents
279             // of a linked VOB list file.
280             if (filename.compare(filename.size() - 4, 4, ".vob") == 0)
281             {
282                 if (!Glib::file_test(filename, Glib::FILE_TEST_IS_REGULAR))
283                     throw std::runtime_error(
284                         filename + " is missing or not a regular file");
285                 vob_list
286                     .append("<vob file='")
287                     .append(xml_escape(filename))
288                     .append("'/>\n");
289             }
290             else
291             {
292                 assert(filename.compare(filename.size() - 8, 8, ".voblist")
293                        == 0);
294                 // TODO: Validate the file contents
295                 vob_list.assign(Glib::file_get_contents(filename));
296             }
297
298             contents_.titles.push_back(dvd_contents::title(vob_list));
299             return next_title;
300         }
301     }
302
303     void webdvd_window::load_next_page()
304     {
305         assert(!page_queue_.empty());
306         const std::string & uri = page_queue_.front();
307         std::cout << "loading " << uri << std::endl;
308
309         browser_widget_.load_uri(uri);
310     }
311
312     void webdvd_window::on_net_state_change(const char * uri,
313                                            gint flags, guint status)
314     {
315 #       ifdef DEBUG_ON_NET_STATE_CHANGE
316         std::cout << "webdvd_window::on_net_state_change(";
317         if (uri)
318             std::cout << '"' << uri << '"';
319         else
320             std::cout << "NULL";
321         std::cout << ", ";
322         {
323             gint flags_left = flags;
324             static const struct {
325                 gint value;
326                 const char * name;
327             } flag_names[] = {
328                 { GTK_MOZ_EMBED_FLAG_START, "STATE_START" },
329                 { GTK_MOZ_EMBED_FLAG_REDIRECTING, "STATE_REDIRECTING" },
330                 { GTK_MOZ_EMBED_FLAG_TRANSFERRING, "STATE_TRANSFERRING" },
331                 { GTK_MOZ_EMBED_FLAG_NEGOTIATING, "STATE_NEGOTIATING" },
332                 { GTK_MOZ_EMBED_FLAG_STOP, "STATE_STOP" },
333                 { GTK_MOZ_EMBED_FLAG_IS_REQUEST, "STATE_IS_REQUEST" },
334                 { GTK_MOZ_EMBED_FLAG_IS_DOCUMENT, "STATE_IS_DOCUMENT" },
335                 { GTK_MOZ_EMBED_FLAG_IS_NETWORK, "STATE_IS_NETWORK" },
336                 { GTK_MOZ_EMBED_FLAG_IS_WINDOW, "STATE_IS_WINDOW" }
337             };
338             for (int i = 0; i != sizeof(flag_names)/sizeof(flag_names[0]); ++i)
339             {
340                 if (flags & flag_names[i].value)
341                 {
342                     std::cout << flag_names[i].name;
343                     flags_left -= flag_names[i].value;
344                     if (flags_left)
345                         std::cout << " | ";
346                 }
347             }
348             if (flags_left)
349                 std::cout << "0x" << std::setbase(16) << flags_left;
350         }
351         std::cout << ", " << "0x" << std::setbase(16) << status << ")\n";
352 #       endif // DEBUG_ON_NET_STATE_CHANGE
353
354         if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
355         {
356             if (flags & GTK_MOZ_EMBED_FLAG_START)
357                 ++pending_req_count_;
358
359             if (flags & GTK_MOZ_EMBED_FLAG_STOP)
360             {
361                 assert(pending_req_count_ != 0);
362                 --pending_req_count_;
363             }
364         }
365             
366         if (flags & GTK_MOZ_EMBED_FLAG_IS_DOCUMENT
367             && flags & GTK_MOZ_EMBED_FLAG_START)
368         {
369             pending_window_update_ = true;
370             have_tweaked_page_ = false;
371         }
372
373         if (flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW
374             && flags & GTK_MOZ_EMBED_FLAG_STOP)
375         {
376             // Check whether the load was successful, ignoring this
377             // pseudo-error.
378             if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
379                 check(status);
380
381             pending_window_update_ = false;
382         }
383
384         if (!browser_is_busy())
385         {
386             try
387             {
388                 if (!process_page())
389                     Gtk::Main::quit();
390             }
391             catch (std::exception & e)
392             {
393                 std::cerr << "Fatal error";
394                 if (!page_queue_.empty())
395                     std::cerr << " while processing <" << page_queue_.front()
396                               << ">";
397                 std::cerr << ": " << e.what() << "\n";
398                 Gtk::Main::quit();
399             }
400             catch (Glib::Exception & e)
401             {
402                 std::cerr << "Fatal error";
403                 if (!page_queue_.empty())
404                     std::cerr << " while processing <" << page_queue_.front()
405                               << ">";
406                 std::cerr << ": " << e.what() << "\n";
407                 Gtk::Main::quit();
408             }
409         }
410     }
411
412     bool webdvd_window::process_page()
413     {
414         assert(!page_queue_.empty());
415
416         nsCOMPtr<nsIWebBrowser> browser(browser_widget_.get_browser());
417         nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
418         assert(doc_shell);
419         nsCOMPtr<nsIPresShell> pres_shell;
420         check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
421         nsCOMPtr<nsIPresContext> pres_context;
422         check(doc_shell->GetPresContext(getter_AddRefs(pres_context)));
423         nsCOMPtr<nsIDOMWindow> dom_window;
424         check(browser->GetContentDOMWindow(getter_AddRefs(dom_window)));
425
426         // If we haven't done so already, apply the stylesheet and
427         // disable scrollbars.
428         if (!have_tweaked_page_)
429         {
430             apply_style_sheet(stylesheet_, pres_shell);
431
432             // This actually only needs to be done once.
433             nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
434             check(dom_window->GetScrollbars(getter_AddRefs(dom_bar_prop)));
435             check(dom_bar_prop->SetVisible(false));
436
437             have_tweaked_page_ = true;
438
439             // Might need to wait a while for things to load or more
440             // likely for a re-layout.
441             if (browser_is_busy())
442                 return true;
443         }
444
445         // All further work should only be done if we're not in preview mode.
446         if (!output_dir_.empty())
447         {
448             // If we haven't already started work on this menu, save a
449             // screenshot of its normal appearance.
450             if (!page_state_.get())
451                 save_screenshot();
452
453             // Start or continue processing links.
454             process_links(pres_shell, pres_context, dom_window);
455
456             // If we've finished work on the links, move on to the
457             // next page, if any, or else generate the DVD filesystem.
458             if (!page_state_.get())
459             {
460                 page_queue_.pop();
461                 if (page_queue_.empty())
462                 {
463                     generate_dvd(contents_, output_dir_);
464                     return false;
465                 }
466                 else
467                 {
468                     load_next_page();
469                 }
470             }
471         }
472
473         return true;
474     }
475
476     void webdvd_window::save_screenshot()
477     {
478         Glib::RefPtr<Gdk::Window> window(get_window());
479         assert(window);
480         window->process_updates(true);
481
482         background_temp_.reset(new temp_file("webdvd-back-"));
483         background_temp_->close();
484         std::cout << "saving " << background_temp_->get_name() << std::endl;
485         Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
486                             window->get_colormap(),
487                             0, 0, 0, 0,
488                             frame_params_.width, frame_params_.height)
489             ->save(background_temp_->get_name(), "png");
490     }
491
492     struct webdvd_window::page_state
493     {
494         page_state(nsIDOMDocument * doc, int width, int height)
495                 : diff_pixbuf(Gdk::Pixbuf::create(
496                                   Gdk::COLORSPACE_RGB,
497                                   true, 8, // has_alpha, bits_per_sample
498                                   width, height)),
499                   spumux_temp("webdvd-spumux-"),
500                   links_temp("webdvd-links-"),
501                   link_num(0),
502                   links_it(doc),
503                   link_changing(false)
504             {
505                 spumux_temp.close();
506                 links_temp.close();
507             }
508
509         Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
510
511         temp_file spumux_temp;
512         std::ofstream spumux_file;
513
514         temp_file links_temp;
515
516         unsigned link_num;
517         link_iterator links_it, links_end;
518
519         rectangle link_rect;
520         bool link_changing;
521         Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
522     };
523
524     void webdvd_window::process_links(nsIPresShell * pres_shell,
525                                      nsIPresContext * pres_context,
526                                      nsIDOMWindow * dom_window)
527     {
528         Glib::RefPtr<Gdk::Window> window(get_window());
529         assert(window);
530
531         nsCOMPtr<nsIDOMDocument> basic_doc;
532         check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
533         nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
534         assert(ns_doc);
535         nsCOMPtr<nsIEventStateManager> event_state_man(
536             pres_context->EventStateManager()); // does not AddRef
537         assert(event_state_man);
538         nsCOMPtr<nsIDOMDocumentEvent> event_factory(
539             do_QueryInterface(basic_doc));
540         assert(event_factory);
541         nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
542         assert(doc_view);
543         nsCOMPtr<nsIDOMAbstractView> view;
544         check(doc_view->GetDefaultView(getter_AddRefs(view)));
545
546         // Set up or recover our iteration state.
547         std::auto_ptr<page_state> state(page_state_);
548         if (!state.get())
549         {
550             state.reset(
551                 new page_state(
552                     basic_doc, frame_params_.width, frame_params_.height));
553             
554             state->spumux_file.open(state->spumux_temp.get_name().c_str());
555             state->spumux_file <<
556                 "<subpictures>\n"
557                 "  <stream>\n"
558                 "    <spu force='yes' start='00:00:00.00'\n"
559                 "        highlight='" << state->links_temp.get_name() << "'\n"
560                 "        select='" << state->links_temp.get_name() << "'>\n";
561         }
562
563         rectangle window_rect = {
564             0, 0, frame_params_.width, frame_params_.height
565         };
566
567         unsigned menu_num = resource_map_[page_queue_.front()].index;
568
569         for (/* no initialisation */;
570              state->links_it != state->links_end;
571              ++state->links_it)
572         {
573             nsCOMPtr<nsIDOMNode> node(*state->links_it);
574
575             // Find the link URI and separate any fragment from it.
576             nsCOMPtr<nsILink> link(do_QueryInterface(node));
577             assert(link);
578             nsCOMPtr<nsIURI> uri_iface;
579             check(link->GetHrefURI(getter_AddRefs(uri_iface)));
580             std::string uri_and_fragment, uri, fragment;
581             {
582                 nsCString uri_and_fragment_ns;
583                 check(uri_iface->GetSpec(uri_and_fragment_ns));
584                 uri_and_fragment.assign(uri_and_fragment_ns.BeginReading(),
585                                         uri_and_fragment_ns.EndReading());
586
587                 std::size_t hash_pos = uri_and_fragment.find('#');
588                 uri.assign(uri_and_fragment, 0, hash_pos);
589                 if (hash_pos != std::string::npos)
590                     fragment.assign(uri_and_fragment,
591                                     hash_pos + 1, std::string::npos);
592             }
593
594             // Is this a new link?
595             if (!state->link_changing)
596             {
597                 // Find a rectangle enclosing the link and clip it to the
598                 // window.
599                 nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
600                 assert(elem);
601                 state->link_rect = get_elem_rect(ns_doc, elem);
602                 state->link_rect &= window_rect;
603
604                 if (state->link_rect.empty())
605                 {
606                     std::cerr << "Ignoring invisible link to "
607                               << uri_and_fragment << "\n";
608                     continue;
609                 }
610
611                 ++state->link_num;
612
613                 if (state->link_num >= unsigned(dvd::menu_buttons_max))
614                 {
615                     if (state->link_num == unsigned(dvd::menu_buttons_max))
616                         std::cerr << "No more than " << dvd::menu_buttons_max
617                                   << " buttons can be placed on a menu\n";
618                     std::cerr << "Ignoring link to " << uri_and_fragment
619                               << "\n";
620                     continue;
621                 }
622
623                 state->spumux_file <<
624                     "      <button x0='" << state->link_rect.left << "'"
625                     " y0='" << state->link_rect.top << "'"
626                     " x1='" << state->link_rect.right - 1 << "'"
627                     " y1='" << state->link_rect.bottom - 1 << "'/>\n";
628
629                 // Check whether this is a link to a video or a page then
630                 // add it to the known resources if not already seen; then
631                 // add it to the menu entries.
632                 dvd_contents::pgc_ref target;
633                 // FIXME: This is a bit of a hack.  Perhaps we could decide
634                 // later based on the MIME type determined by Mozilla?
635                 if ((uri.size() > 4
636                      && uri.compare(uri.size() - 4, 4, ".vob") == 0)
637                     || (uri.size() > 8
638                         && uri.compare(uri.size() - 8, 8, ".voblist") == 0))
639                 {
640                     PRBool is_file;
641                     check(uri_iface->SchemeIs("file", &is_file));
642                     if (!is_file)
643                     {
644                         std::cerr << "Links to video must use the file:"
645                                   << " scheme\n";
646                         continue;
647                     }
648                     target = add_title(uri);
649                     target.sub_index =
650                         std::strtoul(fragment.c_str(), NULL, 10);
651                 }
652                 else
653                 {
654                     target = add_menu(uri);
655                     // TODO: If there's a fragment, work out which button
656                     // is closest and set target.sub_index.
657                 }
658                 contents_.menus[menu_num].entries.push_back(target);
659
660                 nsCOMPtr<nsIContent> content(do_QueryInterface(node));
661                 assert(content);
662                 nsCOMPtr<nsIDOMEventTarget> event_target(
663                     do_QueryInterface(node));
664                 assert(event_target);
665
666                 state->norm_pixbuf = Gdk::Pixbuf::create(
667                     Glib::RefPtr<Gdk::Drawable>(window),
668                     window->get_colormap(),
669                     state->link_rect.left,
670                     state->link_rect.top,
671                     0,
672                     0,
673                     state->link_rect.right - state->link_rect.left,
674                     state->link_rect.bottom - state->link_rect.top);
675
676                 nsCOMPtr<nsIDOMEvent> event;
677                 check(event_factory->CreateEvent(
678                           NS_ConvertASCIItoUTF16("MouseEvents"),
679                           getter_AddRefs(event)));
680                 nsCOMPtr<nsIDOMMouseEvent> mouse_event(
681                     do_QueryInterface(event));
682                 assert(mouse_event);
683                 check(mouse_event->InitMouseEvent(
684                           NS_ConvertASCIItoUTF16("mouseover"),
685                           true,  // can bubble
686                           true,  // cancelable
687                           view,
688                           0,     // detail: mouse click count
689                           state->link_rect.left, // screenX
690                           state->link_rect.top,  // screenY
691                           state->link_rect.left, // clientX
692                           state->link_rect.top,  // clientY
693                           false, false, false, false, // qualifiers
694                           0,     // button: left (or primary)
695                           0));   // related target
696                 PRBool dummy;
697                 check(event_target->DispatchEvent(mouse_event,
698                                                   &dummy));
699                 check(event_state_man->SetContentState(content,
700                                                        NS_EVENT_STATE_HOVER));
701
702                 pres_shell->FlushPendingNotifications(true);
703
704                 // We may have to exit and wait for image loading
705                 // to complete, at which point we will be called
706                 // again.
707                 if (browser_is_busy())
708                 {
709                     state->link_changing = true;
710                     page_state_ = state;
711                     return;
712                 }
713             }
714
715             window->process_updates(true);
716
717             Glib::RefPtr<Gdk::Pixbuf> changed_pixbuf(
718                 Gdk::Pixbuf::create(
719                     Glib::RefPtr<Gdk::Drawable>(window),
720                     window->get_colormap(),
721                     state->link_rect.left,
722                     state->link_rect.top,
723                     0,
724                     0,
725                     state->link_rect.right - state->link_rect.left,
726                     state->link_rect.bottom - state->link_rect.top));
727             diff_rgb_pixbufs(
728                 state->norm_pixbuf,
729                 changed_pixbuf,
730                 state->diff_pixbuf,
731                 state->link_rect.left,
732                 state->link_rect.top,
733                 state->link_rect.right - state->link_rect.left,
734                 state->link_rect.bottom - state->link_rect.top);
735         }
736
737         quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
738
739         std::cout << "saving " << state->links_temp.get_name()
740                   << std::endl;
741         state->diff_pixbuf->save(state->links_temp.get_name(), "png");
742
743         state->spumux_file <<
744             "    </spu>\n"
745             "  </stream>\n"
746             "</subpictures>\n";
747
748         state->spumux_file.close();
749
750         // TODO: if (!state->spumux_file) throw ...
751
752         {
753             std::ostringstream command_stream;
754             command_stream << "pngtopnm "
755                            << background_temp_->get_name()
756                            << " | ppmtoy4m -v0 -n1 -F"
757                            << frame_params_.rate_numer
758                            << ":" << frame_params_.rate_denom
759                            << " -A" << frame_params_.pixel_ratio_width
760                            << ":" << frame_params_.pixel_ratio_height
761                            << (" -Ip -S420_mpeg2"
762                                " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
763                                " | mplex -v0 -f8 -o/dev/stdout /dev/stdin"
764                                " | spumux -v0 -mdvd ")
765                            << state->spumux_temp.get_name()
766                            << " > "
767                            << contents_.menus[menu_num].vob_temp->get_name();
768             std::string command(command_stream.str());
769             const char * argv[] = {
770                 "/bin/sh", "-c", command.c_str(), 0
771             };
772             std::cout << "running " << argv[2] << std::endl;
773             int command_result;
774             Glib::spawn_sync(".",
775                              Glib::ArrayHandle<std::string>(
776                                  argv, sizeof(argv)/sizeof(argv[0]),
777                                  Glib::OWNERSHIP_NONE),
778                              Glib::SPAWN_STDOUT_TO_DEV_NULL,
779                              SigC::Slot0<void>(),
780                              0, 0,
781                              &command_result);
782             if (command_result != 0)
783                 throw std::runtime_error("spumux pipeline failed");
784         }
785     }
786
787     const video::frame_params & lookup_frame_params(const char * str)
788     {
789         assert(str);
790         static const struct { const char * str; bool is_ntsc; }
791         known_strings[] = {
792             { "NTSC",  true },
793             { "ntsc",  true },
794             { "PAL",   false },
795             { "pal",   false },
796             // For DVD purposes, SECAM can be treated identically to PAL.
797             { "SECAM", false },
798             { "secam", false }
799         };
800         for (std::size_t i = 0;
801              i != sizeof(known_strings)/sizeof(known_strings[0]);
802              ++i)
803             if (std::strcmp(str, known_strings[i].str) == 0)
804                 return known_strings[i].is_ntsc ?
805                     video::ntsc_params : video::pal_params;
806         throw std::runtime_error(
807             std::string("Invalid video standard: ").append(str));
808     }
809
810     void print_usage(std::ostream & stream, const char * command_name)
811     {
812         stream << "Usage: " << command_name
813                << (" [gtk-options] [--video-std std-name]"
814                    " [--preview] menu-url [output-dir]\n");
815     }
816     
817     void set_browser_preferences()
818     {
819         nsCOMPtr<nsIPrefService> pref_service;
820         static const nsCID pref_service_cid = NS_PREFSERVICE_CID;
821         check(CallGetService<nsIPrefService>(pref_service_cid,
822                                              getter_AddRefs(pref_service)));
823         nsCOMPtr<nsIPrefBranch> pref_branch;
824
825         // Disable IE-compatibility kluge that causes backgrounds to
826         // sometimes/usually be missing from snapshots.  This is only
827         // effective from Mozilla 1.8 onward.
828 #       if MOZ_VERSION_MAJOR > 1                                 \
829            || (MOZ_VERSION_MAJOR == 1 && MOZ_VERSION_MINOR >= 8)
830         check(pref_service->GetDefaultBranch("layout",
831                                              getter_AddRefs(pref_branch)));
832         check(pref_branch->SetBoolPref(
833                   "fire_onload_after_image_background_loads",
834                   true));
835 #       endif
836
837         // Set display resolution.  With standard-definition video we
838         // will be fitting ~600 pixels across a screen typically
839         // ranging from 10 to 25 inches wide, for a resolution of
840         // 24-60 dpi.  I therefore declare the average horizontal
841         // resolution to be 40 dpi.  The vertical resolution will be
842         // slightly higher (PAL/SECAM) or lower (NTSC), but
843         // unfortunately Mozilla doesn't support non-square pixels
844         // (and neither do fontconfig or Xft anyway).
845         check(pref_service->GetDefaultBranch("browser.display",
846                                              getter_AddRefs(pref_branch)));
847         check(pref_branch->SetIntPref("screen_resolution", 40));
848     }
849
850 } // namespace
851
852 int main(int argc, char ** argv)
853 {
854     try
855     {
856         video::frame_params frame_params = video::pal_params;
857         bool preview_mode = false;
858         std::string menu_url;
859         std::string output_dir;
860
861         // Do initial option parsing.  We have to do this before
862         // letting Gtk parse the arguments since we may need to spawn
863         // Xvfb first.
864         int argi = 1;
865         while (argi != argc)
866         {
867             if (std::strcmp(argv[argi], "--") == 0)
868             {
869                 break;
870             }
871             else if (std::strcmp(argv[argi], "--help") == 0)
872             {
873                 print_usage(std::cout, argv[0]);
874                 return EXIT_SUCCESS;
875             }
876             else if (std::strcmp(argv[argi], "--preview") == 0)
877             {
878                 preview_mode = true;
879                 argi += 1;
880             }
881             else if (std::strcmp(argv[argi], "--video-std") == 0)
882             {
883                 if (argi + 1 == argc)
884                 {
885                     std::cerr << "Missing argument to --video-std\n";
886                     print_usage(std::cerr, argv[0]);
887                     return EXIT_FAILURE;
888                 }
889                 frame_params = lookup_frame_params(argv[argi + 1]);
890                 argi += 2;
891             }
892             else
893             {
894                 argi += 1;
895             }
896         }
897
898         std::auto_ptr<x_frame_buffer> fb;
899         if (!preview_mode)
900         {
901             // Spawn Xvfb and set env variables so that Xlib will use it
902             // Use 8 bits each for RGB components, which should translate into
903             // "enough" bits for YUV components.
904             fb.reset(new x_frame_buffer(frame_params.width,
905                                         frame_params.height,
906                                         3 * 8));
907             setenv("XAUTHORITY", fb->get_authority().c_str(), true);
908             setenv("DISPLAY", fb->get_display().c_str(), true);
909         }
910
911         // Initialise Gtk
912         Gtk::Main kit(argc, argv);
913
914         // Complete option parsing with Gtk's options out of the way.
915         argi = 1;
916         while (argi != argc)
917         {
918             if (std::strcmp(argv[argi], "--") == 0)
919             {
920                 argi += 1;
921                 break;
922             }
923             else if (std::strcmp(argv[argi], "--preview") == 0)
924             {
925                 argi += 1;
926             }
927             else if (std::strcmp(argv[argi], "--video-std") == 0)
928             {
929                 argi += 2;
930             }
931             else if (argv[argi][0] == '-')
932             {
933                 std::cerr << "Invalid option: " << argv[argi] << "\n";
934                 print_usage(std::cerr, argv[0]);
935                 return EXIT_FAILURE;
936             }
937             else
938             {
939                 break;
940             }
941         }
942
943         // Look for a starting URL or filename and (except in preview
944         // mode) an output directory after the options.
945         if (argc - argi != (preview_mode ? 1 : 2))
946         {
947             print_usage(std::cerr, argv[0]);
948             return EXIT_FAILURE;
949         }
950         if (std::strstr(argv[argi], "://"))
951         {
952             // It appears to be an absolute URL, so use it as-is.
953             menu_url = argv[argi];
954         }
955         else
956         {
957             // Assume it's a filename.  Resolve it to an absolute URL.
958             std::string path(argv[argi]);
959             if (!Glib::path_is_absolute(path))
960                 path = Glib::build_filename(Glib::get_current_dir(), path);
961             menu_url = Glib::filename_to_uri(path);             
962         }
963         if (!preview_mode)
964             output_dir = argv[argi + 1];
965
966         // Initialise Mozilla
967         browser_widget::initialiser browser_init;
968         set_browser_preferences();
969
970         // Run the browser/converter
971         webdvd_window window(frame_params, menu_url, output_dir);
972         Gtk::Main::run(window);
973     }
974     catch (std::exception & e)
975     {
976         std::cerr << "Fatal error: " << e.what() << "\n";
977         return EXIT_FAILURE;
978     }
979
980     return EXIT_SUCCESS;
981 }